协慌网

登录 贡献 社区

JavaScript 闭包如何工作?

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

我已经看过维基百科上给出的 Scheme 示例 ,但遗憾的是它并没有帮助。

答案

适用于初学者的 JavaScript 闭包

由 Morris 于星期二提交,2006-02-21 10:19。社区编辑以来。

关闭不是魔术

这个页面解释了闭包,以便程序员可以理解它们 - 使用工作的 JavaScript 代码。它不适合大师或功能程序员。

一旦核心概念被弄清楚,关闭并不难理解。但是,通过阅读任何理论或学术导向的解释,他们无法理解!

本文面向具有主流语言编程经验的程序员,并且可以阅读以下 JavaScript 函数:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');

两个简短的摘要

  • 当函数(foo)声明其他函数(bar 和 baz)时,在函数退出时不会销毁在 foo 中创建的局部变量族。变量只会变得对外界不可见。因此,Foo 可以巧妙地返回功能栏和 baz,并且他们可以通过这个封闭的变量系列(“封闭”)继续读取,写入和通信,其他任何人都无法干涉,甚至没有人打电话 foo 将来再来一次。

  • 闭包是支持一流功能的一种方式; 它是一个表达式,可以引用其范围内的变量(首次声明时),分配给变量,作为参数传递给函数,或作为函数结果返回。

闭包的一个例子

以下代码返回对函数的引用:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

大多数 JavaScript 程序员都会理解在上面的代码中如何将函数的引用返回给变量( say2 )。如果你不这样做,那么你需要先了解它,然后才能学习闭包。使用 C 的程序员会将函数视为返回指向函数的指针,并且变量saysay2都是指向函数的指针。

指向函数的 C 指针和对函数的 JavaScript 引用之间存在严重差异。在 JavaScript 中,您可以将函数引用变量视为既包含指向函数的指针, 包含指向闭包的隐藏指针。

上面的代码有一个闭包因为匿名函数function() { console.log(text); }是另一个函数, 声明sayHello2()在这个例子中。在 JavaScript 中,如果在另一个函数中使用function关键字,则创建一个闭包。

在 C 和大多数其他常用语言中, 函数返回后,所有局部变量都不再可访问,因为堆栈帧被销毁。

在 JavaScript 中,如果在另一个函数中声明一个函数,那么从函数返回后,外部函数的局部变量仍然可以访问。这在上面说明,因为我们在从sayHello2()返回后调用函数say2() sayHello2() 。请注意,我们调用的代码引用变量text ,它是函数sayHello2()局部变量

function() { console.log(text); } // Output of say2.toString();

查看say2.toString()的输出,我们可以看到代码引用了变量text 。匿名函数可以引用包含值'Hello Bob' text ,因为sayHello2()的局部变量已在闭包中秘密保持活动状态。

天才是在 JavaScript 中一个函数引用也有一个秘密引用它所创建的闭包 - 类似于委托是方法指针加上对象的秘密引用。

更多例子

出于某种原因,当你阅读它们时,闭包似乎很难理解,但是当你看到一些例子时,它们的工作方式就变得清晰了(我花了一段时间)。我建议您仔细研究这些示例,直到您了解它们的工作原理。如果你开始使用闭包而没有完全理解它们是如何工作的,你很快就会创建一些非常奇怪的错误!

例 3

此示例显示未复制局部变量 - 它们通过引用保留。即使外部函数存在,就好像堆栈框架在内存中保持活跃!

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43

例 4

所有三个全局函数都对同一个闭包有一个共同的引用,因为它们都是在一次调用setupSomeGlobals()

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

这三个函数共享访问同一个闭包 - 当定义了三个函数时, setupSomeGlobals()的局部变量。

请注意,在上面的示例中,如果再次调用setupSomeGlobals() ,则会创建一个新的闭包(stack-frame!)。旧的gLogNumbergIncreaseNumbergSetNumber变量将被具有新闭包的函数覆盖。 (在 JavaScript 中,当你声明另一个函数内部功能,内部功能(S)是 / 再次每个外侧函数被调用时重建)。

例 5

此示例显示闭包包含在退出之前在外部函数内声明的任何局部变量。请注意,变量alice实际上是在匿名函数之后声明的。首先声明匿名函数,并且当调用该函数时,它可以访问alice变量,因为alice在同一范围内(JavaScript 执行变量提升 )。另外sayAlice()()只是直接调用从sayAlice()返回的函数引用 - 它与先前所做的完全相同但没有临时变量。

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

Tricky:还要注意, say变量也在闭包内部,并且可以由任何其他可能在sayAlice()声明的函数访问,或者可以在 inside 函数内递归访问。

例 6

对于很多人来说,这是一个真正的问题,所以你需要了解它。如果要在循环中定义函数,请务必小心:闭包中的局部变量可能不会像您首先想到的那样起作用。

您需要了解 Javascript 中的 “变量提升” 功能才能理解此示例。

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    // Using j only to help prevent confusion -- could use i.
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //logs "item2 undefined" 3 times

