JavaScript忍者秘籍——函数,下

概要:本篇博客主要介绍函数的一些类型以及常见示例

1.匿名函数

使用匿名函数的常见示例:

window.onload = function(){
    assert(true,'power!');
};  //创建一个匿名函数作为事件处理程序,这里无需定义函数名,直接在其位置为其赋值即可;

var ninja = {
    shout: function(){
        assert(true,"Ninja");
    }
}; 
ninja.shout();  //创建一个函数,将其作为ninja的一个方法,使用shout属性去调用该方法;

setTimeout(function(){
    assert(true,"Forever!");
},500);  //将函数作为callback回调传递给setTimeout()函数,以便在定时器到期时进行调用。

2.递归

当函数调用自身,或调用另一个函数,但这个函数的调用树中的某个地方又调用了自己时,递归就发生了。

- 普通命名函数中的递归

  例如:用于检测回文的递归函数。

  回文的定义:

(1)单个和零个字符都是一个回文

(2)如果字符串的第一个字符和最后一个字符相同,并且除了两个字符以外剩余的其他字符也是一个回文的话,我们称原字符串是一个回文

基于该定义的实现如下:

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));
}

再例如:忍者之间也经常需要沟通,通常用自然声作掩护。在这里,我们给忍者赋予发出"啾啾"声的能力,利用不同数量的啾啾声编码不同的消息。代码如下:

function chirp(n){
    return n > 1 ? chirp(n-1) + "-chirp" : "chirp";
}
assert(chirp(3) == "chirp-chirp-chirp","Calling the named function comes naturally.");

声明一个chirp()的函数,该函数通过调用自身函数名进行递归,就像回文的例子一样。

- 方法中的递归

  声明一个递归函数并将其作为ninja对象的方法来完成这个任务。

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.");

在上述代码中,我们将递归函数定义成一个匿名函数,并将其引用到ninja对象的chirp属性。在该函数内,我们通过对象的ninja.chirp()属性递归调用了函数自身。但这么做会导致引用的丢失问题,解决办法就是使用函数上下文进行引用,示例如下:

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

- 内联命名函数

  当一个匿名函数取名之后就称为了内联函数,这么做事为了解决不同对象的属性都引用匿名函数的问题。

例如:

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.");

  上述代码中,我们给内联函数取了一个名字signal,并在函数体内使用该名称进行递归引用,然后验证了,作为ninja对象的方法进行调用时执行正常。之后将该函数的引用复制给samurai.chirp,并清空原始ninja对象。将其作为samurai的方法进行调用,我们发现一切代码都正常运行,因为清除ninja对象的chirp属性时,没有影响给内联函数所取的用于递归调用的名字。

3.将函数视为对象

- 函数存储

  有时候,我们可能需要存储一组相关但又独立的函数,事件回调管理是最明显的例子。我们可以利用函数的属性特性,给函数添加一个附加属性从而实现上述目的:

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.");

  在上述代码中,我们创建一个对象并赋值给store变量,我们将在该对象里存储一组独立的函数。该对象有两个数据属性:一个用于存储下一个可用的id值,另外一个cache用于存储函数。函数是通过add()方法添加到cache中的。

  在add()中,我们首先要检查要添加的函数是否有一个id属性,如果有,则表示函数已经被处理过,那就忽略它。如果没有,我们就给函数分配一个id属性。

  然后,通过将函数转换为等效的布尔函数,我们返回了true,以便知道在调用add()之后,函数是否成功添加进去。

  提示:!!构造是一个可以将任意JavaScript表达式转化为其等效布尔值的简单方式。例如,!!"He shot me down" ===true和!!0===false。

  另一个有用的技巧是,通过暴露函数属性,我们可以对函数自身进行修改。该技巧可以用于记住以前计算的值,以便在未来计算时节约时间。

- 自记忆函数

  缓存记忆是构建函数的过程,这种函数能够记住先前计算的结果。我们先通过保存昂贵计算的结果来了解一下这种技术,然后再看一个更实际的例子,在列表里存储已经遍历过的DOM元素。

  缓存记忆昂贵的计算结果

