[JS之作用域和闭包-5]闭包的实现原理和作用

570 阅读7分钟

闭包的历史

闭包翻译自英文单词 closure,这是个不太好翻译的词,在计算机领域,它就有三个完全不相同的意义:

  • 编译原理中,它是处理语法产生式的一个步骤;
  • 计算几何中,它表示包裹平面点集的凸多边形(翻译作凸包);
  • 而在编程语言领域,它表示一种函数。

闭包这个概念第一次出现在 1964 年的《The Computer Journal》上,由 P. J. Landin 在《The mechanical evaluation of expressions》一文中提出了 applicative expressionclosure 的概念。

9b6c6693afe654b4cfdbf16852b82a0c

在上世纪 60 年代,主流的编程语言是基于 lambda 演算的函数式编程语言,所以这个最初的闭包定义,使用了大量的函数式术语。一个不太精确的描述是“带有一系列信息的λ表达式”。对函数式语言而言,λ表达式其实就是函数。

我们可以这样简单理解一下,闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。

这个古典的闭包定义中,闭包包含两个部分。

  • 环境部分
    • 环境
    • 标识符列表
  • 表达式部分

JavaScript中的闭包

先看一下闭包的定义:

MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

你不知道的 JavaScript 上卷:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。

JavaScript 高级程序设计 第4版:闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

由此可以知道:

JavaScript 闭包的本质源自两点,词法作用域和函数当作值传递。

词法作用域,就是,按照代码书写时的样子,内部函数可以访问函数外面的变量。引擎通过数据结构和算法表示一个函数,使得在代码解释执行时按照词法作用域的规则,可以访问外围的变量,这些变量就登记在相应的数据结构中。

函数当作值传递,即所谓的first class对象。就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。

举一个例子加深记忆:

function foo() {
    var myName = " JavaScript "
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName: function () {
            console.log(test1)
            return myName
        },
        setName: function (newName) {
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName(" CSS ")
bar.getName()
console.log(bar.getName())

首先我们看看当执行到foo函数内部的return innerBar这行代码时调用栈的情况,如下图:

从上面的代码可以看出,innerBar 是一个对象,包含了 getNamesetName 的两个方法(通常我们把对象内部的函数称为方法)。你可以看到,这两个方法都是在 foo 函数内部定义的,并且这两个方法内部都使用了 myNametest1 两个变量。

根据词法作用域的规则,内部函数 getNamesetName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getNamesetName 函数依然可以使用 foo 函数中的变量 myName test1。所以当 foo 函数执行完成之后,其整个调用栈的状态如下图所示:

QQ图片20210602131921

从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setNamegetName 方法中使用了 foo 函数内部的变量 myNametest1,所以这两个变量依然保存在内存中。这像极了 setNamegetName 方法背的一个专属背包,无论在哪里调用了 setNamegetName 方法,它们都会背着这个 foo 函数的专属背包。

之所以是专属背包,是因为除了 setNamegetName 函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为 foo 函数的闭包。

好了,现在我们终于可以给闭包一个正式的定义了。**在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。**比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

那这些闭包是如何使用的呢?当执行到 bar.setName 方法中的 myName = "CSS" 这句代码时,JavaScript 引擎会沿着“当前执行上下文–> foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量,你可以参考下面的调用栈状态图:

QQ图片20210602132247

从图中可以看出,setName 的执行上下文中没有 myName 变量,foo 函数的闭包中包含了变量 myName,所以调用 setName 时,会修改 foo 闭包中的 myName 变量的值。

同样的流程,当调用 bar.getName 的时候,所访问的变量 myName 也是位于 foo 函数闭包中的。

你也可以通过“开发者工具”来看看闭包的情况,打开 Chrome 的“开发者工具”,在 bar 函数任意地方打上断点,然后刷新页面,可以看到如下内容:

image-20210602132810134

从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:

  • Local 就是当前的 getName 函数的作用域;
  • Closure(foo) 是指 foo 函数的闭包;
  • 最下面的 Global 就是指全局作用域;

从“ Local –> Closure(foo) –> Global ”就是一个完整的作用域链。

所以说,你以后也可以通过 Scope 来查看实际代码作用域链的情况,这样调试代码也会比较方便。

闭包的作用

  1. 闭包的第一个作用是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  2. 闭包的另一个作用是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

闭包应用场景

柯里化

柯里化:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数。

// 柯里化前
const getVolume = (l, w, h) => l * w * h
const volume1 = getVolume(100, 200, 100)
const volume2 = getVolume(100, 200, 300)

// 柯里化后
const getVolume = l => w => h => l * w * h
const getVolumeWithDefaultLW = getVolume(100)(200)
const volume1 = getVolumeWithDefaultLW(100)
const volume2 = getVolumeWithDefaultLW(300)

模块化

用于将内部实现封装,仅对外暴露接口,常见于工具库的开发中。

var counter = (function() {
  var privateCounter = 0
  function changeBy(val) {
    privateCounter += val
  }
  return {
    increment: function() {
      changeBy(1)
    },
    decrement: function() {
      changeBy(-1)
    },
    value: function() {
      return privateCounter
    }
  }
})()

参考:

MDN

你不知道的JavaScript 上卷

JavaScript高级程序设计 第4版

作用域与闭包 - 闭包的实现原理和作用

「2021」高频前端面试题汇总之JavaScript篇(上)

什么是闭包?