1. 程式人生 > 實用技巧 >你也許不知道的javascript高階函式

你也許不知道的javascript高階函式

前言

高階函式是對其他函式進行操作的函式,可以將它們作為引數或通過返回它們。簡單來說,高階函式是一個函式,它接收函式作為引數或將函式作為輸出返回。

例如Array.prototype.map,Array.prototype.filter,Array.prototype.reduce都是一些高階函式。

尾呼叫和尾遞迴

尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,本身非常簡單,一句話就能說清楚。就是指某個函式的最後一步是呼叫另一個函式。

function g(x) {
  console.log(x)
}
function f(x) {
  return g(x)
}
console.log(f(1))
//上面程式碼
中,函式f的最後一步是呼叫函式g,這就是尾呼叫。

上面程式碼中,函式 f 的最後一步是呼叫函式 g,這就是尾呼叫。尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生棧溢位錯誤。但是隊伍尾遞迴來說,由於只存在一個呼叫幀,所以永遠不會發生棧溢位錯誤。

function factorial(n) {
  if (n === 1) {
    return 1
  }
  return n * factorial(n - 1)
}

上面程式碼是一個階乘函式,計算 n 的階乘,最多需要儲存 n 個呼叫資料,複雜度為 O(n),如果改寫成尾呼叫,只保留一個呼叫記錄,複雜度為 O(1)。

function factor(n, total) {
  if (n === 1) {
    return total
  }
  return factor(n - 1, n * total)
}

斐波拉切數列也是可以用於尾呼叫。

function Fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return Fibonacci(n - 1) + Fibonacci(n - 2)
}
//尾遞迴
function Fibona(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  }
  return Fibona(n - 1, ac2, ac1 + ac2)
}

柯理化函式

在數學和電腦科學中,柯里化是一種將使用多個引數的一個函式轉換成一系列使用一個引數的函式的技術。所謂柯里化就是把具有較多引數的函式轉換成具有較少引數的函式的過程。
舉個例子

//普通函式
function fn(a, b, c, d, e) {
  console.log(a, b, c, d, e)
}
//生成的柯里化函式
let _fn = curry(fn)

_fn(1, 2, 3, 4, 5) // print: 1,2,3,4,5
_fn(1)(2)(3, 4, 5) // print: 1,2,3,4,5
_fn(1, 2)(3, 4)(5) // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5) // print: 1,2,3,4,5

柯理化函式的實現

// 對求和函式做curry化
let f1 = curry(add, 1, 2, 3)
console.log('複雜版', f1()) // 6

// 對求和函式做curry化
let f2 = curry(add, 1, 2)
console.log('複雜版', f2(3)) // 6

// 對求和函式做curry化
let f3 = curry(add)
console.log('複雜版', f3(1, 2, 3)) // 6

// 複雜版curry函式可以多次呼叫,如下:
console.log('複雜版', f3(1)(2)(3)) // 6
console.log('複雜版', f3(1, 2)(3)) // 6
console.log('複雜版', f3(1)(2, 3)) // 6

// 複雜版(每次可傳入不定數量的引數,當所傳引數總數不少於函式的形參總數時,才會執行)
function curry(fn) {
  // 閉包
  // 快取除函式fn之外的所有引數
  let args = Array.prototype.slice.call(arguments, 1)
  return function() {
    // 連線已快取的老的引數和新傳入的引數(即把每次傳入的引數全部先儲存下來,但是並不執行)
    let newArgs = args.concat(Array.from(arguments))
    if (newArgs.length < fn.length) {
      // 累積的引數總數少於fn形參總數
      // 遞迴傳入fn和已累積的引數
      return curry.call(this, fn, ...newArgs)
    } else {
      // 呼叫
      return fn.apply(this, newArgs)
    }
  }
}

柯里化的用途

柯里化實際是把簡答的問題複雜化了,但是複雜化的同時,我們在使用函式時擁有了更加多的自由度。 而這裡對於函式引數的自由處理,正是柯里化的核心所在。 柯里化本質上是降低通用性,提高適用性。來看一個例子:

