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