JavaScript 闭包如何工作?

2024-11-02 21:00:00
admin
原创
34
摘要:问题描述:如何向那些了解 JavaScript 闭包所包含的概念(例如函数、变量等),但不了解闭包本身的人解释 JavaScript 闭包?我曾经看过Wikipedia 上给出的Scheme 示例,但不幸的是它没有帮助。解决方案 1:闭包是以下一对:函数和对该函数外部范围(词法环境)的引用词法环境是每个执行上...

问题描述:

如何向那些了解 JavaScript 闭包所包含的概念(例如函数、变量等),但不了解闭包本身的人解释 JavaScript 闭包?

我曾经看过Wikipedia 上给出的Scheme 示例,但不幸的是它没有帮助。


解决方案 1:

闭包是以下一对:

  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`

运行代码片段Hide results展开片段

换句话说:在 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())

运行代码片段Hide results展开片段

函数式编程

在下面的代码中,函数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

运行代码片段Hide results展开片段

面向事件的编程

在下面的代码中,函数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>

运行代码片段Hide results展开片段

模块化

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

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())

运行代码片段Hide results展开片段

示例

示例 1

此示例表明,局部变量未在闭包中复制:闭包保留对原始变量本身的引用。 就好像即使外部函数退出后,堆栈框架仍在内存中保持活动状态。

function foo() {
  let x = 42
  let inner = () => console.log(x)
  x = x + 1
  return inner
}

foo()() // logs 43

运行代码片段Hide results展开片段

示例 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

运行代码片段Hide results展开片段

示例 3

如果您使用 声明的变量var,请务必了解要关闭哪个变量。使用 声明的变量会被提升。由于引入了和 ,var在现代 JavaScript 中,这个问题不再那么严重。let`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]() 
}

运行代码片段Hide results展开片段

最后几点:

  • 每当在 JavaScript 中声明一个函数时,就会创建闭包。

  • 从另一个函数内部返回function是闭包的经典示例,因为即使外部函数已完成执行,外部函数内部的状态对于返回的内部函数也是隐式可用的。

  • 每当您在函数内部使用时eval(),都会使用闭包。文本您eval可以引用函数的局部变量,并且在非严格模式下,您甚至可以使用创建新的局部变量eval('var foo = …')

  • 在函数内部使用new Function(…)(函数构造函数)时,它不会关闭其词法环境:而是关闭全局上下文。新函数不能引用外部函数的局部变量。

  • JavaScript 中的闭包就像在函数声明点保留对范围的引用(而不是副本),而该函数声明点又保留对其外部范围的引用,依此类推,一直到范围链顶部的全局对象。

  • 当声明一个函数时,就会创建一个闭包;这个闭包用于配置函数调用时的执行上下文。

  • 每次调用函数时都会创建一组新的局部变量。

链接

  • Douglas Crockford使用闭包模拟对象的私有属性和私有方法。

  • 很好地解释了如果不小心,闭包可能会导致 IE 中的内存泄漏。

  • 有关JavaScript 闭包的 MDN 文档。

  • JavaScript 闭包初学者指南。

解决方案 2:

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

因此,只要您看到function关键字,该函数内的代码就可以访问函数外声明的变量。

function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

运行代码片段Hide results展开片段

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

函数bar,连同它与函数的词法环境的链接一起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

运行代码片段Hide results展开片段

上述函数也将记录 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();

运行代码片段Hide results展开片段

当 JavaScript 函数被调用时,ec会创建一个新的执行上下文。除了函数参数和目标对象之外,此执行上下文还会接收指向调用执行上下文的词法环境的链接,这意味着在外部词法环境中声明的变量(在上面的例子中为ab)可从 获得ec

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

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

解决方案 3:

前言:这个答案是在以下问题时写的:

就像老艾伯特说的:“如果你不能向一个六岁的孩子解释清楚,那你自己真的不懂。”我曾经尝试向一个 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();

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

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

...实际上是一位公主,内心却有一个小女孩。

解决方案 4:

认真对待这个问题,我们应该找出一个典型的 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

运行代码片段Hide results展开片段

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

  • 每次makeKitchen()调用时,都会创建一个具有自己单独内容的新闭包trashBags

  • trashBags变量是每个厨房内部的局部变量,外部无法访问,但getTrashBag属性的内部函数可以访问它。

  • 每次函数调用都会创建一个闭包,但没有必要保留闭包,除非可以从闭包外部调用可以访问闭包内部的内部函数。getTrashBag在这里,使用函数返回对象就是这么做的。

