JavaScript执行上下文与作用域深度解析

何川
何川
发布时间:2026-10-15 | 5人浏览过
原创

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运行时代码分为两个阶段:进入执行上下文和代码执行。所有代码都在某种执行上下文中运行。

上下文的分类与属性

  1. 全局执行上下文:唯一,包裹顶层代码。其变量对象即全局对象(浏览器中为 window)。
  2. 函数执行上下文:每次函数调用都会创建一个新上下文。其变量对象称为活动对象(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()

执行流程演示:

  1. 初始化:Stack [globalContext]。
  2. 调用 fun1:Stack.push(<fun1>)。
  3. fun1 调用 fun2:Stack.push(<fun2>)。
  4. fun2 调用 fun3:Stack.push(<fun3>)。
  5. fun3 执行完毕:Stack.pop(),弹出 <fun3>。
  6. 依次弹出 <fun2> 和 <fun1>。

作用域(Scope)详解

作用域定义了变量的可访问区域。JavaScript采用词法作用域(Lexical Scope),即函数的作用域在定义时确定,而非调用时。

作用域链的构建

当需要查找变量时,JS引擎会从当前执行上下文的VO开始查找;若未找到,则沿父级上下文查找,直至全局对象。这条链表即为作用域链。

每个函数对象在创建时都有一个内部属性 [[scope]],保存了父级变量对象的层级链:

function foo(){
    function bar(){}
}
  • foo.[[scope]] 指向全局上下文VO。
  • bar.[[scope]] 指向 foo 的活动对象及全局上下文VO。

注意:[[scope]] 不等于完整的作用域链。当函数激活(执行)时,其自身的AO会被推入作用域链的最前端,从而形成完整的访问路径。

总结:执行上下文与作用域的区别

  1. 创建时机不同:作用域是静态的,在函数定义时确定且不变;执行上下文是动态的,在函数调用前创建,执行完后销毁。
  2. 范围界定不同:作用域决定了变量在哪里可见;执行上下文决定了变量在这个可见区域内具体的值和this指向。

以上就是JavaScript执行上下文与作用域的详细内容,更多关于JavaScript基础概念的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表