闭包是什么?密室逃脱,哇擦!有人在马桶下偷窥!

今天我想有别于其他作者,以密室逃脱主题来聊聊闭包这个鬼东西,made!当初就是他卡住了我进阿里的路,面试官嫌我技术深度不够,理解不透彻!功夫不负有心人!用了两天时间,本大神终于把这个闭包搞的,一塌糊涂了。。。额!
来,来,来!观众老爷们,跟我到小黑屋里聊聊,我们从变量->作用域->闭包这个姿势搞下,看本大神实力把观众老爷们,将这个闭包问题,说的彻彻底底,彻底迷糊了~_~!

变量

在js中,变量就分为两种:全局变量和局部变量

  • 全局变量:在全局起作用的变量,作用域为全局
  • 局部变量:在局部起作用的变量,作用域为局部

那么在js中,如何区别两者呢?js中,只有一种方法来声明局部变量,就是在方法体中,function里面;js没有java或c中的块级作用域的概念。简单说就是,在function里面的就是局部变量,在外面的就是全局变量。可以把function想象成电影里面那种密室,从里面可以看到外面,从外面看墙是镜子,只能看到自己。但在里面,既能知道里面有啥,还知道外面有啥。

其实还有一种特殊情况,局部变量中的局部变量,就是function嵌套;密室套密室,外层密室看内层密室,同样墙是面镜子,只能看到自己。

在ES6之前,只有函数能产生局部变量,函数就是小密室,它能把内部的东西隐藏起来,不被外部访问到;所以,在函数内部就是局部变量,在外部就是全局变量。ES6之后增加了像java一样的块级作用域,这在之前是没有的,有兴趣的可以去了解下。

1
2
3
4
5
6
7
function test(){
var a = 1;
}
console.log(a); //Uncaught ReferenceError: a is not defined
/*
a被声明在函数中,所以它是局部变量,在外部访问会报"ReferenceError"错误
*/

围绕变量就出现了作用域,作用域链两个东西

###作用域

  • 定义:程序源代码中定义这个变量的区域; (《js权威指南》一书中的描述)
  • 白话说:就是你定义的变量在哪个区域起作用,就是这个变量的作用域;
  • 打比方:把你比作变量,你现在所在的密室,能看到所有本密室内和本密室外的东西,但如果你所在密室里,还有一个小密室,你是看不到的,可能里面有人在偷窥你,我擦!我给你打一电话,问你,你能看到啥呀,你德比德比告诉了我密室里有啥,密室外有啥;但你没办法告诉我你密室里的小密室里有啥,你就能发挥这么点作用,这就是你的作用域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function a(){
var aa = 1;
function b(){
var bb = 2;
console.log('aa='+aa+';bb='+bb);
function c(){
var cc = 3 ;
console.log('aa='+aa+';bb='+bb+';cc='+cc);
}
c();//执行c
}
b();//执行b
}
a();//aa=1;bb=2 ,这是执行b方法打印出的
//aa=1;bb=2;cc=3 ,这是执行c方法打印出的
/*
可以看到b和c都可以访问aa,c可以访问bb,但是a和b是不能访问cc的,大家可以试下。像上面说的,只能从里往外看
*/

###作用域链(scope chain)

  • 百度百科:作用域链决定了哪些数据能被函数访问。当一个函数创建后,它的作用域链,会被“创建此函数的-作用域中-可访问的数据对象”,填充。
  • 白话说:就是你使用的变量,在哪个作用域里,去沿着一条线索,一条链去找,从局部作用域一级一级往上找,直到找到最上层,全局作用域,这个线索就是作用域链。
  • 打比方:接着你还是个变量,我把你var出来,把你var在里各种嵌套的某一层密室中,现在我有其中某层密室的钥匙,我想用你,但是又不知道你在哪?那么开始找,从我所在的密室开始往外一层一层的找你,只能往外,往内我不知道里面有啥也没钥匙,一直找到最外层,直到找到你。这一层一层密室,是一个个的function,上面说了,每个密室每个function里是一个单独的作用域,那么这样一层层的找你,所走过的路,所经历的域链起来,就是作用域链。

聊到这,相信观众老爷们应该知道变量,作用域,作用域链是个啥了吧,啥?还不知道?脸上笑眯眯,心里mmp,啥也不说了,找我单聊吧!

闭包

