闭包(closure)

前言

在 MDN 中对于闭包的描述:

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

词法作用域

请看下面的例子:

export const InitComp = () => { const init = () => { const name = 'liwenkai' const displayName = () => { alert(name) } displayName() } return ( 点我 ) }

export const InitComp = () => {
  const init = () => {
    const name = 'liwenkai'
    const displayName = () => {
      alert(name)
    }
    displayName()
  }
  return (
    <button className="rounded bg-blue-500 p-2" onClick={init}>
      点我
    </button>
  )
}

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。

闭包

现在来看这个例子:

export const MakeFuncComp = () => { const makeFunc = () => { const name = 'liwenkai' const displayName = () => { alert(name) } return displayName } const myFunc = makeFunc() return ( 点我 ) }

export const MakeFuncComp = () => {
  const makeFunc = () => {
    const name = 'liwenkai'
    const displayName = () => {
      alert(name)
    }
    return displayName
  }
  const myFunc = makeFunc()
  return (
    <button className="rounded bg-blue-500 p-2" onClick={myFunc}>
      点我
    </button>
  )
}

运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回。

第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 makeFunc() 执行完毕,你可能会认为 name 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。

原因在于,JavaScript 中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中。

下面是一个更有意思的示例 — 一个 makeAdder 函数:

export const MakeAdderComp = () => { const makeAdder = (x) => { return function (y) { return x + y } } const add5 = makeAdder(5) const add10 = makeAdder(10) const [number, setNumber] = React.useState(0) return ( <button className="rounded bg-blue-500 p-2" onClick={() => setNumber(add5(number))}> 点我+5 <button className="rounded bg-blue-500 p-2" onClick={() => setNumber(add10(number))}> 点我+10 <button className="rounded bg-blue-500 p-2" onClick={() => setNumber(0)}> 重置为0 {number} ) }

export const MakeAdderComp = () => {
  const makeAdder = (x) => {
    return function (y) {
      return x + y
    }
  }
  const add5 = makeAdder(5)
  const add10 = makeAdder(10)
  const [number, setNumber] = React.useState(0)
  return (
    <div className="flex gap-4">
      <button className="rounded bg-blue-500 p-2" onClick={() => setNumber(add5(number))}>
        点我+5
      </button>
      <button className="rounded bg-blue-500 p-2" onClick={() => setNumber(add10(number))}>
        点我+10
      </button>
      <button className="rounded bg-blue-500 p-2" onClick={() => setNumber(0)}>
        重置为0
      </button>
      <span className="w-20 rounded bg-blue-500 p-2">{number}</span>
    </div>
  )
}

实用的闭包

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

在 Web 中,你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。

接着看下面一个利用闭包来改变字号的例子:

export const MakeSizerComp = () => { const makesize = (size) => { return function () { return 'text-' + size } } const [spanClass, setSpanClass] = React.useState('text-base') return ( <> Some paragraph text <button className="rounded bg-blue-500 p-2" onClick={() => setSpanClass(makesize('sm'))}> 点我 sm <button className="rounded bg-blue-500 p-2" onClick={() => setSpanClass(makesize('base'))}> 点我 base <button className="rounded bg-blue-500 p-2" onClick={() => setSpanClass(makesize('2xl'))}> 点我 2xl </> ) }

export const MakeSizerComp = () => {
  const makesize = (size) => {
    return function () {
      return 'text-' + size
    }
  }
  const [spanClass, setSpanClass] = React.useState('text-base')
  return (
    <>
      <div className="flex h-20 items-center">
        <span className={spanClass}>Some paragraph text</span>
      </div>
      <div className="flex gap-4">
        <button className="rounded bg-blue-500 p-2" onClick={() => setSpanClass(makesize('sm'))}>
          点我 sm
        </button>
        <button className="rounded bg-blue-500 p-2" onClick={() => setSpanClass(makesize('base'))}>
          点我 base
        </button>
        <button className="rounded bg-blue-500 p-2" onClick={() => setSpanClass(makesize('2xl'))}>
          点我 2xl
        </button>
      </div>
    </>
  )
}

用闭包模拟私有方法

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其他方法所调用。

而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

export const CounterComp = () => { const Counter = () => { let privateCounter = 0 const changeBy = (val) => { privateCounter += val } return { increment: () => changeBy(1), decrement: () => changeBy(-1), reset: () => { privateCounter = 0 }, value: () => { return privateCounter } } } const myCounter = React.useMemo(() => Counter(), []) const [count, setCount] = React.useState(myCounter.value()) return ( <> {count} <button className="rounded bg-blue-500 p-2" onClick={() => { myCounter.increment() setCount(myCounter.value()) }} > 点我+1 <button className="rounded bg-blue-500 p-2" onClick={() => { myCounter.decrement() setCount(myCounter.value()) }} > 点我-1 <button className="rounded bg-blue-500 p-2" onClick={() => { myCounter.reset() setCount(myCounter.value()) }} > 重置为0 </> ) }

export const CounterComp = () => {
  const Counter = () => {
    let privateCounter = 0
    const changeBy = (val) => {
      privateCounter += val
    }
    return {
      increment: () => changeBy(1),
      decrement: () => changeBy(-1),
      reset: () => {
        privateCounter = 0
      },
      value: () => {
        return privateCounter
      }
    }
  }
  const myCounter = React.useMemo(() => Counter(), [])
  const [count, setCount] = React.useState(myCounter.value())
  return (
    <>
      <div className="flex h-20 items-center">
        <span className="w-20 rounded bg-blue-500 p-2">{count}</span>
      </div>
      <div className="flex gap-4">
        <button
          className="rounded bg-blue-500 p-2"
          onClick={() => {
            myCounter.increment()
            setCount(myCounter.value())
          }}
        >
          点我+1
        </button>
        <button
          className="rounded bg-blue-500 p-2"
          onClick={() => {
            myCounter.decrement()
            setCount(myCounter.value())
          }}
        >
          点我-1
        </button>
        <button
          className="rounded bg-blue-500 p-2"
          onClick={() => {
            myCounter.reset()
            setCount(myCounter.value())
          }}
        >
          重置为0
        </button>
      </div>
    </>
  )
}