javascript - JavaScript 闭包是如何工作的?

标签 javascript function variables scope closures

这个问题的答案是community effort .编辑现有答案以改进这篇文章。它目前不接受新的答案或互动。








你会如何向了解 JavaScript 闭包组成的概念(例如函数、变量等)但不了解闭包本身的人解释 JavaScript 闭包?

我见过the Scheme example在维基百科上给出,但不幸的是它没有帮助。

最佳答案

闭包是一对:

  • 一个函数,和
  • 对该函数外部作用域(词法环境)的引用

  • 词法环境是每个执行上下文(堆栈帧)的一部分,是标识符(即局部变量名称)和值之间的映射。
    JavaScript 中的每个函数都维护对其外部词法环境的引用。此引用用于配置调用函数时创建的执行上下文。此引用使函数内部的代码能够“查看”函数外部声明的变量,而不管函数何时何地被调用。
    如果一个函数被一个函数调用,而该函数又被另一个函数调用,那么就会创建一个指向外部词法环境的引用链。这个链称为作用域链。
    在以下代码中,inner用在 foo 时创建的执行上下文的词法环境形成一个闭包被调用,关闭变量 secret :

    function foo() {
      const secret = Math.trunc(Math.random()*100)
      return function inner() {
        console.log(`The secret number is ${secret}.`)
      }
    }
    const f = foo() // `secret` is not directly accessible from outside `foo`
    f() // The only way to retrieve `secret`, is to invoke `f`

    换句话说:在 JavaScript 中,函数携带对私有(private)“状态框”的引用,只有它们(以及在同一词法环境中声明的任何其他函数)才能访问该引用。这个状态框对于函数的调用者是不可见的,为数据隐藏和封装提供了一个很好的机制。
    请记住:JavaScript 中的函数可以像变量(一等函数)一样传递,这意味着这些功能和状态的配对可以在您的程序中传递:类似于您在 C++ 中传递类的实例的方式。
    如果 JavaScript 没有闭包,则必须在函数之间显式传递更多状态,从而使参数列表更长且代码更嘈杂。
    因此,如果您希望函数始终可以访问私有(private)状态,则可以使用闭包。
    ...而且经常我们确实希望将状态与函数相关联。例如,在 Java 或 C++ 中,当您向类添加私有(private)实例变量和方法时,您将状态与功能相关联。
    在 C 和大多数其他常见语言中,在函数返回后,所有局部变量都不再可访问,因为堆栈帧被破坏。在 JavaScript 中,如果你在另一个函数中声明一个函数,那么外部函数的局部变量在从它返回后仍然可以访问。这样,在上面的代码中,secret保持对函数对象可用 inner , 从 foo 返回后.
    闭包的使用
    当您需要与函数关联的私有(private)状态时,闭包很有用。这是一个非常常见的场景 - 请记住:JavaScript 直到 2015 年才有类语法,而且它仍然没有私有(private)字段语法。闭包满足了这种需求。
    私有(private)实例变量
    在下面的代码中,函数 toString关闭汽车的细节。

    function Car(manufacturer, model, year, color) {
      return {
        toString() {
          return `${manufacturer} ${model} (${year}, ${color})`
        }
      }
    }
    const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
    console.log(car.toString())

    函数式编程
    在下面的代码中,函数 inner两者均收盘 fnargs .

    function curry(fn) {
      const args = []
      return function inner(arg) {
        if(args.length === fn.length) return fn(...args)
        args.push(arg)
        return inner
      }
    }
    
    function add(a, b) {
      return a + b
    }
    
    const curriedAdd = curry(add)
    console.log(curriedAdd(2)(3)()) // 5

    面向事件的编程
    在以下代码中,函数 onClick关闭变量 BACKGROUND_COLOR .

    const $ = document.querySelector.bind(document)
    const BACKGROUND_COLOR = 'rgba(200,200,242,1)'
    
    function onClick() {
      $('body').style.background = BACKGROUND_COLOR
    }
    
    $('button').addEventListener('click', onClick)
    <button>Set background color</button>

    模块化
    在以下示例中,所有实现细节都隐藏在立即执行的函数表达式中。功能 ticktoString关闭他们完成工作所需的私有(private)状态和功能。闭包使我们能够模块化和封装我们的代码。

    let namespace = {};
    
    (function foo(n) {
      let numbers = []
      function format(n) {
        return Math.trunc(n)
      }
      function tick() {
        numbers.push(Math.random() * 100)
      }
      function toString() {
        return numbers.map(format)
      }
      n.counter = {
        tick,
        toString
      }
    }(namespace))
    
    const counter = namespace.counter
    counter.tick()
    counter.tick()
    console.log(counter.toString())

    例子
    示例 1
    这个例子表明局部变量没有在闭包中复制:闭包维护对原始变量本身的引用。就好像堆栈帧在外部函数退出后仍然存在于内存中一样。

    function foo() {
      let x = 42
      let inner  = function() { console.log(x) }
      x = x+1
      return inner
    }
    var f = foo()
    f() // logs 43

    示例 2
    在下面的代码中,三个方法 log , increment , 和 update都关闭在同一个词法环境中。
    并且每次createObject被调用,一个新的执行上下文(堆栈帧)被创建,一个全新的变量 x ,并创建了一组新的函数(log 等),它们关闭了这个新变量。

    function createObject() {
      let x = 42;
      return {
        log() { console.log(x) },
        increment() { x++ },
        update(value) { x = value }
      }
    }
    
    const o = createObject()
    o.increment()
    o.log() // 43
    o.update(5)
    o.log() // 5
    const p = createObject()
    p.log() // 42

    示例 3
    如果您使用的是使用 var 声明的变量,请注意您了解要关闭的变量。使用 var 声明的变量被吊起。由于 let 的引入,这在现代 JavaScript 中的问题要小得多。和 const .
    在下面的代码中,每次循环,一个新函数inner已创建,结束于 i .但是因为var i被提升到循环外,所有这些内部函数都关闭同一个变量,这意味着 i 的最终值(3) 打印,三遍。

    function foo() {
      var result = []
      for (var i = 0; i < 3; i++) {
        result.push(function inner() { console.log(i) } )
      }
      return result
    }
    
    const result = foo()
    // The following will print `3`, three times...
    for (var i = 0; i < 3; i++) {
      result[i]() 
    }

    最后的要点:
  • 每当在 JavaScript 中声明一个函数时,都会创建闭包。
  • 返回 function从另一个函数内部是闭包的经典例子,因为外部函数内部的状态对于返回的内部函数是隐式可用的,即使在外部函数完成执行之后也是如此。
  • 每当您使用 eval()在函数内部,使用了闭包。文你eval可以引用函数的局部变量,在非严格模式下,甚至可以使用eval('var foo = …')创建新的局部变量.
  • 当您使用 new Function(…) ( Function constructor )在函数内部,它不会关闭其词法环境:而是关闭全局上下文。新函数不能引用外部函数的局部变量。
  • JavaScript 中的闭包就像在函数声明点保留对作用域的引用( 不是 副本),而函数声明又保留对其外部作用域的引用,依此类推,一直到作用域链顶部的全局对象。
  • 声明函数时会创建一个闭包;这个闭包用于在调用函数时配置执行上下文。
  • 每次调用函数时都会创建一组新的局部变量。

  • 链接
  • Douglas Crockford 的模拟 private attributes and private methods对于一个对象,使用闭包。
  • 关于闭包如何做的很好的解释 cause memory leaks in IE如果你不小心。
  • JavaScript Closures 上的 MDN 文档.
  • 关于javascript - JavaScript 闭包是如何工作的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/111102/

    相关文章:

    python - (Python 2.7) 在函数中使用列表作为参数?

    javascript - Jquery 根据点击将一个元素的值更改为另一个元素

    c - 将数组传递给 C 中的函数来存储数据

    javascript - 将字符串从函数传递到变量 Javascript

    php - 如何从 Laravel 上的 MessageSent 事件访问邮件数据?

    javascript - jQuery UI 可选 : get the text of the selected item

    javascript - jQuery datePicker 中的奇怪行为

    objective-c - 接口(interface)中的变量声明问题

    javascript - iPhone 上的锁定方向 UIWebView

    javascript - 如何调用bootstrap colorpicker的 'changeColor'方法?