第四章.挥舞函数(4.Wielding functions)[完成]

翻译 Secrets of the JavaScript Ninja (JavaScript忍者禁术)

第四章.挥舞函数(4.Wielding functions)

 本章重点:

 1.为什么匿名函数如此重要

 2.函数中的递归

 3.函数可以被引用后再调用

 4.如何为函数缓存索引

 5.利用函数的能力来实现记忆

 6.利用函数上下文

 7.处理参数长度

 8.判断一个对象是否为函数

在上一章我们了解到函数作为自然类型的对象(first-order objects),并且了解到什么是函数式编程。在本章,我们会利用函数来解决一些问题,也许以后做web开发时候可以用到。

我们展示的例子并不会直接解决你开发中的问题,那岂不成了另一个什么指南之类的书了。

我们知道,本书的本质目的是让你能够真正的了解这门语言的精髓。

 4.1 匿名函数(Anonymous functions)

不知道你是否已经熟悉了匿名函数,匿名函数的确是一个十分重要的概念需要我们来理解,如果你还在为JavaScript的忍者头巾而奋斗。他们是是否重要的特点,并且是一个函数化语言的灵魂,例如Scheme

匿名函数通常会被用于后续使用,例如存储一个变量,作为一个对象的方法,用于回调函数(例如timeout或者事件处理)

Listing 4.1: Common examples of using anonymous functions  
window.onload= function(){ assert(true, 'power!');};  
var ninja = {  
    shout: function(){  
        assert(true, "Ninja");  
    }  
}  
ninja.shout();  

setTimeout(function(){ assert(true, 'Forever!')}, 500)

我们将会在本书的后面看到大量的匿名函数,因为是否能将JavaScript使用的很有力量,取决于你是否将它作为函数式语言在使用。所以我们将会在后面加入很重的函数式的代码。

函数式编程专注于:小,每段小代码只做一个事。

 4.2 递归(Recursion)

递归是是否有用的技术。你可能以为递归只是用于数学计算。很多情况下是这样的。

但是他同样适合于其他事情,例如遍历一个树,包括DOM本身,我们在web开发中经常遇到。

所以递归是个很有用的概念,通过它我们可以更加深刻的理解函数如何在JavaScript中作用。

让我们开始通过最简单的方式来看看递归

 4.2.1 递归在普通函数中(Recursion in named functions)

我们来写一个例子,这个例子是判断一个字符串是否为对称

我们的实现如下:

function isPalindrome(text){  
    if (text.length <= 1) return true;  
    if (text.charAt(0) != text.charAt(text.length - 1)) return false;  
    return isPalindrome(text.substr(1, text.length - 2));  
}  


Listing 4.2: Chirping using a named function  
funcction chirp(n){  
    return n > 1? chirp(n -1) + "-chirp": "chirp";  
}  

assert(chirp(3) == "chirp-chirp-chirp", "Calling the named function comes naturally.")  

上面的例子都是在用实名函数在实现递归,让我们下面看看如何利用匿名函数实现递归。

 4.2.2 递归在对象的方法中(Recursion with object methods)

我们对上面的例子做个改造,让一个匿名函数赋予在一个对象的属性上。

Listing 4.3: Method recursion within an object

var ninja={

chirp: function(n){

return n > 1 ? ninja.chirp(n - 1) + “-chirp” : “chirp”;

}

}

assert(ninja.chirp(3) == “chirp-chirp-chirp”, “An object property isn’t too confusing, either.”)

 4.2.3 引用的丢失问题(The pilfered reference problem)

我们对上个例子继续改造一下,我们增加一个新的对象,samurai,让它继续引用ninja对象的匿名函数。

Listing 4.4 Recursion using a missing function reference  
var ninja = {  
    chirp: function(n){  
        return n > 1? ninja.chirp(n - 1) + "-chirp" : "chirp";  
    }  
}  

var samurai = { chirp : ninja.chirp};  
ninja = {};  
try{  
    assert(samurai.chirp(3) == "chirp-chirp-chirp", "Is this going to work?");  
}  
catch(e){  
    assert(false, "Uh, this isn't good! Where'd ninja.chirp go?");  
}  

这里的引用关系是,samurai和ninja同事引用了一个函数,如图4.2:

这并不是问题所在,问题在于这个共同的函数引用了ninja自身,他并不在乎是谁调用的他。

我们可以修复这个问题,相比于通过在匿名函数中引用ninja对象,不如通过函数上下文(this)来搞定这个问题:

var ninja = {  
    chirp: funciton(n){  
        return n > 1 ? this.chirp(n - 1) + "-chirp" : "chirp";  
    }  
}  

还记得函数作为方法被调用的时候函数上下文就是对象本身吗?

