ES 基础 —— 闭包

published

闭包(Closure)是函数式编程中的概念,出现于 20 世纪 60 年代。最早实现闭包的语言是 Scheme,之后闭包这个特性被其他语言吸纳。

闭包的严格定义:由函数(环境)及其封闭的自由变量组成的集合体。 ES 中的每个函数都是一个闭包,但通常只有嵌套的函数才能够体现出闭包的特性。 比如:

const genCounter = function () {
  let count = 0;

  return function () {
    count = count + 1
    return count
  }
}

const counter = genCounter()

console.log(counter())  // 1
console.log(counter())  // 2
console.log(counter())  // 3

上面的代码中, genConuter() 函数中有一个初始值为 count 的局部变量,并返回了一个函数 —— 给 count 加 1,并返回 count 。之后调用 genCounter() 获得了 counter 函数,反复调用 counter 函数,获得了 1, 2, 3。

看起来蛮简单,其实有些玄机。

通常来讲,人们会这么理解:countgenCounter() 函数内部的变量,它的声明周期就是 genCounter() 被调用的时期,当 genCounter() 调用结束后, count 变量就被销毁了,也就无法被访问到了。

可是现在: genCounter() 调用结束后, counter() 却引用了「已经被销毁」的 count 变量,而且没有出错。

这是怎么回事?这就是闭包的特性。

闭包的实现原理

一般来说,函数调用完毕后,其 VO 就会被销毁。但在之前的代码中,即使 genCounter() 函数调用完毕后,count 还可以被内部函数访问。之所以能够访问,是因为内部函数的作用域链中包含 genCounter() 函数的 VO。下面说说其中的细节。

在介绍 Execution Context 时,详细描述了函数执行的细节,可以回顾一下。

在一个函数(外部函数)内部定义另一个函数(内部函数)时,内部函数会把外部函数的 VO 添加到自己的作用域链中。因此,在内部函数从 genCounter() 中被返回后,它的作用域链包含:

  • 自身的 VO
  • genCounter() 的 VO
  • 全局 VO

这样,内部函数就可以访问在 genCounter() 中定义的变量。当 genCounter() 函数返回后,其作用域链会被销毁,但是由于内部函数的作用域链仍然在引用这个 VO,所以其 VO 会被保存 。直到内部函数的作用域链被销毁后, genCounter() 的 VO 才会被销毁。

由于闭包的作用域链会始终携带外部函数的作用域,因此会比其他函数占更多的内存。建议只在绝对必要时再考虑使用闭包。

闭包的应用

封装(实现私有属性)

ES 中的对象并没有私有属性,对象的每一个属性都是暴露给外部的。 虽然通过约定 —— 在私有属性前加下划线(如 _secret )可以表示「这个属性是私有的,外部对象不应该直接读写它」。但这也只是个约定而已。如果就是有人要来直接修改这个属性,有没有更严格的机制呢? 通过闭包来实现 —— 使得属性不能被外部随意修改,同时又可以通过指定的函数接口来操作。 比如:

const foo = function() {
  let secret = 'secret'

  // 闭包内的函数可以访问 secret,而 secret 对于外部却是隐藏的
  return {
    get_secret: function () {
      // 通过定义的函数接口来访问 secret
      return secret
    },
    new_secret: function (new_secret) {
      // 通过定义的函数接口来修改 secret
      secret = new_secret
    }
  }
}()

foo.get_secret();               // 'secret'
foo.secret;                     // Type error
foo.new_secret('a new secret'); // 通过函数接口,修改 secret 变量
foo.get_secret();               // 'a new secret'

其他参考:

闭包的反模式

在多层嵌套函数中使用闭包

记住,多层嵌套函数中,每当要使用一个变量,总要通过 Scope Chain 寻找这个变量,变量所处的嵌套层数越深,在查找这个变量上所花费的时间就越多。

闭包中的变量冲突

function creationFunctions() {
  var result = new Array()

  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i
    }
  }

  return result
}

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值。但实际上,每个函数都返回 10。因为每个函数的作用域链中都引用着 createFunctions() 函数的 VO,所以它们引用的都是同一个变量 i 。当 createFunctions() 函数返回后,变量 i 的值是 10。之后再调用函数数组中的每个函数时都会返回 10。

闭包中的 this

在闭包中使用 this 需要特别注意。

const name = 'Global'

const object = {
  name: 'My Object',
  getNameFunc: function () {
    return function () {
      return this.name
    }
  }
}

// object.getNameFunc() 返回的匿名闭包函数不是作为对象的方法被调用的,
// 所以其中的 this 指向全局对象
console.log(object.getNameFunc()());  // Global (非严格模式)

更多