阅读 360

JavaScript高阶函数

前言

在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++;
  }
};
复制代码

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情

文章分类
前端
文章标签