本文的思路:javascript
學習thunk相關知識,主要參考阮一峯的介紹html
一步步實現thunkify模塊,而且使用測試用例來完善咱們的代碼,打造出一個健壯
的模塊java
Thunk函數的誕生是源於一個編譯器設計的問題:求值策略
,即函數的參數到底應該什麼時候求值。node
例如:git
var x = 1; function f(m) { return m * 2; } f(x + 5);
其中x+5
這個表達式應該何時求值,有兩種思路github
傳值調用(call by value),即在進入函數體之間,先計算x+5的值,再將這個值(6)傳入函數f,例如c語言,這種作法的好處是實現比較簡單,可是有可能會形成性能損失。app
傳名調用(call by name),即直接將表達式(x+5)傳入函數體,只在用到它的時候求值。函數
編譯器的傳名調用
實現,每每就是將參數放到一個臨時函數之中,再將這個臨時函數轉入函數體,這個臨時函數就叫作Thunk函數
。性能
來看一段代碼示例:學習
function f(m) { return m*2; } f(x + 5); // 等價於如下代碼 var thunk = function () { return x + 5; }; function f(thunk) { return thunk() * 2; }
咱們都知道Javascript是傳值調用的,那麼js中的Thunk函數又是怎麼回事?
在Javascript語言中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數。
仍是經過代碼來理解,即
// 正常版本的readFile,須要兩個參數filename、callback fs.readFile(fileName, callback); // thunk版本的readFile var readFileThunk = thunkify(fs.readFile); readFileThunk(fileName)(callback);
原文中例子就是柯里化,預置參數fileName,直接調用fs.readFile
好,如今咱們來思考如何實現thunkify
函數。咱們從調用的形式來看,返回的應該是一個高階函數,即返回一個函數a,a的返回仍是一個函數。
var thunkify = function (fn) { return function () { return function () { } } };
結合上述例子,由於是包裝函數,所以最終仍是readFile執行,且須要fileName,所以:
var thunkify = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); return function (callback) { args.push(callback); return fn.apply(this, args); } } };
這樣彷佛很完美,咱們運行整個示例
const fs = require('fs'); var thunkify = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); return function (callback) { args.push(callback); return fn.apply(this, args); } } }; var readFileThunk = thunkify(fs.readFile); readFileThunk('test.txt', 'utf-8')( (err, data) => { console.log(data); });
運行結果爲
要寫出一個健壯的thunkify
函數,須要考慮的各類狀況,而咱們經過tj大神寫的thunkify模塊的測試代碼,來看看咱們本身的thunkify
還存在哪些不足,一步步來優化。
一、保存上下文的問題
function load(fn) { fn(null, this.name); } var user = { name: 'tobi', load: thunkify(load) }; user.load()((err, res) => { console.log(res); });
運行以後,res的結果爲undefined
,緣由是沒有保存上下文,改進一下
var thunkify = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); var ctx = this; return function (callback) { args.push(callback); return fn.apply(ctx, args); } } };
二、捕抓錯誤
function load(fn) { throw new Error('boom'); } load = thunkify(load); load()(err => console.log(err.message));
運行以後,發現並無捕抓到錯誤,咱們須要執行函數進行try/catch,而且當出錯時,傳遞出錯信息。
var thunkify = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); var ctx = this; return function (callback) { args.push(callback); var result; // try/catch捕抓信息,而且出錯時,傳遞給回調函數 try { result = fn.apply(ctx, args); } catch (e) { callback(e); } return result; } } };
三、回調函數應該只調用一次。
function load(fn) { fn(null, 1); fn(null, 2); fn(null, 3); } load = thunkify(load); load()((err,ret) => console.log(ret));
運行輸出結果爲1 2 3
,而咱們指望結果只爲1
,那麼須要判斷callback是否已經執行過了,使其只執行一次。
var thunkify = function (fn) { return function () { var args = Array.prototype.slice.call(arguments); var ctx = this; return function (callback) { var called; // 對callback進行封裝,使其只能執行一次。 args.push(function () { if(called) return; called = true; callback.apply(null, arguments); }); var result; try { result = fn.apply(ctx, args); } catch (e) { callback(e); } return result; } } };
到這裏,咱們經過了全部的測試,完成了一個健壯
的thunkify
模塊。
在學習一個概念或者一個模塊時,測試代碼加深你對知識的理解和掌握。