在 JavaScript 中,函数为一等公民(First Class),所谓的“一等公民”,指的是函数与其余数据类型一样,处于平等位置,能够赋值给其余变量,也能够作为参数,传入另一个函数,或作为其它函数的返回值。
接下来阿宝哥将介绍与函数相干的一些技术,浏览完本文,你将理解高阶函数、函数组合、柯里化、偏函数、惰性函数和缓存函数的相干常识。
一、高阶函数
在数学和计算机科学中,高阶函数是至多满足下列一个条件的函数:
- 承受一个或多个函数作为输出;
- 输入一个函数。
接管一个或多个函数作为输出,即函数作为参数传递。这种利用场景,置信很多人都不会生疏。比方罕用的 Array.prototype.map()
和 Array.prototype.filter()
高阶函数:
// Array.prototype.map 高阶函数
const array = [1, 2, 3, 4];
const map = array.map(x => x * 2); // [2, 4, 6, 8]
// Array.prototype.filter 高阶函数
const words = ['semlinker', 'kakuqo', 'lolo', 'abao'];
const result = words.filter(word => word.length > 5); // ["semlinker", "kakuqo"]
而输入一个函数,即调用高阶函数之后,会返回一个新的函数。咱们日常工作中,常见的 debounce
和 throttle
函数就满足这个条件,因而它们也能够被称为高阶函数。
关注「全栈修仙之路」浏览阿宝哥原创的 3 本收费电子书及 50 几篇“重学 TS”教程。
二、函数组合
函数组合就是将两个或两个以上的函数组合生成一个新函数的过程:
const compose = function (f, g) {return function (x) {return f(g(x));
};
};
在以上代码中,f
和 g
都是函数,而 x
是组合生成新函数的参数。
2.1 函数组合的作用
在我的项目开发过程中,为了实现函数的复用,咱们通常会尽量保障函数的职责繁多,比方咱们定义了以下性能函数:
在领有以上性能函数的根底上,咱们就能够自在地对函数进行组合,来实现特定的性能:
function lowerCase(input) {return input && typeof input === "string" ? input.toLowerCase() : input;
}
function upperCase(input) {return input && typeof input === "string" ? input.toUpperCase() : input;
}
function trim(input) {return typeof input === "string" ? input.trim() : input;
}
function split(input, delimiter = ",") {return typeof input === "string" ? input.split(delimiter) : input;
}
const trimLowerCaseAndSplit = compose(trim, lowerCase, split);
trimLowerCaseAndSplit("a,B,C"); // ["a", "b", "c"]
在以上的代码中,咱们通过 compose
函数实现了一个 trimLowerCaseAndSplit
函数,该函数会对输出的字符串,先执行去空格解决,而后在把字符串中蕴含的字母对立转换为小写,最初在应用 ,
分号对字符串进行拆分。利用函数组合的技术,咱们就能够很不便的实现一个 trimUpperCaseAndSplit
函数。
2.2 组合函数的实现
function compose(...funcs) {return function (x) {return funcs.reduce(function (arg, fn) {return fn(arg);
}, x);
};
}
在以上的代码中,咱们通过 Array.prototype.reduce 办法来实现组合函数的调度,对应的执行程序是从左到右。这个执行程序与 Linux 管道或过滤器的执行程序是统一的。
不过如果你想从右往左开始执行的话,这时你就能够应用 Array.prototype.reduceRight 办法来实现。
其实每当看到 compose
函数,阿宝哥就不由自主想到“如何更好地了解中间件和洋葱模型”这篇文章中介绍的 compose
函数:
function compose(middleware) {
// 省略局部代码
return function (context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err);
}
}
};
}
利用上述的 compose
函数,咱们就能够实现以下通用的工作解决流程:
三、柯里化
柯里化(Currying)是一种处理函数中含有多个参数的办法,并在只容许繁多参数的框架中应用这些函数。这种转变是当初被称为“柯里化”的过程,在这个过程中咱们能把一个带有多个参数的函数转换成一系列的嵌套函数。它返回一个新函数,这个新函数冀望传入下一个参数。当接管足够的参数后,会主动执行原函数。
在实践计算机科学中,柯里化提供了简略的实践模型,比方:在只承受一个繁多参数的 lambda 演算中,钻研带有多个参数的函数的形式。与柯里化相同的是 Uncurrying,一种应用匿名单参数函数来实现多参数函数的办法。比方:
const func = function(a) {return function(b) {return a * a + b * b;}
}
func(3)(4); // 25
Uncurrying 不是本文的重点,接下来咱们应用 Lodash 提供的 curry
函数来直观感受一下,对函数进行“柯里化”解决之后产生的变动:
const abc = function(a, b, c) {return [a, b, c];
};
const curried = _.curry(abc);
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]
_.curry(func, [arity=func.length])
创立一个函数,该函数接管
func
的参数,要么调用func
返回的后果,如果func
所需参数曾经提供,则间接返回func
所执行的后果。或返回一个函数,承受余下的func
参数的函数,能够应用func.length
设置须要累积的参数个数。起源:https://www.lodashjs.com/docs…
这里须要特地留神的是,在数学和实践计算机科学中的柯里化函数,一次只能传递一个参数。而对于 JavaScript 语言来说,在理论利用中的柯里化函数,能够传递一个或多个参数。好的,介绍完柯里化的相干常识,接下来咱们来介绍柯里化的作用。
3.1 柯里化的作用
3.1.1 参数复用
function buildUri(scheme, domain, path) {return `${scheme}://${domain}/${path}`;
}
const profilePath = buildUri("https", "github.com", "semlinker/semlinker");
const awesomeTsPath = buildUri("https", "github.com", "semlinker/awesome-typescript");
在以上代码中,首先咱们定义了一个 buildUri
函数,该函数可用于构建 uri 地址。接着咱们应用 buildUri
函数构建了阿宝哥 Github 个人主页 和 awesome-typescript 我的项目的地址。对于上述的 uri 地址,咱们发现 https
和 github.com
这两个参数值是一样的。
如果咱们须要持续构建阿宝哥其余我的项目的地址,咱们就须要反复设置雷同的参数值。那么有没有方法简化这个流程呢?答案是有的,就是对 buildUri
函数执行柯里化解决,具体解决形式如下:
const _ = require("lodash");
const buildUriCurry = _.curry(buildUri);
const myGithubPath = buildUriCurry("https", "github.com");
const profilePath = myGithubPath("semlinker/semlinker");
const awesomeTsPath = myGithubPath("semlinker/awesome-typescript");
3.1.2 提早计算 / 运行
const add = function (a, b) {return a + b;};
const curried = _.curry(add);
const plusOne = curried(1);
在以上代码中,通过对 add
函数执行“柯里化”解决,咱们能够实现提早计算。好的,简略介绍完柯里化的作用,咱们来入手实现一个柯里化函数。
3.2 柯里化的实现
当初咱们曾经晓得了,当柯里化后的函数接管到足够的参数后,就会开始执行原函数。而如果接管到的参数有余的话,就会返回一个新的函数,用来接管余下的参数。基于上述的特点,咱们就能够本人实现一个 curry
函数:
function curry(func) {return function curried(...args) {if (args.length >= func.length) { // 通过函数的 length 属性,来获取函数的形参个数
return func.apply(this, args);
} else {return function (...args2) {return curried.apply(this, args.concat(args2));
};
}
}
}
四、偏函数利用
在计算机科学中,偏函数利用(Partial Application)是指固定一个函数的某些参数,而后产生另一个更小元的函数。而所谓的元是指函数参数的个数,比方含有一个参数的函数被称为一元函数。
偏函数利用(Partial Application)很容易与函数柯里化混同,它们之间的区别是:
- 偏函数利用是固定一个函数的一个或多个参数,并返回一个能够接管残余参数的函数;
- 柯里化是将函数转化为多个嵌套的一元函数,也就是每个函数只接管一个参数。
理解完偏函数与柯里化的区别之后,咱们来应用 Lodash 提供的 partial
函数来理解一下它如何应用。
4.1 偏函数的应用
function buildUri(scheme, domain, path) {return `${scheme}://${domain}/${path}`;
}
const myGithubPath = _.partial(buildUri, "https", "github.com");
const profilePath = myGithubPath("semlinker/semlinker");
const awesomeTsPath = myGithubPath("semlinker/awesome-typescript");
_.partial(func, [partials])
创立一个函数。该函数调用
func
,并传入预设的partials
参数。起源:https://www.lodashjs.com/docs…
4.2 偏函数的实现
偏函数用于固定一个函数的一个或多个参数,并返回一个能够接管残余参数的函数。基于上述的特点,咱们就能够本人实现一个 partial
函数:
function partial(fn) {let args = [].slice.call(arguments, 1);
return function () {const newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
};
}
4.3 偏函数实现 vs 柯里化实现
五、惰性函数
因为不同浏览器之间存在一些兼容性问题,这导致了咱们在应用一些 Web API 时,须要进行判断,比方:
function addHandler(element, type, handler) {if (element.addEventListener) {element.addEventListener(type, handler, false);
} else if (element.attachEvent) {element.attachEvent("on" + type, handler);
} else {element["on" + type] = handler;
}
}
在以上代码中,咱们实现了不同浏览器 增加事件监听 的解决。代码实现起来也很简略,但存在一个问题,即每次调用的时候都须要进行判断,很显著这是不合理的。对于上述这个问题,咱们能够通过惰性载入函数来解决。
5.1 惰性载入函数
所谓的惰性载入就是当第 1 次依据条件执行函数后,在第 2 次调用函数时,就不再检测条件,间接执行函数。要实现这个性能,咱们能够在第 1 次条件判断的时候,在满足判断条件的分支中笼罩掉所调用的函数,具体的实现形式如下所示:
function addHandler(element, type, handler) {if (element.addEventListener) {addHandler = function (element, type, handler) {element.addEventListener(type, handler, false);
};
} else if (element.attachEvent) {addHandler = function (element, type, handler) {element.attachEvent("on" + type, handler);
};
} else {addHandler = function (element, type, handler) {element["on" + type] = handler;
};
}
// 保障首次调用能失常执行监听
return addHandler(element, type, handler);
}
除了应用以上的形式,咱们也能够利用自执行函数来实现惰性载入:
const addHandler = (function () {if (document.addEventListener) {return function (element, type, handler) {element.addEventListener(type, handler, false);
};
} else if (document.attachEvent) {return function (element, type, handler) {element.attachEvent("on" + type, handler);
};
} else {return function (element, type, handler) {element["on" + type] = handler;
};
}
})();
通过自执行函数,在代码加载阶段就会执行一次条件判断,而后在对应的条件分支中返回一个新的函数,用来实现对应的解决逻辑。
六、缓存函数
缓存函数是将函数的计算结果缓存起来,当下次以同样的参数调用该函数时,间接返回已缓存的后果,而无需再次执行函数。这是一种常见的以空间换工夫的性能优化伎俩。
要实现缓存函数的性能,咱们能够把通过序列化的参数作为 key
,在把第 1 次调用后的后果作为 value
存储到对象中。在每次执行函数调用前,都先判断缓存中是否含有对应的 key
,如果有的话,间接返回该 key
对应的值。剖析完缓存函数的实现思路之后,接下来咱们来看一下具体如何实现:
function memorize(fn) {const cache = Object.create(null); // 存储缓存数据的对象
return function (...args) {const _args = JSON.stringify(args);
return cache[_args] || (cache[_args] = fn.apply(fn, args));
};
};
定义完 memorize
缓存函数之后,咱们就能够这样来应用它:
let complexCalc = (a, b) => {// 执行简单的计算};
let memoCalc = memorize(complexCalc);
memoCalc(666, 888);
memoCalc(666, 888); // 从缓存中获取
关注「全栈修仙之路」浏览阿宝哥原创的 3 本收费电子书及 50 几篇“重学 TS”教程。
七、参考资源
- 维基百科 – 高阶函数
- 维基百科 – 柯里化
- javascript-functional-programming-explained-partial-application-and-currying