在開始正文以前我想談些與此文相關性很低的話題。對這部分不感興趣的讀者可直接跳過。javascript
在我發表上一篇文章或許咱們在 JavaScript 中不須要 this 和 class以後,看到一種評論比較有表明性。此評論認爲咱們應該以 MDN 文檔爲指南。MDN 推薦的寫法,應當是無需質疑的寫法。java
我不這麼看。編程
MDN 文檔不是用來學習的,它是你在不肯定某個具體語法時的參考指南。它是 JavaScript 使用說明書。說明書通常都不帶主觀思辨,它沒法提供指引。就好比你光是把市面上的全部調味料買來,看它們的說明書,你仍是學不會怎麼作菜的……數組
很碰巧我看的比較多的一些 JS 教程,都比較主觀,與 MDN 有不少誤差。好比 Kyle Simpson 認爲 JS 裏面根本沒有繼承,提供 new 操做符以及 class 語法糖是在誤導開發者。JS 原型鏈的正確用法應該是代理而不是繼承。(我贊成他)promise
更明顯的例子是 Douglas Crockford,他認爲 JS 中處理異步編程的主流方案—— callback hell, promise, async/await 全都錯了。你在看他的論述以前有十足把握判定他在胡說嗎?他在 How JavaScript Works 裏面論述了他對事件編程(Eventual Programming)的見解,並寫了個完整的庫,提供他的解決方案。app
批判和辯證地看問題,咱們才能進步。異步
我以前有兩篇文章寫過 JS 裏面惰性求值的實現,但都是淺嘗輒止,沒有過橫向擴展的打算。相關工做已經有人作了(如 lazy.js),我再作意義就不大了。這周在 GitHub 上看到有人寫了個相似的庫,用原生 Generator/Iterator 實現,人氣還很高。我一看仍是有人在寫,我也試試吧。而後我就用 Douglas Crockford 倡導的一種編程風格去寫這個庫,想驗證下這種寫法是否可行。async
Crockford 倡導的寫法是,不用 this 和原型鏈,不用 ES6 Generator/Iterator,不用箭頭函數…… 數據封裝則用工廠函數來實現。異步編程
首先,若是不用 ES6 Generator 的話,咱們得本身實現一個 Generator,這個比較簡單:函數
function getGeneratorFromList(list) {
let index = 0;
return function next() {
if (index < list.length) {
const result = list[index];
index += 1;
return result;
}
};
}
// 例子:
const next = getGeneratorFromList([1, 2, 3]);
next(); // 1
next(); // 2
next(); // 3
next(); // undefined
複製代碼
ES6 給數組提供了 [Symbol.Iterator] 屬性,給數據賦予了行爲,很方便咱們進行惰性求值操做。而拋棄了 ES6 提供的這個便利以後,咱們就只有手動將數據轉換成行爲了。來看看怎麼作:
function Sequence(list) {
const iterable = Array.isArray(list)
? { next: getGeneratorFromList(list) }
: list;
}
複製代碼
若是給 Sequence 傳入原生數組的話,它會將數組傳給 getGeneratorFromList
,生成一個 Generator,這樣就完成了數據到行爲的轉換
最核心的這兩個功能寫完以後,咱們來實現一個 map
:
function createMapIterable(mapping, { next }) {
function map() {
const value = next();
if (value !== undefined) {
return mapping(value);
}
}
return { next: map };
}
function Sequence(list) {
const iterable = Array.isArray(list)
? { next: getGeneratorFromList(list) }
: list;
function map(mapping) {
return Sequence(createMapIterable(mapping, iterable));
}
return {
map,
};
}
複製代碼
map
寫完後,咱們還須要一個函數幫咱們把行爲轉換回數據:
function toList(next) {
const arr = [];
let value = next();
while (value !== undefined) {
arr.push(value);
value = next();
}
return arr;
}
複製代碼
而後咱們就有一個半完整的惰性求值的庫了,雖然如今它只能 map
:
function Sequence(list) {
const iterable = Array.isArray(list)
? { next: getGeneratorFromList(list) }
: list;
function map(mapping) {
return Sequence(createMapIterable(mapping, iterable));
}
return {
map,
toList: () => toList(iterable.next);
};
}
// 例子:
const double = x => x * 2 // 箭頭函數這樣用是沒問題的啊啊啊,破個例吧
Sequence([1, 3, 6])
.map(double)
.toList() // [2,6,12]
複製代碼
再給 Sequence 加個 filter
方法就差很少完整了,其它方法再擴展很簡單了。
function createFilterIterable(predicate, { next }) {
function filter() {
const value = next();
if (value !== undefined) {
if (predicate(value)) {
return value;
}
return filter();
}
}
return {next: filter};
}
function Sequence(list) {
const iterable = Array.isArray(list)
? { next: getGeneratorFromList(list) }
: list;
function map(mapping) {
return Sequence(createMapIterable(mapping, iterable));
}
function filter(predicate) {
return Sequence(createFilterIterable(predicate, iterable));
}
return {
map,
filter,
toList: () => toList(iterable.next);
};
}
// 例子:
Sequence([1, 2, 3])
.map(triple)
.filter(isEven)
.toList() // [6]
複製代碼
看樣子接着上面的例子繼續擴展就沒問題了。
我繼續寫了十幾個函數,如 take, takeWhile, concat, zip 等。直到寫到我不知道接着寫哪些了,而後我去參考了下 lazy.js 的 API,一看倒吸一口涼氣。lazy.js 快 200 個 API 吧(沒數過,目測),寫完代碼還要寫文檔。我實在不想這麼折騰了。更嚴重的問題不在於工做量,而是這麼龐大的 API 數量讓我意識到我這種寫法的問題。
在使用工廠函數實現鏈式調用的時候,每次調用都返回了一個新的對象,這個新對象包含了全部的 API。假設有 200 個 API,每次調用都是隻取了其中一個,剩下 199 個全扔掉了…… 內存再夠用也不能這麼玩吧。我有強迫症,受不了這種浪費。
結論就是,若是想實現鏈式調用,仍是用原型鏈實現比較好。
然而鏈式調用自己就沒問題了嗎?雖然用原型鏈實現的鏈式調用能省去後續調用的對象建立,可是在初始化的時候也無可避免浪費內存。好比,原型鏈上有 200 個方法,我只調用其中 10 個,剩下的那 190 個都不須要,但它們仍是會在初始化時建立。
我想到了 Rx.js 在版本 5 升級到版本 6 的 API 變更。
// rx.js 5 的寫法:
Source.startWith(0)
.filter(predicate)
.takeWhile(predicate2)
.subscribe(() => {});
// rx.js 6 的寫法:
import { startWith, filter, takeWhile } from 'rxjs/operators';
Source.pipe(
startWith(0),
filter(predicate),
takeWhile(predicate2)
).subscribe(() => {});
複製代碼
RxJS 6 裏面採用了管道組合替代了鏈式調用。這樣子改動以後,想用什麼操做符就引用什麼,沒有多餘的操做符初始化,也利於 tree shaking。那麼咱們就模仿 Rxjs 6 的 API 改寫上面的 Sequence 庫吧。
操做符的實現和上面沒太大區別,主要區別在操做符的組合方式變了:
function getGeneratorFromList(list) {
let index = 0;
return function generate() {
if (index < list.length) {
const result = list[index];
index += 1;
return result;
}
};
}
function toList(sequence) {
const arr = [];
let value = sequence();
while (value !== undefined) {
arr.push(value);
value = sequence();
}
return arr;
}
// Sequence 函數自己很是輕量,操做符按需引入
function Sequence(list) {
const initSequence = getGeneratorFromList(list);
function pipe(...args) {
return args.reduce((prev, current) => current(prev), initSequence);
}
return { pipe };
}
function filter(predicate) {
return function(sequence) {
return function filteredSequence() {
const value = sequence();
if (value !== undefined) {
if (predicate(value)) {
return value;
}
return filteredSequence();
}
};
};
}
function map(mapping) {
return function(sequence) {
return function mappedSequence() {
const value = sequence();
if (value !== undefined) {
return mapping(value);
}
};
};
}
function take(n) {
return function(sequence) {
let count = 0;
return function() {
if (count < n) {
count += 1;
return sequence();
}
};
};
}
function skipWhile(predicate) {
return function(sequence) {
let startTaking = false;
return function skippedSequence() {
const value = sequence();
if (value !== undefined) {
if (startTaking) {
return value;
} else if (!predicate(value)) {
startTaking = true;
return value;
}
return skippedSequence();
}
};
};
}
function takeUntil(predicate) {
return function(sequence) {
return function() {
const value = sequence();
if (value !== undefined) {
if (predicate(value)) {
return value;
}
}
};
};
}
Sequence([2, 4, 6, 7, 9, 11, 13]).pipe(
filter(x => x % 2 === 1),
skipWhile(y => y < 10),
toList
); // [11,13]
複製代碼
參考:
Let’s experiment with functional generators and the pipeline operator in JavaScript