JavaScript闭包如何工作?


JavaScript闭包如何工作?


适用于初学者的JavaScript闭包

闭包不是魔术

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

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

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

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

两个简短的摘要

  • 当函数(foo)声明其他函数(bar和baz)时,在函数退出时不会销毁在foo中创建的局部变量族。这些变量只会变得对外界不可见。因此,Foo可以狡猾地返回函数bar和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上面代码中的variable()。如果你不这样做,那么你需要先了解它,然后才能学习闭包。使用C的程序员会将函数视为返回指向函数的指针,并且变量say和say2每个函数都是指向函数的指针。

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

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

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

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

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

查看输出say2.toString(),我们可以看到代码引用变量text。匿名函数可以引用text保存该值的值,'Hello Bob'因为局部变量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!)。老gLogNumber,gIncreaseNumber,gSetNumber变量被改写新的具有新功能关闭。(在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"

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