解决方案 5:

稻草人

我需要知道按钮被点击了多少次,并且在每第三次点击时执行某些操作......

相当明显的解决方案

// 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>

运行代码片段Hide results展开片段

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

考虑此选项

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>

运行代码片段Hide results展开片段

请注意这里几点。

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

简单的一行闭包

//          _______________________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 注意事项)

解决方案 6:

闭包很难解释,因为它们用于使某些行为起作用,而每个人都直观地期望这些行为会起作用。我发现解释闭包的最好方法(以及了解闭包作用的方法)是想象没有闭包的情况:

const makePlus = function(x) {
    return function(y) { return x + y; };
}

const plus5 = makePlus(5);
console.log(plus5(3));

运行代码片段Hide results展开片段

如果 JavaScript不知道闭包会发生什么?只需将最后一行中的调用替换为其方法主体(这基本上就是函数调用所做的事情),您将获得:

console.log(x + 3);

现在, 的定义在哪里x?我们没有在当前作用域中定义它。唯一的解决方案是让plus5 带动其作用域(或者更确切地说,其父作用域)。这样,x就定义明确,并且绑定到值 5。

解决方案 7:

结论

闭包是函数和其外部词法(即书面)环境之间的链接,这样在该环境中定义的标识符(变量、参数、函数声明等)在函数内部是可见的,无论何时何地调用该函数。

细节

按照 ECMAScript 规范的术语来说,闭包可以说是由[[Environment]]每个函数对象的引用来实现的,它指向定义该函数的词法环境。

当通过内部方法调用函数时[[Call]][[Environment]]函数对象上的引用将被复制到新创建的执行上下文(堆栈框架)的环境记录的外部环境引用中。

在下面的例子中,函数f关闭了全局执行上下文的词法环境:

function f() {}

在下面的例子中, functionh关闭了 function 的词法环境g,而后者又关闭了全局执行上下文的词法环境。

function g() {
    function h() {}
}

如果内部函数由外部函数返回,则外部词法环境将在外部函数返回后继续存在。这是因为如果内部函数最终被调用,则外部词法环境需要可用。

在以下示例中,函数j关闭了函数的词法环境i,这意味着在函数完成执行之后很长时间,变量x在函数内部仍然可见:j`i`

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

运行代码片段Hide results展开片段

在闭包中,外部词法环境中的变量本身可用,而不是副本。

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

运行代码片段Hide results展开片段

词法环境链通过外部环境引用在执行上下文之间建立链接,形成作用域链并定义从任何给定函数可见的标识符。

请注意,为了提高清晰度和准确性,该答案与原来的答案相比有较大改变。

解决方案 8:

好的,6 岁的闭包迷。你想听听最简单的闭包示例吗?

让我们想象一下下一种情况:司机坐在车里。那辆车在飞机里。飞机在机场。司机能够访问车外但在飞机内的东西,即使飞机离开机场,也是一种闭包。就是这样。当你 27 岁时,看看更详细的解释或下面的例子。

以下是我将飞机故事转换成代码的方法。

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");

运行代码片段Hide results展开片段

解决方案 9:

这是为了澄清其他一些答案中出现的关于闭包的几个(可能的)误解。

  • 闭包不仅在返回内部函数时创建。事实上,封闭函数根本不需要返回即可创建其闭包。您可以将内部函数分配给外部作用域中的变量,或将其作为参数传递给另一个函数,该函数可以立即调用或稍后随时调用。因此,封闭函数的闭包可能在调用封闭函数时立即创建,因为任何内部函数都可以访问该闭包,无论是在封闭函数返回之前还是之后。

  • 闭包不会引用其范围内变量的旧值的副本。变量本身是闭包的一部分,因此访问其中一个变量时看到的值是访问时的最新值。这就是为什么在循环内创建的内部函数可能很棘手,因为每个函数都可以访问相同的外部变量,而不是在创建或调用函数时获取变量的副本。

  • 闭包中的“变量”包括在函数内声明的任何命名函数。它们还包括函数的参数。闭包还可以访问其包含的闭包的变量,直至全局范围。

  • 闭包会占用内存,但不会导致内存泄漏,因为 JavaScript 本身会清理其自身未引用的循环结构。当 Internet Explorer 无法断开引用闭包的 DOM 属性值,从而保留对可能的循环结构的引用时,就会产生与闭包相关的内存泄漏。

