美文网首页html5javascript前端
教你步步为营掌握JavaScript闭包

教你步步为营掌握JavaScript闭包

作者: 工程师milter | 来源:发表于2016-09-10 10:23 被阅读2006次

    毫无疑问,掌握闭包是学好JavaScript绕不过去的坎儿。但它也是JavaScript中的一个难点。为了搞定它,我再次采取了死磕战术,疯狂阅读在google上搜到的与闭包相关的文章,并认真做笔记,随着get的知识点越来越多,突然有一天,这些零散的知识点在我的大脑中互相连接了起来,形成了一张逻辑清晰的图谱。我知道,自己终于攻克了这一技术难点。本文就是对我学习过程的一个系统的总结,希望对学习JavaScript的你有帮助。

    学习闭包的笔记(字丑莫怪).jpg

    一、JavaScript代码是怎样执行的?

    1、JavaScript代码的执行环境

    JavaScript作为一个脚本语言,必须运行在一个宿主环境(host environment)中,宿主环境包含一个JavaScript解释器,负责将JavaScript代码翻译成本地机器指令执行。浏览器就是最常见的宿主环境。

    JavaScript语言本身没有输入输出,它只负责与宿主环境交互,而宿主环境负责与外界的通信。JavaScript与宿主环境的交互,依靠的是宿主环境提供的宿主对象。

    怎么理解这段话?一个程序语言自己竟然没有输入输出机制?

    下面以浏览器加载一个html文档为例来说明这个问题。

    <html >
       <head >
             <title>JavaScript创建元素</title>
             <script type="text/javascript">
                window.onload = function () {
                           var input = document.createElement("input");  
                           input.type = "button";  
                           input.value = "创建一个按钮";  
                           document.body.appendChild(input);
                   }
            </script>
        </head>
         <body>
        </body>
    </html>
    
    

    以上代码在浏览器中加载后的结果是这样的:

    javascript-create-button.png

    我们看到JavaScript之所以能够在浏览器中创建一个按钮,是因为它直接使用了两个对象:windowdocument。这两个对象不是JavaScript语言本身提供的,而是浏览器这个宿主环境提供的。我们的JavaScript代码通过操作doucument对象,向浏览器表达了添加一个按钮的意向,浏览器通过document对象收到并实现了我们的请求。

    如果将按钮看作JavaScript的输出,那么我们看到,这个输出必须通过宿主环境提供的宿主对象来完成,这就是我们说JavaScript本身没有输入输出的含义。

    2、JavaScript代码的上下文

    JavaScript代码必须在一个宿主环境中执行,浏览器也好,Node.js也好,对JavaScript来说,都只是一个宿主环境而已。宿主环境为JavaScript提供宿主对象,让JavaScript代码能够与宿主环境交互。

    宿主环境中的解释器负责具体执行JavaScript代码,那么,解释器是怎么解释执行JavaScript代码的呢?

    我们举例说明,假如我们有一个js文件,内容如下:

    var  global_var1 = 10;
    function  global_function1(parameter_a){
        var  local_var1 = 10 ;
        return  local_var1 + parameter_a + global_var1;
    }
    var global_sum = global_function1(10);
    alert(global_sum);
    

    如果想查看这段代码的执行结果,请点击:

    这里

    下面我们来一步一步说明解释器是如何执行这段代码的:

    (1).创建全局上下文

    解释器看到我们这段代码后,第一反应就是:来活了!

    在解释器眼中,global_var1、global_sum叫做全局变量,因为它们不属于任何函数。local_var1叫做局部变量,因为它定义在函数global_function1内部。global_function1叫做全局函数,因为它没有定义在任何函数内部。如果你是从Java转学JavaScript的,很可能会问,难道在函数内部还可以定义函数?答案是:YES!后面会细说。

    解释器首先扫描了这段代码,为执行这段代码做了一些准备工作——创建了一个全局上下文。
    这个全局上下文是什么呢?可以把它看成一个JavaScript对象,姑且称之为global_context。这个对象是解释器创建的,当然也是由解释器使用。我们的JavaScript代码是接触不到这个对象的。

    global_context对象大概是这个样子的:

           Variable_Object :{......},
            Scope          :[......],
            this           :{......}
    }```
    可以看到,global_context有三个属性,其中Variable_Object和this属性的值是一个对象,Scope属性的值是一个数组,下面我们分别来看。
    - Variable_Object(以下简称VO)
    VO是一个JavaScript对象,里面内容如下:
    `{
         global_var1:undefined
         global_function1:函数 global_function1的地址
         global_sum:undefined
    }`
    我们看到,解释器在VO中记录了变量全局变量global_var1、global_sum,但它们的值现在是undefined的,还记录了全局函数global_function1,但是没有记录局部变量local_var1。VO的原型是Object.prototype。
    - Scope
    global_context对象的Scope数组中的内容如下:
    `[     global_context.Variable_Object     ]`
    我们看到,Scope数组中只有一个对象,就是前面刚创建的对象VO。
    - this
    this的值现在是undefined,不同的宿主环境有不同的实现,在有的浏览器中,这个this会指向window对象。
    
    global_context对象被解释器压入一个栈中,不妨叫这个栈为context_stack。现在的context_stack是这样的:
    
    
    ![context_stack.png](http:https://img.haomeiwen.com/i1371984/74d75817b488aa43.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    
    创建出global_context后,解释器又偷偷摸摸干了一件事,它给global_function1设置了一个内部属性,也叫scope,它的值就是global_context中的scope!
    也就是说,现在:
    
    `global_function1.scope === [  global_context.Variable_Object   ];`
    
    然而,由于解释器是背着我们干的这件事,我们是获取不到global_function1的scope属性的,只有解释器自己能获取到。
    
    **你猜,解释器这是要闹哪样?**猜不出来不要紧,咱们骑驴看唱板——走着瞧!
    
    **(2).逐行执行代码**
    
    解释器在创建了全局上下文后,就开始执行这段代码了。
    
    **(一)第一句:**
    
    `var  global_var1 = 10;`
    
    解释器会把VO中的global_var1属性的值设为10。现在global_context对象变成了这样:
    
    ```global_context = {
           Variable_Object :{ 
                   global_var1:10,
                   global_function1:函数 global_function1的地址,
                   global_sum:undefined
            },
            Scope          :[ global_context.Variable_Object ],
            this           :undefined
    }```
    
    然后,解释器继续执行我们的代码,它碰到了声明式函数global_function1,由于在创建global_context对象时,它就已经记录好了该函数,所以现在它什么也不用做。
    
    **(二)第二句:**
    
    然后,解释器继续前进,它碰到了语句:
    
    `var global_sum = global_function1(10);`
    
    解释器看到,我们在这里调用了函数global_function1(解释器已经提前在global_context的VO中记录下了global_function1,所以它知道我们这里是一个函数调用),并且传入了一个参数10,函数的返回结果赋值给了全局变量global_sum。
    
    解释器并没有立即执行函数中的代码,因为它要为函数global_function1创建一个专门的context,我们叫它执行上下文(execute_context)吧,因为每当解释器要执行一个函数时,都会创建一个类似的context。
    
    execute_context也是一个对象,并且与global_context还很像,下面是它里面的内容:
    ```execute_context = {
           Variable_Object :{ 
                   parameter_a:10,
                   local_var1:undefined,
                   arguments:[10]              
            },
            Scope          :[execute_context.Variable_Object, global_context.Variable_Object ],
            this           :undefined
    }```
    
    我们看到,execute_context与global_context相比,有以下几点变化:
    - VO
    VO中,首先记录了函数的形式参数parameter_a,并且给它赋值10,这个10就是我们调用函数时传递进去的。然后记录了函数体内的局部变量local_var1,它的值还是undefined。然后是一个arguments属性,它的值是一个数组,里面只有一个10。
    你可能疑惑,不是已经在parameter_a中记录了参数10了吗,为什么解释器还要搞一个arguments,再来记录一遍呢?原因是如果我们这样调用函数:
    `global_function1(10,20,30);`
    在JavaScript中是不违法的。此时VO中的arguments会变成这样:
    `arguments:[10,20,30]`
    parameter_a的值还是10。可见,arguments是专门记录我们传进去的所有参数的。
    
    - Scope
    Scope属性仍然是一个数组,只不过里面的元素多了个execute_context.Variable_Object,并且排在了global_context.Variable_Object前面。
    解释器是根据什么规则决定Scope中的内容的呢?
    答案非常简单:
    `execute_context.Scope = execute_context.Variable_Object + global_function1.scope。`
    也就是说,每当要执行一个函数时,解释器都会将执行上下文(execute_context)中Scope数组的第一个元素设为该执行上下文(execute_context)的VO对象,然后取出**函数创建时**保存在函数中的scope属性,将其添加到执行上下文(execute_context)Scope数组的后面。
    我们知道,global_function1是在global_context下创建的,创建的时候,它的scope属性被设置成了global_context的Scope,里面只有一个global_context.Variable_Object,于是这个对象被添加到execute_context.Scope数组中execute_context.Variable_Object对象后面。
    这里我们看到了一个很重要的知识点,就是任何一个函数在创建时,解释器都会把它所在的执行上下文或者全局上下文的Scope属性对应的数组设置给函数的scope属性,这个属性是函数**“与生俱来”**的。
    
    - this
    this的值此时仍然是undefined的,但不同的解释器可能有不同的赋值,我们这里就是undefined。this的值不是本文重点,我们不做深究。
    
    解释器为函数global_function1创建好了execute_context(执行上下文)后,会把这个上下文对象压入context_stack中,所以,现在的context_stack是这样的:
    
    ![context_stack-1.png](http:https://img.haomeiwen.com/i1371984/ce8c8ebe8f532b50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    做好了准备工作,解释器开始执行函数里面的代码了,此时我们称函数是在执行上下文中运行的。
    
    它首先碰到了语句`  var  local_var1 = 10 ;`它的处理办法很简单,将execute_context的VO中的local_var1赋值为10。这一点与在global_context下执行的变量赋值语句的处理一样。此时的execute_context变成这样:
    ```execute_context = {
           Variable_Object :{ 
                   parameter_a:10,
                   local_var1:10,                      //为local_var1赋值10
                   arguments:[10]              
            },
            Scope          :[execute_context.Variable_Object, global_context.Variable_Object ],
            this           :undefined
    }```
    然后解释器继续执行,发现了语句`return local_var1 + parameter_a + global_var1;`
    
    解释器进一步考察语句,发现这是一个返回语句,于是它开始计算return 后面的表达式的值。
    
    在表达式中它首先碰到了变量local_var1,它首先在execute_context的Scope中依次查找,在第一个元素execute_context的VO发现了local_var1,并且知道它的值是10,然后解释器继续前进,碰到了变量parameter_a,它如法炮制,在execute_context的VO中发现了parameter_a,并且确定它的值是10。
    
    解释器好高兴啊,已经确定两个变量的值了,只要再确定一个变量的值,把三个变量值相加返回,这个函数就执行完了!
    
    然后它碰到了变量global_var1,它还是从execute_context中调出Scope数组,从它的第一个元素execute_context.VO中查找,结果这次它失望了!没有发现global_var1。好桑心有木有!
    
    解释器没有气馁,它继续查看Scope数组的第二个元素,即global_context.VO,终于在里面发现了global_var1,并且确定了它的值为10。于是,解释器将三个变量值相加得到了30,然后就返回了。
    
    此时,解释器知道函数已经执行完了,那么它为这个函数创建的执行上下文也没有用了,于是,它将execute_context从context_stack中弹出,由于没有其他对象引用着execute_context,解释器就把它销毁了。现在context_stack中又只剩下了global_context。
    
    **(三)第三句:**
    
    现在解释器又回到全局上下文中执行代码了,这时它要把30赋值给sum,方法就是更改global_context中的VO对象的global_sum属性的值,对此我们已经很熟悉了。
    
    **(四)第四句:**
    
    解释器继续前进,碰到了语句`alert(global_sum);`很简单,就是发出一个弹窗,弹窗的内容就是global_sum的值30,当我们点击弹窗上的**确定**按钮后,解释器知道,这段代码终于执行完了,它会打扫战场,把global_context,context_stack等资源全部销毁。
    
    #二、炮声一响,闭包出场!
    
    现在,知道了上下文,函数的scope属性的知识后,我们就可以开始学习闭包了。让我们将上面的js代码改成这样:
    

    var global_var1 = 10;
    function global_function1(parameter_a){
    var local_var1 = 10 ;
    function local_function1(parameter_b){
    return parameter_b + local_var1 + parameter_a + global_var1;
    }
    return local_function1 ;
    }
    var global_sum = global_function1(10);
    alert(global_sum(10));

    
    想查看这段代码的执行结果,请点击:
    
    [这里](http://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=es2015%2Creact%2Cstage-2&code=)    后将代码粘贴进左侧输入栏。
    
    这段代码与原先的代码最大的不同是,在global_function1内部,我们创建了一个函数local_function1,并且将它作为返回值返回了。
    
    当解释器执行函数global_function1时,仍然会为它创建执行上下文,只不过此时,execute_context.VO中多了一个属性local_function1,它的值是一个函数。
    然后,解释器就会开始执行global_function1中的代码。我们直接从创建local_function1语句开始分析,看解释器是怎么执行的,闭包的所有秘密就隐藏在其中。
    
    当解释器在execute_context中执行创建local_function1时,它仍然会将execute_context的Scope设置给函数local_function1的scope属性,也就是这样:
    `local_function1.scope = [ execute_context.Variable_Object,   global_context.Variable_Object ]`
    
    然后,解释器碰到了返回语句,把local_function1返回并赋值给了全局变量global_sum。此时global_context的VO中global_sum的值就是函数local_function1。
    
    此时,函数global_function1已经执行完了,解释器会怎么处理它的execute_context呢?
    
    首先,解释器会把execute_context从context_stack中弹出,但并不把它完全销毁,而是保留了execute_context.Variable_Object对象,把它转移到了另一块堆内存中。为什么不销毁呢?因为还有对象引用着它呢。引用链如下:
    
    
    
    ![引用链.png](http:https://img.haomeiwen.com/i1371984/abfad0a22f09d724.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    这意味着什么呢?这说明,当global_function1结束返回后,它的形式参数parameter_a,局部变量local_var1以及局部函数local_function1都没有销毁,还仍然存在。这一点,与我们在面向对象的语言Java中的经验完全不同,这也是闭包难以理解的根本所在。
    
    下面我们的解释器继续执行语句`alert(global_sum(10));`alert参数是对函数global_sum的调用,global_sum的参数为10,我们知道函数global_sum的代码是这样的:
    

    function local_function1(parameter_b){
    return parameter_b + local_var1 + parameter_a + global_var1;
    }

    要执行这个函数,解释器仍然会为它创建一个执行上下文,我们姑且称之为local_context2,这个对象的内容是这样的:
    
    ```execute_context2 = {
           Variable_Object :{ 
                   parameter_b:10,
                   arguments:[10]              
            },
            Scope          :[execute_context2.Variable_Object, execute_context.Variable_Object, global_context.Variable_Object ],
            this           :undefined
    }```
    这里我们重点看看Scope属性,它的第一个元素毫无疑问是execute_context2.Variable_Object,后面的元素是从local_function1.scope属性中获得的,它是在local_function1创建时所在的执行上下文的Scope属性决定的。
    
    创建的execute_context2压入context_stack后,解释器开始执行语句`   return parameter_b  + local_var1 + parameter_a + global_var1;`
    
    对于该句中四个变量,解释器确定它们的值的办法一如既往的简单,首先在当前执行上下文(也就是execute_context2)的Scope的第一个元素中查找,第一个找不到就在第二个元素中查找,然后就是第三个,直至global_context.Variable_Object。
    
    我们可以预料,四个变量都可以经过这样的一番查找确定自己的值。
    
    然后,解释器就会将四个变量值相加后返回。弹出execute_context2,此时execute_context2已经没有对象引用着它,解释器就把它销毁了。
    
    最后,alert函数会收到值40,然后发出一个弹窗,弹窗的内容就是40。
    程序结束,解释器负责清扫现场。
    
    说到现在,啥是闭包啊?
    
    简单讲,当我们从函数global_function1中返回另一个函数local_function1时,由于local_function1的scope属性中引用着为执行global_function1创建的execute_context.Variable_Object对象,导致global_function1在执行完毕后,它的execute_context.Variable_Object对象并不会被回收,此时我们称函数local_function1是一个闭包,因为它除了是一个函数外,还保存着创建它的执行上下文的变量信息,使得我们在调用它时,仍然能够访问这些变量。
    
    函数将创建它的上下文中的VO对象**封闭包含**在自己的scope属性中,函数就变成了一个闭包。从这个广泛的意义上来说,global_function1也可以叫做闭包,因为它的scope内部属性也包含了创建它的全局上下文的变量信息,也就是global_context.VO
    
    如果这篇文章对你有帮助,请帮我点个赞,我会根据大家的热情程度,决定是否就this专门写一篇文章。

    相关文章

      网友评论

      • SOTA:感觉我要多读几遍才能理解
      • 9d13dac0b326:仔细阅读即便不懂得地方,然后又请教了做前端的同事,这才明白点,还需再读几遍这个文章,最后的总结非常好啊!!!
        工程师milter: @_壮志凌云_ 你同事是高人!
      • 9d13dac0b326:后面有点反应不过来了~~
      • e17c3a292957:小白表示看懂了,,6666
        工程师milter: @幻00城 那我的目的也就达到了
      • i7eo:跟楼主一样做了很多调查,不过昨晚回顾高程3的时候,看到“闭包保存了整个VO而不是某个特殊值”才开始顿悟。早上看到楼主文章回顾一遍很棒。声明函数叫VO执行时就叫AO啦 :grin:
        i7eo:@milter 期待新作品,相互学习 :fist:
        工程师milter: @LazyGeorge 受教了!
      • 8bf590593297:我差点看懂了,至少知道为啥函数执行完后变量不被销毁了
      • 落魄的安卓开发:楼主牛逼
        工程师milter: @thc 说明你认真看了😏
      • 梦想怪:很好(✪▽✪),学到了。谢谢(*°∀°)=3
        工程师milter: @梦想怪 😏
      • 落丨木:不错
      • a3106a4eac54:有点点懵懂~
        工程师milter: @SevenSevenLiu 还有一点点清醒?
      • 假如真:先收藏再看
        工程师milter: @假如真 聪明人!
      • jkhmnk:解析的这一过程分析的很好,但是说到闭包还是缺少一些案例来帮助说明,所以闭包这块还是总感觉少了些什么没理解透。 :sob:
        工程师milter: @jkhmnk 理解了原理,所有的案例都是浮云,网上的例子大把大把的
      • 初临:很深刻
        工程师milter:@damonvip 只是幽了一默而已,至于你这样吗?
        哼哼哈哈好:@milter 真不要脸,一唱一和
        工程师milter:@初临 可惜像你这么有眼光,有技术追求的人太少!
      • 孔二二:谢谢分享,虽然我看不懂
      • 孔二二:这样拐弯抹角夸自己的行为表示…
        工程师milter: @孔二二 😏
      • 孔二二:喜欢,虽然看不懂
        工程师milter: @孔二二 说明你审美好!
      • 一杯半盏:写的很详细,尤其是js的Context,刨根问底!写这篇文章最快需要1个小时吧。
        工程师milter: @无限卷积 写不是问题,问题是知道怎么写
      • EitanLiu:手写笔记👍
      • 向右奔跑:这么长的链接显示出来,阅读体验不太好
        工程师milter: @向右奔跑 如何弄短链接,这是什么黑科技?
        向右奔跑:@milter 试一下转在短链接
        工程师milter: @向右奔跑 没办法这个链接用简书mark down语法无法创建

      本文标题:教你步步为营掌握JavaScript闭包

      本文链接:https://www.haomeiwen.com/subject/pholettx.html