JavaScript闭包的工作原理
技术背景
在JavaScript中,闭包是一个非常重要且强大的特性。它使得函数能够访问并操作其外部作用域中的变量,即使外部函数已经执行完毕。这为数据隐藏、封装以及实现一些高级编程模式提供了可能。在2015年之前,JavaScript没有类语法,也没有私有字段语法,闭包在一定程度上弥补了这些不足。
实现步骤
理解闭包的基本概念
闭包是函数和对其外部作用域(词法环境)的引用的组合。词法环境是每个执行上下文的一部分,它是标识符(如局部变量名)和值之间的映射。
创建闭包
以下是一个简单的闭包示例:
1 2 3 4 5 6 7 8
| function foo() { const secret = Math.trunc(Math.random() * 100); return function inner() { console.log(`The secret number is ${secret}.`); }; } const f = foo(); f();
|
在这个例子中,inner
函数形成了一个闭包,它引用了 foo
函数执行时创建的词法环境中的 secret
变量。
闭包的使用场景
私有实例变量
1 2 3 4 5 6 7 8 9 10
| function Car(manufacturer, model, year, color) { return { toString() { return `${manufacturer} ${model} (${year}, ${color})`; } }; }
const car = new Car('Aston Martin', 'V8 Vantage', '2012', 'Quantum Silver'); console.log(car.toString());
|
在这个例子中,toString
函数关闭了 Car
函数的细节,形成了闭包。
函数式编程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function curry(fn) { const args = []; return function inner(arg) { if (args.length === fn.length) return fn(...args); args.push(arg); return inner; }; }
function add(a, b) { return a + b; }
const curriedAdd = curry(add); console.log(curriedAdd(2)(3)());
|
这里的 inner
函数关闭了 fn
和 args
,形成闭包。
事件驱动编程
1 2 3 4 5 6 7 8
| const $ = document.querySelector.bind(document); const BACKGROUND_COLOR = 'rgba(200, 200, 242, 1)';
function onClick() { $('body').style.background = BACKGROUND_COLOR; }
$('button').addEventListener('click', onClick);
|
onClick
函数关闭了 BACKGROUND_COLOR
变量,形成闭包。
模块化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| let namespace = {};
(function foo(n) { let numbers = [];
function format(n) { return Math.trunc(n); }
function tick() { numbers.push(Math.random() * 100); }
function toString() { return numbers.map(format); }
n.counter = { tick, toString }; }(namespace));
const counter = namespace.counter; counter.tick(); counter.tick(); console.log(counter.toString());
|
在这个例子中,tick
和 toString
函数关闭了它们完成工作所需的私有状态和函数,实现了代码的模块化和封装。
核心代码
基本闭包示例
1 2 3 4 5 6 7 8 9 10
| function outer(x) { var tmp = 3; return function inner(y) { console.log(x + y + (++tmp)); }; }
var bar = outer(2); bar(10); bar(10);
|
闭包用于统计点击次数
1 2 3 4 5 6 7 8 9 10 11 12
| var element = document.getElementById('button');
element.addEventListener("click", (function () { var count = 0; return function (e) { count++; if (count === 3) { console.log("Third time's the charm!"); count = 0; } }; })());
|
最佳实践
- 数据隐藏和封装:使用闭包来隐藏函数内部的变量,防止外部代码直接访问和修改。
- 函数复用:通过闭包可以创建具有特定状态的函数,这些函数可以在不同的地方复用。
- 事件处理:在事件处理函数中使用闭包来保存事件处理所需的状态。
常见问题
变量作用域问题
使用 var
声明变量时,由于变量提升,可能会导致闭包引用的变量值不符合预期。建议使用 let
和 const
来声明变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function foo() { var result = []; for (var i = 0; i < 3; i++) { result.push(function inner() { console.log(i); }); }
return result; }
const result = foo(); for (var i = 0; i < 3; i++) { result[i](); }
|
在这个例子中,所有的 inner
函数都引用了同一个 i
变量,最终输出的都是 3
。
内存泄漏问题
在IE浏览器中,如果DOM元素引用了闭包,而闭包又引用了DOM元素,可能会导致内存泄漏。在现代浏览器中,JavaScript会自动清理不再引用的循环结构,但在使用时仍需注意。