解决方案 10:

我之前写了一篇博客文章来解释闭包。以下是我关于闭包的说明,以及为什么需要闭包。

闭包是一种让函数拥有持久的、私有变量的方法——也就是说,只有一个函数知道的变量,它可以跟踪以前运行时的信息。

从这个意义上来说,它们让函数表现得有点像具有私有属性的对象。


例子
var iplus1= (function () 
{
    var plusCount = 0;
    return function () 
    {
        return ++plusCount;
    }
})();

这里外部自调用匿名函数只运行一次并将plusCount变量设置为 0,并返回内部函数表达式。

而内部函数可以访问plusCount变量。现在,每次我们调用该函数时iplus1(),内部函数都会增加plusCount变量。

要记住的一点是,页面上的其他脚本都无法访问plusCount变量,改变变量的唯一方法plusCount是通过iplus1函数。


阅读更多以供参考:那么这些封闭的东西是什么?

解决方案 11:

原始问题有这样一句话:

如果你不能向一个六岁的孩子解释清楚,那说明你自己真的还不明白。

我尝试向一个真正的六岁孩子这样解释:

你知道成年人可以拥有一所房子,并称其为家吗?当妈妈有了孩子,孩子实际上并不拥有任何东西,对吧?但是孩子的父母拥有一所房子,所以每当有人问“你的家在哪里?”时,孩子可以回答“那所房子!”,并指向父母的房子。

“封闭性”是指孩子始终能够(即使在国外)想到自己的家,尽管这所房子实际上是父母的。

解决方案 12:

闭包很简单:

下面的简单示例涵盖了 JavaScript 闭包的所有要点。*
 

这是一家生产可以进行加法和乘法运算的计算器的工厂:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

关键点:每次调用都会make_calculator创建一个新的局部变量,该变量在返回后很长时间内仍可供该计算器和函数n继续使用。add`multiply`make_calculator

如果您熟悉堆栈框架,那么这些计算器看起来很奇怪:它们如何在返回n后继续访问make_calculator?答案是想象 JavaScript 不使用“堆栈框架”,而是使用“堆框架”,它们可以在使它们返回的函数调用后继续存在。

add像和 这样的内部函数multiply,它们访问在外部函数*中声明的变量,被称为闭包*。

这就是关于闭包的全部内容。


例如,它涵盖了另一个答案中给出的“傻瓜闭包”文章中的所有要点,除了示例 6,它只是表明变量可以在声明之前使用,这是一个值得了解的事实,但与闭包完全无关。它还涵盖了已接受答案中的所有要点,除了以下几点:(1) 函数将其参数复制到局部变量(命名函数参数)中,以及 (2) 复制数字会创建一个新数字,但复制对象引用会为您提供对同一对象的另一个引用。这些也很好了解,但同样与闭包完全无关。它也与这个答案中的例子非常相似,但更短一些,不那么抽象。它没有涵盖这个答案或这个评论的要点,即 JavaScript 使得将循环变量的当前*值插入内部函数变得困难:“插入”步骤只能通过封闭内部函数并在每次循环迭代中调用的辅助函数来完成。 (严格来说,内部函数访问辅助函数的变量副本,而不是插入任何东西。)同样,在创建闭包时非常有用,但不是闭包的一部分或工作原理。由于闭包在函数式语言(如 ML)中的工作方式不同,变量绑定到值而不是存储空间,因此不断有人们以一种方式(即“插入”方式)理解闭包,而这种方式对于 JavaScript 来说是完全不正确的,因为 JavaScript 中的变量始终绑定到存储空间,而不是值。

**任何外部函数(如果有多个嵌套),甚至在全局上下文中,正如该答案明确指出的那样。

解决方案 13:

您能向 5 岁的孩子解释闭包吗?*

我仍然认为谷歌的解释非常有效并且简洁:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

证明此示例即使内部函数不返回也会创建闭包

*AC# 问题

解决方案 14:

我倾向于通过好坏比较来更好地学习。我喜欢先看到工作代码,然后看到人们可能遇到的不工作代码。我整理了一个 jsFiddle来进行比较,并尝试将差异归结为我能想到的最简单的解释。

