JS 闭包

闭包是函数和函数所声明地方的变量作用域的结合;

闭包

    function makeFunc() {
        let name = "Mozilla";
        function displayName() {
            console.log(`name = ${name}`); 
        }
        return displayName;
    }

    let myFunc = makeFunc();
    myFunc();

myFunc() 执行, 会输出字符串 Mozilla;
displayName() 是内部函数, 但是在执行是, 作为返回值赋值给了内部函数, 仍然正常运行; 在部分语言中, 函数内部的局部变量, 仅在函数执行期间可用, 一旦函数执行完成, 内部局部变量将不可用; 但是上面代码, makeFunc() 执行过后,函数内部变量 name 仍然可以输出, 这个就是js闭包的效果;

闭包是一种特殊的对象, 由两部分组成: 函数, 以及创建该函数时的环境. 环境由创建该函数时 在作用域中的所有局部变量组成;

闭包模拟对象及私有变量方法

JS 本身不支持私有方法和变量; 因为闭包拥有函数创建时 所处作用域的所有环境, 从而可以使用闭包来模拟私有属性;

    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; 由于是匿名函数调用执行, 外部无任何变量是指向当前匿名函数的, 从而外部无法修改调用 privateCounter 变量, 从而实现私有变量, 内部privateCounter变量修改, 只能通过返回的闭包修改, 从而实现了类似Java中的类和私有属性.

常见错误, 循环中创建闭包

循环体中创建的闭包, 闭包创建时的作用域, 因此可以使用循环体中声明的变量, 而循环体中的变量 可能在循环的时候 值发生变化, 造成最终效果和预期的不一样;

    

Helpful notes will appear here

E-mail:

Name:

Age:

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();

上面代码, 无论点击哪个, 出现的都是年龄相关的信息;
造成这个的原因, 就是循环体类, 创建的3个闭包, 每个都引用 item, 单循环体结束后, item指向最后一个变量, 3个闭包内部调用item时, 都是使用的最后指向的值;

解决办法
  1. 使创建的多个闭包, 使用的是不用的环境, 从而不相互影响

     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++) {
             (function() {
                 var item = helpText[i];
                 document.getElementById(item.id).onfocus = function() {
                     showHelp(item.help);
                 }
             })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
         }
     }
    
     setupHelp();
    

    上面代码中, 在创建闭包的外面, 包裹了一个立即调用的匿名函数, 从而使每个闭包创建时的环境都不一样了, 从而使各个闭包内部使用的 item 值, 不会指向同一个位置;

    1. 使用let关键词
      var的作用域是整个全局的; 而let的作用域是当前块, 子块中的调用修改,不会影响子块外面的变量;
      let类似于Java函数中的局部变量, 每个块中声明使用, 都会生成新变量; 不像var, var声明的变量, 会提升到当前上下文的顶部执行, 多次生声明赋值的var, 其实只是同一个变量, 多次赋值;

      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++) {
           let item = helpText[i];
           document.getElementById(item.id).onfocus = function() {
               showHelp(item.help);
           }
       }
      

      }

      setupHelp();
      用let而不用var,使得每个闭包绑定块内变量,不需要额外的闭包, 可以降低内存上面的消耗;

性能考量

闭包对性能有负面影响, 不是必须使用闭包时, 应该尽量减少闭包的使用;

eg. 使用函数模拟传统语言的类时, 方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建);

    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();
    }
    (function() {
        this.getName = function() {
            return this.name;
        };
        this.getMessage = function() {
            return this.message;
        };
    }).call(MyObject.prototype);

参考: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

你可能感兴趣的:(JS 闭包)