深入js——变量对象

1,353 阅读6分钟

前言

文章深入js——执行上下文栈主要讲了代码执行过程中,执行上下文栈的变化,从文本开始,主要研究下执行上下文内部。 与执行上下文相关的3个概念:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this 本文首先研究下变量对象。

变量对象VO

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

  • 变量 (var, 变量声明);
  • 函数声明 (FunctionDeclaration, 缩写为FD);
  • 函数的形参(仅在函数执行上下文中存在) 全局上下文和函数上下文中的变量对象有些不同,因此以下分开讨论。

浏览器控制台查看VO

在讨论前,我们先看下在浏览器控制台如何查看VO,以帮助我们更好的理解后续内容。

之前在文章深入js——执行上下文栈中提到通过Call Stack查看执行上下文栈,变量对象也可以在控制台中看到。

变量对象

图中红框的部分确切来说是用来看作用域链的,但这块还没讲到,我们可以暂且这样做:需要观察哪个执行上下文的变量对象,就debugger到对应的可执行代码块中,然后观察Local内变量的状态。

全局上下文中的变量对象

全局对象

全局对象Global是JavaScript最特别的一个对象,是在进入任何执行上下文之前就已经创建了的对象。 全局对象在初始阶段就将Math、String、Date、parseInt等属性作为自身属性初始化,同样在全局作用域创建的所有变量或方法,也就是我们通常称为的全局对象或全局方法,也都是全局对象的属性。

区别全局对象和window window只是Global在浏览器环境下的一种体现,也就是在浏览器中Global === window,但不能混淆这2个概念。比如在Node.js中,Global仍然存在,但不等于window了。

全局上下文中的VO

通过以上对全局上下文的解释,我们可以知道, 全局上下文中的变量对象就是Global。即

VO(globalContext) === global;

也就是在全局上下文中的变量,都可以通过Global的属性来访问。

debugger
var x = 1;
function bar (b) {
    debugger
    var a = 1;
    console.log(arguments)
    function foo () {
        console.log(a)
    }
    foo()
}
bar(1)

运行上述代码,打开控制台。在第一个debugger处,可以看到右侧是Global,展开会发现里面包含了很多Global自带的属性(Math、String等),同时也包含了我们声明的x变量和bar函数。 因此,说了这么多,就是一句话:全局上下文中的VO就是全局对象!

函数上下文的VO

在函数上下文中,将活动对象(activation object, AO)作为变量对象。 活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。 注释掉bar中的其他变量声明

可以看到,进入bar时,arguments就已经初始化了。

整体流程

除了形参和arguments的差异,全局上下文和函数上下文的变量对象的变化过程都是一样的,因此下面统一讨论。

进入执行上下文

在进入执行上下文时,变量对象VO已经包含了如下属性

  • 函数形参(仅存在与函数执行上下文) 由名称和对应值组成的一个变量对象的属性被创建;没有传递对应参数的话,那么由名称和undefined值组成的一种变量对象的属性也将被创建。
  • 函数声明 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建;如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 变量声明 — 由名称和对应值(undefined)组成一个变量对象的属性被创建;如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

还是上面那段代码,在第二个debugger处查看函数的变量对象

说明:除了形参和arguments,其他变量在全局环境中观察也是一样的,只是Global中太多自身属性,要找到我们定义的变量比较麻烦,因此就在函数中观察,但结果可扩展到全局上下文中。

函数变量对象
可以看到,包含了arguments、函数foo、形参b、变量a

这里要特别说明2个问题:

1.函数声明会覆盖同名的变量声明,后面的函数声明会覆盖同名的前面的函数声明

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

打开控制台,可以看到进入执行上下文时,即使var foo = 1在函数声明后面,最终foo也是函数而不是变量,且console会打印出2(后面的函数声明覆盖了前面的)。

结果

2.变量只能通过使用var关键字才能声明

变量声明要通过var, 即var b = 10是声明了变量foo,但b = 10没有。

console.log(a); // undefined
console.log(b); // 报错,"b" 没有声明
b = 10;
var a = 20;

console.log(b)会报错,因为在进入b还没有声明,查看右侧Global中也会发现,在进入执行上下文时,只有a,没有b

var b = 10可以分解为2个步骤:变量声明和变量赋值,但b = 10只是一个单纯的赋值操作(这里是Global.b = 10)。而变量声明是在进入执行上下文发生,而赋值操作是在执行赋值代码时发生,因此在赋值操作前是访问不到b的。

代码执行

在代码执行阶段,就会按顺序执行代码,根据代码修改变量的值。 沿用上面的一个例子,修改下

function bar () {
    debugger
    var foo = 1;
    foo();
    function foo () {
    	console.log(1)
    }
    function foo () {
    	console.log(2)
    }
}
bar()

打开控制台,会发现,在debugger处(进入执行上下文),foo是函数,且没有报错;但继续顺序执行代码会报错,因为执行到var foo = 1,此时foo已经被赋值为1,不为函数,故执行foo()时会报错。 通过以上例子,能比较好的分清楚进入执行上下文阶段和代码执行阶段的区别。