本文深入解析JavaScript引擎的执行机制,详细阐述执行上下文的创建过程、变量对象(VO)与活动对象(AO)的区别,以及作用域链的构建原理。通过代码示例说明变量提升、函数声明提升及调用栈的工作流程,帮助开发者理解词法作用域与动态作用域的差异,掌握JavaScript核心内存管理概念。
执行上下文
首先观察以下代码片段:
var foo = function () {
console.log("foo1")
}
foo() // foo1
var foo = function () {
console.log("foo2")
}
foo() // foo2接着看这段代码的表现:
function foo() {
console.log("foo1")
}
foo() // foo2
function foo() {
console.log("foo2")
}
foo()// foo2结果是否令人困惑?第一段代码逻辑直观,但第二段代码为何会输出两个"foo2"?
原因在于JavaScript引擎并非逐行分析与执行代码。在代码执行前,引擎会进行一系列预处理工作。具体涉及哪些准备工作?
接下来我们将逐步剖析。
console.log(a) // undefined
var a = 10在代码中,我们在变量a定义之前打印a,并未报错,说明在console.log(a)执行阶段,a已被声明。这即所谓的变量提升,属于预处理环节。
var a
console.log(a)
a = 10具体而言,引擎会将变量a的定义提前声明,但不进行赋值操作。
随后考察函数声明与函数表达式在预处理阶段的差异。
console.log(add2(1, 2)) // 3
function add2(a, b) {
return a + b
}
console.log(add1(1, 2)) // 报错:add1 is not a function
var add1 = function (a, b) {
return a + b
}结果显示,使用函数语句定义的add2,其函数名与函数体均被提前,可在声明前调用。而函数表达式仅变量声明提前,赋值仍保留在原位。至此,前文代码的输出结果便清晰可解。
那么,JavaScript引擎究竟完成了哪些预处理工作?
- 变量与函数表达式:变量声明提前,初始值为undefined。
- 函数声明:声明与赋值均提前。
此外,this关键字也在预处理阶段被确定并赋值。
当函数被调用时,即触发预处理过程,这一过程即为“执行上下文”。
执行上下文栈
执行上下文栈负责管理执行上下文。JavaScript代码存在两种执行上下文:全局执行上下文和函数执行上下文(暂不考虑eval)。全局执行上下文唯一,而每次函数调用都会创建一个新的函数执行上下文。
每个执行上下文包含三个核心属性:
- 变量对象(
Variable object, VO) - 作用域链(
Scope chain) - this指向
变量对象
变量对象是与当前执行上下文关联的数据作用域,存储了该上下文中定义的变量和函数声明。
不同执行上下文的变量对象各不相同,下面分别探讨全局上下文与函数上下文中的变量对象。
全局上下文
- 全局对象是预定义对象,作为JavaScript全局函数和全局属性的占位符。通过全局对象,可访问所有其他预定义对象、函数和属性。
- 在顶层JavaScript代码中,可使用关键字this引用全局对象。由于全局对象位于作用域链顶端,所有非限定性变量和函数名均作为该对象的属性进行查询。
- 例如,当JavaScript代码引用parseInt()函数时,实际引用的是全局对象的parseInt属性。
函数上下文
在函数上下文中,我们使用活动对象(activation object, AO)来表示变量对象。
活动对象与变量对象本质相同,区别在于变量对象是规范或引擎实现层面的概念,无法在JavaScript环境中直接访问。只有当执行上下文进入激活状态时,变量对象才会被激活,此时称为activation object。只有被激活的变量对象(即活动对象)上的属性才可被访问。
活动对象在进入函数上下文时创建,并通过函数的arguments属性进行初始化,该属性值为Arguments对象。
执行过程
执行上下文的代码处理分为两个阶段:
- 进入执行上下文
- 代码执行
进入执行上下文
函数调用后进入执行上下文,在代码执行前,变量对象包含以下内容:
函数的所有形参
- 由名称和对应值组成的变量对象属性被创建
- 若无实参,属性值设为undefined
函数声明
- 由名称和对应值(函数对象)组成的变量对象属性被创建
- 若变量对象已存在同名属性,则完全替换该属性
变量声明
- 由名称和对应值(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={
arguments: {
0:1,
length:1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}简要总结变量对象:
- 全局上下文的变量对象初始化为全局对象。
- 函数上下文的变量对象初始化包含Arguments对象。
- 进入执行上下文时,会给变量对象添加形参、函数声明、变量声明等初始属性值。
- 在代码执行阶段,会再次修改变量对象的属性值。
接下来观察执行上下文栈的工作机制。
function fun3() {
console.log("fun3")
}
function fun2() {
fun3()
}
function fun1() {
fun2()
}
fun1()使用数组模拟执行上下文栈,最先遇到的是全局代码,初始化时将全局执行上下文globalContext压入执行上下文栈。
Stack=[
globalContext
]当执行函数时,会创建执行上下文并压入栈中;函数执行完毕后,将该函数的执行上下文从栈中弹出。所有代码执行完毕后,上下文将被销毁。
// 执行fun1
Stack.push(<fun1>functionContext);
// fun1中调用了fun2
Stack.push(<fun2>functionContext);
//fun2中调用了fun3
Stack.push(<fun3>functionContext);
//fun3执行完毕 弹出
Stack.pop()
//fun2执行完毕 弹出
Stack.pop()
//fun1执行完毕 弹出
Stack.pop()栈底始终存在全局执行上下文globalContext。
作用域
作用域指程序源代码中定义变量的区域,规定了如何查找变量,即确定当前执行代码对变量的访问权限。JavaScript采用词法作用域(静态作用域)。
静态作用域与动态作用域
JavaScript采用词法作用域,函数的作用域在函数定义时确定。词法作用域相对的是动态作用域,函数的作用域在函数调用时确定。
作用域链
查找变量时,先从当前上下文的变量对象中查找,未找到则向父级执行上下文的变量对象查找,直至全局上下文的变量对象(即全局对象)。由多个执行上下文的变量对象构成的链表称为作用域链。
函数创建
前文提到,函数的作用域在定义时已确定。这是因为函数具有内部属性[[scope]],函数创建时会保存所有父变量对象至其中。可理解[[scope]]为所有父变量对象的层级链,但[[scope]]不代表完整的作用域链。参考以下代码:
function foo(){
function bar(){
}
}函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
]
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]当函数激活并进入函数体,创建VO/AO后,活动对象将被添加到作用域链的前端。
总结
执行上下文与作用域的区别:
1. 除全局作用域外,每个函数都会创建自己的作用域,作用域在函数定义时确定,而非调用时。
全局执行上下文环境在全局作用域确定后、JavaScript代码立即执行前创建。
函数执行上下文在调用函数时、执行函数体代码前创建。
2. 作用域是静态的,只要函数定义好便一直存在且不再变化。
执行上下文环境是动态的,调用函数时创建,函数调用结束后上下文环境即被释放。
以上即为JavaScript执行上下文与作用域的核心内容,更多相关资料请关注本站其他文章。