持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
前言
闭包这个东西,几乎每一次面试都会被拿出来问,什么是闭包,今天就来学习一下它吧,在说闭包前,需要先明确几个概念
js中函数是一等公民
这句话的意思就是说,在js中函数既可以作为参数也可以作为返回值,只有具有这个特性的编程语言才能形成闭包
闭包的定义
我们分为两部分来看,计算机科学和js中的闭包
- 计算机科学(维基百科):
- 在[计算机科学]中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),
- 是在支持[头等函数]的编程语言中实现[词法][绑定]的一种技术。
- 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括[约束变量](该函数内部绑定的符号),也要包括[自由变量](在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。
- 闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。
- js中的闭包 (mdn)
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
- 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
- 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
怎么样是不是很枯燥难以理解,没关系,用自己的大白话说就是
- 一个函数和对其周围状态(词法环境)的引用,捆绑在一起的组合就叫闭包
- 也就是说,闭包可以让你在内部函数中,访问到外部函数的作用域
- 在js中,每创建一个函数就会有一个闭包被创建出来
还是很难理解吗,没关系,我们接着往下看
到底什么是闭包!
先来看一个小栗子
// 代码的执行结果是什么呢?
function fn () {
var name = 'kaka'
function bar() {
console.log(name)
}
return bar
}
var fun = fn()
fun()
来看下结果,显而易见的,它的打印结果是kaka
有没有发现什么重点,在全局作用域里的fun,可以访问到函数fn里的变量,这个由函数bar和fn函数作用域(fn函数)组成的组合就叫做闭包
闭包在内存中的执行过程
可能这个时候,又会有人要疑惑了,搜索脑海里的知识,明明只有在作用域链里边的变量才能被访问到,fun的作用域链是全局作用域,那么name不在全局作用域里,它又是怎么被func访问到的呢? 不要着急,我们接着往下看
还是这段代码,我这次直接把编辑器里粘贴了过来,有了行号一会儿就方便多了
当我们的这段代码执行的时候,就是在内存中一个怎么样的表现呢,下面来一步一步的进行介绍
-
代码的编译阶段,也就是parse(从源码到ast树的阶段),这个阶段也大致分为以下几步
-
我们的js引擎会首先生成一个一个GO对象和一个空的调用栈
-
GO对象用来存放全局的函数和方法,以及我们代码定义的变量及函数
-
我们编译阶段,遇到基本数据和函数的处理方法是不同的
- 基本的数据就是给它赋值一个undefied
- 函数则会在内存中开辟一个空间,里边存放着函数的父级作用域和函数的执行体,我们写定的变量里则储存这个内存的地址
-
做完上述的操作后,至此编译阶段结束
-
-
编译过后,我们的js引擎开始对代码进行执行,在这个阶段,它做了这么几件事
-
在调用栈中创建一个全局执行上下文,用于执行代码
-
全局执行上下文中创建一个叫VO的对象,它指向GO
-
然后开始一行一行的执行代码
-
开始执行第12行代码,执行函数fn并将其结果赋值给变量fun,其中函数fn的执行,它在内存中是这样表现的
-
先对函数fn进行编译,内存中开辟空间生成函数fn的AO对象,AO对象有fn中定义的变量
- 基础类型name赋值为undefind
- 对函数bar进行编译,在内存中生成一块函数bar的空间,里面存放着bar的父级作用域(它的父级作用域就是函数fn的作用域,也就是fn的AO对象)和bar执行函数体,bar变量存放着bar函数的内存空间的地址
-
在调用栈中放入函数fn的执行上下文,当函数执行的时候都需要生成一个执行上下文
-
在函数fn的执行上下文中生成一个VO对象,它就是fn的AO对象
- 接着开始执行代码,name赋值为kaka,然后将bar函数作为返回值返回给了fun(GO对象里),所以fun存放的是bar的内存地址0xb00
-
函数fn执行完毕,fn的执行上下文出栈,随着执行上下文的出栈,它的vo对象和里面的指向也被回收
划重点: 此时按理说,fn的AO对象应该随着它执行上下文的出栈,被回收,但是因为它指向了函数bar,所以它此时是不会是被销毁的
-
-
至此第12行,代码执行完毕,开始执行第13行代码也就是执行func,也就是执行函数bar
-
执行函数bar还是重复上述的步骤
-
内存中开辟空间生成函数bar的AO对象,AO对象有fn中定义的变量
-
在调用栈中加入一个函数bar的执行上下文,然后生成一个VO,VO指向bar的AO
- 在bar函数体的内部,打印了变量name,根据变量是沿着作用域链找的原则
-
现在bar的作用域里找,没找到,于是去父级作用域找,此时发现内存中函数bar指向fn的AO对象,发现了name的值为kaka,就打印kaka
-
-
-
至此,函数bar执行完毕,它的执行上下文出栈,然后同时销毁bar的AO对象
-
这就是闭包在内存中的执行过程
划重点:函数bar和它绑定的fn的AO对象的组合就叫做闭包,这个时候bar还指向着fn的Ao对象,所以即使fn被销毁,依旧能访问到它作用域的值,理解了这个过程再去看闭包的定义是不是就容易理解多了
关于闭包的内存回收
这个时候可能有小伙伴就问了,按照之前说的,一个函数执行完毕,随着执行上下文的出栈,它的AO对象也会随着被回收,为什么函数fn的就不会被销毁呢?
闭包造成的内存泄漏
- 之前学过js的垃圾回收机制主要是标记清除,也就是从gc会定期从根开始往前查找它的引用,对于那些没有引用的对象,和找不到的对象,进行回收。
- 它的根就是GO对象,沿着GO往后找,发现fn的Ao被bar和函数fn指向,所以,它不会被清除
这个时候,如果bar对象以后你不想使用的话,就造成了内存泄漏,如果还想使用的话,就不叫内存泄漏
怎么优化闭包的内存泄漏呢
- 很简单,给对应的函数赋值null就可以了,例如给func赋值为null,就是它们指向了一个null的空的地方,它是这样表现的
- 此时发现从go开始找,函数fn的ao对象和函数bar,不在这条线路上了,就把它俩回收,至于它俩互相循环引用,就不管,刚好对应了
标记清除算法可以解决引用计数算法无法解决循环引用的这个优势