深入js之执行上下文中的变量对象

399 阅读5分钟

我们知道当js执行代码时会创造执行上下文(执行环境),今天我们聊聊执行上下文中变量对象的细节 JavaScript有三种类型的执行上下文:全局执行上下文、函数执行上下文和Eval函数上下文(eval很少涉及这里不做讨论)

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

我们本篇重点聊聊变量对象:变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。

(1)我们先来介绍全局上下文

1.全局对象是由 Object 构造函数实例化的一个对象。

2.作为全局变量的宿主。

var a = 1; console.log(this.a);

在浏览器环境中,JavaScript 的全局对象是 window。当你声明一个全局变量,比如 var a = 1;,这个变量实际上成为了 window 对象的一个属性。因此,console.log(window.a); 会输出 1,因为你正在访问全局变量 a,它等同于 window.a

接下来,你使用 this.window.b = 2; 这样的语法。在全局作用域中,this 通常指向 window 对象(除非在严格模式下,this 的值是 undefined)。因此,this.window.b = 2; 实际上等同于 window.window.b = 2;。由于 window 对象自身引用了自己,window.window 还是 window,所以这行代码最终的效果是给 window 对象添加了一个名为 b 的属性,并赋值为 2

因此,console.log(this.b); 在全局作用域中执行时,会输出 2。这里的 this 指向 window,所以 this.b 就是 window.b

其实最重要的记住这句话---全局上下文中的变量对象就是全局对象

(2)聊聊函数上下文

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

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

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

这个对象的作用就是函数上下文的变量对象初始化只包括 Arguments 对象,arguments对象是一个类数组对象,它包含了函数调用时传入的所有参数。这个对象的主要用途是允许函数内部访问其被调用时传递的参数,即使函数定义时没有明确指定这些参数。arguments对象的每个元素对应一个传递给函数的参数,其索引从0开始。此外,arguments对象还有一个length属性,表示传递给函数的参数个数。

我们来看看这两种上下文的执行过程

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

  1. 进入执行上下文
  2. 代码执行

当引擎进入上下文但没有执行的时候变量对象会包括 变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

eg:

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

  b = 3;

}

foo(1);

进入后AO:

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

在这个时间点:

  • arguments对象已经根据调用foo(1)时传入的参数进行了初始化。
  • 形参a被赋值为传入的第一个参数,即1
  • 变量bd虽然已经被声明(因为var声明会被提前),但还没有执行到它们的赋值语句,所以它们的值是undefined
  • 函数c是一个函数声明,函数声明会被完全提前,所以在函数体开始执行之前,c就已经被初始化并可以在函数作用域内被访问。

随着函数foo内部代码的执行,bd将被赋予它们各自的值(b变为2然后变为3d变为一个函数对象的引用)。但在执行任何这些语句之前,它们的值是undefined

而在执行的时间点

代码执行时AO:

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

执行时

AO = {
    arguments: { // arguments对象,包含传入函数的参数
        0: 1,    // 第一个参数的值
        length: 1 // 传入参数的个数
    },
    a: 1,        // 形参a的值
    b: 3,        // 变量b的值,注意这里是最后赋的值
    c: <reference to FunctionDeclaration "c">, // 函数c的引用
    d: <reference to FunctionExpression "d">  // 函数d的引用
}