一文带你了解闭包

104 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

在认识闭包前,我们先来聊一聊内存管理

内存管理

不管是什么编程语言,在代码执行的时候都会对内存进行分配,有些编程语言需要程序员手动分配,例如 C++

而有些编程语言会自动帮助我们管理内存。

然而,不管以什么样的方式来管理内存,内存管理的流程大致都是差不多的:

  1. 申请分配内存空间
  2. 使用内存空间
  3. 不需要使用的时候,对其进行释放

JavaScript 在定义变量的时候会自动为我们分配内存,对于不同的变量类型,内存分配的方式也是不一样的:

  1. 对于基本数据类型来说,在分配内存时,会直接在栈空间中进行分配

  2. 对于引用数据类型来说,在分配内存时,会在堆内存中分配一块空间,并且将这块空间的指针返回至变量引用image-20211128211658267

垃圾回收

内存的大小是有限的,因此当内存不再需要的时候,我们需要对其释放,以便腾出更多的内存空间

对于手动分配内存的语言中,我们需要通过一些方式自己手动分配内存,这种方式对开发者的要求很高,一不小心就容易产生内存泄漏

JavaScript 和大多数现代编程语言一样,有自己的垃圾回收机制。

垃圾回收(Garbage Collection)简称GC,在有些地方,垃圾回收器也被简称为GC。

GC有自己的算法来判断哪些对象是不再使用,并且需要释放的。接下来我们来说一说一些常见的GC算法。

引用计数

引用计数的原理:当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;

但是引用计数存在一个非常明显的缺点就是循环引用

image-20211128211635277

标记清除

标记清除的原理:设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对

于哪些没有引用到的对象,就认为是不可用的对象。

image-20211128211911050

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函数调用的时候,会经历如下步骤:

  1. 创建 foo 的函数执行上下文
  2. 创建一个 VO 对象,指向 fooAO 对象,此时AO对象包含:变量:namebar(地址)
  3. 然后执行 foo 中的代码,将函数 bar ,也就是它的内存地址返回给了全局变量fn

然后在全局作用域中调用 fn 函数,fn指向的是bar的函数对象,因此调用的是 bar 这个函数对象。

当调用 fn 函数时,会经历如下流程:

  1. 创建 fn 的函数执行上下文
  2. 创建一个 VO 对象,指向 barAO 对象,此时 AO 对象为空
  3. 然后执行代码

注意:由于在解析的时候bar 这个函数对象中的作用域链条已经确定,因此会从它的父级作用域查找到 name 这个变量

画图大概就是如下:

image-20211128215003822

此时,foo 函数调用完毕,foo 函数对象被销毁,但是由于 bar 的函数对象中的作用域链(scope chain)中指向了 fooAO 对象的 bar,因此,

fooAO 对象并不会销毁。此时 bar 就是一个闭包。

此时再去回看MDN中的解释就会清晰很多了。

对于一个函数来说,如果它可以访问作用于外层的自由变量,那么它就是一个闭包

闭包的内存泄漏

上面我们可以看到,全局变量 fn 一直保存着 bar 函数对象的引用,而 bar 函数对象的作用域中又保存着 fooAO 对象的引用,因此造成这部分内存一直不会被释放,这其实就是闭包产生的内存泄漏。

如何解决呢?答案也很简单,只要将全局变量 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 中的函数如果访问了外层作用域的变量,它就是一个闭包。