1. 1. 高阶函数
  2. 2. 相关应用
    1. 2.1. 实现AOP
    2. 2.2. 函数柯里化
    3. 2.3. 反柯里化
    4. 2.4. 节流与防抖
    5. 2.5. 分时函数
    6. 2.6. 惰性加载函数
高阶函数

高阶函数

高级函数是指至少满足下列条件之一的函数。

  • 函数可以作为参数被传递

    实际应用场景:回调函数(callback)、常见的数组方法,如sort filter map reduce

  • 函数可以作为返回值输出

    实际应用场景:闭包等各种场景

相关应用

实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑无关的功能抽离出来,通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性(一个模块只负责一种功能),其次是可以很方便的复用代码。

代码如下:

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
Function.prototype.before = function (beforefn) {
const __self = this // 保存原函数的引用
return function () { // 返回包含了原函数和新函数的“代理”函数
beforefn.apply(this, arguments) // 执行新函数,修正this
return __self.apply(this, arguments) // 执行原函数
}
}

Function.prototype.after = function (afterfn) {
const __self = this
return function () {
const ret = __self.apply(this, arguments) // 执行原函数
afterfn.apply(this, arguments)
return ret
}
}

let func = function () {
console.log(2)
}

func = func.before(() => {
console.log(1)
}).after(() => {
console.log(3)
})

func() // 1 2 3

函数柯里化

柯里化又称部分求值,柯里化函数会接受一些参数,然后不会立即求值,而是继续返回一个新函数,将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性把所有传入的参数进行求值。

代码如下:

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
// 普通函数
function add (x, y) {
return x + y
}

add(3, 4) // 7

// 实现了柯里化的函数
// 接收参数,返回新函数,把参数传给新函数使用,最后求值
let add = function (x) {
return function (y) {
return x + y
}
}

add(3)(4) // 7

// 通用版
function curry (fn) {
function _c(restNum, args) {
return restNum === 0
? fn.apply(null, args) // 当存储的参数已达到原函数的参数数量时,执行原函数
: function (x) {
return _c(restNum - 1, [...args, x])
}
}
return _c(fn.length, [])
}

let add = curry((x, y) => {
return x + y
})

add(3)(4) // 7

函数柯里化在业务中的应用

  • 提高代码的通用性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function square(i) {
return i * i
}

function double(i) {
return i *= 2;
}

function map(handler, list) {
return list.map(handler)
}

// 数组的每一项平方
map(square, [1, 2, 3, 4, 5])
map(square, [6, 7, 8, 9, 10])
map(square, [10, 20, 30, 40, 50])
// ......

// 数组的每一项加倍
map(double, [1, 2, 3, 4, 5])
map(double, [6, 7, 8, 9, 10])
map(double, [10, 20, 30, 40, 50])

在以上例子中,创建了一个map通用函数,用于适应不同的应用场景,但是在例子中,反复的传入了相同的处理函数:square和double。

我们利用柯里化将其改造一下:

1
2
3
4
5
6
7
8
9
10
11
12
function curry (fn) {
const args = [].slice.call(arguments, 1)
return function () {
return fn.apply(null, [...args, ...arguments])
}
}

const mapSQ = curry(map, square)
mapSQ([1, 2, 3, 4, 5]) //[1, 4, 9, 16, 25]

const mapDB = curry(map, double)
mapDB([1, 2, 3, 4, 5]) // [ 2, 4, 6, 8, 10 ]

如此,便降低了代码的重复性。

  • 延迟执行:利用闭包的特点,缓存积累传入的参数,等到需要的时候再执行函数。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function curry (fn) {
let args = []
return function cb () {
if (arguments.length === 0) { // 当不传入参数时,调用原函数
return fn.apply(this, args)
} else {
args.push(...arguments) // 存储每次传入的参数
return cb
}
}
}

const curryAdd = curry((...args) => args.reduce((sum, single) => sum += single))

curryAdd(1)
curryAdd(2)
curryAdd(3)
curryAdd(4)
curryAdd() // 10
  • Function.prototype.bind 方法的实现
1
2
3
4
5
6
Function.prototype.bind = function (scope) {
const fn = this
return function () {
fn.apply(scope, arguments)
}
}

反柯里化

从字面意义上讲,反柯里化的意义和用法和柯里化正好相反,反柯里化的主要作用是使本来只有特定对象才适用的方法,扩展到更多的对象。例如我们经常使用call和apply去借用Array.prototype上的方法,使其适用的范围从数组扩大到类数组对象。

