Skip to content

高阶函数

高阶函数在 JavaScript 中广泛使用。 如果你已经用 JavaScript 编程了一段时间,你可能已经在不知不觉中用过它们了。

什么是高阶函数

高阶函数需要至少满足下列条件中的一个

  • 接受一个或者多个函数作为输入
  • 输出一个函数

也就是说高阶函数是对其他函数进行操作的函数,操作可以是将它们作为参数,或者是返回它们。 简单来说,高阶函数是一个接收函数作为参数或将函数作为输出返回的函数。

函数作为参数传递

使用高阶函数

让我们看看一些内置高阶函数的例子,看看它与不使用高阶函数的方案对比如何。

  • Array.prototype.map

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

假设现在有个数字数组,想创建一个新数组,其中它的每个元素时第一个数组的对应元素的 2 倍,现在看看解决这个问题

js
// 不使用高阶函数
let arr1 = [1, 2, 3];
let arr2 = [];
for (let i = 0; i < arr1.length; i++) {
  arr.push(arr1[i] * 2)
}
console.log(arr2); // [2, 4, 6]

// 使用高阶函数 map
let arr1 = [1, 2, 3];
let arr2 = arr1.map(item => item * 2);
console.log(arr2); // [2, 4, 6]
  • Array.prototype.filter

filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

假设有一个包含分数的数组,现在想找到所有及格的分数。

js
// 不使用高阶函数
let arr1 = [75, 34, 56, 78, 99, 77, 65, 88, 65, 96];
let arr2 = [];
for (let i = 0; i < arr1.length; i++) {
  if (arr1[i] >= 60) {
    arr2.push(arr1[i])
  }
}
console.log(arr2); // [75, 78, 99, 77, 65, 88, 65, 96]

// 使用高阶函数 filter
let arr1 = [75, 34, 56, 78, 99, 77, 65, 88, 65, 96];
let arr2 = arr1.filter(item => item >= 60);
console.log(arr2); // [75, 78, 99, 77, 65, 88, 65, 96]

可以看到使用高阶函数使我们的代码更清晰简洁。

创建自己的高阶函数

上面已经看到了内置的一些高阶函数,现在让我们创建自己的高阶函数。下面自己实现一个 map 方法。

js
function map(arr, fn) {
  let res = [];
  for (let i = 0; i < arr.length; i++) {
    res.push(
      fn(arr[i])
    )
  }
  return res;
}

let arr1 = [1, 2, 3];
let arr2 = map(arr1, item => item * 2);
console.log(arr2); // [2, 4, 6]

上面的例子中,创建了一个高阶函数 map,它接收一个数组和一个回调函数作为参数。遍历传入的数组,在每一次迭代中调用 fn,并将当前元素作为参数传给 fn,然后把 fn 的返回结果存储到 res 中。循环结束之后,将 res 返回。

函数作为返回值输出

这个很好理解,就是返回一个函数,类似的场景也很多。其实从闭包的例子就可以看到高阶函数。

  • isType
js
function isType(type) {
  return function(val) {
    return Object.prototype.toString.call(val) === `[object] ${type}`;
  }
}

const isArray = isType('Array');
const isString = isType('String');

isArray([1, 2]); // true
isString({}); // false
  • 预置函数

它的实现原理也很简单,当达到条件时再执行回调函数

js
function after(time, cb) {
  return function() {
    if (--time === 0) {
      cb();
    }
  }
}
// 举个栗子吧,吃饭的时候,我很能吃,吃了三碗才能吃饱
let eat = after(3, function() {
  console.log('吃饱了');
});
eat();
eat();
eat(); // 吃饱了

应用

柯里化

函数节流

分时函数

节流函数为我们提供了一种限制函数被频繁调用的解决方案。

下面我们将遇到另外一个问题,某些函数是用户主动调用的,但是由于一些客观的原因,这些操作会严重的影响页面性能,此时我们需要采用另外的方式去解决。

如果我们需要在短时间内才页面中插入大量的 DOM 节点,那显然会让浏览器吃不消。可能会引起浏览器的假死,所以我们需要进行分时函数,分批插入。

js
/**
* 分时函数
* @param {Array} list 创建节点需要的数据
* @param {Function} fn 创建节点逻辑函数
* @param {Number} count 每一批节点的数量
*/
const timeChunk = function(list, fn, count = 1){
  let timer = null;
  const start = function(){
    let len = Math.min(count, list.length)
    for (let i = 0; i < len; i++) {
      let item = list.shift()
      // 对执行函数逐个进行调用
      fn(item)
    }
  }
  return function(){
    timer = setInterval(() => {
      if (list.length === 0) {
        return window.clearInterval(timer)
      }
      // 一批一批的插入
      start()
    },200)
  }
}

// 分时函数测试
const arr = []
for (let i = 0; i < 94; i++) {
  arr.push(i)
}
const renderList = timeChunk(arr, function(data){
  let div =document.createElement('div');
  div.innerHTML = data + 1;
  document.body.appendChild(div);
}, 20)
renderList()

惰性载入函数

因为浏览器的之间的行为差异,多数的 JavaScript 代码包含了大量的 if 判断语句,将执行引导到正确的代码中。

例如一个事件的绑定函数

js
function addEvent(el, type, handler) {
  if (window.addEventListener) {
    el.addEventListener(type, handler, false)
  } else {
    el.attachEvent(`on${type}`, handler)
  }
}

每次调用 addEvent 的时候,它都会对浏览器多支持的能力进行判断,即使每次调用时分支的结果都不会变:如果浏览器支持 addEventListener 或者 attachEvent,那它就一直支持了,那么这种判断就没有必要了。

即使只有一个 if 语句的代码,也肯定要比没有 if 语句的慢,所以如果 if 语句不必每次执行,那么代码就可以运行地更快一些。解决方案就是惰性载入的技巧。

惰性载入表示函数执行的分支仅发生一次。有两种方法可以实现惰性载入的方式:

  1. 在函数被调用时在处理函数。第一次调用的时候,该函数被覆盖为一个按合适方式执行的函数,这样接下来对原函数的调用就不需要经过执行的分支了。
js
function addEvent(el, type, handler) {
  if (window.addEventListener) {
    // 覆盖
    addEvent = function (el, type, handler) {
      el.addEventListener(type, handler, false)
    }
  } else {
    // 覆盖
    addEvent = function (el, type, handler) {
      el.attachEvent(`on${type}`, handler)
    }
  }
  addEvent(el, type, handler)
}

这个惰性载入,在 if 语句的每个分支都会对 addEvent 重新赋值,有效覆盖了原来的函数。最后一步是调用新赋值的函数。下一次调用 addEvent() 的时候,就会直接调用被分配的函数,而不需要进行判断。这样,在第一次调用时会损失一点性能。

  1. 在声明函数时就指定适当的函数
js
var addEvent = (function () {
  if (window.addEventListener) {
    return function (el, type, handler) {
      el.addEventListener(type, handler, false)
    }
  } else {
    return function (el, type, handler) {
      el.attachEvent(type, handler, false)
    }
  }
})()

通过创建一个自动执行函数,用以确定使用哪一个函数来实现。这样,在代码首次加载时会损失一点性能。

惰性载入的优点是只在执行分支代码时牺牲一点性能。至于那种方式更合适,就要看自己具体的需求了。这两种方式都可以避免执行不需要的代码。

参考

高阶函数 has loaded