傻瓜学习JavaScript闭包(译)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了傻瓜学习JavaScript闭包(译)相关的知识,希望对你有一定的参考价值。

在《高级程序设计》中,对于闭包一直没有很好的解释,在stackoverflow上翻出了一篇很老的《javascript closure for dummies》(2016)~

出处:http://stackoverflow.com/questions/111102/how-do-javascript-closures-work


 

闭包不是魔法

本文旨在用JavaScript代码让程序员理解闭包,函数式编程的程序员或者导师请绕行。

只要理解了闭包的核心理念,闭包并不难学。但是通过学习一些相关的学术论文或者学术方面的信息很难理解闭包。

本文是写给有主流语言编程经验的程序员的,至少应该能够看懂如下JavaScript函数:

1 function sayHello(name) {
2     var text = ‘Hello ‘ + name;
3     var say = function() { console.log(text); }
4     say();
5 }

 

闭包举例

两个一句话总结:

  • 闭包是函数的局部变量 —— 但是当函数return后,它仍旧有效
  • 闭包是一个函数return后仍不会释放的的堆栈结构(就像分配了一个堆栈结构而不是在一个栈上)

以下代码给函数返回了一个引用:

1 function sayHello2(name) {
2     var text = ‘Hello ‘ + name; // Local variable
3     var say = function() { console.log(text); }
4     return say;
5 }
6 var say2 = sayHello2(‘Bob‘);
7 say2(); // logs "Hello Bob"

大多数的JavaScript程序员能够理解,上述代码是如何返回给变量(say2)一个函数的引用的,如果你不理解的话,请在学习闭包前先弄懂它。C语言程序员可能认为这个函数是返回了一个函数的指针,并认为变量say和say2是两个指向了函数的指针。

C的函数指针和JavaScript的函数引用之间有关键性的区别:在JavaScript中你可以认为,一个函数引用变量既是一个指向了函数的指针,也是一个指向了闭包的隐性指针。

因为匿名函数  function() { console.log(text); } 是在另外一个函数( sayHello2() )里面声明的,所以以上代码包含一个闭包。在JavaScript中,如果你在另外一个函数里面使用 function 关键字,那么你就创造了一个闭包。

在C和大多数其他常见语言中,当一个函数return后,因为堆栈已经被清理了,所以所有的局部变量都无法再被访问。

在JavaScript中,如果你在另外一个函数中声明一个函数,那么当你调用一个函数并return后,这个局部变量仍旧是可访问的,上述代码已经证实过这一点:我们在sayHello2()函数return后调用了函数say()。

function() { console.log(text); } // Output of say2.toString();
通过say2.toString()的输出结果可以看到,代码指向了变量text。因为sayHello()的局部变量text被保存在一个闭包里,所以匿名函数可以引用值为‘Hello Bob‘的text。

魔法在于,在JavaScript中,一个函数引用会有一个隐式的、指向该函数被创建时所在的闭包的引用,就像是方法指针加上对一个对象的隐式引用一样。

更多举例

出于某些原因闭包有时看起来的确难以理解,但是当你学习一些例子的时候你就能够明白他们是怎样工作起来的了。我建议认真逐一的研究以下举例,直到你理解了他们是如何运作的。如果你在不完全理解闭包是怎样运作之前就使用他们,很快你就会创造一些非常诡异的bug~

例3

这个举例展示了:局部变量并没有被赋值,他们是通过引用被保持的。这有点像当外部函数存在时,保持了一个堆栈结构在内存里。

function say667() {
    // Local variable that ends up within closure
    var num = 666;
    var say = function() { console.log(num); }
    num++;
    return say;
}
var sayNumber = say667();
sayNumber(); // alerts 667

 

例4

因为三个全局函数在同一个调用setupSomeGlobals()的闭包中,所以他们有指向相同闭包的同一个引用。

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
    // Local variable that ends up within closure
    var num = 666;
    // Store some references to functions as global variables
    gLogNumber = function() { console.log(num); }
    gIncreaseNumber = function() { num++; }
    gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 667
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 666

oldLog() // 5

 

