js 中函數做爲一等公民,函數執行中既能夠做爲函數的參數也能夠做爲函數的返回值,而這類執行函數叫作高階函數,利用高階函數的特性很容易就能夠實現柯里化(柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術),根據百科的理解大概就是下面的例子。node
function add(x, y) { return x + y } function curryind_add(x) { return function(y) { return x + y } } const curried_add = curryind_add(1) curried_add(2) // 3 curried_add(3) // 4
從例子能夠看出,柯里化有防止參數重複的做用,並且具備延遲執行的特徵。下面利用柯里化的特徵看看在異步編程的應用,在開始時先簡單介紹一下 async/await 的原理。編程
async/await 語法實際上是 Generator 和 promise 的語法糖。 promise
Generator 函數執行時會返回一個生成器對象,該對象是一個特殊的迭代器對象,並且自己也是一個可迭代對象(迭代器對象和可迭代對象),下面說說其特殊性。瀏覽器
function *generator() { const value = yield 1 console.log(value) try { // 捕獲錯誤 yield 2 } catch(e) { console.log(e) } } const iterator = generator(); iterator === iterator[Symbol.iterator](); // true 自身部署了可迭代接口,該接口返回自身 [...iterator] // [1, 2] const iterator2 = generator(); // 迭代器對象是一次消耗的,須要從新起一個迭代器 iterator2.next() // {value: 1, done: false} iterator2.next(`value 的值`) // 利用 next 向生成器函數發送數據 iterator2.throw(new Error('error')) // 利用 throw 向生成器函數拋出錯誤
從上面的例子能夠看出,生成器對象,在迭代的過程當中是能夠和生成器函數進行通訊的,若是用在 promise 中,只要把 promise 在狀態改變後執行的回調結果回傳到生成器函數內就能夠實現相似async/await 的效果。而迭代過程,能夠實現一個執行器函數進行迭代,這個函數就相似於執行 async。babel
function createDelayPromise (time) { return new Promise((resolve) => { setTimeout(() => { resolve(time) }, time) }) } function *createIterator() { const time1 = yield createDelayPromise(1000) console.log(time1) const time2 = yield createDelayPromise(2000) console.log(time2) const time3 = yield createDelayPromise(3000) console.log(time3) return time1 + time2 + time3 } function run(createIterator) { const iterator = createIterator() let result = iterator.next() let r const p = new Promise(resolve => { r = resolve }) function next() { if (result.done) { r(result.value) } else { Promise.resolve(result.value).then(value => { result = iterator.next(value) next() }).catch((err) => { result = iterator.throw(err) next() }) } } next() return p } run(createIterator).then((value) => { console.log(value) })
上面使用 createDelayPromise 封裝了一個 promise 類型的異步任務,而後使用 next 方法去迭代這個迭代器對象。等待對應的異步任務有結果後就經過生成器對象的 next 和 throw 方法把結果回傳到生成器函數中。
上面是使用 promise 配合 generator 可讓異步代碼以相似同步的形式寫在生成器函數中,一樣地對應柯里化後的函數,一樣具備延遲執行的效果,也能夠經過配合 generator 實現相似的效果。app
除了一些接口是實現了 promise 化外,有不少比較久的接口依然是使用 callback 類型的接口,像 node 中的不少接口會把 callback 參數放在參數列表的末尾,並把 err 放在回調執行的第一個位置,相似這樣異步
function createDelayCurry (time, callback) { setTimeout(() => { callback(null, time) }, time) }
下面經過實現不一樣以上的 run 方法,結合柯里化實現以上的效果,此次 yield 後面再也不是 promise 化後的對象,而是柯里化後的函數
在開始以前先實現一個通用的 curry 函數async
const curry = (fn, ...args) => fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
這個 curry 函數的做用是等初始存入的函數的參數收集夠就執行,若是不夠就繼續收集,下面會在迭代中補上回調參數,這樣異步任務才能夠執行。函數式編程
const curry = (fn, ...args) => fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args); function createDelayCurry (time, callback) { setTimeout(() => { callback(null, time) }, time) } const curringDelay = curry(createDelayCurry) function *createIterator() { const time1 = yield curringDelay(1000) console.log(time1) const time2 = yield curringDelay(2000) console.log(time2) const time3 = yield curringDelay(3000) console.log(time3) return time1 + time2 + time3 } function run(createIterator, callback) { const iterator = createIterator() let result = iterator.next() function next() { if (result.done) { callback(null, result.value) } else { result.value((err, value) => { // 補上最後 callback 參數啓動任務 if (err) { result = iterator.throw(err) next() } else { result = iterator.next(value) next() } }) } } next() } run(createIterator, (err, value) => { console.log(err, value) })
執行效果和 promise 版是差很少同樣的。異步編程
上面是串行的版本,下面看看並行的實現
先看 promise 並行通常寫法
function *createIteratorParallel() { const times = yield Promise.all([createDelayPromise(1000), createDelayPromise(2000), createDelayPromise(3000)]) return times }
相應的也實現一個 all 方法
function curryAll(curries, callback) { let len = curries.length let result = [] let count = 0 curries.forEach((curried, index) => { curried((err, value) => { if (err) { callback(err) return } else { count++ result[index] = value if (count == len) { callback(null, result) } } }) }) } const curriedAll = curry(curryAll) function *createIteratorParallel() { const times = yield curriedAll([curringDelay(1000), curringDelay(2000), curringDelay(3000)]) return times }
yield 後面依然是柯里化後的函數
上面的方案要求異步任務的回調須要在參數的末尾位置,若是回調不在末尾,那麼就須要修改 curry 函數的實現方式,可是有沒有可能不用修改其實現方式,甚至不用寫 curry 呢?
curry 做爲函數式編程的基本單元,最新的 es 規範實驗性地從語法的角度提供了支持。
function add(x, y) { return x + y; } const addOne = add(1, ?); // apply from the left addOne(2); // 3 const addTen = add(?, 10); // apply from the right addTen(2); // 12
以上的 addOne addTen 就是一個柯里化後的函數,這個語法大部分瀏覽器都沒有支持,若是要使用也很簡單,使用一個 babel 插件就能夠了 babel --plugins @babel/plugin-proposal-partial-application script.js
若是使用了上面的語法,能夠不用引入 curry 函數
function *createIterator() { const time1 = yield createDelayCurry(1000, ?) console.log(time1) const time2 = yield createDelayCurry(2000, ?) console.log(time2) const time3 = yield createDelayCurry(3000, ?) console.log(time3) return time1 + time2 + time3 }
若是 callback 在前面,那麼就把問號放在前面,實際上能夠放在任何參數位置上,主要看具體的接口要求。
柯里化的應用很是普遍,上面只是舉了一個簡單的例子,經過這個例子,也瞭解了 async/await 基本實現原理,雖然 async/await 已經被普遍支持,promise 也被普遍使用,可能再也不須要直接使用生成器函數了,但也不妨礙去簡單地瞭解一下。