原文:
https://staltz.com/javascript-getter-setter-pyramid.html
Functions
与硬编码相比。函数提供一些好处:
- 惰性/可复用
在函数里的代码是惰性的(他不会执行除非被调用)
- 实现灵活
函数的使用者并不关心函数内部是如何实现的,因此这意味着可以灵活地以各种方式实现函数。
Getter:
() => X
Getter没有输入值,并返回一个X值的函数
Getter是一种不需要参数传递,但是会返回一个值的函数,在JavaScript中有很多getter,比如Math.radom(),Date.now()等等,Getter 作为值的抽象也很有用。比如:
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(),那么用户对象就不会白白创建。
我们也得到了实现灵活的好处,因为我们可以通过多种不同的方式计算返回对象,或者通过创建一个普通对象,或者通过返回一个类的实例,或者通过使用原型上的属性等。使用硬编码的值,我们将没有这种灵活性。
Getter 还提供了可以执行副作用的hook。每当执行 getter 时,我们都可以触发有用的副作用,例如 console.log 或触发 Analytics 事件,例如:
function getUser() {
Analytics.sendEvent('User object is now being accessed');
return {name: 'Alice', age: 30};
}
getter 上的计算也可以是抽象的,因为函数可以作为 JavaScript 中的一等值传递。例如,考虑这个以 getter 作为参数的加法函数,并返回一个数字的 getter,而不是直接返回一个数字。
function add(getX, getY) {
return function getZ() {
const x = getX();
const y = getY();
return x + y;
}
}
当 getter 返回不可预测的值时,这种抽象计算的好处更加明显,例如使用 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 与 Promises 一起使用也很常见,因为已知 Promise 不是可重用的计算,因此将 Promise 构造函数包装在 getter(也称为“工厂”或“thunk”)中使其可重用。
const p = function() {
return new Promise(){}
}
GETTER GETTERS
() => (() => X)
getter-getter 是一个没有输入参数和返回一个 getter 的函数。
getter-getter的出现是为了解决遍历序列的问题。例如,如果我们想显示数字序列是 2 的幂,我们可以使用 getter getNextPowerOfTwo()
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。
getter-getters会继承getter的所以优点。比如:例如:(1)实现的灵活性,(2)副作用的钩子,(3)惰性。这次的惰性体现在初始化步骤中。外部函数可以延迟变量初始化,而内部函数可以延迟遍历值。
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
Iterables
() => (() => ({done, value}))
可迭代函数是特殊化的getter-getter,返回一个对象形式包含value和done的x值。
getter-getter 能够表示可重新启动的值序列,但它们没有约定来表示序列的结束。
返回的迭代对象的done值,可以让外部知道何时终止迭代。
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);
}
ES6 的迭代器还有进一步约定这个getter-getters的实现,他们会加一个包装对象给每一个getter
- 外部 getter f 会变成一个对象 { [Symbol.iteration]: f }
- 内部 getter 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 getOddNum = oddNums[Symbol.iterator]();
for (let result = getOddNum.next(); !result.done; result = getOddNum.next()) {
console.log(result.value);
}
以这种形式实现就能用到ES6 提供语法糖 for-let-of能更方便地使用 Iterables:(省略next调用并当done为true的时候终止循环)
for (let x of oddNums) {
console.log(x);
}
为了轻松创建 Iterables,ES6 还提供了生成迭代对象函数语法糖函数*:(即省略掉[Symbol.iterator]对象函数和return的next对象函数,并简化 return{ value: any, done: boolean}, 直接yield后面是value,结束语句return即可)
function* oddNums() {
let i = 40;
while (true) {
if (i <= 48) {
const value= i;
i += 2;
yield value;
} else {
return;
}
}
}
}
for (let x of oddNums()) {
console.log(x);
}
自es2015,有了创建迭代对象的语法糖函数function*和迭代迭代对象的语法糖for…of就变得更方便的创建迭代。
const oddNums= {
[Symbol.iterator](){
let i = 40;
let outside, error
return {
next(outside){
try{
if (i <= 48) {
const next = i ;
i += 2;
outside = outside
if(error) throw(error)
return {done: false, value: next};
} else {
return {done: true};
}
}catch(e){
console.log(e)
}
},
error(str){
error = str
this.next()
}
}
}
}
let getOddNum = oddNums[Symbol.iterator]();
for (let result = getOddNum.next(); !result.done; result = getOddNum.next()) {
console.log(result.value);
}
Setters:
X => ()
setter 是一个以 X 作为输入但没有输出的函数
Setter 是一种函数,接受参数,但不返回输出值。 JavaScript 运行时和 DOM 中有许多原生的 setter,例如 console.log(x)、document.write(x) 等。
与 getter 不同,setter 通常不是抽象,因为如果函数没有值,则意味着该函数仅用于发送数据或命令 JavaScript 运行时。
对于我们的例子,getTen是一个对于10的抽象,我们可以传递getter作为一个值,但是传一个setTen是没有意义的,因为你不会从调用函数中拿到什么值。
这就是说setters可以是其他 setter 的简单包装器。考虑 console.log 设置器的这个包装器:
function fancyConsoleLog(str) {
console.log('⭐ ' + str + ' ⭐');
}
Setter Setters
(X => ()) => ()
setter-setter 是一个以 setter 作为输入但没有输出的函数。
虽然基本的 setter 不是抽象函数(console.log),但 setter-setter 能在代码库当中作为如何利用呈现这个值的抽象函数(fn(cb){cb(val)})。
例如,考虑如何通过这个 setter-setter 来表示数字 10:
function setSetTen(setTen) {
setTen(10)
}
注意这里没有返回值,因为setter永远不会返回值。通过简单地重命名一些参数,上面的示例可能更具可读性:
function setTenListener(cb) {
cb(10)
}
顾名思义,cb 代表“回调”,并说明了 setter-setter 在 JavaScript 中是如何常见的,因为有大量的回调用例。
调用一个setter-setter的抽象函数,他的相反方法是调用一个getter
setter(setter)
setter(getter())
下面的这两个示例在功能上是等效的,但调用方式却大不相同。
setSetTen(console.log);
// compare with...
console.log(getTen());
setter-setter 的好处与 getter 相同——惰性、实现灵活性、副作用挂钩——但有两个 getter 没有的新属性:控制反转(函数里面决定如何调用那个传进来的函数,setter作为参数传进来,再setter里面再调用)和异步性。而getter就需要先调用转化为值之后再传给setter。
上面的示例中,使用 getter 的代码指示 getter 何时与 console.log 一起使用。但是,当使用 setter-setter 时,是 setter-setter 本身决定何时调用 console.log。这种控制反转允许 setter-setter 比 getter 拥有更多的权力,例如通过向setter内部发送许多值给传递进来的setter:
function setSetTen(setTen) {
setTen(10)
setTen(10)
setTen(10)
setTen(10)
}
控制反转还允许 setter-setter 决定何时向回调传递值,例如异步传递。回想一下 setSetTen 的另一个名称可能是 setTenListener:
function setTenListener(cb) {
setTimeout(() => { cb(10); }, 1000);
}
Promise
(X => (), Err => ()) => ()
Promise是接受两个setter的setter-setter函数,并有一些额外保证。
虽然 setter-setter 功能强大,但由于控制反转,它们可能非常不可预测。它们可以是同步的或异步的,并且可以随时间传递零个或一个或多个值。
Promise 是一种特殊的 setter-setter,它为值的传递提供一些保证。
- 内部setter不会同步调用。
- 内部setter只会调用一次。
- 提供一个可选的第二个setter来传递错误信息
对比:
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提供能生成Promise和使用promise的语法糖,async-await,async会返回一个由Promise包裹的值,而await能解开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
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
OBSERVABLES
Observables
(X => (), Err => (), () => ()) => ()
Observable是接受三个setter的setter-setter函数,并有一些额外的保证。
就像 Iterables 是一种特殊类型的 getter-getter 增加了结束的标志,Observables 也是一种增加了用来结束的 setter-setter。JavaScript 中典型的 setter-setter,如 element.addEventListener,不通知事件流是否完成,因此这使得连接事件流或执行其他与完成相关的逻辑变得困难。
不像Iterables,有标准化的js定义,Observables在TC39有过提议,但还没被接受。
Observables 是 Iterables 的对偶,这可以通过一些对称性看出
-
Iterable
- 是对象
- 有“iterate” 方法, a.k.a.
Symbol.iterator
- “iterate” 方法是一个Iterator对象里的一个getter方法
- 返回的Iterator 对象会有一个next方法的getter
-
Observable
- 是对象
- 有“observe” 方法, a.k.a.
subscribe
- “observe” 方法是一个Observer 对象里的一个setter方法
- 返回的Observer对象会有一个next方法的setter
function setter-setter
function setSetTen(next, err, complete) {
let x = 40;
let clock = setInterval(() => {
if (x <= 48) {
next(x);
x += 2;
} else {
complete();
clearInterval(clock);
}
}, 1000);
return function() {
clearInterval(clock)
}
}
将传进来的三个函数转化为observe对象:
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'),
});
与 setter-setter 一样,Observables 会导致控制反转,因此调用代码(oddNums.subscribe)无法暂停或取消传入的数据流。
大多数 Observable 实现添加了一个重要的细节,以允许从消费者到生产者的取消传输订阅。
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);
Async Iterables
() => (() => Promise<{done, value}>)
异步迭代函数就像一个yield promise的迭代。
Iterables 可以表示任何无限或有限的值序列,但它们有一个限制:一旦消费者调用 next() 方法,该值必须同步可用。AsyncIterables 通过允许“稍后”而不是在请求时立即传递值来扩展 Iterables 的功能。
AsyncIterables 使用 Promise 实现值的异步传递,因为 Promise 代表单个异步值。可以通过调用这个Promise后在value传递(yield就可以传递这个)。
在下面的示例中,我们采用oddNums Iterable 示例并使其产生延迟后解析的值的Promises:
function slowResolve(val) {
return new Promise(resolve => {
setTimeout(() => resolve(val), 1000);
});
}
function* oddNums() {
let i = 40;
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;
}
}
}
要使用 AsyncIterable,我们可以在请求下一个 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
上面的示例为 AsyncIterables 创建了一个很直观,但它实际上不是一个有效的 ES2018 AsyncIterables。我们上面所做的是一个 ES6 Iterable of Promises,但 ES2018 AsyncIterables 是一个 {done, value} 对象的 Promise 的 getter-getter。比较这两个:
- Promise 的可迭代: () => (() => {done, value: Promise<X>})
- ES2018 AsyncIterable: () => (() => Promise<{done, value}>)
这是因为需要允许返回的done也可能是异步的,所以是用Promise去返回整个{done, value}。
因为AsyncIterable不是有效的Iterable,所以是用另一个Symbol实现[Symbol.asyncIterator]
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();
跟普通Iterable一样,也有生产端和消费端的语法糖
function* -> async function *
for..of -> await for..of
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();
这对于迭代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();
Operators
Function也可以像值一样传递给函数,无论是上面的Observables or AsyncIterables,所以我们也能在别的函数里操纵他们来实现功能。
最常见的操作之一是 map,它在数组中很流行,但也与其他抽象相关。在下面的示例中,我们为 AsyncIterables 创建了 map 运算符,并使用它来创建数据库中用户名的 AsyncIterable。
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();