前言
在工作中,经常遇到需要增强原来函数功能的情况,例如: 打印函数参数列表或者返回值,让函数只执行一次等。起初都是直接去修改函数的实现,方便而又快捷,直到有一天修改的时候不小心改了原来的代码,加入了一个不易被识别出来的隐藏bug,最后造成了一定的影响,于是乎考虑有没有比较优雅的方式来增强原来函数的功能呢,最终找到了答案 — 高阶函数。
需求
在此之前,我们先来看一段代码:
function sum(...args) {
return args.reduce((pre, cur) => pre + cur, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 输出:15
复制代码
这是一段很简单的求和代码,假设我们现在接到一个新的需求,要求我们在调用这个函数的时候,打印出参数的参数和返回值,那么我们该如何做呢?
不优雅的实现
话不多说,直接上源码:
function sum(...args) {
console.log("参数 => ", args);
let result = args.reduce((pre, cur) => pre + cur, 0);
console.log("结果 => ", result);
return result;
}
console.log(sum(1, 2, 3, 4, 5));
// 参数 => [ 1, 2, 3, 4, 5 ]
// 结果 => 15
// 15
复制代码
我们通过修改源码的方式满足了这个需求,但是这个版本的实现不够优雅,原因在于:
- 违背了设计模式的开闭原则,在加入新功能的时候有可能会引入新的bug。
- 没法复用,假如现在有一个函数 sub 也需要打印参数和结果的话,便需要重新写一遍逻辑。
优雅的实现–高阶函数
什么是高阶函数
简而言之就是返回值为函数或者参数为函数的参数的函数。再js中,Array.prototype
上的map
,forEach
等都是高阶函数。
高阶函数版实现
在下面的例子中,我们便使用高阶函数来实现需求。
function sum(...args) {
return args.reduce((pre, cur) => pre + cur, 0);
}
function logParamsAndResult(fn) {
return function (...args) {
console.log("参数 => ", args);
let result = fn.apply(this, args);
console.log("结果 => ", result);
return result;
};
}
let sumPro = logParamsAndResult(sum);
console.log(sumPro(1, 2, 3, 4, 5));
// 参数 => [ 1, 2, 3, 4, 5 ]
// 结果 => 15
// 15
复制代码
这个版本的实现中,我们并没有去修改原来的代码,而是返回一个函数,在这个函数中打印了传入的参数,并且通过apply调用了原来的函数,获取到返回值,最终实现了需求。相比于上一个版本的实现,具有以下优点:
- 没有修改到原来的代码,无侵入地增强了函数的功能。
- 复用性强,对于所有的需要打印参数和返回值的函数,都可以用这个高阶函数包装一下。
由于这类函数都是用来增强原有函数的功能, 这里姑且称他们为 增强函数 或者 enhancer
如何编写一个增强函数
在讨论如何编写一个增强函数之前,我们首先看一下这两个经典的增强函数例子,以作参考:
function once(fn) {
let called = false;
return function (...args) {
if (called) return;
called = true;
let ret = fn.apply(this, args);
return ret;
};
}
function withCache(fn) {
let cache = {};
return function (...args) {
let key = JSON.stringify(args);
return cache[key] || (cache[key] = fn.apply(this, args));
};
}
复制代码
once
函数的功能是返回一个只执行一次的函数,利用 called
变量来保证传入的fn
函数只会执行一次。
withCache
函数则是通过cache
变量来实现了缓存,从而让fn
函数具有了缓存功能。从以上几个增强函数的实现中,我们不难看出,他们的实现是有一定的套路的,具体看下面代码的注释:
function wrapper(fn) {
// 1 闭包处理
return function (...args) {
// 2 拦截参数
const ret = fn.apply(this, args);
// 3 拦截返回值
return ret;
};
}
复制代码
忘了是哪个超级大佬说的,在计算机领域没有什么问题是不能通过增加一个间接的中间层来解决的,如果有,那就再加一层。 增强函数也无非就是加了一个中间层(即返回的函数),从而达到拦截函数调用,拓展函数功能的目的。如上面的例子所示,最核心的代码就是通过 const ret = fn.apply(this, args)
调用原来的函数,而我们有三个地方可以对函数功能进行拓展:
- 在注释1这里,我们可以利用闭包机制,实现一些特殊的功能,例如前文的
once
和withCache
函数; - 在注释2这里,我们可以拦截到参数,进而对参数进行处理;
- 在注释3这里,可以拦截到返回值,进而对返回值进行处理。
以上就是我们编写增强函数拓展函数功能的三个地方。除此之外,还有两个点需要注意:
- 在核心代码
const ret = fn.apply(this, args)
调用这里,apply
的第一个参数一定要用 this 而不是null
或者fn
(之前看别人的博客有人这样写), 这样才保证fn
在调用时this
能动态绑定到调用它的对象上。 具体的可以参照《你不知道的Javascript》上卷关于this
的解释(强烈地推荐) - 拦截函数返回值后要记得将返回值返回。
总结
高阶函数的运用使得我们可以优雅地增强函数功能,值得在工作中使用,此类增强函数的编写套路也十分清晰,只要掌握到精髓,便能写出更优雅的代码。