翻译 Secrets of the JavaScript Ninja - 5.闭包 (5.Closing in on closures)

这章写得极为精彩,让我明白了好多以前根本不知道的概念。
闭包是一个神奇的东西,新果说过,闭包就是可以当作鞭子来用,可以打出鞭子头一样的力道。
所谓的函数化这个概念中,闭包是一个很重要并且很精髓的概念。
想要理解函数化编程,就要深刻的理解闭包。

本章重点:
1.闭包的定义,闭包是什么,闭包如何工作
2.利用闭包来实现一些简单的开发
3.利用闭包实现性能上的增强
4.利用闭包实现私有域

5.1 闭包如何工作(How closures work)

问:闭包是什么?
答:简单来说,closure是一块域,这块域是由创建一个function而来,这个function可以访问和操作它的外部的变量
(Simply put, a closure is the scope created when a function is declared that allows the function to access and manipulate variables that are external to that function.)
这个概念最好还是用代码来解释,所以让我们看看5.1这个例子:

Listing 5.1: A simple closuer

var outerValue = 'ninja';  
function outerFunction(){  
    assert(outerValue == 'ninja', "I can see the ninja")  
}  
outerFunction(); 

你可能写过n多次这样的代码,可你竟然没有意识到你在创建闭包(closure)!
你不相信?估计是因为你没有感觉到有任何的惊喜。
因为out value和outer function的作用域是全局域(global scope),而全局域是永远不会消失(自从这个页面被加载就存在了),
另外就算是这个function可以访问outer value,这个特点也没有实用的价值。
所以就算closure已经存在了,它的用处也不明显。
让我们给他加上一点料,来看看5.2这个列子吧:

Listing 5.2: A not-so-simple closue

var outerValue = 'ninja';  
var later;  

function outerFunction(){  
    var innerValue = 'samurai';  

    function innerFunction(){  
        assert(outerValue, "I can see the ninja");  
        assert(innerValue, "I can see the samurai");  
    }  

    later = innerFunction;  
}  

outerFunction();  
later();  

首先,我们定义了later这个变量,我们会在后面用到它。
然后,我们在outer function中定义了inside这个变量,这样inside就被限制在outerFunction域内了。
然后,我们在outerFunction中定义了一个innerFunction,innerFunction可以访问我们已经定义过的所有外部变量。
是的!我们可以这样做!记住上一章我们讨论过的吗?
function是自然类型的对象(first-class objects),它和变量一样在任何地方都可以被创建。
我们当然也可以将inner function赋给later变量,以便稍后调用。

一切就绪,我们开始运行。
我们调用outer function,创建了inner function,然后inner function赋给了later变量(全局变量),然后通过调用全局变量later,等价于调用了inner function。
发生了什么事情?
1.inner function可以访问outerValue,因为outerValue属于全局域,
2.由于js的特性,在正常情况下,全局域中我们是无法访问到innerValue的,但是通过将innerFunction赋给later变量,later就称为了域之间的桥梁,我们自然在全局域可以访问到innerValue。
这是什么原理呢?答案就是,闭包(closures)!

当我们创建innerFunction的时候,不仅仅是定义了一个function,我们还创建了一个域。
在外部掉用innerFUnction的时候,虽然调用发生在全局域,但是innerFunction还是会访问当初被定义的时刻的那个原始域。这个域就是它的闭包(closure)

让我们来看看下一个例子:5.3

Listing 5.3: What else closures can see

var outerValue = 'ninja';  
var later;  

function outerFunction()  
{  
    var innerValue = 'samuai';  

    function innerFunction(paramValue)  
    {  
        assert(outerValue, "Inner can see the ninja");  
        assert(innerValue, "Inner can see the samurai");  
        assert(paramValue, "Inner can see the wakizashi");  
        assert(tooLate, "Inner can see the ronin");  
    }  

    later = innerFunction;        
}  

assert(!tooLate, "Outer can't see the ronin");  

var tooLate = 'ronin';  

outerFunction();  
later('wakizashi');  

这个例子中表达了3个关于closure的概念:
1.function的参数也被包含在闭包之中
2.所有的外部变量都属于闭包可访问的范围(外部域),包括那些在闭包后面创建出来的变量。
3.在同一个域中,不可以访问后面才创建的变量
第2和第3点解释了为什么只有inner closure可以看见tooLate,而outer closure则不可以看见tooLate。

