闭包

作者: _咻咻咻咻咻 | 来源:发表于2020-09-28 17:29 被阅读0次

    一 概念

    闭包是指有权访问另一个函数作用域中的变量的函数。
    在JavaScript中,就是在一个函数内部创建另一个函数,内部函数为闭包。内部函数可以访问外部函数环境内的变量。

    function f1(){
      var n=999;
      function f2(){
        alert(n);
      }
      return f2;
      }
    var result=f1();
    result(); // 999
    

    以上f2函数为闭包。 f2从f1中被返回后,它的作用域链被初始化为包含f1函数的活动对象和全局变量对象,而且,f1执行完后它的活动对象也不会被销毁,因为f2的作用域链还在引用这个活动对象(f1的作用域链被销毁,但是活动对象还在内存中),直到f2被销毁,f1的活动对象才会被销毁。
    f1 = null; // 释放内存

    function f1() {
      var n = 999;
      nAdd = function () { n += 1 }
      function f2() {
        alert(n);
      }
      return f2;
    }
    var result = f1();
    result(); // 999
    nAdd();
    result(); // 1000
    

    n没有被销毁,所以再次调用result()会增加。
    另nAdd定义时没有用var或者let和const,因此是全局变量,也是一个闭包,可以在外部调用。

    function makeAdder(x) {
      return function(y) {
        return x + y;
      };
    }
    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    console.log(add5(2));  // 7
    console.log(add10(2)); // 12
    

    二 用途

    1. 使用闭包解决递归调用问题

    function factorial(num) {
      if (num <= 1) {
        return 1;
      } else {
        return num * factorial(num - 1)
      }
    }
    var anotherFactorial = factorial
    factorial = null
    anotherFactorial(4)   // 报错 ,因为最好是return num* arguments.callee(num-1),arguments.callee指向当前执行函数,但是在严格模式下不能使用该属性也会报错,所以借助闭包来实现
    
    //arguments.callee是一个指向正在执行函数的指针。可以解决以上问题,但是在严格模式下会报错。
    function factorial(num) {
        if(num<=1){
            return 1;
        }else{
            return num * arguments.callee(num-1)
        }
    }
    
    // 使用闭包实现递归
    var newFactorial = (function f(num) {
      if (num < 1) { return 1 }
      else {
        return num * f(num - 1)
      }
    }) //这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial
    

    2. 模仿块级作用域

    (function () {
      // 这里是块级作用域
    })()
    

    现在多用es6的let和const

    3. 访问私有变量

    在函数中定义的变量为私有变量,闭包可以创建用于访问私有变量的公有方法,这种方法叫特权方法
    私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

    3.1 使用构造函数/原型模式实现自定义类型的特权方法

    构造函数:私有变量name在Person的每一个实例中都不同, 因此每次调用构造函数都会重新创建getName和setName。构造函数模式的缺点是针对每个实例都会创建同样一组方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

    function Person(name) {
        this.getName = function () {
            return name;
        }
        this.setName = function (vlaue) {
            name = value;
        }
    }
    var person = new Person("aaa")
    console.log(person.getName()); //aaa
    person.setName('bbb')
    console.log(person.getName()); //bbb
    

    原型模式:私有变量和函数是由实例共享的。变量name变成了静态的,由所有实例共享的属性。

    (function () {
        var name = "";
        Person = function (value) {
            name = value
        }
        Person.prototype.getName = function () {
            return name;
        }
        Person.prototype.setName = function (value) {
            name = value;
        }
    })();
    var person1 = new Person("aaa");
    console.log(person1.getName()); //aaa
    person1.setName('bbb')
    console.log(person1.getName()); //bbb
    
    var person2 = new Person('ccc')
    console.log(person1.getName()); //ccc
    console.log(person2.getName()); //ccc
    
    3.2 使用模块模式/增强的模块模式实现单例的特权方法

    单例:只有一个实例的对象。

    模块模式:

    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 */
    

    增强的模块模式: 适合那种实例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增强的情况。

    var singleton = function () {
        //私有变量和私有属性
        var privateVatiable = 10;
        function privateFunction() {
            return false;
        }
        //创建对象
        var object = new CustomType();
        //添加特权/公有属性和方法
        object.publicProperty = true;
        object.publicMethod = function () {
            privateVatiable++;
            return privateFunction();
        }
        return object
    }
    

    三 使用闭包需要注意

    1. 闭包只能取得外部函数中任何变量的最后一个值
    function outer() {
      var result = [];
      for (var i = 0; i < 5; i++) {
        result[i] = function () {
          return i;
        }
      }
      return result
    }
    var a = outer();
    a[0]() //5
    a[1]() //5
    a[2]() //5
    a[3]() //5
    a[4]() //5
    

    这里闭包函数引用了变量i,但是并没有立即执行。变量i用var声明的,作用域在外部函数outer中,等到闭包函数执行的时候,i已经变成了5。所以result中每一项打印的都是5。

    解决方案1: 多用一层闭包

    function outer() {
      var result = [];
      for (var i = 0; i < 5; i++) {
        result[i] = (function (num) {
          return function(){
            return num;
          }
        })(i)
      }
      return result
    }
    var a = outer();
    a[0]() //0
    a[1]() //1
    a[2]() //2
    a[3]() //3
    a[4]() //4
    

    解决方案2:

    function outer() {
      var result = [];
      for (var i = 0; i < 5; i++) {
        (function(){
          var item = i
          result[item] = function(){
            return item;
          }
        })()
      }
      return result
    }
    var a = outer();
    a[0]() //0
    a[1]() //1
    a[2]() //2
    a[3]() //3
    a[4]() //4
    

    解决方案3: i 用let来声明

    function outer() {
      var result = [];
      for (let i = 0; i < 5; i++) {
        result[i] = function(){
          return i;
        }
      }
      return result
    }
    var a = outer();
    a[0]() //0
    a[1]() //1
    a[2]() //2
    a[3]() //3
    a[4]() //4
    

    解决方案4: 用forEach

    function outer() {
      var result = [];
      var arr = [0,1,2,3,4]
      result = arr.forEach(function(i){
        return i;
      })
      return result
    }
    
    2. this指向问题

    匿名函数的执行环境具有全局性,因此其this对象通常指向window。

    var name = "The Window";
    var object = {
      name: "My Object",
      getNameFunc: function () {
        return function () {
          return this.name;
        };
      }
    };
    alert(object.getNameFunc()()); //The Window
    
    var name = "The Window";
    var object = {
      name: "My Object",
      getNameFunc: function () {
        var that = this;
        return function () {
          return that.name;
        };
      }
    };
    alert(object.getNameFunc()()); //My Object
    
    3. 内存泄漏问题
    function showId() {
      var el = document.getElementById("app")
      el.onclick = function () {
        alert(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
      }
    }
    
    // 改成下面
    function showId() {
      var el = document.getElementById("app")
      var id = el.id
      el.onclick = function () {
        alert(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
      }
      el = null    // 主动释放el
    }
    

    四 性能

    因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,在处理速度和内存消耗方面对脚本性能具有负面影响,所以如果不是某些特定任务需要使用闭包,尽量避免闭包的使用。

    例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
    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();
    }
    MyObject.prototype.getName = function() {
      return this.name;
    };
    MyObject.prototype.getMessage = function() {
      return this.message;
    };
    

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

    五 总结

    当在函数内部定义了其他函数,就创建了闭包。
    闭包的作用域包含它自己的作用域,包含函数的作用域和全局作用域。
    使用闭包可以模仿块级作用域。
    闭包可以用于在对象中创建私有变量。可以使用构造函数模式,原型模式实现自定义类型的特权方法,也可以使用模块模式,增强的模块模式实现单例的特权方法。

    相关文章

      网友评论

          本文标题:闭包

          本文链接:https://www.haomeiwen.com/subject/fjgduktx.html