我們工作中會遇到各種需要通過正則檢驗的需求,比如校驗電話號碼、校驗郵箱、校驗身份證號、校驗密碼等, 這時我們會封裝一個通用函式 checkByRegExp ,接收兩個引數,校驗的正則物件和待校驗的字串

function checkByRegExp(regExp, string) {
  return regExp.text(string)
}

checkByRegExp(/^1\d{10}$/, '18642838455') // 校驗電話號碼
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]') // 校驗郵箱

我們每次進行校驗的時候都需要輸入一串正則,再校驗同一型別的資料時,相同的正則我們需要寫多次, 這就導致我們在使用的時候效率低下,並且由於 checkByRegExp 函式本身是一個工具函式並沒有任何意義。此時,我們可以藉助柯里化對 checkByRegExp 函式進行封裝,以簡化程式碼書寫,提高程式碼可讀性。

//進行柯里化
let _check = curry(checkByRegExp)
//生成工具函式,驗證電話號碼
let checkCellPhone = _check(/^1\d{10}$/)
//生成工具函式,驗證郵箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)

checkCellPhone('18642838455') // 校驗電話號碼
checkCellPhone('13109840560') // 校驗電話號碼
checkCellPhone('13204061212') // 校驗電話號碼

checkEmail('[email protected]') // 校驗郵箱
checkEmail('[email protected]') // 校驗郵箱
checkEmail('[email protected]') // 校驗郵箱

柯里化函式引數 length

函式 currying 的實現中,使用了 fn.length 來表示函式引數的個數,那 fn.length 表示函式的所有引數個數嗎?並不是。

函式的 length 屬性獲取的是形參的個數,但是形參的數量不包括剩餘引數個數,而且僅包括第一個具有預設值之前的引數個數,看下面的例子。

((a, b, c) => {}).length
// 3

((a, b, c = 3) => {}).length
// 2

((a, b = 2, c) => {}).length
// 1

((a = 1, b, c) => {}).length
// 0

((...args) => {}).length
// 0

const fn = (...args) => {
  console.log(args.length)
}
fn(1, 2, 3)
// 3

廣州vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com

compose 函式

compose 就是組合函式,將子函式串聯起來執行,一個函式的輸出結果是另一個函式的輸入引數,一旦第一個函式開始執行,會像多米諾骨牌一樣推導執行後續函式。

const greeting = name => `Hello ${name}`
const toUpper = str => str.toUpperCase()

toUpper(greeting('Onion')) // HELLO ONION

compose 函式的特點

  • compose 接受函式作為引數,從右向左執行,返回型別函式
  • fn()全部引數傳給最右邊的函式,得到結果後傳給倒數第二個,依次傳遞

compose 的實現

var compose = function(...args) {
  var len = args.length // args函式的個數
  var count = len - 1
  var result
  return function func(...args1) {
    // func函式的args1引數列舉
    result = args[count].call(this, args1)
    if (count > 0) {
      count--
      return func.call(null, result) // result 上一個函式的返回結果
    } else {
      //回覆count初始狀態
      count = len - 1
      return result
    }
  }
}

舉個例子

var greeting = (name) =>  `Hello ${name}`
var toUpper = str => str.toUpperCase()
var fn = compose(toUpper, greeting)
console.log(fn('jack'))

大家熟悉的webpack裡面的 loader 執行順序是從右到左,是因為webpack選擇的是 compose 方式,從右到左依次執行 loader,每個 loader 是一個函式。

rules: [
  { test: /\.css$/, use: ['style-loader', 'css-loader'] }
]

如上,webpack 使用了 style-loader 和 css-loader,它是先用 css-loader 載入.css 檔案,然後 style-loader 將內部樣式注入到我們的html頁面。

webpack 裡面的 compose 程式碼如下:

const compose = (...fns) => {
  return fns.reduce(
    (prevFn, nextFn) => {
      return value =>prevFn(nextFn(value)) 
    },
    value => value
  )
}