教你步步为营掌握JavaScript闭包

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

教你步步为营掌握JavaScript闭包_第1张图片
学习闭包的笔记(字丑莫怪).jpg

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

1、JavaScript代码的执行环境

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

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

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

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


   
         JavaScript创建元素
         
    
     
    


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

教你步步为营掌握JavaScript闭包_第2张图片
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://upload-images.jianshu.io/upload_images/1371984-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://upload-images.jianshu.io/upload_images/1371984-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://upload-images.jianshu.io/upload_images/1371984-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专门写一篇文章。

你可能感兴趣的:(教你步步为营掌握JavaScript闭包)