这篇文章比较清晰的解释了一些作用域链相关的概念,忍不住收藏了

原文地址:http://segmentfault.com/a/1190000000533094

前言

这几天在看《javascript高级程序设计》,看到执行环境和作用域链的时候,就有些模糊了。书中还是讲的不够具体。
通过上网查资料,特来总结,以备回顾和修正。

要讲的依次为:

  • EC(执行环境或者执行上下文,Execution Context)
  • ECS(执行环境栈Execution Context Stack)
  • VO(变量对象,Variable Object)|AO(活动对象,Active Object)
  • scope chain(作用域链)和[[scope]]属性

EC

每当控制器到达ECMAScript可执行代码的时候,控制器就进入了一个执行上下文(好高大上的概念啊)。

javascript中,EC分为三种:

  • 全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
  • 函数级别的代码 – 当执行一个函数时,运行函数体中的代码。
  • Eval的代码 – 在Eval函数内运行的代码。

EC建立分为两个阶段:进入执行上下文和执行阶段。

1.进入上下文阶段:发生在函数调用时,但是在执行具体代码之前(比如,对函数参数进行具体化之前)
2.执行代码阶段:变量赋值,函数引用,执行其他代码。

我们可以将EC看做是一个对象。

EC={
VO:{/* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */},
this:{},
Scope:{ /* VO以及所有父执行上下文中的VO */}
}

ECS

一系列活动的执行上下文从逻辑上形成一个栈。栈底总是全局上下文,栈顶是当前(活动的)执行上下文。当在不同的执行上下文间切换(退出的而进入新的执行上下文)的时候,栈会被修改(通过压栈或者退栈的形式)。

压栈:全局EC-->局部EC1-->局部EC2-->当前EC
出栈:全局EC<--局部EC1<--局部EC2<--当前EC

我们可以用数组的形式来表示环境栈:

ECS=[局部EC,全局EC];

每次控制器进入一个函数(哪怕该函数被递归调用或者作为构造器),都会发生压栈的操作。过程类似javascript数组的push和pop操作。

当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。

VO|AO

VO

每一个EC都对应一个变量对象VO,在该EC中定义的所有变量和函数都存放在其对应的VO中。

VO分为全局上下文VO(全局对象,Global object,我们通常说的global对象)和函数上下文的AO。

VO: {
// 上下文中的数据 (变量声明(var), 函数声明(FD), 函数形参(function arguments))
}

1.进入执行上下文时,VO的初始化过程具体如下:

  • 函数的形参(当进入函数执行上下文时)
    —— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined

  • 函数声明(FunctionDeclaration, FD) —— 变量对象的一个属性,其属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则替换它的值

  • 变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。

注意:该过程是有先后顺序的。

2.执行代码阶段时,VO中的一些属性undefined值将会确定。

AO

在函数的执行上下文中,VO是不能直接访问的。它主要扮演被称作活跃对象(activation object)(简称:AO)的角色。

这句话怎么理解呢,就是当EC环境为函数时,我们访问的是AO,而不是VO。

VO(functionContext) === AO;

AO是在进入函数的执行上下文时创建的,并为该对象初始化一个arguments属性,该属性的值为Arguments对象。

AO = {
arguments: {
callee:,
length:,
properties-indexes: //函数传参参数值
}
};

FD的形式只能是如下这样:

function f(){

}

示例

VO示例

alert(x); // function

var x = 10;
alert(x); // 10 x = 20; function x() {}; alert(x); // 20

进入执行上下文时,

ECObject={
VO:{
x:<reference to FunctionDeclaration "x">
}
};

执行代码时:

ECObject={
VO:{
x:20 //与函数x同名,替换掉,先是10,后变成20
}
};

对于以上的过程,我们详细解释下。

在进入上下文的时候,VO会被填充函数声明; 同一阶段,还有变量声明“x”,但是,正如此前提到的,变量声明是在函数声明和函数形参之后,并且,变量声明不会对已经存在的同样名字的函数声明和函数形参发生冲突。因此,在进入上下文的阶段,VO填充为如下形式:

VO = {};

VO['x'] = <引用了函数声明'x'>

// 发现var x = 10;
// 如果函数“x”还未定义
// 则 "x" 为undefined, 但是,在我们的例子中
// 变量声明并不会影响同名的函数值 VO['x'] = <值不受影响,仍是函数>

执行代码阶段,VO被修改如下:

VO['x'] = 10;
VO['x'] = 20;

如下例子再次看到在进入上下文阶段,变量存储在VO中(因此,尽管else的代码块永远都不会执行到,而“b”却仍然在VO中)

if (true) {
var a = 1;
} else {
var b = 2;
} alert(a); // 1
alert(b); // undefined, but not "b is not defined"

