js闭包 JavaScript闭包如何工作?



15 Answers

每当在另一个函数中看到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对象的内存泄漏的基础。

闭包阮一峰

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

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




认真对待这个问题,我们应该找出一个典型的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函数返回对象就可以了。



闭包很难解释,因为它们被用来使某些行为工作,每个人都直观地期望工作。 我发现解释它们的最佳方式(以及学习它们的方式)是想象没有它们的情况:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

如果JavaScript 知道闭包会发生什么? 只需用它的方法体替换最后一行中的调用(基本上是函数调用的函数),你得到:

console.log(x + 3);

现在, x的定义在哪里? 我们没有在当前范围内定义它。 唯一的解决方案是让plus5 携带其范围(或者更确切地说,其父节点的范围)。 这样, x是明确定义的,它绑定到值5。




好的,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");




我在一段时间后写了一篇博文,解释了闭包。这就是我所说的关于你想要一个闭包的原因

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

从这个意义上讲,它们让函数有点像具有私有属性的对象。

全文:

那么封闭东西是什么?




我如何向一个六岁的孩子解释:

你知道成年人如何拥有房子,他们称之为家?当一个妈妈有一个孩子,孩子真的没有任何东西,对吧?但是它的父母拥有一所房子,所以每当有人问孩子“你的家在哪里?”时,他/她就可以回答“那所房子!”,并指向其父母的房子。一个“关闭”是孩子总是(即使在国外)能够说它有一个家的能力,即使它真的是父母拥有房子。




我倾向于通过GOOD / BAD比较来更好地学习。我喜欢看到工作代码后跟有人可能会遇到的非工作代码。我把一个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 in 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 in badArr) {
    console.log(badArr[index]());
}
  • 在上面的代码中,循环在createClosureArray()函数内移动,函数现在只返回完成的数组,乍一看似乎更直观。

  • 可能不明显的是,因为createClosureArray()只有在为此函数创建一个范围而不是为循环的每次迭代创建一个范围时才调用。

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

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

  • 添加到数组的所有函数都index从定义它的父作用域返回SAME 变量,而不是像第一个示例那样从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




我整理了一个交互式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



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

这是一个闭包:

var a = 42;

function b() { return a; }

是。 你可能每天都要多次使用它。


没有理由相信闭包是一个复杂的设计黑客来解决具体问题。不,闭包只是从声明函数(不运行)的角度使用来自更高范围的变量。

现在它允许你做什么可以更壮观,看到其他答案。




闭包是内部函数可以访问其外部函数中的变量的地方。这可能是闭包可以获得的最简单的一行解释。




你正在睡觉,你邀请丹。你告诉Dan带一个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.");



Closures的作者很好地解释了闭包,解释了为什么我们需要它们,并解释了理解闭包所必需的LexicalEnvironment。
以下是摘要:

如果访问变量,但它不是本地的,该怎么办? 像这儿:

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

该过程包括两个步骤:

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

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

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

嵌套函数

函数可以嵌套在另一个函数中,形成一个LexicalEnvironments链,也可以称为范围链。

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

关闭

外部函数完成后,嵌套函数可能会继续存在:

标记LexicalEnvironments:

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

如果你记得,什么时候this.say创建,它(作为每个函数)获得this.say.[[Scope]]当前LexicalEnvironment 的内部引用。因此,当前用户执行的LexicalEnvironment保留在内存中。User的所有变量也都是它的属性,因此它们也被仔细保存,而不是通常的垃圾。

重点是确保如果内部函数将来想要访问外部变量,它就能够这样做。

总结一下:

  1. 内部函数保留对外部LexicalEnvironment的引用。
  2. 即使外部函数完成,内部函数也可以随时访问它的变量。
  3. 浏览器将LexicalEnvironment及其所有属性(变量)保留在内存中,直到有一个引用它的内部函数。

这称为闭包。




好的,和一个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

如您所见,无论房间是否锁定,房间内的玩具仍可通过兄弟进入。这是一个jsbin来玩它。




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

闭包是具有状态的功能。它有点类似于“this”,意思是“this”也为函数提供状态但函数和“this”是单独的对象(“t​​his”只是一个奇特的参数,并且是将它永久绑定到a的唯一方法函数是创建一个闭包)。虽然“this”和函数总是分开存在,但函数不能与其闭包分离,并且语言无法访问捕获的变量。

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

此外,必须要理解的是,JavaScript中的局部变量不是在堆栈帧上创建的,而是在堆上创建的,只有在没有人引用它们时才会被销毁。当函数返回时,对其局部变量的引用会递减,但如果在当前执行期间它们成为闭包的一部分并且仍然由其词法嵌套函数引用(它们仅在引用时才会发生),它们仍然可以为非null。这些嵌套函数被返回或以其他方式传递给某些外部代码)。

一个例子:

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



我只是将它们指向Mozilla Closures页面。这是我发现的关闭基础知识和实际用法的最佳,最简洁和简单的解释。强烈建议任何学习JavaScript的人。

是的,我甚至推荐给一个6岁的孩子 - 如果这个6岁的孩子正在学习闭包,那么他们已经准备好理解文章中提供的简洁明了的解释是合乎逻辑的。




Related