查看原文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金字塔中的抽象的狀況下編寫上面的代碼示例須要更多的代碼,也更難閱讀。如何利用這些函數特例的優勢,以更少的代碼完成更多功能,而不犧牲可讀性?請使用運算符和新語法糖特性。