閉包詳解二:JavaScript中的高階函式

lce_shou發表於2018-06-05

:文章最末尾有個人公眾號二維碼,會分享更多技術文章等,敬請關注

本文講解的高階函式是之前講解的閉包的續集,所以在學習高階函式之前,一定要確保對閉包以及作用域的概念已經有了解:

理解抽象

引出抽象的概念

有Java、C#等開發經驗的同學對程式碼抽象的思想一定不會陌生,抽象類、介面平時寫的非常多,但是對於一直都從事前端開發的同學來說,“抽象”這個詞就比較陌生了,畢竟JavaScript中沒有abstract、interface。

但是JS中肯定是有程式碼抽象的思想的,只不過是形式上和Java等語言不同罷了!

先來看Java中的一個抽象類:

public abstract class SuperClass {
  public abstract void doSomething();
}
複製程式碼

這是Java中的一個類,類裡面有一個抽象方法doSomething,現在不知道子類中要doSomething方法做什麼,所以將該方法定義為抽象方法,具體的邏輯讓子類自己去實現。

建立子類去實現SuperClass:

public class SubClass  extends SuperClass{
  public void doSomething() {
    System.out.println("say hello");
  }
}
複製程式碼

SubClass中的doSomething輸出字串“say hello”,其他的子類會有其他的實現,這就是Java中的抽象類與實現。

那麼JS中的抽象是怎麼樣的,最為經典的就是回撥函式了:

function createDiv(callback) {
  let div = document.createElement('div');
  document.body.appendChild(div);
  if (typeof callback === 'function') {
    callback(div);
  }
}
createDiv(function (div) {
  div.style.color = 'red';
})
複製程式碼

這個例子中,有一個createDiv這個函式,這個函式負責建立一個div並新增到頁面中,但是之後要再怎麼操作這個div,createDiv這個函式就不知道,所以把許可權交給呼叫createDiv函式的人,讓呼叫者決定接下來的操作,就通過回撥的方式將div給呼叫者。

這也是體現出了抽象,既然不知道div接下來的操作,那麼就直接給呼叫者,讓呼叫者去實現。 和Java中抽象類中的抽象方法的思想是一樣的。

總結一下抽象的概念:抽象就是隱藏更具體的實現細節,從更高的層次看待我們要解決的問題

陣列中的遍歷抽象

在程式設計的時候,並不是所有功能都是現成的,比如上面例子中,可以建立好幾個div,對每個div的處理都可能不一樣,需要對未知的操作做抽象,預留操作的入口,作為一名程式設計師,我們需要具備這種在恰當的時候將程式碼抽象的思想。

接下來看一下ES5中提供的幾個陣列操作方法,可以更深入的理解抽象的思想,ES5之前遍歷陣列的方式是:

var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < arr.length; i++) {
  var item = arr[i];
  console.log(item);
}
複製程式碼

仔細看一下,這段程式碼中用for,然後按順序取值,有沒有覺得如此操作有些不夠優雅,為出現錯誤留下了隱患,比如把length寫錯了,一不小心複用了i。既然這樣,能不能抽取一個函式出來呢?最重要的一點,我們要的只是陣列中的每一個值,然後操作這個值,那麼就可以把遍歷的過程隱藏起來:

function forEach(arr, callback) {
  for (var i = 0; i < arr.length; i++) {
    var item = arr[i];
    callback(item);
  }
}
forEach(arr, function (item) {
  console.log(item);
});
複製程式碼

以上的forEach方法就將遍歷的細節隱藏起來的了,把使用者想要操作的item返回出來,在callback還可以將i、arr本身返回:callback(item, i, arr)

JS原生提供的forEach方法就是這樣的:

arr.forEach(function (item) {
  console.log(item);
});
複製程式碼

跟forEach同族的方法還有map、some、every等。思想都是一樣的,通過這種抽象的方式可以讓使用者更方便,同事又讓程式碼變得更加清晰。

抽象是一種很重要的思想,讓可以讓程式碼變得更加優雅,並且操作起來更方便。在高階函式中也是使用了抽象的思想,所以學習高階函式得先了解抽象的思想

高階函式

什麼是高階函式

至少滿足以下條件的中的一個,就是高階函式:

  • 將其他函式作為引數傳遞

  • 將函式作為返回值

簡單來說,就是一個函式可以操作其他函式,將其他函式作為引數或將函式作為返回值。我相信,寫過JS程式碼的同學對這個概念都是很容易理解的,因為在JS中函式就是一個普通的值,可以被傳遞,可以被返回。

引數可以被傳遞,可以被返回,對Java等語言開發的同學理解起來可能會稍微麻煩一些,因為Java語言沒有那麼的靈活,不過Java8的lambda大概就是這意思;

函式作為引數傳遞

