前情提要
最近在学习snabbdom的源码,发现了一段很有意思的代码,如下:
了解到函数重载的概念,于是就又去搜了搜相关知识,发现了一篇很有意思的博文:
博文中提到如何用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"]
但是,看到这里我发现自己处于七窍通了六窍的状态.
每行代码都能看得懂,闭包的概念我也知道,但是怎么连在一起就看不懂了呢?就算你设了一个闭包,但每次调用addMethod不是只存上一次的函数(也就是红框中的old变量)吗?
那当我第三次调用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是什么
我们发现people.find其实是一个通用的函数,它并不是具体的我们传入addMethod中的函数,也就是说fn和old都是闭包,fn我们知道了就是我们传入addMethod中的函数,那么old呢?让我们来打印一下old
我们发现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闭包,最终符合合适的结果.
总结
当然,多层的闭包嵌套肯定会造成内存泄露,而且该题也并不是没有其它的解法,只是其中的闭包+递归的思路让人眼前一亮,并由此发散其它的场景是否也有相似的思路呢?