深入 JavaScript 变量对象

1,474 阅读5分钟

欢迎关注微信公众号:前端阅读室

前言

在上节《深入 JavaScript 执行上下文栈——Web 前端进阶系列第三节》我们讲到,JavaScript 引擎执行一段可执行代码时,会创建对应的执行上下文。

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

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

今天我们来重点讲解变量对象。

变量对象

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

执行上下文分为两种:全局上下文和函数上下文,接下来我们来分别讲解这两种上下文的变量对象。

全局上下文中变量对象

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

下面我们来了解一下全局对象,在 W3school 中的介绍有:

  1. 全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他预定义的对象、函数和属性。

  2. 在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。全局对象在作用域链最底端,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

  3. 由于全局对象在作用域链最底端,这也意味着在顶层 JavaScript 代码中声明的变量都将成为全局对象的属性。

字面上大家理解起来可能比较抽象,接下来我们结合具体例子作进一步讲解。

  1. 在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。在浏览器 JavaScript 中,全局对象是 window。在 node.js 中,全局对象是 global。
console.log(this); // window
console.log(this === window); // true
  1. 全局对象是 JavaScript 的全局函数和全局属性的占位符。在顶层 JavaScript 代码中声明的变量都将成为全局对象的属性。
// 声明的变量成为了全局对象的属性
var a = 1;
console.log(this.a); // 1

// 声明的函数成为了全局对象的属性
function b() {}
console.log(this.b); // function b
  1. 通过使用全局对象,可以访问全局函数和全局属性,也可以访问所有其他预定义的对象、函数和属性。
// 使用全局对象访问全局属性 Math,它是一个对象,它拥有 random 方法。
console.log(this.Math.random()); // 打印一个随机数
  1. 所有非限定性的变量和函数名都会作为该对象的属性来查询。
// 这里的 Math 是非限定性的函数名
console.log(Math.random()); // 打印一个随机数
  1. 全局对象是 Object 构造函数的实例,这也意味着 Object.prototype(原型)上预定义的属性和方法,是可以通过全局对象访问到的。
console.log(this instanceof Object); // true
  1. 在浏览器 JavaScript 中,全局对象有 window 属性且指向自身。
console.log(this.window === this); // true

函数上下文中的变量对象

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

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

活动对象是在进入函数上下文时被创建的,它有函数的 arguments 属性作为初始化属性。arguments 属性的值就是 Arguments 对象。

执行过程

函数上下文的代码执行过程共分成两个阶段,分别是:预编译和执行。

预编译

  • 创建 AO 对象,寻找形参和变量声明

  • 把形参和变量名作为 AO 对象的属性名,值为 undefined

  • 把实参赋给形参,实参形参相统一

  • 寻找函数声明,值为函数体

我们来看个例子:

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
}

代码执行

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

上面的例子当代码执行完,AO 会变为:

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

总结

至此,变量对象的创建过程我们就介绍完了,我们来做个总结:

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、变量声明、函数声明等初始的属性值(预编译)
  4. 在代码执行阶段,会修改变量对象的属性值

练习题

  1. 第一题

来看下面两端代码,分别会打印什么?

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

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

第一段会报错:Uncaught ReferenceError: a is not defined。

第二段会打印:1。

因为第一段代码 a 没有变量声明,所以函数执行上下文的 AO 中没有 a 变量的定义,此时 AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}

执行打印时,在函数执行上下文的 AO 中没有找到 a 变量的定义,然后就会去全局上下文中找,发现全局也没有,所以就会报未定义的错。

第二段代码,没有使用 var 关键字声明的变量会成为全局对象的属性,所以执行打印时,会从全局对象找到 a 的值,所以会打印 1。

  1. 第二题
console.log(foo);

function foo() {}

var foo = 1;

会打印 foo 函数,而不是 undefined。

因为在预编译的第 4 步,会寻找函数声明,值为函数体,也就是函数声明会覆盖变量声明。

欢迎关注微信公众号:前端阅读室