闭包是JavaScript语言的一大特点,可以说再开发的过程中,它无处不在,即使有时候我们都没有发现它的存在。接触JavaScript以来,虽然说是一直有对闭包了解,但是实际应用起来或者分析一些代码的时候却有些吃力。现在回过头总结,才又进一步的揭开了闭包的一层面纱。而在讲闭包之前,我们要先明白JavaScript中又一个重要的知识,那就是JavaScript的内存机制以及作用域,这两个是我们有效理解闭包原理的前提吧。前面我们已经讲了JavaScript的内存机制,那么在这里先理解什么是作用域和作用域链。
作用域与作用域链
之前我们已经了解了JavaScript内存以及垃圾回收的机制了,在讲作用域之前,其实还有一个知识点,那就是JavaScript的执行环境产生的变量对象。
变量对象
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。而每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但是解析器在处理数据时会在后台处理它们。
JavaScript的运行环境大概有三种:
- 全局环境:全局环境是最外围的一个执行环境。在web浏览器中,全局执行环境被认为是window对象,因此所有的全局变量和函数都会作为window对象的属性和方法创建的;
- 局部环境:通常是函数内部创建的执行环境。
- eval
变量对象的创建过程:
- 创建arguments对象。检查当前执行环境中的参数,建立该对象下的属性与属性值。
- 检查当前执行环境中的函数声明,即function关键字的函数声明,在变量对象中以函数名建立一个属性,其值是指向该函数所在内存地址的引用。若函数名已经存在,则改属性将会被新的引用说覆盖。
- 检查当前执行环境中的变量声明,在变量对象以变量名建立一个属性,并初始化为undefined。
我们经常说到的变量提升,函数优先提升就是这样一个过程,在每个执行环境中的变量对象被创建的时候都会经历这几个步骤。
而当某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的变量和函数定义也随之销毁,全局执行环境直到应用程序退出比如浏览器关闭时才会销毁。
作用域
了解了变量对象之后,我们在这里给作用域一个定义,作用域就是用来管理JavaScript引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称(这里指的是变量名和函数名)进行变量查找的一套规则。说的简单点就是作用域就是变量与函数的可访问范围,它控制着变量与函数的可见性与生命周期。
JavaScript代码的整个执行过程分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器执行完成,将代码编译成可执行代码,并确立作用域的规则。执行阶段由引擎完成,主要是执行可执行代码,执行上下文在这个阶段创建。
我们举一个简单粗暴的🌰解释一下引擎、编译器和作用域是如何协同工作的就明白了:
1 | var a = 10; |
JavaScript执行上面代码的时候会将其看成是两个声明,一个是编译阶段的定义声明,一个是引擎执行阶段的赋值声明,所以我们可以分解成:
1 | var a; |
- 编译阶段:“编译器"会询问"作用域”:当前的作用域中是否已经有变量a,如果有,那么"编译器"会忽略这个声明,继续进行编译;如果没有,那么它会要求“作用域”在当前的作用域声明一个新的变量,并命名为a;
- 执行阶段:“引擎"处理a = 10时,首先会询问"作用域”:当前的作用域中是否存在一个a的变量,如果存在,那么引擎就会使用这个变量,如果否,那么"引擎"会继续查找该变量。如果"引擎"最终找到了a变量,那么就将10赋值给它,否则"引擎"就会抛出一个异常。
- 总结一下变量赋值操作过程,即:首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时,引擎会在作用域中查找该变量,如果能够找到就对它赋值,否则就抛出异常。
编译阶段是在当前的作用域中声明变量,而引擎查找时,是在整个作用域中查找该变量。
作用域链
上面我们讲了,引擎在查找变量的时候,是在整个作用域中查找的,在当前作用域中找不到它会继续往上一层作用域中查找,直至到顶层作用域即全局作用域。而这一个过程中就产生了我们所说的作用域链,作用域链是在执行上下文创建的时候产生的。
我们说作用域是JavaScript编译执行的一套规则,那么作用域链就是这个规则的一种具体体现。
作用域链是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终是当前执行的代码所在环境的变量对象。而前面我们已经讲了变量对象的创建过程。作用域链的下一个变量对象来自包含环境即外部环境,这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
闭包
定义
理解了作用域与作用域链之后,我们再来看看闭包的一些定义:
MDN 对闭包的定义:
闭包是指那些能够访问独立(自由)变量的函数(变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以「记忆」它被创建时候的环境。
《JavaScript 权威指南(第6版)》对闭包的定义:
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包。
《JavaScript 高级程序设计(第3版)》对闭包的定义:
闭包是指有权访问另一个函数作用域中的变量的函数。
最后是阮一峰老师对闭包的解释:
由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成定义在一个函数内部的函数。
从以上定义我们可以总结,我们可以简单理解就是一个函数被一个外部函数所包含,根据上面我们讲到的作用域链只是我们可以知道,当前被包含的函数内部(即当前的执行环境)是有权访问它外部函数(外部的执行环境)的变量对象的,那么这个函数就是我们所说的闭包了。原来对闭包的理解就是这么的通畅,哈哈😆。
闭包的用途
我们都知道,当函数执行完毕的时候,局部的活动对象就会退出执行栈,随后就会被销毁,但是闭包则不是酱紫的。废话不多说,我们再来看一个🌰(图画的有点丑,将就着看吧哈哈)
1 | function fun(){ |
上图展示的是调用函数fun()的过程中产生的作用域链之间的关系。作用域链本质上就是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。上面代码很明显,fun1()就是一个闭包,因为它的作用域链包含了外部函数fun()的活动对象,那为什么闭包在运行完之后不会被销毁呢?下面解释一下:
在函数fun()中,返回了内部函数fun1(),它的作用域链被初始化为包含fun()函数的活动对象和全局变量对象。因此,fun1()就可以访问fun()中定义的所有变量。接下来最重要的一点就是,fun()函数在执行完毕之后其活动对象也不会被销毁,因为闭包fun1()的作用域链仍然在引用这个活动对象。换句话说,当fun()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中,直到闭包fun1()被销毁后,fun()的活动对象才会被销毁。例如当上面的result被赋值为null时,此时闭包fun1()就被解除了引用,随后被垃圾收集器回收,最后释放其内存。
因此,可以总结出闭包有两大用途:1、可以读取函数内部的变量;2、可以让函数内部的变量始终保存在内存中。
注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存泄漏。