Scope Chain in JavaScript

in scope •  6 years ago 

[JavaScript] 作用域链 Scope Chain

介绍

变量对象中已经介绍过,执行上下文(变量,函数声明和函数形式参数)的数据被存储为变量对象的属性

此外,我们知道每次进入上下文时都会创建变量对象并填充初始值,并且它的更新发生在代码执行阶段

举个栗子🌰

function test(a, b) {
  console.log(c); // function c() {}
  var c = 10;
  function c() {};
  console.log(c); // 10
  c = 1;
  console.log(c); // 1
  var e = function _e() {};
  (function x() {});
}
test(10);

这次我们讨论作用域链 Scope Chain

定义

如果要简要说明,作用域链主要与内部函数有关

正如我们所知,ECMAScript允许创建内部函数,我们甚至可以从父函数返回这些内部函数

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

众所周知,每个上下文都有自己的变量对象:对于全局上下文,其变量对象就是全局对象本身,对于函数,其变量对象是活动对象

作用域链是内部上下文的所有变量对象的列表,该作用域链用于变量查找,在上面的例子中,“bar”上下文的作用域链包括AO(bar),AO(foo)和VO(global)

让我们从定义开始,进一步讨论更多的例子


作用域链与执行上下文相关联,是一条变量对象的链,用于在处理标识符时的变量查找


函数上下文的作用域链在函数调用时创建,由该函数的活动对象和内部[[Scope]]属性组成

用伪代码可以表示为:

activeExecutionContext = {
    VO: {...}, // or AO
    this: thisValue,
    Scope: [ // Scope chain
      // list of all variable objects
      // for identifiers lookup
    ] 
};

根据定义,Scope可以表示为:

Scope = AO + [[Scope]]

我们可以将Scope和[[Scope]]表示为ECMAScript数组:

var Scope = [VO1, VO2, ..., VOn]; // scope chain

我们下面将讨论AO + [[Scope]]组合以及标识符解析过程,都与函数生命周期有关

函数生命周期

函数的生命周期分为创建阶段和激活(调用)阶段

函数创建

众所周知,函数声明在进入上下文阶段时被放入变量/活动对象(VO / AO)中,让我们看一下全局上下文中的变量和函数声明(其中变量对象是全局对象本身):

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

在函数激活时,我们看到了正确(预期)的结果 => 30

在这里,我们看到“y”变量在函数“foo”中定义(这意味着它在“foo”上下文的AO中),但变量“x”没有在“foo”的上下文中定义,因此不会被添加到“foo”的AO

乍一看,“foo”函数根本不存在“x”变量,正如我们将在下面看到的,“foo”上下文的活动对象只包含一个属性“y”:

fooContext.AO = {
  y: undefined // undefined – on entering the context, 20 – at activation
};

函数“foo”如何访问“x”变量呢?函数应该可以访问更高层上下文的变量对象,实际上,确实如此,这个机制是通过函数的内部[[Scope]]属性来实现的

[[Scope]]是包含了所有父级变量对象的层级链,它位于当前函数上下文中,在函数创建时被保存到函数中
[[Scope]]是在创建函数时保存的,静态的(不变的),只有一次并且一直都存在,直到函数销毁

注意一点,[[Scope]]与Scope(作用域链)是不同的,前者是函数的属性,后者是上下文的属性
以上述例子为例,“foo”函数的[[Scope]]如下所示:

foo.[[Scope]] = [
  globalContext.VO // === Global
];

之后,函数调用时,会进入一个函数上下文,其中活动对象被创建,并且this值和Scope(作用域链)被确定

函数激活

正如定义中提到的那样,在进入上下文并且在创建AO / VO之后,上下文的Scope属性(作用域链,用于变量查找)定义为:

Scope = AO|VO + [[Scope]]

这里要强调活动对象是Scope数组的第一个元素,即添加到作用域链的最前面:

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

这个特征对标识符解析过程非常重要

标识符解析是确定变量(或函数声明)属于作用域链中哪个变量对象的过程

这个算法返回的是一个Reference类型的值,其base属性是相应的变量对象(如果没有找到变量,则为null),其property name属性的名字是查找到的标识符的名称,细节可参考 this

标识符解析的过程包括与变量名称对应的属性查找,即从作用域链的最底层上下文一直到最上层上下文

