【JS必知必会】高阶函数详解与实战

前言

一道经典面试题:javascript

//JS实现一个无限累加的add函数
add(1)  //1 
add(1)(2)  //3
add(1)(2)(3)  //6

当你们看到这个面试题的时候,可否在第一时间想到使用高阶函数实现?想到在实际项目开发过程当中,用到哪些高级函数?有没有想过本身创造一个高阶函数呢?开始本篇文章的学习java

文章已同步都github博客地址:
程序员成长指北技术栈博客地址node

高阶函数定义

高阶函数英文叫 Higher-order function。高阶函数是对其余函数进行操做的函数,操做能够是将它们做为参数,或者返回它们。简单总结为高阶函数是一个接收函数做为参数或者将函数做为返回输出的函数。git

函数做为参数状况

Array.prototype.mapArray.prototype.filterArray.prototype.reduceArray.prototype.sort是JavaScript中内置的高阶函数。它们接受一个函数做为参数,并应用这个函数到列表的每个元素。下面是一些内置高阶函数的具体说明讲解,以及和不使用高阶函数状况下的对比程序员

Array.prototype.map

map()(映射)方法最后生成一个新数组,不改变原始数组的值。其结果是该数组中的每一个元素都调用一个提供的函数后返回的结果。github

array.map(callback,[ thisObject]);

callback(回调函数)面试

[].map(function(currentValue, index, array) {
    // ...
});

传递给 map 的回调函数(callback)接受三个参数,分别是currentValue——正在遍历的元素、index(可选)——元素索引、array(可选)——原数组自己,除了 callback 以外还能够接受 this 值(可选),用于执行 callback 函数时使用的this 值。算法

来个简单的例子方便理解,如今有一个数组[1,2,3,4],咱们想要生成一个新数组,其每一个元素皆是以前数组的两倍,那么咱们有下面两种使用高阶和不使用高阶函数的方式来实现。后端

不使用高阶函数

// koala
const arr1 = [1, 2, 3, 4];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
  arr2.push( arr1[i] * 2);
}

console.log( arr2 );
// [2, 4, 6, 8]
console.log( arr1 );
// [1, 2, 3, 4]

使用高阶函数

// kaola
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(item => item * 2);

console.log( arr2 );
// [2, 4, 6, 8]
console.log( arr1 );
// [1, 2, 3, 4]

map高阶函数注意点

callback须要有return值,不然会出现全部项映射为undefind;数组

// kaola
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(item => {});

console.log( arr2 );
// [ undefined, undefined, undefined, undefined ]
console.log( arr1 );
// [1, 2, 3, 4]

map高阶函数对应的一道经典面试题

//输出结果
["1", "2", "3"].map(parseInt);

看了这道题不知道会不会有大多数开发者认为输出结果是[1,2,3],错误

正确的输出结果为:

[1,NaN,NaN]
分析与讲解

由于mapcallback函数有三个参数,正在遍历的元素, 元素索引(index), 原数组自己(array)。parseInt有两个参数,string和radix(进制),注意第二个参数进制当为0或者没有参数的时候,parseInt()会根据string来判断数字的基数。当忽略参数 radix , JavaScript 默认数字的基数以下:

  • 若是 string 以 "0x" 开头,parseInt() 会把 string 的其他部分解析为十六进制的整数。
  • 若是 string 以 0 开头,那么 ECMAScript v3 容许 parseInt() 的一个实现把其后的字符解析为八进制或十六进制的数字。
  • 若是 string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数。

只传入parseInt的话,map callback会自动忽略第三个参数array。而index参数不会被忽略。而不被忽略的index(0,1,2)就会被parseInt当作第二个参数。

将其拆开看:

parseInt("1",0);//上面说过第二个参数为进制,因此"1",radix为0上面提到过,会忽略,根据string 以 1 ~ 9 的数字开头,parseInt() 将把它解析为十进制的整数1。
parseInt("2",1);//此时将2转为1进制数,因为超过进制数1,因此返回NaN。
parseInt("3",2);//此时将3转为2进制数,因为超过进制数1,因此返回NaN。

因此最终的结果为[1,NaN,NaN]

那么若是想要获得[1,2,3]该怎么写。

["1","2","3"].map((x)=>{
    return parseInt(x);
});

也能够简写为:
["1","2","3"].map(x=>parseInt(x));

这样写为何就能返回想要的值呢?由于,传一个完整函数进去,有形参,有返回值。这样就不会形成由于参数传入错误而形成结果错误了,最后返回一个经纯函数处理过的新数组。

Array.prototype.reduce

reduce() 方法对数组中的每一个元素执行一个提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。传递给 reduce 的回调函数(callback)接受四个参数,分别是累加器 accumulatorcurrentValue——正在操做的元素、currentIndex(可选)——元素索引,可是它的开始会有特殊说明、array(可选)——原始数组自己,除了 callback 以外还能够接受初始值 initialValue 值(可选)。

  • 若是没有提供 initialValue,那么第一次调用 callback 函数时,accumulator 使用原数组中的第一个元素,currentValue 便是数组中的第二个元素。 在没有初始值的空数组上调用 reduce 将报错。
  • 若是提供了 initialValue,那么将做为第一次调用 callback 函数时的第一个参数的值,即 accumulator,currentValue 使用原数组中的第一个元素。