所以当samurai.chirp()调用的时候,this就指向了samurai。

 4.2.4 内部实名函数(Inline named functions)

Listing 4.5: Using a inline function in a recursive fashion  
var ninja = {  
    chirp: function signal(n){  
        return n > 1 ? signal(n - 1) + "-chirp": "chirp";  
    }  
}  
assert(ninja.chirp(3) == "chirp-chirp-chirp", "Works as we would expect it to!");  

var samurai = {chirp: ninja.chirp};  
ninja = {};  

assert(samurai.chirp(3) == "chirp-chirp-chirp", "The method correctly calls itself.");  

Listing 4.6: Verifying the identity of an inline function  
var ninja=function myNinja(){  
    assert(ninja == myNinja,"This function is named two things at once!");  
}  

ninja();  

assert(typeof myNinja == "undefined", "But myNinja isn't defined outside of the function.")  

 4.2.5 callee属性(The callee property)

我们来看另外的一个函数的概念:arguments参数的属性callee

Listing 4.7: Using arguments.callee to reference the calling function  

var ninja={  
    chirp: function(n){  
        return n>1? arguments.callee(n-1) + "-chirp" : "chirp";  
    }  
}  

assert(ninja.chirp(3) == "chirp-chirp-chirp", "arguments.callee is the function itself.")  

arguments有一个属性叫做callee,这个属性指向的是当前执行的函数。这个属性可以一直用于在函数内部获取到函数自身。

 4.3 函数作为对象(Fun with function as objects)

在JavaScript中的函数并不像其他语言中的函数,JavaScript给予了函数很多的能力,不只是被当作自然对象(first-class objects)

我们已经看到,函数可以拥有属性,可以拥有方法,可以被赋到变量和属性上,但是最牛的一个能力是他们可以被调用(callable)

首先让我们看一下将函数缓存在一个集合中,然后要研究“memoizing“这个技术。

 4.3.1 存储函数(Storing functions)

很多时候,我们会想要存储一些具有唯一标识的函数。

当我们想添加一个函数到一个集合中的时候,我们需要判断这个函数之前是否已经被添加到这个集合中了。

之前我们用的普通的方法是将函数添加到一个数组里,每次添加新的函数的时候都要遍历一次数组进行比较,我们下面要做得更好一些。

我们可以利用函数自身的属性来达到这个目的。

Listing 4.8: Storing a collection of unique functions  
var store = {  
    nextId: 1,  
    cache: {},  
    add: function(fn){  
        if (!fn.id){  
            fn.id = store.nextId++;  
            return !!(store.cache[fn.id] = fn);  
        }  
    }  
}  

function ninja(){};  

assert(store.add(ninja), "Function was safely added.");  
assert(!store.add(ninja), "But it was only added once.");  

这里要注意:!!是一个很简单的方式,让任意JavaScript表达式变成Boolean的方式,

例如:!!“hello” === true and !!0 === false

 4.3.2 自身缓存函数(Self-memoizing functions)

Memoization是让函数具有记忆能力的技术,可以让函数记住上一次计算的结果。他可以提升效率。

 MEMOIZING EXPENSIVE COMPUTATIONS

如果是负责计算,这里的情况很适合。我们会构建一个"answer cache" ,它会存储上一次运行的结果。

Listing 4.9: Meemoizing previously-compued values  
function isPrime(value){  
    if (!isPrime.anwers) isPrime.answers={};  
    if (isPrime.answers[value] != null){  
        return isPrime.answers[value];  
    }  
    var prime = value != 1; // 1 can never be prime  
    for (var i = 2; i < value; i++){  
        if (value % i == 0){  
            prime = false;  
            break;  
        }  
    }  
    return isPrime.answers[value] = prime;  
}  

assert(isPrime(5), "5 is prime!");  
assert(isPrime.answers[5], "The answer was cached!");  

 MEMOIZING DOM ELEMENTS

function getElements(name){  
    if (!getElements.cache) getElements.cache = {};  
    return getElements.cache[name] =  
        getElements.cache[name] || document.getElementsByTagName(name);  
}  

 4.3.3 Faking array methods

我们来构造自定义的一个类似Array的对象

Listing 4.10: Simulating array-like methods  
<input id="first"/>  
<input id="second"/>  
var elems = {  
    length: 0,  
    add: function(elem){  
        Array.prototype.push.call(this, elem);  
    },  
    find: function(id){  
        this.add(document.getElementById(id));  
    }  
}  

elems.find("first");  
assert(elems.length == 1 && elems[0].nodeType, "Verify that we have an element in our stash");  

