JavaScript闭包如何工作?
我们将如何向了解了闭包本身的概念(例如函数,变量等)的人解释JavaScript闭包,但又不了解闭包本身呢?
我已经在Wikipedia上看到了Scheme示例,但是不幸的是它没有帮助。
解决方案
Submitted by Morris on Tue, 2006-02-21 10:19. Community-edited since.
关闭不是魔术
该页面介绍了闭包,以便程序员可以使用有效的JavaScript代码来理解它们。它不适用于专家或者功能性程序员。
一旦核心概念浮出水面,关闭就不难理解。但是,通过阅读任何以理论或者学术为导向的解释是不可能理解它们的!
本文面向具有某种主流语言编程经验并且可以阅读以下JavaScript函数的程序员:
function sayHello(name) { var text = 'Hello ' + name; var say = function() { console.log(text); } say(); } sayHello('Joe');
两篇摘要
- 当一个函数(foo)声明其他函数(bar和baz)时,在该函数退出时,在foo中创建的局部变量族不会被破坏。这些变量只是对外部世界不可见。因此,foo可以巧妙地返回bar和baz函数,并且它们可以通过这个封闭的变量家族("闭包")继续进行读取,写入和通信,这些变量是其他任何人都无法干预的,甚至没有人可以介入foo再来一次。
- 闭包是支持一流功能的一种方式。它是一个表达式,可以引用其范围内的变量(首次声明时),分配给变量,作为参数传递给函数或者作为函数结果返回。
闭包的例子
以下代码返回对函数的引用:
function sayHello2(name) { var text = 'Hello ' + name; // Local variable var say = function() { console.log(text); } return say; } var say2 = sayHello2('Bob'); say2(); // logs "Hello Bob"
大多数JavaScript程序员将理解上面代码中如何将对函数的引用返回到变量(" say2")。如果不这样做,那么我们需要先进行研究,然后才能学习闭包。使用C的程序员会将函数视为返回函数的指针,而变量" say"和" say2"分别是指向函数的指针。
指向函数的C指针和指向函数的JavaScript引用之间存在关键区别。在JavaScript中,我们可以认为函数引用变量既具有指向函数的指针,也具有指向闭包的隐藏指针。
上面的代码已关闭,因为匿名函数function(){console.log(text); }是在另一个函数" sayHello2()"中声明的。在JavaScript中,如果在另一个函数中使用
function`关键字,则将创建一个闭包。
在C语言和大多数其他常见语言中,函数返回后,所有本地变量将不再可访问,因为堆栈框架被破坏了。
在JavaScript中,如果我们在另一个函数中声明一个函数,则外部函数从其返回后仍可访问。上面已经说明了这一点,因为从sayHello2()返回后,我们调用了函数say2()。注意,我们调用的代码引用了变量text
,这是函数sayHello2()
的局部变量。
function() { console.log(text); } // Output of say2.toString();
查看say2.toString()的输出,我们可以看到代码引用了变量text
。匿名函数可以引用包含值" Hello Bob"的"文本",因为" sayHello2()"的局部变量已秘密地保持在闭包中。
天才之处在于,在JavaScript中,函数引用还具有对它所创建的闭包的秘密引用,类似于委托是方法指针又是对对象的秘密引用。
更多例子
出于某种原因,当我们阅读闭包时,似乎真的很难理解它们,但是当我们看到一些示例时,就会很清楚它们是如何工作的(花了我一段时间)。
我建议仔细研究这些示例,直到我们理解它们的工作原理为止。如果我们在不完全了解闭包如何使用的情况下开始使用闭包,那么我们很快就会创建一些非常奇怪的错误!
例子3
此示例显示不复制局部变量,而是通过引用保留它们。好像在外部函数退出后,堆栈框架仍在内存中保持活动状态!
function say667() { // Local variable that ends up within closure var num = 42; var say = function() { console.log(num); } num++; return say; } var sayNumber = say667(); sayNumber(); // logs 43
例子4
这三个全局函数都对同一个闭包有共同的引用,因为它们都是在对setupSomeGlobals()的一次调用中声明的。
var gLogNumber, gIncreaseNumber, gSetNumber; function setupSomeGlobals() { // Local variable that ends up within closure var num = 42; // Store some references to functions as global variables gLogNumber = function() { console.log(num); } gIncreaseNumber = function() { num++; } gSetNumber = function(x) { num = x; } } setupSomeGlobals(); gIncreaseNumber(); gLogNumber(); // 43 gSetNumber(5); gLogNumber(); // 5 var oldLog = gLogNumber; setupSomeGlobals(); gLogNumber(); // 42 oldLog() // 5
当定义了三个函数时,这三个函数可以共享访问相同的闭包" setupSomeGlobals()"的局部变量。
请注意,在上面的示例中,如果再次调用setupSomeGlobals()
,则会创建一个新的闭包(stack-frame!)。旧的gLogNumber,gIncreaseNumber,gSetNumber变量将被具有新闭包的新函数覆盖。 (在JavaScript中,每当在另一个函数中声明一个函数时,每次调用外部函数时都会重新创建一个(或者多个)内部函数。)
例子5
此示例显示闭包包含退出前在外部函数内部声明的任何局部变量。注意,变量" alice"实际上是在匿名函数之后声明的。首先声明匿名函数;并在调用该函数时可以访问" alice"变量,因为" alice"在同一范围内(JavaScript进行变量提升)。
同样," sayAlice()()"仅直接调用从" sayAlice()"返回的函数引用,它与之前所做的完全相同,但没有临时变量。
function sayAlice() { var say = function() { console.log(alice); } // Local variable that ends up within closure var alice = 'Hello Alice'; return say; } sayAlice()();// logs "Hello Alice"
棘手的:还请注意,say
变量也位于闭包内部,可以由在sayAlice()
中声明的任何其他函数访问,或者可以在内部函数中递归访问。
例子6
对于许多人来说,这是一个真正的陷阱,因此我们需要了解它。如果要在循环中定义函数,请非常小心:闭包中的局部变量可能不会像我们首先想到的那样起作用。
我们需要了解Javascript中的"变量提升"功能才能了解此示例。
function buildList(list) { var result = []; for (var i = 0; i < list.length; i++) { var item = 'item' + i; result.push( function() {console.log(item + ' ' + list[i])} ); } return result; } function testList() { var fnlist = buildList([1,2,3]); // Using j only to help prevent confusion -- could use i. for (var j = 0; j < fnlist.length; j++) { fnlist[j](); } } testList() //logs "item2 undefined" 3 times
`result.push(function(){console.log(item +''+ list [i])}})行向结果数组添加了对匿名函数的引用三遍。认为像:
pointer = function() {console.log(item + ' ' + list[i])}; result.push(pointer);
请注意,在运行示例时,将记录三次"" item2 undefined"!这是因为就像前面的示例一样,对于buildList的局部变量(结果,i和item)只有一个闭包。当在
fnlist j行上调用匿名函数时;它们都使用相同的单个闭包,并且在该闭包内使用" i"和" item"的当前值(其中" i"的值为" 3",因为循环已完成,而" item"的值为'item2'的值)。请注意,我们是从0开始索引,因此
item的值为
item2`。 i ++将把" i"的值增加到" 3"的值。
如果使用变量item
的块级声明(通过let
关键字)而不是通过var
关键字进行函数范围的变量声明,则可能会有所帮助。如果进行了更改,则数组result
中的每个匿名函数都有其自己的闭包。运行示例时,输出如下:
item0 undefined item1 undefined item2 undefined
如果变量" i"也是使用" let"而不是" var"定义的,则输出为:
item0 1 item1 2 item2 3
例子7
在最后一个示例中,对主函数的每次调用都会创建一个单独的闭包。
function newClosure(someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push(num); console.log('num: ' + num + '; anArray: ' + anArray.toString() + '; ref.someVar: ' + ref.someVar + ';'); } } obj = {someVar: 4}; fn1 = newClosure(4, obj); fn2 = newClosure(5, obj); fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4; fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4; obj.someVar++; fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5; fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
概括
如果一切看起来都不是很清楚,那么最好的办法就是看这些例子。阅读说明比理解示例要困难得多。
最后一点:
- 每当在另一个函数中使用
function
时,都会使用闭包。 - 每当在函数内部使用
eval()
时,都会使用闭包。eval
文本可以引用该函数的局部变量,在eval
中甚至可以使用`eval('var foo =')创建新的局部变量。 - 当在函数内部使用
new Function()
(Function构造函数)时,它不会创建闭包。 (新函数不能引用外部函数的局部变量。) - JavaScript中的闭包就像保留所有局部变量的副本一样,就像它们退出函数时一样。
- 最好考虑一下,闭包总是仅创建到函数的入口,而局部变量会添加到该闭包中。
- 每次调用带有闭包的函数时,都会保留一组新的局部变量(假设该函数内部包含一个函数声明,并且将返回对该内部函数的引用或者以某种方式为其保留外部引用)。
- 两个函数可能看起来像具有相同的源文本,但是由于它们的"隐藏"关闭而具有完全不同的行为。我认为JavaScript代码实际上无法找出函数引用是否具有闭包。
- 如果我们尝试进行任何动态源代码修改(例如:
myFunction = Function(myFunction.toString()。replace(/ Hello /,'Hola'));
),则如果myFunction
不起作用是一个闭包(当然,我们甚至不会想到在运行时进行源代码字符串替换,但是...)。 - 可以在函数内的函数声明中获取函数声明,并且可以在多个级别获得闭包。
- 我认为通常闭包既是函数又是捕获的变量的术语。请注意,我不在本文中使用该定义!
- 我怀疑JavaScript中的闭包与功能语言中的闭包不同。
链接
- 道格拉斯·克罗克福德(Douglas Crockford)使用闭包为对象模拟了对象的私有属性和私有方法。
- 如果不小心,闭包将如何导致IE中的内存泄漏的绝佳解释。
谢谢
我对闭包和堆栈框架等的解释在技术上并不正确,它们是旨在帮助理解的粗略简化。一旦了解了基本概念,我们便可以稍后进行详细了解。
如果我们刚刚了解了闭包(在这里或者其他地方!),那么我对我们可能会建议使本文更清晰的任何更改所产生的反馈意见感兴趣。发送电子邮件至morrisjohns.com(morris_closure @)。请注意,我既不是JavaScript专家,也不是闭包专家。
莫里斯(Morris)的原始帖子可以在Internet存档中找到。
闭包很像一个对象。每当我们调用函数时,它都会实例化。
JavaScript中闭包的范围是词法的,这意味着闭包所属的函数中包含的所有内容都可以访问其中的任何变量。
- 给它分配
var foo = 1;
或者 - 只需写
var foo;
如果我们在闭包中包含一个变量,
如果内部函数(包含在另一个函数中的函数)在不使用var定义其范围的情况下访问此类变量,则会在外部闭包中修改变量的内容。
例子
function example(closure) { // define somevariable to live in the closure of example var somevariable = 'unchanged'; return { change_to: function(value) { somevariable = value; }, log: function(value) { console.log('somevariable of closure %s is: %s', closure, somevariable); } } } closure_one = example('one'); closure_two = example('two'); closure_one.log(); closure_two.log(); closure_one.change_to('some new value'); closure_one.log(); closure_two.log();
输出
somevariable of closure one is: unchanged somevariable of closure two is: unchanged somevariable of closure one is: some new value somevariable of closure two is: unchanged
闭包的寿命超过了产生它的函数的运行时间。如果其他函数超出了定义它们的闭包/范围(例如,作为返回值),则这些函数将继续引用该闭包。
var bind = function(x) { return function(y) { return x + y; }; } var plus5 = bind(5); console.log(plus5(3));
闭包很难解释,因为闭包用于使某些行为正常工作,每个人都希望它们能正常工作。我发现解释它们的最佳方法(以及我了解它们的方法)是想象没有它们的情况:
console.log(x + 3);
如果JavaScript不知道闭包,在这里会发生什么?只需将最后一行的调用替换为其方法主体(基本上是函数调用所做的工作),我们将获得:
现在," x"的定义在哪里?我们没有在当前范围内对其进行定义。唯一的解决方案是让plus5携带其范围(或者更确切地说,其父级的范围)。这样,x
定义明确,并绑定到值5.
function foo(x) { var tmp = 3; function bar(y) { console.log(x + y + (++tmp)); // will log 16 } bar(10); } foo(2);
每当我们在另一个函数中看到function关键字时,内部函数就可以访问外部函数中的变量。
这将始终记录16,因为bar可以访问定义为foo参数的x,并且还可以从foo访问tmp。
function foo(x) { var tmp = 3; return function (y) { console.log(x + y + (++tmp)); // will also log 16 } } var bar = foo(2); // bar is now a closure. bar(10);
那是一个封闭。一个函数不必为了被称为闭包而返回。只需访问直接词法范围之外的变量即可创建闭包。
上面的函数也将记录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函数时,将创建一个新的执行上下文。与函数参数和父对象一起,此执行上下文还接收在其外部声明的所有变量(在上面的示例中," a"和" b")。
可以通过返回一个闭包函数列表或者将它们设置为全局变量来创建多个闭包函数。所有这些都将引用相同的x
和相同的tmp
,但它们并不自己复制。
这里的数字" x"是一个文字数字。与JavaScript中的其他文字一样,当调用foo
时,数字x
被复制为foo
作为其参数x
。
function foo(x) { var tmp = 3; return function (y) { console.log(x + y + tmp); x.memb = x.memb ? x.memb + 1 : 1; console.log(x.memb); } } var age = new Number(2); var bar = foo(age); // bar is now a closure referencing age. bar(10);
另一方面,JavaScript在处理对象时总是使用引用。如果说,我们用一个对象调用了" foo",则它返回的闭包将引用该原始对象!
不出所料,每次调用bar(10)都会使x.memb递增。可能不会想到的是,x
只是与age
变量引用相同的对象!两次调用bar之后,age.memb将为2!此引用是HTML对象内存泄漏的基础。
闭包是内部函数可以访问其外部函数中的变量的地方。这可能是闭包最简单的单行解释。
- 闭包不仅在我们返回内部函数时创建。实际上,封闭函数根本不需要返回即可创建封闭函数。我们可以改为将内部函数分配给外部作用域中的变量,或者将其作为参数传递给另一个函数,在该函数中可以立即或者在以后的任何时间调用它。因此,封闭函数的关闭很可能在调用封闭函数后立即创建,因为只要在调用封闭函数之前或者之后,任何内部函数都可以访问该封闭。
- 闭包在其范围内未引用变量的旧值的副本。变量本身是闭包的一部分,因此访问这些变量之一时看到的值是访问该变量时的最新值。这就是为什么在循环内部创建内部函数会很棘手的原因,因为每个函数都可以访问相同的外部变量,而不是在创建或者调用函数时获取变量的副本。
- 闭包中的"变量"包括在函数内声明的任何命名函数。它们还包括函数的参数。闭包还可以访问其包含的闭包的变量,直到全局范围为止。
- 闭包使用内存,但是它们不会导致内存泄漏,因为JavaScript本身会清理自己的未引用的循环结构。当Internet Explorer无法断开引用闭包的DOM属性值的连接时,就会创建涉及闭包的Internet Explorer内存泄漏,从而维护对可能为圆形结构的引用。
这是为了消除对其他一些答案中出现的闭包的几种(可能的)误解。
A closure is not only created when you return an inner function. In fact, the enclosing function does not need to return at all. You might instead assign your inner function to a variable in an outer scope, or pass it as an argument to another function where it could be used immediately. Therefore, the closure of the enclosing function probably already exists at the time that enclosing function was called since any inner function has access to it as soon as it is called.
var i; function foo(x) { var tmp = 3; i = function (y) { console.log(x + y + (++tmp)); } } foo(2); i(3);
我们能向5岁的孩子解释关闭吗?*
dlaliberte第一点的示例:
/* * When a function is defined in another function and it * has access to the outer function's context even after * the outer function returns. * * An important concept to learn in JavaScript. */ function outerFunction(someNum) { var someString = 'Hey!'; var content = document.getElementById('content'); function innerFunction() { content.innerHTML = someNum + ': ' + someString; content = null; // Internet Explorer memory leak for DOM reference } innerFunction(); } outerFunction(1);?
我仍然认为Google的解释非常有效且简洁:
段落数量不匹配