JavaScript 闭包是如何工作的?

IT技术 javascript function variables scope closures
2020-12-11 21:21:40

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

我看过维基百科上给出的 Scheme 例子,但不幸的是它没有帮助。

6个回答

闭包是一对:

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

词法环境是每个执行上下文(堆栈帧)的一部分,是标识符(即局部变量名称)和值之间的映射。

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 中,函数携带对私有“状态框”的引用,只有它们(以及在同一词法环境中声明的任何其他函数)才能访问该引用。这个状态框对于函数的调用者是不可见的,为数据隐藏和封装提供了一个很好的机制。

请记住:JavaScript 中的函数可以像变量(一等函数)一样传递,这意味着这些功能和状态的配对可以在您的程序中传递:类似于您在 C++ 中传递类的实例的方式。

如果 JavaScript 没有闭包,则必须在函数之间显式传递更多状态,从而使参数列表更长且代码更嘈杂。

因此,如果您希望函数始终可以访问私有状态,则可以使用闭包。

...而且经常我们确实希望将状态与函数相关联。例如,在 Java 或 C++ 中,当您向类添加私有实例变量和方法时,您将状态与功能相关联。

在 C 和大多数其他常见语言中,在函数返回后,所有局部变量都不再可访问,因为堆栈帧被破坏。在 JavaScript 中,如果你在另一个函数中声明一个函数,那么外部函数的局部变量在从它返回后仍然可以访问。这样一来,在上面的代码,secret仍然可用的函数对象inner之后它已经从返回foo

闭包的使用

当您需要与函数关联的私有状态时,闭包很有用。这是一个非常常见的场景 - 请记住:JavaScript 直到 2015 年才有类语法,而且它仍然没有私有字段语法。闭包满足了这种需求。

私有实例变量

在以下代码中,该函数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())

函数式编程

在以下代码中,该函数innerfn和 上关闭args

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>

module化

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

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

在下面的代码中,三个方法logincrementupdate都关闭在同一个词法环境中。

每次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

如果您正在使用 using 声明的变量var,请注意您了解要关闭的变量。使用声明的变量var被提升。由于引入了let,这在现代 JavaScript 中已经不是什么问题了const

在下面的代码中,每次循环时,inner都会创建一个新函数,它关闭i. 但是因为var i是在循环外提升的,所有这些内部函数都关闭了同一个变量,这意味着i(3)的最终值被打印了 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从另一个函数内部返回 a是闭包的经典示例,因为外部函数内部的状态对于返回的内部函数是隐式可用的,即使在外部函数完成执行之后也是如此。
  • 每当您eval()在函数内部使用时,都会使用闭包。文本eval可以引用函数的局部变量,在非严格模式下,你甚至可以使用eval('var foo = …').
  • 当您new Function(…)函数使用( Function 构造函数) 时,它不会关闭其词法环境:而是关闭全局上下文。新函数不能引用外部函数的局部变量。
  • JavaScript 中的闭包就像在函数声明点保留对作用域的引用(而不是副本),而后者又保留对其外部作用域的引用,依此类推,一直到顶部的全局对象作用域链。
  • 声明函数时会创建一个闭包;这个闭包用于在调用函数时配置执行上下文。
  • 每次调用函数时都会创建一组新的局部变量。

链接

JavaScript 中的每个函数都维护一个指向其外部词法环境的链接。词法环境是范围内所有名称(例如变量、参数)及其值的映射。

因此,每当您看到该function关键字时,该函数内的代码都可以访问该函数外声明的变量。

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

这将记录,16因为函数bar关闭了参数x和变量tmp,这两者都存在于外部函数的词法环境中foo

Functionbar连同它与函数的词法环境的链接foo是一个闭包。

函数不必为了创建闭包返回仅仅凭借它的声明,每个函数都在其封闭的词法环境上关闭,形成一个闭包。

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2);
bar(10); // 16
bar(10); // 17

上面的函数也会记录 16,因为里面的代码bar仍然可以引用参数x和变量tmp,即使它们不再直接在作用域中。

但是,由于tmp仍然在 内部bar的闭包中徘徊,因此可以增加它。每次调用时它都会增加bar

最简单的闭包例子是这样的:

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

调用 JavaScript 函数时,ec会创建一个新的执行上下文连同函数参数和目标对象,这个执行上下文还接收到调用执行上下文的词法环境的链接,这意味着在外部词法环境中声明的变量(在上面的例子中,ab)都可以从 获得ec

每个函数都会创建一个闭包,因为每个函数都有一个指向其外部词法环境的链接。

请注意,变量本身在闭包中是可见的,而不是副本。

前言:这个答案是在问题是:

就像老 Albert 说的:“如果你不能向一个 6 岁的孩子解释它,你自己真的不明白。”我试图向一个 27 岁的朋友解释 JS 闭包,但完全失败了。

任何人都可以认为我 6 岁并且对那个主题很感兴趣吗?

我很确定我是唯一试图从字面上理解最初问题的人之一。从那以后,这个问题已经发生了好几次变化,所以我的回答现在可能看起来非常愚蠢和不合适。希望故事的总体思路对某些人来说仍然很有趣。


