寫一個自己的Thunkify模塊

什么是Thunk函數

本段內容無恥抄襲自阮一峰的《ESMAScript 6 入門》中對Thunk函數的介紹

Thunk 函數早在上個世紀60年代就誕生了。那時,編程語言剛剛起步,計算機學家還在研究編譯器怎么寫比較好。一個爭論的焦點是"求值策略",即函數的參數到底應該何時求值。

var x = 1

function f(m){
  return m * 2
}

f(x + 5)

上面代碼先定義函數f,然后向它傳入表達式x + 5。請問,這個表達式應該何時求值?

一種意見是"傳值調用"(call by value),即在進入函數體之前,就計算x + 5的值(等于6),再將這個值傳入函數f。C語言就采用這種策略。

f(x + 5)
// 傳值調用時,等同于
f(6)

另一種意見是“傳名調用”(call by name),即直接將表達式x + 5傳入函數體,只在用到它的時候求值。Haskell 語言采用這種策略。

f(x + 5)
// 傳名調用時,等同于
(x + 5) * 2

傳值調用和傳名調用,哪一種比較好?回答是各有利弊。傳值調用比較簡單,但是對參數求值的時候,實際上還沒用到這個參數,有可能造成性能損失。

編譯器的“傳名調用”實現,往往是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫做 Thunk 函數。

function f(m) {
  return m * 2
}

f(x + 5)

// 等同于

var thunk = function () {
  return x + 5
}

function f(thunk) {
  return thunk() * 2
}

上面代碼中,函數f的參數x + 5被一個函數替換了。凡是用到原參數的地方,對Thunk函數求值即可。這就是 Thunk 函數的定義,它是“傳名調用”的一種實現策略,用來替換某個表達式。

我眼中的Thunk函數

上面對Tunk函數的定義已經非常明白了,但是對于JS這門語言來說,其本身就是傳值調用的,所以JS中的Thunk函數一般是指,將原本接受回調函數作為參數的多參函數轉化為只接受回調函數作為參數的單參函數。這么說好像有點拗口,看下面一段例子:

// 轉化前
function fn(arg, cb) {
  ...
  cb(err, result)
}
// 轉化后
function thunkFn(arg) {
  return function (cb) {
    return fn(arg, cb)
  }
}

上面這段代碼可以看出一般函數轉化為Thunk函數需要做一步轉換工作,如果每次轉化都要這么做無疑是一件麻煩的事情,有句話(瞎編的話)叫做重復的事情做兩次就需要抽象提取,對重復工作的厭惡、鄙視是一個優秀(強迫癥、龜毛)程序員的基本素養。那么下面我們就開始分析下怎么抽離一個公共的模塊(thunkify函數)替代重復勞動。

Thunkify模塊的基礎功能

根據之前的例子我們可以很輕松的推演出一個可以實現thunkify基礎功能的函數:

function thunkify (fn) {
  // 返回準備接收前置參數的Thunk函數
  return function () {
    // 閉包傳入的前置參數
    var args = Array.prototype.slice.call(arguments)

    // 返回準備接受回調函數的完整功能函數
    return function (cb) {
      // 推入回調函數組成完整參數數組
      args.push(cb)

      // 返回最終調用的完整功能函數的返回值
      return fn.apply(null, args)
    }
  }
}

以上代碼基本可以完成一般情況下的Thunkify功能,可以看到整段代碼其實和柯里化的原理是類似的,只是并沒有像柯里化函數一樣細的參數顆粒度,而且對傳入的功能函數要求最后一個參數必須是回調函數。第一步返回的Thunk函數需要根據傳入的功能函數的要求,在調用時傳入所有需要的前置參數,最后必須要傳入回調函數才能完成整個Thunk函數的功能。形象的區別可以用代碼表示為

// 柯里化函數的調用,最后的參數‘c’可以為任何需要的類型
curryingFn(a)(b)(c)

// Thunk函數的調用,最后的參數‘cb’必須為回調函數
thunk(a,b)(cb)

更健壯的Thunkify模塊

上面的代碼已經實現了Thunkify模塊的主題功能,但是對于一些細節和特殊情況缺少處理,這樣必然會導致錯誤的發生,下面讓我們打造一個更強壯的可以放心使用的公共模塊吧。

  • 上下文丟失
    上面的代碼沒有對回調函數的上下文做特殊處理,比如下面的代碼就會出現問題
var fn = function (cb) {
    cb(null, this.text)
}
var obj = {
    text: 'abc',
    thunkFn: thunkify(fn)
}
obj.thunkFn()(function (null, text) {
    return text // undefined
})

解決辦法:

function thunkify (fn) {
    return function () {
      var args = Array.prototype.slice.call(arguments)
      // 閉包上下文
      var ctx = this
      return function (cb) {
        args.push(cb)
        return fn.apply(ctx, args)
      }
    }
}

ES6簡潔寫法:

function thunkify (fn) {
    // 此處不能使用箭頭函數,想想為什么?
    return function (...args) {
      return cb => {
        args.push(cb)
        return fn.apply(this, args)
      }
    }
}
  • 錯誤捕獲
    目前還沒有添加對錯誤的捕獲和處理,可能會造成程序的意外中斷,添加錯誤處理:
function thunkify (fn) {
  return function (...args) {
    return cb => {
      args.push(cb)
      let result
      try {
        result = fn.apply(this, args)
      } catch(e) {
        cb(e)
      }
      return result
    }
  }
}
  • 回調函數只運行一次
    這個是之前沒考慮到的情況,參考了tj大神寫的thunkify模塊才意識到這個問題,補上回調函數只運行一次的限制:
function thunkify (fn) {
  return function (...args) {
    return cb => {
      let done = false
      let result
      // 替換掉原有回調函數,加上運行判斷
      args.push(function (...cbArgs) {
        if (done) {
          return
        }
        done = true
        cb(...cbArgs)
      })
      try {
        result = fn.apply(this, args)
      } catch(e) {
        cb(e)
      }
      return result
    }
  }
}

這樣我們就得到了一個完整的、健壯的Thunkify模塊,具體代碼見es6-thubkify

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容