例子,如今有一个数组 [0, 1, 2, 3, 4],须要计算数组元素的和,需求比较简单,来看下代码实现。

不使用高阶函数

//koala
const arr = [0, 1, 2, 3, 4];
let sum = 0;
for (let i = 0; i < arr.length; i++) {
  sum += arr[i];
}

console.log( sum );
// 10
console.log( arr );
// [0, 1, 2, 3, 4]

使用高阶函数

无 initialValue 值

const arr = [0, 1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator + currentValue;
});

console.log( sum );
// 10
console.log( arr );
// [0, 1, 2, 3, 4]

上面是没有 initialValue 的状况,代码的执行过程以下,callback 总共调用四次。

callback accumulator currentValue currentIndex array return value
first call 0 1 1 [0, 1, 2, 3, 4] 1
second call 1 2 2 [0, 1, 2, 3, 4] 3
third call 3 3 3 [0, 1, 2, 3, 4] 6
fourth call 6 4 4 [0, 1, 2, 3, 4] 10

有 initialValue 值

咱们再来看下有 initialValue 的状况,假设 initialValue 值为 10,咱们看下代码。

//koala
const arr = [0, 1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue, currentIndex, array) => {
  return accumulator + currentValue;
}, 10);

console.log( sum );
// 20
console.log( arr );
// [0, 1, 2, 3, 4]

代码的执行过程以下所示,callback 总共调用五次。

callback accumulator currentValue currentIndex array return value
first call 10 0 0 [0, 1, 2, 3, 4] 10
second call 10 1 1 [0, 1, 2, 3, 4] 11
third call 11 2 2 [0, 1, 2, 3, 4] 13
fourth call 13 3 3 [0, 1, 2, 3, 4] 16
fifth call 16 4 4 [0, 1, 2, 3, 4] 20

Array.prototype.filter

filter(过滤,筛选) 方法建立一个新数组,原始数组不发生改变。

array.filter(callback,[ thisObject]);

其包含经过提供函数实现的测试的全部元素。接收的参数和 map 是同样的,filter的callback函数须要返回布尔值true或false. 若是为true则表示经过啦!若是为false则失败,其返回值是一个新数组,由经过测试为true的全部元素组成,若是没有任何数组元素经过测试,则返回空数组。

来个例子介绍下,如今有一个数组 [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4],咱们想要生成一个新数组,这个数组要求没有重复的内容,即为去重。

不使用高阶函数

const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
  if (arr1.indexOf( arr1[i] ) === i) {
    arr2.push( arr1[i] );
  }
}
console.log( arr2 );
// [1, 2, 3, 5, 4]
console.log( arr1 );
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]

使用高阶函数

const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = arr1.filter( (element, index, self) => {
    return self.indexOf( element ) === index;
});

console.log( arr2 );
// [1, 2, 3, 5, 4]
console.log( arr1 );
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]

filter注意点说明

callback在过滤测试的时候,必定要是Boolean值吗?
例子:

var arr = [0, 1, 2, 3];
var arrayFilter = arr.filter(function(item) {
    return item;
});
console.log(arrayFilter); // [1, 2, 3]

经过例子能够看出:过滤测试的返回值只要是弱等于== true/false就能够了,而非非得返回 === true/false.

Array.prototype.sort

sort() 方法用原地算法对数组的元素进行排序,并返回数组,该排序方法会在原数组上直接进行排序,并不会生成一个排好序的新数组。排序算法如今是稳定的。默认排序顺序是根据字符串Unicode码点。

// 语法
arr.sort([compareFunction])

compareFunction参数是可选的,用来指定按某种顺序进行排列的函数。注意该函数有两个参数:

参数1:firstEl

第一个用于比较的元素。

参数2:secondEl

第二个用于比较的元素。看下面的例子与说明:

// 未指明compareFunction函数

