前言

在JavaScript中,函数不仅可以被调用,还可以像普通变量一样被赋值、传参、返回,所以我们说JavaScript函数是JavaScript语言中的一等公民。如果一个函数可以作为另一个函数的参数传入,或者该函数反回一个函数,那么这个函数就被称为高阶函数(High Order Function)。

JavaScript中的高阶函数

其实JavaScript就定义了很多的高阶函数让我们,比如:

  • Array.prototype.map()
  • Array.prototype.reduce()
  • Array.prototype.every()
  • Array.prototype.some()
  • Array.prototype.filter()
  • ...

这些函数都可以接收一个函数作为参数

//计算数组和
function countSum(a, b) {
return a + b;
}
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(countSum);
console.log(sum); //15

这些高阶函数的使用方法可以自行百度/谷歌,在这里我还想介绍一个常用的JavaScript函数​​Array.prototype.sort()​​​,它是一个对数组进行​​原地排序​​的函数,我们试着将一个Number类型的数组进行排序:

const arr = [5, -1, 3, -6, 7, -1];
arr.sort();
console.log(arr); //[-1, -1, -6, 3, 5, 7]

咦,这个排序结果既不是升序也不是降序,究竟是怎么回事呢?这是因为使用​​sort()​​排序时,默认排序顺序是在将元素转换为字符串后,再比较它们的UTF-16代码单元值序列。所以我们比较的其实是Number类型转换为字符串后的UTF-16代码单元值序列呐。

其实​​sort()​​​还支持传入一个函数作为参数,让它支持特定类型的排序,这个传入函数的返回值和JAVA的​​java.lang.Comparable​​接口类似:

//升序排序
const arr = [5, -1, 3, -6, 7, -1];
arr.sort((a, b) => a - b);
console.log(arr); //[-6, -1, -1, 3, 5, 7]

//降序排序
const arr = [5, -1, 3, -6, 7, -1];
arr.sort((a, b) => b - a);
console.log(arr); //[7, 5, 3, -1, -1, -6]

//多维数组排序
const arr = [[1,2], [-1, 1], [1, 0], [-1, 0]];
arr.sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]);
console.log(arr); //[[-1, 0], [-1, 1], [1, 0], [1, 2]]

高阶函数基础

说了这么多,我们该怎么定义一个高阶函数呢?再次回顾一下高阶函数的概念:如果一个函数可以作为另一个函数的参数传入,或者该函数反回一个函数,那么这个函数就被称为高阶函数。

一个简单的forEach函数

我们假设一个情景,一款浏览器中对​​Array.prototype.forEach()​​方法不支持...

如果要考虑兼容这个浏览器,我们可以试着实现这个函数的polyfill。一个常见的polyfill模板如下:

(function () {
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (func) {
//在这里实现
}
}
})()

​forEach​​函数接收一个函数作为参数,这个函数的形参分别为当前遍历的值、当前遍历的数组下标以及当前的数组。当然forEach还支持传入第二个参数作为传入函数的一个上下文,因为我们实现的是一个简单的forEach,所以我们忽略第二个参数。比较完整的forEach的polyfil实现在文后。

对上面的细节进行补充,首先形参func不能为空且它是一个函数:

if(typeof func !== 'function') {throw new TypeError(func.toString() + `is not a function`)}

对数组进行遍历,并将相应的参数传入func中,注意​​forEach​​函数的没有返回值。

const arr = this;
const length = arr.length >>> 0; //无符号右移0位,保证length为正整数
let k = 0;
while(k < length) {
if(k in arr) {
func(arr[k], k, arr);
}
k++;
}

于是,一个简单的​​forEach​​函数就完成啦。

(function () {
if (!Array.prototype.forEach) {
Array.prototype.forEach = function (func) {
if (typeof func !== 'function') {
throw new TypeError(func.toString() + `is not a function`)
}
const arr = this;
const length = arr.length >>> 0; //无符号右移0位,保证length为正整数
let k = 0;
while (k < length) {
if (k in arr) {
func(arr[k], k, arr);
}
k++;
}
}
}
})()

