闭包(Closure)是 JavaScript 中最强大也最令人困惑的特性之一。它与作用域链、执行上下文、垃圾回收等底层机制紧密相关。本文将带你从底层原理到实战场景,彻底掌握闭包。
作用域与作用域链
在理解闭包之前,必须先理解作用域。JavaScript 使用的是词法作用域(静态作用域)——函数的作用域在定义时就已经确定,而不是在调用时。
const value = 'global';\nfunction outer() {\n const value = 'outer';\n function inner() { console.log(value); }\n inner();\n}\nouter(); // 输出 'outer',不是 'global'inner 函数在 outer 内部定义,它的作用域链是:自身作用域 → outer 作用域 → 全局作用域。这个链条在代码编写时就已确定,与运行时调用位置无关。
执行上下文与变量对象
每次调用函数时,JavaScript 引擎会创建一个执行上下文(Execution Context),包含:变量对象(VO)——存储函数内部的变量、函数声明、参数;作用域链(Scope Chain)——当前 VO + 所有外部 VO;this 绑定。
function foo() {\n var a = 1;\n function bar() { var b = 2; console.log(a + b); }\n bar();\n}\nfoo();当 bar 执行时,它的作用域链为:bar.VO → foo.VO → global.VO。查找变量 a 时,先在 bar.VO 中查找(未找到),然后沿作用域链在 foo.VO 中找到。
闭包的真正定义
闭包 = 函数 + 该函数能访问的外部变量(自由变量)。MDN 的定义更精确:"闭包是函数和其声明时所在词法环境的组合。"形成闭包需要两个条件:函数嵌套(内部函数引用了外部函数的变量)和内部函数被传递到其词法作用域之外执行。
function createCounter() {\n let count = 0;\n return function() { count++; return count; };\n}\nconst counter = createCounter();\nconsole.log(counter()); // 1\nconsole.log(counter()); // 2\nconsole.log(counter()); // 3关键洞察:createCounter 执行完毕后,其执行上下文从调用栈中弹出,但 count 变量没有被销毁——因为返回的匿名函数仍然保持着对 count 的引用。
闭包的底层原理:[[Environment]] 内部属性
ECMAScript 规范中,每个函数都有一个内部属性 [[Environment]],指向它被创建时所在的词法环境(Lexical Environment)。当函数被调用时,新的词法环境会将其 [[Environment]] 作为外部引用,形成作用域链。
function makeAdder(x) {\n return function adder(y) { return x + y; };\n}\nconst add5 = makeAdder(5);\nconsole.log(add5(10)); // 15——x=5 仍然存活闭包的实战应用
1. 数据私有化(模块模式)
const User = (function() {\n let password = 'secret123';\n return { login(pwd) { return pwd === password; } };\n})();2. 函数柯里化
function curry(fn) {\n return function curried(...args) {\n if (args.length >= fn.length) return fn.apply(this, args);\n return (...nextArgs) => curried(...args, ...nextArgs);\n };\n}\nconst add = curry((a, b, c) => a + b + c);\nconsole.log(add(1)(2)(3)); // 63. 防抖与节流
function debounce(fn, delay) {\n let timer = null;\n return function(...args) {\n clearTimeout(timer);\n timer = setTimeout(() => fn.apply(this, args), delay);\n };\n}4. 循环中的 var 陷阱
// 错误:所有回调引用同一个 i\nfor (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); } // 5,5,5,5,5\n// 方案1:let 的块级作用域\nfor (let i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); } // 0,1,2,3,4\n// 方案2:IIFE 创建闭包\nfor (var i = 0; i < 5; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); }闭包与内存管理
闭包可能导致内存泄漏,但这通常只在以下场景中才会成为问题:闭包引用了大量数据或 DOM 元素、闭包的生命周期极长(如全局事件监听器)、意外引用了不需要的外部变量。
// 潜在泄漏:闭包引用整个 largeData\nfunction setupHandler() {\n const largeData = new Array(1000000).fill('data');\n document.getElementById('btn').addEventListener('click', function() {\n console.log(largeData.length);\n });\n}\n// 优化:只保留需要的值\nfunction setupHandlerOptimized() {\n const dataLength = 1000000;\n document.getElementById('btn').addEventListener('click', function() {\n console.log(dataLength);\n });\n}总结
闭包不是需要死记硬背的概念——它是词法作用域和执行上下文的自然结果。理解它最好的方式是在脑中模拟 JavaScript 引擎的工作过程:函数被创建时记住外部作用域 → 函数被调用时创建新的作用域 → 查找变量时沿作用域链逐级向上。
评论 (0)