【译】理解JavaScript的高阶函数

7,218 阅读7分钟

原文链接: Understanding Higher-Order Functions in JavaScript
原文作者: Sukhjinder Arora
译者: 进击的大葱
推荐理由: 本文详细介绍了什么是函数式编程以及如何写自己的高阶函数High-order functions。

如果你有学习过JavaScript, 你一定有听过高阶函数这个词。高阶函数虽然听起来比较复杂,可是实际上并不难。

JavaScript之所以很适合拿来进行函数式编程是因为它允许高阶函数的存在。

为了完全理解高阶函数的概念,你一定要先理解什么是函数式编程(Functional Programming)以及一等类函数(First-class Functions)的概念。

什么是函数式编程(Functional Programming)

用最简单的话来说,函数式编程就是将函数作为另外一个函数的参数或者返回值。在函数式编程的世界里面,我们用函数的方式进行思考和编码。

JavaScript,HasKell,Clojure,Scala和Erlang是一些支持函数式编程的语言。

一等类函数(First-class Functions)

如果你使用过JavaScript,你或许听说过JavaScript将函数作为一等公民进行对待。这是因为在JavaScript或者其他语言里面,函数也是对象。

在JavaScript中函数是一种特殊类型的对象。它们是Function对象。举个例子:

function greeting() {
    console.log('Hello World');
}

// 调用函数
greeting(); // 打印 'Hello World'

为了验证JavaScript的函数也是对象,我们可以对函数进行像对象一样的赋值操作:

greeting.lang = 'English';
// 输出 'English'
console.log(greeting.lang);

备注 - 虽然这样写没有任何错,不过给函数对象赋值是一个十分危险的做法。你不应该随便为函数对象添加任意的属性,如果你有这个需求请使用object。

在JavaScript里面,所有你可以对其他类型例如对象,字符串,或者数字进行的操作,你都可以对function进行。你可以将他们作为参数传递给另外的函数(回调函数),将它们赋值给其他变量。这就是为什么说在JavaScript里面函数是一等的了。

将函数赋值给变量

我们可以将函数赋值给变量,例如:

const square = function (x) {
    return x * x;
}

// 打印出25
square(5);

我们还可以将它继续传给其它的变量,例如:

const foo = square;

// 打印出36
foo(6);

将函数作为参数

我们可以将函数作为参数传递给其他函数。例如:

function formalGreeting() {
    console.log("How are you?");
}

function casualGreeting() {
    console.log("What's up?")
}

function greet(type, greetFormal, greetCasual) {
    if (type === 'formal') {
        greetFormal();
    } else if (type === 'casual') {
        greetCasual();
    }
}

// 打印 What's up
greet('casual', formalGreeting, casualGreeting)

现在我们知道什么是一等函数了,让我们再深入理解一下JavaScript里面什么是高阶函数。

高阶函数

高阶函数是那些操作其他函数的函数。用最简单的话来说,高阶函数就是一个将函数作为参数或者返回值的函数。

例如Array.prototype.map, Array.prototype.filterArray.prototype.reduce是JavaScript原生的高阶函数。

高阶函数实践

让我们看一下一些使用原生高阶函函数的例子,并将它们和不使用高阶函数的情况进行对比。

Array.prototype.map

map()函数会创建一个新的数组,数组里面的元素是传进来的函数(callback)调用原来数组相同位置的元素的返回值。

传给map()的回调函数callback接收三个参数:elementindexarray

让我们来看一些例子:

例子1

假设我们现在有一个整形数组,然后想要新建一个数组,新建的数组的元素是原来数组各个位置的数字的两倍。让我们看一下如何用非高阶函数的方法来解决问题:

不利用高阶函数
const arr1 = [1, 2, 3];
const arr2 = [];

for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] * 2);
}

// 打印出 [2, 4, 6]
console.log(arr2)
使用高阶函数map
const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item * 2;
});
console.log(arr2);

我们可以用更简洁的箭头函数:

const arr1 = [1, 2, 3];
const arr2 = arr1.map(item => item * 2);
console.log(arr2);
例子2

假设我们现在有一个存储人们出生年份的数组,想要获得一个人们年龄的数组。

不使用高阶函数
const birthYear = [1975, 1997, 2002, 1995, 1985];
const ages = [];
for(let i = 0; i < birthYear.length; i++) {
  let age = 2018 - birthYear[i];
  ages.push(age);
}
// 打印 [ 43, 21, 16, 23, 33 ]
console.log(ages);
使用高阶函数map
const birthYear = [1975, 1997, 2002, 1995, 1985];
const ages = birthYear.map(year => 2018 - year);
// 打印 [ 43, 21, 16, 23, 33 ]
console.log(ages);