正确完成闭包:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index of arr) {
    console.log(arr[index]());
}
  • 上面的代码在createClosure(n)循环的每次迭代中都会被调用。请注意,我命名变量n是为了强调它是在新的函数作用域中创建的index变量,与绑定到外部作用域的变量不同。

  • 这将创建一个新范围并n绑定到该范围;这意味着我们有 10 个独立的范围,每个迭代一个。

  • createClosure(n)返回一个返回该范围内的 n 的函数。

  • 每个范围内n都绑定到createClosure(n)调用时所具有的任何值,因此返回的嵌套函数将始终返回调用n时所具有的值。createClosure(n)

错误的闭包:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index of badArr) {
    console.log(badArr[index]());
}
  • 在上面的代码中,循环被移到createClosureArray()函数内部,函数现在只返回完成的数组,乍一看这似乎更直观。

  • 可能不太明显的是,由于createClosureArray()只调用一次,因此只为该函数创建一个作用域,而不是为循环的每次迭代创建一个作用域。

  • 在此函数中,定义了一个名为的变量index。循环运行并将函数添加到返回的数组中index。请注意,在函数index中定义createClosureArray,该函数只会被调用一次。

  • 因为函数内只有一个作用域createClosureArray()index所以 仅与该作用域内的值绑定。换句话说,每次循环更改 的值时index,它都会更改该作用域内引用它的所有内容的值。

  • 添加到数组中的所有函数都index从定义它的父作用域返回相同的变量,而不是像第一个示例那样从 10 个不同的作用域返回 10 个不同的变量。最终结果是所有 10 个函数都从同一作用域返回相同的变量。

  • 循环完成并index修改后,最终值为 10,因此添加到数组的每个函数都会返回单个index变量的值,该值现在设置为 10。

结果

正确完成闭包

n = 0

n = 1

n = 2

n

= 3

n = 4

n = 5 n

= 6 n = 7

n = 8

n = 9

错误的闭包

n = 10

n = 10

n

= 10

n = 10

n = 10

n = 10 n

= 10 n = 10

n = 10 n = 10

n = 10

解决方案 15:

维基百科关于闭包的内容:

在计算机科学中,闭包是一个函数以及该函数的非本地名称(自由变量)的引用环境。

从技术上讲,在JavaScript中,每个函数都是一个闭包。它总是可以访问周围范围内定义的变量。

由于JavaScript 中的作用域定义构造是一个函数,而不是像其他许多语言中的代码块,因此,我们通常所说的JavaScript闭包是一个与已经执行的周围函数中定义的非局部变量一起工作的函数

闭包通常用于创建具有一些隐藏私有数据的函数(但并非总是如此)。

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

紧急医疗服务

上面的例子使用了一个匿名函数,该函数只执行一次。但不一定非要这样。它可以被命名(例如mkdb)并在稍后执行,每次调用时都会生成一个数据库函数。每个生成的函数都有自己的隐藏数据库对象。闭包的另一个使用示例是,我们不返回一个函数,而是返回一个包含多个用于不同目的的函数的对象,每个函数都可以访问相同的数据。

解决方案 16:

孩子们永远不会忘记他们与父母分享的秘密,即使父母去世了。这就是函数的闭包。

JavaScript 函数的秘密在于私有变量

var parent = function() {
 var name = "Mary"; // secret
}

每次调用它时,都会创建局部变量“name”,并将其命名为“Mary”。每次函数退出时,变量都会丢失,名称也会被遗忘。

您可能已经猜到了,因为每次调用函数时都会重新创建变量,而且没有其他人会知道它们,所以一定有一个秘密的地方存储它们。它可以被称为密室堆栈本地作用域,但这并不重要。我们知道它们就在那里,隐藏在内存中的某个地方。

但是,在 JavaScript 中,有一个非常特殊的事情,即在其他函数内部创建的函数也可以知道其父级的局部变量,并且在它们存在期间保留这些变量。

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

因此,只要我们在父函数中,它就可以创建一个或多个子函数,这些子函数确实共享来自秘密位置的秘密变量。

但可悲的是,如果子函数也是其父函数的私有变量,那么当父函数结束时它也会消亡,秘密也会随之消亡。

所以为了活下去,孩子必须趁还来得及的时候离开

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

现在,尽管玛丽“不再逃亡”,但对她的记忆并没有消失,她的孩子也将永远记得她的名字和他们在一起时分享的其他秘密。

所以,如果你叫孩子“爱丽丝”,她会回应

child("Alice") => "My name is Alice, child of Mary"

需要说的就这些。

解决方案 17:

我整理了一份交互式 JavaScript 教程来解释闭包的工作原理。
什么是闭包?

以下是其中一个例子:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here

解决方案 18:

