[闭包]该如何理解?

前言

说到闭包,实在是居家旅行破境渡劫摄魄迷魂必备良药!不吃不知道,一吃哇哇叫,下面咱们也去搞两盒试试。git

1、闭包是什么

闭包,一个近乎神话的概念,从字面上理解感受就像是一个比较封闭的东西,百度百科上的定义是:闭包就是可以读取其余函数内部变量的函数。github

而我我的比较倾向于这么理解:闭包就是一个封闭包裹了它所能使用的做用域的函数。闭包

这样看起来好像有点那个意思了,通俗的说就是:函数这个袋子把一些做用域装起来了,哪些做用域呢?这个函数做用域链上的做用域。异步

光说不写假帅气,下面来些例子瞧瞧:函数

1.1 函数传递

// 1.函数做为返回值
function foo() { 
    var a = 2; 
    function bar() {  
        console.log( a ); 
    } 
    return bar; 
} 

var f = foo(); 
f();   // 2 这就是闭包的效果,或者说f即bar函数就是一个闭包,它把a所在的做用域包了起来,以便本身随时使用
复制代码

上面的例子是将函数做为值返回,下面咱们换个方式试试(其实不管使用何种方式对函数类型的值进行传递,当函数在别处被调用时均可以观察到闭包)。工具

// 2.函数做为参数传递
function foo() { 
    var a = 2; 
    function bar() { 
        console.log( a );
    } 
    f(bar); 
} 

function f(fn) { 
    fn();  // 函数做为参数传递,也包裹了a的做用域,这也是闭包
}

foo();  // 2
复制代码
// 3.间接传递函数
var fn; 
function foo() { 
    var a = 2; 
    function bar() { 
        console.log( a ); 
    } 
    fn = bar; // 将bar分配给全局变量fn
} 

function f() { 
    fn(); // fn指向bar,bar包裹着a的做用域,这也是闭包
} 

foo(); 
f(); // 2
复制代码
// 4.回调函数,传递给JS引擎调用
function wait(message) { 
    setTimeout(function timer() { 
        console.log(message); 
    }, 1000); 
} 

wait( "Hello" );  // 'Hello'
// 将一个内部函数timer传递给setTimeout,timer具备涵盖wait做用域的闭包,所以还有对变量message的引用
复制代码

其实,在定时器、事件监听器、Ajax请求、跨窗口通讯、Web Workers或者任何其余的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。ui

因此不管经过何种手段将内部函数传递到所在的词法做用域之外,它都会持有对原始定义做用域的引用(包裹),不管在何处执行这个函数都会使用闭包。spa

tip: 词法做用域指由书写代码时变量所在的位置所决定的做用域。code

1.2 IIFE

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

以上这个当即执行函数是闭包吗?嗯,看起来应该是。cdn

但严格来说它并非闭包。为何?由于上面的函数并非在它自己的词法做用域之外执行的,它在定义时所在的做用域中执行,a是经过普通的词法做用域查找而非闭包被发现的。

尽管IIFE自己并非观察闭包的恰当例子,但它的确建立了闭包,而且也是最经常使用来建立能够被封闭起来的闭包的工具,后面咱们会讲到。

1.3 循环与闭包

说到这个循环闭包的例子,可谓是如影随形,惺惺相惜,让猿欲罢不能。

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

这个想必你们伙就算没吃过也见过这个猪是怎么跑的:以每秒一次的频率输出五次6,而不是每秒一次一个的分别输出1~5。

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

仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。但事实上,当定时器运行时即便每一个迭代中执行的是setTimeout(.., 0),全部的回调函数依然是在循环结束后才会被执行,所以会每次输出一个6出来。

到底是什么缘由致使这结果和咱们预想的不同呢?

缘由是咱们试图假设循环中的每一个迭代在运行时都会给本身“捕获”一个i的副本。可是根据做用域的工做原理,实际状况是尽管循环中的五个函数是在各个迭代中分别定义的,可是它们都被封闭在一个共享的全局做用域中,所以实际上只有一个i,因此都是在共享同一个i。

如何解决这个问题?

咱们设想一下若是每次循环函数都能将属于本身的i包裹起来,而后保存下来,那就须要闭包做用域,下面咱们试试:

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

这样行吗?答案是不行。为何?上面的确建立了五个封闭的做用域,但你们有没有注意到,但这个做用域是空的,它们并无将i包裹并存储起来,咱们依旧是引用外部的同一个全局i,因此这个封闭的做用域须要有本身的变量,用来在每一个迭代中储存i的值:

for (var i=1; i<=5; i++) { 
    (function() { 
        var j = i;   // 将i的值存储在闭包内
        setTimeout(function timer() { 
            console.log(j); 
        }, j*1000); 
    })(); 
}
复制代码

搞定!将timer传递给setTimeout,时间到后,JS引擎会调用timer函数,而后找到对应包裹起来的i,咱们还能够再改进一下:

