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

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


闭包不是魔法

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

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

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

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

闭包举例

两个一句话总结:

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

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

 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和大多数其他常见语言中,当一个函数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(); //
gSetNumber(5);
gLogNumber(); // var oldLog = gLogNumber; setupSomeGlobals();
gLogNumber(); // oldLog() //

当三个函数被定义的时候,三个函数共享了对于相同闭包的访问,这个闭包是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的闭包与通常来讲在函数式语言中的闭包并不一样。

相关链接

深入理解JavaScript闭包【译】的更多相关文章

  1. 我从来不理解JavaScript闭包,直到有人这样向我解释它...

    摘要: 理解JS闭包. 原文:我从来不理解JavaScript闭包,直到有人这样向我解释它... 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 正如标题所述,JavaScript闭包 ...

  2. 【译】理解JavaScript闭包——新手指南

    闭包是JavaScript中一个基本的概念,每个JavaScript开发者都应该知道和理解的.然而,很多新手JavaScript开发者对这个概念还是很困惑的. 正确理解闭包可以帮助你写出更好.更高效. ...

  3. 【转】深入理解JavaScript闭包闭包(closure) (closure)

    一.什么是闭包?"官方"的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.相信很少有人能直接看懂这句话,因为他描述 ...

  4. 全面理解Javascript闭包和闭包的几种写法及用途

    好久没有写博客了,过了一个十一长假都变懒了,今天总算是恢复状态了.好了,进入正题,今天来说一说javascript里面的闭包吧!本篇博客主要讲一些实用的东西,主要将闭包的写法.用法和用途.  一.什么 ...

  5. 深入理解JavaScript闭包(closure)

    最近在网上查阅了不少javascript闭包(closure)相关的资料,写的大多是非常的学术和专业.对于初学者来说别说理解闭包了,就连文字叙述都很难看懂.撰写此文的目的就是用最通俗的文字揭开Java ...

  6. 深入理解javascript闭包(一)

    闭包(closure)是Javascript语言的一个难点.也是它的特色,非常多高级应用都要依靠闭包实现. 一.什么是闭包? 官方"的解释是:闭包是一个拥有很多变量和绑定了这些变量的环境的表 ...

  7. 深入理解javascript闭包(一)

    原文转自脚本之家(http://www.jb51.net/article/24101.htm) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. ...

  8. 深入理解Javascript闭包概念

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部能够直接读取全局变量 ...

  9. 轻松理解JavaScript闭包

    摘要 闭包机制是JavaScript的重点和难点,本文希望能帮助大家轻松的学习闭包 一.什么是闭包? 闭包就是可以访问另一个函数作用域中变量的函数. 下面列举出常见的闭包实现方式,以例子讲解闭包概念 ...

随机推荐

  1. 如何完全卸载oracle11g?

    步骤一: 停止Oracle的所有服务. 步骤二: 运行%oracle_home%\app\Administrator\product\11.2.0\dbhome_1\deinstall \deinst ...

  2. C fgetc

    格式:int fgetc(FILE *stream); 这个函数的返回值,是返回所读取的一个字节.如果读到文件末尾或者读取出错时返回EOF. 位于stdio.h中.从流中读取字符,即从stream所指 ...

  3. servlet配置restful

    所需jar包如下,jar下载地址如下http://download.csdn.net/detail/zhouminglan1992/9730354 1.web.xml配置 <!--给servle ...

  4. Codeforces Round #389 (Div. 2,) B C

    考完复变之后沉迷联盟不能自拔...明天就开始抢救计组 ... B 一个人装错了键帽 选择几个pair 把pair里面的键帽交换 并且每个键帽最多可以换一次 给出按键序列和输出序列 判断是否可以 如果可 ...

  5. 使用Java开发高性能网站需要关注的那些事儿

    无论大型门户网站还是中小型垂直类型网站都会对稳定性.性能和可伸缩性有所追求.大型网站的技术经验分享值得我们去学习和借用,但落实到更具体的实践上并不是对所有网站可以适用,其他语言开发的网站我还不敢多说, ...

  6. PARALLEL PROCESSING

    COMPUTER ORGANIZATION AND ARCHITECTURE DESIGNING FOR PERFORMANCE NINTH EDITION

  7. iOS索引列开发详解

    做苹果开发的朋友在地区列表可能会遇到在页面的右侧有一列类似与导航的索引列,这次有机会遇到了,细细研究了一下,原来没有想象中的困难,只需要简单的几步就能做出自己的索引列.本来想和搜索条在一块讲解,后来考 ...

  8. 移动端rem 适配

    在 index.html 中添加如下代码 <script> let html = document.documentElement; window.rem = html.getBoundi ...

  9. C#类继承和接口继承时一些模棱两可的问题[转]

    原文地址:http://www.cnblogs.com/harleyhu/archive/2012/11/29/2794809.html 1.在father定义的方法若含有virtual关键字,chi ...

  10. 使用Canvas绘制背景图

    原文  http://www.imququ.com/post/use-canvas-as-background-image.html 最近iCloud Web的Beta版换了UI,整体风格变得和iOS ...