elems.find("second");  
assert(elems.length == 2 && elems[1].nodeTpe, "Verify the other insertion");  

 4.4 Variable-length argument lists

JavaScript是一门很灵活的语言.其中具体的一个特点是他可以让函数接受任意长度的arguments。

我们下面要看一些列子来如何应用这个特性来增强他们的能力,通过这些例子我们可以学到:

 1.如何支持任意数量的arguments

 2.如何利用variable-length argument列表来实现函数的重载

 3.如何认识和使用argument列表的length属性

让我们来利用apply()来搞定这些

 4.4.1 Using apply() to supply variable arguments

如果我们求一个集合的最大值,我们会想到Math

例如:

var biggest = Math.max(1,2);

var biggest = Math.max(1,2,3);

var biggest = Math.max(1,2,3,4);

但是,我们不能这么做:

var biggest = Math.max(listp[0], list[1], list[2]);

如果搞定这个问题?通过apply()或者call()

Listing 4.11: Generic min and max funcctions for arrays  
function smallest(array){  
    return Math.min.apply(Math, array);  
}  

function largest(array){  
    return Math.max.apply(Math, array);  
}  

assert(smallest([0,1,2,3]) == 0, "Located the smallest value.");  
assert(largest([0,1,2,3]) == 3, "Located the largest value.");  

这里通过apply将数组的参数转换成了正常参数:

Math.min(0,1,2,3);

 4.4.2 Function overloading

在3.2章节,我们知道argments参数是隐性的传递给被调用的函数的,现在让我们详细的看一下这它。

所有函数都接受到这个隐性的参数,它给了我们力量来处理未知数目的参数。

让我们来看看如何通过它来实现函数的重载(function overloading)

 DETECTING AND TRAVERSING ARGUMENTS

在大部分面向对象的语言中,重载一般是通过定义同样的名字的函数,通过定义不同的参数来实现区别。但是在JavaScript不是这样,在JavaScript中我们可以只用一个函数,在函数内部通过判断参数的个数来实现逻辑划分。

在下面的例子中,我们要merge两个对象的属性到一个root对象中。

Listing 4.12: Traversing variable-length argument lists  
function merge(root){  
    for (var i = 1; i < arguments.length; i++){  
        for (var key in arguments[i]){  
            root[key] = arguments[i][key];  
        }  
    }  
    return root;  
}  

var merged = merge(  
    {name: "Batou"},  
    {city: "Niihama"});  

assert(merged.name == "Batou", "The original name is intact.");  
assert(mmerged.city == "Niihama", "And the city has been copied over.");  

 SLICING AND DICING AN ARGUMENTS LIST

我们要利用arrays的slice()方法来忽略arguments的第一个参数

让我们来看看例4.13

Listing 4.13: Slicing the arguments list  
function multiMax(multi){  
    return multi*Math.max.apply(Math, arguments.slice(1));  
}  

assert(multiMax(3,1,2,3) == 9, "3*3 =9(First arg, by largest.");  

运行之后报错,因为argument不是Array,所以他没有slice方法。

让每重写一下这段代码

Listing 4.14: Slicing the arguments list - successfully this time  
function multiMax(multi){  
    return multi * Math.max.apply(Math,  
        Array.prototype.slice.call(arguments, 1));  
}  

assert(multiMax(3,1,2,3) == 9, "3*3=9 (First arg, by largest.");  

我们利用Array的slice()方法,让arguments看起来也是一个array,尽管它自身不是。

 FUNCTION OVEERLOADING APPROACHES

普通的做法是根据参数的不同在函数内部写if-then-else-if代码块,但是这样又太不简洁。

我们可以利用一个不常用的函数的属性来实现代码的简洁化。

 THE FUNCTION’S LENGTH PROPERTY

函数中有一个我们很少知道的属性,但是它能告诉我们这个函数是如何被定义的,它就是length属性。

请不要把这个属性和arguments的length属性混淆,它是专指,被显性定义的函数的参数的个数。

因此,如果我们只是定义了一个入参的函数,那么这个属性值就是1。

代码示例如下:

function makeNinja(name){}

function makeSamurai(name, rank){}

assert(makeNinja.length == 1, “Only expecting a single argument”);

assert(makeSamurai.length == 2, “Two arguments expected”);

所以针对一个函数,我们可以断定2个事情:

 1.通过函数length属性,我们可以知道他的显性参数的数目

 2.通过arguments的length属性,我们可以知道调用的时候真正传递进去的参数个数。

让我们看看如同运用这个属性来实现函数的重载

 OVERLOADING FUNCTIONS BY ARGUMENT COUNT

这里有很多方式来根据参数实现函数的重载。

一个普遍的做法是根据参数的类型,另一种是根据参数的个数。

