javaScript中的闭包原理 (译)

这篇文章通过javaScript代码解释了闭包的原理,来让编程人员理解闭包。它不是写给大牛或使用功能性语言进行编程的程序员的。一旦意会了其核心概念,闭包理解起来并不难。然而,你不可能通过阅读任何有关闭包的学术文章或学术类的指导信息来弄明白它!

这篇文章是为有一些主流语言的编程经验,并且能读懂下面这段javaScript代码的编程人员准备的:

1 function sayHello(name) {
2     var text = 'Hello ' + name;
3     var say = function() { console.log(text); }
4     say();
5 }
闭包举例

       

      两句话总结:

  • 闭包是一个函数的局部变量,函数返回后不会消失,或
  • 闭包是函数返回后一个未释放的堆栈帧(就像一个“堆栈帧”被malloc分配,而不是放在堆栈上。

      下面的代码返回了一个函数引用:

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程序员会认为函数sayHello2会将函数say的指针返回给say2,并且变量saysay2是两个指向目标函数的不同指针。

      在这里,C函数指针和javaScript函数引用之间有着关键的区别。在javaScript中,你可以将函数引用变量看成既有一个指向函数的指针,又有一个指向闭包的隐藏指针。上面的代码中,由于在函数sayHello2中声明了另外一个匿名函数function(){console.log(text);},因此有一个闭包。在javaScript中,如果在一个函数中再使用function关键字,那你就是在创建一个闭包。

      在C和其他大多数常用语言中,当一个函数返回后,由于栈帧被销毁,所有的局部变量将不能再被访问。javaScript中,如果在一个函数中声明另一个函数,则当局部变量从你调用的函数返回后仍然可以访问。这点在上面已经证明了,因为在从sayHello2()返回后又调用了函数say2()。注意,我们称之为引用变量的代码text,是函数sayHello2()的一个局部变量。

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

      通过观察say2.toString()的输出,我们可以看到,该代码是指变量text。由于sayHello2()的局部变量仍保留在闭包中,因此匿名函数可以引用值为“Hello Bob”的变量text

      神奇之处在于,在javaScript中,函数引用也有一个对创建它的闭包的隐蔽引用——类似于委托(delegates)是一个方法指针加对一个对象的隐蔽引用。

 

  更多的例子

      

      出于某种原因,当你通过某些资料了解闭包的时候它们似乎很难理解,但是当你看一些例子的时候,你可以实际点击并了解它们的工作原理(这花了我一段时间来理解)。我建议你仔细研究一些有关闭包的例子,直到你能理解它们的工作原理。如果你在没有完全弄明白闭包工作原理的情况下就开始使用它的话,那你将会很快遇到一些非常奇怪的bug

 

例3

      

      这个例子说明,局部变量没有被复制——它们被引用保留了。就像当外部函数退出后仍然在内存中保留着一个栈帧。

1 function say667() {
2     // Local variable that ends up within closure
3     var num = 666;
4     var say = function() { console.log(num); }
5     num++;
6     return say;
7 }
8 var sayNumber = say667();
9 sayNumber(); // logs 667
例4

 

  因为都在一个setupSomeGlobals()的调用中声明,所以三个全局函数对同一个闭包有一个共同的引用。

 1 var gLogNumber, gIncreaseNumber, gSetNumber;
 2 function setupSomeGlobals() {
 3     // Local variable that ends up within closure
 4     var num = 666;
 5     // Store some references to functions as global variables
 6     gLogNumber = function() { console.log(num); }
 7     gIncreaseNumber = function() { num++; }
 8     gSetNumber = function(x) { num = x; }
 9 }
10 
11 setupSomeGlobals();
12 gIncreaseNumber();
13 gLogNumber(); // 667
14 gSetNumber(5);
15 gLogNumber(); // 5
16 
17 var oldLog = gLogNumber;
18 
19 setupSomeGlobals();
20 gLogNumber(); // 666
21 
22 oldLog() // 5

  当setupSomeGlobals()中定义三个函数时,这三个函数共享访问该闭包相同的局部变量。

  注意上面的例子,如果你再次调用setupSomeGlobals()的时候,就会创建一个新的闭包(堆栈帧)。已有的gLogNumbergIncreaseNumbergSetNumber旧变量会被新闭包的函数重新覆盖。(在javaScript中,当你在函数中声明一个内部函数,每次调用外部函数的时候都会重新创建一次内部函数)。

 

例5

 

  这个例子对很多人来理解起来回比较麻烦,所以你需要弄懂它。如果你在一个循环体中定义一个函数的时候要非常小心:闭包中的局部变量并不会像你刚开始想象的样子起作用。

 1 function buildList(list) {
 2     var result = [];
 3     for (var i = 0; i < list.length; i++) {
 4         var item = 'item' + i;
 5         result.push( function() {console.log(item + ' ' + list[i])} );
 6     }
 7     return result;
 8 }
 9 
10 function testList() {
11     var fnlist = buildList([1,2,3]);
12     // Using j only to help prevent confusion -- could use i.
13     for (var j = 0; j < fnlist.length; j++) {
14         fnlist[j]();
15     }
16 }
17 
18 result.push( function() {console.log(item + ' ' + list[i])}行向result数组添加了三次对匿名函数的引用。如果你对匿名函数不是太熟悉的话,可以这样理解:
19 pointer = function() {console.log(item + ' ' + list[i])};
20 result.push(pointer);   

  注意,当你运行这个例子的时候,将会出现三次“item2 undefined”警告!这是因为,就像前面的例子,这里只有一个局部变量buildList的闭包。当在fnList[j]()行中调用匿名函数的时候,每次都使用了同样的单个闭包,并且使用了对应闭包中iitem的当前值(由于已经循环完,因此i的值为3item的值为item2)。注意我们是从0开始索引的,因此item的值为item2i++会将i的值增加到3

 

例6

 

  这个例子说明,在退出前,闭包中包含了所有在外部函数内声明的局部变量。注意,变量alice实际上是在匿名函数后声明的。首先定义了匿名函数,因为alice和匿名函数在同一个范围(javaScript变量提前声明),所以当调用函数的时候能访问alice变量。同时,sayAlice()()只需要直接调用从sayAlice()返回的函数引用——这非常像之前完成(what was done previously),但是没有临时变量。

1 function sayAlice() {
2     var say = function() { console.log(alice); }
3     // Local variable that ends up within closure
4     var alice = 'Hello Alice';
5     return say;
6 }
7 sayAlice()();

  同时注意,变量say也在闭包中,可能会被在sayAlice()中声明的其他函数访问,或者在内部函数中被递归访问。

 

例7

 

  

  最后一个例子表明,函数的每次调用都会为局部变量创建一个单独的闭包。函数声明时不会有单独的闭包,每次调用函数时才会有单独的闭包。

 1 function newClosure(someNum, someRef) {
 2     // Local variables that end up within closure
 3     var num = someNum;
 4     var anArray = [1,2,3];
 5     var ref = someRef;
 6     return function(x) {
 7         num += x;
 8         anArray.push(num);
 9         console.log('num: ' + num +
10             '
anArray ' + anArray.toString() +
11             '
ref.someVar ' + ref.someVar);
12       }
13 }
14 obj = {someVar: 4};
15 fn1 = newClosure(4, obj);
16 fn2 = newClosure(5, obj);
17 fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
18 fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
19 obj.someVar++;
20 fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
21 fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
总结

  如果你对某件事不是完全明白,那最好的方法就是举些例子来理解它。读一些学术性的解释远远没有切实地理解一些例子来的容易。我对闭包和堆栈帧等概念的解释在技术上不完全科学——这些解释总的来说只是为了来帮助理解这些概念。一旦掌握了基本思想,之后你可以再具体理解细节部分。

 

最后观点

 

    l    当你在一个函数中声明另一个函数,就用到了闭包。

    l  当你在一个函数中使用了eval(),就用到了闭包。eval中的代码可以引用这个函数的局部变量,在eval中你甚至可以通过eval(‘var foo = …’)创建新的变量。

    l  当你在一个函数中使用new Function(…)(函数构造器)的时候,这时并没有创建闭包。(new出来的函数不能引用外部函数中的局部变量。)

    l  javaScript中的闭包就像一个保存着所有局部变量的副本,变量值为函数返回时对应的值。

    l  最好这样想,总是在进入一个函数的时候就创建了闭包,并且向该闭包中加入了局部变量。

    l  每次调用包含闭包的函数的时候,都会对应有一个新的局部变量集合(假设这个函数中包含了一个内部函数,并且返回了这个内部函数的引用或者以某种方式存在的该内部函数的外部引用)。

    l  两个函数可能看起来有相同的代码,但是由于它们隐藏的闭包,因此有着完全不同的行为。我不认为javaScript代码真的可以发现一个函数引用是否有闭包。

    l  如果你正在尝试动态修改源代码(例如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));,如果myFunction是一个闭包的话,该修改将不会起 作用(当然,你肯定不会在代码运行的时候进行源代码的修改,但是)。

    l  你可能在函数中的函数声明中获得函数声明,同时你可以在多个级别获得闭包。

    l  我认为,通常来说,一个闭包是函数和其包含的变量的术语。注意,在本文我没有用到这个概念。

   我怀疑javaScript中的闭包和那些在功能性语言中的闭包有所不同。

 

    译自:http://stackoverflow.com/questions/111102/how-do-javascript-closures-work 

       翻译的不太好,对英文和对技术还有很大的提升空间,后续如果发现某个句子有更好的翻译方法会继续改进。

原文地址:https://www.cnblogs.com/alkq1989/p/5590173.html