查看原文javascript
函数是JavaScript的基石。它是一种灵活的抽象,能够做为其余抽象的基础,例如Promises,Iterables,Observables等。我一直在会议和研讨会上教授这些概念,随着时间的推移,我发现了一个金字塔模型,能够对这些抽象作一个优雅的总结。在这篇博客中,我将为你们介绍这个金字塔的各个层级。html
X => Yvue
一等公民是JavaScript的基础,如number,string,object,boolean等。尽管你能够只用值和控制流写出一个程序,但很快你就会须要写一个函数来改进你的程序。java
函数是JavaScript中不可避免的抽象,它们一般用回调实现异步的I/O。“函数”这个词在JavaScript中并不像在函数式编程中那样表明“纯函数”。由于它们只是惰性的可复用代码块,具备可选的输入(参数)和输出(返回值),把它们理解为简单的“过程”会更好。git
与硬编码的代码块相比,函数有两个很重要的优点:github
() => X数据库
getter是一个没有输入参数并输出X的函数编程
getter是一种函数,它不须要传递参数但能够返回一个指望值。在JavaScript的运行时中有很是多这样的getter,如Math.random()
,Date.now()
等。getter做为值的抽象也很是有用。请比较下面的user
与getUser
:json
const user = {name: 'Alice', age: 30};
console.log(user.name); // Alice
function getUser() {
return {name: 'Alice', age: 30};
}
console.log(getUser().name); // Alice
复制代码
经过使用getter表示一个值,咱们继承了函数的优势,如惰性:若是咱们不调用getUser()
,那么user对象就不会被建立出来。数组
由于咱们能够用多种不一样的方式(建立一个普通的对象,或者返回一个类的实例,又或者使用原型上的属性等等)来计算返回的对象,因此咱们也得到了实现的灵活性。采用硬编码的话就作不到这么灵活。
getter还容许咱们使用反作用钩子。不管getter在何时被执行,咱们都能触发一个有用的反作用,像一个console.log
或者触发一个分析事件,下面是一个例子:
function getUser() {
Analytics.sendEvent('User object is now being accessed');
return {name: 'Alice', age: 30};
}
复制代码
getter上的计算也能够是抽象的,由于函数在JavaScript中能够被看成一等公民进行传递。举个例子,看下面这个求和函数,它用getter做为参数并返回一个number型的getter,而不是直接返回一个number类型的值。
function add(getX, getY) {
return function getZ() {
const x = getX();
const y = getY();
return x + y;
}
}
复制代码
当getter须要返回一个不可预测的值时,这种抽象计算的好处是很明显的,例如使用Math.random
做为参数:
const getTen = () => 10;
const getTenPlusRandom = add(getTen, Math.random);
console.log(getTenPlusRandom()); // 10.948117215055046
console.log(getTenPlusRandom()); // 10.796721274448556
console.log(getTenPlusRandom()); // 10.15350303918338
console.log(getTenPlusRandom()); // 10.829703269933633
复制代码
getter与Promise一同使用也是很常见的,因为Promise被认为是不可复用的计算,因此将Promise构造器包在getter(也被称为“工厂”或“形式转换”)中使其可复用。
SETTERS
X => ()
setter是一个接受X做为参数而没有输出的函数
setter是一种接收参数但没有返回值的函数。JavaScript运行时和DOM中有许多原生的setter,例如console.log(x)
,document.write(x)
等。
与getter不一样,setter一般不是抽象,由于函数没有返回值意味着函数只能在JavaScript运行时中发送数据或命令。举个例子,名为getTen
的getter是一个对数字10的抽象而且咱们能够把它看成一个值进行传递,而将setTen
做为值进行传递则没有任何意义,由于你不能经过调用它来得到任何数字。
也就是说,setter能够是对其余setter的简单封装,看下面对console.log
这个setter的封装:
function fancyConsoleLog(str) {
console.log('⭐ ' + str + ' ⭐');
}
复制代码
() => ( () => X )
getter-getter是一个不须要输入参数并输出一个getter的函数
有一类特殊的getter能够返回另外一个getter,因此它是一个getter的getter。对getter-getter的需求源于使用getter迭代序列。举个例子,若是咱们想要显示2的幂的数字序列,咱们可使用getNextPowerOfTwo()
这个getter:
let i = 2;
function getNextPowerOfTwo() {
const next = i;
i = i * 2;
return next;
}
console.log(getNextPowerOfTwo()); // 2
console.log(getNextPowerOfTwo()); // 4
console.log(getNextPowerOfTwo()); // 8
console.log(getNextPowerOfTwo()); // 16
console.log(getNextPowerOfTwo()); // 32
console.log(getNextPowerOfTwo()); // 64
console.log(getNextPowerOfTwo()); // 128
复制代码
这段代码的问题是变量i
是一个全局变量,若是咱们想重启这个序列,就必须以正确的方式操做这个变量,从而暴露了这个getter的实现细节。
想要这段代码有更高的可复用性而且不依赖全局变量,咱们须要作的是用一个函数封装这个getter。而这个包装函数也是一个getter。
function getGetNext() {
let i = 2;
return function getNext() {
const next = i;
i = i * 2;
return next;
}
}
let getNext = getGetNext();
console.log(getNext()); // 2
console.log(getNext()); // 4
console.log(getNext()); // 8
getNext = getGetNext(); // 🔷 restart!
console.log(getNext()); // 2
console.log(getNext()); // 4
console.log(getNext()); // 8
console.log(getNext()); // 16
console.log(getNext()); // 32
复制代码
由于getter-getter是一类特殊的getter,它们继承了getter全部的优势,好比:
在这里惰性反映在初始化的步骤。外层函数支持惰性初始化,与此同时内层函数支持惰性的值迭代:
function getGetNext() {
// 🔷 LAZY INITIALIZATION
let i = 2;
return function getNext() {
// 🔷 LAZY ITERATION
const next = i;
i = i * 2;
return next;
}
}
复制代码
( X => () ) => ()
setter-setter是接收一个setter做为输入且没有输出的函数
setter-setter是一种特别的setter函数,其参数也是一个setter。尽管基础的setter不是抽象,但setter-setter是抽象,它可以表示能够在代码中进行传递的值。
例如,请思考是否可能借助下面的setter-setter表示数字10:
function setSetTen(setTen) {
setTen(10)
}
复制代码
要注意缺乏返回值,由于setter历来没有返回值。经过对参数进行简单的重命名可使上面的例子更具备可读性。
function setTenListener(cb) {
cb(10)
}
复制代码
顾名思义,cb
表明“回调(callback)”,代表了在有大量回调用例时setter-setter在JavaScript中是多么常见。将setter-setter表示的抽象值反过来用其实就获得了getter。
setSetTen(console.log);
// compare with...
console.log(getTen())
复制代码
setter-setter的好处与getter相同——惰性,灵活的实现,反作用钩子——但有两个getter没有的新属性:控制反转和异步性。
在上面的例子中,使用getter的代码决定什么时候将getter与console.log
一块儿使用。然而,使用setter-setter时,由setter-setter本身决定什么时候调用console.log
。责任倒置使setter-setter比getter更增强大,下面的例子中发送了多个值给消费者:
function setSetTen(setTen) {
setTen(10)
setTen(10)
setTen(10)
setTen(10)
}
复制代码
控制反转还容许setter-setter决定什么时候将值传递给回调,例如异步。假设把setSetTen
的名字改成setTenListener
:
function setTenListener(cb) {
setTimeout(() => { cb(10); }, 1000);
}
复制代码
尽管setter-setter在JavaScript中经常使用于异步编程,但回调中的代码不必定是异步的。在下面的这个setSetTen
的例子中,它与getter同样是同步的:
function setSetTen(setTen) {
setTen(10)
}
console.log('before');
setSetTen(console.log);
console.log('after');
// (Log shows:)
// before
// 10
// after
复制代码
() => ( () => ({done, value}) )
可迭代对象(忽略了一些细节)是一个getter-getter,它返回一个描述了值和完成状态的对象
getter-getter可以表示一个可重启的值序列,但没有约定用什么标记序列的结束。可迭代对象是一类特殊的getter-getter,它的值老是一个有两个属性的对象:done
(指示是否结束的布尔值)和value
(done
不为true时实际被传递的值)。
结束标记让使用可迭代对象的消费者知道序列将返回无效的数据,因此消费者可以知道什么时候中止迭代。
在下面的例子中,咱们能够根据完成指示器(completion indicator)生成一个有限的getter-getter,其值为40-48之间的偶数:
function getGetNext() {
let i = 40;
return function getNext() {
if (i <= 48) {
const next = i;
i += 2;
return {done: false, value: next};
} else {
return {done: true};
}
}
}
let getNext = getGetNext();
for (let result = getNext(); !result.done; result = getNext()) {
console.log(result.value);
}
复制代码
相比简单的() => ( () => ({done, value}) )
模式,ES6的可迭代对象有更深刻的约定,它们在每一个getter上添加了一个包装器对象:
f
变成了对象{[Symbol.iterator]: f}
g
变成了对象{next: g}
这里是一个有效的ES6可迭代对象,代码的功能与以前的例子相一致:
const oddNums = {
[Symbol.iterator]: () => {
let i = 40;
return {
next: () => {
if (i <= 48) {
const next = i;
i += 2;
return {done: false, value: next};
} else {
return {done: true};
}
}
}
}
}
let iterator = oddNums[Symbol.iterator]();
for (let result = iterator.next(); !result.done; result = iterator.next()) {
console.log(result.value);
}
复制代码
请注意两个例子之间的不一样点:
-function getGetNext() {
+const oddNums = {
+ [Symbol.iterator]: () => {
let i = 40;
- return function getNext() {
+ return {
+ next: () => {
if (i <= 48) {
const next = i;
i += 2;
return {done: false, value: next};
} else {
return {done: true};
}
}
+ }
}
+}
-let getNext = getGetNext();
-for (let result = getNext(); !result.done; result = getNext()) {
+let iterator = oddNums[Symbol.iterator]();
+for (let result = iterator.next(); !result.done; result = iterator.next()) {
console.log(result.value);
}
复制代码
ES6提供了方便使用可迭代对象的语法糖for-let-of
:
for (let x of oddNums) {
console.log(x);
}
复制代码
ES6还提供了生成器函数的语法糖function*
以简化建立可迭代对象:
function* oddNums() {
let i = 40;
while (true) {
if (i <= 48) {
const next = i;
i += 2;
yield next;
} else {
return;
}
}
}
复制代码
从2015年开始,配合生产端和消费端的语法糖,JavaScript中的可迭代对象是一种易于使用的对可完成的值序列的抽象。注意生成器函数自身不是一个可迭代对象,但调用生成器函数会返回一个可迭代对象:
function* oddNums() {
let i = 40;
while (true) {
if (i <= 48) {
yield i;
i += 2;
} else {
return;
}
}
}
for (let x of oddNums()) {
console.log(x);
}
复制代码
( X => (), Err => () ) => ()
Promise(忽略了一些细节)是有附加保证的,含有两个setter的setter
尽管setter-setter已经很强大,但因为控制反转,它们可能会很是不可预测。它们多是同步的,也多是异步的,而且能够随着时间推移传递零或一个或多个值。Promise是一种特别的setter-setter,它能够在传递值时提供一些保证:
将下面的setter-setter与等效的Promise进行对比。Promise将只会传一次值,而且不在两个console.log
之间,由于值的传递是异步的:
function setSetTen(setTen) {
setTen(10)
setTen(10)
}
console.log('before setSetTen');
setSetTen(console.log);
console.log('after setSetTen');
// (Log shows:)
// before setSetTen
// 10
// 10
// after setSetTen
复制代码
与之相比:
const tenPromise = new Promise(function setSetTen(setTen) {
setTen(10);
setTen(10);
});
console.log('before Promise.then');
tenPromise.then(console.log);
console.log('after Promise.then');
// (Log shows:)
// before Promise.then
// after Promise.then
// 10
复制代码
Promise方便地表示了一个异步且不可复用的值,此外ES2017提供了生产和消费的语法糖:async-await
。只能在有async
前缀的函数中使用await
来消费Promise的值:
async function main() {
console.log('before await');
const ten = await new Promise(function setSetTen(setTen) {
setTen(10);
});
console.log(ten);
console.log('after await');
}
main();
// (Log shows:)
// before await
// 10
// after await
复制代码
async-await
语法糖能够用来建立一个Promise,由于async function
返回一个Promise,它包着函数中被返回的值。
async function getTenPromise() {
return 10;
}
const tenPromise = getTenPromise();
console.log('before Promise.then');
tenPromise.then(console.log);
console.log('after Promise.then');
// (Log shows:)
// before Promise.then
// after Promise.then
// 10
复制代码
可观察对象(忽略了一些细节)是有附加保证的,含有三个setter的setter
就像可迭代对象是一类特别的getter-getter,可以标记完成的状态。可观察对象是一类可以添加完成状态的setter-setter。JavaScript中典型的setter-setter,像element.addEventListener
,不会通知事件流是否已完成,因此链接事件流或执行其余的与完成状态相关的逻辑会很困难。
与可迭代对象已经在JavaScript规范中被标准化不一样,可观察对象是RxJS,most.js,xstream,Bacon.js等库之间达成的松散约定。尽管Observable被考虑为TC39的提案,可是该提案一直在变更,因此在这篇文章中让咱们假定一个Fantasy Observable规范,像RxJS,most.js和xstream这样的库都遵循这个规范。
可观察对象是可迭代对象的另外一面,这能够经过一些对称性看出来:
Symbol.iterator
next
方法,是一个gettersubscribe
next
方法,是一个setter观察者对象还有两个方法,complete
和error
,分别表示成功完成和失败。
complete
setter至关于可迭代对象里的done
指示符,而error
setter至关于从迭代器getter中抛出一个例外。
与Promise同样,可观察对象在传递值的时候增长了一些保证:
complete
setter被调用,error
setter将不会被调用error
setter被调用,complete
setter将不会被调用complete
setter或error
setter被调用,next
setter将不会被调用在下面的例子中,可观察对象表示一个异步有限的数值序列:
const oddNums = {
subscribe: (observer) => {
let x = 40;
let clock = setInterval(() => {
if (x <= 48) {
observer.next(x);
x += 2;
} else {
observer.complete();
clearInterval(clock);
}
}, 1000);
}
};
oddNums.subscribe({
next: x => console.log(x),
complete: () => console.log('done'),
});
// (Log shows:)
// 40
// 42
// 44
// 46
// 48
// done
复制代码
与setter-setter同样,可观察对象致使控制反转,因此消费端(oddNums.subscribe
)没有办法暂停或取消进入的数据流。大多数可观察对象的实现添加了一个重要的细节——容许消费者发送取消信号给生产者:订阅者。
subscribe
函数能够返回一个对象——订阅者——拥有一个方法:unsubscribe
,消费端可使用这个方法停止进入的数据流。subscribe
是一个既有输入(观察者)又有输出(订阅者)的函数,所以它再也不是一个setter。下面,咱们将一个订阅者对象添加到咱们以前的例子中:
const oddNums = {
subscribe: (observer) => {
let x = 40;
let clock = setInterval(() => {
if (x <= 48) {
observer.next(x);
x += 2;
} else {
observer.complete();
clearInterval(clock);
}
}, 1000);
// 🔷 Subscription:
return {
unsubscribe: () => {
clearInterval(clock);
}
};
}
};
const subscription = oddNums.subscribe({
next: x => console.log(x),
complete: () => console.log('done'),
});
// 🔷 Cancel the incoming flow of data after 2.5 seconds
setTimeout(() => {
subscription.unsubscribe();
}, 2500);
// (Log shows:)
// 40
// 42
复制代码
**() => ( () => Promise<{done, value}>) **
异步可迭代对象(忽略一些细节)是一个生成Promise的可迭代对象,值在Promise中
可迭代对象能够表示任何无限或有限的值序列,但它有一个限制:在消费者调用next()
方法时值必须能够同步被使用。异步可迭代对象拓展了可迭代对象的能力,容许值被异步传递而不是在被请求时当即返回。
异步可迭代对象经过使用Promise实现了值的异步传递。每一次迭代器的next()
(内层的getter函数)被调用,建立并返回一个Promise。
下面的例子中,咱们采用了oddNums
可迭代对象的例子并使它生成延迟resolve的Promise:
function slowResolve(val) {
return new Promise(resolve => {
setTimeout(() => resolve(val), 1000);
});
}
function* oddNums() {
let i = 40;
while (true) {
if (i <= 48) {
yield slowResolve(i); // 🔷 yield a Promise
i += 2;
} else {
return;
}
}
}
复制代码
为了使用异步可迭代对象,咱们要在请求下一个Promise前等待当前的Promise:
async function main() {
for (let promise of oddNums()) {
const x = await promise;
console.log(x);
}
console.log('done');
}
main();
// (Log shows:)
// 40
// 42
// 44
// 46
// 48
// done
复制代码
上面的例子很符合直觉,但它并非一个有效的ES2018异步可迭代对象。咱们在上面构造的是一个包含Promise的ES6可迭代对象,但ES2018异步可迭代对象是包着Promise的getter-getter,Promise返回的值是done, value
对象。将二者进行对比:
ES2018可迭代对象不是可迭代对象,它们只是基于Promise的getter-getter,在许多方面相似可迭代对象而已,这是反直觉的。这个细节上的差别是由于异步可迭代对象还须要异步地发送完成状态(done
),因此Promise必须包着整个{done, value}
对象。
由于异步可迭代对象不是可迭代对象,因此使用了不一样的Symbol。可迭代对象依赖Symbol.iterator
,而异步可迭代对象使用Symbol.asyncIterator
。咱们用了一个与前面相似的例子,实现了一个有效的ES2018异步可迭代对象:
const oddNums = {
[Symbol.asyncIterator]: () => {
let i = 40;
return {
next: () => {
if (i <= 48) {
const next = i;
i += 2;
return slowResolve({done: false, value: next});
} else {
return slowResolve({done: true});
}
}
};
}
};
async function main() {
let iter = oddNums[Symbol.asyncIterator]();
let done = false;
for (let promise = iter.next(); !done; promise = iter.next()) {
const result = await promise;
done = result.done;
if (!done) console.log(result.value);
}
console.log('done');
}
main();
复制代码
可迭代对象有function*
和for-let-of
语法糖,Promise有async-await
语法糖,ES2018中的异步可迭代对象一样有两个语法糖:
在下面的示例中,咱们使用这两个特性来建立异步数字序列,并在for-await循环中使用它们:
function sleep(period) {
return new Promise(resolve => {
setTimeout(() => resolve(true), period);
});
}
// 🔷 Production side can use both `await` and `yield`
async function* oddNums() {
let i = 40;
while (true) {
if (i <= 48) {
await sleep(1000);
yield i;
i += 2;
} else {
await sleep(1000);
return;
}
}
}
async function main() {
// 🔷 Consumption side uses the new syntax `for await`
for await (let x of oddNums()) {
console.log(x);
}
console.log('done');
}
main();
复制代码
尽管它们是新的特性,但异步可迭代对象的语法糖已被Babel,TypeScript,Firefox,Chrome,Safari以及Node.js支持。异步可迭代对象能够十分方便地与基于Promise的API相结合(例如fetch
)以建立异步序列,如一次请求一个用户并列举数据库中的用户:
async function* users(from, to) {
for (let x = from; x <= to; x++) {
const res = await fetch('http://jsonplaceholder.typicode.com/users/' + x);
const json = await res.json();
yield json;
}
}
async function main() {
for await (let x of users(1, 10)) {
console.log(x);
}
}
main();
复制代码
这篇文章中所列举的抽象只是JavaScript函数的简单特列。从定义上来讲,它们不会比函数更增强大,这使得函数成为最强大和灵活的抽象。彻底灵活的缺点是不可预测。这些抽象提供的是保证,基于保证你能够写出更易组织和更可预测的代码。
从另外一方面来讲,函数是一个JavaScript值,这容许在JavaScript中传递和修改它们。把函数看成值传递的能力还能被用于咱们在这篇文章中看到的抽象。咱们能将可迭代对象或可观察对象或异步可迭代对象做为值传递并在这个过程当中操做它们。
最多见的操做之一就是在数组中很流行的map
,但也可用于抽象中。下面的例子里,咱们为异步可迭代对象建立了map
操做符,并使用它建立一个包含用户名称的异步可迭代对象:
async function* users(from, to) {
for (let i = from; i <= to; i++) {
const res = await fetch('http://jsonplaceholder.typicode.com/users/' + i);
const json = await res.json();
yield json;
}
}
// 🔷 Map operator for AsyncIterables
async function* map(inputAsyncIter, f) {
for await (let x of inputAsyncIter) {
yield f(x);
}
}
async function main() {
const allUsers = users(1, 10);
// 🔷 Pass `allUsers` around, create a new AsyncIterable `names`
const names = map(allUsers, user => user.name);
for await (let name of names) {
console.log(name);
}
}
main();
复制代码
在没有Getter-Setter金字塔中的抽象的状况下编写上面的代码示例须要更多的代码,也更难阅读。如何利用这些函数特例的优势,以更少的代码完成更多功能,而不牺牲可读性?请使用运算符和新语法糖特性。