当三个函数被定义的时候,三个函数共享了对于相同闭包的访问,这个闭包是setupSomGlobals()的局部变量。

注意在以上距离中,如果你再次调用setupSomeGlobals() ,会创建一个新的闭包(堆栈空间)。旧的变量gAlertNumbergIncreaseNumbergSetNumber值会被有着新闭包的新函数覆盖。(在JavaScript中,无论什么时候在其他函数中声明了一个新函数,当外部函数每次被调用时,内部函数都会被创新创建)。

例5

对于很多人来说在本例都会犯错,所以你一定要理解闭包才行。当你在循环里面定义函数的时候一定要非常小心,因为闭包里的局部变量的表现通常不像你想的那样。

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]();
    }
}

行 result.push( function() {console.log(item + ‘ ‘ + list[i])} 三次向结果数组添加了一个指向匿名函数的引用,如果你不是很熟悉匿名函数的话,你可以认为这里匿名函数的作用类似如下代码:

pointer = function() {console.log(item + ‘ ‘ + list[i])};
result.push(pointer);

注意当你执行这个例子的时候,"item2 undefined"会被alert三次!这是因为如前例所述,这三次都是指向buildList的本地变量的同一个闭包。当行  fnlist[j](); 被执行时调用了匿名函数,这些匿名函数都使用了一个相同的闭包,他们都使用这个闭包的当前值i和item(i的值是3是因为循环已经结束了,也因此item的值是item2)。注意因为索引是从0开始的,因此item的值是item2,而i++会使i的值加到3。

例6

本例说明了,闭包中会包含任何在闭包存在前的、该闭包的外部函数内声明的局部变量。注意变量alice实际上是在匿名函数之后声明的,即匿名函数先被声明了。而因为alice与闭包在同一个作用域(JavaScript做了变量提升),所以调用的函数可以存取到alice变量。同样的,sayAlice()()直接调用了sayAlice()返回的引用,这与之前的例子其实是一样的,只是没有临时变量而已。

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = ‘Hello Alice‘;
    return say;
}
sayAlice()();

注意:变量say也在闭包之中,并且可以被任何在sayAlice()中声明的函数存取,也可以被函数内部的递归存取。

例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 +
            ‘\nanArray ‘ + anArray.toString() +
            ‘\nref.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;

Summary 小结

如果一切都似乎不是很清晰的话那么最好通过这些举例去详细理解他们。比起理解举例来说,读懂其中的说明更加困难。我对闭包和堆栈结构等的解释在技术上来说并不严格正确,但是他们能够更有效简单的帮助我们理解闭包。当完全掌握这些基础理念后再着眼于细节会更有帮助。

结论:

  • 任何时候在另外一个函数里面使用函数,都使用了一个闭包。
  • 任何时候在函数内使用eval(),都使用了一个闭包。eval的内容指向了当前函数的局部变量,同事,在闭包内甚至可以通过用eval(‘var foo = ...‘)创建一个新的局部变量。
  • 当在函数中使用使用 new Fucntion(...)(Function构造器,Function constructor)的时候,不会产生一个闭包。(新函数不能够引用外部函数的局部变量)。
  • JavaScript里的闭包就像保持了一份所有局部变量的拷贝,就跟函数之前存在的时候一样。
  • 最好认为闭包总是在进入函数的时候创建的,并且函数内所有的局部变量都会被添加到该闭包里面。
  • 一个带有闭包的函数每次被调用时都会保持一组新的局部变量。(鉴于函数包含了一个内部声明的函数,并且通过返回或者外部引用或者其他某种方式,包含并保持了一个对于内部函数的引用)。
  • 两个函数或许看起来有相同的源文本,但是因为他们的“隐形”闭包而可能有完全不同行为。我认为JavaScript代码事实上没有办法确认一个函数引用是否存在一个闭包。。
  • 如果你在尝试做一些动态的源代码修改(比如: myFunction = Function(myFunction.toString().replace(/Hello/,‘Hola‘)); ),如果myFunction是一个闭包那么修改就可能不会生效(当然你不可能在运行时还会想着对源代码的字符串进行修改,但是总是有一些特殊的情况...)
  • 可以在函数里进行函数声明,并在函数声明里继续进行函数声明——这种方式你会得到超过一层的闭包。。。
  • 我认为通常来讲闭包是函数和捕获的变量的范围(term),但是请注意,我在本文中并没有使用这个定义!
  • 我推测JavaScript的闭包与通常来讲在函数式语言中的闭包并不一样。

