闭包( closure ), 也叫词法闭包( lexical closure )或函数闭包( function closure )。语义上,闭包是一个保存了函数及其环境的记录。

在 MDN 上定义 JavaScript 中的闭包:A closure is the combination of a function and the lexical environment within which that function was declared。(闭包是一个函数以及声明该函数的词法环境的组合)。

理解词法作用域( lexical scoping )的规则–函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是在函数调用是决定的。所以,为了实现这种词法作用域,JavaScript 函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。

个人理解一个闭包必备的条件:

  • 一个函数
  • 该函数提供了外部访问函数内部变量的返回

词法作用域与闭包

词法作用域跟闭包有着很相似的结构,但是有着本质的区别。正常情况下,init 函数执行之后,通常函数创建的内部作用域被销毁。而闭包 makeFunc 可以阻止函数创建的作用域销毁。

词法作用域:

function init() {
  var name = 'Mozilla';
  function displayName() {
    console.log(name);
  }
  displayName();
}
init();

闭包:

function makeFunc() {
  var name = 'Mozilla';
  function displayName() {
    console.log(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

在循环中建立闭包

以下是一个很常见的例子

for (var i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}

我们的预期是分别输出 1 ~ 5,每秒输出一个数字。但上述代码将 5 个匿名函数分别间隔 1~5 秒放入异步队列中,当异步队列开始执行的时候,此时 i 作为全局作用域中的变量,其值是 6。记住一点,嵌套的函数不会将作用域内的私有成员复制一份,也不会对绑定的变量生成静态快照(static snapshot)。

第一个解决方式:创建一个闭包,或者使用立即执行表达式创建匿名闭包函数。

// 使用闭包
function makeTimeout(i) {
  return function () {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  };
}

for (var i = 1; i <= 5; i++) {
  makeTimeout(i)();
}
// 使用匿名闭包
for (var i = 1; i <= 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  })(i);
}

PS: 使用立即表达式会立即执行,如果想延迟执行,请使用闭包。

第二个解决方式:使用 ES6 中的 let 关键字

for (let i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}

用途

  • 读取函数内部的变量,使用闭包模拟私有方法
  • 变量的值始终保持在内存中,不会在函数调用后被自动清除

闭包创建单例模式

可以使用闭包实现单例模式。让一个类仅有一个实例,并且全局可以访问该实例。从而避免一个全局类的频繁创建域销毁。我们在运行创建的时候首先判断该单例是否存在,如果存在则返回,不存在则创建。

var Singleton = function (name) {
  this.name = name;
  this.instance = null;
};

Singleton.prototype.getName = function () {
  return this.name;
};

Singleton.getInstance = function (name) {
  if (!this.instance) {
    this.instance = new Singleton(name);
  }
  return this.instance;
};

// have a try
var a = Singleton.getInstance('a');
var b = Singleton.getInstance('b');
console.log(a === b); // true

注意点

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。解决方法是,在退出函数之前,将不使用的局部变量全部删除

参考链接