js深入系列二(执行上下文栈,变量对象,作用域链)

313 阅读5分钟

参考文章1

参考文章2

参考文章3

1. 作用域

作用域是指程序源代码中定义变量的区域

2.

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域,即函数的作用域在函数定义的时候就决定了

3.

与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的

4. 执行上下文:

当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"

5. 执行上下文栈:

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

6.

执行上下文有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this (this写在下一篇深入系列三)

7. 变量对象:

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。变量对象一般包括全局上下文下的变量对象和函数上下文下的变量对象

7.1 全局上下文

全局上下文中的变量对象就是全局对象

7.2 函数上下文

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象

活动对象和变量对象其实是一个东西,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object ,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问

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

7.3 执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,也可以叫做:

  • 进入执行上下文
  • 代码执行

7.3.1 进入执行上下文

当进入执行上下文时,这时候还没有执行代码,变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文):
  • 由名称和对应值组成的一个变量对象的属性被创建
  • 没有实参,属性值设为 undefined
  1. 函数声明:
  • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
  • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  1. 变量声明
  • 由名称和对应值(undefined)组成一个变量对象的属性被创建
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

代码示例:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);

上述代码进入执行上下文过程:

AO = {
    arguments: {
        0: 1, 
        length: 1
    },
    a: 1, // 形参
    b: undefined, // 实参
    c: reference to function c(){},
    d: undefined
}

7.3.2 代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

7.3.3 例子一:

function foo() {
    console.log(a);
    a = 1;
}

foo(); // 第一段会报错:Uncaught ReferenceError: a is not defined

function bar() {
    a = 1;
    console.log(a);
}
bar(); // 第二段会打印:1
// 原因:因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中
// 第一段执行 console 的时候, AO 的值是:
AO = {
    arguments: {
        length: 0
    }
}
没有 a 的值,然后就会到全局去找,全局也没有,所以会报错
当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1

7.3.4 例子二:

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

// 会打印函数,而不是 undefined 
// 这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,
// 如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

8. 作用域链(Scope chain)

8.1 定义

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

9. 总结7,8

函数执行上下文中作用域链和变量对象的创建过程:

// 示例代码
var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下:

  1. checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
// 保存全局作用域到checkscope内
checkscope.[[scope]] = [
    globalContext.VO
];
  1. 创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
];
  1. 开始做准备工作
// 第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    Scope: checkscope.[[scope]],
}

// 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

// 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  1. 准备工作做完,开始顺序执行函数内部语句,修改 AO 的属性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  1. 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
];