在解释困难的概念时,我非常喜欢类比和隐喻,所以让我尝试讲一个故事。

曾几何时:

有一个公主...

function princess() {

她生活在一个充满冒险的奇妙世界。她遇到了她的白马王子,骑着独角兽环游了她的世界,与龙战斗,遇到了会说话的动物,以及许多其他奇妙的事情。

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

但她总是不得不回到她沉闷的家务和大人世界。

    return {

她经常向他们讲述她作为公主最近的奇妙冒险。

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

但他们看到的只是一个小女孩……

var littleGirl = princess();

...讲述关于魔法和幻想的故事。

littleGirl.story();

即使大人知道真正的公主,他们也永远不会相信独角兽或龙,因为他们永远看不到它们。大人说,他们只存在于小女孩的想象之中。

但我们知道真正的真相;那个抱着公主的小女孩……

……真是个公主,里面有一个小女孩。

白马王子可以添加到她的冒险中,可以杀死所有的龙,以将她从以下危险中拯救出来: function princeCharming { adventures.push('Honeymoon Trip', 'Skydiving', 'Visiting Somalia'); const pickADragonToKill = dragons.pop(); }
2021-02-08 21:21:40
我喜欢这个解释,真的。对于那些阅读它但不遵循的人来说,类比是这样的: Princess() 函数是一个包含私有数据的复杂作用域。在函数之外,无法看到或访问私有数据。公主将独角兽、龙、历险记等留在她的想象中(私人资料),大人无法亲眼看到。但是公主的想象力在story()函数的闭包中被捕获,这是littleGirl实例暴露在魔法世界中的唯一接口
2021-02-09 21:21:40
哦,太好了,我差点要进行编辑以删除一开始我认为是多余的空间。干得好,+1
2021-02-28 21:21:40
具有未定义的值使其更难以理解。这是真实的故事jsfiddle.net/rjdx34k0/3
2021-03-05 21:21:40

认真对待这个问题,我们应该找出一个典型的 6 岁儿童的认知能力,尽管不可否认,对 JavaScript 感兴趣的人并不是那么典型。

关于 儿童发展:5 到 7 年它说:

您的孩子将能够遵循两步指导。例如,如果您对您的孩子说,“去厨房给我拿一个垃圾袋”,他们将能够记住那个方向。

我们可以用这个例子来解释闭包,如下:

厨房是一个闭包,它有一个名为 的局部变量trashBags厨房里有一个函数叫做getTrashBag获取一个垃圾袋并返回它。

我们可以在 JavaScript 中这样编码:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

进一步解释闭包为何有趣的要点:

  • 每次makeKitchen()调用时,都会创建一个具有自己单独的trashBags.
  • trashBags变量是每个厨房内部的局部变量,外部不可访问,但该getTrashBag属性的内部函数确实可以访问它。
  • 每个函数调用都会创建一个闭包,但是除非可以从闭包外部调用可以访问闭包内部的内部函数,否则不需要保留闭包。getTrashBag这里函数返回对象就是这样做的。

稻草人

我需要知道一个按钮被点击了多少次,并在每三次点击时做一些事情......

相当明显的解决方案

// Declare counter outside event handler's scope
var counter = 0;
var element = document.getElementById('button');

element.addEventListener("click", function() {
  // Increment outside counter
  counter++;

  if (counter === 3) {
    // Do something every third time
    console.log("Third time's the charm!");

    // Reset counter
    counter = 0;
  }
});
<button id="button">Click Me!</button>

现在这将起作用,但它确实通过添加一个变量侵入了外部范围,其唯一目的是跟踪计数。在某些情况下,这会更可取,因为您的外部应用程序可能需要访问此信息。但在这种情况下,我们只更改每三次点击的行为,因此最好将此功能包含在事件处理程序中

考虑这个选项

var element = document.getElementById('button');

element.addEventListener("click", (function() {
  // init the count to 0
  var count = 0;

  return function(e) { // <- This function becomes the click handler
    count++; //    and will retain access to the above `count`

    if (count === 3) {
      // Do something every third time
      console.log("Third time's the charm!");

      //Reset counter
      count = 0;
    }
  };
})());
<button id="button">Click Me!</button>

注意这里的一些事情。

在上面的例子中,我使用了 JavaScript 的闭包行为。此行为允许任何函数无限期地访问其创建的范围。为了实际应用这一点,我立即调用了一个返回另一个函数的函数,并且因为我返回的函数可以访问内部计数变量(由于上面解释的闭包行为),这会导致私有范围供结果使用功能……没那么简单?让我们稀释它...

一个简单的单线闭合

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

返回函数之外的所有变量对返回函数都是可用的,但不能直接对返回函数对象可用...

func();  // Alerts "val"
func.a;  // Undefined

得到它?因此,在我们的主要示例中,count 变量包含在闭包中并且始终可供事件处理程序使用,因此它从单击到单击都保持其状态。

此外,这个私有变量状态是完全可访问的,用于读取和分配给它的私有范围变量。

你去吧;你现在完全封装了这种行为。

完整的博客文章(包括 jQuery 考虑因素)