Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

闭包:我让你云里雾里摸不着头脑,你怕不怕? #6

Open
lizhongzhen11 opened this issue Jun 26, 2018 · 0 comments
Open
Labels
js基础 Good for newcomers

Comments

@lizhongzhen11
Copy link
Owner

lizhongzhen11 commented Jun 26, 2018

前言

原先在老博客里面有写过闭包相关知识,本以为懂了,但是前两天思考惰性求值时又有点迷糊了,这东西看来我还没到融会贯通的地步,只靠理论是不行的,还是要写例子,并且多写,闭包是典型的孰能生巧的技术。
原先博客里的闭包保留,这里主要翻译介绍stackoverflow上高达6055票的高赞回答:How do JavaScript closures work?,回答的还真不错,翻译过来既让自己再度学习,也给看到的人输送“国际知识”。

翻译正文

闭包并不是魔法

这篇文章讲解闭包是为了让程序员能够理解并通过js使用它。并不是为专家或功能程序员(不知道翻译对不对)而写的。

一旦你领悟到了闭包的核心概念,那么对于你来说闭包并不难以理解。但是,我们不能仅通过阅读学术文章或者一些学术方面的知识去理解闭包

这篇文章是针对具有主流语言编程经验的程序员,能够阅读并理解接下来的js函数:

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

一个关于闭包的例子

两句话总结:

  • 闭包是一种支持一等函数(函数在js里是“一等公民”)的方式;它是一个表达式,可以引用其作用域内的变量(当它被首次声明时),被赋值给变量,作为参数传递给函数,或作为函数结果返回。
  • 或者,闭包是当函数开始执行时分配的堆栈帧(翻译起来有点别扭),在函数返回后不会释放(就好像一个'堆栈帧'被分配在堆上而不是堆栈上!)

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

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语言里面指向函数的指针和js里面函数的引用有非常关键的区别。在js里面,你可以将一个函数引用变量想象成既有指向函数的指针又指向闭包的隐藏指针。

上述代码中存在闭包,因为匿名函数function() { console.log(text);}声明在另一个函数(上例中的sayHello2())的内部。在js世界里,如果你在一个函数内部使用了function关键字,那么你正在创建一个闭包

C或者其他大多数与C相似的语言中,函数返回后,所有函数内的局部变量不能外部被获取,因为堆栈帧被销毁了。

在js中,如果你在一个函数内部声明另一个函数,当你调用返回的函数时,局部变量保持可访问状态。上面已经说明了,因为我们在函数sayHello2()返回之后才调用函数say2()。注意在上述代码中,我们调用了函数sayHello2()中局部变量text的引用。

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

观察say2.toString()的输出,我们可以看到代码引用了变量text。匿名函数为什么可以引用保存了值为**'Hello Bob'变量text呢?这是因为函数sayHello2()的局部变量被保存在了闭包**内!

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

更多的例子

因为某些原因,闭包似乎真的很难理解当你去阅读相关理论的话。但是当你看到一些例子的话,闭包是如何起作用的将会变得清晰易懂(我花了一些时间去理解它)。我建议仔细研究这些例子,直到你明白它们是如何工作的。如果你在没有完全理解它们如何工作的情况下开始使用闭包,你很快就会创建一些非常奇怪的bugs

例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()后,三个全局变量在该函数内被赋值,通过引用保存了三个不同的局部函数,他们三个与外部setupSomeGlobals()一起构成了一个闭包,所以他们三共享同一个闭包内的所有变量)。

注意上面的例子,如果你又一次调用setupSomeGlobals(),那么会创建一个新的闭包。原先的gLogNumbergIncreaseNumbergSetNumber三个变量所保存的引用将会被新闭包内的新函数覆盖。(在js中,无论何时你在一个函数中声明另一个函数,每次外部函数被调用时,其内部的函数也会被重新创建。)

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

提示:注意变量say也在闭包内,可以被任何在sayAlice()内部声明的其它函数访问,或者它也可以在内部函数中递归访问。

例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

译者注:我以为输出 "item2 3" 3 times,但其实是"item2 undefined" 3 times,可见我掌握的还是有问题,被绕进去了,希望看到的人能真正理解而不犯我这样的错!

result.push( function() {console.log(item + ' ' + list[i])}这段代码向resule这个数组中添加了3次对同一个匿名函数的引用。如果你对匿名函数不熟悉,不妨想象成下面这样:

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

注意当你运行这个例子时,item2 undefined被打印三次!就跟先前的例子一样,对函数buildList 内的局部变量(result,i,item)而言只有一个闭包。在fnlist[j]()这一行时调用匿名函数;他们都使用相同的单个闭包,并且他们使用该闭包内的当前iitem值。(这里i值为3,因为循环已经完成并且item值为item2)。请注意,我们从0开始索引,因此item的值为item2。 而i ++会将i增加到3。(译者注:我就是在这里踩得坑,我以为i === 2,但是接下来i需要自增1来结束循环,最终闭包内保存的应该是结束循环时i的值;至于item为何不是item3,因为当i === 3时不满足循环条件,循环结束,所以itemitem2)。

当使用块级作用域声明变量item(通过let)代替用var声明的函数作用域变量看看会发生什么,可能会有帮助。如果发生了这种变化,那么数组result中的每个匿名函数都有自己的闭包(译者注:将var item = 'item' + i 改为let item = 'item' + i);那么这个例子输出将会像下面这样:

item0 undefined
item1 undefined
item2 undefined

如果变量i也用let声明,输出会变成下面这样:

item0 1
item1 2
item2 3

例7

最后一个例子,每次调用主函数(外部函数)都会创建一个独立的闭包。

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;

概要

如果一切似乎完全不清楚,那么最好的办法就是通过例子去实践。阅读解释(纯理论知识)比理解示例要困难得多。我对闭包和栈帧的解释等在技术上并不正确——它们都是为了帮助理解而进行的粗略简化。一旦基本想法得到了解决,您可以稍后获取详细信息。

最后一点:

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

链接

感谢

如果你刚刚学习闭包(无论在哪!),我对任何有关您可能建议的更改可能会使本文变得更清楚的任何反馈感兴趣。发送邮件到morrisjohns.com(morris_closure @)。 请注意,我不是JavaScript的专家,也不是闭包

@lizhongzhen11 lizhongzhen11 added js基础 js基础 Good for newcomers and removed js基础 labels Oct 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js基础 Good for newcomers
Projects
None yet
Development

No branches or pull requests

1 participant