整体看,javascript非常灵活,正是它的灵活性构成了它今天的样子。其中一个灵活性就是函数可以接收任意长度的参数表。本节中我们将看到如下几个例子:
- 如何给变参函数提供参数
- 如果实现函数重载
- 如何使用和理解 length 属性
下面我们先看看如何使用 apply() 来处理变参函数的问题。
任何语言的设计者总会忽略一些非常基本的功能函数,js也是如此。比较典型的就是寻找数组中的min和max元素。其中功能最接近的就是Math的min和max函数,但是他们是变参的,只能这样调用
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(1,2,3,4,5,6,7,8,9,10,2058);
如果我们现在要处理数组,必须这样写:
var biggest = Math.max(list[0],list[1],list[2]);
这就要求我们必需提前知道数组大小,否则用起来会很麻烦,似乎只有通过循环来解决。不过本着一种忍者精神,我们可以想一想,是否有一种更简单地方法,可以把array作为可变长参数传入呢?
有!那就是 apply() 函数。
回想一下,任何函数都有call和apply方法,包括js的内建函数(我们在“模仿数组”的例子中已经见识了)。下面我们看看如何使用这个能力来实现适于数组的min/max函数。
Listing 4.11 Generic min() and max() functions for arrays
function smallest(array){ //#1 定义寻找最小
return Math.min.apply(Math, array); //#1 数组元素的函数
}
function largest(array){ //#2 寻找数组最大
return Math.max.apply(Math, array); //#2 元素
}
assert(smallest([0, 1, 2, 3]) == 0, //#3 测试
"Located the smallest value."); //#3
assert(largest([0, 1, 2, 3]) == 3, //#3
"Located the largest value."); //#3
就是这么简单。注意我们把Math设为了函数上下文,其实这里函数上下文可以为任意对象,设成Math只是显得更清晰明了。
在3.3节我们介绍了 arguments 参数,它会隐式的传入任何函数中。下面我们更加“近距离”的观察一下它。
因为它包含了所有传入的参数,所以即使我们只指定了一定数目的参数,我们依然可以获得所有参数值。通过它,我们可以方便的实现函数重载。
js的函数重载就定义一个函数,通过监控参数的数目和内容实现不同功能。
下面的代码中,我们将把多个object的属性融合到一个root对象,这在继承中非常有用(我们将在第六章对象原型中详细讨论)
Listing 4.12 Traversing variable-length argument lists
function merge(root){ //#1 函数实现
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
root[key] = arguments[i][key];
}
}
return root;
}
var merged = merge( //#2 调用merge
{name: "Batou"}, //#2
{city: "Niihama"}); //#2
assert(merged.name == "Batou", //#3 测试
"The original name is intact."); //#3
assert(merged.city == "Niihama", //#3
"And the city has been copied over."); //#3
其中merge只定义了一个参数,不过我们传入的参数个数则是没有限制的,即使不传参数也可以。函数是否可以正确处理这些参数完全取决于函数的定义本身。
我们只定义了root
参数,说明我们只需要给第一个参数名字,其他参数没有名字。
TIP: 通过判断
paramname === undefined
可以知道该参数是否传入了。
我们通过从索引1往后遍历arguments的方法来获取其他参数。对于每个对象,我们遍历对象属性,并把属性赋值给root。
TIP:
for-in
会遍历所有对象的属性,并把属性名作为字符串赋值给key
这个功能是不是很灵活?事实上很多库都大量使用了这个技巧实现函数重载,比如jQuery UI,你可以通过dialog创建或操作对话框:
创建对话框如下:
$("#myDialog").dialog({ caption: "This is a dialog" })
操作对话框如下,这个命令就打开了对话框:
$("#myDialog").dialog("open");
dialog函数就是通过检测参数的类型,实现了不同功能。
下面我们再看一个稍微复杂的例子。
下面的例子要把第一个参数和后面参数中最大的数相乘,返回乘积结果。尽管实际中这种需求很少,但是对于学习arguments的更多操作技巧有帮助。
初看起来,这个很容易实现,我们的代码可能如下:
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.)");
通过slice(1)获得后面的所有参数,然后通过max.apply获得其中的最大元素。但是当我们执行这段代码的时候,我们却得到了一个异常:
Uncaught TypeError: arguments.slice is not a function(…)
像我们之前指出的,arguments并不是一个真正的array对象,尽管它看起来很像:我们可以通过for循环遍历它,可以访问length属性,但是它却没有array对象的方法。
我们可以手动创建一个array对象,或者为arguments设计一个slice方法,不过最好是继续使用Array的slice方法,我们可以回想4.10例子中方法,如下:
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)); //#1 使用call把arguments当做Array对待
}
assert(multiMax(3, 1, 2, 3) == 9,
"3*3=9 (First arg, by largest.)");
通过上面的学习,我们对arguments有了更深了解。下面我们就来看看几个实现函数重载的技巧。
通过判断参数的数量和种类实现重载一般通过if-else分支实现。对于简单函数这比较方便,不过一旦函数复杂了,这种方案就非常臃肿。本节剩下的部分,我们会介绍一种技巧,利用它我们可以创建很多同名但是参数个数不同的函数,可以把他们写成独立的匿名函数而不是冗长的if-then-else-if块。
该技巧的核心,同时也是我们将首先学习的,是函数的一个几乎无人知道的属性,length。
函数有一个不为人知的属性,叫length,等于函数声明时使用的形参个数。注意它和arguments的length不同。如下所示:
function makeNinja(name){}
function makeSamurai(name, rank){}
assert(makeNinja.length == 1, "Only expecting a single argument");
assert(makeSamurai.length == 2, "Two arguments expected");
所以,给定一个函数,我们可以通过这2个length获知2个信息:
- 通过函数的length属性,我们能知道形参有几个
- 通过arguments的length属性,我们能知道实际有多少参数传入
如上所述,我们可以通过判断arguments的个数实现函数重载:
var ninja = {
whatever: function() {
switch (arguments.length) {
case 0:
/* do something */
break;
case 1:
/* do something else */
break;
case 2:
/* do yet something else */
break;
//and so on ...
}
}
}
这种方法是不是看起来不像“忍者”作风?下面我们来探索一下其他方案,比如我们可以这样创建函数:
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的实现如下:
Listing 4.15 A method-overloading function
function addMethod(object, name, fn) {
var old = object[name]; //#1 保存旧函数,如何没有合适的用old
object[name] = function(){
if (fn.length == arguments.length) //#2 判断形参和实参
return fn.apply(this, arguments) //#2 个数是否一致
else if (typeof old == 'function') //#3 如果没有匹配的
return old.apply(this, arguments); //#3 使用old
};
}
下面再来看看我们的例子:
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时,它先创建了一个匿名函数,并赋值给whatever属性,当无参数调用whatever时,会调用fn。注意,每次调用addMethod都会创建一个闭包,其中fn和old就是闭包中的2个变量(我们将在下一章详细讲述闭包)。
第二次调用addMethod时,它会先保存上一个已经创建的函数old,然后再创建一个匿名函数赋值给whatever,当调用whatever时,如果形参个数为1,就会调用此时的fn,如果不是,则会尝试old,即第一次创建的匿名函数。
第三次调用addMethod和第二次类似,如果形参个数为2,则会调用此时的fn,否则会执行第二次创建的匿名函数old。在old中,如果参数个数不是1,则尝试在第一次中生成的匿名函数。
整个过程就像剥洋葱,先检查最外层的函数是不是符合要求,否则就一层层向里检查。其实和if-else逻辑很像。
下面我们就来测试一下我们的addMethod
函数
Listing 4.16 Testing the addMethod() function
var ninjas = { //#1 测试对象
values: ["Dean Edwards", "Sam Stephenson", "Alex Russell"]
};
addMethod(ninjas, "find", function(){ //#2 增加无参数函数
return this.values;
});
addMethod(ninjas, "find", function(name){ //#3 接受一个参数的函数
var ret = [];
for (var i = 0; i < this.values.length; i++)
if (this.values[i].indexOf(name) == 0)
ret.push(this.values[i]);
return ret;
});
addMethod(ninjas, "find", function(first, last){ //#4 接受2个参数的函数
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, //#5 测试
"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", "Russell", "Jr") == null,
"Found nothing");
这个方案非常漂亮,我们并没有把这些匿名函数保存在典型数据结构中,它们只是闭包中的引用。当然它也有一些不足
- 只能区分参数个数,不能区分参数种类、名字等。而我们经常需要这种区分。
- 函数调用的性能有些损失。在追求高性能的应用中可能不适合。
这一节讲了一等公民“函数”的很多功能,下面我们讲讲如何分辨函数。