终于到了今天要聊的重点了,啰嗦了一大堆,闭包是个啥?!等等,让我按套路出牌

  • 定义:

    1. 闭包就是能够读取其他函数内部变量的函数,就是将函数内部和函数外部连接起来的一座桥梁;(阮一峰)
    2. 闭包是指有权访问另一个函数作用域中的变量函数;(《javascript高级程序设计》)
    3. 从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链;《javascript权威指南》
    4. 闭包Closure)是词法闭包Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外;(维基百科)
    5. 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行;(《你不知道的javascript》)

    为了写一遍假装很严谨、不误导大家的文章,我用了两天时间,查阅了部分大神的博客关于闭包的文章、查阅了多本javascript排名靠前的书籍,把其中一些定义给大家丢出来。大家对比下便可发现,不同的人对闭包这个东西,理解不太相同。

  • 吐槽:一些大神博客、一些入门级js书籍都对闭包这个概念说的很简单,说的很浅。可能出于对初学者的友好考虑,初学者开始不需要理解太深,甚至深了也理解不了。所以会出现这样的情况。

  • 引言:引用知乎上一位大神的话如下:

    为了通俗而通俗,为了容易理解而理解,只會使得读者同样流于表面,自以为理解,实际上根本不懂。一旦难度稍微加大,旧有的思維不再好用,就难以提升了。

    反之一開始就接受抽象思維的训练而不是套用旧有的思維,雖然一開始艰难,而後必會突飞猛進。

    建议楼主不要看任何低水平的「科普」文章,直接看英文維基百科、MDN、ECMAScript 草案比較好。

    真正的入門往往就是在一瞬間,過了這道坎,你就入門了。盲目地看再多低水平的文章,照樣毫無幫助。

    把这几天扣的这些个博客和书籍,再加上我的理解说下。我的理解是,闭包不能定义为一个具体的概念或函数,不完全同意阮大神的说法,我认为闭包是满足一些条件的状态,描述的是一种状态,而非是什么什么函数。满足以下两个条件,即形成闭包这个状态:

    1. 函数有嵌套 —>函数
    2. 内部函数使用了外部函数的变量 —> 环境

    函数加环境才形成闭包。函数定义时,记住了它所在的环境,也就是绑定了它当时所在作用域链。这时形成闭包状态;闭包有以下几个特性:

    1. 可以通过内层函数访问函数内的变量:
    1
    2
    3
    4
    5
    6
    7
    8
    function outer(){
    var a = 1;
    return function inner(){
    console.log('a='+a);
    }
    }
    var b = outer();
    b();// a=1

    白话说:我们上面聊作用域的时候说过,每一个function会创建一个独立的作用域,每一个function就是一个小密室,我站在最外面是无法知道密室里面有啥的,但是你在inner这个密室里,var b = outer()给我返回了你的联系方式,然后我就可以通过你,知道你作用域内的所有东西。

    1. 外层函数活动对象与作用域本该被垃圾回收,但由于被内层函数引用,持久化在内存中,不会销毁。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function outer(){
    var a = 1;
    return function inner(){
    console.log('a='+a++);
    }
    }
    var b = outer();
    b();// a=1
    b();// a=2
    b();// a=3
    b();// a=4

    白话说:如果不是闭包,一般函数在执行完以后,它本身包括它的执行上下文,作用域都是要被GC回收的,但是通过上面的例子可以看出,这个a变量是outer这个函数的,他早已执行完了,正常它应该被销毁的,但是我们在反复调用b(),发现a的确一直存在。假设我就是b,你是inner,我一直拿着你的联系方式,我需要你的时候,必须能联系上你,但你也不知道我什么时候会联系你,所以你是不能被干掉的,包括你的整个作用域。

    :这个特性是闭包最大的特点,也是最大的缺点,闭包所引用的变量都在内存中不被销毁,造成内存泄漏(无法释放已申请的内存),一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,最终结果内存溢出,进程死掉,程序挂掉,甚至机器宕掉。

    闭包加循环

    这块单独拿出来说下,闭包加循环有些特殊,我初学js时遇到一个坑

  • js从入门到放弃

    功能需求:通过书名循环请求某网站,将书的信息取回来

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function getBookData(books){
    var booksArr = books;//传入一个书名的数组,
    for(var i = 0 ; i<booksArr.length ; i++){ //循环
    request.get(booksArr[i],function(err,res,json){ //请求
    //【1】
    if (response.statusCode == 200){ //当返回状态码200请求成功
    console.log(json);//输出书挡信息
    }
    })
    }
    }

    这个问题在我js初级时,在公司写的业务(上面这段代码是直接运行不了的,我只想表达意思,截取了部分),我写的这段代码把我坑够呛。得到的结果跟我想的完全不一样。假设我要获取10本书的信息,最后得到的10本书,都是最后一本,10条信息一模一样,都是最后一本书的信息。我就理解不了,在上面【1】的位置我写了一句console.log(booksArr[i]),发现给我输出10个booksArr[9]。我擦,mmp!我请求了10遍bookArr[9]呗。在我当时的js水平,我最后只能理解为,js是异步的,由于request是去执行I/O操作去了,for循环又速度快,一下i 就变成最后一个。。。这样的想法明显经不起推敲,先自己骗自己。。。因为水平有限,暂时理解不了

  • 终于等到你

    我们看下上述问题一个简化的例子

    1
    2
    3
    4
    5
    6
    7
    8
    function test(){
    for(var i=0 ; i<10 ; i++){//循环
    setTimeout(function(){//模拟请求
    console.log(i)
    },1000)
    }
    }
    test();//(10)10 十个十

    将setTimeout函数的时间1000改为0,结果还是一样的。10个10;将上面循环拆开,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function test(){
    var i = 0 ;
    {
    var i=1;//在for循环结束后i=10
    setTimeout(function(){//模拟请求
    console.log(i)
    },1000)
    }
    {
    var i=2;//在for循环结束后i=10
    setTimeout(function(){//模拟请求
    console.log(i)
    },1000)
    }
    ...//一共10个
    {
    var i=9;//在for循环结束后i=10
    setTimeout(function(){//模拟请求
    console.log(i)
    },1000)
    }
    //以上是for循环
    i=10;//i=10 for 循环结束
    }
    test();//(10)10 十个十

    结果一样的,当这样拆开后相信大家就明白为什么了,js没有块级作用域的概念,使用for循环,循环定义了10个块,并且在块内保存了i变量,但是由于没有块级作用域,i都会被最后的i所覆盖。js异步设计就是这样,只要出现延时函数,哪怕0秒,只要出现i/o操作,那么会跳过往下执行,等待延时或i/o结束再回来执行内部。

    es6提出的块级作用域,只要用let定义的变量。所以上述问题只需要将var改为let即可避免。

    使用函数级作用域保存每个i也可以解决

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function test(){
    for(var i=0 ; i<10 ; i++){//循环
    (function(i){//只用匿名函数将每一个i传入并保存独立作用域
    setTimeout(function(){//模拟请求
    console.log(i)
    },1000)})(i)//立即执行函数写法
    }
    }
    test();//(10)10 十个十