在开始正文以前我想谈些与此文相关性很低的话题。对这部分不感兴趣的读者可直接跳过。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