总结一下,我们实现的这个函数有一个函数作为形参,我们并不关心这个形参的具体内容、具体实现,我们只是在​​forEach​​函数中直接调用了这个函数,也就是让这个函数“生效”。

HOF0

再来看一个例子:

function HOF0(func) {
const resFunc = function (...args) {
return func.apply(null, args);
}
return resFunc;
}

这个可以称为高阶函数的一个范式,首先HOF0函数传入一个函数func,然后HOF0返回一个匿名函数,匿名函数又返回func函数的调用。在忽略上下文的情况下,下面两者的调用是一致的:

function sum(a, b) {
return a + b;
}
let countSum = HOF0(sum); //返回的是一个函数
countSum(1, 2) === sum(1, 2);

那为什么要将它复杂化呢?别急,我们很快就要基于这个HOF0函数来进行应用了。

高阶函数进阶

在面试过程中,面试官经常会让我们手写节流、防抖这些函数,这些函数都是高阶函数。如果你掌握了上面的知识,那么手写这些函数简直是轻轻松松!

手写函数节流

函数节流(throttle):当持续触发事件时,保证一定时间内使用一次事件处理函数。

首先要保证一段时间内只执行一次,所以可以使用一个定时器来控制“次数”,其次在这段时间内还要触发一次事件处理函数,这个不就是要在函数内调用一次事件处理函数嘛!所以我们将上面的HOF0函数拿过来修改一下:

//delay为需要延迟的时间
function throttle(func, delay) {
const resFunc = function (...args) {
func.apply(null, args);
}
return resFunc;
}

事件调用的功能已经完成,接下来就是控制“间隔一段时间”再执行事件处理函数:把​​func.apply(null, args)​​​放到​​setTimeout​​​中,​​delay​​作为延迟的时间

//delay为需要延迟的时间
function throttle(func, delay) {
const resFunc = function (...args) {
setTimeout(() => {
func.apply(null, args);
}, delay)
}
return resFunc;
}

我们试着执行一下这个函数,发现它每一次调用都会执行,没有实现“间隔”。这是因为调用resFunc函数时每一次都会生成一个新的定时器,所以还需要阻止该函数在delay的时间内不再生成新的定时器,这里我们使用到了闭包。

//delay为需要延迟的时间
function throttle(func, delay) {
let timer = null;
const resFunc = function (...args) {
if(timer == null) {
timer = setTimeout(() => {
func.apply(null, args);
timer = null;
}, delay)
}
}
return resFunc;
}

这样子我们就实现一个高阶函数啦!示例代码:​​codepen.io/hengistchan…​

如果你对为什么返回的函数还能引用throttle函数的timer变量有疑问的话,可以去看一下关于闭包的知识。

手写函数防抖

函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

按照上面的分析,只需要改一下节流的代码即可。如果触发了事件,就把定时器清空,再创建一个新的定时器。

//delay为需要延迟的时间
function debounce(func, delay) {
let timer = null;
const resFunc = function (...args) {
if(timer != null) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(null, args);
}, delay)
}
return resFunc;
}

示例代码:​​codepen.io/hengistchan…​

总结

如果一个函数可以作为另一个函数的参数传入,或者该函数反回一个函数,那么这个函数就被称为高阶函数(High Order Function)。不只是函数防抖和函数节流,高阶函数在很多方面都有应用,比如说限制函数的执行次数、函数柯里化等。待我找到实习后再慢慢补上????。

附:一个较为完备的forEach的polyfill实现,在此基础上,也可以通过添加或删除一些代码实现更多的polyfill,比如map、reduce、some等。

Array.prototype.forEach = function (func, thisArg = window) {
if (this == null) throw new Error("");

if (typeof func !== 'function') throw new Error("is not a function");

//这里为什么要使用Object(this)呢?可以参考:
//https://stackoverflow.com/questions/66941001/in-the-array-prototype-finds-implementation-why-do-we-need-to-use-objetctthis
const O = Object(this);
const length = O.length >>> 0;
let k = 0;
while(k < length) {
if (k in O) {
func.call(thisArg, O[k], k, O);
}
k++;
}
};