资讯专栏INFORMATION COLUMN

JavaScript闭包,只学这篇就够了

CoderBear / 1198人阅读

摘要:当在中调用匿名函数时,它们用的都是同一个闭包,而且在这个闭包中使用了和的当前值的值为因为循环已经结束,的值为。最好将闭包当作是一个函数的入口创建的,而局部变量是被添加进这个闭包的。

闭包不是魔法

这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义。

其实只要理解了核心概念,闭包并不是那么的难于理解。但是,网上充斥了太多学术性的文章,对于新手来说,看完这些文章可能会更加一头雾水。

这篇文章面向的是使用主流开发语言的程序员,如果你能读懂下面这段代码,恭喜你,你可以开始JavaScript闭包的学习之旅了。

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

我相信你一定看懂了,那我们就开始吧!

闭包的一个例子

举例之前,我们先用两句话概括一下:

闭包是支持一类函数特性的一种方式(如果你还不知道什么是一类函数,请自行百度);它是一个表达式,这个表达式可以在其作用域(当它被初次定义时)内引用变量,或者被赋值给一个变量,或者被当做一个变量传递给某个函数,甚至被当作一个函数的执行结果被返回出去。

闭包也可以看作是某个函数被调用时分配的栈帧,而且当这个函数返回结果之后它也不会被回收(就好像它被分配给了堆,而不是栈)

下面的例子返回了对一个方法的引用:

function sayHello2(name){
    var text= "Hello" + name; //局部变量
    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中,如果你在一个函数中定义了另外一个函数,那么你就创建了一个闭包。

在C语言或者其他流行的开发语言当中,函数返回之后,所有局部变量都不能再被访问,因为栈帧已经被销毁了。

在JavaScript中,如果在一个函数中定义了另外一个函数,即使从被调用的函数中返回,局部变量依然能够被访问到。正如上面例子中我们在得到sayHello2()的返回值之后又调用了say2()一样。需要注意到,我们调用的代码中引用了函数sayHello2()中的局部变量text

function(){console.log(text);} //say2.toString()的输出结果;

观察say2.toString()的输出结果,我们会发现代码指向变量text。这个匿名函数能够引用值为Hello Bob的变量text是因为sayHello2()的局部变量被保留在了闭包中。

在JavaScript中神奇的地方在于引用一个函数的同时会有一个秘密的引用指向在这个函数内部创建的闭包,类似于委托一个方法指针加一个隐藏的对象引用。

更多例子

当你读到很多关于闭包的文章时,总会感觉一头雾水,但是当你看到一些应用的例子时,你就能清晰的理解闭包是如何工作的了。下面是我推荐的一些例子,希望大家能够认真研究直到真正清楚闭包是如何工作的。如果在你没有完全理解的情况下就开始使用闭包,你很快就会成为很多奇怪bug的创造者。

下面这个例子展示了局部变量不是被复制,而是被保留在了引用当中。这是当外部函数存在的情况下将栈帧保存在内存中的方法之一。

function say667(){
//处于闭包中的局部变量
var num=42;
var say=function(){console.log(num);}
num++;
return say;
}
var sayNumber=say667();
sayNumber();//logs 43

下面例子中的三个全局函数有对同一个闭包的共同引用,因为他们都在setupSomeGlobals()中被定义。

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  //处于闭包中的局部变量
  var num = 42;
  // 用全局变量存储对函数的引用
  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(),会创建一个新的闭包。gLogNumber()gSetNumber()gLogNumber()会被带有新闭包的函数重写(在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]);
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

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

