[关闭]
@TryLoveCatch 2016-11-08T06:44:08.000000Z 字数 4188 阅读 1503

JavaScript核心概念之作用域链(Scope Chain)

javascript核心概念


基本概念

VO(variable object)

  一般指全局,是全局执行上下文的一个属性

  1. activeExecutionContext = {
  2. VO: {
  3. // 上下文数据(var, FD, function arguments)
  4. }
  5. };

全局对象(Global object) 是在进入任何执行上下文之前就已经创建了的对象;
这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。
只有全局上下文的变量对象允许通过VO的属性名称来间接访问

  而在全局对象里
  VO === this === global

AO(activation object)

  在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色,AO比VO多了一个arguments变量。

  活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。

执行上下文(execution context 简称-EC)

EC分两种,全局执行上下文和函数执行上下文。全局执行上下文只有一份,函数执行上下文可以有多个。对于每个Execution Context都有三个重要的属性,变量对象(Variable object,VO),作用域链(Scope chain)和this。

执行上下文栈(Execution context stack,ECS)

当一段程序开始时,会先进入全局执行上下文环境[global execution context],用来存储当前上下文中所有已定义或可获取的变量、函数等,这个也是堆栈中最底部的元素,全局上下文取决于执行环境,如Node中的global和Browser中的window

在生成新的上下文时,首先会绑定该上下文的变量对象,其中包括arguments和该函数中定义的变量;之后会创建属于该上下文的作用域链(scope chain),最后将this赋予这一function所属的Object,这一过程可以通过下图表示

  1. var a = "global var";
  2. function foo(){
  3. console.log(a);
  4. }
  5. function outerFunc(){
  6. var b = "var in outerFunc";
  7. console.log(b);
  8. function innerFunc(){
  9. var c = "var in innerFunc";
  10. console.log(c);
  11. foo();
  12. }
  13. innerFunc();
  14. }
  15. outerFunc();

代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,ECS就可以表示为:

  可以看出来,Js本身是单线程的,每当有function被执行时,就会产生一个新的上下文,这一上下文会被压入Js的ECS中,function执行结束后则被弹出,因此Js解释器总是在栈顶上下文中执行。

执行上下文的代码被分成两个基本的阶段来处理:

  1. 进入执行上下文(当函数被调用,但是开始执行函数内部代码之前)
  2. 执行代码
  1. function test(a, b) {
  2. var c = 10;
  3. function d() {}
  4. var e = function _e() {};
  5. (function x() {});
  6. }
  7. test(10); // call

进入执行上下文

这个阶段,VO将会包含如下属性(顺序也是如此):
1. 函数的所有形参
  没有传递对应参数的话,那么由名称和undefined值组成的一种变量对象的属性也将被创建。
2. 所有函数声明
  如果变量对象已经存在相同名称的属性,则完全替换这个属性。
3. 所有变量声明
  如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
这些就是上篇声明提升Hoisting的原因,加强记忆,我们在看一个例子:

  1. (function(){
  2. console.log(foo);
  3. console.log(bar);
  4. console.log(baz);
  5. var foo = function(){};
  6. function bar(){
  7. console.log("bar");
  8. }
  9. var bar = 20;
  10. console.log(bar);
  11. function baz(){
  12. console.log("baz");
  13. }
  14. })()

结果如下:

  1. undefined
  2. [Function: bar]
  3. [Function: baz]
  4. 20

声明提升,代码改造一下:

  1. (function(){
  2. function bar(){
  3. console.log("bar");
  4. }
  5. function baz(){
  6. console.log("baz");
  7. }
  8. var foo;
  9. var bar;
  10. console.log(foo);
  11. console.log(bar);
  12. console.log(baz);
  13. foo = function(){};
  14. bar = 20;
  15. console.log(bar);
  16. })()

开头那个例子,当进入带有参数10的test函数上下文时,AO表现为如下:

  1. AO(test) = {
  2. a: 10,
  3. b: undefined,
  4. d: <reference to FunctionDeclaration "d">
  5. c: undefined,
  6. e: undefined
  7. };

