JavaScript 中的闭包
GenAI LIVE! | 2025年6月4日
什么是闭包?
我认为 JavaScript 中的闭包是一个高级主题。它是面试中经常被问到的话题之一。
如果你读过我之前的博客文章,或者了解 JavaScript 的作用域,理解闭包会更容易。
JavaScript 中的函数作用域是指在函数内部声明的变量只能在该函数内访问。然而,该函数及其任何子函数都可以访问它。闭包则更进一步。闭包确保即使父函数执行完毕,子函数仍然可以访问父函数的作用域。
例子
function outer() {
const outerVariable = "outer";
function inner() {
const innerVariable = "inner";
console.log(`${outerVariable} ${innerVariable}`); // outer inner
}
inner();
}
outer();
我创建并执行了outer
上面的函数。此操作创建并调用了inner
该函数。inner
该函数成功记录了它声明的变量以及父函数中的变量。这是预料之中的,因为子函数可以访问父函数的作用域。
现在我们不要调用该inner
函数而是返回它。
function outer() {
const outerVariable = "outer";
function inner() {
const innerVariable = "inner";
return (`${outerVariable} ${innerVariable}`);
}
return inner;
}
const returnFunction = outer();
console.log(returnFunction); // Function returnFunction
变量returnFunction
是一个函数,因为它就是 的返回值outer
。不出所料。
🚨至此,outer
函数执行完成,返回值被赋值给一个新变量。
这很关键。JavaScript 垃圾回收机制应该清除所有 的痕迹,outerVariable
因为当函数从堆栈弹出并执行完毕时,就会发生这种情况。让我们运行returnFunction
。
function outer() {
const outerVariable = "outer";
function inner() {
const innerVariable = "inner";
return (`${outerVariable} ${innerVariable}`); // outer inner
}
return inner;
}
const returnFunction = outer();
console.log(returnFunction); // Function returnFunction
console.log(returnFunction()); // outer inner
惊喜!它仍然可以在其父函数(已执行完毕)中记录变量的值。
如果函数返回了子函数,JavaScript 垃圾回收器不会清除该函数的变量。这些子函数可以稍后运行,并且完全有资格根据词法作用域原则访问父函数的作用域。
这种垃圾回收行为并不仅限于子函数。只要还有对象保持对某个变量的引用,该变量就不会被垃圾回收。
现实世界的例子
假设我正在编写一辆汽车的程序。这辆车可以像现实世界中的汽车一样加速,并且每当它加速时,车速就会增加。
function carMonitor() {
var speed = 0;
return {
accelerate: function () {
return speed++;
}
}
}
var car = new carMonitor();
console.log(car.accelerate()); // 0
console.log(car.accelerate()); // 1
console.log(car.accelerate()); // 2
console.log(car.accelerate()); // 3
console.log(car.accelerate()); // 4
您可以看到汽车的速度是如何由 提供的,carMonitor,
并且可以通过accelerate
函数访问。每次我调用 时accelerate
,它不仅可以访问该变量,还可以从上一个值增加并返回该值。
使用闭包创建私有变量
让我们举个例子carMonitor.
function carMonitor() {
var speed = 0;
return {
accelerate: function () {
return speed++;
}
}
}
var car = new carMonitor();
console.log(car.accelerate()); // 0
console.log(car.accelerate()); // 1
console.log(car.accelerate()); // 2
console.log(car.accelerate()); // 3
console.log(car.accelerate()); // 4
console.log(speed); // speed is not defined
您可以看到,变量 是函数 的私有变量carMonitor
,并且只能由子函数访问accelerate.
。外部任何变量都无法访问它。有人可能会说,由于函数作用域的原因,这是理所当然的。它对 是私有的carMonitor
,并且对 的每个新实例也是私有的carMonitor
。
每个实例都维护其副本并增加它!
这应该可以帮助您认识到闭包的威力!
function carMonitor() {
var speed = 0;
return {
accelerate: function () {
return speed++;
}
}
}
var car = new carMonitor();
var redCar = new carMonitor()
console.log(car.accelerate()); // 0
console.log(car.accelerate()); // 1
console.log(redCar.accelerate()); // 0
console.log(redCar.accelerate()); // 1
console.log(car.accelerate()); // 2
console.log(redCar.accelerate()); // 2
console.log(speed); // speed is not defined
car
并redCar
维护自己的私有speed
变量,speed
外部不可访问。
我们强制使用者使用函数或类上定义的方法,而不是直接访问属性(他们不应该这样做)。这就是你封装代码的方式。
我希望这篇文章能够消除您对 JavaScript 中闭包的任何疑虑!
常见面试问题
这是一个关于闭包的面试问题,经常被问到。
您认为以下代码段的输出是什么:
for (var i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
如果你猜的是 0 到 5 之间的数字,中间间隔一秒,那你肯定会大吃一惊。setTimeout 调用时,1 秒后i
的值变成了 6!我们希望使用创建时的值i
,IIFE + 闭包可以帮助你做到这一点。
for (var i = 0; i <= 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
})(i);
}
还有另一种方法可以解决这个问题。使用let
关键字。
var
在我们用于声明的循环中,i
创建了一个函数作用域。这会导致所有循环迭代共享一个绑定。当六个计时器完成时,它们都使用同一个变量,最终值为 6。
let
具有块作用域,在for
循环中使用时会为循环的每次迭代创建一个新的绑定。循环中的每个计时器都会获得一个不同的变量,其值从 0 到 5 不等。
for (let i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
现在输出将是 0 到 5 之间的数字。如果您的目标是 ES5,请使用 IIFE 加闭包方法。如果您可以使用 ES6,请使用let
关键字方法。
这就是 Babel 在将 ES6 代码转译为 ES5 时所做的事情。它将上述let
代码转换为闭包 + IIFE 的组合!