注意到result.push( function() {console.log(item + " " + list[i])}result数组中插入了三次对匿名函数的引用。如果你对匿名函数不太熟悉,可以想象成下面的代码:

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

需要注意到,当你运行上面的例子时,item2 undefined被打印了三次!这是因为像前一个例子中提到的,buildList的局部变量只有一个闭包。当在fnlist[j]()中调用匿名函数时,它们用的都是同一个闭包,而且在这个闭包中使用了iitem的当前值(i的值为3因为循环已经结束,item的值为item2)。因为我们从0开始计数所以item的值为item2,而i++会使i的值变为3

下面这个例子展示了闭包在退出之前包含了外部函数中定义的任何局部变量。注意到变量alice其实是在匿名函数之后定义的。匿名函数先定义,但是当它被调用时它能够访问alice,因为alice和匿名函数处于同一作用域(JavaScript会进行变量提升)。sayAlice()()只是直接调用了sayAlice()返回的函数引用-但结果却和之前一样,只不过没有临时变量而已。

function sayAlice() {
    var say = function() { console.log(alice); }
    var alice = "Hello Alice";
    return say;
}
sayAlice()();// logs "Hello Alice"

注意到变量say也在闭包中,能够被任何在sayAlice()中定义的函数访问,或者在内部函数中被递归调用。

最后一个例子展现了每次调用都为局部变量创建一个独立闭包。不是每个函数定义都会有一个闭包,而是每次函数调用产生一个闭包。

function newClosure(someNum, someRef) {
    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("val foo=...")来创建局部变量。

当你在函数中使用new Function(...)时,不会创建一个闭包(这个新的函数不能引用外部函数的局部变量)。

JavaScript中的闭包就好像保存了一份局部变量的备份,他们保持在函数退出时的状态。

最好将闭包当作是一个函数的入口创建的,而局部变量是被添加进这个闭包的。

当一个带有闭包的函数被调用时,总会保存一组新的局部变量。

两个看似代码相同的函数却有不同的行为,是因为隐藏的闭包在作怪。我不认为JavaScript代码能够判断出一个函数引用是否有闭包。

如果你尝试做任何动态代码的改动(例如:myFunction = Function(myFunction.toString().replace(/Hello/,"Hola"));),如果myFunction是个闭包,那就不会起作用(当然,你不会想在运行时里进行源代码的字符串替换,除非...)。

在函数中定义多层函数是有可能的,这样你就可以得到多个级别的闭包。

我认为在通常情况下,闭包是函数及被捕获的变量的术语,请注意在这篇文章里我没有用到闭包的定义。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87301.html

相关文章

  • JS正则表达式入门,看这篇够了

    摘要:如果遇到非常的复杂的匹配,正则表达式的优势就更加明显了。关于正则表达式书写规则,可查看,上面说的很清楚了,我就不贴出来了。替换与正则表达式匹配的子串,并返回替换后的字符串。结语正则表达式并不难,懂了其中的套路之后,一切都变得简单了。 前言 在正文开始前,先说说正则表达式是什么,为什么要用正则表达式?正则表达式在我个人看来就是一个浏览器可以识别的规则,有了这个规则,浏览器就可以帮我们判断...

    wenzi 评论0 收藏0
  • 前端异常监控-看这篇够了

    摘要:前端异常监控如果是移除的流程,那么编程就一定是将放进去的流程。过滤掉运行时错误上报加载错误事件捕获异常最新的规范中定义了事件用于全局捕获对象没有处理器时异常情况。 前端异常监控 如果debug是移除bug的流程,那么编程就一定是将bug放进去的流程。如果没有用户反馈问题,那就代表我们的产品棒棒哒,对不对? 主要内容 Web规范中相关前端异常 异常按照捕获方式分类 异常的捕获方式 日志...

    Aklman 评论0 收藏0
  • 前端异常监控-看这篇够了

    摘要:前端异常监控如果是移除的流程,那么编程就一定是将放进去的流程。过滤掉运行时错误上报加载错误事件捕获异常最新的规范中定义了事件用于全局捕获对象没有处理器时异常情况。 前端异常监控 如果debug是移除bug的流程,那么编程就一定是将bug放进去的流程。如果没有用户反馈问题,那就代表我们的产品棒棒哒,对不对? 主要内容 Web规范中相关前端异常 异常按照捕获方式分类 异常的捕获方式 日志...

    guqiu 评论0 收藏0
  • Lombok 看这篇够了

    摘要:注解在类上为类提供一个全参的构造方法,加了这个注解后,类中不提供默认构造方法了。这个注解用在类上,使用类中所有带有注解的或者带有修饰的成员变量生成对应的构造方法。 转载请注明原创地址:http://www.54tianzhisheng.cn/2018/01/07/lombok/ showImg(http://ohfk1r827.bkt.clouddn.com/blog/180107/7...

    LeanCloud 评论0 收藏0
  • 从0到1学AJAX,这篇够了!(进阶三)

    摘要:一定在发送请求之前注册不管同步或者异步为了让这个事件可以更加可靠一定触发,一定是先注册了解同步模式即可,切记不要使用同步模式。至此,我们已经大致了解了的基本。一种数据描述手段,基本现在的项目不用了,淘汰的原因数据冗余太多。 什么是ajax? AJAX 就是浏览器提供的一套 API,可以通过 JavaScript 调用,从而实现通过代码控制请求与响应。实现网络编程 1、使用 AJAX 的...

    CoderDock 评论0 收藏0

发表评论

0条评论

CoderBear

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<