['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];

// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

// 没法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
//正确的结果
[6, 8, 1, 2].sort(); // [1, 2,6, 8]

// 指明compareFunction函数
'use strict';
var arr = [10, 20, 1, 2];
    arr.sort(function (x, y) {
        if (x < y) {
            return -1;
        }
        if (x > y) {
            return 1;
        }
        return 0;
    });
console.log(arr); // [1, 2, 10, 20]

若是没有指明 compareFunction ,那么元素会按照转换为的字符串的诸个字符的Unicode位点进行排序。例如 "Banana" 会被排列到 "cherry" 以前。当数字按由小到大排序时,10 出如今 2 以前,但由于(没有指明 compareFunction),比较的数字会先被转换为字符串,因此在Unicode顺序上 "10" 要比 "2" 要靠前。

若是指明了 compareFunction ,那么数组会按照调用该函数的返回值排序。即 a 和 b 是两个将要被比较的元素:

  • 若是 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 以前;
  • 若是 compareFunction(a, b) 等于 0 , a 和 b 的相对位置不变。备注: ECMAScript 标准并不保证这一行为,并且也不是全部浏览器都会遵照(例如 Mozilla 在 2003 年以前的版本);
  • 若是 compareFunction(a, b) 大于 0 , b 会被排列到 a 以前。

compareFunction(a, b) 必须老是对相同的输入返回相同的比较结果,不然排序的结果将是不肯定的。

sort排序算法的底层实现

看了上面sort的排序介绍,我想小伙伴们确定会对sort排序算法的内部实现感兴趣,我在sf上面搜了一下,发现有些争议。因而去查看了V8引擎的源码,发如今源码中的710行

源码地址: https://github.com/v8/v8/blob...
// In-place QuickSort algorithm.
// For short (length <= 22) arrays, insertion sort is used for efficiency.

V8 引擎 sort 函数只给出了两种排序 InsertionSortQuickSort数量小于等于22的数组使用 InsertionSort,比22大的数组则使用 QuickSort,有兴趣的能够看看具体算法实现。

注意:不一样的浏览器引擎可能算法实现并不一样,我这里只是查看了V8引擎的算法实现,有兴趣的小伙伴能够查看下其余开源浏览器具体sort的算法实现。

如何改进排序算法实现数字正确排序呢?

对于要比较数字而非字符串,比较函数能够简单的以 a 减 b,以下的函数将会将数组升序排列,降序排序则使用b-a。

let compareNumbers= function (a, b) {
    return a - b;
}
let koala=[10, 20, 1, 2].sort(compareNumbers)

console.log(koala);
// [1 , 2 , 10 , 20]

函数做为返回值输出

返回一个函数,下面直接看两个例子来加深理解。

isType 函数

咱们知道在判断类型的时候能够经过Object.prototype.toString.call 来获取对应对象返回的字符串,好比:

let isString = obj => Object.prototype.toString.call( obj ) === '[object String]';

let isArray = obj => Object.prototype.toString.call( obj ) === '[object Array]';

let isNumber = obj => Object.prototype.toString.call( obj ) === '[object Number]';

能够发现上面三行代码有不少重复代码,只须要把具体的类型抽离出来就能够封装成一个判断类型的方法了,代码以下。

let isType = type => obj => {
  return Object.prototype.toString.call( obj ) === '[object ' + type + ']';
}

isType('String')('123');        // true
isType('Array')([1, 2, 3]);    // true
isType('Number')(123);            // true

这里就是一个高阶函数,由于 isType 函数将 obj => { ... } 这一函数做为返回值输出。

add求和函数

前言中的面试题,用 JS 实现一个无限累加的函数 add,示例以下:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6

分析面试题的结构,都是将函数做为返回值输出,而后接收新的参数并进行计算。

咱们知道打印函数时会自动调用 toString()方法(若是不知道的能够去看个人这篇文章),函数 add(a) 返回一个sum(b)函数,函数 sum() 中累加计算 a = a + b,只须要重写sum.toString()方法返回变量 a 就能够了。

function add(a) {
    function sum(b) { // 使用闭包
        a = a + b; // 累加
        return sum;
     }
     sum.toString = function() { // 重写toString()方法
        return a;
    }
     return sum; // 返回一个函数
}

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6

如何本身建立高阶函数

前面讲了语言中内置的各类高阶函数。知道了到底啊什么是高阶函数,有哪些类型的高阶函数。那么让咱们本身建立一个高阶函数吧!

假设 JavaScript 没有原生的 map 方法。 咱们本身构建个相似map的高阶函数,从而建立咱们本身的高阶函数。
假设咱们有一个字符串数组,咱们但愿把它转换为整数数组,其中每一个元素表明原始数组中字符串的长度。

const strArray=['JavaScript','PHP','JAVA','C','Python'];
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;
});

console.log(lenArray);//[10,3,4,1,6]

代码分析讲解:

咱们建立了一个高阶函数 mapForEach ,它接受一个数组和一个回调函数 fn。 它循环遍历传入的数组,并在每次迭代时在 newArray.push 方法调用回调函数 fn 。

回调函数 fn 接收数组的当前元素并返回该元素的长度,该元素存储在 newArray 中。 for 循环完成后,newArray 被返回并赋值给 lenArray。

总结

咱们已经了解了高阶函数和一些内置的高阶函数,还学习了如何建立本身的高阶函数。简而言之,高阶函数是一个能够接收函数做为参数,甚至返回一个函数的函数。 它就像常规函数同样,只是多了接收和返回其余函数的附加能力,即参数和输出。

公众号技术栈路线

你们好,我是koala,公众号「程序员成长指北」做者,这篇文章是【JS必知必会系列】的高阶函数讲解。目前在作一个node后端高级工程师进阶路线,加入咱们一块儿学习吧!

16b8a3c3064ef334?w=1576&h=800&f=png&s=330091

加入咱们

16b8a3d23a52b7d0?w=940&h=400&f=jpeg&s=217901

相关文章
相关标签/搜索