【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

相關文章
相關標籤/搜索