JavaScript之深刻理解闭包

什么是闭包?

闭包的概念
《JavaScript权威指南》:函数对象能够经过做用域链相互关联起来,函数体内部的变量能够保存在函数做用域内,这种特性称为“闭包”。javascript

很差理解?那就通俗点讲:所谓闭包,就是一个函数,这个函数可以访问其余函数的做用域中的变量。前端

理解闭包

理解闭包首先要了解嵌套函数的词法做用域规则,先来看一下这段代码java

var scope = 'global scope';        // 全局变量
var checkScope = function () {
    var scope = 'local scope';    // 局部变量
    function f() {
        return scope;
    }
    return f();                    // => local scope
};
checkScope();

checkScope()函数声明了一个局部变量,并定义了一个函数f(),函数f()反回了这个变量的值,最后将函数f()的执行结果返回。你应当很是清楚为何调用checkscope()函数会返回“local scope”。编程

这个词法做用域的例子介绍了引擎是如何解析函数嵌套中的变量的。词法做用域中使用的域,是变量在代码中声明的位置所决定的。嵌套的函数能够访问在其外部声明的变量。segmentfault

如今来考虑如下例子 :数组

var scope = 'global scope';        // 全局变量
var checkScope = function () {
    var scope = 'local scope';    // 局部变量
    function f() {
        return scope;
    }
    return f;    
};
checkScope()();                    // 返回值是什么?

这段代码中,咱们将函数内的一对圆括号移动到了checkscope()以后。checkscope()如今仅仅返回函数内嵌套的一个函数对象,而不是直接返回结果。在函数做用域外面,调用这个嵌套的函数会发生什么呢?闭包

这个谜题的答案是,JavaScript中的函数会造成闭包,闭包是由函数以及建立该函数的词法环境组合而成。也就是说,这个环境包含了这个闭包建立时所能访问的全部局部变量。架构

在这个例子中,嵌套的函数f()定义在这个做用域链里,其中的变量scope必定是局部变量,无论什么时候何地执行f(),这种绑定在执行f()时依然有效。所以最后一行代码返回“local scope”,而不是“global scope“。编程语言

若是你理解了词法做用域的规则,你就能很容易地理解闭包:函数定义时的做用域链到函数执行时依然有效。函数

然而不少同窗以为闭包很是难理解,由于他们在深刻学习闭包的实现细节时将自已搞得晕头转向。他们以为在外部函数中定义的局部变量在函数返回后就不存在了,那么嵌套的函数如何能调用不存在的做用域链呢?若是你想搞清楚这个问题,你须要更深刻地了解相似C语言这种更底层的编程语言,并了解基于栈的CPU架构:若是一个函数的局部变量定义在CPU的栈中,那么当函数返回时它们的确就不存在了。

但回想一下咱们是如何定义做用域链的。咱们将做用域链描述为一个对象列表,不是绑定的栈。每次调用JavaScript函数的时候,都会为之建立一个新的对象用来保存局部变量,把这个对象添加至做用域链中。当函数返回的时候,就从做用域链中将这个绑定变量的对象删除。若是不存在嵌套的函数,也没有其余引用指向这个绑定对象,它就会被当作垃圾回收掉。若是定义了嵌套的函数,每一个嵌套的函数都各自对应一个做用域链,而且这个做用域链指向一个变量绑定对象。但若是这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指向的变量绑定对象同样当作垃圾回收。可是若是这个函数定义了嵌套的函数,并将它做为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当作垃圾回收,而且它所指向的变量绑定对象也不会被当作垃圾回收。

下面再来看一个更有意思的示例:— makeAdder函数:

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

在这个示例中,咱们定义了makeAdder(x)函数,它接受一个参数x,并返回一个新的函数。返回的函数接受一个参数y,并返回x+y的值。

从本质上讲,makeAdder是一个函数工厂 — 他建立了将指定的值和它的参数相加求和的函数。在上面的示例中,咱们使用函数工厂建立了两个新函数 — 一个将其参数和 5 求和,另外一个和 10 求和。

Add5和add10都是闭包。它们共享相同的函数定义,可是保存了不一样的词法环境。在add5的环境中,x为 5。而在add10中,x则为 10。

实用的闭包

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

所以,一般你使用只有一个方法的对象的地方,均可以使用闭包。

接着来看一个uniqueInteger()函数,这个函数使用自身的一个属性来保存每次返回的值,以便每次都能跟踪上次返回的值。

var uniqueInteger = (function() {
    var counter = 0;
    return function() {
        return counter++;
    }
})();

你须要仔细阅读这段代码才能理解其含义。粗略来看,第一行代码看起来像将函数赋值给一个变量 uniqueInteger,实际上,这段代码定义了一个当即调用的函数,所以是这个函数的返回值赋值给变量uniqueInteger。如今,咱们来看函数体,这个函数返回另一个函数,这是一个嵌套的函数,咱们将它赋值给变量uniqueInteger,嵌套的函数是能够访问做用域内的变量的,并且能够访问外部函数中定义的 counter变量。当外部函数返回以后,其余任何代码都没法访问 counter变量,只有内部的函数才能访问到它。

