深入解析JavaScript执行上下文与作用域机制

作者:函数茶馆时间:2026-10-26 10:10:57浏览:455

本文深入解析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执行上下文与作用域的核心内容,更多相关资料请关注本站其他文章。

作者最新文章