JavaScript执行上下文与作用域深度解析
JavaScript代码的执行并非简单的一行行线性处理,而是涉及复杂的后台准备工作。本文详细拆解执行上下文的创建流程、变量对象的初始化细节,以及作用域链在静态环境中的工作原理,通过实际代码案例厘清函数声明与表达式、全局与局部上下身的区别。
为什么代码表现与直觉不符?
观察以下两段代码的运行结果:
// 示例1:函数表达式覆盖
var foo = function () {
console.log("foo1")
}
foo() // 输出: foo1
var foo = function () {
console.log("foo2")
}
foo() // 输出: foo2
// 示例2:函数声明提升
function foo() {
console.log("foo1")
}
foo() // 输出: foo2
function foo() {
console.log("foo2")
}
foo()// 输出: foo2
示例1容易理解,因为变量赋值按顺序发生。但示例2中,尽管先定义了 `foo1`,最终调用的却是 `foo2`。这揭示了JavaScript引擎在执行代码前的“准备工作”——即执行上下文的初始化。
变量提升与函数声明的差异
通过以下代码可以验证提升行为:
console.log(a) // undefined
var a = 10
此处未报错说明 `a` 在 `console.log` 时已被声明。其内部逻辑等价于:
var a
console.log(a) // undefined
a = 10
再看函数层面的区别:
// 函数声明:名称和函数体均被提前
console.log(add2(1, 2)) // 3
function add2(a, b) {
return a + b
}
// 函数表达式:仅变量名提前,值为undefined
console.log(add1(1, 2)) // 报错:add1 is not a function
var add1 = function (a, b) {
return a + b
}
- 变量/函数表达式:变量提前声明,默认为
undefined。 - 函数声明:提前声明并完整赋值。
回到示例2,由于两次均为函数声明,后定义的重写了前者的引用,因此最终调用的是第二个版本。
执行上下文栈(Call Stack)
JavaScript运行时代码分为两个阶段:进入执行上下文和代码执行。所有代码都在某种执行上下文中运行。
上下文的分类与属性
- 全局执行上下文:唯一,包裹顶层代码。其变量对象即全局对象(浏览器中为
window)。 - 函数执行上下文:每次函数调用都会创建一个新上下文。其变量对象称为活动对象(Activation Object, AO)。
每个执行上下文包含三个核心属性:
- 变量对象(Variable Object, VO):存储变量、函数声明和形参。
- 作用域链(Scope Chain):用于变量查找的链式结构。
- this:当前执行环境的引用。
执行过程的细节分析
以函数为例,进入执行上下文时的AO构建过程如下:
function foo(a) {
var b = 2
function c() {}
var d = function () {}
b = 3
}
foo(1)
1. 进入上下文阶段(参数赋值)
- 形参:创建属性名,赋值实参值。无实参则设为
undefined。 - 函数声明:创建属性名,赋值为函数对象引用。若已存在同名项,完全替换。
- 变量声明:创建属性名,赋值为
undefined。若已存在同名形参或函数,则不干扰该属性。
此时 AO 状态:
AO={
arguments: { 0:1, length:1 },
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
2. 代码执行阶段(赋值修正)
按行执行代码,修改变量值:
b = 3:AO.a.b 从undefined变为3。d = function...:AO.d 从undefined变为具体函数表达式对象。
最终 AO 状态:
AO={
arguments: { 0:1, length:1 },
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
调用栈的工作原理
JavaScript使用栈结构管理执行上下文。栈底始终是全球上下文 globalContext。
function fun3() { console.log("fun3") }
function fun2() { fun3() }
function fun1() { fun2() }
fun1()
执行流程演示:
- 初始化:Stack [
globalContext]。 - 调用
fun1:Stack.push(<fun1>)。 - fun1 调用
fun2:Stack.push(<fun2>)。 - fun2 调用
fun3:Stack.push(<fun3>)。 - fun3 执行完毕:Stack.pop(),弹出
<fun3>。 - 依次弹出
<fun2>和<fun1>。
作用域(Scope)详解
作用域定义了变量的可访问区域。JavaScript采用词法作用域(Lexical Scope),即函数的作用域在定义时确定,而非调用时。
作用域链的构建
当需要查找变量时,JS引擎会从当前执行上下文的VO开始查找;若未找到,则沿父级上下文查找,直至全局对象。这条链表即为作用域链。
每个函数对象在创建时都有一个内部属性 [[scope]],保存了父级变量对象的层级链:
function foo(){
function bar(){}
}
foo.[[scope]]指向全局上下文VO。bar.[[scope]]指向foo的活动对象及全局上下文VO。
注意:[[scope]] 不等于完整的作用域链。当函数激活(执行)时,其自身的AO会被推入作用域链的最前端,从而形成完整的访问路径。
总结:执行上下文与作用域的区别
- 创建时机不同:作用域是静态的,在函数定义时确定且不变;执行上下文是动态的,在函数调用前创建,执行完后销毁。
- 范围界定不同:作用域决定了变量在哪里可见;执行上下文决定了变量在这个可见区域内具体的值和this指向。
以上就是JavaScript执行上下文与作用域的详细内容,更多关于JavaScript基础概念的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