Links

Thanks

If you have just learnt closures (here or elsewhere!), then I am interested in any feedback from you about any changes you might suggest that could make this article clearer. Send an email to morrisjohns.com (morris_closure @). Please note that I am not a guru on JavaScript — nor on closures.

 


 

以下是原文:

JavaScript Closures for Dummies

Submitted by Morris on Tue, 2006-02-21 10:19. Community-edited since.

Closures Are Not Magic

This page explains closures so that a programmer can understand them — using working JavaScript code. It is not for gurus or functional programmers.

Closures are not hard to understand once the core concept is grokked. However, they are impossible to understand by reading any academic papers or academically oriented information about them!

This article is intended for programmers with some programming experience in a mainstream language, and who can read the following JavaScript function:

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

An Example of a Closure

Two one sentence summaries:

  • a closure is the local variables for a function — kept alive after the function has returned, or
  • a closure is a stack-frame which is not deallocated when the function returns (as if a ‘stack-frame‘ were malloc‘ed instead of being on the stack!).

The following code returns a reference to a function:

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"

Most JavaScript programmers will understand how a reference to a function is returned to a variable (say2) in the above code. If you don‘t, then you need to before you can learn closures. A C programmer would think of the function as returning a pointer to a function, and that the variablessay and say2 were each a pointer to a function.

There is a critical difference between a C pointer to a function and a JavaScript reference to a function. In JavaScript, you can think of a function reference variable as having both a pointer to a function as well as a hidden pointer to a closure.

The above code has a closure because the anonymous function function() { console.log(text); } is declared inside another function, sayHello2() in this example. In JavaScript, if you use the function keyword inside another function, you are creating a closure.

In C and most other common languages, after a function returns, all the local variables are no longer accessible because the stack-frame is destroyed.

In JavaScript, if you declare a function within another function, then the local variables can remain accessible after returning from the function you called. This is demonstrated above, because we call the function say2() after we have returned from sayHello2(). Notice that the code that we call references the variable text, which was a local variable of the function sayHello2().

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

Looking at the output of say2.toString(), we can see that the code refers to the variable text. The anonymous function can reference text which holds the value ‘Hello Bob‘ because the local variables of sayHello2() are kept in a closure.

The magic is that in JavaScript a function reference also has a secret reference to the closure it was created in — similar to how delegates are a method pointer plus a secret reference to an object.

More examples

For some reason, closures seem really hard to understand when you read about them, but when you see some examples you can click to how they work (it took me a while). I recommend working through the examples carefully until you understand how they work. If you start using closures without fully understanding how they work, you would soon create some very weird bugs!

Example 3

This example shows that the local variables are not copied — they are kept by reference. It is kind of like keeping a stack-frame in memory when the outer function exits!

function say667() {
    // Local variable that ends up within closure
    var num = 666;
    var say = function() { console.log(num); }
    num++;
    return say;
}
var sayNumber = say667();
sayNumber(); // alerts 667

Example 4

All three global functions have a common reference to the same closure because they are all declared within a single call to setupSomeGlobals().

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
    // Local variable that ends up within closure
    var num = 666;
    // Store some references to functions as global variables
    gLogNumber = function() { console.log(num); }
    gIncreaseNumber = function() { num++; }
    gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 667
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 666

oldLog() // 5

The three functions have shared access to the same closure — the local variables of setupSomeGlobals() when the three functions were defined.

Note that in the above example, if you call setupSomeGlobals() again, then a new closure (stack-frame!) is created. The old gAlertNumbergIncreaseNumbergSetNumber variables are overwritten with new functions that have the new closure. (In JavaScript, whenever you declare a function inside another function, the inside function(s) is/are recreated again each time the outside function is called.)

