Javascript面向对象编程指南(五)——闭包

闭包

在我们讨论闭包之前,最好先来回顾以下Javascript中作用域的概念,然后再进行某些话题拓展。

作用域链

尽管在Javascript中不存在大括号级的作用域,但它有函数作用域,也就是说,在某函数内定义的所有变量在该函数外是不可见的。但如果该变量是在某代码块中被定义的(如在某个if或for语句中),那它在代码块外是可见的。

var a = 1;
function f() {
  var b = 1;
  return a;
}

f();  // 1
b;  // b is not defined

在这里,变量a是属于全局域的,而变量b的作用域就在函数f()内了。所以:

  • 在f()内,a和b都是可见的;
  • 在f()外,a是可见的,b则是不可见的。

在下面的例子中,如果我们在函数outer()定义了另一个函数inner(),那么,在inner()中可以访问的变量即来自它自己的作用域,也可以来则其“父级”作用域。这就形成了一条作用域链,该链的长度(或深度)则取决于我们的需要。

var global = 1;
function outer() {
  var outer_local = 2;
  function inner () {
    var inner_local = 3;
    return inner_local + outer_local +global;
  }
return inner();
}
outer();  // 6

利用闭包突破作用域

现在,让我们想通过图示的方式来介绍以下闭包的概念。让我们通过这段代码了解其中的奥秘。

var a = "global variable";
var F = function () {
  var b = "local variable";
  var N = function () {
    var c = "inner variable"
  }
}

首先当然就是全局作用域G,我们可以将其视为包含一切的宇宙。


其中包含各种全局变量(a1,a2),和函数(如F)。


每个函数也都会拥有一块自己的私有空间,用以储存一些别的变量(例如b)以内部函数(例如N)。所以,我们最终可以把示意图化成这样。

再上图中,如果我们在a点,那么就位于全局空间中。而如果是在b点我们就在函数F的空间里,在这里我们即可以访问全局空间,也可以访问F空间。如果我们在c点,那就位于函数N中,我们可以访问的空间包括全局空间,F空间和N空间。其中,a和b之间是不联通的,因为b在F以外是不可见的。但是如果愿意的话,我们是可以将c点和b点联通起来,或者说将N于b联通起来。当我们将N的空间拓展到F以外,并止步于全局空间以内时,就产生了一件有趣的东西——闭包。


知道接下来会发生什么吗?N将会和a一样置身全局空间。而且由于由于函数还记得它在被定义时所设定的环境,因此它依然可以访问F空间,并使用b。这很有趣,因为现在N和a处于同一空间,但是N可以访问b,而a不能。

那么,N究竟时如何突破作用域的呢?我们只需要将它升级为全局变量(不使用var语句)或通过F传递给(或返回)给全局空间即可。下面,我们来看看具体时怎么实现的。

闭包#1

首先我们来看一个函数。这个函数与之前所描述的一样,之不过在F中多了返回N,而在函数N中多了返回变量b,N和b都可以通过作用域链进行访问。

var a = "global variable";
var F = function () {
  var b = "local variable";
  var N = function () {
    var c = "inner local";
    return b;
  };
  return N;
}

函数F中包含了局部变量b,因此后者在全局空间里是不可见的。

b;  // b is not defined

函数N有自己的私有空间,同时也可以访问F()的空间和全局空间 ,所以b对它来说是可见的。因为F()是可以在全局空间中被调用的(它是一个全局函数),所以我们可以将它的返回值赋值给另一个全局变量,从而生成一个可以访问F()私有空间的新全局函数。

var inner = F();
inner();  // "local variable"

闭包#2

下面这个例子的最终结果与之前相同,但是实现方法上存在一些细微的不同。在这里F()不再返回函数了,而是直接在函数体内创建一个新的全局函数inner()。

首相,我们需要声明一个全局函数的占位符。尽管这种占位符不是必须的,但是还是声明一下,然后,我们就可以将函数F()定义如下:

var inner;  // 占位符
var F = function () {
    var b = "local variable";
    var N = function () {
      return b;  
    };
    inner = N;
};

F()被调用时会发生什么:

F();

我们在F()中定义了一个新的函数N(),并且将它赋值给了全局变量inner。由于N()是在F()内部定义的,它可以访问F()的作用域,所以即使该函数后来审计成了全局函数,但它依然可以保留对F()作用域的访问权。

inner();
"local variable"

相关定义的闭包#3

事实上,每个函数都可以被认为是一个闭包。因为每个函数都在其所在域(即该函数的作用域)中维护了某种私有联系。但是大多数时候,该作用域在函数执行完之后就自行销毁了——除非发生一些有趣的事(比如像上一小节所述的那样)。导致作用域被保持。

