|

JavaScript 中的函数重载和多态的实现

函数重载就是使用相同函数名实现不同的函数功能。

JavaScript 没有完整的有关 的定义,JavaScript 中也没有函数重载的概念,但是 JavaScript 却能实现类似 函数重载 的功能,这里主要是应用 arguments 参数来实现,函数重载是多态特征的表现。本文主要整理和总结 JavaScript 中的 “函数重载”。

函数重载

JavaScript 没有原生的函数重载,但是却能实现类似函数重载的功能,这主要是通过 arguments 属性来实现的;另一种实现方式,则是通过给函数传递一个对象,然后分别判断对象的属性值来实现不同的功能。

使用 arguments 参数实现重载

先看一个经常用到且非常简单的例子:

console.log('a', 'b', 'c', 'd');
// a b c d

我们给 console.log 方法传递了四个字符串参数,发方法打印出了这四个字符串,看起来很简单。那么 console.log 方法的到底需要传递多少个参数?

其实这个是没有限制的,我们调用函数时传递的参数都会保存在变量 arguments 中,这是一个函数内部作用域的变量,每个函数都拥有自己的 arguments 。

function print() {
    let output = "";
    for (let i in arguments) {
        output += arguments[i].toString();
    }
    console.log(output);
}

print("Hi,", "hello ", "world");
// Hi,hello world

如上,虽然 print 函数没有定义明确的参数,但是却可以传递任意的参数,函数内部通过遍历 arguments 对象来获取每一项的参数,拼接以后通过 console.log 打印,这是一个很简单的例子。

基于这个特性,再定义一个简单的函数,用来求解面积:

function getArea (w) {
    if (!arguments.length) {
        return 0;
    }
    // 只有一个参数,即正方形面积
    if (arguments.length === 1) {
        return w * w;
    }
    // 参数大于等于两个时只使用前两个,求解面积
    else {
        return w * arguments[1];
    }
}
// 正方形面积
console.log(getArea(5));    // 25
// 长方形面积
console.log(getArea(5, 3)); // 15

上面定义了一个求解面积的函数,当只传递一个参数时,求正方形面积,当传递两个参数时,求长方形面积。所以 getArea 函数被 “重载” 了。

另一种更像重载的写法是这样的:

function getArea(w, h) {
    if (!w || w <= 0) return 0;
    if (w !== undefined && h === undefined) {
        return w * w;
    }
    if (w !== undefined && h !== undefined) {
        return w * h;
    }
}
// 正方形面积
console.log(getArea(5));    // 25
// 长方形面积
console.log(getArea(5, 3)); // 15

引申:ES6的参数默认值

在ES6中,可以使用参数默认值来代替 undefined ,修改上面的例子如下:

function getArea(w = 0, h = 0) {
    if (w <= 0 || h === undefined) return 0;
    let height = h <= 0 ? w : h;
    return w * height;
}
// 正方形面积
console.log(getArea(5));    // 25
// 长方形面积
console.log(getArea(5, 3)); // 15

使用参数默认值后,没有传递的参数将使用定义时的默认值代替,也能实现类似重载的功能。

以对象为参数实现函数类重载

还是以求解面积为例,另一种方式是使用对象最为函数参数。使用对象作为函数参数的一个好处是可以让参数语义化,这样更好理解。

举个例子,当调用 getArea(5) 时,我们并不知道 5 所代表的含义,因为这个参数除了通过函数定义查看含义之外没有办法语义化。而是用对象参数,函数调用就可以变成下面这样:

function getArea (option = {}) {
    if (!option.width || option.height <= 0) return 0;
    let h = option.height || option.width;
    return option.width * h;
}
// 正方形面积
console.log(getArea( {width: 5} ));             // 25
// 长方形面积
console.log(getArea( {width: 5, height: 3} ));  // 15

使用这种方式将更语义化,即便不去查看函数的定义,也能通过参数猜测其含义。一般在函数参数较多的时候会使用这种方式来传参,以便更好的管理,对于普通的函数,仅需要传递一两个参数的情况,可以不使用这种方式。

函数重写

函数重载和重写是一种多态特性。在其他面向对象的语言中,可以使用重写函数的方式来实现重载,但是在 JavaScript 中,重新函数意味的覆盖,即新的函数会覆盖掉旧的函数。

以输出数组为例:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
console.log(arr.toString());
// 1,2,3,4,5,6,7,8,9,0

这个看似简单的例子,其实就包含了函数的重写,重写的函数为 toString() ,根据原型与原型链的关系,所以对象的父对象都是 Object,在 Object 上其实就有一个 toString() 方法。但是由于每一个类型对转字符串的需求不同,所以其派生对象都会重写 toString 方法,使其适合输出当前类型的数据。

上面的代码中,arr.toString() 就是调用了原型链上 Array.prototype.toString() 方法得到的,所以上面的例子与下面的是等价的:

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
console.log(Array.prototype.toString.call(arr));
// 1,2,3,4,5,6,7,8,9,0

如果我们不希望数组以这种方式输出,而是希望数组输出时可以包含两边的括号,那么就可以重写 Array 原型对象的 toString 方法:

Array.prototype.toString = function () {
    var output = "";
    for (let i = 0; i < this.length; i++) {
        output += this[i] + ',';
    }
    return "[" + output.slice(0, -1) + "]";
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
console.log(arr.toString());
// [1,2,3,4,5,6,7,8,9,0]

使用 Array 原型的 join 方法也可以达到效果:

Array.prototype.toString = function () {
    return "[" + this.join(',') + "]";
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
console.log(arr.toString());
// [1,2,3,4,5,6,7,8,9,0]

以上便是对函数的重写,重写只会覆盖当前对象的方法,所以基于当前原型的对象都可能受到影响,例如上面的例子中我们重写了 Array 原型的 toString 方法,那么所有的数组在调用 toString 方法时都将受到影响。

引申:如何检测一个原生方法是否被重写

既然有了函数重写,那么就有判断函数是否被重写的需要,例如上面的例子中,Array对象原型的 toString 方法被重写了,那么我们怎么通过 JavaScript 来判断其是否被重写呢?

其实方法是有的,还是使用 toString 检测,不过这次的 toString 不在是 Array 原型上的 toString 方法了,而是 Function 原型上的 toString 方法。

任何函数对象都是 Function 的子类,所以我们就可以调用函数对象的 toString 方法:

console.log(Array.prototype.toString.toString());
// function toString() { [native code] }

一个原生的内部函数,在没有被重写之前,我们是看不到函数代码的,所以通过 toString 转字符串,可以看到函数体为 [native code],但是,当一个函数被重写后,这个值就不再是 native code 了,以上文中重写的 Array.prototype.toString() 方法为例,我们重写后,再通过 Function.prototype.toString 来查看,则可以输出相应的代码:

Array.prototype.toString = function () {
    return "[" + this.join(',') + "]";
}
// 下面两行代码是等价的
console.log(Function.prototype.toString.call(Array.prototype.toString));
console.log(Array.prototype.toString.toString());
// 输出了函数代码
// function () {
//     return "[" + this.join(',') + "]";
// }

基于上面的特性,就可以写一个方法,用来检测某个原型的属性方法是否被重写:

function isFunRewarited (func) {
    if (func.toString().indexOf("{ [native code] }") > 0) return false;
    return true;
}

console.log(isFunRewarited(Array.prototype.toString));

以上便是检测原生函数是否被重写的方法。

类似文章

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注