1
2
3
4
(function () {
Array.prototype.push.call(arguments, 4) // arguments为类数组对象,借用Array.prototype.push方法
console.log(arguments)
})(1, 2, 3)

反柯里化的函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.uncurrying = function () {
const self = this
// this就是Array.prototype.push
return function () {
// Array.prototype.push当作Function.prototype.call的this传进去
return Function.prototype.call.apply(self, arguments)
// arguments是[obj, 'first'],apply接受数组形式的传参
// 也就相当于Array.prototype.push.call(arguments)
// 即Array.prototype.push.call(obj, 'first')
}
}

const push = Array.prototype.push.uncurrying()

const obj = {}
push(obj, 'first')
console.log('obj', obj) // obj { '0': 'first', length: 1 }

节流与防抖

节流函数:让原先频繁触发的函数在间隔某一段时间内只执行一次。

常见的使用场景:

  • window.onresize 事件
  • mousemove 事件
  • 上传进度

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const throttle = function (fn, interval) {
const __self = fn // 保存需要被延迟执行的函数引用
let timer, // 定时器
firstTime = true // 是否第一次调用
return function () {
const args = arguments,
__me = this
if (firstTime) { // 如果是第一次调用,不需要延迟执行
__self.apply(__me, args)
return firstTime = false
}
if (timer) {
return false // 如果定时器还在,说明前一次延迟执行还没有完成
}
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
__self.apply(__me, args)
}, interval || 500)
}
}

防抖函数:指在事件被触发的某个时间间隔后再执行相应的回调,如果再这个时间间隔内事件再次被触发,则重新计时。

常见的使用场景:

  • input 输入事件所触发的ajax请求

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const debounce = function (fn, interval) {
const __self = fn
let timer
return function () {
const args = arguments,
__me = this
if(timer !== null) { // 在间隔内又被触发,则清空定时器,重新计时
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
__self.apply(__me, args)
}, interval || 500)
}
}

分时函数

分时函数的主要作用就是避免在短时间内执行太多任务,通常用于优化大量数据渲染的场景。

函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const timeChunk = function (ary, fn, count, duration) {
let timer
const start = function () {
for (let i = 0; i < Math.min(count || 1, ary.length); i ++) {
const obj = ary.shift()
fn(obj)
}
}
return function () {
timer = setInterval(() => {
if (ary.length === 0) {
return clearInterval(timer)
}
start()
}, duration || 500)
}
}

const render = timeChunk(new Array(10), () => { console.log('render') }, 5, 1000)

render()

惰性加载函数

由于浏览器之间的行为差异,经常会在函数中包含大量的if语句,以检查浏览器的特性,解决不同浏览器的兼容问题。比如,最常见的为dom节点添加事件的函数:

1
2
3
4
5
6
7
8
9
function addEvent (elem, type, handler) {
if (window.addEventListener) {
elem.addEventListener(type, handler, false)
} else if (window.attachEvent) {
elem.attachEvent('on' + type, handler)
} else {
elem['on' + type] = handler
}
}

每次调用addEvent函数的时候,它都要对浏览器所支持的能力进行检查,但其实,当页面加载完毕时,浏览器已经是一个固定的浏览器了,检查只需要做一次。我们可以使用立即执行函数来优化这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const addEvent = (function () {
if (window.addEventListener) {
return function (elem, type, handler) {
elem.addEventListener(type, handler, false)
}
} else if (window.attachEvent) {
return function (elem, type, handler) {
elem.attachEvent('on' + type, handler)
}
} else {
return function (elem, type, handler) {
elem['on' + type] = handler
}
}
})()

以上就是惰性加载的一种方式。所谓的惰性加载,就是函数的if分支只会执行一次

不过上述函数也存在一个缺点,也许我们从头到尾都没有使用过addEvent函数,那么执行这个函数就是一个多余的操作,并且也会延长页面ready的时间。

优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let addEvent = function (elem, type, handler) {
if (window.addEventListener) {
// 第一次进入时对函数进行重写
// 第二次进入时就不再进入条件分支
addEvent = function (elem, type, handler) {
elem.addEventListener(type, handler, false)
}
} else if (window.attachEvent) {
addEvent = function (elem, type, handler) {
elem.attachEvent('on' + type, handler)
}
} else {
addEvent = function (elem, type, handler) {
elem['on' + type] = handler
}
}
}

文章参考:
《JavaScript设计模式与开发实践》