之前写过一篇详解js闭包的文章,后来重新读了一遍后感觉自己还是功力不够,很多知识点讲得都不够明白,文中举的例子也不够典型,而且动辄就是长篇大论,写了一大段文字却没有一个足够简单直白的例子,导致我自己再看的时候都觉得费劲。

功力不够,那就退而求其次,只好多找几个我觉得能够帮助更深刻地理解闭包的代码样例来分析。
(本次的例子都是我在stackoverflow上翻到的,然后加上了自己的解释)

例一:

function fun() {
  var num = 5;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = fun();
sayNumber(); //6

这个例子在函数fun中创建了一个内部函数say,函数say中引用了函数fun中的变量num。定义了函数say后,对num这个变量执行了自加操作,然后将函数say返回。

函数say的创建形成了闭包,如果闭包保存的是对外部函数变量的拷贝值的话,那么之后再执行函数sayNumber时,就应该打印出5,而不是6,因此本例证明了当形成闭包后,即使退出了函数fun的作用域,函数fun的变量依然没有被收回,而是保存在内存中,因此可以被函数sayNumber访问。

例二:

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  var num = 42;
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

这个例子稍微复杂一点,但是也好理解。首先定义了三个全局变量,这三个全局变量在行数setupSomeGlobals被赋值为三个函数,分别用于打印num,自增num以及改num的值。
由于这三个函数都引用了外部函数setupSomeGlobals中的变量num,因此会形成一个闭包。
在执行函数setupSomeGlobals后,调用这三个函数时,可以看到都是对同一个num变量进行操作,因此和上面的例子一样证明了闭包中的变量并不是一份拷贝。
不过要注意,之后又执行了一次函数setupSomeGlobals,此时三个全局变量被赋值为新的三个函数,此时形成了一个新的闭包,因此执行打印方法时,会打印出num的值为42。

例三:

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //打印 "item2 undefined" 3 次

这是一个很经典的关于闭包的例子,如果不理解闭包在这个例子中起到的作用,会对最后打印出的结果非常费解。
buildList方法其实是返回了一组函数,每个函数的“本意”都希望打印出在这个函数被定义时的list中下标为i的元素。但是在我之前的文章中介绍过javascript有一个特性,就是没有块级作用域,这意味着buildList方法中的for循环里定义的变量i以及变量item,其实在for循环结束后,依然存在于buildList的作用域中。并且在本例中,i最后的值为3,item最后的值为’item2’(循环体在i为3时并不会被执行,因此item的值不为’item3’)。
而在之前的两个例子中都介绍到了闭包保存的是外部函数中变量的引用而不是拷贝值。因此通过buildList生成的这一组函数其实在执行console.log(item + ‘ ‘ + list[i])时,i的值为3,item的值为’item2’,由于所传数只有三个元素,最后一个元素的下标仅到2,所以在本例的最后打印了三次”item2 undefined”。

例四:

function sayAlice() {
    var say = function() { console.log(alice); }
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// 打印 "Hello Alice"

本例涉及到闭包和变量提升两个知识点,观察函数sayAlicef可以发现,在定义函数say时,使用了一个尚未定义的变量alice,alice是在say被定义完成后才被声明和赋值。这里就涉及到javascript的预编译和变量提升的知识点了,当执行一个js脚本时,js引擎会预先扫描所有脚本中的所有变量和函数,然后将所有变量赋值为undefined,将所有函数都执行其指定的构造函数生成函数对象。
在变量被定义前访问该变量将会获得一个undefined。不过在本例中函数say只是通过闭包持有了变量alice的引用,在之后执行sayAlice()()时,alice已经完成了赋值,因此可以打印出”Hello Alice”。

例五:

function newClosure(someNum, someRef) {
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

本例证明了每次执行包含了内部函数的外部函数,都将形成一个新的闭包,因此不能说一个外部函数对应一个闭包,而应该说一个外部函数的一次执行对应一个闭包。
该例中的newClosure函数每次调用将产生一个新的闭包,因此fn1和fn2在执行时,对闭包内变量的访问和操作互不影响,因为闭包之间是隔离的,并不共享内存。

发表评论

电子邮件地址不会被公开。 必填项已用*标注