JavaScript 作用域链、执行上下文与闭包深度解析
本文深入解析 JavaScript 的词法作用域、作用域链及执行上下文机制,阐明闭包的实现原理。通过变量提升、全局与局部变量优先级、活动对象及栈结构等核心概念,详细解释闭包如何保持对定义时作用域的引用,并探讨 for 循环中常见的闭包陷阱及解决方案。
引言
闭包与作用域链是 JavaScript 核心且重要的概念,本文整理相关资料,对相关知识点进行系统总结。
闭包与词法作用域基础
JavaScript 采用词法作用域(lexical scoping),即函数执行时所依赖的变量作用域由函数定义时的环境决定,而非执行时决定。以代码片段为例,在基于栈的实现(如 C 语言)中,foo 调用结束后其本地变量 scope 通常会被释放;但从词法角度看,foo 内嵌的匿名函数引用的 scope 应指向 foo 的本地变量 scope,且实际运行结果与词法表达式一致:调用 f 后返回的是 local scope。函数对象 f 在主体函数 foo 调用结束后,仍保持对 foo 函数体作用域变量的引用,这便是闭包。
var scope = 'global scope';
function foo() {
var scope = 'local scope';
return function () {
return scope;
}
}
var f = foo();
f(); // 返回 "local scope"
那么闭包究竟如何工作?理解闭包需先掌握变量作用域、作用域链以及执行上下文环境这三个关键概念。
变量作用域与函数声明提升
变量作用域
在 JavaScript 中,全局变量拥有全局作用域,函数体内声明的变量作用域为整个函数体(局部),嵌套函数亦包含在内。函数体内局部变量的优先级高于全局变量,若局部变量与全局变量重名,全局变量会被局部变量覆盖;同理,嵌套函数内定义的局部变量优先级高于其所在函数的局部变量。这属于基础常识。接下来探讨较为陌生的部分。
函数声明提升
用一句话概括函数声明提升:函数体内声明的变量在整个函数内有效,即即使在函数体最底部声明的变量,也会被提升到最顶部。例如:
var scope = 'global scope';
function foo() {
console.log(scope); // 这里不会打印出 "global scope",而是 "undefined"
var scope = 'local scope';
console.log(scope); // 很显然,打印出 "local scope"
}
foo();
第一个 console.log(scope) 打印出 undefined 而非 global scope,原因是局部变量的声明被提升,但尚未赋值。
全局与局部变量的属性本质
作为属性的变量
JavaScript 中有三种定义全局变量的方式,如示例代码中的 globalVal1、globalVal2 和 globalValue3。一个有趣的现象是,全局变量实际上只是全局对象 window(浏览器环境)或 global(Node.js 环境)的属性。为了更符合常规变量定义,JavaScript 将使用 var 定义的全局变量设计为不可删除的全局对象属性。通过 Object.getOwnPropertyDescriptor(this, 'globalVal1') 可知,其 configurable 属性为 false。
var globalVal1 = 1; // 不可删除的全局变量
globalVal2 = 2; // 可删除的全局变量
this.globalValue3 = 3; // 同 globalValue2
delete globalVal1; // => false 变量没有被删除
delete globalVal2; // => true 变量被删除
delete this.globalValue3; //=> true 变量被删除
那么,函数体内定义的局部变量是否也作为某个对象的属性?答案是肯定的。该对象与函数调用相关,在 ECMAScript 3 中称为“call object”,在 ECMAScript 5 中称为“declarative environment record”。这个特殊对象对我们而言是一种不可见的内部实现。
作用域链机制
作用域链
由上一节可知,函数局部变量可视为某个不可见对象的属性。JavaScript 词法作用域的实现可描述为:每段 JavaScript 代码(全局或函数)都关联一个作用域链,其结构可以是数组或链表;作用域链中的每个元素定义了一组作用域内的变量。查找变量 x 的值时,从作用域链的第一个元素开始查找,若未找到则继续查找链表中的下一个元素,直到找到或抵达链尾。理解作用域链的概念对理解闭包至关重要。
执行上下文详解
执行上下文
每段 JavaScript 代码的执行都与执行上下文绑定,运行的代码通过执行上下文获取可用的变量、函数、数据等信息。全局执行上下文唯一,与全局代码绑定;每执行一个函数都会创建一个执行上下文与其绑定。JavaScript 通过栈数据结构维护执行上下文:全局执行上下文位于栈底,执行函数时,新创建的函数执行上下文压入栈中,执行上下文指针指向栈顶,运行的代码即可获得当前执行的函数绑定的执行上下文。若函数体执行嵌套函数,也会创建执行上下文并压入栈,指针指向栈顶;当嵌套函数运行结束后,与其绑定的执行上下文被推出栈,指针重新指向函数绑定的执行上下文。同理,函数执行结束后,指针会指向全局执行上下文。
执行上下文可描述为包含变量对象(对应全局)/活动对象(对应函数)、作用域链和 this 的数据结构。当函数执行时,活动对象被创建并绑定到执行上下文。活动对象包括函数体内声明的变量、函数、arguments 等。作用域链已在上一节提及,是按词法作用域构建的。需注意,this 不属于活动对象,在函数执行的那一刻即已确定。执行上下文的创建有特定的次序和阶段,不同阶段状态不同,具体细节可参考文末列出的参考资料。
闭包原理与常见陷阱
闭包
了解了作用域链和执行上下文,回顾篇首代码,即可解释闭包的工作机制。函数调用时创建的执行上下文及词法作用域链保留了函数调用所需信息,f 函数调用后即可返回 local scope。
需注意,函数内定义的多个函数共享同一个作用域链,在使用 for 循环赋值匿名函数对象的场景中易引发错误,举例如下:
var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = {
func: function() {
return i;
}
};
}
arr[0].func(); // 返回 10,而不是 0
arr[0].func() 返回的是 10 而非 0,这与感官语义有偏差。在 ECMAScript 6 引入 let 之前,变量作用域范围是在整个函数体内而非代码区块之内,因此上述例子中所有定义的 func 函数引用了同一个作用域链,在 for 循环结束后,i 的值已变为 10。
正确的做法如下:
var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = {
func: getFunc(i)
};
}
function getFunc(i) {
return function() {
return i;
}
}
arr[0].func(); // 返回 0
以上内容介绍了 JavaScript 作用域链、执行上下文与闭包的相关知识,希望对大家有所帮助。
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
