JavaScript 闭包如何工作?
对于了解闭包所包含的概念(例如函数、变量等),但不了解闭包本身的人,您将如何解释 JavaScript 闭包?
我已看到 Wikipedia 上提供的 Scheme 示例 ,但不幸的是,它并没有帮助。
闭包是以下项的配对:
- 函数和
- 对该函数的外部作用域(词法环境)的引用
词法环境是每个执行上下文(堆栈框架)的一部分,是标识符(即局部变量名)和值之间的映射。
JavaScript 中的每个函数都维护对其外部词法环境的引用。此引用用于配置调用函数时创建的执行上下文。此引用使函数内的代码能够“看到”在函数外部声明的变量,无论何时何地调用该函数。
如果一个函数被另一个函数调用,而该函数又被另一个函数调用,则会创建对外部词法环境的引用链。此链称为作用域链。
在下面的代码中,
inner
与调用
foo
时创建的执行上下文的词法环境形成一个闭包,
封闭
变量
secret
:
function foo() {
const secret = Math.trunc(Math.random() * 100)
return function inner() {
console.log(`The secret number is ${secret}.`)
}
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret` is to invoke `f`
换句话说:在 JavaScript 中,函数携带对私有“状态框”的引用,只有它们(以及在同一词法环境中声明的任何其他函数)才可以访问。此状态框对于函数调用者是不可见的,从而提供了一种出色的数据隐藏和封装机制。
请记住:JavaScript 中的函数可以像变量(一等函数)一样传递,这意味着这些功能和状态的配对可以在程序中传递,类似于在 C++ 中传递类的实例的方式。
如果 JavaScript 没有闭包,那么就必须在函数之间 显式 传递更多状态,从而使参数列表更长、代码更嘈杂。
因此,如果您希望函数始终能够访问私有状态,则可以使用闭包。
...并且我们经常 确实 希望将状态与函数关联起来。例如,在 Java 或 C++ 中,当您向类添加私有实例变量和方法时,您就是将状态与功能关联起来。
在 C 和大多数其他常用语言中,函数返回后,所有局部变量都不再可访问,因为堆栈框架已被破坏。在 JavaScript 中,如果您在另一个函数中声明一个函数,则外部函数的局部变量在从该函数返回后仍可访问。这样,在上面的代码中,
secret
在从
foo
返回后仍可供函数对象
inner
使用。
闭包的用途
每当您需要与函数关联的私有状态时,闭包都很有用。这是一种非常常见的情况 - 请记住:JavaScript 直到 2015 年才有类语法,而且它仍然没有私有字段语法。闭包满足了这一需求。
私有实例变量
在下面的代码中,函数
toString
封闭了汽车的详细信息。
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())
函数式编程
在下面的代码中,函数
inner
覆盖了
fn
和
args
。
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)()) // 5
面向事件的编程
在下面的代码中,函数
onClick
覆盖了变量
BACKGROUND_COLOR
。
const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200, 200, 242, 1)'
function onClick() {
$('body').style.background = BACKGROUND_COLOR
}
$('button').addEventListener('click', onClick)
<button>Set background color</button>
模块化
在下面的示例中,所有实现细节都隐藏在立即执行的函数表达式中。函数
tick
和
toString
覆盖了完成工作所需的私有状态和函数。闭包使我们能够模块化和封装我们的代码。
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())
示例
示例 1
此示例表明,闭包中未复制局部变量:闭包保留对原始变量 本身 的引用。就好像堆栈框架在外部函数退出后仍保留在内存中一样。
function foo() {
let x = 42
let inner = () => console.log(x)
x = x + 1
return inner
}
foo()() // logs 43
示例 2
在下面的代码中,三个方法
log
、
increment
和
update
都覆盖相同的词法环境。
每次调用
createObject
时,都会创建一个新的执行上下文(堆栈框架),并创建一个全新的变量
x
和一组新函数(
log
等),这些函数会覆盖这个新变量。
function createObject() {
let x = 42;
return {
log() { console.log(x) },
increment() { x++ },
update(value) { x = value }
}
}
const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42
示例 3
如果您使用通过
var
声明的变量,请仔细了解要覆盖哪个变量。使用
var
声明的变量会被提升。由于引入了
let
和
const
,在现代 JavaScript 中,这个问题不再那么严重。
在下面的代码中,每次循环时,都会创建一个新的函数
inner
,该函数在
i
上闭合。但是因为
var i
被提升到循环外部,所以所有这些内部函数都覆盖同一个变量,这意味着
i
的最终值 (3) 被打印了三次。
function foo() {
var result = []
for (var i = 0; i < 3; i++) {
result.push(function inner() { console.log(i) } )
}
return result
}
const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
result[i]()
}
最后几点:
- 只要在 JavaScript 中声明一个函数,就会创建闭包。
-
从另一个函数内部返回一个
function
是闭包的经典示例,因为外部函数内部的状态对于返回的内部函数而言是隐式可用的,即使在外部函数完成执行之后也是如此。 -
只要在函数内部使用
eval()
,就会使用闭包。您eval
中的文本可以引用函数的局部变量,在非严格模式下,您甚至可以使用eval('var foo = …')
创建新的局部变量。 -
当您在函数内部使用
new Function(…)
( Function 构造函数 )时,它不会关闭其词法环境:而是关闭全局上下文。新函数不能引用外部函数的局部变量。 - JavaScript 中的闭包就像在函数声明点保留对作用域的引用( 不是 副本),而函数声明点又保留对其外部作用域的引用,依此类推,一直到作用域链顶部的全局对象。
- 声明函数时会创建一个闭包;此闭包用于在调用函数时配置执行上下文。
- 每次调用函数时都会创建一组新的局部变量。
链接
- Douglas Crockford 使用闭包模拟了对象的 私有属性和私有方法 。
- 很好地解释了如果不小心,闭包如何 导致 IE 内存泄漏 。
- 有关 JavaScript 闭包 的 MDN 文档。
- 初学者指南 JavaScript 闭包 。
JavaScript 中的每个函数都维护与其外部词法环境的链接。词法环境是范围内所有名称(例如变量、参数)及其值的映射。
因此,只要您看到
function
关键字,该函数内的代码就可以访问函数外部声明的变量。
function foo(x) {
var tmp = 3;
function bar(y) {
console.log(x + y + (++tmp)); // will log 16
}
bar(10);
}
foo(2);
这将记录
16
,因为函数
bar
覆盖了参数
x
和变量
tmp
,这两个变量都存在于外部函数
foo
的词法环境中。
函数
bar
及其与函数
foo
词法环境的链接是一个闭包。
函数不必 return 即可创建闭包。仅凭其声明,每个函数都会封闭其封闭的词法环境,形成一个闭包。
function foo(x) {
var tmp = 3;
return function (y) {
console.log(x + y + (++tmp)); // will also log 16
}
}
var bar = foo(2);
bar(10); // 16
bar(10); // 17
上述函数也将记录 16,因为
bar
内的代码仍然可以引用参数
x
和变量
tmp
,即使它们不再直接在范围内。
但是,由于
tmp
仍然在
bar
的闭包内徘徊,因此可以增加它。每次调用
bar
时,它都会增加。
闭包最简单的例子是:
var a = 10;
function test() {
console.log(a); // will output 10
console.log(b); // will output 6
}
var b = 6;
test();
当调用 JavaScript 函数时,会创建一个新的执行上下文
ec
。除了函数参数和目标对象之外,此执行上下文还接收指向调用执行上下文的词法环境的链接,这意味着在外部词法环境中声明的变量(在上面的例子中,
a
和
b
)可从
ec
获得。
每个函数都会创建一个闭包,因为每个函数都有一个指向其外部词法环境的链接。
请注意,变量 本身 在闭包内可见, 而不是 副本。
前言:这个答案是在问题为:
Like the old Albert said : "If you can't explain it to a six-year old, you really don't understand it yourself.”. Well I tried to explain JS closures to a 27 years old friend and completely failed.
Can anybody consider that I am 6 and strangely interested in that subject ?
时写的,我很确定我是唯一试图从字面上理解最初问题的人之一。从那时起,这个问题已经多次改变,所以我的答案现在可能看起来非常愚蠢和不合时宜。希望故事的总体思路对某些人来说仍然很有趣。
在解释困难的概念时,我非常喜欢类比和隐喻,所以让我试着讲一个故事。
从前:
有一位公主......
function princess() {
她生活在一个充满冒险的奇妙世界里。她遇见了白马王子,骑着独角兽环游世界,与恶龙战斗,遇到了会说话的动物,还经历了许多其他奇妙的事情。
var adventures = [];
function princeCharming() { /* ... */ }
var unicorn = { /* ... */ },
dragons = [ /* ... */ ],
squirrel = "Hello!";
/* ... */
但她总是不得不回到那个沉闷的、充满琐事和成年人的世界。
return {
她经常向他们讲述自己作为公主的最新奇妙冒险。
story: function() {
return adventures[adventures.length - 1];
}
};
}
但他们所看到的只是一个小女孩……
var littleGirl = princess();
……讲述魔法和幻想的故事。
littleGirl.story();
尽管大人知道真正的公主,但他们永远不会相信独角兽或恶龙的存在,因为他们永远看不到它们。大人们说她们只存在于小女孩的想象中。
但我们知道真正的真相;那个怀揣着公主的小女孩……
……真的是一位怀揣着小女孩的公主。