前言
文章深入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()
时会报错。
通过以上例子,能比较好的分清楚进入执行上下文阶段和代码执行阶段的区别。