闭包和高级函数

闭包(closure)

两个前置知识

  • 变量的作用域

    • 函数中声明一个变量,如果没有加 var 之类的声明关键字,这个变量就会成为全局变量,造成冲突,在 strict 模式下报错。
    • var 在函数中声明的变量是局部变量,只有在该函数内部才能访问到这个变量,在函数外面是访问不到的。
    var func = function () {
      var a = 1;
      console.log(a); // 1
    };
    func();
    console.log(a); // undefined
    • const、let 新增了块级作用域概念,即 {} 内部声明的变量在其内部才能访问,在 {} 外部是访问不到的。
  • 变量的生存周期

    • 全局变量的生存周期是永久的,除非我们主动销毁。
    • var 关键字生命的局部变量,当退出函数时,局部变量也会随着函数调用结束而被销毁。
    // 退出函数后,局部变量a将被销毁
    let func = function () {
      let a = 1;
      console.log(a);
    };
    func();
     
    /**
     * 与之前的相反,退出函数后,局部变量a并没有消失,这是因为,当执行 let fn = func(); 时,f返回了一个匿名函数的引用,它可以访问到func()被调用时的函数,而局部变量a一直处在这个环境中,这就产生了一个闭包结构,局部变量的声明看起来被延续了。
     */
    let func = function () {
      let a = 1;
      return function () {
        a++;
        console.log(a);
      };
    };
    let fn = func();
    fn(); // 2
    fn(); // 3
    fn(); // 4

根据闭包,我们可以编写 Type 对象来封装一个类型判断工具,代码如下:

let Type = {};
for (let i = 0, type; (type = ['String', 'Array', 'Number'][i++]); ) {
  (function (type) {
    Type['is' + type] = function (obj) {
      return Object.prototype.toString.call(obj) === '[object ' + type + ']';
    };
  })(type);
}
console.log(Type.isArray([])); // true
console.log(Type.isString('str')); // true

闭包的作用

  1. 封装变量 闭包可以帮助把一些不需要暴露再全局的变量封装成“私有变量”
  2. 延长变量寿命
  3. 可以用闭包来实现面向对象
  4. 可以用闭包实现一些设计模式

闭包和面向对象设计

过程与数据的结合是面向对象系统中形容“对象”的常用表达。对象是以方法的形式包含了过程,而闭包则实在过程中以环境的形式包含了数据。所以我们可以用闭包来实现一个完整的面向对象系统。

闭包的写法是:

var extent = function () {
  var value = 0;
  return {
    call: function () {
      value++;
      console.log(value);
    },
  };
};
var extent = extent();
extent.call(); // 1
extent.call(); // 2

面向对象的写法是:

var extent = {
  value: 0,
  call: function () {
    this.value++;
    console.log(this.value);
  },
};
extent.call(); // 1
extent.call(); // 2

或者:

let Extent = function () {
  this.value = 0;
};
Extent.prototype.call = function () {
  this.value++;
  console.log(this.value);
};
let extent = new Extent();
extent.call(); // 1
extent.call(); // 2

闭包和命令模式

命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令执行之前,可以预先往命令对象中植入命令的接收者。

var Tv = {
  open: function () {
    console.log('打开电视机');
  },
  close: function () {
    console.log('关闭电视机');
  },
};
var createCommand = function (receiver) {
  var execute = function () {
    return receiver.open();
  };
  var undo = function () {
    return receiver.close();
  };
  return {
    execute: execute,
    undo: undo,
  };
};
var setCommand = function (command) {
  document.getElementById('execute').onclick = function () {
    command.execute();
  };
  document.getElementById('undo').onclick = function () {
    command.undo();
  };
};
setCommand(createCommand(Tv));

闭包和内存管理

人们对闭包有很多误解,其中最出名的是闭包会造成内存的泄露,所以要减少闭包的使用。

局部变量本应在函数退出时被解除引用,但如果局部变量被封印在闭包形成的环境中 ,那么这个局部变量就能一直生存下去。这样来看闭包确实会造成数据无法及时被销毁。

但是使用闭包的一部分原因就是我们选择将局部变量封闭起来,以待未来使用,所以对于内存的影响和全局变量是一致的,这不能说是内存泄露。如果需要回收这部分变量,我们可以手动将其置为 null。

真正的内存泄露来自使用闭包时容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,就有可能造成内存泄露。但是这并非 JavaScript 或者闭包的问题,而是 IE 浏览器选择了引用计数策略的垃圾收集机制,如果产生了循环引用,那么互相引用的两个对象都无法被回收。 需要解决这个问题,我们只需要把循环引用的变量设为 null 即可。