函数柯里化
柯里化百科:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
收集百科中重要消息:
- 非柯里化:接收多个参数的函数
- 柯里化后:接收一个单一参数(最初函数的第一个参数)的函数
- 返回一个新函数
- 新函数可以满足接收余下参数
- 新函数必须返回结果
对于第三点。我们要返回的新函数能够接收余下参数且新函数返回结果。要同时满足这两点需要注意
以简单的累加器作为案例
非柯里化实现累加器
let sum = function (money0: number, money1: number, money2: number) {
return money0 + money1 + money2;
}
let result = sum(100, 200, 300);
console.log(result);
简单柯里化实现
// 简单柯里化
const curringCost = function (money0: number): Function {
// 定义一个具名函数,作为在未结束前的返回值
return function (money1: number, money2: number) {
return money0 + money1 + money2; // 0号位
};
};
// 新的调用方法
const result = curringCost(100)(200, 300);
console.log(result);
我们为什么能取到money0的值呢?这里涉及到闭包,我们在”0号位“打断点看看作用域
注意两点内容:
- 图片中我们看到,在这个匿名函数的作用域链上有3个,一个是
本地 -> 闭包 -> 全局
(查找变量的方式也是这个顺序),我们的money0因为在本地作用域
上没有值,所以沿着作用域链查找到了闭包作用域
,找到了money0然后返回- 我们注意定义的test变量,并不再闭包中出现,说明原函数curringCost已经释放了,money0这个变量成为了匿名函数的专属背包
问题来了,那么这么一个函数有什么用呢?
提前执行一部分仅仅需要依赖参数1,并且是后面不再变化的死任务
// 原函数 function say (name , args1, args2){ console.log(`${name}说:${args1} + ${args2} = ${args1 + args2}`) } say("lilei", 1, 9); say("hanmeimei", 4, 9); say("lilei", 3, 9); say("hanmeimei", 7, 9); // 柯里化后函数 function curringSay (name){ return function (args1, args2){ console.log(`${name}说:${args1} + ${args2} = ${args1 + args2}`) } } let lilei = curringSay("lilei"); let hanmeimei = curringSay("hanmeimei"); lilei(1, 9); hanmeimei(4, 9); lilei(3, 9); hanmeimei(7, 9);
面试题1:实现一个函数curringSum
,可以curringSum(100)(200)(300)()
这样计算出结果为600
面试题2:实现一个函数,可以将普通函数累加函数丢进去,进化成一个面试题1中的curringSum
函数
反柯里化
interface Function {
uncurrying: () => (obj:any, number: any) => {};
}
Function.prototype.uncurrying = function(){
// 将this指针保留下来
let self = this;
// 返回一个匿名函数被“push”接收了
return function() {
/**
* push(currentObj, 5)
* 获取第一个参数,也就是需要执行的对象(currentObj)
* 这个时候arguments就只剩下“5”这个变量了
*/
let obj = Array.prototype.shift.call(arguments);
/**
* 执行这个方法,注意这个方法是由谁调用的
* 在这个案例中,这个匿名函数是被Array的push方法调用的
* 所以这里的self为push这个方法
*/
return self.apply(obj, arguments);
}
}
// 提取push方法
const push = Array.prototype.push.uncurrying();
let currentObj = {
0 : 1,
length : 1
}
push(currentObj, 5); // {0: 1, 1: 5, length: 2}
console.log(currentObj)
反柯里化: 扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。 定义来源:JS中的反柯里化
本案例中将push方法提取出来,让对象也能正确执行数组的push方法
注意: 这里的length需要设置为1,如果没有这个标识,会覆盖掉0号元素,这个应该是由数组的特殊性决定的
分时函数
/**
* 定义分时函数
* @param arr 任务列表
* @param fn 任务执行函数
* @param count 一次执行任务函数的个数
* @param time 多久开启下一批任务
*/
function timeSharing<T>(arr:Array<T>, fn:Function, count:number, time:number) {
// 内部函数start进行任务操作
function strat(){
let index = 0,
len = Math.min(arr.length, count);
for (; index < len; index++) {
const element = arr.shift();
fn(element);
}
}
// 定义定时器进行任务
let timer = setInterval(() => {
// 当任务跑完后就清理掉定时器
if(arr.length === 0) {
return clearInterval(timer);
}
// 按照传入时间进行任务
strat();
}, time)
}
// 模拟一千个数据
let arr = [];
for (let index = 0; index < 1000; index++) {
arr.push(index);
}
// 进行分时操作,一组10个任务,200毫秒处理完成一批
timeSharing(arr, function(num: number){
console.log(num)
}, 10, 200);
分时函数能够分批解决大量任务,这样浏览器不会因为你一个任务时间过长而导致卡顿问题
惰性加载
其实我觉得这个还是比较勤快,也许是聪明的懒惰。如果方法里有判断,就先加载方法,后面就不需要再进行判断了,下面模拟 浏览器嗅探
-
if A浏览器 使用 aTest方法
-
if B浏览器 使用 bTest方法
公共代码
// 全局控制变量,模拟不同浏览器
const GLOBAL_CONTROL = "A";
function aTest(){
console.log("A浏览器中使用")
}
function bTest(){
console.log("B浏览器中使用")
}
基础版
/**
* 简版
* 需要使用if判断,每次执行都需要
*/
function compatibleFunction() {
if(GLOBAL_CONTROL === "A") {
aTest();
} else {
bTest();
}
}
compatibleFunction();
console.log(compatibleFunction)
使用这种简版,每次都需要判断是使用什么方法,感觉就不是很智能
先行版
/**
* 惰性加载函数 先行版
*/
let lazyCompatibleBefore = (function() {
if(GLOBAL_CONTROL === "A") {
return aTest;
} else {
return bTest;
}
})()
lazyCompatibleBefore();
console.log(lazyCompatibleBefore)
优点:
- 避免了每次判断,只需要开始的时候执行一遍即可
缺点:
- 但是如果项目中没有用到,那这个方法就累赘并且还占用了启动时间
懒加载版
/**
* 惰性加载函数 懒加载版
*/
let lazyCompatibleRuntime = function() {
// 第一次执行后将lazyCompatibleRuntime替换为正确的方法
if(GLOBAL_CONTROL === "A") {
lazyCompatibleRuntime = aTest;
} else {
lazyCompatibleRuntime = bTest;
}
// 这里需要执行一遍,完成操作
lazyCompatibleRuntime();
}
console.log(lazyCompatibleRuntime)
lazyCompatibleRuntime();
console.log(lazyCompatibleRuntime)
优点:
避免了每次判断,执行完第一遍后,后面调用都是正确的方法
同时避免了方法不用时,占用启动时间的问题
节流函数
/**
* 节流函数
* 通用的节流函数
* @param fn 需要节流的函数
* @param time 节流时间
* @param isFrist 第一次是否立即执行
*/
function throttlingWrapper(fn:Function, time:any, isFrist = true) {
var timer:any = null,
_self = fn;
let throtting = () => {
let _me:any = this;
// 如果timer有值,也就是还在执行中,则直接返回
if(timer){
return;
}
// 是否首次触发
if(isFrist){
_self.apply(_me, arguments);
isFrist = false;
return;
}
// 节流函数本体
timer = setTimeout(() => {
_self.apply(_me, arguments);
clearTimeout(timer);
timer = null
}, time)
}
return throtting;
}
其他方法经过这个方法的加工后,我们能拿到一个全新的节流函数方法,这个方法的好处就在于,不管触发多少次,在规定时间内,只触发一次
面试题答案
题目一:实现一个函数curringSum
,可以curringSum(100)(200)(300)()
这样计算出结果为600
- curringSum(100)调用返回的是一个函数,所以才可以这样继续调用curringSum(100)(200)
- 当没有参数的时候就返回结果
- 需要返回所有参数的和,所以我们需要使用闭包保存结果
var curringSum = function (args0: number): any {
let sum: number = args0;
let fun = function (args1?: number) {
// 以这个参数有无为结束标志
if (args1) {
sum += args1;
// 还没有结束,我继续将函数返回,让其继续调用
return fun;
} else {
return sum;
}
}
return fun;
}
let result = curringSum(100)(200)(300)();
console.log(result)
有没有发现这种调用方式是不是好玩多了
题目二 :实现一个函数,可以将普通函数累加函数丢进去,进化成一个面试题1中的curringSum
函数
- 需要一个累加函数
- 需要返回一个柯里化的函数
function sum(...args: Array<number>) {
let count = args.reduce((previousValue: number, currentValue: number) => previousValue += currentValue, 0);
return count;
}
// 柯里化外套
const shellCurring = function (fn: Function): (args: number) => Function {
let argsInner: Array<any> = [];
const fun = function (args: number) {
if (args !== void 0) {
// 有参数,保存,并返回函数
argsInner.push(args);
return fun;
} else {
// 无参数,使用参入fn对数据处理
return fn.apply(this, argsInner);
}
}
return fun;
}
const curringSum = shellCurring(sum);
console.log(curringSum(100)(200)(300)())
总结
这些个函数大部分是在《JavaScript设计模式与开发实践》书中看到的案例,书中后面讲到的设计模式通俗易懂,可以去看看,还有函数内容以及书中的设计模式内容持续更新