result.push( function() {console.log(item + ' ' + list[i])}将一个匿名函数的引用添加到结果数组三次。如果你不熟悉匿名函数想到的话就如:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);

请注意,运行该示例时,会记录"item2 undefined"三次!这是因为就像前面的例子一样, buildList的局部变量只有一个闭包( resultiitem )。当在fnlist[j]()行上调用匿名函数时; 它们都使用相同的单个闭包,并且它们在一个闭包中使用iitem的当前值(其中i的值为3因为循环已完成,而item的值为'item2' )。请注意,我们从 0 开始索引,因此item的值为item2 。并且 i ++ 会将i增加到值3

查看使用变量item的块级声明(通过let关键字)而不是通过var关键字的函数范围变量声明时会发生什么可能会有所帮助。如果进行了更改,那么数组result中的每个匿名函数都有自己的闭包; 运行示例时,输出如下:

item0 undefined
item1 undefined
item2 undefined

如果变量i也使用let而不是var定义,则输出为:

item0 1
item1 2
item2 3

例 7

在最后一个示例中,每次调用 main 函数都会创建一个单独的闭包。

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

摘要

如果一切看起来都不清楚,那么最好的办法是玩这些例子。阅读解释要比理解例子困难得多。我对闭合和堆叠框架等的解释在技术上并不正确 - 它们是用于帮助理解的粗略简化。一旦基本想法被理解,您可以稍后获取详细信息。

最后一点:

  • 每当在另一个函数中使用function ,都会使用闭包。
  • 每当在eval()内部使用eval() ,都会使用闭包。你eval的文本可以引用函数的局部变量,在eval你甚至可以使用eval('var foo = …')创建新的局部变量eval('var foo = …')
  • 函数内部使用new Function(…)函数构造函数)时,它不会创建闭包。 (新函数不能引用外部函数的局部变量。)
  • JavaScript 中的闭包就像保留所有局部变量的副本一样,就像函数退出时一样。
  • 最好认为闭包始终只是函数的一个入口,并且局部变量被添加到该闭包中。
  • 每次调用带闭包的函数时,都会保留一组新的局部变量(假设函数内部包含函数声明,并且返回对该函数内部的引用,或者以某种方式保留外部引用) )。
  • 两个函数可能看起来像具有相同的源文本,但由于它们的 “隐藏” 闭包而具有完全不同的行为。我不认为 JavaScript 代码实际上可以找出函数引用是否有闭包。
  • 如果您正在尝试进行任何动态源代码修改(例如: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola')); ),如果myFunction是一个闭包,它将无法工作(当然,你甚至不会想到在运行时进行源代码字符串替换,但是...)。
  • 可以在函数&mdash 中的函数声明中获取函数声明,并且可以在多个级别获得闭包。
  • 我认为通常闭包是函数和捕获的变量的术语。请注意,我在本文中没有使用该定义!
  • 我怀疑 JavaScript 中的闭包与函数式语言中的闭包有所不同。

链接

谢谢

如果您刚刚学习了闭包(在这里或其他地方!),那么我对您提出的任何可能使本文更清晰的更改的反馈感兴趣。发送电子邮件至 morrisjohns.com(morris_closure @)。请注意,我不是 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可以访问被定义为foo参数的x ,并且它还可以从foo访问tmp

一个关闭。函数不必返回以便被称为闭包。 只需访问直接词法范围之外的变量就可以创建一个闭包

function foo(x) {
  var tmp = 3;

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

var bar = foo(2); // bar is now a closure.
bar(10);

上面的函数也会记录 16,因为bar仍然可以引用xtmp ,即使它不再直接在范围内。

然而,由于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 函数时,会创建一个新的执行上下文。与函数参数和父对象一起,此执行上下文还接收在其外部声明的所有变量(在上面的示例中,“a” 和 “b”)。

可以通过返回它们的列表或将它们设置为全局变量来创建多个闭包函数。所有这些都将引用相同的 x和相同的tmp ,它们不会制作自己的副本。

这里的数字x是一个字面数字。与 JavaScript 中的其他文字一样,当调用foo时,数字x复制foo作为其参数x

另一方面,JavaScript 在处理对象时总是使用引用。如果说,你用一个对象调用foo ,它返回的闭包将引用该原始对象!

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + tmp);
    x.memb = x.memb ? x.memb + 1 : 1;
    console.log(x.memb);
  }
}

var age = new Number(2);
var bar = foo(age); // bar is now a closure referencing age.
bar(10);

正如所料,每次调用bar(10)都会增加x.memb 。可能没有预料到的是, x只是指与age变量相同的对象!经过几次电话调barage.memb将是 2!此引用是 HTML 对象的内存泄漏的基础。

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

就像老阿尔伯特所说的那样:“如果你不能解释它给一个六岁的孩子,你自己真的不明白。” 我试着向一位 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();

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

但我们知道真相; 里面有公主的小女孩......

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