JS 闭包

函数嵌套在其他函数中,能够访问到它们被定义时所处作用域中的任何变量,这给 JS 带来了强劲的编程能力。

什么是闭包?    —  闭包是函数和声明该函数的词法环境的组合。理解闭包,其关键在于:

外部函数调用后其变量对象本应被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象。


[ 词法作用域 ] 词法作用域中使用的域,由变量在代码中声明的位置所决定;嵌套的函数可访问在其外部声明的变量。

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

代码解析:init()创建了一个局部变量 name和一个名为 displayName()的函数。displayName()是定义在 init()里的内部函数,仅在该函数体内可用。displayName() 内没有自己的局部变量,然而它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init()中声明的变量 name


理解闭包

function makeFunc() {
    var name = "Webpiece";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

代码解析:运行代码的效果和上例效果一样,但不同,且有意思的地方在于:内部函数 displayName()在执行前,被外部函数返回。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc()执行完毕,我们会认为 name变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。


这个谜题的答案是:JS 中的函数会形成闭包闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。在这个例子中,myFunc是执行 makeFunc 时创建的 displayName函数实例的引用,而 displayName实例仍可访问其词法作用域中的变量,即可以访问到 name 。由此,当 myFunc 被调用时,name 仍可被访问,其值 Mozilla 就被传递到alert中。


下面是一个更有意思的示例 — makeAdder函数:                  // return function(y){ ... } 为简写形式(推荐)

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

代码解析:add5add10都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5的环境中,x为 5。而在 add10中,x则为 10。


闭包的应用

[1] 实用的闭包

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

在 Web 中,你想要这样做的情况特别常见。大部分我们所写的 JavaScript 代码都是基于事件的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常作为回调:为响应事件而执行的函数。

[ 效果演示?!] 假如,我们想在页面上添加一些可以调整字号的按钮。一种方法是以像素为单位指定 body元素的 font-size,然后通过相对的 em单位设置页面中其它元素(例如header)的字号:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

文本尺寸调整按钮可以修改 bodyfont-size ,由于使用相对单位,页面中的其它元素也会相应地调整。

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12、size14、size16 将分别把 body 文本调整为 12,14,16 px,可以将它们分别添加到按钮的点击事件上。


[2] 在循环中创建闭包:一个常见错误                   // 闭包只能取得包含函数中的任何变量的最后一个值

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//2

代码解析:代码的运行结果是2,而不是预想的0。出错的原因在于:在循环的过程中,并没有把函数的返回值赋值给数组元素,而仅仅是把函数赋值给了数组元素。这使得在调用匿名函数时,通过作用域找到的执行环境中储存的变量的值已经不是循环时的瞬时索引值,而是循环执行完毕之后的索引值。

解决方式1:可以利用IIFE传参和闭包来创建多个执行环境来保存循环时各个状态的索引值。因为函数传参是按值传递的,不同参数的函数被调用时,会创建不同的执行环境。

function foo(){
    var arr = [];
    for(var i = 0; i < 2; i++){
        arr[i] = (function fn(j){
            return function test(){
                return j;
            }
        })(i);
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0

解决方式2:使用块级声明可能比IIFE更为方便一些,由于块作用域可以将索引值i重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,相当于为每一次索引值都创建一个执行环境。

function foo(){
    var arr = [];
    for(let i = 0; i < 2; i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var bar = foo();
console.log(bar[0]());//0

[3] 用闭包模拟私有方法

编程语言中,比如 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量 — 也称为模块模式:

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