函式作為引數傳遞就是我們上面提到的回撥函式,回撥函式在非同步請求中用的非常多,使用者想要在請求成功後利用請求回來的資料做一些操作,但是又不知道請求什麼時候結束。

用jQuery來發一個Ajax請求:

function getDetailData(id, callback) {
  $.ajax('http://xxxxyyy.com/getDetailData?' + id, function (res) {
    if (typeof callback === 'function') {
      callback(res);
    }
  });
}
getDetailData('78667', function (res) {
  // do some thing
});
複製程式碼

類似Ajax這種操作非常適合用回撥去做,當一個函式裡不適合執行一些具體的操作,或者說不知道要怎麼操作時,可以將相應的資料傳遞給另一個函式,讓另一個函式來執行,而這個函式就是傳遞進來的回撥函式。

另一個典型的例子就是陣列排序

函式作為值返回

在判斷資料型別的時候最常用的是typeof,但是typeof有一定的侷限性,比如:

console.log(typeof []); // 輸出object
console.log(typeof {}); // 輸出object
複製程式碼

判斷陣列和物件都是輸出object,如果想要更細緻的判斷應該要使用Object.prototype.toString

console.log(Object.prototype.toString.call([])); // 輸出[object Array]
console.log(Object.prototype.toString.call({})); // 輸出[object Object]
複製程式碼

基於此,我們可以寫出判斷物件、陣列、數字的方法:

function isObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}
function isArray(arr) {
  return Object.prototype.toString.call(arr) === '[object Array]';
}
function isNumber(number) {
  return Object.prototype.toString.call(number) === '[object Number]';
}
複製程式碼

我們發現這三個方法太像了,可以做一些抽取:

function isType(type) {
  return function (obj) {
    return Object.prototype.toString.call(obj) === '[object ' + type + ']';
  }
}
var isArray = isType('Array');
console.log(isArray([1,2]));
複製程式碼

這個isType方法就是高階函式,該函式返回了一個函式,並且利用閉包,將程式碼變得優雅。

高階函式的應用

lodash中的使用

高階函式在平時的開發中用的非常多,只是有時候你不知道你的這種用法就是高階函式,在一些開源的類庫中也用的很多,比如很有名的 lodash,挑其中一個before函式:

function before(n, func) {
  let result
  if (typeof func != 'function') {
    throw new TypeError('Expected a function')
  }
  return function(...args) {
    if (--n > 0) {
      result = func.apply(this, args)
    }
    if (n <= 1) {
      func = undefined
    }
    return result
  }
}
複製程式碼

在before函式中,同時有用到將函式當做傳遞進來,又返回了一個函式,這是一個很經典的高階函式的例子。

看一下該程式碼可以怎麼用吧:

jQuery(element).on('click', before(5, addContactToList))
複製程式碼

所以before函式就是讓某個方法最多呼叫n次。

:before函式程式碼不難,使用也不難,但就是這麼一個簡單的工具方法需要了解的知識點有:作用域、閉包、高階函式,所以說知識點都是連貫的,接下來要寫的JavaScript設計模式系列,同樣也要用到這些知識。

函式節流

在寫程式碼的時候,大多數情況都是由我們自己主動去呼叫函式。不過在有一些情況下,函式的呼叫不是由使用者直接控制的,在這種情況下,函式有可能被廢除頻繁的呼叫,從而造成效能問題。

Element-UI 中,有一個 el-autocomplete 元件,該元件可以在使用者輸入的時候在輸入框下方列出相關輸入項:

autocomplete.png

其實就是可以在使用者輸入的時候,可以用已經輸入的內容做搜尋,餓了麼在實現該元件的時候是利用input元件,並且監聽使用者的輸入:

實現autocomplete.png

用input事件去監聽使用者輸入的話,使用者輸入的每一個字都會觸發該方法,如果是要用輸入的內容去做網路搜尋,使用者輸入的每一字都搜尋的話,觸發的頻率太高了,效能消耗就有點大了,而且在網路比較差的情況下使用者體驗也比較不好。

餓了麼實現該元件的時候當然也考慮到了這些問題,用的是業界比較通用的做法→節流,就是當輸入後,延遲一段時間再去執行搜尋,如果該次延遲執行還沒有完成的話,就忽略接下來搜尋的請求。

看一下其實現:

實現節流.png

autocomplete的節流思想就是剛才說的那種,並且用了 throttle-debounce 這個工具庫,其實現就是利用高階函式,有興趣的同學可以看它的原始碼:https://github.com/niksy/throttle-debounce,程式碼並不複雜。

高階函式還有其他的用法,比如用在設計模式中等,這些內容將會在後面詳細介紹。

特別注意

可以關注我的公眾號:icemanFE,接下來持續更新技術文章!

公眾號.png

相關文章