虽然闭包并不引人注意(你并不能通过工具检测到“闭包”这个对象),但是我门还是要意识到闭包其实是占用了内存的。
请记住,闭包威力无穷,但是也有危险。
如果你有强烈的意愿想用好闭包,闭包毫无疑问是威力无穷的。
但是它理所当然需要系统开销。
所有闭包中的信息数据都已经缓存在内存之中,
如何释放掉这块内存呢?
这里有两种办法:
1.通过JavaScript引擎的GC垃圾回收机制
2.刷新或者关闭当前页面。

5.2 让闭包用起来(Putting closures to work)

现在,我们已经明白了闭包是什么,并且知道它如何使用(虽然是很high level的),
现在我们要真正让闭包用起来。

5.2.1 私有变量(Private variables)

在JavaScript中如何实现让一个域拥有私有的变量呢?
答案就是使用闭包。
我们看到大部分JavaScript菜鸟都是在用面向对象(Object-oriented)的方式来编写JavaScript,
这样做带来的缺点就是无法实现私有变量。

在例子5.4中我们会看到闭包是如何搞定私有变量的

Listing 5.4:Using closures to approxiate private variables

function Ninja()  
{  
    var slices = 0;  

    this.getSlices = function()  
    {  
        return slices;  
    }  

    this.slice = function()  
    {  
        slices++;  
    }  
}  

var ninja = new Ninja();  

ninja.slice();  

assert(ninja.getSlices() == 1,  
    "We're able to access the internal slice data.");  
assert(ninja.slices == undefined,  
    "And the private data is inaccessible to us.");  

这个例子表明了,Ninja这个函数中的数据状态,是由Ninja这个函数来维护,
外部是无法干预到Ninja这个函数内部的变量的。
函数内部的变量,只允许Ninja的内部方法来访问。

现在,让我们回过神来,我们要继续探索闭包的另一个牛逼的用法。

5.2.2 Callbacks and timers

另一个闭包的牛逼用法,就是实现了回调(callbacks)和定时器(timer)
让我们来看一个利用jQuery来实现Ajax请求的例子5.5

Listing 5.5: Using closures from a callback for an Ajax request

var jQuery = function(){  
    return {  
        click:function(){}  
    }  
};  

jQuery('#testButton').click(function(){  
    var elem$ = jQuery("div");  
    elem$.heml("Loading...");  

    jQuery.ajax({  
        url: test.html,  
        success: function(html){  
            assert(elem$,  
                "We can see elem$, via the closure for this callback.");  
            // elem$.html(html);  
        }  
    })  
})  

jQuery大家制定用的很熟悉了,这里就不过多解释了,如果你看不懂这个例子,先去http://www.w3cschool.cn/index-30.html看看jQuery的概念吧,我只能帮你到这里了Brother。

下面让我们看看Timer的例子:

Listing 5.6: Using a closure in a timer interval callback

var elem = document.getElementById("box");  
var tick = 0;  

var timer = setInterval(function(){  
    if (tick < 100){  
        elem.style.left = elem.style.top = tick + "px";  
        tick++;  
    }else{  
        clearInterval(timer);  
        assert(tick == 100,   
            "Tick accessed via a closure.");  
        assert(elem,  
            "Element also acessed via a closure.");  
        assert(timer,  
            "Timer reference also obtained via a closure.")  
    }  
}, 10); 

下面,让我们来看看如何让function的上下文(contexts)为我们工作。

5.3 Binding function contexts

我们在上一章已经讨论果函数上下文这个概念,你还记得如何更改一个函数的上下文吗?
对了!用.call()和.apply()。
ps:你还记得call和apply的区别吗?不记得的话就回去看看吧。

在下面这个例子中,我们想让一个对象的方法绑定到一个dom元素上

Listing 5.7:Bingding a specific context to a function

var button = {  
    clicked : false,  
    click: function(){  
        assert(this == elem, "This is the elem, not the object button")  
        this.clicked = true;  
        assert(button.clicked, "The button has been clicked");  
    }  
}  

var elem = document.getElementById("test 5.7");  
elem.addEventListener("click", button.click, false);  

