柯里化

柯里化又称为部分求值。一个 柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

简单来说就是实现这样的效果: add(1)(2, 3)(4)(5) = 15

实现一个简单的柯里化函数并不困难,我们要做只有两点

  1. 用闭包保存之前传入的参数
  2. 在每次传入参数后判断是否已经收集了足够多的参数,如果是就执行函数

来看看代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
*
* @param targetFun 要柯里化的函数
* @param targetLength 要收集的参数个数
* @param targetThis 函数执行时绑定的this
* @returns {wait}
*/
function curry(targetFun, targetLength, targetThis) {
// 需要收集的参数个数,收集足够后执行
targetLength = (targetLength >= 0 ? targetLength : 0) '' targetFun.length;
// 存储已经收集到的参数
const allArgs = [];
// 返回一个函数,这个函数会继续接收参数
const wait = function (...arg) {
// 存储参数
allArgs.push(...arg);
// 判断是否已经收集了足够多的参数,足够就执行函数
if (allArgs.length >= targetLength){
return targetFun.apply(targetThis, allArgs);
} else {
return wait;
}
};
return wait;
}

可以看到代码实现并不复杂,至于为什么要传入targetLength呢,一方面是为了更灵活地处理收集个数问题,另一个就是用Function.length判断参数个数可能会造成意料之外的结果

根据MDN的定义,函数参数会排除掉带收集运算符(…)的参数,并只统计第一个有默认数据的参数前的参数个数。

1
2
3
4
console.log((function(...args) {}).length); 
// 0, rest parameter is not counted(排除...带运算符的参数)
console.log((function(a, b = 1, c) {}).length);
// 1, only parameters before the first one with, 只统计第一个含默认值的参数前的参数

柯里化有什么应用呢,其实一时半会我也想不出来……因为这只是一种优化策略…..想到再补充

防抖

防抖函数是一种只在最后一次会触发防抖函数的操作结束一段时间后后才会执行的函数,举个通俗易懂的例子,在你使用百度搜索框的时候,如果你输入字符的速度比较快,就会发现候选词列表的更新是在你输入完一串此之后才更新的,而不是输入一个字符更新一次,这算是防抖函数一个很经典的应用。或者生活中的一个很经典的场景,公交车的车门是在所有人都进入后,等待一段时间后发现没有人继续上车才关闭的,这也算一种防抖。

防抖函数的实现思路比较简单,只要设置一个定时器,在定时器内部执行真正要执行的函数,如果防抖函数再次被执行,就取消定时器,然后再次设置一个相同的计时器重新开始计时。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 防抖
* @param handler 真正执行的函数
* @param delay 等待延时
* @returns {Function} 防抖函数
*/
function debounce(handler, delay) {
// 闭包存储计时器
let timer = null;
return function () {
const self = this;
const args = arguments;
// 如果防抖函数被再次执行,取消定时器
clearTimeout(timer);
timer = setTimeout(function () {
// 等待时间到达后才执行真正的函数
handler.apply(self,args);
},delay);
}
}

节流

节流的目的是,让一个函数被调用后,要到一段时间后才能再次被调用,防止过高频率的调用。具体的应用有,降低一些过于频繁的事件比如window.onresize,mousemove事件这样的触发频率,还有就是防止某些脚本(笑),因为之前听到过阿里工程师写脚本抢月饼券抢了几千张的故事233,这肯定是发AJAX的函数没做点什么处理

来看看节流函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 节流
* @param handler 要加工的函数
* @param time 被加工的函数执行后,需要等待多少秒才能继续执行
* @returns {Function} 加工后的函数
*/
function throttle(handler, time) {
let preTime = 0;
let curTime = 0;
return function (...args) {
// 执行时判断时间
curTime = new Date().getTime();
// 时间差大于等待时间后才执行
if (curTime - preTime > time){
handler.apply( this, args);
// 记录本次执行时间
preTime = curTime;
}
}
}

实现起来比较简单,但是有个问题,就是如果最后一次执行真正函数的时间和最后一次触发节流函数的时间差过短,就可能导致最后一次触发被忽略,这就有可能带来一些bug,比如要把窗口的大小实时显示在html中,如果最后一次绑定的onresize的真正的处理函数没有被执行,就可能导致显示的数值不对,所以我们要稍微处理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 节流
* @param handler 要加工的函数
* @param time 被加工的函数执行后,需要等待多少秒才能继续执行
* @returns {Function} 加工后的函数
*/
function throttle(handler, time) {
let preTime = 0;
let curTime = 0;
let timer = null;
return function (...args) {
// 执行时判断时间
curTime = new Date().getTime();
// 如果时间过短,可以认为不是最后一次操作,清空定时器
clearTimeout(timer);
// 时间差大于等待时间后才执行
if (curTime - preTime > time){
handler.apply( this, args);
// 记录本次执行时间
preTime = curTime;
} else {
// 假设当次是最后一次,设置定时器来防止最后一次操作不能触发真正的函数
timer = setTimeout(() => {
handler.apply(this, args);
}, time);
}
}
}

从代码中可以看出,如果该次代码没有执行,就设置一个定时器,如果下次触发节流函数的时间小于规定时间,取消定时器,重新计数,否则执行回调,这样就保证了最后一次调用一定可以执行到真正的回调函数。