在第二章变量对象的时候,已经介绍过执行上下文的数据是以变量对象的属性的形式进行存储的。算法
还介绍了,每次进入执行上下文的时候,就会建立变量对象,而且赋予其属性初始值,随后在执行代码阶段会对属性值进行更新。数组
本文要与执行上下文密切相关的另一个重要的概念——做用域链(Scope Chain)。bash
众所周知,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)。ecmascript
下面就来详细介绍下做用域链。ide
先从定义开始,随后再结合例子详细介绍:函数
做用域链是一条变量对象的链,它和执行上下文有关,用于在处理标识符时候进行变量查询。
复制代码
函数上下文的做用域链在函数调用的时候建立出来,它包含了活跃对象和该函数的内部[[Scope]]属性。关于[[Scope]]会在后面做详细介绍。post
大体表示以下:ui
activeExecutionContext = {
VO: {...}, // 或者 AO
this: thisValue,
Scope: [ // 所用域链
// 全部变量对象的列表
// 用于标识符查询
]
};
复制代码
上述代码中的Scope定义为以下所示:
Scope = AO + [[Scope]]
复制代码
针对咱们的例子来讲,能够将Scope和[[Scope]]用普通的ECMAScript数组来表示:
var Scope = [VO1, VO2, ..., VOn]; // 做用域链
复制代码
除此以外,还能够用分层对象链的数据结构来表示,链中每个连接都有对父做用域(上层变量对象)的引用。这种表示方式和第二章中讨论的某些实现中__parent__的概念相对应:
var VO1 = {__parent__: null, ... other data}; -->
var VO2 = {__parent__: VO1, ... other data}; -->
// etc.
复制代码
然而,使用数组来表示做用域链会更方便,所以,咱们这里就采用数组的表示方式。 除此以外,不论在实现层是否采用包含__parent__特性的分层对象链的数据结构,标准自身对其作了抽象的定义“做用域链是一个对象列表”。 数组就是实现列表这一律念最好的选择。
下面将要介绍的 AO+[[Scope]]以及标识符的处理方式,都和函数的生命周期有关。
var x = 10;
function foo() {
var y = 20;
alert(x + y);
}
foo(); // 30
复制代码
在函数激活后,咱们看到了正确(预期)的结果——30。不过,这里有一个很是重要的特性。
在说当前上下文的变量对象前。上述代码中咱们看到变量“y”是在“foo”函数中定义的(意味着它存储在“foo”上下文的AO对象中), 然而变量“x”则并无在“foo”上下文中定义,天然也不会添加到“foo”的AO中。乍一眼看过去,变量“x”压根就不在“foo”中存在; 然而,正如咱们下面要看到的——仅仅只是“乍一眼看过去“而已。咱们看到“foo”上下文的活跃对象中只包含一个属性——“y”:
fooContext.AO = {
y: undefined // undefined – 在进入上下文时, 20 – 在激活阶段
};
复制代码
那么,“foo”函数究竟是如何访问到变量“x”的呢?一个顺其天然的想法是:函数应当有访问更高层上下文变量对象的权限。 而事实也恰是如此,就是经过函数的内部属性[[Scope]]来实现这一机制的。
[[Scope]]是一个包含了全部上层变量对象的分层链,它属于当前函数上下文,并在函数建立的时候,保存在函数中。
这里要注意的很重要的一点是:[[Scope]]是在函数建立的时候保存起来的——静态的(不变的),只有一次而且一直都存在——直到函数销毁。 比方说,哪怕函数永远都不能被调用到,[[Scope]]属性也已经保存在函数对象上了。
另外要注意的一点是: [[Scope]]与Scope(做用域链)是不一样的,前者是函数的属性,后者是上下文的属性。 以上述例子来讲,“foo”函数的[[Scope]]以下所示:
foo.[[Scope]] = [
globalContext.VO // === Global
];
复制代码
以后,有了函数调用,就会进入函数上下文,这个时候会建立活跃对象而且this的值和Scope(做用域链)都会肯定。下面来详细介绍下。
Scope = AO|VO + [[Scope]]
复制代码
这里要注意的是活跃对象是Scope数组的第一个元素。添加在做用域链的最前面:
Scope = [AO].concat([[Scope]]);
复制代码
此特性对处理标识符很是重要。
处理标识符其实就是一个肯定变量(或者函数声明)属于做用域链中哪一个变量对象的过程。
复制代码
此算法返回的老是一个引用类型的值,其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:
};
复制代码
在“foo”函数建立的时候,其[[Scope]]属性以下所示:
foo.[[Scope]] = [
globalContext.VO
];
复制代码
在“foo”函数激活的时候(进入上下文时),“foo”函数上下文的活跃对象以下所示:
fooContext.AO = {
y: 20,
bar:
};
复制代码
同时,“foo”函数上下文的做用域链以下所示:
fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];
复制代码
在内部“bar”函数建立的时候,其[[Scope]]属性以下所示:
bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];
复制代码
在“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
复制代码
以下例子所示:
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。 而且,上述例子清楚的展现了函数(上述例子中指的是函数“foo”返回的匿名函数)的[[Scope]]属性,即便在建立该函数的上下文结束的时候依然存在。
更多关于ECMAScript对闭包的实现细节会在第六章-闭包中作介绍。
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();
复制代码
上述例子中,函数“barFn”就是经过Function构造器来建立的,这个时候变量“y”就没法访问到了。 但这并不意味着函数“barFn”就没有内部的[[Scope]]属性了(不然它连变量“x”都没法访问到了)。 问题就在于当函数经过Function构造器来建立的时候,其[[Scope]]属性永远都只包含全局对象。 哪怕在上层上下文中(非全局上下文)建立一个闭包都是无济于事的。
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。
一样的状况,在某些版本的SpiderMonkey中,经过命名函数表达式(简称:NFE)也会发生,其中的存储了可选的函数表达式的名字的特殊对象也继承自Object.prototype, 一样的,在某些版本的Blackberry中,也是如此,其活跃对象是继承自Object.prototype的。不过,关于这块详细的特性将会在第五章-函数中做介绍。
globalContext.Scope = [
Global
];
evalContext.Scope === callingContext.Scope;
复制代码
Scope = withObject|catchObject + AO|VO + [[Scope]]
复制代码
以下例子中,with语句添加了foo对象,使得它的属性能够不须要前缀直接访问。
var foo = {x: 10, y: 20};
with (foo) {
alert(x); // 10
alert(y); // 20
}
复制代码
对应的做用域链修改成以下所示:
Scope = foo + AO|VO + [[Scope]]
复制代码
接着来看下面这个例子:
var x = 10, y = 10;
with ({x: 20}) {
var x = 30, y = 30;
alert(x); // 30
alert(y); // 30
}
alert(x); // 10
alert(y); // 30
复制代码
发生了什么?怎么最外层的“y”变成了30? 在进入上下文的时候,“x”和“y”标识符已经添加到了变量对象。以后,到了执行代码阶段,发生了以下的改动:
一样的,catch从句(能够访问参数异常)会建立一个只包含一个属性(异常参数名)的新对象。以下所示:
try {
...
} catch (ex) {
alert(ex);
}
复制代码
做用域链修改成以下所示:
var catchObject = {
ex:
};
Scope = catchObject + AO|VO + [[Scope]]
复制代码
在catch从句结束后,做用域链一样也会恢复到原先的状态。
本文,介绍了几乎全部与执行上下文相关的概念以及相应的细节。后面的章节中,会给你们介绍函数对象的细节:函数的类型(FunctionDeclaration,FunctionExpression)和闭包。 顺便提下,本文中介绍过,闭包是和[[Scope]]有直接的关系,可是关于闭包的细节会在后续章节中做介绍。
重学JavaScript深刻理解系列(一)
重学JavaScript深刻理解系列(二)
重学JavaScript深刻理解系列(三)
重学JavaScript深刻理解系列(五)
重学JavaScript深刻理解系列(六)