纯函数, 高阶函数,函数组合,函数柯里化,偏函数,惰性载入函数,缓存函数
这些概念在函数编程中真的是太常见了,尤其是很多类库实现或者组件封装都会用到这些函数编程技巧。
比如React-redux中的connect方法,React中的高阶组件其实都或多或少用到了上述一些函数编程技巧。刚好最近有幸看到一篇关于这方面的文章,记录一下。
在JS中,函数总是被称为一等公民,那到底为什么会被称为一等公民呢?主要是因为,在JS中函数可以作为普通变量一样使用,可以作为函数的参数,可以被赋值,可以作为函数的return值。这样就导致了函数在JS中具有极其灵活的用法,因此也有了更加强大的功能。
那下面就介绍下上面所说的这些内容,在阅读别人代码或者自己封装一些方法时都会很有用,最重要的是面试的时候被面试官问到了,不至于完全不知道而尴尴尬尬。
1. 纯函数
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数
重点有两点:一是返回结果只依赖它的参数,二是执行过程中没有副作用。
执行过程没有副作用是指,不会对函数外面的变量造成任何影响。
let num = 0;
// bar不是纯函数,因为返回值依赖了外部变量num
function bar(a, b) {
return a+b+num;
}
// laa不是纯函数,执行过程中产生副作用,影响外面num变量
function laa(a, b) {
num++;
return a+b;
}
// foo是纯函数
function foo(a, b) {
return a+b;
}
看一下上面的例子就ok了。使用纯函数的原因是因为它的特点是比较靠谱,接收相同的参数,就一定能输出相同的值,这样的程序易于调试,不易出现莫名其妙的问题。
2. 高阶函数
高阶函数有两种形式:其一是函数的参数是另一个函数(回调函数)其二是函数的返回值是一个函数。
函数参数为一个函数,可以理解成回调函数就是高阶函数的一种,这样理解我觉得没有什么错。可以参考知乎回调函数和高阶函数的区别?
提问下的回答。
ES6版本常见的数组的方法们map
filter
reduce
等等都是高阶函数。
函数的返回值是一个函数,举个例子,比如防抖(debouce)函数和节流(throttle)函数
借助下lodash的例子看下
// 避免窗口在变动时出现昂贵的计算开销。
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
// 当点击时 `sendMail` 随后就被调用。
jQuery(element).on('click', _.debounce(sendMail, 300, {
'leading': true,
'trailing': false
}));
接收一个或多个函数作为输入,经过加工最终输出一个新的函数,中间可以定义一些其他逻辑。比如再看个小例子:
function Eat(a,b){ //核心业务代码
console.log(a,b)
}
Function.prototype.before = function(callback){ //高阶函数
return (...args)=>{ //使用rest运算符接收
callback();
this(...args); //使用展开运算符传入
}
}
let beforeEat = Eat.before(function(){ //自己扩展业务代码
console.log("before eat")
})
beforeEat("米饭","牛肉") //传参
可以用来扩展函数功能。
3. 函数组合
函数组合就是将功能单一的函数,进行组合返回一个功能更加强大的函数。(如果看过铠甲勇士的话,想象一下金木水火土合成帝皇侠那种感觉,如果没看过就算了)
为了降低耦合性,我们封装的函数功能性比较单一,便于在不同的场景下使用。比如如下函数:
function lowerCase(str) {
return str.toLowerCase();
}
function upperCase(str){
return str.toUpperCase();
}
function trim(str) {
return str.trim();
}
三个函数分别代表三个功能,当有一天忽然需要转换小写和去除字符串的头尾空格的函数,比如lowerCaseAndtrim方法这时候可以实现一个compose方法如下调用。
let lowerCaseAndtrim = compost(lowerCase, trim);
lowerCaseAndtrim(' JaVascRipt ');
最终目标输出 javascript;
接下来实现一下compose函数
思路:compose函数返回值是一个函数
function compose(...funcs) {
return function(x) {
let result = x;
for(let i = 0; i < funcs.length; i++) {
result = funcs[i](result)
}
return result;
}
}
遍历执行compose方法接收的每一个方法,将目标参数逐一交给每个函数执行最终返回。
借助 Array.prototype.reduce实现
function compose(...funcs) {
return (x) => {
return funcs.reduce((prev, next) => {
return next(prev);
}, x)
}
}
4. 函数柯里化
函数柯里化是一种可以实现函数多参变单参的方式,在柯里化的过程总,将一个带有多个参数的函数,转换为带有一个参数的一系列的嵌套函数。它返回一个新函数,这个新函数期望传入下一个参数。当接收足够的参数后,会自动执行原函数
function add(a, b, c) {
return a+b+c;
}
let addCurry = curry(add) // curry就是函数实现柯里化的一个方法
addCurry(1)(2)(3); // 本来应该add(1,2,3)调用的 进行柯里化之后就可以addCurry(1)(2)(3) 调用了
关于函数柯里化的讨论可以参考柯里化对函数式编程有何意义?
柯里化实现:
思路:返回值是一个函数,需要根据原函数参数是否等于调用时传入的参数个数来判断是否相等,相等直接传入参数执行原函数,否则返回函数,继续接收参数,并递归判断原函数参数是否等于调用时传入的参数个数逻辑。
function _curry(func) {
return function curried(...args) {
if(func.length === args.length) {
return func.apply(this, args)
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
}
}
5. 偏函数
偏函数指的是固定函数某些参数,从而产生更小元的函数。元指的是函数的参数个数。
比如封装函数的时候函数一共有三个参数,第一个参数是固定不变的,后面的参数是变化的那么就可以使用偏函数。当然也可以固定前两个参数。
function add(int, a, b){
return int+a+b;
}
let partialAdd = partial(add, 10);
partialAdd(1,2); // 输出13
partialAdd(3,4); // 输出17
上面是偏函数使用形式,现在实现一个偏函数
function partial(func, ...args){
return (...args2) => {
return func.apply(this, [...args, ...args2]);
}
}
6. 惰性载入函数
惰性载入函数旨在提升代码性能。比如函数需要根据不同的判断结果返回不同的值,每一次执行函数的时候就需要判断一次,判断的逻辑如果复杂的话,那么对于性能消耗就比较大。尤其是当判断条件是固定的时候使用惰性载入函数是非常适合的。
比如JS代码判断运行平台是安卓还是IOS
let isIos = 1;
function engin() {
if(isIos){
console.log('ios');
// ios逻辑
} else {
console.log('android');
// 安卓逻辑
}
}
engin();
engin();
执行这个engin方法的时候次都要去走判断平台逻辑,性能肯定是消耗滴,我们要的是当第一次执行的时候判断下平台逻辑,之后每一次执行就不需要去判断了,因为平台是不变的,可以节省一些开销。
惰性载入函数的实现1:利用命名覆盖
const isIos = 1;
let engin = function() {
console.log('这里只执行一次哦'); // 注意这里
if(isIos){
engin = function() {
console.log('ios');
// ios逻辑
}
} else {
engin = function() {
console.log('android');
// 安卓逻辑
}
}
return engin();
}
engin(); // ios
engin(); // ios
engin(); // ios
最终ios输出3次,但是上面的console只输出一次。
惰性载入函数的实现2:利用立即执行函数
const isIos = 1;
let engin = (function() {
if(isIos){
return function() {
console.log('ios');
// ios逻辑
}
} else {
return function() {
console.log('android');
// 安卓逻辑
}
}
})();
engin();
engin();
JS文件加载时直接执行一次,之后每次调用都是执行判断之后的逻辑。对于性能优化还是比较有用的。
7. 缓存函数
缓存函数,指的是根据函数参数将函数执行的结果缓存起来,当下次再调用的时候不用去计算就可以直接拿到结果,很明显,这是一个空间换时间的做法。
直接写实现了。
function memorize(func) {
let cache = Object.create(null);
return (...args) => {
let key = JSON.stringify(args);
return cache[key] || (cache[key] = fn.apply(this, args));
}
}
function add(a,b) {
console.log('lala');
return a+b;
}
let d = memorize(add);
console.log(d(1,2)); // 计算获取
console.log(d(1,2)); // 从缓存获取
console.log(d(1,3)); // 计算获取
console.log(d(1,3)); // 从缓存获取
当第二次以相同参数去计算的时候,默认首先从缓存中找,如果找到对应的值,那么直接返回,不参与计算。这样程序运行速度会比较快,但是,比较消耗内存。
以上就是一些关于函数编程中的一些技巧使用。