理解 Javascript 中的闭包 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
4ark
V2EX    前端开发

理解 Javascript 中的闭包

  •  
  •   4ark
    gd4ark 2019-01-20 18:27:29 +08:00 2272 次点击
    这是一个创建于 2541 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    继上一篇《理解 Javascript 中的作用域》后,我立刻写下了这篇文章,因为这两者是存在关联的,在理解闭包前,你需要知道作用域。

    而对于那些有一点 Javascript 使用经验的人来说,理解闭包可以看做是某种意义上的重生,但这并不简单,你需要付出非常多的努力和牺牲才能理解这个概念。

    如果你理解了闭包,你会发现即便是没理解闭包之前,你也用到了闭包,但我们要做的就是根据自己的意愿正确地识别、使用闭包。

    什么是闭包

    闭包的定义,你需要掌握它才能理解和识别闭包:

    当函数可以记住并访问所在的词法作用域时,就产生了闭包,即便函数是在当前词法作用域之外执行。

    下面用一些代码来解释这个定义:

    function foo(){ var a = 2; function bar(){ console.log(a); // 2 } bar(); } foo(); 

    很明显这是一个嵌套作用域,而bar的作用域也确实能够访问外部作用域,但这就是闭包吗?

    不,不完全是,但它是闭包中很重要的一部分:根据词法作用域的查找规则,它能够访问外部作用域。

    下面再来看这段代码,它清晰地使用了闭包:

    function foo(){ var a = 2; function bar(){ console.log(a); } return bar; } var baz = foo(); baz(); // 2 这就是闭包 

    由于bar的词法作用域能够访问foo的内部作用域,然后我们把bar这个函数本身当作返回值,然后在调用foo时把bar引用的函数赋值给baz(其实是两个标识符引用同一个函数),所以baz能够访问foo的内部作用域。

    而这里正是印证前面的定义:函数是在当前词法作用域之外执行。

    其实按正常情况下,引擎有垃圾回收器用来释放不再使用的内存空间,当foo执行完毕时,自然会将其回收,但闭包的神奇之处正是可以阻止这件事情的发生,因为内部作用域依然存在,bar在使用它。

    由于bar声明位置的原因,它涵盖了foo内部作用域的闭包,使得该作用域能够一直存活,以供bar在之后任何时间进行引用。

    bar依然有对该作用域的引用,而这个引用就叫做闭包。

    因此,当baz在调用时,它自然能够访问到foo的内部作用域。

    当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包的存在:

    function foo(){ var a = 2; function baz(){ console.log(a); } bar(baz); } function bar(fn){ fn(); // 2 这也是闭包 } 

    把内部函数baz作为fn参数传递给bar,当调用fn时,它能够访问到foo的内部作用域。

    传递函数也可以是间接的:

    var fn; function foo(){ var a = 2; function baz(){ console.log(a); } fn = baz; } foo(); fn(); // 2 这也是闭包 

    所以:

    无论通过何种方式将内部函数传递到所在的词法作用于之外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

    闭包的使用

    既然前面说闭包无处不在,那不妨看看几个平时经常看到的片段,看看闭包的妙用。

    function wait(message){ setTimeout(function timer(){ console.log(message); },1000); } wait("Hello, closure!"); 

    将一个内部函数(这里叫做timer)作为参数传递给setTimeout,而timer能够访问wait的内部作用域。

    如果你使用过jQuery,不难发现下面代码中也使用了闭包:

    function setupBot(name,selector){ $(selector).click(function activator(){ console.log("Activating:" + name); }) } setupBot("Closure Bot 1","#btn_1"); setupBot("Closure Bot 2","#btn_2"); 

    本质上无论何时何地,如果将函数( 访问它们各自的词法作用域)当作第一级的值类型并到处传递, 你就会看到闭包在这些函数中的应用。 在定时器、 事件监听器、Ajax 请求、 跨窗口通信、Web Workers 或者任何其他的异步( 或者同步)任务中, 只要使用了回调函数,实际上就是在使用闭包!

    再来看一个很经典的闭包面试题:

    for (var i=1; i<=5; i++){ setTimeout(function(){ console.log(i); },i*1000); } 

    正常情况下,我们对这段代码行为的预期是每秒一次输出 1~5。

    但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。

    为什么?

    首先解释 6 是从哪里来的,这个循环的终止条件是i不再<=5,所以当条件成立时,i等于 6。因此,输出显示的是循环结束时i的最终值。

    也就是我们陷入了一个这样的误区:以为循环中每个迭代在运行时都会复制一个i的副本,但根据作用域的工作原理,它们都共享同一个全局作用域,因此实际上只有一个i

    要使这段代码的运行与我们预期一致,解决方法如下:

    for (var i=1; i<=5; i++){ (function(j){ setTimeout(function(){ console.log(j); },j*1000); })(i) } 

    在这段代码中我们使用了IIFE,将i作为参数j传递进去,在每个迭代IIFE会生成一个自己的作用域,它们接受参数j不一样,所以这段代码能够符合我们预期地运行。

    还有别的解决方案吗?

    是的,使用 ES6 新出的let可以解决这个问题:

    for (let i=1; i<=5; i++){ setTimeout(function(){ console.log(i); },i*1000); } 

    我们仅仅把var替换为let就轻松地解决了该问题,原因如下:

    • for中有自己的块作用域(()是父级作用域,{}是子级作用域)。
    • 使用let能够创建块作用域的变量。

    好了,到现在你应该能够很容易地识别闭包,那么接下来,我们继续介绍闭包更高级的用法。

    假设我们有这样一个对象:

    var box = { age : 18, } console.log(box.age); // 18 

    然而这里有一个问题,那就是属性age可以随意改变,如果我们使用闭包,就可以实现私有化,将age属性保护起来,只做允许的修改。

    var box = (function (){ var age = 18; return { birthday : function(){ age++; }, sayAge : function(){ console.log(age); } } })(); box.birthday(); box.sayAge(); // 19 

    这样我们就保证age属性只能增加,而不能减少,毕竟没有人能够越活越年轻。

    注意:

    1. 其实对象也有方法可以控制属性的修改,但这里主要讲述闭包,就不过多赘述。
    2. 使用闭包能够轻松实现原本在 Javascript 较复杂的设计。

    后记

    其实当你理解了闭包之后,你就会发现一切都是那么的理所当然,就仿佛它本该如此。

    最后,如果你已经理解了闭包并且想练习一下,那么我可以出一道题目给你:

    实现一个add函数,功能:add(1)(2)(3); // 6

    难一点的:

    实现一个add函数,功能:add(3)(‘*’)(3); // 9

    有几点:

    1. add函数可以被无限调用。
    2. 调用完毕后将结果输出到控制台。

    感谢观看!

    注:此文为原创文章,如需转载,请注明出处。

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5692 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 34ms UTC 06:26 PVG 14:26 LAX 22:26 JFK 01:26
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86