注意,AO里并不包含函数“x”。这是因为“x”是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是函数声明,函数表达式不会影响VO。

  这个阶段,其实可以解释上一篇关于声明提升的原理,变量声明在顺序上跟在函数声明和形式参数声明之后,变量声明不会干扰同名的形参和函数。

执行代码

这个周期内,AO已经拥有了属性,不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值undefined 。

还是前面那个例子, AO在代码执行期间被修改如下:

  1. AO['c'] = 10;
  2. AO['e'] = <reference to FunctionExpression "_e">;

在看一个例子:

  1. if (true) {
  2. var a = 1;
  3. } else {
  4. var b = 2;
  5. }
  6. console.log(a); // 1
  7. console.log(b); // undefined,不是b没有声明,而是b的值是undefined

虽然else部分代码永远不会执行,但是不管怎样,变量“b”仍然存在于VO中。javascript没有块状作用域。

作用域链(scope chain)

作用域链,是在函数调用时创建的,包含该函数的AO和[[scope]],如下:

  1. Scope = AO + [[Scope]]

这个涉及到函数的的生命周期,它分为创建和激活阶段(调用时)。

还是最开始的例子

  1. var x = 10;
  2. function foo() {
  3. var y = 20;
  4. function bar() {
  5. var z = 30;
  6. alert(x + y + z);
  7. }
  8. bar();
  9. }
  10. foo(); // 60

函数创建

  [[scope]]是函数内部的一个属性,与作用域链对比,[[scope]]是函数的一个属性而不是上下文。

  [[scope]]在函数创建时已经存在,静态不变的,永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。

[[scope]]保存所有父VO,举个例子

  1. var x = 10;
  2. function foo() {
  3. var y = 20;
  4. function bar() {
  5. var z = 30;
  6. alert(x + y + z);
  7. }
  8. bar();
  9. }
  10. foo(); // 60

程序开始执行时,创建了全局执行上下文,并压入ECS底部:

  1. globalContext.VO === Global = {
  2. x: 10
  3. foo: function>
  4. };

在foo创建时,foo的[[scope]]属性是:

  1. foo.[[Scope]] = [
  2. globalContext.VO // === Global
  3. ];

内部函数bar创建时,其[[scope]]为:

  1. bar.[[Scope]] = [
  2. fooContext.AO,
  3. globalContext.VO
  4. ];

函数调用

当执行foo()时,进入foo的EC,可参考上面关于EC的两个阶段,总的来说,这个时候很foo的AO对象创建了,如下

  1. fooContext.AO = {
  2. y: 20,
  3. bar: function>
  4. };

这个时候,foo的SC(作用域链)就是

  1. fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
  2. fooContext.Scope = [
  3. fooContext.AO,
  4. globalContext.VO
  5. ];

执行foo的时候,bar也就执行了创建操作

  1. bar.[[Scope]] = [
  2. fooContext.AO,
  3. globalContext.VO
  4. ];

接下来调用了bar(),所以bar的AO对象创建了:

  1. barContext.AO = {
  2. z: 30
  3. };

这个时候,bar的SC(作用域链)就是

  1. barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
  2. barContext.Scope = [
  3. barContext.AO,
  4. fooContext.AO,
  5. globalContext.VO
  6. ];

然后,程序执行到x + y + z,开始在作用域链中查找变量

  1. x
  2. barContext.AO (没有找到)
  3. fooContext.AO (没有找到)
  4. globalContext.VO (找到,返回10)
  5. y
  6. barContext.AO (没有找到)
  7. fooContext.AO (找到,返回20)
  8. globalContext.VO (没有查找)
  9. z
  10. barContext.AO (找到,返回30)
  11. fooContext.AO (没有查找)
  12. globalContext.VO (没有查找)

结论

  Scope = AO + [[Scope]]

1. 全局EC只有一个,永远位于ECS栈底。
2. 函数创建时,作用域[[scope]]就创建了,不会更改
3. 函数执行时,当前函数的EC生成,通过arguments初始化AO并绑定到EC,然后与[[scope]]创建函数的作用域链(SC),之后this赋值。

参考:https://www.kancloud.cn/kancloud/deep-understand-javascript/43689
https://www.kancloud.cn/kancloud/deep-understand-javascript/43691

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注