你也許不知道的javascript高級函數

前言

高階函數是對其餘函數進行操做的函數,能夠將它們做爲參數或經過返回它們。簡單來講,高階函數是一個函數,它接收函數做爲參數或將函數做爲輸出返回。javascript

例如Array.prototype.mapArray.prototype.filterArray.prototype.reduce 都是一些高階函數。css

本文源自個人掘金 https://juejin.im/post/5e96c3...
歡迎閱讀其餘js系列文章html

JS基礎總結(1)——數據類型
JS基礎總結(2)——原型與原型鏈
JS基礎總結(3)——做用域和閉包
JS基礎總結(4)——this指向及call/apply/bind
JS基礎總結(5)—— JS執行機制與EventLoopd前端

尾調用和尾遞歸

尾調用(Tail Call)是函數式編程的一個重要概念,自己很是簡單,一句話就能說清楚。就是指某個函數的最後一步是調用另外一個函數。vue

function g(x) {
  console.log(x)
}
function f(x) {
  return g(x)
}
console.log(f(1))
//上面代碼中,函數f的最後一步是調用函數g,這就是尾調用。

上面代碼中,函數 f 的最後一步是調用函數 g,這就是尾調用。尾調用不必定出如今函數尾部,只要是最後一步操做便可。java

函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生棧溢出錯誤。可是隊伍尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生棧溢出錯誤。webpack

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

上面代碼是一個階乘函數,計算 n 的階乘,最多須要保存 n 個調用數據,複雜度爲 O(n),若是改寫成尾調用,只保留一個調用記錄,複雜度爲 O(1)。web

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)
}

柯理化函數

在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。所謂柯里化就是把具備較多參數的函數轉換成具備較少參數的函數的過程。
舉個例子npm

//普通函數
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+)+)$/, 'test@163.com') // 校驗郵箱

咱們每次進行校驗的時候都須要輸入一串正則,再校驗同一類型的數據時,相同的正則咱們須要寫屢次, 這就致使咱們在使用的時候效率低下,而且因爲 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('test@163.com') // 校驗郵箱
checkEmail('test@qq.com') // 校驗郵箱
checkEmail('test@gmail.com') // 校驗郵箱

柯里化函數參數 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

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
  )
}

推薦文章

總結javascript處理異步的方法
總結移動端H5開發經常使用技巧(乾貨滿滿哦!)
從零開始構建一個webpack項目
總結幾個webpack打包優化的方法
總結前端性能優化的方法
總結vue知識體系之高級應用篇
總結vue知識體系之實用技巧
幾種常見的JS遞歸算法
封裝一個toast和dialog組件併發布到npm
一文讀盡前端路由、後端路由、單頁面應用、多頁面應用
淺談JavaScript的防抖與節流

相關文章
相關標籤/搜索