闭包是一个比较难理解的问题,下面就自已的理解写写
如果说闭包,首先还是牵涉到函数。函数在JS中是一个非常特殊的存在。原因在于,它经常在表面上迷惑,好像是一段代码,其实它也是对象。
对于这个问题,我们可以先从直观上了解一下,比如:
function f(){ } f.show=function(){ console.log('对象f的show方法') } f.show()
----#2020-10-25 23:02:16补充----:
对于上面这个方法,我们用另一种思路来理解,
f.show=function(){...},可以等价于f.show = new Function(....),这么写可以更直观的认识到函数是一种对象,这个思路很重要!
如果f不是对象,它怎么能挂载show属性呢?因此,在下面分析之前,首先达成一个认识:函数就是对象,当我们看到函数时,脑子中就把它当成let o={}这种普通对象来看待。
一旦得到这个结论,我们就会得到另一个令人惊奇的结论,既然是对象,那么对象就可以多份,就好比通过一个构造函数得到多个对象一样,这有点颠覆日常的认知,为什么会这样是因为
在我们的概念里,函数就是一段代码,被编译之后内存中只有一份。比如像C这种编译型的语言。但是,我们的推导从逻辑上来说也没有问题对吗?
我们推导的理由是:函数就是对象,而对象是可以多个的,所以函数可以在内存中多份。我们不管对不对,只是纯粹从逻辑上推导。
因为函数是对象,所以在一个函数里面返回一个函数,就好比返回一个‘普通对象’一样,这么说也是合乎逻辑的
这种基础的认识一直在重复,因为我觉得基础的认识要在脑子中形成本能,而且这是所有推导的核心依据
代码如下:
function f(){ //等价:return new Function('console.log("内部返回的对象")') return function(){ console.log('内部返回的对象') } } let a=f() let b=f() let c=f() console.log(a==b) //false console.log(a==c) //false console.log(b==c) //false
上面这段代码,调用f()就返回一个函数,但是前面说过,函数是对象,所以我们说这个函数f()就像工厂机器一样,调用一次就生产一个对象出来。代码中生产出来三个对象a,b,c,我们有理由相信这三个对象都是不同的。
于是,下面的判断都是false。这从侧面也证明了,函数就像对象一样,可以多个。
下面,在函数f()里面放置一个数据,代码如下:
function f(){ let data = 10 return function(){ console.log(data) } } let a=f() let b=f() let c=f() a() b() c()
因为a,b,c三个对象都不一样,那么从常理上来想,它引用的data也不一样,就像中秋节发月饼,每个人各领自己的一份,东西是独占的。这就是闭包的基本概念,把[对象]+[引用的数据]打包成一份,进行各自分发
有了这个思路,我们很快就想到,这是一个好东西,一个机器可以生产很多份样式相同的东西,但是每个东西又是独立的,不是正好可以隔离吗?
这一点像类构造对象的模式,同一个构造函数可以创建很多不同的对像
那么data为什么会像各种资料上所说的不会消失呢?这个道理也很容易理解,data是被对象所引用的,至少目前为止,对象还是活蹦乱跳的。比如说let a,这个a虽然不是全局的(参考es6),但是在当前这块大环境里面还是存活的,对象还是活着的,那么它引用的数据也会一直存活。比如说,A引用B,B引用C,C引用D,只要源头的引用还存在,后续的都必须存在,这个思路是完全合理的
有了这几个核心的认识,而且还是简单的认识,我们就可以写出各种变化的代码,比如下面一段:
function f(){ let data = 0 return { get:function(){ return data }, set:function(v){ data = v } } } let a=f() //机器生产一份 let b=f() //机器再生产一份 a.set(10) console.log(a.get()) b.set(20) console.log(b.get())
我们让机器f生产两份对象出来,每个对象包裹了两个函数属性,其实在我们眼里,所有都是对象,这一点反复在说明,因为把函数看成对象就会变的清晰
两份东西都不一样,如果非要在计算机上认识,我们可以想像的认为它们就在内存中的不同地址处。而且data是两份对象各自拥有的,因此a调用set()进行设置和b一点关系都没有,毫无影响
下面,我们改造一下,因为上面说过,用闭包可以形成隔离。如果要说到隔离,就会想到面向对象,我们沉浸这个概念很久了。面向对象的一个重要特征之一就是把数据封装。
首先,我们先用正常的思路构造对象
function Person(name,age){ this.name=name this.age=age } let o = new Person('tianya',20) console.log(o.name)
如代码所示,这是常用的构造方式,但是好像违背了面向对象的初衷,里面的name和age都展示在外面。于是联想到闭包,把name和age放在里面,外面只通过get/set进行操作,代码变成这种样式:
function Person(){ let _name,_age this.getName=function(){ return this._name } this.setName=function(name){ this._name=name } //... } let o = new Person() o.setName('tianya') //o.name 未定义 console.log(o.getName()) //'tianya'
_name和_age已经封装到构造函数内部,外部只能通过接口进行访问,姑且不说这种方式实际是否合适,仅仅是理论上的分析
下面,分析一下闭包的引用
闭包的数据引用,其实非常符合我们的日常思维,内部方法能访问外部的数据,这不是非常符合常理吗,几乎都不需要特别的说明,就像上面的代码,内部函数可以访问自己上面的数据,但还是有几点需要说明
先看一份代码:
function f(){ let data=10 function g(){ //我能看到什么? console.log(data) } }
内部函数g就像有一只眼睛,它能看到多大的范围呢?首先从直觉上,我们都知道,至少它头顶之上的都可以看到,而且函数f之外也能看到。对于人来说,是很快能反应出来的,但是语言没有那么聪明,它需要一套结构记录下来。
我们这样联想,g能看到data,而且还能看到f()之外的东西,这就像一根链条,是逐级往上查找。看到链条,我们马上就会联想原型链。这两者确实原理相似
如果我们是语言设计者会怎么考虑呢?从字面上来看,g比f的视野更宽,也就是越往内部眼睛能看到的数据就越多,而函数f是入口,所以我们可以先从f入手,把f能看到的记录下来记为F。g除了能看到F,还能看到里面的数据G,因此可以看到F+G,以此类推....一根链条就这样建立起来
关于这张图,我们不要像考古一样关注这张图是否准确,正确,仅仅是从思路上观察它
每个函数调用时都会形成一个闭包,为了容易理解,表述都是不严谨的,比如对于g来说,它需要收集下面几个信息:
1、自己的父闭包是谁?
2、自己内部定义了哪些变量?
3、自己内部定义了哪些方法
就像一张登记表,有了这张表就可以向上查询,又回到原型链的思路。比如说,g()内部引用了变量outer,查找路径如下:
1、自己内部有吗?
2、如果没有,在其父闭包中查找,本例中就是闭包f
3、如果再没有,就在闭包f的父闭包中查找....以此类推,直到最源头为止
这就是为什么g()能引用outer的原因
接下来,我们考虑一个问题,既然上面建了表,那么这个表是什么时候创建呢?按照正常理解,应该是调用进入时创建合适。这样就引起一个问题,函数还没有执行,表已经创建好了
这个给我们的启示是:函数还没有执行,内部结构已经登记好了,于是就出现这种代码:
let outer='outer data' function f(){ let data=10 g() //先执行 function g(){ let inner=20 console.log(outer) } } f()
这种现象就是常说的函数提升。不仅函数能提升,我们观察到,内部声明的变量也被提前登记了,至少说明可以在变量赋值之前就可以引用变量。这个问题也是一直诟病的,为什么要把var变成let的原因。比如:
console.log(msg)
var msg = 'hello js'
按照推导,执行打印之前,msg登记在册,仅仅没有赋值,因此并不报错只是显示为undefined
同时也解释了另一个问题,为什么使用变量赋值函数没有得到提升?比如:
g()
var g=function(){}
登记在册的仅仅是变量g,但是执行时并没有赋值,因此,变量声明的函数没有得到提升。
# 2020-10-26 21:00:24 追加
闭包的记忆功能
闭包的记忆功能可能是最奇妙的一种功能
首先,想象若干个桶去接水
每个桶都保存当时那个时间段的状态,这有点像游戏中所说的封印
封印这个词来形容闭包实在最合适不过,就是指闭包把当时那一刻的状态整个的封印起来,也可以简单化来说,是函数对象+数据进行封印,或者直接说 [对象+数据] 封印
应用一:对象的get/set设置
在js中,对象的属性可以指定get/set,这种模式是对封印非常好的理解,代码如下:
假如,有一个对象
let o={
'name':'tianya'
}
我们要对name属性设置get/set,但是我们必须有一个中间量来保存name的值,否则会产生无限递归。考虑到这个中间量最好能封装起来,于是用闭包来保存最合适!
代码如下:
let x = { 'name': 'tianya' } function define(obj, key) { let value = obj[key] Object.defineProperty(obj, key, { get() { console.log('获取值') return value }, set(v) { value = v } }) } define(x, 'name') //属性操作 console.log(x.name) //取值 x.name = 'xz' console.log(x.name)
应用2:数组遍历器
前面我们看到,创建一个闭包之后,并不是不管了,而是在程序整个运行期间,可能会发生不断的执行。每执行一次(可能)就会改变数据的状态,但这正是闭包的用途。
下面看一个数组遍历器的例子,也是利用闭包的记忆功能。