/ JavaScript

Javascript函数重载解决方案

方法重载是让类以统一的方式处理不同类型数据的一种手段。Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给它们的不同个数和类型的参数来决定具体使用哪个方法, 这就是多态性。

Javascript作为弱类型的语言,不能像JAVA、C#等静态语言一样,在语法层面上支持函数的重载,这体现在两个方面:

  • 参数数量相同,类型不同 – Javascript的参数声明中根本没有类型
  • 参数数量不同 – 后定义的函数会覆盖之前定义的

因此以下的代码

function fn(a) {
    alert(a);
}

function fn(a, b) {
    alert(a + b);
}

alert(fn(3)); // -> NaN
alert(fn(3, 4)); // -> 7

fn(a, b)函数覆盖了fn(a)函数,因此即使只给一个参数,也会被认为有2个参数而输出NaN

好在Javascript提供了callapply两个函数,可以更动态地去调用函数,这也成为了重载的实现的基础.

重载的基本组成

让函数支持重载,则需要有重载的策略,一般来说,一组重载策略包括以下内容:

  • 参数的数量。
  • 参数的类型。
  • 参数的顺序。

而这三者,归根结底,可以描述为不同类型不同数量的参数的特定且唯一的组织方式

在Javascript中,基本类型(不是原生类型)有Number、String、Object、Array、Date、Function、RegExp、Undefined、Null。而对于Undefined和Null,我们不认为在重载中他能代表一个特定的类型,毕竟很少听说某函数的第二个参数必须是null这种情况。

在以上的前提下,我们分别用nsoadfr来代表不同的类型,因此不同的参数组合可以用单纯的字符串来表示,如:

'nso'; // -> Number + String + Object
'dfsr'; // -> Date + Function + String + RegExp

基本实现

所谓重载,其最基本的原理就是生成一张“参数组合”至“函数对象”的映射表,根据不同的组合调用不同的函数,因此重载的入口函数可以设计成如下形式:

overload({
    rule1: fn1,
    rule2: fn2,
    //...
    rulex: fnx
});

其中关键的几步,不外乎:

  1. 获取表示参数组合的字符串。
  2. 查找对应的函数。
  3. 调用函数。

其实实现很简单,直接给代码就行,只不过确实没有考虑到nullundefined的情况,调用的时候必须小心(当然因为这不是最终形态啦):

function type(obj) {
    var type = Object.prototype.toString.call(obj);
    type = type.substring('[Object '.length, type.length - 1);
    return type.charAt(0).toLowerCase();
}

function overload(config) {
    return function() {
        var input = [],
            length = arguments.length,
            i = 0,
            fn;
        //获取参数组合表达形式
        for (; i < length; i++) {
            input[i] = type(arguments[i]);
        }
        input = input.join('');
        //查找对应函数
        fn = config[input];
        if (fn) {
            return fn.apply(this, arguments);
        }
    };
}

当然这是最基本的重载实现,功能是差了点,连最基本的可变参数都不能用,有待改进。

支持可变参数

如果定死了参数的类型和顺序,那Javascript和静态语言就没区别了,作为如此优雅的语言,过分的静态化是绝对不允许的,因此即使是重载,也得去支持可变参数数量、可变参数类型

这个从接口层面上来说,还是比较简单的:

  • ?代表一个类型不定的参数
  • *代表一个或多个类型不定的参数

实现上说,这是一种基于字符的从头到尾的句法分析,但是笔者比较懒,所以决定用懒方法,干脆拿出正则来做吧,接着上面的代码:

if (!fn) {
    for (i in config) {
        regex = i.replace(/\?/g, '[nsbodrf]').replace(/\*/g, '[nsbodrf]+');
        if (new RegExp('^' + regex + '$', 'g').test(input)) {
            fn = config[i];
            break;
        }
    }
}

直接把?和*替换成相应的正则表达式,逐条规则的匹配就行。

处理null和undefined

nullundefined可以被认为是任何类型的对象,所以这个的处理也很简单:

  1. 生成参数表达式的时候,将nullundefined用特定的字符代替,比如#这个字符。
  2. 生成正则时,考虑到#这个字符。

所以对上面的代码稍加改造,主要集中在生成正则的一段,可以改造成以下形式:

regex = ('[' + i.split('').join('#][') + '#]')
    .replace(/\[\?#\]/g, '[nsbodrf#]')
    .replace(/\[\*#\]/g, '[nsbodrf#]+');

同时type函数允许返回#,只需要加一句就可以:

if (obj === undefined || obj === null) {
    return '#';
}

试试效果:

var fn = overload({
    'nssn': function() {
        alert('nssn');
    },
    'nns': function() {
        alert('nns');
    },
    'n?n': function() {
        alert('n?n');
    },
    's*': function() {
        alert('s*');
    },
    '*': function() {
        alert('*');
    }
});

fn(3, 'a', 'b', 4); // -> nssn
fn(3, 4, 'a'); // -> nns
fn(3, /s/, 4); // -> n?n
fn(3, new Date, 4); // -> n?n
fn(3, 'a', 4); // -> n?n
fn('a', 'b', 'c'); // -> s*
fn('a', 3, 4); // -> s*
fn('a', function(){}); // -> s*
fn(function(){}, new Date); // -> *
fn('a', null); // -> s*
fn(3, undefined, 4); // -> n?n
fn(null); // -> *
fn(undefined, null); // -> s*

问题

  • 正则毕竟有些慢,配置型函数重载是有额外开销的,可以在函数体内通过arguments对象判断的就尽量用判断,overload只是用来对复杂的重载进行配置上的支持。
  • NaN被认为是Number,这是合理的,但代码中别忘了处理。