前言
在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++;
}
};