闭包深入理解阅读笔记

    今天去看了很多关于闭包的解释,比如MDN上的:Closures的解释或者是在stackoverflow别人的一些总结。发现了自己有很多很多的误区。比如潜意识认为闭包是一定要在一个函数内返回(return)一个函数,才是闭包。这是很错误的。再比如认为闭包会导致内存泄漏。前几个月面试的时候,有时候会说闭包会导致内存泄漏,但是面试官们也没有给我指出来。闭包不会导致内存泄漏,会占用内存,还好及时发现了。看到了一些好的解释都是以讲故事的形式解释的,中间不一定对,但是都是帮助了我理解的。还看了一些咱们自己翻译的外国人的故事,或者文章,在看了原文以后,发现还是看原文比较好懂,也比较清晰。

    下面是我找到的一些解释,这些解释大多是英文,放在博客里面也只是方便我翻阅和记录,不用到处找。我没有进行任何翻译,也没有写任何自己的理解。并不是原创,我想保留原作者的原文。也会标注上作者和来源。


    a story help you understand

    下面是我发现最好懂的一个故事,来自stackoverflow:Tero Tolonen,地址在这里

    The children will always remember the secrets they have shared with their parents, even after their parents are gone. This is what closures are for functions.

    The secrets for JavaScript functions are the private variables

    1
    2
    3
    var parent = function() {
    var name = "Mary"; // secret
    }

    Every time you call it, local variable “name” is created and given name “Mary”. And every time the function exits the variable is lost and the name is forgotten.

    As you may guess, because the variables are re-created every time the function is called, and nobody else will know them, there must be a secret place where they are stored. It could be called Chamber of Secrets or stack or local scope but it doesn’t really matter. We know they are there, somewhere, hidden in the memory.

    But, in JavaScript there is this very special thing that functions which are created inside other functions, can also know the local variables of their parents and keep them as long as they live.

    1
    2
    3
    4
    5
    6
    var parent = function() {
    var name = "Mary";
    var child = function(childName) {
    // I can also see that "name" is "Mary"
    }
    }

    So, as long as we are in the parent -function, it can create one or more child functions which do share the secret variables from the secret place.

    But the sad thing is, if the child is also a private variable of its parent function, it would also die when the parent ends, and the secrets would die with them.

    So to live, the child has to leave before it’s too late

    1
    2
    3
    4
    5
    6
    7
    8
    var parent = function() {
    var name = "Mary";
    var child = function(childName) {
    return "My name is "+childName+", child of "+name;
    }
    return child; // child leaves the parent ->
    }
    var child = parent(); // < - and here it is outside

    And now, even though Mary is “no longer running”, the memory of her is not lost and her child will always remember her name and other secrets they shared during their time together.

    So, if you call the child “Alice”, she will respond

    1
    child("Alice") => "My name is Alice, child of Mary"

    That’s all there is to tell.


    misunderstandings

    This is an attempt to clear up several (possible) misunderstandings about closures that appear in some of the other answers.

    来自stackoverflow:地址在这里

    • A closure is not only created when you return an inner function. In fact, the enclosing function does not need to return at all in order for its closure to be created. You might instead assign your inner function to a variable in an outer scope, or pass it as an argument to another function where it could be called immediately or any time later. Therefore, the closure of the enclosing function is probably created as soon as the enclosing function is called since any inner function has access to that closure whenever the inner function is called, before or after the enclosing function returns.
    • A closure does not reference a copy of the old values of variables in its scope. The variables themselves are part of the closure, and so the value seen when accessing one of those variables is the latest value at the time it is accessed. This is why inner functions created inside of loops can be tricky, since each one has access to the same outer variables rather than grabbing a copy of the variables at the time the function is created or called.
    • The “variables” in a closure include any named functions declared within the function. They also include arguments of the function. A closure also has access to its containing closure’s variables, all the way up to the global scope.
    • Closures use memory, but they don’t cause memory leaks since JavaScript by itself cleans up its own circular structures that are not referenced. Internet Explorer memory leaks involving closures are created when it fails to disconnect DOM attribute values that reference closures, thus maintaining references to possibly circular structures.

    Closures provide an environment, a stack frame, for local variables.


    read examples to understand better

    1
    2
    3
    4
    5
    6
    7
    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"
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function say667() {
    // Local variable that ends up within closure
    var num = 666;
    var say = function() { console.log(num); }
    num++;
    return say;
    }
    var sayNumber = say667();
    sayNumber(); // logs 667
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var gLogNumber, gIncreaseNumber, gSetNumber;
    function setupSomeGlobals() {
    // Local variable that ends up within closure
    var num = 666;
    // Store some references to functions as global variables
    gLogNumber = function() { console.log(num); }
    gIncreaseNumber = function() { num++; }
    gSetNumber = function(x) { num = x; }
    }
    setupSomeGlobals();
    gIncreaseNumber();
    gLogNumber(); // 667
    gSetNumber(5);
    gLogNumber(); // 5
    var oldLog = gLogNumber;
    setupSomeGlobals();
    gLogNumber(); // 666
    oldLog() // 5
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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]();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
    }
    sayAlice()();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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 +
    '\nanArray ' + anArray.toString() +
    '\nref.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;

    闭包的用途

    用闭包模拟私有方法

    来自MDNCLOSURES

    诸如 Java 在内的一些语言支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

    对此,JavaScript 并不提供原生的支持,但是可以使用闭包模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

    下面的示例展现了如何使用闭包来定义公共函数,且其可以访问私有函数和变量。这个方式也称为 模块模式(module pattern):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    var Counter = (function() {
    var privateCounter = 0;
    function changeBy(val) {
    privateCounter += val;
    }
    return {
    increment: function() {
    changeBy(1);
    },
    decrement: function() {
    changeBy(-1);
    },
    value: function() {
    return privateCounter;
    }
    }
    })();
    console.log(Counter.value()); /* logs 0 */
    Counter.increment();
    Counter.increment();
    console.log(Counter.value()); /* logs 2 */
    Counter.decrement();
    console.log(Counter.value()); /* logs 1 */

    这里有很多细节。在以往的示例中,每个闭包都有它自己的环境;而这次我们只创建了一个环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。

    该共享环境创建于一个匿名函数体内,该函数一经定义立刻执行。环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。 这两项都无法在匿名函数外部直接访问。必须通过匿名包装器返回的三个公共函数访问。

    这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法范围的作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

    您应该注意到了,我们定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给 Counter 变量。也可以将这个函数保存到另一个变量中,以便创建多个计数器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var makeCounter = function() {
    var privateCounter = 0;
    function changeBy(val) {
    privateCounter += val;
    }
    return {
    increment: function() {
    changeBy(1);
    },
    decrement: function() {
    changeBy(-1);
    },
    value: function() {
    return privateCounter;
    }
    }
    };
    var Counter1 = makeCounter();
    var Counter2 = makeCounter();
    console.log(Counter1.value()); /* logs 0 */
    Counter1.increment();
    Counter1.increment();
    console.log(Counter1.value()); /* logs 2 */
    Counter1.decrement();
    console.log(Counter1.value()); /* logs 1 */
    console.log(Counter2.value()); /* logs 0 */

    请注意两个计数器是如何维护它们各自的独立性的。每次调用 makeCounter() 函数期间,其环境是不同的。每次调用中, privateCounter 中含有不同的实例。

    这种形式的闭包提供了许多通常由面向对象编程U所享有的益处,尤其是数据隐藏和封装。


    容易犯的错误-在循环中创建闭包v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function showHelp(help) {
    document.getElementById('help').innerHTML = help;
    }
    function setupHelp() {
    var helpText = [
    {'id': 'email', 'help': 'Your e-mail address'},
    {'id': 'name', 'help': 'Your full name'},
    {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];
    for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
    showHelp(item.help);
    }
    }
    }
    setupHelp();

    组 helpText 中定义了三个有用的提示信息,每一个都关联于对应的文档中的输入域的 ID。通过循环这三项定义,依次为每一个输入域添加了一个 onfocus 事件处理函数,以便显示帮助信息。

    运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个输入域上,显示的都是关于年龄的消息。

    该问题的原因在于赋给 onfocus 是闭包(setupHelp)中的匿名函数而不是闭包对象;在闭包(setupHelp)中一共创建了三个匿名函数,但是它们都共享同一个环境(item)。在 onfocus 的回调被执行时,循环早已经完成,且此时 item 变量(由所有三个闭包所共享)已经指向了 helpText 列表中的最后一项。

    解决这个问题的一种方案是使onfocus指向一个新的闭包对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function showHelp(help) {
    document.getElementById('help').innerHTML = help;
    }
    function makeHelpCallback(help) {
    return function() {
    showHelp(help);
    };
    }
    function setupHelp() {
    var helpText = [
    {'id': 'email', 'help': 'Your e-mail address'},
    {'id': 'name', 'help': 'Your full name'},
    {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];
    for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
    }
    }
    setupHelp();


    性能考量

    如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。

    例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。

    考虑以下虽然不切实际但却说明问题的示例:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
      this.getName = function() {
        return this.name;
      };
    
      this.getMessage = function() {
        return this.message;
      };
    }
    

    上面的代码并未利用到闭包的益处,因此,应该修改为如下常规形式:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    MyObject.prototype = {
      getName: function() {
        return this.name;
      },
      getMessage: function() {
        return this.message;
      }
    };
    

    或者改成:

    function MyObject(name, message) {
      this.name = name.toString();
      this.message = message.toString();
    }
    MyObject.prototype.getName = function() {
      return this.name;
    };
    MyObject.prototype.getMessage = function() {
      return this.message;
    };
    

    在前面的两个示例中,继承的原型可以为所有对象共享,且不必在每一次创建对象时定义方法。


    总结

    感谢MDN-Web/JavaScript/Closures还有这篇文章stackoverflow。整体的把自己一些错误的想法稍微给纠正了,然后加深了对闭包的理解。