我不明白为什么这里的答案如此复杂。

这是一个闭包:

var a = 42;

function b() { return a; }

是的。你可能一天会用到很多次。

没有理由相信闭包是为了解决特定问题而采用的复杂设计技巧。不,闭包只是从函数声明(而不是运行)的位置的角度来看使用来自更高范围的变量。

现在它允许您做的事情可以更加精彩,请参阅其他答案。

解决方案 19:

闭包是指内部函数可以访问其外部函数中的变量。这可能是你能得到的关于闭包的最简单的一行解释。

内部函数不仅可以访问外部函数的变量,还可以访问外部函数的参数,如下例所示

例子

// Closure Example
function addNumbers(firstNumber, secondNumber) 
{
    var returnValue = "Result is : ";
    
    // This inner function has access to the outer function's variables & parameters
    function add() 
    {
        return returnValue + (firstNumber + secondNumber);
    }
    return add();
}

var result = addNumbers(10, 20);
console.log(result); //30

在此处输入图片描述

解决方案 20:

dlaliberte 给出的第一点示例:

闭包不仅在返回内部函数时创建。事实上,封闭函数根本不需要返回。您可以将内部函数分配给外部作用域中的变量,或将其作为参数传递给可以立即使用的另一个函数。因此,封闭函数的闭包可能在调用封闭函数时已经存在,因为任何内部函数在调用时都可以访问它。

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

解决方案 21:

我知道已经有很多解决方案,但我猜这个小而简单的脚本可以用来演示这个概念:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

解决方案 22:

Closures的作者对闭包的解释非常透彻,解释了为什么我们需要闭包,还解释了理解闭包所必需的 LexicalEnvironment。

总结如下:

如果访问一个变量,但它不是局部的,该怎么办?如下所示:

在此处输入图片描述

在这种情况下,解释器在外部对象中找到该变量LexicalEnvironment

该过程包括两个步骤:

  1. 首先,当创建函数 f 时,它不是在空的空间中创建的。有一个当前的 LexicalEnvironment 对象。在上面的例子中,它是窗口(函数创建时 a 未定义)。

在此处输入图片描述

当创建一个函数时,它会获得一个名为 [[Scope]] 的隐藏属性,它引用当前的 LexicalEnvironment。

在此处输入图片描述

如果读取了变量,但在任何地方都找不到,就会产生错误。

嵌套函数

函数可以嵌套,形成词法环境链,也可以称为作用域链。

在此处输入图片描述

因此,函数 g 可以访问 g、a 和 f。

闭包

嵌套函数可以在外部函数完成后继续存在:

在此处输入图片描述

标记词汇环境:

在此处输入图片描述

正如我们所见,this.say是用户对象中的一个属性,因此在用户完成后它会继续存在。

如果你还记得的话,当this.say创建时,它(和每个函数一样)会获得this.say.[[Scope]]对当前词法环境的内部引用。因此,当前用户执行的词法环境保留在内存中。用户的所有变量也是它的属性,因此它们也会被小心地保存,而不是像通常那样被丢弃。

关键在于确保如果内部函数将来想要访问外部变量,它能够这样做。

总结一下:

  1. 内部函数保留对外部 LexicalEnvironment 的引用。

  2. 即使外部函数已经完成,内部函数也可以随时访问其中的变量。

  3. 浏览器将 LexicalEnvironment 及其所有属性(变量)保存在内存中,直到有一个内部函数引用它。

这被称为闭包。

解决方案 23:

你要过夜并邀请丹。你告诉丹带一个 XBox 控制器。

丹邀请保罗。丹要求保罗带一个控制器。派对上一共带了多少个控制器?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");

解决方案 24:

JavaScript 函数可以访问其:

  1. 参数

  2. 局部变量(即其局部变量和局部函数)

  3. 环境,包括:

* 全局变量,包括 DOM
* 外部函数中的任何内容

如果一个函数访问它的环境,那么该函数就是闭包。

请注意,外部函数不是必需的,但它们确实提供了我在此未讨论的好处。通过访问其环境中的数据,闭包可以使该数据保持活动状态。在外部/内部函数的子情况下,外部函数可以创建本地数据并最终退出,但是,如果在外部函数退出后任何内部函数仍然存在,则内部函数将使外部函数的本地数据保持活动状态。

使用全局环境的闭包示例:

