JavaScript?getter?setter金字塔???????
引言
英文鏈接: staltz.com/javascript-…
函數(shù)是JavaScript的基石。 它是一種靈活的、抽象的,可作為其他抽象的基礎, 如:Promise、Iterables、Observables等。 之前我在會議或研討會中傳遞這些概念,隨著時間的推移,我發(fā)現(xiàn)這些抽象的概念可以抽象為金字塔狀排列的模型,在本文中,將介紹金字搭中的各層。
函數(shù)
JavaScript的基礎是一級值,如: numbers、strings、objects、booleans等。 雖然,您可以只使用這些值和控制流(if/else/for等)來編寫程序,但很快您可能需要編寫一個新的函數(shù)來改進您的程序。
JavaScript中必須通過函數(shù)進行抽象,像異步I/O操作等通常都必須使用callback回調(diào)函數(shù)。 JavaScript中的函數(shù),與【函數(shù)式編程】中的純函數(shù)
不同,建議最好將它們簡單理解為“流程”,因為它只得惰性的可重用代碼塊,具有可選的輸入(參數(shù))和可選的輸出(返回值)
與硬編碼相比,函數(shù)有以下幾個重要的好處:
- 懶惰/可重用性, 函數(shù)內(nèi)的代碼必須是惰性的(即除非被調(diào)用,否則不會執(zhí)行)才能使其可重用
- 實現(xiàn)的靈活性,函數(shù)的使用者并不關心函數(shù)內(nèi)部是如何實現(xiàn)的,這意味著可以各種方式靈活的實現(xiàn)函數(shù)。
getters
getter是一種不用傳遞任何參數(shù)但需要有返回值的函數(shù)。
函數(shù)定義: () => X
getter是一種不用傳遞任何參數(shù)但需要有返回值的函數(shù)。 JavaScript中有許多這樣的getter, 例如: Math.random()、Date.now()等
getter作為值的抽象也很有用,例如:user
與 getUser
:
const user = {name: 'Alice', age:30}; console.log(user.name); // Alice function getUser(){ return {name: 'Alice', age:30}; } console.log(getUser().name); // Alice
通過使用getter, 我們可以利用函數(shù)的一個好處: 惰性
. 即我們不調(diào)用 getUser()
,user
對象就不會被白白創(chuàng)建。 同時,我們利用了函數(shù)的另一好處:實現(xiàn)的靈活性, 因為我們可以通過多種不同的方式來返回對象。
getter還允許我們對副作用有一個鉤子,當執(zhí)行getter時,我們可以觸發(fā)有用的副作用函數(shù),比如console.log
輸出日志 或是 觸發(fā)Analytics事件等,例如:
function getUser(){ Analytics.sendEvent('User object is now being accessed'); return {name:'Alice', age:30}; }
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與Promise一起搭配使用,因為Promise是不可重用的計算,因此,將Promise的構造函數(shù)包裝在一個getter中(如我們熟知的"工廠函數(shù)"或"thunk")使其可重用。
setters
setters是有一個或多個輸入?yún)?shù),但沒有返回值的函數(shù)。
函數(shù)定義: X => ()
setters是有一個或多個輸入?yún)?shù),但沒有返回值的函數(shù)。 在JavaScript運行時或DOM中有許多這樣的setters, 比如: console.log(x), document.write(x)
等。
與getter不同,setter通常不用抽象。 因為函數(shù)沒有返回值,這意味著函數(shù)僅用于發(fā)送數(shù)據(jù)或執(zhí)行JavaScript命令等。 例如, 上文中的getter getTen
是數(shù)字10的抽象,我們可以將它作為值進行傳遞,但如果將函數(shù)setTen
作為值傳遞是沒有意義的,因為它沒有返回值。
換句話說,setter可以是其他setter的簡單包裝器。例如:簡單包裝下setter console.log
:
function fancyConsoleLog(str) { console.log('? ' + str + ' ?'); }
getter-getters
getter-getters是一種不需要輸入?yún)?shù)且返回一個getter的函數(shù)。
函數(shù)定義: () => (() => X)
“getter-getter”: 一種特殊類型的getter,其返回值是一個getter.
對getter的原始需求是使用getter
的返回值進行迭代。
例如,我們想顯示二次冪的數(shù)字序列,我們可以使用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
是全局聲明的,若我們想重新迭代序列,則必須要重置變量i
, 從而泄漏了getter的實現(xiàn)細節(jié)。
若想將上述代碼變?yōu)榭芍赜们也话肿兞?,我們需要?strong>getter封裝在另一個函數(shù)中,這個包裝函數(shù)也是一個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的所有優(yōu)點,例如:
- 實現(xiàn)的靈活性
- 用于副作用的鉤子(hook)
- 惰性,其惰性體現(xiàn)在函數(shù)在初始化時,外部函數(shù)啟用延遲初始化,而內(nèi)部函數(shù)啟用值時延遲初始化。
function getGetNext() { // ?? LAZY INITIALIZATION let i = 2; return function getNext() { // ?? LAZY ITERATION const next = i; i = i * 2; return next; } }
setter-setter
setter-setter是一種以setter作為輸入且無返回值的函數(shù)。 函數(shù)定義: (X => ()) => ()
setter-setter
是一種特殊的setter函數(shù),其中傳遞的參數(shù)也是setter。雖然基本的setter不是抽象函數(shù),但setter-setter
是能夠表示可在代碼庫中傳遞的值的抽象。 例如下列中,通過這個setter來表示數(shù)字10:
function setSetTen(setTen) { setTen(10) }
注意: setter沒有返回值。
通過簡單地重構,讓上述示例代碼的可讀性更強:
function setTenListener(cb) { cb(10) }
cb
,即callback縮寫,名為“回調(diào)”,它表明在JavaScript中如何使用setter-setter
。
在JavaScript中,有大量回調(diào)的案例。 顧名思義,cb
代表“回調(diào)”,通過上述示例代碼來說明,cb在setter-setter
如何使用。
下面兩個示例,在功能上是等價的,但是調(diào)用方式不同。
setSetTen(console.log); // compare with... console.log(getTen());
setter-setter
具有與getter-getter
相同的好處:
- 實現(xiàn)的靈活性
- 用于副作用的鉤子(hook)
- 惰性,其惰性體現(xiàn)在函數(shù)在初始化時,外部函數(shù)啟用延遲初始化,而內(nèi)部函數(shù)啟用值時延遲初始化。
同時,它還有兩個getter-getter
所沒有的新特性:
- 控制反轉
- 異步性
在上面的示例中,使用getter的代碼指示何時使用“console.log”使用getter。
然而,當使用setter-setter
時,setter-seter
本身決定何時調(diào)用“console.log”。 這種控制反轉允許setter-setter擁有比getter更大的能力,例如通過向回調(diào)函數(shù)傳遞許多值, 例如:
function setSetTen(setTen) { setTen(10) setTen(10) setTen(10) setTen(10) }
控制反轉還可以使setter
決定何時向回調(diào)傳遞值,例如: 異步傳遞
。 試想一下,若將“setSetTen”的更換為“setTenListener”呢?
function setTenListener(cb) { setTimeout(() => { cb(10); }, 1000); }
setter-setter
在JavaScript異步編程中的很常見,但回調(diào)函數(shù)不一定是異步的。
例如,下面的“setSetTen”示例中,它是同步的:
function setSetTen(setTen) { setTen(10) } console.log('before'); setSetTen(console.log); console.log('after'); // (Log shows:) // before // 10 // after
iterables
iterable是省略了實現(xiàn)細節(jié)的getter-getter, 其返回值是一個描述型的對象值或完成時的對象。
An iterable is (with some details omitted:) a getter-getter of an object that describes either a value or completion
函數(shù)定義: () => (() => ({done, value}))
getter-getter
可以重啟序列值,但它沒有通知序列何時結束的約定。
Iterables是一種特殊的getter-getter
,它的返回值中始終有2個屬性對象:
- done,布爾值,表示是否完成
- value,任意值,實際傳遞的值(在done的值不為true時)
完成指示符done
,能夠使iterable的代碼在執(zhí)行時,知道后續(xù)GET將返回無效數(shù)據(jù),因此iterable知道何時停止迭代。
在下面的示例中,我們可以根據(jù)完成指示符生成一個偶數(shù)范圍為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); }
ES6 Iterables除了簡單的() => (() => ({done, value}))
模式之外,還有更多的約定。它們在每個getter上添加了一個包裝器對象:
- 外部getter包含對象:
{[Symbol.iterator]:f}
- 內(nèi)部getter包含對象:
{next:g}
以下是與上一個示例代碼功能等價的ES6 Iterable
代碼:
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); }
注意,兩個示例間實現(xiàn)的差異點:
-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
以便快速迭代對象:
for (let x of oddNums) { console.log(x); }
為了方便創(chuàng)建Iterables,ES6還提供了生成器函數(shù)語法糖 function*
:
function* oddNums() { let i = 40; while (true) { if (i <= 48) { const next = i; i += 2; yield next; } else { return; } } }
使用生產(chǎn)端語法糖和**消費端語法糖*,自2015年以來,可迭代函數(shù)是JavaScript中可完成值序列的易于使用的抽象。請注意,調(diào)用生成器函數(shù)將返回可迭代函數(shù),生成器函數(shù)本身不是可迭代函數(shù):
自2015年后,更易于使用iterables來抽象可序列化對象值。 需要注意的是,調(diào)用生成器函數(shù)將返回一個可迭代對象,生成器函數(shù)本身不可迭代。
function* oddNums() { let i = 40; while (true) { if (i <= 48) { yield i; i += 2; } else { return; } } } for (let x of oddNums()) { console.log(x); }
promises
Promise是一個(省略了一些細節(jié))特殊的setter, 它包含2個setter, 并且還有額外的保障。
函數(shù)定義: (X => (), Err => ()) => ()
雖然setter-setter功能強大,但由于控制反轉
,它們可能變得非常不可預測。
- 它們可以是同步或異步的
- 也可以隨時間傳遞零或一個或多個值。
Promise是一種特殊的setter,它為返回值提供一些特殊的保證:
- 內(nèi)部setter(“回調(diào)”)從不同步調(diào)用
- 內(nèi)部setter最多調(diào)用一次
- 提供了一個可選的第二setter,用于傳遞錯誤值
將下面的setter與等價的Promise代碼比較,可以看出:
- Promise只提供一次值,且不會在兩個
console.log
輸出 - Promise的返回值是異步返回的:
setter-setter的實現(xiàn):
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
promise的實現(xiàn):
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
語法糖來編寫Promise。 在函數(shù)前使用async
關鍵字,在需要使用Promise值的位置使用await
來接收值。
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
也可用于創(chuàng)建Promise, 因為async
函數(shù)將返回一個Promise對象,該Promise對象將返回return
的值。
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
Observables
observable是(省略了一些細節(jié):)一個包含有3個setter的setter函數(shù),并帶有額外的保障。
函數(shù)定義: (X => (), Err => (), () => ()) => ()
GetterSetterFunctionValue
像Iterables一樣,GetterSetterFunctionValue是一種特殊類型的getter-getter,它增加了發(fā)送完成信號的能力,Observable也是一種setter-setter,它也增加了完成能力。
JavaScript中的典型setter-setter
,如: element.addEventListener
不會通知事件流是否完成,因此很難連接事件流或執(zhí)行其他與完成相關的邏輯。
與JavaScript規(guī)范中標準化的可伸縮性不同,可觀測性是在幾個庫中找到的松散約定,如:
- RxJS
- most.js
- xstream
- Bacon.js
盡管正在考慮將proposal-observable作為一個TC39方案,但該方案仍在不斷修改。
因此在本文中,讓我們假設遵循Fantasy Observable規(guī)范(其中RxJS、most.js和xstream等最受歡迎庫都遵循這一規(guī)范)。
Observables the dual of Iterables,有以下特性:
可迭代:
- 它是一個對象
- 可迭代,有一個“iterate”屬性方法,即:
Symbol.iterator
- “iterate” 方法是迭代器對象的 getter
- 迭代器對象有一個名為
next
的 getter
可觀察:
- 它是一個對象
- 有可觀察method,即:
subscribe
- 可觀察method是Observer對象的setter
- Observer有一個名為
next
的setter
Observer對象還可以包含另外兩個方法:
- complete,表示成功完成,相當于iterator中的“done”指示符
- error,表示失敗完成,相當iterator中執(zhí)行時引發(fā)異常
與Promise一樣,Observable為返回值增加了一些保障:
- 一旦調(diào)用了complete,則不會調(diào)用error
- 一旦調(diào)用了error,就不會調(diào)用complete
- 一旦調(diào)用了complete或error,則不會調(diào)用
next
在下面的示例中,Observable用于異步返回有限的數(shù)字序列
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一樣,Observable具有控制反轉的能力,因此消費端(oddNums.subscribe
)無法暫?;蛉∠麄魅氲臄?shù)據(jù)流。
大多數(shù)Observable的實現(xiàn)都添加了一個重要的細節(jié),允許消費者取消訂閱: unsubscribe
。
“subscribe”函數(shù)將返回一個對象subscription,即“unsubscribe”。 消費端可以使用該方法取消訂閱。
因此,“subscribe”不再是setter,因為它是一個具有輸入(觀察者)和輸出(訂閱)的函數(shù)。
下面,我們在前面的示例中添加了一個訂閱對象:
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
async iterables
異步可迭代函數(shù)(省略了一些細節(jié))類似于yield promise的可迭代函數(shù)
函數(shù)定義: () => (() => Promise<{done, value}>)
Iterables可以表示任何無限或有限的值序列,但它們有一個限制:
- 只要使用者調(diào)用
next()
方法,返回值就必須是同步的。
AsyncIterables擴展了Iterables的功能,它通過允許延后返回值,而不是在調(diào)用時立即返回。
AsyncIterables通過使用Promise實現(xiàn)值的異步傳遞,因為Promise表示單個異步值。每次調(diào)用迭代器的next()
(內(nèi)部getter函數(shù)),都會創(chuàng)建并返回一個Promise。 在下面的示例中,我們以oddNums
可迭代為例,其在迭代時每次返回一個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; } } }
要使用Asynciteable,我們可以使用async - await
語法糖,來遍歷&獲取異步迭代器的值:
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
雖然上例中通過async - await
語法糖(ES6語法)能夠編寫出可讀性較好的代碼。但是,在ES2018中,異步迭代是通過{done,value}
(Promise)對象來迭代的。
比較一下ES6&ES2018異步迭代器的函數(shù)定義:
- ES6異步迭代器:
() => (() => {done, value: Promise<X>})
- ES2018異步迭代器:
() => (() => Promise<{done, value}>)
可以看出,ES2018異步可迭代函數(shù)返回的不是一個可迭代函數(shù),它將返回一個Promise對象,但在許多方面與ES6異步迭代器相似。
這一細節(jié)的原因是,異步可伸縮性還需要允許異步發(fā)送完成(done),因此Promise必須包裹{done,value}
對象。
因為AsyncIterables不是Iterables,所以它們使用不同的符號。
- Iterables 使用
Symbol.iterator
- AsyncIterables 使用'Symbol.asyncItrator'
在下面的示例中,我們使用ES2018 AsyncIterable實現(xiàn)了一個前例等價的功能:
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();
像Iterables有語法糖function*
和for–let–of
,以及Promises有async–await
語法糖一樣,ES2018中的AsyncIterable有兩個語法糖功能:
- 生產(chǎn)端:
async function*
- 消費端:
for–await–let–of
在下面的示例中,我們使用這兩種功能創(chuàng)建異步數(shù)字序列,并使用for-await
循環(huán)遍歷它們:
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中都已經(jīng)支持了。
AsyncIterables可以方便地結合基于Promise的API(如fetch
)來創(chuàng)建異步序列。 例如在數(shù)據(jù)庫中列出用戶,每次請求一個用戶:
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
在本文中,抽象的列舉了一些特殊的JavaScript函數(shù)示例。 根據(jù)定義,它們的功能的不可能比函數(shù)更強大。因此,函數(shù)是最強大、最靈活的抽象。完全靈活性的缺點是不可預測性,這些抽象提供的是“保證”,基于這些“保證”,讓您可以編寫更有組件、更可預測的代碼。
另一方向, 函數(shù)可以作為JavaScript的值,允許將它進行傳遞和操縱。
在本文中,我們將Iterable、Observable、AsyncIterable作為值進行傳遞,并在傳遞過程中對其進行操作。
最常見的操作之一是map
映射,它在數(shù)組中經(jīng)常使用到,但也與其他抽象相關。 在下面的示例中,我們?yōu)锳syncIterables創(chuàng)建map
操作符,并使用它用來創(chuàng)建異步迭代器,以獲取數(shù)據(jù)庫的用戶信息。
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
金字塔中。
若使用getter - setter
來實現(xiàn),需要更多的代碼,可讀性也不好。
因此,在不犧牲可讀性的情況下,使用運算符和新的語法糖,用更少的代碼編寫函數(shù)做更多的事情,以處理這種特殊的場景。
到此這篇關于JavaScript getter setter金字塔的文章就介紹到這了,更多相關JavaScript getter-setter金字塔內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
javascript結合CSS實現(xiàn)蘋果開關按鈕特效
這篇文章主要介紹了javascript結合CSS實現(xiàn)蘋果開關按鈕特效的方法以及全部代碼,效果非常不錯,兼容性也很好,有需要的小伙伴自己參考下2015-04-04JS中setTimeout和setInterval的最大延時值詳解
這篇文章主要介紹了JS中setTimeout和setInterval的最大延時值的相關資料,文中通過示例代碼介紹的很詳細,相信對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-02-02用js模擬struts2的多action調(diào)用示例
這篇文章主要介紹了用js模擬struts2的多action調(diào)用的實現(xiàn)過程,需要的朋友可以參考下2014-05-05