什么是闭包
MDN中对闭包的描述是
闭包是由函数以及创建该函数的词法环境组合而成。
这个环境包含了这个闭包创建时所能访问的所有局部变量。
而es2017文档对词法环境的描述是这样的
A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
environment record(环境记录)记录相应环境中的形参,函数声明,变量声明等。外部的词法环境的引用可以为null,比如全局词法环境。
所以,其实我们可以这样理解
闭包 = 代码块 + 创建该代码块的上下文中数据。
如果以此理解闭包的话,其实下面就是一个闭包
其实它可以看做一个全局环境创建的匿名函数。而《JavaScript权威指南》中也说到:从技术的角度讲,所有的JavaScript函数都是闭包。
但这算是理论意义上的闭包,而我们日常所熟知的是实践意义上的闭包。
在汤姆大叔翻译的文章中指出
ECMAScript中,闭包指的是:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
理论上要实现闭包,对于要实现将局部变量在上下文销毁后仍然保存下来,基于栈的实现显然是不适用的(因为与基于栈的结构相矛盾)。因此在这种情况下,上层作用域的闭包数据是通过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。这种实现方式比基于栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 —— 是将数据存放在堆栈中还是堆中。
同一个父上下文中创建的闭包是共用一个[[Scope]]
而从上文提到EMACSscript所有函数理论上都是闭包。而创建函数的父级上下文的数据是保存在函数的内部属性 [[Scope]]
中的,同一个父上下文中创建的闭包是共用一个[[Scope]]
属性,就是所有的内部函数都 共享同一个父作用域 。
所以从这角度可以解释下面这个经典问题了
|
|
上面的函数中ret[i]保存的都是函数,并引用外部的词法环境的i值,形成多个闭包。可是所有的ret[i]()
都是共享同一个父作用域,也即是 i值都是一样的 。当调用 ret[i]()
的时候, test()
已经执行完毕,i值等于5,所以最后ret数组中函数的返回值都是5,而不是我们所希望的0、1、2、3、4。
要想得到我们想要的结果,我们可以做以下修改:
我们创建了一个test2函数,并赋值给ret[i]。此时test2函数也有自己的[[scope]]
。我们为每一个test2函数绑定了一个i,并以形参x的形式传入函数,则每一个test2函数共有同一个[[scope]]
(即ret[],变量i等)同时,也分别有了自己的变量x,而函数内部返回的是x为非i。
当然我们也可以尝试自己构建Execution Context和Activation Object来分析
应用场景
闭包,其实可以理解为在一定场景中通过额外设置一个独立的函数作用域,并能在其内部可以访问函数外面的变量,无论执行该函数时其外部的作用域是否已经销毁。
其实闭包其实可以适用于很多种情景,下面是一些常用的情景,可能比较局限,以后想到再回来补充~
- 封装变量
情况允许的时候,可以尽量使用闭包把全局变量包裹起来变成局部变量,提高访问数据的效率 - 捕获外部变量
在外部环境被销毁的时候,还能保存引用的外部变量为自己的作用域中,但不使用的时候最好将其设为null,解除引用。 - 模拟私有方法
上面提到外部不能直接访问函数内部定义的函数和变量,而我们也可以通过闭包将其暴露出去。123456789101112131415161718192021222324252627var obj=function(){var priv=0;function cal(i){priv=+i;console.log(priv);}return {add:function(){cal(1);},min:function(){cal(-1);},val:function(){console.log(priv);}}}//obj.add() //报错,obj()才是一个函数对象obj().add(); //1obj().add(); //1obj().val(); //0let count1=obj(); // 把obj()的返回值赋值给count1变量,obj()的返回值也为对象let count2=obj();count1.add();//1count1.val();//1count2.val();//0
方法内变量的生存周期取决于方法实例是否存在活动引用,如没有就销毁活动对象。
前三条执行语句的作用域都是独立的,因为函数中的变量没有存在活动引用。
第三四条语句相当于实例化两个对象,存在活动引用,所以在后三条语句执行后,同对象的共享同一个词法作用域,而不同对象间词法作用域不同。
闭包的性能优化
随意使用过多闭包可能会影响性能,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。像上文中闭包的实现方式,每次调用构造函数的时候,方法都需要重新赋值一遍。在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。所以我们其实可以使用原型链
补充——对相同作用域内创建的闭包共享同一个VO的理解
看这里看这里js学习笔记-闭包与共享作用域
最后
上文是我对闭包的一些知识点的整理和较为浅显的理解,可能有些地方有失偏颇,希望指正~
参考链接
JavaScript深入之闭包
深入理解JavaScript系列(16):闭包(Closures)
JavaScript之作用域与闭包详解
举例详细说明javascript作用域、闭包原理以及性能问题
JavaScript 循环添加事件时闭包的影响有哪些解法?