读《JavaScript高级程序设计》第4、7章有感。
一、基本概念
1.什么是执行环境?(execution context)
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
个人感悟:
所谓执行环境,说的是某一段特殊的代码(比如函数),它在这个“执行环境”下执行,通过这个执行环境的限制,决定了这段代码(此处是函数)允许访问的数据是什么。
2.什么是变量对象?(variable object)
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。(虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它)
个人感悟:
我觉得吧,执行环境是一个很抽象的概念,而在JS代码实现中,需要具体化这个抽象概念,于是,就用一个与执行环境关联的对象,用于表示这个环境的具象存在。
3.什么是作用域链?(scope chain)
当代码在一个环境中执行时,会创建由变量对象构成的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
个人感悟:
前面说道,JavaScript通过执行环境的限制,决定变量或函数允许访问的数据。那么,现在就通过一条链表存放所有可以访问的变量对象(变量对象中存放可以访问的数据),以便我们有序地访问这些数据。(为什么要这样搞,下文会说)
另外,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
4.什么是活动对象?(activation object)
如果这个环境是函数,则将其活动对象作为变量对象。函数的活动对象在最开始时只包含一个变量,即arguments对象。
个人感悟:
按我目前狭隘地知识储备,我所知道的就是凡是函数均会将自己的活动对象作为变量对象,然后插入作用域链的最前端。
二、执行流程
1.关键性知识
先说说一些关键性的东西,再讲流程。
(1)首先,全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
(2)其次,某个执行环境中的所有代码执行完毕后,该环境就会被销毁,保存在其中的所有变量和函数定义也随之销毁。像函数这样的局部环境的变量对象只在函数执行的过程中存在,而全局执行环境直到应用程序退出时才会被销毁。
(3)然后,每个函数在第一次被调用时都会创建自己的执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[[Scope]],然后,使用this、arguments和其他命名参数的值来初始化函数的活动对象。(函数均会将自己的活动对象作为变量对象,用以表示自己的执行环境)
(4)最后,关于上一点还有一些关于创建作用域链的细节。以函数为例,在创建函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行 环境作用域链的前端。
2.执行过程
有上面的铺垫,下面开始讲流程。
(1)开始执行代码。 // 初始环境是在全局执行环境中
(2)构建一条作用域链,链接到全局对象中 // 为window对象创建作用域链,此时链中元素只有一个对象,这就是全局执行对象
执行流进入一个函数时:
{
(3)创建函数自己的执行环境。 // 即使用this、arguments和其他命名参数的值来初始化自己的活动对象
// 并将活动对象作为变量对象,用以表示自己的执行环境
(4)构建一条作用域链,将链头指针赋值到函数的一个特殊的内部属性[[Scope]]中。
// 作用域链的前端,始终都是当前执行的代码所在环境的变量对象
// 活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)
// 作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。
这样,一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象
(5)将函数的环境推入一个环境栈中。
(6)执行函数代码。
// 如果中途需要搜索标识符,则此时就是沿着作用域链一级一级地搜索,这就是作用域链的用途
(7)函数执行完毕,栈将其环境弹出,把控制权返回会之前的执行环境。
// 也就是作用域链中函数执行环境的上一个执行环境
(8)销毁该函数的环境。
//即销毁该函数的变量对象,保存在其中的所有变量和函数定义也随之销毁
}
(9)执行其他代码,退出程序时销毁全局执行环境。
好吧我承认我写得太抽象了,来个例子理解一下吧:
var color = "blue"; function changeColor(){ var anotherColor = "red"; function swapColors(){ var tempColor = anotherColor; anotherColor = color; color = tempColor; // 这里可以访问color、anotherColor和tempColor
} // 这里可以访问color和anotherColor,但不能访问tempColor swapColors();
} changeColor(); // 这里不能访问anotherColor和tempColor,但可以访问color alert("Color is now " + color);
下图是以swapColors函数为例的示意图:(changeColor函数的作用域链构造与此类似)
上图说的就是,当执行流进入swapColors函数的时候,在函数内部维护着这么一条指针链表,也即之前说的:作用域链。
通过这条作用域链,函数在搜索变量或函数的时候,有了一条有序而准确的路径。当函数需要读写某一变量的时候,首先从链表最前端即swapColors Scope(变量对象)中寻找是否有该变量,若有,则搜索停止,不再向上查找,若无,则沿着作用域链向上查找。
通过上图我们可以看出:
这些环境之间的联系是线性的、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
三、总结
作用域链的作用体现在两个很重要的地方。第一个是标识符的解析,第二个就是闭包。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止,找不到则报错。
而闭包,同样需要利用到这个作用域链。关于闭包,下一篇文章再细讲哈。
专心看一大篇东西不容易,谢一个~
共勉。