AO示例

function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
} test(10); // call

当进入test(10)的执行上下文时,它的AO为:

testEC={
AO:{
arguments:{
callee:test
length:1,
0:10
},
a:10,
c:undefined,
d:<reference to FunctionDeclaration "d">,
e:undefined
}
};

由此可见,在建立阶段,VO除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。函数表达式不会对VO造成影响,因此,(function x() {})并不会存在于VO中。

当执行test(10)时,它的AO为:

testEC={
AO:{
arguments:{
callee:test,
length:1,
0:10
},
a:10,
c:10,
d:<reference to FunctionDeclaration "d">,
e:<reference to FunctionDeclaration "e">
}
};

可见,只有在这个阶段,变量属性才会被赋具体的值。

作用域链

在执行上下文的作用域中查找变量的过程被称为标识符解析(indentifier resolution),这个过程的实现依赖于函数内部另一个同执行上下文相关联的对象——作用域链。作用域链是一个有序链表,其包含着用以告诉JavaScript解析器一个标识符到底关联着哪一个变量的对象。而每一个执行上下文都有其自己的作用域链Scope。

一句话:作用域链Scope其实就是对执行上下文EC中的变量对象VO|AO有序访问的链表。能按顺序访问到VO|AO,就能访问到其中存放的变量和函数的定义。

Scope定义如下:

Scope = AO|VO + [[Scope]]

其中,AO始终在Scope的最前端,不然为啥叫活跃对象呢。即:

Scope = [AO].concat([[Scope]]);

这说明了,作用域链是在函数创建时就已经有了。

那么[[Scope]]是什么呢?

[[Scope]]是一个包含了所有上层变量对象的分层链,它属于当前函数上下文,并在函数创建的时候,保存在函数中。

[[Scope]]是在函数创建的时候保存起来的——静态的(不变的),只有一次并且一直都存在——直到函数销毁。 比方说,哪怕函数永远都不能被调用到,[[Scope]]属性也已经保存在函数对象上了。

var x=10;
function f1(){
var y=20;
function f2(){
return x+y;
}
}

以上示例中,f2的[[scope]]属性可以表示如下:

f2.[[scope]]=[
f2OuterContext.VO
]

f2的外部EC的所有上层变量对象包括了f1的活跃对象f1Context.AO,再往外层的EC,就是global对象了。
所以,具体我们可以表示如下:

f2.[[scope]]=[
f1Context.AO,
globalContext.VO
]

对于EC执行环境是函数来说,那么它的Scope表示为:

functionContext.Scope=functionContext.AO+function.[[scope]]

注意,以上代码的表示,也体现了[[scope]]和Scope的差异,Scope是EC的属性,而[[scope]]则是函数的静态属性。

(由于AO|VO在进入执行上下文和执行代码阶段不同,所以,这里及以后Scope的表示,我们都默认为是执行代码阶段的Scope,而对于静态属性[[scope]]而言,则是在函数声明时就创建了)

对于以上的代码EC,我们可以给出其Scope的表示:

exampelEC={
Scope:[
f2Context.AO+f2.[[scope]],
f1.context.AO+f1.[[scope]],
globalContext.VO
]
}

接下来,我们给出以上其它值的表示:

  • globalContext.VO
globalContext.VO={
x:10,
f1:<reference to FunctionDeclaration "f1">
}
  • f2Context.AO
f2Context.AO={
f1Context.AO={
arguments:{
callee:f1,
length:0
},
y:20,
f2:<reference to FunctionDeclaration "f2">
}
}
  • f2.[[scope]]
f2Context.AO={
f1Context.AO:{
arguments:{
callee:f1,
length:0
},
y:20,
f2:<reference to FunctionDeclaration "f2">
},
globalContext.VO:{
x:10,
f1:<reference to FunctionDeclaration "f1">
}
}
  • f1Context.AO
f1Context.AO={
arguments:{
callee:f1,
length:0
},
y:20,
f2:<reference to FunctionDeclaration "f2">
}
f1.[[scope]]={
globalContext.VO:{
x:undefined,
f1:undefined
}
}

好,我们知道,作用域链Scope呢,是用来有序访问VO|AO中的变量和函数,对于上面的示例,我们给出访问的过程:

  • x,f1
- "x"
-- f2Context.AO // not found
-- f1Context.AO // not found
-- globalContext.VO // found - 10

f1的访问过程类似。

  • y
- "y"
-- f2Context.AO // not found
-- f1Context.AO // found -20

我们发现,在变量和函数的访问过程,并没有涉及到[[scope]],那么[[scope]]存在的意义是什么呢?

这个还是看下一篇文章吧。