运行上面这个例子之后,我们会发现,结果并不是我们预期的。
原因是click方法并没有向我们预期的那样绑定在了button对象上。
根据我们之前学习的第三章的知识,如果我们这么调用:
button.click()
那么函数的的上下文就会是button对象了。
但是在这个例子中,函数的上下文则是dom中的元素,不是js中的button对象。

如何解决这个问题呢?
答案还是利用闭包!
我们来改进一下上面的例子,请看例子5.8

Listing 5.8: Bingding a specific context to an event handler

function bind(context, name){  
    return function(){  
        return context[name].apply(context, arguments);  
    };  
}  

var button = {  
    clicked : false,  
    click: function(){  
        assert(this == button, "This is the object button")  
        this.clicked = true;  
        assert(button.clicked, "The button has been clicked");  
    }  
}  

var elem = document.getElementById("test 5.8");  
elem.addEventListener("click", bind(button,"click"), false); 

bind()函数被广泛的应用在了JavaScript的原型库中(Prototype JavaScript Library),
这样就可以优雅的写出典型的面向对象的代码(Object-oriented)
即便如此,在需要编写事件机制的代码时候,还是很容易出现函数上下文错误的问题。
让我们来看看面向对象的代码风格如何实现bind的,请看例子:5.9.

Listing 5.9: An example of the function binding code used in the Protoype library

Function.prototype.bind = function(){  
    var fn = this,   
    args = Array.prototype.slice.call(arguments),  
    object = args.shift();  

    return function(){  
        return fn.apply(object,  
            args.concat(Array.prototype.slice.call(arguments)))  
    };  
};  

var myObject = {};  
function myFunction(){  
    return this == myObject;  
}  

assert( !myFunction(), "Context is not set yet");  

var myFunction = myFunction.bind(myObject);  
assert(myFunction(), "Context is set properly"); 

注意,这里.bind()的目的并不是要取代.apply()或者.call()。
这个例子想要说明的是,我们可以通过闭包和匿名函数(anonymous function)来操控上下文(context),
进而实现,当延迟调用此函数的时候,上下文也不会更改。
这种情形在回调(callbacks)和事件机制(event handlers and timers)中会很有帮助。

5.4 滚雪球式的函数(Partially applying functions)

同学们,注意啦,精彩的章节来了!!!想知道如何利用闭包聚集力量,发出致命一击吗?
忍着师傅要传授绝密武功了,请仔细的阅读本章节。

在学习此章节前,我们先来思考几个题外话。
你们有没有感觉到,想要做好任何一类事情,都可以找到这个事情的一个巧劲。
例如,打羽毛球,不应该用蛮力击球,而是应该找到,放松的通过一段位移,挥舞牌子产生惯性的力来击球。说白了就是一种放松情况下产生的力道。
例如,李小龙悟到的打拳的发力,不应该用蛮力,而是让肌肉放松,将手臂想象成鞭子,将拳头甩出去打人。
例如,弹钢琴,不应该用蛮力,应该用手指产生的惯性力来敲打键盘,有一句话,钢琴是弹的不应该是按的。这里关于钢琴的正确发力的学问,叫做触键。
例如,制造一个大雪球,如果用蛮力来做,这是在地上堆砌雪球,而正确的做法是先在山顶制造一个小雪球,然后通过向下滚动,逐渐让雪球逐渐变大,最后成长为一个大雪球。
总结看来,这些巧劲的共性,就是利用距离和时间转化为力量(类似与势能转换为动能)。而不是蛮力本身的传递。
这样的例子太多了,我们做任何事情,都应该意识到,核心的问题是如何找到那种巧劲,编程也是如此。

那么JavaScript的巧劲是什么呢,请看下文分解?

“滚雪球式”(“Partially applying”)是一个非常有趣的技术,
利用这个技术,我们可以预先将参数安装到函数中,而不用立即执行此函数。
等到我们想执行此函数的时候,我们可以在任意时刻触发。
这里就是将时间(提前按照参数)和距离(中间增加函数)看作势能,
而实际调用此函数的那一瞬间则是是将势能转换为动能。

