JavaScript闭包如何工作?

℡╲_俬逩灬. 提交于 2019-12-05 20:39:09

您将如何向了解其闭包概念(例如函数,变量等)的人解释JavaScript闭包,但却不了解闭包本身?

我已经在Wikipedia上看到了Scheme示例 ,但是不幸的是它没有帮助。


#1楼

我知道已经有很多解决方案,但是我猜想这个小而简单的脚本可以用来说明这个概念:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

#2楼

面向初学者的JavaScript关闭

莫里斯在2006年2月2日星期二提交。 从此开始由社区编辑。

关闭不是魔术

本页说明了闭包,以便程序员可以使用有效的JavaScript代码来理解闭包。 它不适用于专家或功能性程序员。

一旦核心概念浮出水面,关闭就不难理解。 但是,通过阅读任何理论或学术上的解释是不可能理解它们的!

本文面向具有某种主流语言编程经验并且可以阅读以下JavaScript函数的程序员:

function sayHello(name) { var text = 'Hello ' + name; var say = function() { console.log(text); } say(); } sayHello('Joe');

两篇摘要

  • 当一个函数( foo )声明其他函数(bar和baz)时,在该函数退出时,在foo创建的局部变量族不会被破坏 。 这些变量只是对外界不可见。 因此, foo可以巧妙地返回功能barbaz ,并且它们可以通过这个封闭的变量家族(“闭包”)继续进行读取,写入和通信,这些变量是其他任何人都无法干预的,甚至没有人可以介入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的程序员会将函数视为返回函数的指针,并且变量saysay2分别是函数的指针。

指向函数的C指针和指向函数的JavaScript引用之间存在关键区别。 在JavaScript中,你可以把一个函数引用变量作为既具有指针功能以及一个隐藏的指针关闭。

上面的代码已关闭,因为匿名函数function() { console.log(text); } function() { console.log(text); } 另一个函数内部声明,在此示例中为sayHello2() 。 在JavaScript中,如果在另一个函数中使用function关键字,则将创建一个闭包。

在C和其他大多数常用语言,函数返回 ,因为堆栈帧被摧毁了所有的局部变量都不再使用。

在JavaScript中,如果在另一个函数中声明一个函数,则外部函数从其返回后仍可访问。 上面已经演示了这一点,因为我们从sayHello2()返回后调用了函数say2() sayHello2() 。 请注意,我们调用的代码引用了变量text ,这是函数sayHello2()局部变量

function() { console.log(text); } // Output of say2.toString();

查看say2.toString()的输出,我们可以看到该代码引用了变量text 。 匿名函数可以引用包含值'Hello Bob' text ,因为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() ,则会创建一个新的闭包(堆栈框架!)。 旧的gLogNumbergIncreaseNumbergSetNumber变量将被具有新闭包的函数覆盖。 (在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的局部变量( resultilistitem )只有一个闭包。 当在行上调用匿名函数fnlist[j]() ; 它们都使用相同的单个闭包,并且使用该闭包中iitem的当前值(其中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); // attention here: new closure assigned to a new variable! 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中甚至可以使用eval('var foo = …')创建新的局部变量eval('var foo = …')
  • 函数内部使用new Function(…)函数构造函数)时,它不会创建闭包。 (新函数不能引用外部函数的局部变量。)
  • JavaScript中的闭包就像保留所有局部变量的副本一样,就像它们退出函数时一样。
  • 最好考虑一下,总是总是只在函数的入口处创建一个闭包,并且将局部变量添加到该闭包中。
  • 每次调用带有闭包的函数时,都会保留一组新的局部变量(假设该函数内部包含一个函数声明,并且将返回对该内部函数的引用或以某种方式为其保留外部引用)。
  • 两个函数可能看起来像具有相同的源文本,但是由于它们的“隐藏”关闭而具有完全不同的行为。 我认为JavaScript代码实际上无法找出函数引用是否具有闭包。
  • 如果您尝试进行任何动态源代码修改(例如: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola')); ),则如果myFunction是闭包(当然,您甚至都不会想到在运行时进行源代码字符串替换,而是...)。
  • 可以在函数内的函数声明中获取函数声明……并且可以在多个级别上获得闭包。
  • 我认为通常闭包既是函数又是捕获的变量的术语。 请注意,我不在本文中使用该定义!
  • 我怀疑JavaScript中的闭包与功能性语言中的闭包不同。

链接

谢谢

如果您刚刚学习了闭包(在这里或其他地方!),那么我对您可能会建议使本文更清晰的任何更改所产生的反馈意见感兴趣。 发送电子邮件至morrisjohns.com(morris_closure @)。 请注意,我不是JavaScript专家,也不是闭包专家。


莫里斯(Morris)的原始帖子可以在Internet存档中找到。


#3楼

闭包很像一个对象。 每当您调用函数时,它都会实例化。

JavaScript中闭包的范围是词法的,这意味着闭包所属函数中包含的所有内容都可以访问其中的任何变量。

如果您在闭包中包含一个变量,

  1. 给它分配var foo=1; 要么
  2. 只需写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

#4楼

闭包很难解释,因为闭包用于使某些行为正常工作,每个人都希望它们能正常工作。 我发现解释它们的最佳方法(以及了解它们的方法)是想象没有它们的情况:

 var bind = function(x) { return function(y) { return x + y; }; } var plus5 = bind(5); console.log(plus5(3));

如果JavaScript 知道闭包,在这里会发生什么? 只需将最后一行的调用替换为其方法主体(基本上是函数调用所做的工作),您将获得:

console.log(x + 3);

现在, x的定义在哪里? 我们没有在当前范围内对其进行定义。 唯一的解决方案是让plus5 携带其范围(或更确切地说,其父级的范围)。 这样, x定义明确,并绑定到值5。


#5楼

每当您在另一个函数中看到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它被定义为foo的参数),并且还可以从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仍然可以引用xtmp ,即使它不再直接在范围内。

但是,由于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 作为参数x 复制foo中。

另一方面,JavaScript在处理对象时总是使用引用。 如果说,您用一个对象调用了foo ,则它返回的闭包将引用该原始对象!

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);

不出所料,每次调用bar(10)都会使x.memb递增。 可能不会想到的是, x只是与age变量引用相同的对象! 在两次致电barage.memb将为2! 此引用是HTML对象内存泄漏的基础。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!