1.What's is 函数重载?
Function or method overloading is declaring functions with the same name that accept different arguments thus have different behaviors depending on passed arguments.
函数重载(方法重载)是指声明一组具有不同数量或者不同类型的参数的同名函数,这些同名函数会根据参数的不同表现出不同的行为。(翻译的竟然词不达意,罪过罪过)
2.静态语言的重载
像Java、C++这样的静态语言具有天然的函数重载特性,以C++为例,你一定理解下面的代码。
#include <iostream>
using namespace std;
void print(int i) {
cout <<"Here is num " << i << endl;
}
void print(string str) {
cout << "Here is string " <<str<< endl;
}
int main() {
print(10);//Here is int 10
print("ten");//Here is string ten
}
可以发现在C++中会根据参数的类型自动选择合适的函数执行,如果把上面的代码改造成Javascript代码如下:
function print(i) {
console.log("Here is num"+i);
}
function print(str) {
console.log("Here is string "+str);
}
print(100);//Here is string 100
print("ten");//Here is string ten
显然,这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。那Javascript中到底有没有函数重载特性,
正确答案是:绝逼没有,但是可以模拟重载的行为
在《JS高程》中有这样一段总结:Javascript函数不能像传统意义上那样实现重载。而在其他语言(如 Java、C++)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。但是Javascript函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。如果在 Javascript中定义了两个名字相同的函数,则该名字只属于后定义的函数。
也就是说:虽然Javascript不能实现真正的函数重载特性,但是通过检查传入函数中的参数的类型和数量并作出不同的反应,也能模仿方法的重载(想想前面的关于函数重载的定义,重载的结果是同名函数会根据参数的不同表现出不同的行为,既然不能变成真正的“鸭子”,但是能模仿“鸭子”叫,我们就姑且把它看作是“鸭子”了)。
3.如何实现具有Javascript特色的函数重载
Stack Overflow上有人总结了一下几种方法:
- Using different names in the first place(使用不同的函数名)
function printNum(num) {
console.log("Here is num "+num);
}
function printString(str) {
console.log("Here is string "+str);
}
printNum(100);//Here is num 100
printString("ten");//Here is string ten
上述这种做法实际上已经违背了函数重载的基本特性,因为已经出现了不同的函数名。
- Using optional arguments like
y = y || 'default'(检测是否提供实参,否则采用默认值)
举例说明,现在我们要同时打印一个数字和一个字符串
function print(num,str){
var inner_num=num||0;
var inner_str=str||"default";
console.log("Here is num "+inner_num);
console.log("Here is string "+inner_str);
}
print();
//Here is num 0
//Here is string default
print(10);
//Here is num 10
//Here is string default
print(10,"ten");
//Here is num 10
//Here is string ten
使用这种方法的结果也是显而易见的,实现起来也是相当简单,在Javascript程序中被大量采用,但是执行print( ,"ten");这样的操作会发生错误,在某些情况下如果0/null/undefined为有效值时,执行y = y || 'default'会不分青红皂白的把这些值过滤掉。
- Using number of arguments(根据参数数量来实现重载)
function print(num,str){
var len=arguments.length;
if (len===1) {
console.log("Here is num "+num);
}
else if (len===2) {
console.log("Here is num "+num);
console.log("Here is string "+str);
}
else{
console.log("看....,飞碟!");
}
}
print();//看....,飞碟!
print(10);//Here is num 10
print(10,"ten");
//Here is num 10
//Here is string ten
从上面的结果看,如果出入的参数只有一个字符串,仍然无法打印。
- Checking types of arguments (根据参数类型来实现重载)
function print(num,str){
if (typeof num=="number") {
console.log("Here is num "+num);
}
if (typeof str=="string") {
console.log("Here is string "+str);
}
}
print();//无输出
print(10);//Here is num 10
print('',"ten");//Here is string ten
print(10,"ten");
// Here is num 10
// Here is string ten
这种方法的结果算是比较满足函数重载的行为了,但是有没有发现如果只想打印字符串时,也不得不传入两个参数,并且要求开发人员知道函数内部能过滤掉第一个参数,同时检测参数数量和参数类型可以解决这个问题。
下面的例子中直接借用了arguments数组
function print(num,str){
var len=arguments.length;
if((len===1)&& (typeof arguments[0]==="number")){
console.log("Here is num "+arguments[0]);
}
else if ((len===1)&& (typeof arguments[0]==="string")) {
console.log("Here is string "+arguments[0]);
}
else if((len===2)&& (typeof arguments[0]==="number")&& (typeof arguments[1]==="string")) {
console.log("Here is num "+arguments[0]);
console.log("Here is string "+arguments[1]);
}
else{
console.log("看....,飞碟!");
}
}
print();//看....,飞碟!
print(10);//Here is num 10
print("ten");//Here is string ten
print(10,"ten");
// Here is num 10
// Here is string ten
在预期参数较多的情况下,上述的所有方法可能都不尽如人意,检测参数类型会拖慢执行速度,并且并且需要检测的类型过多, Arrays, nulls, Objects, etc.而且如果有几个参数的类型相同呢,那岂不是容易混淆,这时Javascript中有一种代码风格被广泛使用:使用对象作为函数的最后一个参数,对象可以承载任何类型的键值对。
function foo(a, b, opts) {
}
foo(1, 2, {"method":"add"});
foo(3, 4, {"test":"equals", "bar":"tree"});
在代码中只需要检测对象的某个键值是否存在来做相应处理。
至此,前面模拟函数重载的方法的基本思想都是基于传递的参数定义一个有很多不同功能的函数,通过使用if-then或者switch子句处理不同的行为,但是一旦事情开始变得的复杂,大量的分支语句就会导致代码笨拙,John Resig在《Secrets of the JavaScript Ninja》(中译名:JavaScript忍者秘籍)中提到一种基于函数的length属性的重载方法
ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 。其中,length 属性表示函数希望接收的命名参数的个数,如下面的例子所示(见《Js高程P116页》)。
function sayName(name){
alert(name);
}
function sum(num1, num2){
return num1 + num2;
}
function sayHi(){
alert("hi");
}
alert(sayName.length); //1
alert(sum.length); //2
alert(sayHi.length); //0
因此,对于一个函数,在参数方面,我们可以确定两件事
■ 通过其length属性,可以知道声明了多少命名参数
■ 通过arguments.length,可以知道在调用时传入了多少参数
下面介绍一下大牛是如何实现借助这个特性实现函数的重载的
■ 通过其length属性,可以知道声明了多少命名参数
■ 通过arguments.length,可以知道在调用时传入了多少参数
下面介绍一下大牛是如何实现借助这个特性实现函数的重载的
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 */ });
在这里,先创建一个对象,然后使用同样的名称(whatever)将方法添加到该对象上,只不过每个重载的函数都是单独的,注意每个重载的参数个数都不相同。通过这种方式,真正为每个重载都创建了一个独立的匿名函数。漂亮且整洁!现在的关键就是实现addMethod()函数,大牛的实现相当巧妙:
function addMethod(object, name, fn) {
var old = object[name]; //保存原有的函数,因为调用的时候可能不匹配传入的参数个数
object[name] = function() { // 重写了object[name]的方法,是一个匿名函数
// 如果该匿名函数的形参个数和实参个数匹配,就调用该函数
if(fn.length === arguments.length) {
return fn.apply(this, arguments);
// 如果传入的参数不匹配,就调用原有的参数
} else if(typeof old === "function") {
return old.apply(this, arguments);
}
}
}
现在,我们一起来分析一个这个addMethod函数,它接收3个参数:
- 第一个为要绑定方法的对象,
- 第二个为绑定方法所用的属性名称,
- 第三个为需要绑定的方法(一个匿名函数)
好吧,一言不合就举个例子吧
//addMethod
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);
}
}
}
var printer= {};
// 不传参数时,打印“飞碟”
addMethod(printer, "print", function() {
console.log("看,飞碟!");
});
// 传一个参数时,打印数字
addMethod(printer, "print", function(num) {
console.log("Here is num "+num);
});
// 传两个参数时,打印数字和字符串
addMethod(printer, "print", function(num,str) {
console.log("Here is num "+ num);
console.log("Here is string "+str);
});
// 测试:
printer.print(); //看,飞碟!
printer.print(10); //Here is num 10
printer.print(10, "ten");
//Here is num 10
//Here is string ten
对上面的思想稍微改造一下,我们可以直接把重载功能放在函数对象原型上。
// Function.prototype.overload - By 沐浴星光
Function.prototype.overload = function(fn) {
var old= this;
return function() {
if (fn.length===arguments.length) {
return fn.apply(this,arguments);
}else{
return old.apply(this,arguments);
}
}
}
// 不传参数时,打印“飞碟”
function printNone() {
console.log("看,飞碟!");
}
// 传一个参数时,打印数字
function printInt(num) {
console.log("Here is num "+num);
}
// 传两个参数时,打印数字和字符串
function printBoth(num,str) {
console.log("Here is num "+ num);
console.log("Here is string "+str);
}
print=function(){};
print=print.overload(printNone).overload(printInt).overload(printBoth);
print();//看,飞碟!
print(10);//Here is num 10
print(10,"ten");
// Here is num 10
// Here is string ten
参考:
《Javascript高级程序设计-第三版》
《JavaScript忍者秘籍》
扩展阅读:
JavaScript function overloading | Stamat
Function Overloading in JavaScript | Chris West's Blog