携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
在认识闭包前,我们先来聊一聊内存管理
内存管理
不管是什么编程语言,在代码执行的时候都会对内存进行分配,有些编程语言需要程序员手动分配,例如 C++,
而有些编程语言会自动帮助我们管理内存。
然而,不管以什么样的方式来管理内存,内存管理的流程大致都是差不多的:
- 申请分配内存空间
- 使用内存空间
- 不需要使用的时候,对其进行释放
JavaScript 在定义变量的时候会自动为我们分配内存,对于不同的变量类型,内存分配的方式也是不一样的:
-
对于基本数据类型来说,在分配内存时,会直接在栈空间中进行分配
-
对于引用数据类型来说,在分配内存时,会在堆内存中分配一块空间,并且将这块空间的指针返回至变量引用
垃圾回收
内存的大小是有限的,因此当内存不再需要的时候,我们需要对其释放,以便腾出更多的内存空间
对于手动分配内存的语言中,我们需要通过一些方式自己手动分配内存,这种方式对开发者的要求很高,一不小心就容易产生内存泄漏
JavaScript 和大多数现代编程语言一样,有自己的垃圾回收机制。
垃圾回收(Garbage Collection)简称GC,在有些地方,垃圾回收器也被简称为GC。
GC有自己的算法来判断哪些对象是不再使用,并且需要释放的。接下来我们来说一说一些常见的GC算法。
引用计数
引用计数的原理:当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;
但是引用计数存在一个非常明显的缺点就是循环引用
标记清除
标记清除的原理:设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对
于哪些没有引用到的对象,就认为是不可用的对象。
JavaScript引擎大多采用的是标记清除法,V8引擎也为了进行更好的优化,也会将标记清除法和其他算法结合起来。
闭包
好了,在聊完内存管理和垃圾回收之后,我们终于可以来聊一聊闭包了。
不管是面试还是实际开发中,闭包始终是讨论的最多的话题之一。我们先来看看它的定义:
在MDN中:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
看起来很晦涩难懂是吧,我们来通过一个例子来解释一下。
来看这段代码:
function foo(){
var name = 'curry'
function bar(){
console.log(name)
}
return bar
}
var fn = foo()
fn() // 'curry'
当遇到函数的时候,会创建一个叫做FEC的函数执行上下文,因此foo函数调用的时候,会经历如下步骤:
- 创建
foo的函数执行上下文 - 创建一个
VO对象,指向foo的AO对象,此时AO对象包含:变量:name,bar(地址) - 然后执行
foo中的代码,将函数bar,也就是它的内存地址返回给了全局变量fn
然后在全局作用域中调用 fn 函数,fn指向的是bar的函数对象,因此调用的是 bar 这个函数对象。
当调用 fn 函数时,会经历如下流程:
- 创建
fn的函数执行上下文 - 创建一个
VO对象,指向bar的AO对象,此时AO对象为空 - 然后执行代码
注意:由于在解析的时候,
bar这个函数对象中的作用域链条已经确定,因此会从它的父级作用域查找到name这个变量
画图大概就是如下:
此时,foo 函数调用完毕,foo 函数对象被销毁,但是由于 bar 的函数对象中的作用域链(scope chain)中指向了 foo 的 AO 对象的 bar,因此,
foo 的 AO 对象并不会销毁。此时 bar 就是一个闭包。
此时再去回看MDN中的解释就会清晰很多了。
对于一个函数来说,如果它可以访问作用于外层的自由变量,那么它就是一个闭包
闭包的内存泄漏
上面我们可以看到,全局变量 fn 一直保存着 bar 函数对象的引用,而 bar 函数对象的作用域中又保存着 foo 的 AO 对象的引用,因此造成这部分内存一直不会被释放,这其实就是闭包产生的内存泄漏。
如何解决呢?答案也很简单,只要将全局变量 foo 赋值为 null ,不再保存对 bar 函数对象的引用即可,GC再下一次检测的时候,它们的内存就会被销毁掉。
AO不使用的属性
我们最后来考虑一个问题,来看这段代码:
function foo(){
var name = 'curry'
function bar(){
}
return bar
}
var fn = foo()
fn()
这段代码中,虽然 name 属于 bar 的父级作用域,但是由于 bar 中并没有使用父级作用域的 AO 对象中的 name 属性,因此,V8引擎其实会做优化,将 AO 中不使用的属性进行销毁的。
总结
闭包其实就是一个函数。
对于一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包。
广义的角度来说:JavaScript 中的函数都是闭包。
狭义的角度来说:JavaScript 中的函数如果访问了外层作用域的变量,它就是一个闭包。