1 定义

我们已经知道一个执行上下文中的数据(参数,变量,函数)作为属性存储在变量对象中。

也知道变量对象是在每次进入上下文是创建并填入初始值,值的更新出现在代码执行阶段。

作用域链就是这些变量对象的链表。

让我们看一下和作用域相关的上下文结构
VO是当前上下文的变量对象,重点是Scope属性,Scope = VO+[[scope]]。其中[[scope]]为所有父上下文变量对象的链表。

activeExecutionContext = {
    VO: {...}, // or AO
    this: thisValue,
    Scope: [ // Scope chain
      // 所有变量对象的列表
      // for identifiers lookup
    ]
};

函数的生命周期分为创建和激活。

2 函数创建阶段

var x = 10;
  
function foo() {
  var y = 20;
  alert(x + y);
}
  
foo(); // 30

此前,我们仅仅谈到有关当前上下文的变量对象。这里,我们看到变量“y”在函数“foo”中定义(意味着它在foo上下文的AO中),但是变量“x”并未在“foo”上下文中定义,相应地,它也不会添加到“foo”的AO中。乍一看,变量“x”相对于函数“foo”根本就不存在;但正如我们在下面看到的——也仅仅是“一瞥”,我们发现,“foo”上下文的活动对象中仅包含一个属性--“y”。

fooContext.AO = {
y: undefined // undefined – 进入上下文的时候是20 – at activation
};

函数“foo”如何访问到变量“x”?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。

[[scope]]是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。

注意这重要的一点--[[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。

3 函数激活阶段

正如在定义中说到的,进入上下文创建AO/VO之后,上下文的Scope属性(变量查找的一个作用域链)作如下定义:

Scope = AO|VO + [[Scope]]

上面代码的意思是:活动对象是作用域数组的第一个对象,即添加到作用域的前端。
Scope = [AO].concat([[Scope]]);

这个特点对于标示符解析的处理来说很重要。

标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。
标识符解析过程包含与变量名对应属性的查找,即作用域中变量对象的连续查找,从最深的上下文开始,绕过作用域链直到最上层。

这样一来,在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不同的作用域,那么第一个被发现的是在最深作用域中。

4 闭包

在ECMAScript中,闭包与函数的[[scope]]直接相关,正如我们提到的那样,[[scope]]在函数创建时被存储,与函数共存亡。实际上,闭包是函数代码和其[[scope]]的结合。

因为闭包函数在创建的时候就创建了父级的变量对象链表,也就是父级作用域链, 然后闭包函数再访问父级作用域链中的变量,导致父级函数执行完毕后仍然不能释放执行上下文的情况。

5 通过构造函数创建的函数的[[scope]]

在上面的例子中,我们看到,在函数创建时获得函数的[[scope]]属性,通过该属性访问到所有父上下文的变量。但是,这个规则有一个重要的例外,它涉及到通过函数构造函数创建的函数。

var x = 10;
  
function foo() {
  
 var y = 20;
  
 function barFD() { // 函数声明
alert(x);
alert(y);
}
  
 var barFE = function () { // 函数表达式
alert(x);
alert(y);
};
  
 var barFn = Function('alert(x); alert(y);');
  
barFD(); // 10, 20
barFE(); // 10, 20
barFn(); // 10, "y" is not defined
  
}
  
foo();

我们看到,通过函数构造函数(Function constructor)创建的函数“bar”,是不能访问变量“y”的。但这并不意味着函数“barFn”没有[[scope]]属性(否则它不能访问到变量“x”)。问题在于通过函构造函数创建的函数的[[scope]]属性总是唯一的全局对象。考虑到这一点,如通过这种函数创建除全局之外的最上层的上下文闭包是不可能的。

6 全局和eval上下文中的作用域链

这里不一定很有趣,但必须要提示一下。全局上下文的作用域链仅包含全局对象。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链。

globalContext.Scope = [
Global
];
  
evalContext.Scope === callingContext.Scope;

7 代码执行时对作用域链的影响

在ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是with声明和catch语句。它们添加到作用域链的最前端,对象须在这些声明中出现的标识符中查找。如果发生其中的一个,作用域链简要的作如下修改:

Scope = withObject|catchObject + AO|VO + [[Scope]]

eval代码运行的字符串利用当前调用的上下文,并且能够修改当前上下文中的变量对象,也就是说eval内和eval外的代码一样,只是不能变量提升,执行起来是一样的。
with代码块和catch代码块都改变了作用域链,但是在他们代码块中声明的变量,也存在了函数作用域中,只是with的对象和catch对象外部不能访问而已。

eval('var x=3');
console.log(x); //3      
with (obj1) {
    var innner ='inner';
    console.log(a); //1
    console.log(b); //2
}
console.log(innner); // inner 仍能访问
console.log(a) //访问不到
try {
    throw new Error('error')
}
catch (e) {
    var cat = 2;
}
console.log(cat); //2 仍能访问

英文原文:http://dmitrysoshnikov.com/ec...
本文绝大部分内容来自上述地址,仅做少许修改

如果觉得有收获请关注微信公众号 前端良文 每周都会分享前端开发中的干货知识点。


传播正能量
238 声望30 粉丝