像 counter同样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也能够访问它,这多个嵌套函数都共享一个做用域链,看一下这段代码:

function counter() {
    var n = 0;
    return {
        count: function() { return n++; }
        reset: function() { n = 0; }
    };
}
var c = counter(), d = counter();        // 建立两个计数器
c.count();                            // =>0
d.count();                            // =>0: 它们互不干扰
c.reset();                            // reset()和 count()方法共享状态
c.count();                            // =>0: 由于咱们重置了c
d.count();                            // =>1: 而没有重置d

counter()函数返回了一个“计数器”对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。首先要理解,这两个方法均可以访问私有变量n。再者,每次调用counter()都会建立一个新的做用域链和一个新的私有变量。所以,若是调用counter()两次,则会获得两个计数器对象,并且彼此包含不一样的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另一个对象。

从技术角度看,其实能够将这个闭包合并为属性存取器方法getter和setter。下面这段代码的私有状态的实现是利用了闭包,而不是利用普通的对象属性来实现:

function counter(n) { //函数参数n是一个私有变量
    return {
        //属性getter方法返回并给私有计数器var递增1
        get count() { return n++; },
        //属性setter不容许n递减
        set count(m) {
        if (m >= n) n = m;
        else throw Error("count can only be set to a larger value");
        }
    };
}
var c = counter(1000); 
c.count;                    // => 1000
c.count;                    // => 1001
c.count = 2000;
c.count;                    // => 2000
c.count = 2000;            // => Error!

须要注意的是,这个版本的counter()函数并未声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法能够访问n。这样的话,调用counter()的 函数就能够指定私有变量的初始值了。

再来一个例子,用闭包模拟私有方法:
编程语言中,好比 Java,是支持将方法声明为私有的,即它们只能被同一个类中的其它方法所调用。

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

下面的示例展示了如何使用闭包来定义公共函数,并令其能够访问私有函数和变量。这个方式也称为模块模式(module pattern)

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

在以前的示例中,每一个闭包都有它本身的词法环境;而此次咱们只建立了一个词法环境,为三个函数所共享:Counter.increment,Counter.decrement和Counter.value。

该共享环境建立于一个当即执行的匿名函数体内。这个环境中包含两个私有项:名为privateCounter的变量和名为changeBy的函数。这两项都没法在这个匿名函数外部直接访问。必须经过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法做用域,它们均可以访问privateCounter变量和changeBy函数。

闭包常见错误

咱们已经给出了不少例子,在同一个做用域链中定义两个闭包,这两个闭包共享一样的私有变量或变量。这是一种很是重要的技术,但仍是要特别当心那些不但愿共享的变量每每不经意间共享给了其余的闭包,了解这一- 点也很重要。看一下下面这段代码:

//这个函数返回一个老是返回v的函数
function constfunc(v) { return function() { return v; }; }

//建立一个数组用来存储常数函数
var funcs = [];
for(var i = 0; i < 10; i++) funcs[i] = constfunc(i);

//在第5个位置的元素所表示的函数返回值为5
funcs[5]() //=> 5

这段代码利用循环建立了不少个闭包,当写相似这种代码的时候每每会犯-一个错误:那就是试图将循环代码移入定义这个闭包的函数以内,看一下这段代码:

//返回一个函数组成的数组,它们的返回值是0~9
function constfuncs() {
    var funcs=[];
    for(var i = 0; i < 10; i++)
        funcs[i] = function() { return i; };
    return funcs;
}

var funcs = constfuncs();
funcs[5]()     //返回值是什么?

上面这段代码建立了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,所以它们能够共享变量i。当constfuncs()返回时,变量i的值是10,全部的闭包都共享这一个值,所以,数组中的函数的返回值都是同一个值,这不是咱们想要的结果。

关联到闭包的做用域链都是“活动的”,记住这一点很是重要。嵌套的函数不会将做用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照(staticsnapshot)。

书写闭包的时候还需注意一件事情, this是JavaScript的关键字, 而不是变量。正如以前讨论的,每一个函数调用都包含一个thi s值,若是闭包在外部函数里是没法访问this的,除非外部函数将this转存为一个变量:

var self = this;

绑定argument的问题与之相似。arguments并非一个关键字,但在调用每一个函数时都会自动声明它,因为闭包具备本身的绑定的arguments,所以闭包内没法直接访问外部函数的参数数组,除非外部函数将参数保存到另一个变量中:

var outerArguments = arguments;    // 保存起来以便嵌套的函数能使用它

参考:

* 《JavaScript权威指南》第六版
* [MDN Web 文档](https://developer.mozilla.org/zh-CN/)

推荐阅读:
【专题:JavaScript进阶之路】
JavaScript之“use strict”
JavaScript之new运算符
JavaScript之call()理解
JavaScript之对象属性


我是Cloudy,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
我的笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流前端各类问题!
相关文章
相关标签/搜索