总结

  1. EC分为两个阶段,进入执行上下文和执行代码。
  2. ECStack管理EC的压栈和出栈。
  3. 每个EC对应一个作用域链Scope,VO|AO(AO,VO只能有一个),this。
  4. 函数EC中的Scope在进入函数EC时创建,用来有序访问该EC对象AO中的变量和函数。
  5. 函数EC中的AO在进入函数EC时,确定了Arguments对象的属性;在执行函数EC时,其它变量属性具体化。
  6. 函数的[[scope]]属性在函数创建时就已经确定,并保持不变。

【转】javascript 执行环境,变量对象,作用域链的更多相关文章

  1. JavaScript 执行环境(执行上下文) 变量对象 作用域链 上下文 块级作用域 私有变量和特权方法

    总结自<高程三>第四章  理解Javascript_12_执行模型浅析   JS的执行环境与作用域  javascript高级程序第三版学习笔记[执行环境.作用域] 在javascript ...

  2. javascript 执行环境,变量对象,作用域链

    前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体. 通过上网查资料,特来总结,以备回顾和修正. 要讲的依次为: EC( ...

  3. JavaScript闭包理解的关键 - 作用域链

    阮一峰的一篇文章已经对闭包的用途.概念讲解地相对清晰了. 闭包就是能够读取其他函数内部变量的函数. 但我认为里面对于作用域链的解释还不够清晰,这里作一些补充. 闭包之所以可以读取外部函数的内部变量,即 ...

  4. JavaScript——执行环境、变量对象、作用域链

    前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体.通过上网查资料,特来总结,以备回顾和修正. 目录: EC(执行环境或 ...

  5. javascript 执行环境,作用域、作用域链、闭包

    1.执行环境 执行环境是JavaScript中国最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为.每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数 ...

  6. 深度剖析Javascript执行环境、作用域链

    一.执行环境 执行环境(也叫做执行上下文,Execution Context)是Javascript中最为重要的一个概念.执行环境定义了变量或函数有权访问其他数据,决定了它们各自的行为.每个执行环境都 ...

  7. JavaScript 执行环境以及作用域链

    执行环境(execution context,为简单起见,有时也称为"环境")是 JavaScript 中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们 ...

  8. JavaScript 执行环境、作用域、内存管理及垃圾回收机制

    前言 JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存. [原理]找出那些不再继续使用的变量,然后释放其占用的内存.为此,垃圾收集器会按照固定的时间间隔( ...

  9. 理解JavaScript执行环境与作用域

    执行环境定义了变量或函数有权访问的其他数据,决定了它们的各自行为.每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中,虽然我们无法访问这个对象,但是解析器在处理数据时 ...

随机推荐

  1. ubuntu 下使用etcd

    1.安装ETCD_VER=v3.1.0 DOWNLOAD_URL=https://github.com/coreos/etcd/releases/download curl -L ${DOWNLOAD ...

  2. 每天一个linux命令(权限):【转载】chmod命令

    chmod命令用于改变linux系统文件或目录的访问权限.用它控制文件或目录的访问权限.该命令有两种用法.一种是包含字母和操作符表达式的文字设定法:另一种是包含数字的数字设定法. Linux系统中的每 ...

  3. Codeforces 17E Palisection 【Manacher】

    Codeforces 17E Palisection E. Palisection In an English class Nick had nothing to do at all, and rem ...

  4. 【angularJS】Route路由

    介绍 AngularJS 路由允许我们通过不同的 URL 访问不同的内容. 通过 AngularJS 可以实现多视图的单页Web应用(single page web application,SPA). ...

  5. Mybatis逆向工程配置文件详细介绍(转)

    <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration ...

  6. bzoj 5120 [2017国家集训队测试]无限之环——网络流

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=5120 旋转的话相当于去掉一个插头.新增一个插头,所以在这两个插头之间连边并带上费用即可. 网 ...

  7. java代码----------实现创建DataInputStream和DataOutputStream进行读写

    总结: 主要是 捕获异常 package com.a.b; import java.io.*; public class testData { public static void main(Stri ...

  8. 微服务监控之三:Prometheus + Grafana Spring Boot 应用可视化监控

    一.Springboot增加Prometheus 1.Spring Boot 应用暴露监控指标,添加如下依赖 <dependency> <groupId>org.springf ...

  9. 如果先装framework,后装IIS,想使用ASP.NET

    32位系统在Dos下运行以下命令C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_regiis.exe -i 开始安装 ASP.NET (2.0 ...

  10. mysql内置函数,在mysql里面利用str_to_date()把字符串转换为日期格式

    mysql内置函数,在mysql里面利用str_to_date()把字符串转换为日期格式 示例:分隔符一致,年月日要用%号 select str_to_date('2008-4-2 15:3:28', ...