JavaScript不支持重载的语法,它没有重载所需要的函数签名。

ECMAScript函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。如前所述,ECMAScirpt函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。 — JavaScript高级程序设计(第3版)3.7.2小节

而且在JavaScript中,函数名本身就是变量,函数声明类似于变量赋值。当同个函数名被多次声明时,后声明的内容将覆盖前面的内容,如下所示:

function addSomeNumber(num){
  return num + 100;
}
function addSomeNumber(num){
  return num + 200;
}
var result = addSomeNumber(100); // => 300

尽管JavaScript无法做到真正的重载,但是可以通过检查传入函数中参数的类型和数量并作相应的处理,从而实现重载的效果,曲线救国。 下面介绍几种JS重载的姿势:

姿势一:借助流程控制语句

通过判断传入参数的数量(arguments.length),执行相应的代码块。

function doSomething(){
  switch(arguments.length){
    case 0:
      /* 代码块 */
      break;
    ...
    case n:
      /* 代码块 */
      break;
  }
}

姿势二:巧用闭包特性

jQuery创始人John Resig在《Secrets of the JavaScript Ninja》的第4.4.2小节中介绍了一种方法,如下所示,可通过addMethod函数将对象ninja的whatever方法进行重载。

var ninja = {};
addMethod(ninja, 'whatever', function(){/* code */});
addMethod(ninja, 'whatever', function(a){/* code */});
addMethod(ninja, 'whatever', function(a,b){/* code */});

addMethod函数接收3个参数:目标对象、目标方法名、函数体,当函数被调用时:
1. 先将目标object[name]的值存入变量old中,因此起初old中的值可能不是一个函数
2. 接着向object[name]赋值一个代理函数,并且由于变量old、fn在代理函数中被引用,所以old、fn将常驻内存不被回收。

function addMethod(object, name, fn) {
  var old = object[name];  // 保存前一个值,以便后续调用
  object[name] = function(){  // 向object[name]赋值一个代理函数
    // 判断fn期望接收的参数与传入参数个数是否一致
    if (fn.length == arguments.length)
      // 若是,则调用fn
      return fn.apply(this, arguments)
    else if (typeof old == 'function')  // 若否,则判断old的值是否为函数
      // 若是,则调用old
      return old.apply(this, arguments);
  };
}

代理函数被调用时:
1. 先判断传入参数与其父级作用域中fn期望接收参数的个数是否一致,若是则调用该fn;
2. 若否,则判断其父级作用域中old值类型是否为函数,若是则调用该old;
3. 当old中存有上一次生成的代理函数时,则会重复前面两个步骤,直至old值不为代理函数


上述两种方法都是通过检查参数个数来实现重载,不区分参数类型
此外,方法1在继承时重载的那些函数无法被重写,而方法2通过逐个执行代理函数,比对参数个数,直至找到目标函数,效率不高。
为了解决这些问题,我构思设计一个方法,见下文。若你知道更好的方法或思路,还请留言指点一下。


姿势三:巧用引用类型特性

核心思想:由于ECMAScript函数是一种引用类型对象,可扩展属性与方法。借此通过创建一个容器用于存储要重载的函数,并将容器挂载到代理函数上以便后续访问,而代理函数利用闭包特性访问容器。
重载顺序:首先查找参数类型匹配的函数,其次查找参数个数匹配的函数。
存储格式:键值对,键名由逗号与参数个数或参数类型组成,键值为要重载的函数,如下:

{
  ',0': function(){/* code */},
  ',1': function(a){/* code */},
  ',string,number': function(a,b){/* code */}
}

工具函数被调用时
1. 先判断是否已重载过,若有,直接将要重载的函数按格式存入容器;
2. 若未重载过,则创建一个容器变量;
3. 判断未重载前的值是否为一个函数,若是,则以逗号+参数个数的格式存入容器;
4. 将要重载的函数存入容器;
5. 代理原函数,并将容器挂载到代理函数上;
6. 当代理函数被调用时,将依次查找容器中匹配的函数并调用。

/**
 * 重载工具函数
 * @param {Object} ctx - 上下文
 * @param {String} name - 函数名
 * @param {Function} fn - 函数体
 * @param {String} type - 参数类型
 * @author 范围兄 <ambit_tsai@qq>
 * @example 不指定参数类型
 *  overload(obj, 'do', function(){...});
 *  overload(obj, 'do', function(a){...});
 * @example 指定参数类型
 *  overload(obj, 'do', function(a,b){...}, 'string,number');
 */
function overload(ctx, name, fn, type){
  type = type? type.trim().toLowerCase(): fn.length;
  // 已重载过
  if(typeof ctx[name]==='function' && typeof ctx[name]._$fnMap==='object'){
    ctx[name]._$fnMap[','+type] = fn;	// 将fn存入_$fnMap
    return;
  }
  // 未重载过
  var fnMap = {};   // 容器
  if(typeof ctx[name] === 'function'){
    // 若ctx[name]是一个函数,则存入容器
    fnMap[','+ctx[name].length] = ctx[name];
  }
  fnMap[','+type] = fn;
  ctx[name] = function overloading(){   // 代理
    var args = arguments, 
        len = args.length, 
        type, i;
    for(i=0, type=''; i<len; ++i){  // 计算参数类型
      type += ',' + typeof args[i];
    }
    // 依次匹配:参数类型->参数个数
    if(fnMap[type]) return fnMap[type].apply(this, args);
    if(fnMap[','+len]) return fnMap[','+len].apply(this, args);
    throw 'Overload: no matched function';
  };
  ctx[name]._$fnMap = fnMap;   // 将fnMap挂载到代理上
}

都看到这了,给点个赞呗 (。◕∀◕。)

更多推荐

JS方法/函数重载的姿势