Description
前言
在上篇《JavaScript深入之执行上下文栈》中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
今天重点讲讲创建变量对象的过程。
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。
全局上下文
我们先了解一个概念,叫全局对象。在 W3School 中也有介绍:
全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。
在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。
例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。
如果看的不是很懂的话,容我再来介绍下全局对象:
1.可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。
console.log(this);
2.全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object);
3.预定义了一堆,嗯,一大堆函数和属性。
// 都能生效
console.log(Math.random());
console.log(this.Math.random());
4.作为全局变量的宿主。
var a = 1;
console.log(this.a);
5.客户端 JavaScript 中,全局对象有 window 属性指向自身。
var a = 1;
console.log(window.a);
this.window.b = 2;
console.log(this.b);
花了一个大篇幅介绍全局对象,其实就想说:
全局上下文中的变量对象就是全局对象呐!
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。
执行过程
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
- 进入执行上下文
- 代码执行
进入执行上下文
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
-
函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为 undefined
-
函数声明
- 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
-
变量声明
- 由名称和对应值(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"
}
到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:
-
全局上下文的变量对象初始化是全局对象
-
函数上下文的变量对象初始化只包括 Arguments 对象
-
在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
-
在代码执行阶段,会再次修改变量对象的属性值
思考题
最后让我们看几个例子:
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" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。
第一段执行 console 的时候, AO 的值是:
AO = {
arguments: {
length: 0
}
}
没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。
当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。
2.第二题
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
会打印函数,而不是 undefined 。
这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
下一篇文章
本文相关链接
深入系列
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
Activity
izhangzw commentedon May 10, 2017
Arguments对象是什么 - -。
mqyqingfeng commentedon May 10, 2017
引用《JavaScript权威指南》回答你哈:调用函数时,会为其创建一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。所有作为参数传入的值都会成为Arguments对象的数组元素。
izhangzw commentedon May 12, 2017
VO 和 AO 到底是什么关系。
jawil commentedon May 12, 2017
未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。
它们其实都是同一个对象,只是处于执行上下文的不同生命周期。@jDragonV
mqyqingfeng commentedon May 12, 2017
@jawil 非常感谢回答,一语中的。
alexzhao8326 commentedon May 23, 2017
@mqyqingfeng 楼主,有幸拜读你的深入系列,收获颇多,但也存在一些疑问。比如变量对象留给我们的思考题的第二题,按照你的写法:
但个人觉得这句“这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。”解释得有点欠完整,如果我把代码改写成下面这样:
这次打印结果就是“1”;
所以我觉得这么解释比较好:
进入执行上下文时,首先会处理函数声明,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
进入代码执行阶段,先执行console.log(foo),此时foo是函数的应用,再执行var foo = 1;将foo赋值为1,而在我改写的例子里中,先执行var foo = 1;再执行console.log(foo),所以打印1。我觉得加上代码执行阶段会更清晰,哈哈哈
jawil commentedon May 23, 2017
一个执行上下文的生命周期可以分为两个阶段。
都没有错,博主讲的主要是针对变量对象,而变量对象的创建是在EC(执行上下文)的创建阶段,所以侧重点主要是EC的生命周期的第一个阶段,我觉得再执行var foo = 1这句话有点不妥,应该是给foo赋值,应该是执行foo=1这个操作,因为在EC创建阶段var已经被扫描了一遍。
@alexzhao8326
alexzhao8326 commentedon May 23, 2017
是的,显然你的说法更严谨,也符合分析的过程! 学习了@jawil
mqyqingfeng commentedon May 23, 2017
@jawil 哈哈,十分感谢回答~~~ @alexzhao8326 这道题应该是因为没有分成两个阶段来讲,所以让你觉得分析得不是很完整吧。我在写的时候,觉得毕竟是思考题,讲清楚问题的关键点即可,所以也没有给出完整的分析。如果你看完前面的内容,相信你一定能明白结果为什么会是这样,对于你修改后的例子,相信你也能解释的了。当然了,学习时严谨的态度还是要有的,感谢指出,o( ̄▽ ̄)d
mqyqingfeng commentedon May 26, 2017
@wedaren 进入执行上下文时,初始化的规则如下,从上到下就是一种顺序:
282 remaining items
sanhaoys commentedon Mar 22, 2023
参照
函数传入实参时,变量对象的a就已经被赋予实际值了
freewalker8 commentedon Mar 22, 2023
1693146833 commentedon Nov 26, 2023
简而言之
同名的函数和变量之间只能有一个
函数在创建阶段就已经赋好值了,所有这阶段函数优先,而执行阶段变量才会赋好值,在执行阶段变量优先。
freewalker8 commentedon Nov 26, 2023
thiskiller commentedon Feb 21, 2024
“当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。”
这句话有问题吧?我在外层打印a是会报错的,它是在方法内定义的
freewalker8 commentedon Feb 21, 2024
thiskiller commentedon Feb 21, 2024
function foo(a) {
console.log(a);
function a() {
}
}
foo("name");
有个疑问,在执行阶段,形参被赋值时应该会覆盖解析阶段的值啊,此时打印出来的应该是name,而不应该是Function a啊
xingqq commentedon Feb 11, 2025
评论区学习看到这样一句话:在上面的规则中我们看出,function声明会比var声明优先级更高一点。
有一个疑问,变量对象中的变量声明,指的是var声明的变量吗?是否包括let和const
freewalker8 commentedon Feb 11, 2025
JunlinZhu-Tommy commentedon May 13, 2025
这个感觉不太对,为什么不是第一段报错后就中断了呢?
freewalker8 commentedon May 13, 2025
freewalker8 commentedon Jun 18, 2025
loveheavenlina commentedon Jun 18, 2025
rainbowyy commentedon Jun 18, 2025