柯里化 curring 是把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下参数并且返回结果的新函数的技术。

柯里化 -> 部分求值,返回接收剩余参数且返回结果的新函数。

特点:

  • 参数复用 - 复用最初函数的第一个参数
  • 提前返回 - 返回接收余下的参数且返回结果的新函数
  • 延迟执行 - 返回新函数,等待执行

应用

  • 兼容浏览器事件监听方法
  • 性能优化:防抖和节流
  • 兼容低版本IE bind方法等

事件监听

1
2
3
4
5
6
7
var addEvent = function(elm, type, fn, isCapture) {
if (window.addEventListener) {
elm.addEventListner(type, fn, isCapture)
} else if (window.attachEvent) {
elm.attachEvent('on' + type, fn)
}
}

当我们调用addEvent方法时,都会执行if else-if 进行一次兼容判断,其实这个判断是无必要的,用柯里化优化后如下:

1
2
3
4
5
6
7
8
9
10
11
var addEvent = function(elm, type, fn, isCapture) {
if (window.addEventListener) {
return function(elm, type, fn, isCapture) {
elm.addEventListen(type, fn, isCapture)
}
} else if (window.attachEvent) {
return function(elm, type, fn) {
elm.attachEvent('on' + type, fn)
}
}
}

该例就利用了柯里化提前返回和执行的特点:

  • 提前返回 - 使用函数立即调用进行一次兼容判断,返回兼容的事件绑定方法
  • 延迟执行 - 返回新函数,在新函数调用兼容的事件方法。等待addEvent新函数调用,延迟执行

防抖和节流

web开发中,事件resize, scroll, mousemove等属于高频事件。浏览器页面渲染帧率为60fps,大约16.67ms刷新一帧。 若事件触发频率大于显示帧率,则会发生掉帧、卡顿等现象,更坏地浏览器直接崩溃。

要解决高频事件的问题,其根本在于:

  • 高频事件处理函数,不应该含有复杂操作,如DOM操作和复杂计算(DOM操作一般会造成页面回流和重绘,使浏览器不断重新渲染页面)
  • 控制高频事件的触发频率

其中防抖和节流对高频事件进行优化的原理就是通过延迟执行,将多个间隔接近的函数执行合并成一次函数执行。

防抖 debounce

针对高频事件,防抖就是讲多个触发间隔接近的事件函数执行,合并成一次函数执行

实现防抖的关键点有两个:

  • 使用setTimeout延时器,传入延迟时间,将事件处理函数延迟执行,并且通过事件触发频率与延迟事件值比较,控制处理函数是否执行

  • 使用柯里化函数结合闭包思想,将执行状态保存在闭包中,返回新函数,在新函数中通过执行状态控制是否在滚动时执行处理函数

代码如下:

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
29
30
31
32
33
/*
* @param fn Function 事件处理函数
* @param delay Number 延迟时间
* @param isImmediate Boolean 是否滚动时立刻执行
* @return Function 事件处理函数
*/
var debound = function(fn, delay, isImmediate) {
// 使用闭包,保存执行状态,控制函数调用顺序
var timer

return function() {
var _args = [].slice.call(arguments),
context = this

clearTimeout(timer)

var _fn = function() {
timer = null
if (!isImmediate) fn.apply(context, _args)
}
// 是否滚动时立即执行
var callNow = !timer && isImmediate

timer = setTimeout(_fn, delay)

if (callNow) fn.apply(context, _args)
}
}
// 防抖使用如下:
var debounceScroll = debounce(function() {
// 事件处理函数,滚动时进行的处理
}, 100)
window.addEventListener('scroll', debounceScroll)

防抖技术仅靠传入延迟时间值的大小控制高频事件的触发频率。如果传入的延迟事件较大,则可能导致不触发事件处理函数,这时节流就派上用场了

节流 throttle

节流也是将多个触发间隔接近的事件函数执行,合并成一次函数执行,并且在指定时间内执行执行一次事件处理函数。

节流实现原理跟防抖类似,但是比防抖多了一次实函数执行判断,实现的关键点是:

  • 利用闭包存储了当前和上一次执行的时间戳,通过两次函数执行的时间差跟指定的延迟事件的比较,控制函数是否执行

代码如下:

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
29
30
31
32
33
34
35
36
/*
* @param fn Function 事件处理函数
* @param wait Number 延迟时间
* @return Function 事件处理函数
*/
var throttle = function(fn, wait) {
var timer, previous, now, diff

return function() {
var _args = [].slice.call(arguments),
context = this
// 存储当前时间戳
now = Date.now()

var _fn = function() {
// 存储上一次执行的时间戳
previous = Date.now()
timer = null
fn.apply(context, _args)
}
clearTimeout(timer)

if (previous !== undefined) {
// 时间差
diff = now - previous
if (diff >= wait) {
fn.apply(context, _args)
previous = now
} else {
timer = setTimeout(_fn, wait)
}
} else {
_fn()
}
}
}

拓展:节流和防抖都是用setTimeout实现的,改用window.requestAnimationFrame

实现起来更简单,性能更好,但不支持低版本IE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 解决requestAnimationFrame兼容
var reFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60)
}

// 柯里化封装
var refThrottle = function(fn) {
var isLocked
return function() {
var context = this
var _args = arguments

if (isLocked) return

isLocked = true
reFrame(function() {
isLocked = false
fn.apply(context, _args)
})
}
}

bind柯里化函数

函数的bind方法我们不陌生,但在低版本IE不兼容,若要实现兼容其关键点在于:

  • bind方法改变this指向,却不会执行原函数,那么我们可以利用柯里化延迟执行,参数复用和提前返回的特点,返回新函数,在新函数是用apply方法执行原函数

我们这里将bind方法封装为两种情况:

  1. 简单的bind方法封装,不考虑构造函数等,实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!Function.prototype.bind) {
Function.prototype.bind = function(context) {
if (context.toString() !== '[object Object]' && context.toString() !== '[object Window]') {
throw TypeError('context is not a Object')
}
var _this = this
var args = [].slice.call(arguments, 1)

return function() {
var _args = [].slice.call(arguments)
_this.apply(context, _args.concat(args))
}
}
}
  1. 复杂情况,考虑bind的任何用法,这里直接是用MDN的bind兼容方法:
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
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
}

var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(
this instanceof fNOP ? this : oThis,
// 获取调用时 fBound 的传参 .bind返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments))
)
}

// 维护原型关系
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype
}
fBound.prototype = new fNOP()

return fBound
}
}

要理解复杂的bind兼容方法,必须彻底理解以下四个基础知识

  • JS原型对象
  • 构造函数是用new操作符的过程
  • this的指向问题
  • 熟悉bind方法的是用场景

柯里化函数的封装

分析了柯里化的各种是用场景,我们来尝试一下封装一个简单的柯里化函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createCurry(fn) {
if (typeof fn !== 'function') {
throw TypeError('fn is not a function')
}
// 复用第一个参数
var args = [].slice.call(arguments, 1)
// 返回新函数
return function() {
// 收集剩余参数
var _args = [].slice.call(arguments)
// 返回结果
return fn.apply(this, args.concat(_args))
}
}

柯里化函数的特点如上注释所示:

  • 复用第一个参数
  • 返回新函数
  • 收集剩余参数
  • 返回结果