JavaScript 闭包原理 - 函数的"记忆"能力 闭包(Closure)是 JavaScript 中最核心、也最容易让人困惑的概念之一。很多开发者能写出闭包代码,却不理解背后的原理。今天我们来彻底搞懂它。 什么是闭包? 简单来说,闭包是指函数能够记住并访问它被创建时的作用域,即使该函数在其原始作用域之外执行。 一句话定义 闭包 = 函数 + 函数创建时的词法环境 闭包的核心原理 理解闭包需要先理解 JavaScript 的两个机制: 1. 词法作用域(Lexical Scope) 函数的作用域在函数定义时就确定了,而不是在调用时。这决定了函数能访问哪些变量。 2. 作用域链(Scope Chain) 当访问一个变量时,JavaScript 引擎会从当前作用域开始,逐级向外查找,直到全局作用域。 function createCounter() { let count = 0; // 这是一个局部变量 return function() { // 这个内部函数就是一个闭包 count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 | 在这个例子中,createCounter 执行完毕后,按理说 count 应该被销毁。但因为返回的函数仍然引用着 count,这个变量被"保留"在了闭包中。 闭包的典型应用场景 1. 数据私有化(模拟私有变量) function createUser(name) { let _name = name; // 私有变量 return { getName() { return _name; }, setName(newName) { _name = newName; } }; } const user = createUser('张三'); console.log(user.getName()); // '张三' user.setName('李四'); console.log(user.getName()); // '李四' console.log(user._name); // undefined(无法直接访问) | 2. 函数工厂 function createMultiplier(factor) { return function(number) { return number * factor; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 | 3. 防抖和节流 function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } // 使用 const handleSearch = debounce((query) => { console.log('搜索:', query); }, 300); handleSearch('h'); // 不会立即执行 handleSearch('he'); // 取消上一个,重新计时 handleSearch('hello'); // 300ms 后执行一次 | 4. 模块模式 const counterModule = (function() { let count = 0; // 私有变量 return { increment() { count++; }, decrement() { count--; }, getCount() { return count; } }; })(); counterModule.increment(); console.log(counterModule.getCount()); // 1 | 闭包的经典陷阱 陷阱1: 循环中的 var // 错误示例 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出: 3, 3, 3 (而不是 0, 1, 2) // 正确方式1: 使用 let for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 输出: 0, 1, 2 // 正确方式2: 使用 IIFE for (var i = 0; i < 3; i++) { ((j) => { setTimeout(() => console.log(j), 100); })(i); } // 输出: 0, 1, 2 | 原因:var 是函数作用域,循环结束后所有回调函数共享同一个 i(值为3)。let 是块级作用域,每次迭代都创建新的绑定。 陷阱2: 内存泄漏 // 潜在的内存泄漏 function setupHandler() { const largeData = new Array(1000000).fill('x'); document.getElementById('btn').addEventListener('click', () => { console.log(largeData.length); }); } // 解决方案: 只保留需要的部分 function setupHandler() { const largeData = new Array(1000000).fill('x'); const length = largeData.length; // 只保存需要的值 document.getElementById('btn').addEventListener('click', () => { console.log(length); }); } | 闭包 vs 对象 什么时候用闭包,什么时候用对象? | 场景 | 闭包 | 对象 | | 数据量 | 少量私有数据 | 大量结构化数据 | | 访问方式 | 通过方法访问 | 可直接访问属性 | | 可扩展性 | 每次创建新闭包 | 原型共享方法 | | 适用场景 | 工厂函数、回调、事件处理 | 数据模型、实体类 | 闭包的性能考量 闭包会带来额外的内存开销,因为被引用的变量不会被垃圾回收。但这通常不是问题,除非: - 创建大量闭包(如循环中)
- 闭包引用大对象且长期存活
- 忘记解除事件监听器
最佳实践:闭包使用完后,如果不再需要,将引用设为 null,帮助垃圾回收。 总结 | 闭包的本质 - 函数 + 词法环境的组合
- 让函数"记住"创建时的作用域
- 实现数据私有化的核心机制
关键要点 - 理解词法作用域是理解闭包的前提
- 闭包不是"问题",是特性
- 合理使用可以写出优雅的代码
- 滥用可能导致内存问题
| —— JavaScript 日更 Day 24 —— |
评论
发表评论