本文从 语言底层机制、执行过程、内存模型 三个角度,系统性解释 JS 中的作用域、词法作用域链与闭包。既保留通俗风格,又加强专业性,适合前端开发者作为“长期收藏级”的参考文档。
🧠 01. JavaScript 的运行机制:理解作用域的基础
在深入作用域之前,我们必须理解 JS 的运行方式。JS 并非逐行解释执行,而是由 V8 在执行前经历两个关键阶段:
(1)编译阶段:建立作用域结构与词法环境
在代码执行前,V8 会进行一次“轻量编译”:
- 收集所有声明(var、let、const、function)
- 为每个作用域创建 词法环境(Lexical Environment)
- 确定作用域链结构
- 分析变量引用位置
在这个阶段,函数的作用域链已经完全确定,这就是“词法作用域静态决定”的根本原因。
(2)执行阶段:进入调用栈、解析变量、执行逻辑
每当函数执行,就会创建一个新的 执行上下文(Execution Context) ,入栈并运行。
执行上下文包括三部分:
- 变量环境(Variable Environment) :管理 var 声明
- 词法环境(Lexical Environment) :管理 let/const 声明与块级作用域
- 作用域链(Scope Chain) :决定变量查找的路径
当函数执行完毕,其执行上下文会从调用栈中弹出,但并不是所有变量都会被回收——这就是闭包的关键前提。
🔍 02. 词法作用域(Lexical Scope):作用域在“定义时”决定
词法作用域指:
一个函数在“定义时”所处的词法环境,决定它能访问哪些变量,与调用方式无关。
来看例子:
function bar() {
console.log(myName);
}
function foo() {
var myName = '极客邦';
bar();
}
var myName = '极客时间';
foo();
很多开发者误以为 bar() 被 foo() 调用后就能访问 foo 的变量。
事实完全相反:
- bar 的作用域链在定义时已固定为:
bar → 全局 - 与 foo 是否调用它完全无关
所以输出结果是:
极客时间
这体现了词法作用域的两个本质特性:
- 静态性(static) :作用域在编译时确定
- 与调用位置无关:调用方式无法改变作用域链
这是理解闭包的基础。
🔗 03. 作用域链(Scope Chain):变量查找的路径结构
每个函数在创建时都会生成一个 外部引用指针 Outer,指向其定义位置的词法环境。
因此变量查找顺序为:
当前作用域 → 外层作用域 → 全局作用域
例如:
function bar () {
var myName = "极客世界";
if (1) {
let myName = "Chrome 浏览器"
console.log(test)
}
}
查找 test 的过程为:
- 块级作用域(无)
- bar 函数作用域(无)
- 全局(如果全局无 test → ReferenceError)
注意:
- let/const 会创建 块级词法环境
- var 不会创建块级作用域,只归属于函数作用域
作用域链是一种链式结构,但它在编译阶段已构建好,永不因函数调用方式改变。
🧩 04. 闭包(Closure):函数携带“出生时作用域”的能力
闭包的专业定义:
当一个函数可以在其词法作用域之外被访问时,它仍然“记住”定义时的词法环境,这种组合结构就是闭包。
我们来看实际的闭包示例:
function foo(){
var myName = '极客时间';
let test1 = 1;
const test2 = 2;
var innerBar = {
getName: function(){
console.log(test1);
return myName;
},
setName: function(newName){
myName = newName;
}
};
return innerBar;
}
var bar = foo();
bar.setName('极客邦');
console.log(bar.getName());
理解闭包必须理解:
✔ foo 已经执行完毕,但其变量没有被回收
因为:
- 其内部函数仍然引用
myName、test1、test2 - V8 判断这些变量仍然“活着”
- 所以不会回收它们的内存
📌 闭包的本质:
内层函数保存了其外层函数的词法环境引用,使得外层函数生命周期被延长。
图示:
innerBar → [[Environment]] → foo Lexical Environment
→ { myName, test1, test2 }
🎯 05. 闭包的三个充分必要条件(非常关键)
要形成闭包,必须同时满足:
① 有函数嵌套(Nested Function)
没有嵌套就谈不到外部变量引用。
② 内部函数被外部访问(逃逸)
- return 返回出去
- 赋值给外部对象
- 注册为事件回调
③ 内部函数引用外部函数的变量(自由变量)
这些变量会被保存在闭包中。
三个条件同时满足闭包才真正形成。
🧠 06. 闭包的专业应用场景
闭包在现代前端中无处不在:
(1)模拟私有变量(封装)
function Counter() {
let count = 0;
return {
inc() { return ++count },
get() { return count }
}
}
(2)函数式编程的基础(柯里化、高阶函数)
如 lodash 的 curry 实现。
(3)模块化的基础(IIFE 模式)
ES6 以前的大型库如 jQuery 依靠闭包隔离命名空间。
(4)异步回调的变量捕获
事件监听、定时器等使用闭包保存外部变量。
这些都是闭包最具代表性的“专业用途”。
⚠ 07. 闭包是否会造成内存泄漏?专业结论:不会,但可被误用导致泄漏
闭包本身 不会自动造成内存泄漏,它只是延长了变量生命周期。
但是以下情况可能导致真·泄漏:
- 将闭包存放到全局变量,导致大量变量无法释放
- 在循环中创建大量闭包对象
- 将 DOM 对象与闭包相互引用
现代 V8 对闭包有严格优化:
- 未被使用的变量会从闭包中剔除
- 闭包结构会被压缩
只要合理使用,闭包完全是安全的工具。
📝 08. 总结(专业 + 实用)
如果你掌握以下内容,你已经彻底理解 JS 的作用域体系:
✔ 词法作用域:作用域在定义时静态决定
✔ 作用域链:按词法嵌套关系自内向外查找变量
✔ 执行上下文:函数执行时的运行环境
✔ 闭包:内部函数捕获外部变量并延长其生命周期
一句话总结:
作用域链在编译阶段形成,闭包在执行阶段体现。理解它们,你就理解了 JS 的核心机制。