JS如何实现函数重载?

550 阅读5分钟

前情提要


最近在学习snabbdom的源码,发现了一段很有意思的代码,如下:

image.png

了解到函数重载的概念,于是就又去搜了搜相关知识,发现了一篇很有意思的博文:

原博地址:www.cnblogs.com/yugege/p/55…

博文中提到如何用JS实现函数重载,巧妙的思路让人眼前一亮,所以特此做一篇学习笔记.

函数重构的概念


重载就是一组具有相同函数名,但参数个数或参数类型不同的函数.

举个例子:

function add(a:number,b:number){
    console.log(a + b)
}
function add(a:number,b:number,c:number){
    console.log(a + b + c)
}
add(1 + 2) //3
add(1 + 2 + 3) //6

如上面代码所示,我们多次调用add函数,但根据传递的参数不同,所调用的函数也不同.

函数重构的问题


原博主提了一个这样的问题,有一个people对象,里面存着一些人名,如下:

var people = {
  values: ["Dean Edwards", "Sam Stephenson", "Alex Russell", "Dean Tom"]
};

我们希望people对象拥有一个find方法,当不传任何参数时,就会把people.values里面的所有元素返回来;当传一个参数时,就把first-name跟这个参数匹配的元素返回来;当传两个参数时,则把first-name和last-name都匹配的才返回来。因为find方法是根据参数的个数不同而执行不同的操作的,所以,我们希望有一个addMethod方法,能够如下的为people添加find的重载:

addMethod(people, "find", function() {}); /*不传参*/
addMethod(people, "find", function(a) {}); /*传一个*/
addMethod(people, "find", function(a, b) {}); /*传两个*/

原博也贴出了答案:

function addMethod(object, name, fn) {
  var old = object[name]; //把前一次添加的方法存在一个临时变量old里面
  object[name] = function() { // 重写了object[name]的方法
    // 如果调用object[name]方法时,传入的参数个数跟预期的一致,则直接调用
    if(fn.length === arguments.length) {
      return fn.apply(this, arguments);
    // 否则,判断old是否是函数,如果是,就调用old
    } else if(typeof old === "function") {
      return old.apply(this, arguments);
    }
  }
}

最终实现了所需效果,完整代码如下:

//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 people = {
  values: ["Dean Edwards", "Alex Russell", "Dean Tom"]
};
 
/* 下面开始通过addMethod来实现对people.find方法的重载 */
 
// 不传参数时,返回peopld.values里面的所有元素
addMethod(people, "find", function() {
  return this.values;
});
 
// 传一个参数时,按first-name的匹配进行返回
addMethod(people, "find", function(firstName) {
  var ret = [];
  for(var i = 0; i < this.values.length; i++) {
    if(this.values[i].indexOf(firstName) === 0) {
      ret.push(this.values[i]);
    }
  }
  return ret;
});
 
// 传两个参数时,返回first-name和last-name都匹配的元素
addMethod(people, "find", function(firstName, lastName) {
  var ret = [];
  for(var i = 0; i < this.values.length; i++) {
    if(this.values[i] === (firstName + " " + lastName)) {
      ret.push(this.values[i]);
    }
  }
  return ret;
});
 
// 测试:
console.log(people.find()); //["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); //["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean Edwards")); //["Dean Edwards"]

但是,看到这里我发现自己处于七窍通了六窍的状态.

image.png

每行代码都能看得懂,闭包的概念我也知道,但是怎么连在一起就看不懂了呢?就算你设了一个闭包,但每次调用addMethod不是只存上一次的函数(也就是红框中的old变量)吗?

image.png

那当我第三次调用addMethod函数时,第一次调用缓存的old闭包不是取不到了吗?也就是说我再次调用people.find()就会打印["Dean Edwards", "Dean Tom"],但事实证明我是错的.

// 我以为打印的结果
console.log(people.find()); //["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); //["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean Edwards")); //["Dean Edwards"]
console.log(people.find()); //["Dean Edwards", "Dean Tom"]

//实际结果
console.log(people.find()); //["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); //["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean Edwards")); //["Dean Edwards"]
console.log(people.find()); //["Dean Edwards", "Alex Russell", "Dean Tom"]

函数重构的问题解析


于是,我开始研究其中逻辑,首先确认一下people.find是什么

image.png

我们发现people.find其实是一个通用的函数,它并不是具体的我们传入addMethod中的函数,也就是说fn和old都是闭包,fn我们知道了就是我们传入addMethod中的函数,那么old呢?让我们来打印一下old

image.png

我们发现old其实也是那个通用的函数,而它之中同样含有fn和old两个变量,这代表着闭包的嵌套.

所以,答案已经很明显了,当我们第一次调用addMethod函数时,产生了fn和old两个闭包,其中fn是我们传入的函数,old是null.

当我们第二次调用addMethod函数时,产生了新的fn和old两个闭包,其中fn是我们二次调用时传入的函数,old则是之前return的通用函数,其中包含了第一次调用产生的fn和old两个闭包,一共4个闭包.

当们第三次调用addMethod函数时,同样产生了新的fn和old两个闭包,其中fn是我们三次调用时传入的函数,old则是之前return的通用函数,其中的逻辑和前面一样,嵌套了之前的闭包,一共6个闭包.

当我们调用people.find函数时,它会依次递归查找符合条件的fn,如果当前fn不符要求,则调用old函数获取上一次的fn闭包,最终符合合适的结果.

总结


当然,多层的闭包嵌套肯定会造成内存泄露,而且该题也并不是没有其它的解法,只是其中的闭包+递归的思路让人眼前一亮,并由此发散其它的场景是否也有相似的思路呢?