闭包的理解

158 阅读6分钟

什么是闭包

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。

也就是说,闭包可以让你从内部函数访问外部函数作用域。

在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

闭包实例

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。

displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。

请注意,displayName() 没有自己的局部变量。

然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。

闭包原理详解

外层函数的局部变量若被闭包函数调用,就不会在外层函数执行完毕之后立即被回收

不管什么语言,操作系统都会存在一个垃圾回收机制,将多余的分配空间回收以便减小内存。

而一个函数的生命周期的是从调用开始的,在函数调用完毕的时候,函数内部的局部变量等都会被回收机制回收。

但是当外层函数内部的闭包函数引用到某个变量的时候,闭包函数内部就会形成一个指向该变量的地址,所以即使外层函数执行完成,但是被引用的变量还在继续被闭包函数引用,不符合垃圾回收机制的标准(当一个变量在整个环境中都没有被引用的时候就会被清除),所以不会被清除。

但同时也可能会造成内存泄漏,所以需要我们在闭包函数引用完成后将引用的变量设置成null.

同一个闭包函数多次执行,同一作用域中的状态改变是连续的。

每调用一次外层函数都会生成一个新的闭包函数,每个闭包函数之间的作用域不会互相影响。

function people() {
    var age = 1;
    return function() {
        console.log(age++);
    };
}
var lily = people();
var zw = people();
lily(); //1
lily(); //2
zw(); //1

上面的例子中,lily和zw是两个不同的闭包函数。

在lily执行两次后,引用的变量age已经变成2了。

因为lily在多次执行之后,同一闭包函数作用域中的引用变量也在连续发生改变。

但是当新的闭包函数zw生成后,所引用的变量age还是初始的1。

所以我们可以看出不同的闭包函数的作用域是互不影响的。

闭包定义模块模式

使用闭包来定义公共函数,并令其可以访问私有函数和变量

var num = (function () {
    var number = 500;    
    function see() {console.log(number)};
    return {
        lily: () => {number +=2;},
        zw: () => {number +=1;},
        seeNum: () => {see()}
    };
})();
num.lily(); //502
num.lily(); //504
num.seeNum();
num.zw(); //505
num.seeNum();

这次我们只创建了一个词法环境,为三个函数所共享:num.lily,num.zw 和 num.seeNum。

该共享环境创建于一个立即执行的匿名函数体内。

这个环境中包含两个私有项:名为 number的变量和名为 see的函数。

这两项都无法在这个匿名函数外部直接访问,必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。

多亏 JavaScript 的词法作用域,它们都可以访问 number的变量和名为 see的函数。

在循环中创建闭包:一个常见错误

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
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 中定义了三个有用的提示信息,每一个都关联于对应的文档中的input 的 ID。

通过循环这三项定义,依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息。

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

原因是赋值给 onfocus 的是闭包。

这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。

这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。

这是因为变量item使用var进行声明,由于变量提升,所以具有函数作用域。

当onfocus的回调执行时,item.help的值被决定。

由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

解决这个问题的一种方案是使用更多的闭包:特别是使用前面所述的函数工厂:

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

这段代码可以如我们所期望的那样工作。

所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。

在这些环境中,help 指向 helpText 数组中对应的字符串。

另一种方法使用了匿名闭包:

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);
       }
    })(); // 马上把当前循环项的item与事件回调相关联起来
  }
}

setupHelp();

如果不想使用过多的闭包,你可以用ES2015引入的let关键词:

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,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

性能考量

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