因此,查找过程中上下文的局部变量比父上下文的变量具有更高的优先级,如果两个相同名字的变量存在于不同的上下文中时,处于底层上下文的变量会优先被找到

让我们看一个稍微复杂的例子:

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

上述代码,对应了如下的变量/活动对象,函数的[[Scope]]属性以及上下文的作用域链:

全局上下文的变量对象是:

globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};

在创建foo时,foo的[[Scope]]属性为:

foo.[[Scope]] = [
  globalContext.VO
];

在foo函数调用中,foo函数上下文的活动对象是:

fooContext.AO = {
  y: 20,
  bar: <reference to function>
};

foo函数上下文的作用域链是:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
 
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

在创建内部“bar”函数时[[Scope]]属性是:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];

在bar函数调用中,bar函数上下文的活动对象是:

barContext.AO = {
  z: 30
};

“bar”函数上下文的作用域链是:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
 
barContext.Scope = [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

“x”,“y”和“z”标识符的查找过程:

- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10
- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20
- "z"
-- barContext.AO // found - 30

作用域的特性

让我们考虑一些与作用域链和函数[[Scope]]属性相关的重要特性

闭包

ECMAScript中的闭包与函数的[[Scope]]属性直接相关,正如前面指出的那样,[[Scope]]在创建函数时保存并存在,直到函数对象被销毁。实际上,闭包恰好是函数代码和其[[Scope]]属性的组合,因此,[[Scope]]包含了函数创建所在的词法环境(父变量对象),上层上下文中的变量,可以在函数激活的时候,通过变量对象的词法链(函数创建时保存)查找到

例子:

var x = 10;
function foo() {
  alert(x);
}
(function () {
  var x = 20;
  foo(); // 10, but not 20
})();

我们看到x变量在foo函数的[[Scope]中被找到,也就是说,变量的查找是在函数创建时定义的词法(闭包)链,而不是调用的动态链(否则x变量将被解析为20)

闭包的另一个经典例子:

function foo() {
  var x = 10;
  var y = 20;
  return function () {
    alert([x, y]);
  };
}
var x = 30;
var bar = foo(); // anonymous function is returned
bar(); // [10, 20]

我们再次看到,对于标识符解析,使用函数创建时定义的词法作用域链,变量x被解析为10,而不是30
此外,这个例子清楚地表明函数的[[Scope]]属性,即使在函数上下文已经结束,也会继续存在

通过Function构造器创建的函数的[[Scope]]属性

在上面的例子中,我们看到函数创建时就获得[[Scope]]属性,并通过此属性访问所有父上下文的变量,但这有一个重要的例外,就是通过Function构造器创建的函数

var x = 10;
function foo() {
  var y = 20;
  function barFD() { // FunctionDeclaration
    alert(x);
    alert(y);
  }
  var barFE = function () { // FunctionExpression
    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构造器创建的barFn函数,变量y不可访问,但它并不意味着barFn函数没有内部的[[Scope]]属性(否则它将无法访问变量x)

问题是通过Function构造器创建的函数的[[Scope]]属性始终只包含全局对象

二维作用域链查找

在作用域链查找中的一个重点是变量对象的原型,因为ECMAScript的原型特性:
如果在对象中没有直接找到属性,则查找会在原型链中进行

  • 在作用域链的链接上
  • 在每个作用域链接上,深入原型链链接
    如果在Object.prototype中定义属性,我们可以观察到这种效果:
function foo() {
  alert(x);
}
Object.prototype.x = 10;
foo(); // 10

活动对象没有原型,我们可以在下面的例子中看到:

function foo() {
  var x = 20;
  function bar() {
    alert(x);
  } 
  bar();
}
Object.prototype.x = 10;
foo(); // 20

如果bar函数上下文的活动对象有一个原型,那么属性x应该在Object.prototype中找到,因为它不存在于AO中
但是在上面的第一个例子中,遍历标识符查找中的作用域链,我们到达全局对象,该对象从Object.prototype继承,因此x被解析为10

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

全局上下文的作用域链中只包含全局对象
“eval”代码的上下文和调用上下文(calling context)有相同的作用域链

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

引用

ECMA-262-3 in detail. Chapter 4. Scope chain.

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!