理解 JavaScript 中的闭包

前言

继上一篇《理解 JavaScript 中的做用域》后,我又马上写下了这篇文章,由于这二者是存在关联的,在理解闭包前,你须要知道做用域。javascript

而对于那些有一点 JavaScript 使用经验的人来讲,理解闭包能够看作是某种意义上的重生,但这并不简单,你须要付出很是多的努力和牺牲才能理解这个概念。java

若是你理解了闭包,你会发现即使是没理解闭包以前,你也用到了闭包,但咱们要作的就是根据本身的意愿正确地识别、使用闭包。面试

什么是闭包

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

当函数能够记住并访问所在的词法做用域时,就产生了闭包,即使函数是在当前词法做用域以外执行。异步

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

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

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

不,不彻底是,但它是闭包中很重要的一部分:根据词法做用域的查找规则,它可以访问外部做用域。spa

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

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的内部做用域。code

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

其实按正常状况下,引擎有垃圾回收器用来释放再也不使用的内存空间,当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. 调用完毕后将结果输出到控制台。

感谢观看!

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

相关文章
相关标签/搜索