Array.prototype.filter

filter()函数创建一个新的数组,数组里面存储原数组里面可以通过传进来的callback测试的元素。传给filter()的回调函数接收三个参数:element, indexarray

让我们看一下例子:

例子1

假设我们现在有一个person对象数组,数组里面的person有name和age这两个属性。我们想要新建一个数组,这个数组只包含那些那些已经成年的人(年龄大于或者等于18岁)。

不利用高阶函数
const persons = [
  { name: 'Peter', age: 16 },
  { name: 'Mark', age: 18 },
  { name: 'John', age: 27 },
  { name: 'Jane', age: 14 },
  { name: 'Tony', age: 24},
];
const fullAge = [];
for(let i = 0; i < persons.length; i++) {
  if(persons[i].age >= 18) {
    fullAge.push(persons[i]);
  }
}
console.log(fullAge);
使用高阶函数filter
const persons = [
  { name: 'Peter', age: 16 },
  { name: 'Mark', age: 18 },
  { name: 'John', age: 27 },
  { name: 'Jane', age: 14 },
  { name: 'Tony', age: 24},
];
const fullAge = persons.filter(person => person.age >= 18);
console.log(fullAge);

Array.prototype.reduce

reduce方法用被调用数组的元素依次作为参数调用传进来的callback然后产生一个返回值。reduce函数接收两个参数:1) reducer函数(callback), 2) 一个可选的参数intialValue作为初始值。

reducer函数接收四个参数:accumulator, currentValue, currentIndexsourceArray

如果有初始值initialValue,reducer第一次被调用的时候accumulator等于initialValue而且currentValue等于数组的第一个元素。

如果没有初始值initialValue, reducer第一次被调用的时候accumulator等于数组里面的第一个元素,currentValue等于数组的第二个元素。

例子1

假设我们现在要对一个数组求和。

使用高阶函数reduce
const arr = [5, 7, 1, 8, 4];
const sum = arr.reduce(function(accumulator, currentValue) {
  return accumulator + currentValue;
});
// 打印 25
console.log(sum);

reducer会用数组里面的元素currentValue依次执行,accumulator保留着上次从reducer函数返回的结果。最后一次reducer调用的结果作为reduce函数的返回值并被存储在sum变量里面。

我们也可以为这个方法提供一个初始值:

const arr = [5, 7, 1, 8, 4];
const sum = arr.reduce(function(accumulator, currentValue) {
  return accumulator + currentValue;
}, 10);
// 打印出 35
console.log(sum);
不利用高阶函数
const arr = [5, 7, 1, 8, 4];
let sum = 0;
for(let i = 0; i < arr.length; i++) {
  sum = sum + arr[i];
}
// 打印出 25
console.log(sum);

从上面的例子你可以看出,使用高阶函数可以使我们的代码更加干净,准确和简洁。

创建我们自己的高阶函数

到现在为止我们已经看了几个JavaScript原生的高阶函数。现在让我们来创建自己的高阶函数。

让我们想象一下JavaScript没有自己原生的map方法。我们可以自己用高阶函数的方法来实现一个类似功能的函数。

假设我们现在有一个字符串的数组,我们想把这个数组转换成一个整形的数组,这个数组里面的元素是原来数组对应位置的字符串的长度。

const strArray = ['JavaScript', 'Python', 'PHP', 'Java', 'C'];
function mapForEach(arr, fn) {
  const newArray = [];
  for(let i = 0; i < arr.length; i++) {
    newArray.push(
      fn(arr[i])
    );
  }
  return newArray;
}
const lenArray = mapForEach(strArray, function(item) {
  return item.length;
});
// 打印出 [ 10, 6, 3, 4, 1 ]
console.log(lenArray);

上面的例子中,我们创建了一个高阶函数mapForEach,这个函数接收一个数组和回调函数fn作为参数。这个高阶函数循环遍历数组里面的每一个元素,将各个元素作为参数调用fn,并将fn的返回值用newArray.push方法存储在newArray中。

回调函数fn接收原数组的元素作为参数并返回该元素的长度,这个长度会被存储在newArray里面。当for循环结束后,新的数组newArray会被返回并赋值给lenArray

结论

我们学习了什么是高阶函数以及了解了一些原生的高阶函数。我们也学习了如何创建自己的高阶函数。

简而言之,高阶函数就是一个接受其他函数作为输入甚至可以返回其它函数的函数。高阶函数和普通函数其实是一样的,不过它有一个额外的能力就是接受函数作为参数,或者返回值是函数