假设 Stack Overflow 的“赞成”和“反对”按钮事件被实现为闭包 voteUp_click 和 voteDown_click,它们可以访问全局定义的外部变量 isVotedUp 和 isVotedDown。(为了简单起见,我指的是 StackOverflow 的问题投票按钮,而不是答案投票按钮的数组。)

当用户点击 VoteUp 按钮时,voteUp_click 函数会检查 isVotedDown == true 是否确定是投赞成票还是仅取消反对票。函数 voteUp_click 是一个闭包,因为它正在访问其环境。

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

所有这四个函数都是闭包,因为它们都访问它们的环境。

解决方案 25:

作为一个 6 岁孩子的父亲,目前正在教年幼的孩子(而且是编程方面的新手,没有接受过正规教育,因此需要纠正),我认为通过动手实践来学习是最好的。如果 6 岁的孩子已经准备好理解闭包是什么,那么他们就大到可以自己尝试了。我建议将代码粘贴到 jsfiddle.net 中,解释一下,然后让他们自己创作一首独特的歌曲。下面的解释性文字可能更适合 10 岁的孩子。

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "
" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "
" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "
" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "
" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

指示

数据:数据是事实的集合。它可以是数字、文字、测量值、观察值,甚至只是事物的描述。你无法触摸、闻到或品尝它。你可以写下来、说出来和听到它。你可以用计算机用它来创造触觉、嗅觉和味觉。计算机可以使用代码使它变得有用。

CODE: 以上所有文字都称为代码。它使用 JavaScript 编写。

JAVASCRIPT:JavaScript 是一种语言。就像英语、法语或中文都是语言一样。计算机和其他电子处理器可以理解许多语言。JavaScript 要想被计算机理解,就需要一个解释器。想象一下,如果一位只会说俄语的老师来学校教你的课。当老师说“все садятся”时,全班同学都听不懂。但幸运的是,班上有一个俄罗斯学生,他告诉大家这意味着“大家坐下”——所以大家都坐了下来。课堂就像一台计算机,而俄罗斯学生就是解释器。对于 JavaScript,最常见的解释器称为浏览器。

浏览器:当您通过计算机、平板电脑或手机连接到互联网访问网站时,您会使用浏览器。您可能知道的例子有 Internet Explorer、Chrome、Firefox 和 Safari。浏览器可以理解 JavaScript 并告诉计算机需要做什么。JavaScript 指令称为函数。

函数:JavaScript 中的函数就像一个工厂。它可能是一个只有一台机器的小工厂。或者它可能包含许多其他小工厂,每个工厂都有许多机器执行不同的工作。在现实生活中的服装厂中,您可能会有大量的布料和线轴投入生产,然后生产出 T 恤和牛仔裤。我们的 JavaScript 工厂只处理数据,它不能缝纫、钻孔或熔化金属。在我们的 JavaScript 工厂中,数据输入和输出。

所有这些数据听起来有点无聊,但实际上非常酷;我们可能有一个功能可以告诉机器人晚餐做什么。假设我邀请你和你的朋友来我家。你最喜欢鸡腿,我喜欢香肠,你的朋友总是想要你想要的东西,而我的朋友不吃肉。

我没有时间去购物,所以该功能需要知道冰箱里有什么,以便做出决定。每种食材的烹饪时间都不同,我们希望机器人能同时将所有食材热腾腾地端上来。我们需要向该功能提供有关我们喜欢什么的数据,该功能可以与冰箱“对话”,该功能可以控制机器人。

函数通常具有名称、圆括号和大括号。如下所示:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

请注意/*...*/,并//停止浏览器读取代码。

NAME:您可以调用函数,只要您想要任何单词即可。示例“cookMeal”是将两个单词连接在一起并在第二个单词开头添加大写字母的典型示例 - 但这不是必需的。它不能包含空格,也不能单独为数字。

括号:“括号”()是 JavaScript 函数工厂门上的信箱或街道上的邮箱,用于向工厂发送信息包。有时邮箱可能会被标记 cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime),在这种情况下,您知道必须向其提供哪些数据。

支架:看起来像这样的“支架”{}是我们工厂的有色玻璃窗。从工厂内部可以看到外面,但从外面看不到里面。

上面的长代码示例

我们的代码以单词function开头,所以我们知道它是一个函数!然后是函数的名称sing - 这是我自己对函数的描述。然后是括号()。括号总是用于函数。有时它们是空的,有时里面有内容。这个有一个单词 in: (person)。在这之后有一个括号,像这样{。这标志着函数sing()的开始。它有一个括号,标志着sing()的结束,就像这样}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