让我们看看如何通过参数的个数来实现:

var ninja = {  
    whatever: function(){  
        switch(arguments.length){  
            case 0:  
                /* do something */  
                break;  
            case 1:  
                /* do something else */  
            case 2:  
                /* do yet something else */  
                break;  
            // and so on...   
        }  
    }  
} 

在这段代码中,每个case对应的是argument实际传入的数量。

但是不够简洁,不够忍者。

让我们再写一段代码,如果我们想让重载的逻辑在调用的时候:

var ninja = {}

addMethod(ninja, ‘whatever’, function(){/* do something /});

addMethod(ninja, ‘whatever’, function(a){/
do something else /});

addMethod(ninja, ‘whatever’, function(a,b){/
yet something else */});

通过这种方式,我们在调用的时候才定义重载的逻辑,漂亮并且简洁吧。

但是我们还没有定义addMethod这个函数,下面我们来实现它。

让我们来看例4.15

Listing 4.15 A method overloading function  
function addMethod(object, name, fn){  
    var old = object[name];  
    object[name] = function(){  
        if (fn.length == arguments.length)  
            return fn.apply(this, arguments)  
        else if (typeof old == 'function')  
            return old.apply(this, arguments);  
    }  
}  

我们的addMethod()函数接受了三个参数:

 1.一个对象,作为载体

 2.一个要被绑定的方法名字

 3.要被绑定的方法的声明

让每看例4.16来测试一下我们的新函数:

Listing 4.16 Testing the addMethod function  

var ninjas = {  
    values: ["Dean Edwards", "Sam Stephenson", "Alex Russell"]  
}  

addMethod(ninjas, "find", function(){  
    return this.values;  
})  

addMethod(ninjas, "find", function(name){  
    var ret = [];  
    for (var i = 0; i < this.values.length; i++)  
        if (this.valuespi].indexOf(name) == 0)  
            ret.push(this.values[i]);  
    return ret;  
})  

addMethod(ninjas, "find", function(first, last){  
    var ret = [];  
    for(var i = 0; i < this.values.length; i++)  
        if (this.values[i] == (first + " " + last))  
            ret.push(this.values[i]);  
    return ret;  
})  

assert(ninjas.find().length == 3, "Found all ninjas");  
assert(ninjas.find("Sam").length == 1, "Found ninja by first name");  
assert(ninjas.find("Dean", "Edwards").length == 1, "Found ninja by first and last name");  
assert (ninjas.find("Alex", "X", "Russell") == null, "Found nothing");  

这段代码很整洁,因为我们没有将函数真正存储在一个明显的数据结构中。我们是通过闭包(closures)来实现的,我们会在下一个章节来讨论闭包

本章到此为止,我们学习了一个函数如何被当作一个自然对象来使用,我们还要知道如何判断一个对象是否是一个函数。

 4.5 Checking for functions

大多数浏览器都可以通过typeof来判断一个对象的类型,

例如:

function ninja(){}

assert(typeof ninja == “function”, “Functions have a type of function”);

当然有些浏览器是不支持这么写。这就要设计到浏览器兼容性问题

 4.6 Summary

 1.匿名函数可以让代码更简洁

 2.递归函数让我们知道函数如何被引用:

1)通过名字  
2)通过方法  
3)通过变量  
4)通过arguments的callee属性  

 3.函数拥有一些属性,我们可以利用这些属性来存储信息,包括:

1)存储函数,可以后续来调用和引用  
2)利用函数的属性来实现缓存(memoization)  

 4.通过控制函数上下文,我们可以让一个对象的方法不再为这个对象服务,可以利用Array和Math拥有的方法来为我们所用,来计算我们自己的数据。

 5.函数可以根据参数的不同而执行不同的逻辑(function overloading).

 6.利用typeof关键字我们可以检查一个对象实例是否为函数.这里要考虑浏览器兼容性问题。

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

 
35
Kudos
 
35
Kudos

Now read this

305十年再相聚

2104年十一期间,305相聚在北京,happy的度过了美好的假期。 十年前的305,在长春理工寝室后面的树林子里。 现在的305,在国家大剧院音乐厅。 第1天-胖子驾到,老朱请吃铁板烧 胖子从哈尔滨做火车到达我们的小寝室。胖子还是那么…你懂得~~ 然后都到我家,等老朱,挫进我小猫 老朱姗姗来迟,结果遭到胖子撕咬,还是那么销魂。 俩人进屋就开始吃,还叼着蛋卷当雪茄,吊丝啊。 胖子弹钢琴 我和胖子 到了中午,老朱由于迟到啦,所以请我俩吃铁板烧。吃得猛了点,把老朱心疼坏了。... Continue →