我们来看看本书作者如何定义的。
本书作者引用了传统的一个词汇,“currying”
定义如下:
首先将一批函数转入一个函数(然后这个函数返回一个新的函数),这中形式就叫“做科里化”(currying)

我想到新果曾经在解释currying的时候写的一个例子,如下:
用currying化实现2个数相加。

var add = function(a)  
{  
    return function(b){  

        return a + b;  
    }  
}  

assert(add(1)(2) == 3, "Curring: add two integer");  

下面我们来看本书中的如何通过例子来表达currying:
例如我们现在有个需求,需求是我们想将一个字符串转换为CSV格式的一个字符串数组。
如果是菜鸟,我们一般这样写:

var elements = "val1, val2, val3".split(/, \s*/);  

有没有觉得上面这种写法很笨拙,每次想将一个字符串转换为CSV格式的时候,还必须得记住这个特定得正则表达式。
同学们,我们得目标是成为牛逼闪闪得JavaScript Ninja,我们要用国际范的方式来搞定这个问题。
so 让我们创建一个csv()方法来做这个事吧。
让我们想象一下,如果用currying化来编写这个csv()呢?
请看例子5.10

Listing 5.10:Partially applying arguments to a native function

Function.prototype.curry = function(){  
    var fn = this,   
    // 这里就是在预装参数,将参数抓住,缓存在变量args中  
    args = Array.prototype.slice.call(arguments);  
    return function(){  
        return fn.apply(this, args.concat(  
            Array.prototype.slice.call(arguments)))  
    }  
}  

String.prototype.csv = String.prototype.split.curry(/,\s*/);  
var results = ("Mugan, Jin, Fuu").csv();  

assert(results[0] == "Mugan" &&  
        results[1] == "Jin" &&  
        results[2] == "Fuu",  
        "The text values were split properly");  

上面这个例子看懂了吗?
curry这个函数做的事情,是将函数中的this和arguments缓存在了闭包之中。
当split函数调用curry的时候,curry中的this就是split函数本身,
正则表达式这个参数则是预存在curyry中的arguments。

虽然这段代码实现已经很不错了,但是我们还可以进行一些改进。
上面这个curry函数,实现了将所有参数缓存在闭包之中。
当调用闭包返回的新的函数的时候才去处理之前预装的所有参数。

但是如果我不想让所有的参数全部预装,而是只预装其中一部分,
另外一部分参数要在调用闭包返回的那个新的函数中,将这部分参数传入。

