從延遲處理解析JavaScript惰性編程
前文回顧
# ?從歷史講起,JavaScript 基因里寫著函數(shù)式編程
# ?從柯里化講起,一網(wǎng)打盡 JavaScript 重要的高階函數(shù)
我們從閉包起源開(kāi)始、再到百變柯里化等一票高階函數(shù),再講到純函數(shù)、純函數(shù)的組合以及簡(jiǎn)化演算;
學(xué)到了:
- 閉包的設(shè)計(jì)就是因?yàn)?lambda 表達(dá)式只能接受一個(gè)參數(shù)的設(shè)計(jì)導(dǎo)致的,誕生 1930 ;
- 柯里化是閉包的孿生子,柯里化思想是高階函數(shù)的重要指導(dǎo);
- 原來(lái)編程函數(shù)也可以和數(shù)學(xué)函數(shù)一樣運(yùn)算推導(dǎo),無(wú)副作用的純函數(shù)、函數(shù)組合,代碼更易讀;
本篇將展開(kāi)“延遲處理”這一話題,閑言少敘,沖了~
延遲處理
認(rèn)真讀前面幾篇,雖然沒(méi)有專門講“延遲處理”,但實(shí)際上處處都體現(xiàn)著“延遲處理”。
首先閉包是延遲處理:函數(shù)在聲明的時(shí)候,確定了上下作用域關(guān)系。比如以下代碼:
function addA(A){ return function(B){ return B+A } } let count = addA(7) console.log(count(8)) // 15
調(diào)用 addA(7)
函數(shù),它說(shuō):我并不會(huì)執(zhí)行運(yùn)算,而會(huì)返回給你一個(gè)新的函數(shù),以及一個(gè)“閉包”,這個(gè)閉包里面是被引用的變量值。等到時(shí)候你要計(jì)算的時(shí)候,再?gòu)倪@里面拿值就行了~
其次,柯里化和閉包同宗同源,由 add(1,2,3)
柯里化為 add(1)(2)(3)()
,在判定最后的參數(shù)為空之前,都是一個(gè)待執(zhí)行的函數(shù),不會(huì)進(jìn)行真正的運(yùn)算處理。
function addCurry() { let arr = [...arguments] let fn = function () { if(arguments.length === 0) { return arr.reduce((a, b) => a + b) // 當(dāng)參數(shù)為空時(shí)才執(zhí)行求和計(jì)算; } else { arr.push(...arguments) return fn } } return fn }
接著,純函數(shù)中,我們不能保證一直寫出不帶副作用的函數(shù),HTTP 操作/ IO 操作/ DOM 操作等這些行為是業(yè)務(wù)場(chǎng)景必做的,于是想了個(gè)法子:用一個(gè)“盒子”把不純的函數(shù)包裹住,然后一個(gè)盒子連著一個(gè)盒子聲明調(diào)用關(guān)系,直到最后執(zhí)行 monad.value()
時(shí)才會(huì)暴露出副作用,盡最大可能的限制住了副作用的影響,延遲了它的影響。
所以,“延遲處理”思想幾乎是根植在函數(shù)式編程的每一個(gè)要點(diǎn)中~
還沒(méi)完,從專欄的整體角度來(lái)看,至此行文已到中段,除了圍繞“閉包”這一核心點(diǎn),另外一個(gè)核心點(diǎn)“異步”也要逐漸拉開(kāi)帷幕、閃亮登場(chǎng)。
延遲處理是在函數(shù)式編程背景下連接 JavaScript 閉包和異步兩大核心的重要橋梁。
惰性求值
“延遲處理”在函數(shù)式編程語(yǔ)言中還有一個(gè)更加官方、學(xué)術(shù)的名稱,即“惰性求值”。
??我們不妨再用一段代碼作簡(jiǎn)要示例:
// 示例代碼 1
const myFunction = function(a, b, c) { let result1 = longCalculation1(a,b); let result2 = longCalculation2(b,c); let result3 = longCalculation3(a,c); if (result1 < 10) { return result1; } else if (result2 < 100) { return result2; } else { return result3; } }
這是一段求值函數(shù),result1、result2、result3
依次經(jīng)過(guò)一段長(zhǎng)運(yùn)算,然后再走一段條件判斷,return 結(jié)果;
這段代碼的不合理之處在于,每次調(diào)用 myFunction()
都要把 3 個(gè) longCalculation
計(jì)算,很耗時(shí),結(jié)果只需要得到其中的某一個(gè)運(yùn)算結(jié)果。
于是,根據(jù)問(wèn)題,我們優(yōu)化代碼策略為:需要用到哪個(gè)計(jì)算,才計(jì)算哪個(gè)。(言外之意:惰性求值)
// 示例代碼 2
const myFunction = function(a, b, c) { let result1 = longCalculation1(a,b); if (result1 < 10) { return result1; } else { let result2 = longCalculation2(b,c); if (result2 < 100) { return result2; } else { let result3 = longCalculation3(a,c); return result3; } } }
優(yōu)化后的這個(gè)寫法在邏輯上更合理,但是 if...else...
嵌套總讓人看的難受。
因?yàn)?JavaScript 本身不是惰性求值語(yǔ)言,它和比如 C 語(yǔ)言這類主流語(yǔ)言一樣,是【及早求值】,惰性求值語(yǔ)言有比如 Haskell 這類純粹的函數(shù)式編程語(yǔ)言,用 Haskell 實(shí)現(xiàn)上述函數(shù)為:
myFunction :: Int -> Int -> Int -> Int myFunction a b c = let result1 = longCalculation1 a b result2 = longCalculation2 b c result3 = longCalculation3 a c in if result1 < 10 then result1 else if result2 < 100 then result2 else result3
看上去,這似乎和 JavaScript 示例代碼 1 一樣,但是它實(shí)際上實(shí)現(xiàn)的卻是 JavaScript 示例代碼 2 的效果;
在 GHC 編譯器中,result1, result2, 和 result3 被存儲(chǔ)為 “thunk” ,并且編譯器知道在什么情況下,才需要去計(jì)算結(jié)果,否則將不會(huì)提前去計(jì)算!這太牛皮了~
在《Haskell 函數(shù)式編程入門》,thunk 被解釋為:
thunk 意為形實(shí)替換程序(有時(shí)候也稱為延遲計(jì)算,suspended computation)。它指的是在計(jì)算的過(guò)程中,一些函數(shù)的參數(shù)或者一些結(jié)果通過(guò)一段程序來(lái)代表,這被稱為 thunk??梢院?jiǎn)單地把 thunk 看做是一個(gè)未求得完全結(jié)果的表達(dá)式與求得該表達(dá)式結(jié)果所需要的環(huán)境變量組成的函數(shù),這個(gè)表達(dá)式與環(huán)境變量形成了一個(gè)無(wú)參數(shù)的閉包(parameterless closure) ,所以 thunk 中有求得這個(gè)表達(dá)式所需要的所有信息,只是在不需要的時(shí)候不求而已。
雖然 JavaScript 本身語(yǔ)言的設(shè)計(jì)不是惰性求值,但并不意味著它不能用惰性的思想來(lái)編程~
從惰性編程的角度來(lái)思考問(wèn)題,可以消除代碼中不必要的計(jì)算,也可以幫你重構(gòu)程序,使之能更加直接地面向問(wèn)題。
惰性編程
什么是惰性編程?
惰性編程是一種將對(duì)函數(shù)或請(qǐng)求的處理延遲到真正需要結(jié)果時(shí)進(jìn)行的通用概念。
有很多應(yīng)用程序都采用了這種概念,有的非常明顯,有些則不太明顯。
比如 JavaScript 的“父親” Scheme 中就有簡(jiǎn)單的惰性編程,它有兩個(gè)特殊的結(jié)構(gòu),delay
和 force
,delay 接收一個(gè)代碼塊,不會(huì)立即執(zhí)行它們,而是將代碼和參數(shù)作為一個(gè) promise 存儲(chǔ)起來(lái)。而 force promise 則會(huì)運(yùn)行這段代碼,產(chǎn)生一個(gè)返回值;
這里提到 promise?在 JS 中也有 Promise,它是 JS 實(shí)現(xiàn)惰性的關(guān)鍵嗎?
我們不妨用代碼來(lái)測(cè)試一下:
const st=()=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log("done promise") resolve(true) },1000) }) } let a = st() console.log(a)
可以看到,Promise
并不是惰性的,它一旦執(zhí)行,狀態(tài)就轉(zhuǎn)為 Pending
,不能暫停。我們無(wú)法知道 Promise
是剛開(kāi)始執(zhí)行,或者是快執(zhí)行完了,還是其它哪個(gè)具體執(zhí)行階段;內(nèi)部的異步任務(wù)就已經(jīng)啟動(dòng)了,執(zhí)行無(wú)法中途取消;這些問(wèn)題也是面試中??嫉?Promise 的缺點(diǎn)有哪些。
好在,后來(lái),Generator
函數(shù)的出現(xiàn),把 JavaScript 異步編程帶入了一個(gè)全新的階段。
ES6 引入的 Generator ,為 JavaScript 賦予了惰性的能力! ??
Generator
Thunk
Generator
就像是 Haskell 中的 thunk
,賦值的時(shí)候,我不進(jìn)行計(jì)算,把你包裝成一個(gè) <suspended>
暫停等待,等你調(diào)用 next()
的時(shí)候,我再計(jì)算;
function* gen(x){ const y = yield x + 6; return y; } const g = gen(1); g.next() // { value: 7, done: false } g.next() // { value: undefined, done: true }
調(diào)用 Generator
函數(shù)后,該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象,也就是遍歷器對(duì)象。下一步,必須調(diào)用遍歷器對(duì)象的 next
方法,使得指針移向下一個(gè)狀態(tài)。
在異步場(chǎng)景下同樣適用,將上述 promise 的測(cè)試代碼改造為:
function * st1(){ setTimeout(()=>{ console.log("done promise") },1000) yield("done promise") } let aThunk = st1() console.log(aThunk)
只有執(zhí)行 aThunk.next()
時(shí),異步才開(kāi)始執(zhí)行。
迭代生成器
Promise 不能隨用隨停,而 Generator 可以。我們通過(guò) Generator 生成的序列值是可以迭代的,迭代過(guò)程可以操作,比方說(shuō)在循環(huán)中迭代生成器:
//基本的生成器函數(shù)產(chǎn)生序列值。 function* gen(){ yield 'first'; yield 'second'; yield 'third'; } //創(chuàng)建生成器。 var generator = gen(); //循環(huán)直到序列結(jié)束。 while(true) { //獲取序列中的下一項(xiàng)。 let item = generator.next(); //下一個(gè)值等于 'third' 嗎 if(item.value === 'third') { break; } console.log('while', item.value); }
當(dāng) item.value === 'third'
,break 跳出循環(huán),迭代結(jié)束。
循環(huán)+請(qǐng)求
綜合循環(huán)和異步的問(wèn)題,拋一個(gè)經(jīng)典的面試題:
如何依次請(qǐng)求一個(gè) api 數(shù)組中的接口,需保證一個(gè)請(qǐng)求結(jié)束后才開(kāi)始另一個(gè)請(qǐng)求?
代碼實(shí)現(xiàn)如下:
async function* generateSequence(items) { for (const i of items) { await new Promise(resolve => setTimeout(resolve, i)); yield i; } } (async () => { let generator = generateSequence(['3000','8000','1000','4000']); for await (let value of generator) { console.log(value); } })();
這里用 setTimeout 模擬了異步請(qǐng)求,代碼可復(fù)制到控制臺(tái)中自行跑一跑、試一試。
無(wú)限序列
在函數(shù)式編程語(yǔ)言中有一個(gè)特殊的數(shù)據(jù)結(jié)構(gòu) —— 無(wú)限列表,Generator 也可以幫助 JS 實(shí)現(xiàn)這一結(jié)構(gòu):
??比如生成一個(gè)無(wú)限增長(zhǎng)的 id 序列:
function* idMaker(){ let index = 0; while(true) yield index++; } let gen = idMaker(); // "Generator { }" console.log(gen.next().value); // 0 console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 // ...
??比如實(shí)現(xiàn)一個(gè)循環(huán)交替的無(wú)限序列:
//一個(gè)通用生成器將無(wú)限迭代 //提供的參數(shù),產(chǎn)生每個(gè)項(xiàng)。 function* alternate(...seq) { while (true) { for (let item of seq) { yield item; } } } //使用新值創(chuàng)建新的生成器實(shí)例 //來(lái)迭代每個(gè)項(xiàng)。 let alternator = alternator('one', 'two', 'three'); //從無(wú)限序列中獲取前10個(gè)項(xiàng)。 for (let i = 0; i < 6; i++) { console.log(`"${alternator.next().value}"`); } // "one" // "two" // "three" // "one" // "two" // "three"
由于 while 循環(huán)永遠(yuǎn)不會(huì)退出,for 循環(huán)將自己重復(fù)。也就是說(shuō),參數(shù)值會(huì)交替出現(xiàn)了。
無(wú)限序列是有現(xiàn)實(shí)意義的,很多數(shù)字組合都是無(wú)限的,比如素?cái)?shù),斐波納契數(shù),奇數(shù)等等;
結(jié)語(yǔ)
看到這里,大家有沒(méi)有感覺(jué) Generator 和之前講過(guò)的什么東西有點(diǎn)像?
純函數(shù)的衍生 compose 組合函數(shù),把一個(gè)一個(gè)函數(shù)組裝、拼接形成鏈條;Generator 自定義生成序列,依次執(zhí)行。二者有異曲同工之妙。前者側(cè)重函數(shù)封裝、后者側(cè)重異步處理,但二者都有“延遲處理”的思想。真掘了!
JavaScript 也能借助 閉包、柯里化、組合函數(shù)、Generator 實(shí)現(xiàn)惰性編程,減少不必要的計(jì)算、精確控制序列的執(zhí)行、實(shí)現(xiàn)無(wú)限列表等。。。
不愧是你,真膠水語(yǔ)言,啥都能干!
以上就是從延遲處理解析JavaScript惰性編程的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 延遲處理惰性編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
uniapp?u-picker多列用法以及設(shè)置默認(rèn)選中值
uview組件庫(kù)u-picker組件可能很多人不熟悉,下面這篇文章主要給大家介紹了關(guān)于uniapp?u-picker多列用法以及設(shè)置默認(rèn)選中值的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06動(dòng)態(tài)的9*9乘法表效果的實(shí)現(xiàn)代碼
下面小編就為大家?guī)?lái)一篇?jiǎng)討B(tài)的9*9乘法表效果的實(shí)現(xiàn)代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧2016-05-05JavaScript利用crypto模塊實(shí)現(xiàn)加解密
crypto模塊提供了加密功能,包含對(duì) OpenSSL 的哈希、HMAC、加密、解密、簽名、以及驗(yàn)證功能的一整套封裝。本文將利用它實(shí)現(xiàn)加解密算法,需要的可以參考一下2023-02-02javascript對(duì)HTML字符轉(zhuǎn)義與反轉(zhuǎn)義
這篇文章主要介紹了javascript對(duì)HTML字符轉(zhuǎn)義與反轉(zhuǎn)義,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12微信小程序?qū)崿F(xiàn)tab頁(yè)面切換功能
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)tab頁(yè)面切換功能,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07JS對(duì)select控件option選項(xiàng)的增刪改查示例代碼
Javascript操作select是表單中比較常見(jiàn)的,大家可以在網(wǎng)上搜索到很多的相關(guān)資料,接下來(lái)為大家詳細(xì)介紹下,JS動(dòng)態(tài)操作select中的各種方法,感興趣的朋友可以參考下2013-10-10JS+CSS實(shí)現(xiàn)分類動(dòng)態(tài)選擇及移動(dòng)功能效果代碼
這篇文章主要介紹了JS+CSS實(shí)現(xiàn)分類動(dòng)態(tài)選擇及移動(dòng)功能效果代碼,涉及JavaScript實(shí)現(xiàn)頁(yè)面元素動(dòng)態(tài)變換效果實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10JavaScript學(xué)習(xí)筆記--常用的互動(dòng)方法
本文對(duì)JavaScript中常用的互動(dòng)方法進(jìn)行實(shí)例分析介紹,圖文并茂,有需要的朋友可以看下2016-12-12