zoukankan      html  css  js  c++  java
  • 通过示例学习JavaScript闭包

    译者按: 上一篇博客,我们通过实现一个计数器,了解了如何使用闭包(Closure),这篇博客将提供一些代码示例,帮助大家理解闭包。

    原文: JavaScript Closures for Dummies

    译者: Fundebug

    为了保证可读性,本文采用意译而非直译。另外,本文版权归原作者所有,翻译仅用于学习。

    闭包并不神奇

    其实,只要你领会了闭包的关键概念,一切就非常简单了。作为JavaScript开发者,你应该可以理解以下代码:

    Example 1

    function sayHello(name)
    {
     
    var text = 'Hello ' + name;
     
    var sayAlert = function() { console.log(text); }
     
    sayAlert();
    }
     
    sayHello("Bob") // 输出"Hello Bob"

    sayHello()函数中定义并调用了sayAlert()函数;sayAlert()作为内层函数,可以访问外层函数sayHello()中的text变量。理解这一点,你就可以继续阅读这篇博客了。

    一个闭包示例

    两句话总结闭包(注意,这个定义并不规范,但是有助于理解):

    • 闭包就是函数的局部变量,这些变量在函数return之后仍然可以访问
    • 闭包就是函数的内存堆栈,这个内存堆栈在函数return之后并没有被收回

    Example 2

    function sayHello2(name)
    {
    var text = 'Hello ' + name; // 局部变量
     
    var sayAlert = function() { console.log(text); }
     
    return sayAlert;
    }
     
    var say2 = sayHello2("Jane");
    say2(); // 输出"Hello Jane"

    调用sayHello2()函数返回了sayAlert,它是一个引用变量,指向一个函数。相信大多数JavaScript程序员能够理解什么是引用变量,而C程序员则可以把sayAlert以及say2理解为指向函数的指针。

    C指针与JavaScript引用变量并无实质区分。在JavaScript中,不妨这样理解,指向函数的引用变量不仅指向函数本身,还隐含地指向了一个闭包。

    代码中匿名函数function() { alert(text); }是在另一个函数,即sayHello2()中定义的。在JavaScript中,如果你在函数中定义了一个函数,则创建了闭包。

    对于C语言,以及其他绝大多数语言:函数return之后,其局部变量将无法访问,因为内存中的堆栈会被销毁。

    对于JavaScript,如果你在函数中定义函数的话,当外层函数return之后,其局部变量仍然可以访问。代码中已经证明了这一点:当sayHello2()函数return之后,我们调用了say2()函数,成功打印了text变量,而text变量正是sayHello2()函数的局部变量。

    更多示例

    如果只是从定义的角度去理解闭包,显然是非常困难。然而,如果通过代码示例去理解闭包,则简单很多。因此,强烈建议你认真地理解每一个示例,弄清楚它们是如何运行的,这样你会避免很多奇怪的BUG。

    Example 3

    Example 3中,say667()函数return后,num变量将仍然保留在内存中。并且,sayNumba函数中的num变量并非复制而是引用,因此它输出的是667而非666

    function say667() {
     
    var num = 666; // say667()函数return后,num变量将仍然保留在内存中
     
    var sayAlert = function() { console.log(num); }
     
    num++;
     
    return sayAlert;
     
    }
     
    var sayNumba = say667();
     
    sayNumba(); // 输出667

    Example 4

    Example 4中,3个全局函数gAlertNumber,gIncreaseNumber,gSetNumber指向了同一个闭包,因为它们是在同一次setupSomeGlobals()调用中声明的。它们所指向的闭包就是setupSomeGlobals()函数的局部变量,包括了num变量。也就是说,它们操作的是同一个num变量。

    function setupSomeGlobals() {
     
    var num = 666;
     
    gAlertNumber = function() { console.log(num); }
     
    gIncreaseNumber = function() { num++; }
     
    gSetNumber = function(x) { num = x; }
     
    }
     
    setupSomeGlobals();
    gAlertNumber(); // 输出666
     
    gIncreaseNumber();
    gAlertNumber(); // 输出667
     
    gSetNumber(5);
    gAlertNumber(); // 输出5

    Example 5

    Example 5的代码比较难,不少人都会犯同样的错误,因为它的执行结果很可能违背了你的直觉。

    function buildList(list)
    {
    var result = [];
     
    for (var i = 0; i < list.length; i++)
    {
    var item = 'item' + list[i];
    result.push( function() { console.log(item + ' ' + list[i])} );
    }
     
    return result;
    }
     
    var fnlist = buildList([1,2,3]);
     
    for (var j = 0; j < fnlist.length; j++)
    {
    fnlist[j](); // 连续输出3个"item3 undefined"
    }

    result.push( function() {alert(item + ‘ ‘ + list[i])}将指向匿名函数function() {alert(item + ‘ ‘ + list[i])}的引用变量加入了数组,其效果等价于:

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

    代码执行后,连续输出了3个”item3 undefined”,明显与直觉不同。

    调用buildList()函数之后,我们得到了一个数组,数组中有3个函数,而这3个函数指向了同一个闭包。而闭包中的item变量值为“item3”i变量值为3。如果理解了3个函数指向的是同一个闭包,则输出结果就不难理解了。

    Example 6

    Example 6中,alice变量在sayAlert函数之后定义,这并未影响代码执行。因为返回函数sayAlice2所指向的闭包会包含sayAlice()函数中的所有局部变量,这自然包括了alice变量,因此可以正常打印”Hello Alice”。

    function sayAlice()
    {
    var sayAlert = function() { console.log(alice); }
     
    var alice = 'Hello Alice';
     
    return sayAlert;
    }
     
    var sayAlice2 = sayAlice();
     
    sayAlice2(); // 输出"Hello Alice"

    Example 7

    Example 7可知,每次调用newClosure()都会创建独立的闭包,它们的局部变量numref的值并不相同。

    function newClosure(someNum, someRef)
    {
    var anArray = [1,2,3];
    var num = someNum;
    var ref = someRef;
     
    return function(x)
    {
    num += x;
     
    anArray.push(num);
     
    console.log('num: ' + num + "; " + 'anArray ' + anArray.toString() + "; " + 'ref.someVar ' + ref.someVar);
    }
    }
     
    closure1 = newClosure(40, {someVar: "closure 1"});
    closure2 = newClosure(1000, {someVar: "closure 2"});
     
    closure1(5); // 打印"num: 45; anArray 1,2,3,45; ref.someVar closure 1"
    closure2(-10); // 打印"num: 990; anArray 1,2,3,990; ref.someVar closure 2"

    总结

    严格来讲,我对闭包的解释并不准确。不过,将闭包简单地看做局部变量,理解起来会更加简单。

    参考链接

    关于Fundebug

    Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了7亿+错误事件,得到了Google、360、金山软件、百姓网等众多知名用户的认可。欢迎免费试用!

    版权声明:

  • 相关阅读:
    Mybatis 的 xml 文件语法错误,启动项目时控制台一直循环解析但是不打印错误
    在一个由 'L' , 'R' 和 'X' 三个字符组成的字符串(例如"RXXLRXRXL")中进行移动操作。一次移动操作指用一个"LX"替换一个"XL",或者用一个"XR"替换一个"RX"。现给定起始字符串start和结束字符串end,请编写代码,当且仅当存在一系列移动操作使得start可以转换成end时, 返回True。
    【Important】数据库索引原理
    服务化的演变和负载均衡
    【问题集】redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range
    【Spring】Spring中用到的设计模式
    【设计模式】责任链模式
    【!Important】Zookeeper用来做什么的,有几种类型的节点
    【!Important】如何保证线程执行的先后顺序
    【!Important】Java线程死锁查看分析方法
  • 原文地址:https://www.cnblogs.com/fundebug/p/7298786.html
Copyright © 2011-2022 走看看