这类问题,已经在其他语言中实现了,Oliver Steele就是其中的一个作者。
他开发的类库叫Functional.js(http://osteele.com/sources/javascript/functional/)

让我们看看我们自己的实现,例子:5.12

Listing 5.12: A more-complex “partial” function

Function.prototype.partial = function(){  
    var fn = this, args = Array.prototype.slice.call(arguments);  
    return function(){  
        var arg = 0;  
        for (var i = 0; i < args.length && arg < arguments.length; i++){  
            if (args[i] === undefined){  
                args[i] = arguments[arg++];  
            }  

        }  
        return fn.apply(this, args);  
    }  
}  

var delay = setTimeout.partial(undefined, 10);  
delay(function(){  
    assert(true,"A call to this function will be delayed 10 ms");  
})  


var bindClick = document.body.addEventListener.partial("click", undefined, false);  

bindClick(function(){  
    assert(true, "Click event bound via curried function.");  
})  

partial()的实现和curry()有些相像。
在第一批的参数中,我们用undefined代表了那部分缺省参数。
在后面delay调用的时候,才将真正想要被处理的参数传递进去。

5.5 函数的重写(Overriding function behavior)

JavaScript是一个很有趣的语言,在JavaScript中,
你可以在用户毫无感知的情况下,完全的控制一个函数的内部行为。
具体来说有两种方法可以做到这件事:
1.更改已经存在的函数(不需要通过闭包)
2.基于已经存在的函数,来创建一个新的函数,在这个新的函数中加入自身更改的逻辑

记得第四章的一个例子吗?函数结果的记忆能力(memoization)
让我们来看看另一种实现的版本

5.5.1 函数的记忆能力(Memoization)

所谓memoization,即让一个函数具备一种可以记忆它历史被调用时候产生的运算结果的能力。
在第四章中,我们实现memoization的方法是采用了直接去更改一个已经存在的函数。
这样做真是太粗暴了。
我们需要优化这个实现。

让我们创建一个叫做memoized()的方法,在例子5.13中,
我们实现了用memoized()来记住一个已经存在的函数的返回值。
在这个例子中我们并没有用到闭包

Listing: A memorization method for functions.

Function.prototype.memoized = function(key){  
    this._values = this._values || {};  
    return this._values[key] !== undefined ?   
        this._values[key] :   
        this._values[key] = this.apply(this, arguments);  
}     


function isPrime( num ){  
    var prime = num != 1;  
    for ( var i = 2; i < num; i++)  
    {  
        if (num % i == 0){  
            prime = false;  
            break;  
        }  
    }  
    return prime;  
}  
assert(isPrime.memoized(5), "The function works; 5 is prime.");  
assert(isPrime._values[5], "The answer has been cached.");  

在上面这个例子中,结果都被缓存在了_values中。
有趣的一点是,计算和存储是在一个single step中。
结果是通过.apply()来调用父函数来完成,然后直接将结果存储在了父函数的属性_values中。
所以整个事件链是这样:
计算出结果,保存结果,返回结果。所有这些都是在一个逻辑代码块中完成(a single logical unit of code)
这样做的缺点在于:调用isPrime()的时候还必须记得要调用.memoized()方法,来实现memoization。

如果解决这个缺点呢?
用闭包!

Listing 5.14: A technique for memorizing functions using closures

Function.prototype.memoized = function(key){  
    this._values = this._values || {};  
    xyz = this._values;  
    return this._values[key] !== undefined ?  
        this._values[key] :  
        this._values[key] = this.apply(this, arguments);  
}  

// 这里得memoize就是利用闭包的特性,来隐性的更改了ipPrime的行为  
Function.prototype.memoize = function(){  
    var fn = this;                              //#1  
    return function(){                          //#2  
        return fn.memoized.apply(fn, arguments);  
    }  
}  

var isPrime = (function ( num ){  
    var prime = num != 1;  
    for ( var i = 2; i < num; i++)  
    {  
        if (num % i == 0){  
            prime = false;  
            break;  
        }  
    }  
    return prime;  
}).memoize();  

在5.13这个例子的基础上,我们通过一点改进,完美的解决了之前的那个缺陷。
在本例中,我们创建了一个新的方法,memoize()。
在memozie()中,利用memoized()重新包裹了原始函数(original function)。
这样一来,memoize返回的函数就永远会是具备了momoized能力的函数(#2)

请注意,在momozie()这个方法中,我们构建了一个闭包,
在这个闭包中通过fn这个变量拷贝了原始函数:isPrime函数(通过获取其上下文 #1),
这是一个很通用的技巧:每个函数都拥有自己的上下文,所以上下文从来都不是闭包的一部分。
但是我们可以利用一个变量来关联原始函数的上下文的值,
这样就等同于函数上下文(isPrime context)成为了闭包的一部分。
由于原始函数已经成为了闭包的一部分,那么闭包自然可以在返回的新函数中,
随意的操作原始函数(isPrime),让其具备记忆能力.

在例5.14中我们还使用了一个特别技巧:立即执行一个函数(immediately)
在后面5.5.3这个章节中,我们会详细的讨论这个技巧。

例5.14是一个很好的展现了闭包的威力的例子,但是我们也要知道,这种技巧同样也会有缺陷。
如果过多的利用闭包来隐性的修改一个函数的逻辑。那么将会让这个函数成为一个很难被继承的函数。
过于极端的使用这个技巧也是不可取的。
不过无所谓,因为后面我们将会学习一种技巧来搞定这个缺陷,
那就是将这部分逻辑封装在一个可以延迟调用的函数中。

5.5.2 函数包装(Function wrapping)

函数包装是一个的用来封装函数功能的技巧。
如果想要继承或者创建一个新的函数的时候,通过函数包装可直接实现。
最有价值的一个场景是:在我们想要重写(override)一些已经存在的函数的情况下,
并且可以保持在原始函数中那些有用的部分可以在被包装后仍然有效。

另外一个普遍的场景是:兼容不同的浏览器。
例如在Opera浏览器中,有一个获取title属性的bug。
Prototype类库采用了函数包装这个技术来对付这个bug。

为了防止在readAttribute()函数中,过多的出现if/else这样的代码快(这是一个比较丑陋,并且不是一个特别好的分割逻辑的方式)。
Prototype选择利用函数包装(function wrapping)来重写(override)那个老的方法,
但是原始函数中的那些正常的逻辑还应该有效

(这一段比较难翻译,之前读的时候,并没有完全理解作者真正想要表达的那些有价值的细节)

让我看例5.15

Listing 5.15: Wrapping an old function with a new piece of functionality

function wrap(object, method, wrapper){  
    var fn = object[method];                //#1  
    return object[method] = function(){     //#2  
        return wrapper.apply(this, [fn.bind(this)].concat(  
            Array.prototype.slice.call(arguments)));  
    }  
}  

// Example adapted from Prototype  
if (Prototype.Browser.Opera){               //#3  
    wrap(Element.Methods, "readAttribute",  
        function(original, elem, attr){  
            return attr == 'title' ? elem.title : original(elem, attr);   
        })  
}  

//#1 缓存原始函数  
//#2 返回新的包装函数  
//#3 实现函数包装  

我们来分析一下wrap()函数是如何工作的。
在传入wrapp()的参数中包括:
1.一个基础的对象(object)
2.这个基础对象想要被包装的方法(method)
3.新的包装函数(wrapper)

首先,我们将原始方法保存在变量fn中(#1);
我们在后面会通过匿名函数的闭包(anonymous function’s closure)来调用它。
然后我们用一个新的函数来重写这个方法(#2)
这个新的函数执行了之前传进来的wrapper函数(通过一个闭包),并且传给它一个重新构造的参数列表.
在构建这个参数列表时,我们想让第一个参数是我们正在重写的原始函数(original function),
这样一来,我们创建一个数组来连接原始函数(原始函数的上下文已经被绑定,通过bind()函数,我们之前在例5.3中创建的,同样适用于包装函数),然后将原始参数追加到这个数组中。
在apply()函数中,如我们所见,用这个数组作为了他的参数列表。

Prototype类库利用wrap()函数,实现了对一个已经存在的函数的重写(在这个例子中是重写了readAttribute函数)
将这个函数替换成了一个包装后的新函数(#3).
然而,这个新的函数仍然可以拥有原始函数的原始功能(通过原始参数)。
这就意味着,一个函数被重写之后,仍然可以保留原始的功能,这是一个很安全的技术。

结论就是,wrap()函数是一个可以重用的函数,我们可以用一种比较隐蔽的方式,
来重写任何对象的方法中的已有的功能。
这又是一个彰显闭包威力的案例。

现在,让我来看一个经常会被用到,也很古怪的一个表达式。
它同样也是函数式编程的一个重要部分。

5.6 即时函数(Immediate functions)

让我们来看一个简单的代码结构,这是一个函数化编程中很重要的部分,
同样也是闭包中我们还未展示的一个亮点。
代码如下:
(function(){})()
这中模式的代码,毫无疑问的可以用在很多地方,
它给JavaScript带来了很多出乎意料的能力。
这段代码中的花括号和原括号看起来比较陌生,
让我们来一点一点的分析一下:

首先,让我们忽略第一个原括号里的内容,让代码变成这样:
(…)()
我们知道,我们可以通过函数名加原括号(functionName())这样的代码表达式,来调用任意的一个函数
但是我们可以用任意的一个关联函数实例的表达式,来替换函数名(function name).
这正是为什么我们可以调用一个指向函数的变量(variableName())。

所以在(…)()这个代码中,第一组圆括号只是用来划定逻辑范围,第二组圆括号则是操作符。
不同位置的圆括号表示的意义不同,这里可能会有一点小混乱。
如果函数的调用的操作符换成另一个样子,用~~代替(),代码变成这样(…)~~这样会不会清晰一些。

最后,第一组括号里要是一个函数或者引用函数的变量。所以代码还是会变成这样:
(functionName)()
虽然如果没有第一组括号,这个代码仍然有效。

现在,如果我们用一个匿名函数来代替第一组括号里的内容,代码是这呀:
(function(){})();
如果函数体的内部加上一些逻辑语句,代码变成这样:

(function(){  
    statement-1;  
    statement-2;  
    ...  
    stateent-3;  
})  

注意,在我们处理一个函数的时候,我们会的得到一个闭包,
我们可以在这个函数中访问外部变量。
总的来说,这个简单的代码结构,叫做即时函数,
稍后我们会看到它的价值。

让我们来看看即时函数(immediate function)和域(scope)的协作。

5.6.1 临时域和私有变量(Temporary scope and private variables)

利用即时函数,我们可以开始构建一个有趣的封闭空间来做些事情。
正是因为即时函数是直接执行的,所以在这个函数域中的变量是受保护的,
我们可以利用这个来创建一个临时域,来维护数据状态。

注:变量在JavaScript中的作用域是依赖于定义这个变量的函数。
利用创建一个临时函数,我们可以用它来来装载我们的变量。
来看这段小代码:

(function(){  
    var numClicks = 0;  
    document.addEventListener("click", function(){  
        alert( ++numClicks );  
    }, false);  
})() 

就像及时函数的名字一样,这个函数会立即执行,click handler会立即被绑定。
一个闭包被创建出来,允许numClicks变量来存储数据。
这是即时函数非常通用的一个方式,就像一个简单的功能包。

如论怎样,我们要记得他们依旧是函数,他们可以被用的更加有趣,例如:

document.addEventListener("click",(function(){  
    var numClicks = 0;  
    return function(){  
        alert( ++numClicks);  
    };  
})(),false)  

这个例子相比其之前那个例子,会让人有点迷惑。
在这个例子中,我们同样创建了一个即时函数,
但是这一次我们加了一个返回值:一个类似事件处理者的函数(event handler)。
正象其他表达式一样,这个返回值传递给了addEventListener函数。
这个内部函数理所当然的可以访问numClicks变量,利用闭包的特性。

这个技巧是用一个很特别的视角来观察作用域。
在大多数的变成语言中,一个作用域是依赖它所在的代码快的。
但是在JavaScript中变量的作用域是依赖它所在的闭包
(In JavaScript, variables are scoped based upon the closure that they are in.)

此外,利用这个简单的结构,我们可以将作用域延伸到当前代码块和上一级代码块。
通过调用一个函数,让一些代码的作用域可以和传入这个函数的参数相连接。
这种能力无疑是很有用的,并且彰显了JavaScript的灵活性。

List 5.16: Using an immediate function as a variables shortcut

(function(v){  
    Object.extend(v,  
    {  
        href:   v._getAttr,  
        src:    v._getAttr,  
        type:   v._getAttr,  
        action: v._getAttrNode,  
        disabled:   v._flag,  
        checked:    v._flag,  
        readonly:   v._flag,  
        multiple:   v._flag,  
        onload:     v._getEv,  
        onunload:   v._getEv,  
        onclick:    v._getEv  
    });  
})(Element._attributeTranslations.read.values);  

在这个例子中,Prorotype继承了一个对象的属性和方法。
在这个代码中,一个临时的变量被创建出来,目的是操作Element._attributeTranslations.read.values,
这个变量通过第一个参数,传入了这个即时执行的匿名函数。
这就意味着,第一个参数现在已经连接了当前的数据结构和作用域。
这个技巧用在循环(looping)上更有用,我们会在下面讨论。

5.6.2 循环(Loops)

即时函数的另一个有用的地方是,它利用循环和闭包可以解决一些丑陋的问题。
例5.17就是一个拥有普遍问题的代码

Listing 5.17: A problematic piece of code in which the iterator is not maintained in the closure.

<div>DIV 0div>  
<div>DIV 1div>  
var div = document.getElementsByTagName("div");  
for (var i = 0; i < div.length; i++){  
    div[i].addEventListener("click", function(){  
        alert("div #" + i + " was clicked.");  
    }, false);  
}  

我们的目的想看到点击每个

的时候,会显示它的原始的值。
但是当我们点击“DIV 0”的时候,我们竟然看到它显示的是 “div #2 was clicked.”
哪里出错了呢?为什么DIV 0显示的是2

在上个例子中,我们遇到了一个适用闭包和循环时候常见的问题,
也就是说,在函数被绑定后,闭包抓住的变量(在这个例子对应的是i)已经被更改
这就意味着,每个被绑定的处理函数(function handler)都会显示i的最后的值,在这个例子中是2。

这就证明了我们已经在5.2.2章节讨论过的一个问题:
闭包记住的是对变量的引用,而不是闭包创建时刻变量的值。
这一点区别,让很多人都走了弯路。

不过不要害怕,我们可以用另一个闭包和即时函数来修正当前这个闭包的毛病,俗话说的好:以毒攻毒(fighting fire with fire, so to speak)。
请看例5.18
Listing 5.18: Using an immediate funtion to handle the iterator properly

<div>DIV 0div>  
<div>DIV 1div>  
var div = document.getElementsByTagName("div");  
for (var i = 0; i < div.length; i++){  
    (function(i){  
        div[i].addEventListener("click", function(){  
        alert("div #" + i + " was clicked.");  
        }, false);  
    })(i)     
}  

通过在for循环内加入即时函数,我们将正确的值传递给了及时函数的,进而让工作者(handler)得到了正确的值。

这个例子很清楚的展示了,如何利用即时函数和闭包来控制作用域里的变量和值。

5.6.3 类库包装(Library wrappping)

另一个闭包和即时函数很重要的用途,就是用于JavaScript类库的开发。
当我们开发一个类库时,很重要的一点是,我们不希望变量去污染全局命名空间,尤其是一些临时变量

为了解决这个问题,闭包和即时函数十分有用,它可以帮助我们让类库尽可能的私密,并且可以有选择的让将一些变量链接到全局命名空间。
jQuery就专注于这个原则,完全的封装它的功能,只是有选择的将一些变量关联到全局空间。
例如jQuery自身这个变量,如下面:

(function(){  
    var jQuery = window.jQuery = functon(){  
        // Initialize  
    }  
    // ...  
})()  

注意:这里有两次赋值,
首先jQuery构造器(作为一个匿名函数)赋值给了window.jQuery,这样就将其暴露到了全局空间。
尽管如此,并不能保证我们内部域中没有别的变量叫做jQuery,所以我们将它赋予了一个本地变量jQuery。
在我们自己的创建的世界中,jQuery这个本地变量的含义是确定的。

由于类库将变量和函数封装的十分优雅,也就让最终使用类库的用户拥有了很好的灵活性。
当然,我还可以用另外一种形势的代码,同样实现这个效果,如下所示:

var jQuery = (function(){  
    function jQuery(){  
        // Initialize  
    }  
    //...  
    return jQuery;  
})();  

这段代码和之前的那个例子实现了同样的效果,只是代码组织形势不同而已。
我们在匿名函数中定义了一个jQuery函数,它可以自由的被用在这个域中,
然后我们将它return,这样可以将它赋值给同样叫做jQuery的全局变量。
通常情况下,是否使用这个技巧,取决于你是否想暴露唯一的一个变量到外部。
这个技巧看起来会清晰一些。

总值,这么多种不同的形势,到底选择哪个,完全取决于开发者的意愿。

5.7 总结(Summary)

在这个章节中,我们讨论了闭包,在函数化编程(functional programming)中,闭包是十分关键的一个概念。
JavaScript这们语言设计的是按照函数化编程(functional programming)的思想设计的。
理所当然,在JavaScript中我们可以用闭包。

在一开始的基础部分,我们了解了闭包如何实现,并且看到了在一个应用中如何使用闭包。
我们讨论了很多闭包的特别的用处,包括用闭包实现私有变量,实现回调和timers。

接下来我们展示了很多闭包高级的用法,闭包可以帮助重构JavaScript这个语言。
包括强制控制函数上下文,离散化函数,重写函数的行为。

然后我们又深入的探讨了即时函数,正如我们所学,即时函数重新定义了如何应用这们语言。

最终,对闭包的深入理解,会带来很多好处,尤其在我们开发JavaScript复杂应用的时候。
并且利用闭包可以解决很多不可避免的公共问题。

在本章的示例代码中,我们介绍了一点prototypes的概念,接下来的一章让我们来认真的研究一下prototypes。

(转载本文章请注明作者和出处 Yann (yannhe.com),请勿用于任何商业用途)

你可能感兴趣的:(javascript)