所以这个函数可能与唱歌有关,可能需要一些关于某人的数据。它里面有指令来处理这些数据。

现在,在函数sing()之后,靠近代码末尾有这样一行

var person="an old lady";

变量:var代表“变量”。变量就像一个信封。信封外面标着“人”。信封里面有一张纸条,上面写着我们函数需要的信息,一些字母和空格像一根绳子一样连接在一起(这叫做字符串),组成一个短语“老太太”。我们的信封可以包含其他类型的东西,如数字(称为整数)、指令(称为函数)、列表(称为数组)。因为这个变量写在所有括号之外{},而且当你在括号内时,你可以透过有色玻璃窗看到外面,所以这个变量在代码的任何地方都可以看到。我们称之为“全局变量”。

全局变量:person是一个全局变量,这意味着如果您将其值从“老太太”更改为“年轻人”,则该将一直保持为年轻人,直到您决定再次更改它,并且代码中的任何其他函数都可以看到它是一个年轻人。按下按钮F12或查看选项设置以打开浏览器的开发人员控制台,然后键入“person”以查看此值是什么。键入person="a young man"以更改它,然后再次键入“person”以查看它已更改。

之后我们有

sing(person);

这行代码正在调用函数,就像在调用一只狗一样

“快来唱歌,快来找!”

当浏览器加载完 JavaScript 代码并到达此行时,它将启动该函数。我把此行放在最后,以确保浏览器拥有运行它所需的所有信息。

函数定义动作 - 主要函数是关于唱歌的。它包含一个名为firstPart的变量,该变量适用于关于人的歌唱,适用于歌曲的每一节:“有“ + 人 + “吞咽”。如果您在控制台中输入firstPart,您将不会得到答案,因为变量被锁定在函数中 - 浏览器无法看到括号内的有色窗口。

闭包:闭包是位于sing()大函数内部的较小函数。大工厂内部的小工厂。它们各自都有自己的括号,这意味着它们内部的变量从外部是看不到的。这就是为什么变量的名称(creationresult)可以在闭包中重复,但值不同。如果您在控制台窗口中输入这些变量名称,您将无法获得它的值,因为它被两层有色窗口隐藏了。

所有闭包都知道sing()函数的变量firstPart是什么,因为它们可以从有色窗户向外看。

关闭后,

fly();
spider();
bird();
cat();

sing() 函数将按照给定的顺序调用每个函数。然后 sing() 函数的工作就完成了。

解决方案 26:

好的,与一个 6 岁的孩子交谈,我可能会使用以下联想。

想象一下 - 你正在屋子里和弟弟妹妹们玩耍,你拿着玩具四处走动,并把一些玩具带到了哥哥的房间里。过了一会儿,你哥哥从学校回来,回到他的房间,他把门锁在里面,所以现在你再也无法直接拿到留在那里的玩具了。但你可以敲门,向你哥哥要玩具。这叫做玩具的闭包;你的哥哥为你做了这件事,他现在进入了外部作用域

与以下情况进行比较:一扇门被通风口锁住,里面没有人(一般功能执行),然后发生一些局部火灾并烧毁房间(垃圾收集器:D),然后建造了一个新房间,现在您可能会在那里留下其他玩具(新功能实例),但永远不会得到留在第一个房间实例中的相同玩具。

对于有一定水平的孩子,我会写一些类似下面的内容。虽然不完美,但能让你感受到它是什么:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

如您所见,无论房间是否上锁,仍可通过 brothers 访问留在房间里的玩具。这里有一个 jsbin可以玩一下。

解决方案 27:

JavaScript 中的函数不仅仅是对一组指令的引用(就像在 C 语言中一样),它还包含一个隐藏的数据结构,该结构由对其使用的所有非局部变量(捕获变量)的引用组成。这种两部分函数称为闭包。JavaScript 中的每个函数都可以被视为一个闭包。

闭包是具有状态的函数。它与“this”有些相似,因为“this”也为函数提供状态,但函数和“this”是独立的对象(“t​​his”只是一个花哨的参数,将其永久绑定到函数的唯一方法是创建闭包)。虽然“this”和函数始终是分开的,但函数不能与其闭包分开,并且语言不提供访问捕获变量的方法。