for (var i=1; i<=5; i++) { 
    (function(j) {  // j参数也是属于函数隐式声明的变量
        setTimeout(function timer() { 
            console.log(j); 
        }, j*1000); 
    })( i ); 
}
复制代码

等等,解决这个问题的方法是每次迭代咱们都须要一个块做用域,那么用let来生成块做用域不就搞定了吗?

for(let i=1; i<=5; i++) {  // 使用let声明i
    setTimeout(function timer() {
        console.log(i);
    }, i*1000);
}
复制代码

但let的做用不只仅是生成块做用域,for循环头部的let声明还会有一个特殊的行为:变量i在循环过程当中不止被声明一次,每次迭代都会声明,随后的每一个迭代都会使用上一个迭代结束时的值来初始化这个变量。

这种每次迭代从新声明绑定的行为就相似这样:

for (var i=1; i<=5; i++) { 
    let j = i;  //每一个迭代从新声明j并将i的值绑定在这个块做用域内
    setTimeout( function timer() { 
        console.log(j); 
    }, j*1000); 
}
复制代码

这样一路看下来,感受闭包好像也不是那么神秘嘛,我我的理解的话会把以上概括为:只要发生了函数传递与调用,就会产生闭包。好了,了解了闭包是什么,那下面来看看它有什么用途。

2、闭包的应用

2.1 模块

闭包最大的做用莫过于建立模块了:

function betterModule() {
    var name = 'BetterMan';
    var arr = [1, 2, 3];
    function getName() {
        console.log(name);
    }
    function joinArr() {
        console.log(arr.join('-'));
    }
    return {
        getName: getName,
        joinArr: joinArr
    }
}

var foo = betterModule();
foo.getName();  // 'BetterMan'
foo.joinArr();  // '1-2-3'
复制代码

以上就是一个利用闭包来建立的模块,咱们来理一理这段代码:

首先,betterModule()只是一个函数,必需要经过调用它来建立一个模块实例。若是不执行外部函数,内部做用域和闭包都没法被建立。

其次,betterModule()返回一个用对象字面量语法{key: value, ...}来表示的对象,这个返回的对象中含有对内部函数而不是内部数据变量的引用,保持了内部数据变量是隐藏且私有的状态,能够将这个对象类型的返回值看做本质上是模块的公共API。

这个对象类型的返回值最终被赋值给外部的变量foo,而后就能够经过它来访问API中的属性,如foo.joinArr()

tip: 从模块中返回一个实际的对象并非必须的,也能够直接返回一个内部函数。jQuery就是如此,jQuery$标识符就是jQuery模块的公共API,但它们自己都是函数(因为函数也是对象,它们自己也能够拥有属性)。

以上的betterModule函数能够被调用任意屡次,每次调用都会建立一个新的模块实例;但若是咱们只须要一个实例时,能够对这个模式进行简单的改进来实现单例模式

var foo = (function betterModule() {
    var name = 'BetterMan';
    var arr = [1, 2, 3];
    function getName() {
        console.log(name);
    }
    function joinArr() {
        console.log(arr.join('-'));
    }
    return {
        getName: getName,
        joinArr: joinArr
    }
})();
复制代码

咱们将模块函数转换成了IIFE,当即调用这个函数并将返回值直接赋值给单例的模块实例foo。

2.2 柯里化

柯里化也用到了闭包,听起来有点高大上,那什么是柯里化呢?

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数且返回结果的新函数的技术,看起来是否是有点绕,下面看看例子:

function add(a, b, c) {
    return a + b + c;
}
console.log(add(1,2,3));  // 6

function newAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}
console.log(newAdd(1)(2)(3));  // 6
复制代码

看着例子对照着定义,看起来描述得仍是挺贴切的嘛,其实上面也是利用了闭包的功能绑定了参数的做用域,使得每次调用函数时能够访问上次所传入的参数。

3、闭包的注意事项

一般,函数的做用域及其全部变量都会在函数执行结束后被销毁。可是,在建立了一个闭包之后,这个函数的做用域就会一直保存到闭包不存在为止,由于闭包就是一个函数引用另一个函数的变量,由于变量被引用着因此不会被回收。这是优势也是缺点,没必要要的闭包只会徒增内存消耗,因此咱们在使用的时候须要注意这方面。

function add(x) {
  return function(y) {
    return x + y;
  };
}

var add3 = add(3);
var add5 = add(5);

console.log(add3(2));  // 5
console.log(add5(5));  // 10

// 须要手动释放对闭包的引用
add3 = null;
add5 = null;
复制代码

以上的add3add5都是闭包,它们共享相同的函数定义,可是保存了不一样的环境。在add3的环境中,x为3。而在add5中,x则为5,最后咱们经过null手动释放了add3add5对闭包的引用。

最后

若是到了这里你恍然大悟:原来在个人代码中已经处处都是闭包了,只是平时没注意到而已!那说明我这药方仍是有点效果的,若是真的如此,那就来波点赞关注吧,由于你的支持就是我最大的动力!

GitHub传送门
博客园传送门

相关文章
相关标签/搜索