Example 5

This one is a real gotcha for many people, so you need to understand it. Be very careful if you are defining a function within a loop: the local variables from the closure do not act as you might first think.

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]();
    }
}

The line result.push( function() {console.log(item + ‘ ‘ + list[i])} adds a reference to an anonymous function three times to the result array. If you are not so familiar with anonymous functions think of it like:

pointer = function() {console.log(item + ‘ ‘ + list[i])};
result.push(pointer);

Note that when you run the example, "item2 undefined" is alerted three times! This is because just like previous examples, there is only one closure for the local variables for buildList. When the anonymous functions are called on the line fnlist[j](); they all use the same single closure, and they use the current value for i and item within that one closure (where i has a value of 3 because the loop had completed, and item has a value of ‘item2‘). Note we are indexing from 0 hence item has a value of item2. And the i++ will increment i to the value 3.

Example 6

This example shows that the closure contains any local variables that were declared inside the outer function before it exited. Note that the variable alice is actually declared after the anonymous function. The anonymous function is declared first; and when that function is called it can access the alice variable because alice is in the same scope (JavaScript does variable hoisting). Also sayAlice()() just directly calls the function reference returned from sayAlice()— it is exactly the same as what was done previously, but without the temporary variable.

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = ‘Hello Alice‘;
    return say;
}
sayAlice()();

Tricky: note also that the say variable is also inside the closure, and could be accessed by any other function that might be declared within sayAlice(), or it could be accessed recursively within the inside function.

Example 7

This final example shows that each call creates a separate closure for the local variables. There isnot a single closure per function declaration. There is a closure for each call to a function.

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 +
            ‘\nanArray ‘ + anArray.toString() +
            ‘\nref.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;

Summary

If everything seems completely unclear then the best thing to do is to play with the examples. Reading an explanation is much harder than understanding examples. My explanations of closures and stack-frames, etc. are not technically correct — they are gross simplifications intended to help understanding. Once the basic idea is grokked, you can pick up the details later.

Final points:

  • Whenever you use function inside another function, a closure is used.
  • Whenever you use eval() inside a function, a closure is used. The text you eval can reference local variables of the function, and within eval you can even create new local variables by using eval(‘var foo = …‘)
  • When you use new Function(…) (the Function constructor) inside a function, it does not create a closure. (The new function cannot reference the local variables of the outer function.)
  • A closure in JavaScript is like keeping a copy of all the local variables, just as they were when a function exited.
  • It is probably best to think that a closure is always created just on entry to a function, and the local variables are added to that closure.
  • A new set of local variables is kept every time a function with a closure is called (given that the function contains a function declaration inside it, and a reference to that inside function is either returned or an external reference is kept for it in some way).
  • Two functions might look like they have the same source text, but have completely different behaviour because of their ‘hidden‘ closure. I don‘t think JavaScript code can actually find out if a function reference has a closure or not.
  • If you are trying to do any dynamic source code modifications (for example: myFunction = Function(myFunction.toString().replace(/Hello/,‘Hola‘));), it won‘t work if myFunction is a closure (of course, you would never even think of doing source code string substitution at runtime, but...).
  • It is possible to get function declarations within function declarations within functions — and you can get closures at more than one level.
  • I think normally a closure is the term for both the function along with the variables that are captured. Note that I do not use that definition in this article!
  • I suspect that closures in JavaScript differ from those normally found in functional languages.

Links

Thanks

If you have just learnt closures (here or elsewhere!), then I am interested in any feedback from you about any changes you might suggest that could make this article clearer. Send an email to morrisjohns.com (morris_closure @). Please note that I am not a guru on JavaScript — nor on closures.

以上是关于傻瓜学习JavaScript闭包(译)的主要内容,如果未能解决你的问题,请参考以下文章

译理解JavaScript闭包——新手指南

[译] 我从没理解过 JavaScript 闭包

[译]Javascript中闭包的各种例子

[译] Lua中的闭包

译10个机器学习的JavaScript示例

通过示例学习JavaScript闭包