因为词法嵌套函数引用的所有这些外部变量实际上都是其词法封闭函数链中的局部变量(可以将全局变量假定为某个根函数的局部变量),并且函数的每次执行都会创建其局部变量的新实例,因此,每次执行返回(或以其他方式将其传出,例如将其注册为回调)嵌套函数的函数时都会创建一个新的闭包(具有其自己可能唯一的一组引用的非局部变量,这些变量代表其执行上下文)。

此外,必须明白,JavaScript 中的局部变量不是在堆栈框架上创建的,而是在堆上创建的,并且只有在没有人引用它们时才会销毁。当函数返回时,对其局部变量的引用会减少,但如果在当前执行期间它们成为闭包的一部分并且仍然被其词法嵌套函数引用(只有当对这些嵌套函数的引用被返回或以其他方式转移到某个外部代码时才会发生这种情况),则它们仍然可以为非空。

举个例子:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();

解决方案 28:

适合六岁小孩的答案(假设他知道什么是函数,什么是变量,什么是数据):

函数可以返回数据。您可以从函数返回一种数据,即另一个函数。当返回新函数时,创建它的函数中使用的所有变量和参数都不会消失。相反,父函数会“关闭”。换句话说,除了它返回的函数之外,没有任何东西可以查看它的内部并查看它使用的变量。新函数具有一种特殊能力,可以回顾创建它的函数内部并查看其中的数据。

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

另一个非常简单的解释方法是从范围的角度:

任何时候在较大范围内创建较小范围时,较小范围将始终能够看到较大范围内的内容。

解决方案 29:

也许有点超出六岁小孩所能理解的范围,但这些例子帮助我理解了 JavaScript 中闭包的概念。

闭包是可以访问另一个函数的作用域(其变量和函数)的函数。创建闭包的最简单方法是使用函数内的函数;原因是在 JavaScript 中,函数始终可以访问其包含函数的作用域。

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

运行代码片段Hide results展开片段

警告:猴子

在上面的例子中,调用了 outerFunction,而后者又调用了 innerFunction。请注意 outerVar 如何可供 innerFunction 使用,这可以通过正确提醒 outerVar 的值来证明。

现在考虑以下情况:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

运行代码片段Hide results展开片段

警告:猴子

referenceToInnerFunction 设置为 outerFunction(),它仅返回对 innerFunction 的引用。调用 referenceToInnerFunction 时,它会返回 outerVar。同样,如上所述,这表明 innerFunction 可以访问 outerVar(outerFunction 的一个变量)。此外,值得注意的是,即使在 outerFunction 执行完毕后,它仍保留此访问权限。

事情变得非常有趣。如果我们要删除 outerFunction,比如将其设置为 null,您可能会认为 referenceToInnerFunction 将失去对 outerVar 值的访问权限。但事实并非如此。

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

运行代码片段Hide results展开片段

警告:猴子 警告:猴子

但这是为什么呢?既然 outerFunction 已被设置为 null,referenceToInnerFunction 怎么还能知道 outerVar 的值呢?

referenceToInnerFunction 仍可访问 outerVar 的值,原因是当首次通过将 innerFunction 放置在 outerFunction 内部来创建闭包时,innerFunction 会将对 outerFunction 范围(其变量和函数)的引用添加到其范围链中。这意味着 innerFunction 具有指向 outerFunction 的所有变量(包括 outerVar)的指针或引用。因此,即使 outerFunction 已完成执行,或者即使它被删除或设置为 null,其范围内的变量(如 outerVar)仍会保留在内存中,因为 innerFunction 已返回给 referenceToInnerFunction 的部分对它们的未完成引用。要真正从内存中释放 outerVar 和 outerFunction 的其余变量,您必须摆脱对它们的未完成引用,例如通过将 referenceToInnerFunction 也设置为 null。

//////////

关于闭包,还有两点需要注意。首先,闭包总是可以访问其包含函数的最后一个值。

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

运行代码片段Hide results展开片段

警报:大猩猩

其次,当创建闭包时,它会保留对其封闭函数的所有变量和函数的引用;它无法挑选。但因此,闭包应谨慎使用,或至少要谨慎使用,因为它们可能占用大量内存;在包含函数执行完毕后,许多变量仍会保留在内存中很长时间。

解决方案 30:

我会直接向他们推荐Mozilla Closures 页面。这是我发现的关于闭包基础知识和实际用法的最好、最简洁、最简单的解释。强烈推荐给任何学习 JavaScript 的人。

是的,我甚至会向 6 岁的孩子推荐它——如果 6 岁的孩子正在学习闭包,那么他们就有资格理解文章中提供的简洁而简单的解释。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用