根据目前的讨论,我们可以说,如果一个函数会在其父级函数返回之后留住对父级作用域的链接的话(如上例,F是N的父级函数,F返回之后,N依然可以访问F中的局部变量),相关的闭包就会被创建起来。但其实每个函数本身就是一个闭包,因为每个函数至少都会有访问全局作用域的权限,而全局作用域是不会被破坏的。

让我们再来看一个闭包的例子。这次我们使用的是函数参数。该参数与函数的局部变量没有什么不同,但是它们是隐式创建的(即它们不需要使用var来声明。)我们在这里创建一个函数,该函数返回一个子函数,而这个子函数返回的则是其父函数的参数。

function F(param) {
  var N = function () {
    return param;
  }
  param++;
  return N;
}

然后我们可以这样调用它:

var inner = F(123);
inner();  // 124

请注意,当我们的返回函数被调用时(N被赋值时函数并没有被调用,调用是在N被求值,也就是执行return N语句时发生的),param++已经执行过一次递增操作了。所以inner()返回的是更新后的值。由于,该函数所绑定的是作用域本身,而不是在函数定义时该作用域中的变量或变量当前所返回的值。

循环中的闭包

接下来,让我们来看看新手们在闭包问题上会犯哪些典型的错误。毕竟有闭包所导致的bug往往很难被发现。因为它们总是看起来一切正常。

让我来看一个三次的循环操作,它在每次迭代中都会创建一个返回当前循环序号的函数。该新函数会被添加到一个数组中,并最终返回。具体如下:

function F() {
  var arr = [], i;
  for (i = 0; i < 3; i++) {
    arr[i] = function () {
      return i;
    };
  }
  return arr;
}
//下面我们来运行一下函数,并将解惑复制给数组arr;
var arr = F();

现在我们拥有了一个包含三个函数的数组。你可以通过在每个数组元素后面加一对括号来调用它们。按通常的估计,它们英爱会依照循环顺序分别输出0,1,2,下面我们来运行:

arr[0] ();  // 3
arr[1] ()  // 3
arr[2]();  // 3

显然这并不是我们想要的结果。究竟是怎么回事呢?原来我们在这里创建了3个闭包,而它们都指向一个共同的局部变量i。但是闭包并不会i记录它们的值,它们所拥有的知识相关域在创建时的一个链接(即引用)。在这个例子中,变量i恰巧存在于定义这三个函数域中。对这三个函数中的任何一个而言,当它要去获取某个变量时,它会从其所在的域开始逐级寻找那个距离最近的i值。由于循环结束时i的值为3,所以这3个函数指向了一个共同的值。

为什么结果是3,而不是2呢?这也是一个值的思考的问题,它能帮助你更好的理解for循环,请自行思考:

function F() {
  var arr = [], i;
  for (i = 0; i < 3; i++){
    arr[i] = (function (x) {
      return function () {
        return x;
      }
    } (i) );
  }
  return arr;
}

这样就能获得我们所要的结果了:

var arr = F();
arr[0] ();  // 0
arr[1] ();  // 1
arr[2] ();  // 2

在这里,我们不再直接创建一个返回i的函数了,而是将i传递给另一个即时函数。在该函数中,i就被赋值给了局部变量x,这样一来,每次迭代中的x就会拥有各自不同的值了。

或者,我们可以定义一个”正常点的“内部函数(不使用即时函数)来实现相同的功能。要点是在每次迭代操作中,我们要在中间函数内将i的值”本地化“。

function F() {
  function binder(x) {
    return function() {
      return x;
    };
  }
  var arr = [], i;
  for (i = ; i < 3; i++){
    arr[i] = binder(i);
  }
  return arr;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 作用域和闭包是 JavaScript 最重要的概念之一,想要进一步学习 JavaScript,就必须理解 Java...
    劼哥stone阅读 1,143评论 1 13
  • 闭包可以说是javascript中最令人迷惑的概念了。需要我们在实践中去慢慢理解,在实际编码中,由于闭包的效率和会...
    六尺帐篷阅读 675评论 1 10
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zock阅读 1,039评论 2 6
  • ​明亮的星空里的那些星星 在城市的喧嚣中慢慢黯落 独自坐在角落的冰凉石头 坚定的甚至一丝不苟 那些无所阻挡的东风 ...
    程石阅读 182评论 0 1
  • 【R:阅读原文】 培养理财的能力首先要有理财意识,其次清理自己的资产情况,第三学习理财相关理论知识,第四多与他人交...
    谢荣75阅读 114评论 0 0