function isPrime(value){
    if(!isPrime.answers) 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!");

  在isPrime()函数里,首先检测用于缓存结果的answers属性是否存在,如果不存在,就创建它。初始空对象的创建只在函数第一次调用的时候进行。之后缓存就已经存在了。然后,检查缓存answers里是否已经存在参数值对应的结果,在该缓存里,我们使用参数值作为属性的key,使用布尔值作为属性的值进行保存,如果发现缓存结果,则直接返回它。如果没有找到缓存值,则继续判断该值是否是素数,然后将结果存储在缓存中,并返回它。

  缓存记忆主要有两个优点:

  ■ 在函数调用获取之前计算结果的时候,最终用户享有性能优势。

  ■ 发生在幕后,完全无缝,最终用户和页面开发人员都无需任何特殊操作或为此做额外的初始化工作。

  但它也有缺点:

  ■ 为了提高性能,任何类型的缓存肯定会牺牲掉内存。

  ■ 纯粹主义者可能认为缓存这个问题不应该与业务逻辑放在一起,一个函数或方法应该只做一件事,并把它做好。

  ■ 很难测试或测量一个算法的性能,就像本例一样。

  缓存记忆DOM元素

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

  将状态和缓存信息存储在一个封装的独立位置上,不仅在代码组织上有好处,而且外部存储或缓存对象无需污染作用域就可以获得性能提升。

-伪造数组方法

  有时,我们可能想创建一个包含一组数据的对象。如果只是集合,则只需要创建一个数组即可。但在某些情况下,除了集合本身,可能会有更多的状态需要保存——比如与集合项有关的一些元数据。一种选择可能是,每次创建对象新版本的时候都创建一个新数组,然后将元数据作为属性或方法添加到这个新数组上。例如:

<input id = "first"/>
<input id = "second"/>

<script>
    var elems = {
        length: 0 ,
        add: function(elem){
            Array.prototype.push.call(this,elem);
        },
        gather: function(id){
            this.add(document.getElementById(id));
        },
    };

    elems.gather('first');
    assert(elems.length == 1 && elems[0].nodeType,
        "Verify that we have an element in our stash");
    elems.gather('second');
    assert(elems.length == 2 && elems[1].nodeType,
        "Verify the other insertion");
</script>

  在本例中,我们创建了一个"普通"对象,并添加了一些模拟数组的行为。首先,定义了一个length属性,像数组一样,记录所保存元素的个数。然后,再定义一个方法用于将元素添加到模拟数组的结尾,只需调用add()方法即可。利用原生的JavaScript数组方法:Array.prototype.push。

4.可变长度的参数列表

- 使用apply()支持可变参数

function(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.");

  在上述代码中,我们定义了两个函数:一是查找数组中的最小值,另外一个是查找数组中的最大值。调用smallest(),传入数组[0,1,2,3],其调用Math.min()的结果等价于如下代码:Math.min(0,1,2,3)。

- 函数重载

  所有函数都隐式传递了内置arguments参数,这使函数有能力处理任意数量的参数。即使我们只定义固定数量的形参,通过arguments参数我们还是总是能够访问到传递给函数的所有参数。我们大概看一下如何实现函数重载。

  检测并遍历参数

  JavaScript重载函数时通过传入参数的特性和个数进行相应修改来达到目的。例如,遍历可变长度的参数列表:

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(merged.city == "Niihama",
    "And the city has been copied over.");

  尽管merge()函数的签名里只声明了一个root参数,但是我们在调用的时候可以传入任意数量的参数也可以什么都不传。不过只能用root这个参数作为传入参数的第一个。

  提醒:要检查对应于已命名的形参的参数是否传入,可以使用表达式:paramname === undefined,如果没有对应的参数,则会返回true。

  记住,我们要做的事情,是想第二个甚至第n个参数上的属性合并到传入的root对象中,所以在遍历列表中的参数时,为了跳过第一个参数,索引要从1开始。

  每次遍历时,被遍历项目就是传递给函数的一个对象,然后遍历传入对象的所有属性,并将这些属性复制到root对象上。

  通过root可以获取第一个参数,而arguments参数指向的是所有传入参数的集合。

- 对arguments列表进行切片(slice)和取舍(dice)

  构建一个函数,将第一个参数与剩余参数的最大值进行相乘。首先获取第一个参数,然后将其与剩余参数上调用Math.max()函数的结果进行相乘。由于给Math.max()传递的参数要从数组的第二个元素开始,所以我们使用数组的slice()方法重新创建一个省略了第一个元素的新数组。

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.)")

  arguments参数引用的不是真正的数组。尽管它看起来,且感觉上很像——例如,我们可以使用for循环进行遍历——但其缺乏基本数组应该有的所有方法,包括非常方便的slice()。我们可以创建自己的切片和取舍方法——参数伪装。

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.)");