js尾調(diào)用優(yōu)化的實(shí)現(xiàn)
尾調(diào)用(Tail Call)是函數(shù)式編程的一個(gè)重要概念,本文介紹它的含義和用法。
一、什么是尾調(diào)用?
尾調(diào)用的概念非常簡(jiǎn)單,一句話就能說(shuō)清楚,就是指某個(gè)函數(shù)的最后一步是調(diào)用另一個(gè)函數(shù)。
function f(x){ return g(x); }
上面代碼中,函數(shù)f的最后一步是調(diào)用函數(shù)g,這就叫尾調(diào)用。
以下兩種情況,都不屬于尾調(diào)用。
// 情況一 function f(x){ let y = g(x); return y; } // 情況二 function f(x){ return g(x) + 1; }
上面代碼中,情況一是調(diào)用函數(shù)g之后,還有別的操作,所以不屬于尾調(diào)用,即使語(yǔ)義完全一樣。情況二也屬于調(diào)用后還有操作,即使寫(xiě)在一行內(nèi)。
尾調(diào)用不一定出現(xiàn)在函數(shù)尾部,只要是最后一步操作即可。
function f(x) { if (x > 0) { return m(x) } return n(x); }
上面代碼中,函數(shù)m和n都屬于尾調(diào)用,因?yàn)樗鼈兌际呛瘮?shù)f的最后一步操作。
二、尾調(diào)用優(yōu)化
尾調(diào)用之所以與其他調(diào)用不同,就在于它的特殊的調(diào)用位置。
我們知道,函數(shù)調(diào)用會(huì)在內(nèi)存形成一個(gè)"調(diào)用記錄",又稱"調(diào)用幀"(call frame),保存調(diào)用位置和內(nèi)部變量等信息。如果在函數(shù)A的內(nèi)部調(diào)用函數(shù)B,那么在A的調(diào)用記錄上方,還會(huì)形成一個(gè)B的調(diào)用記錄。等到B運(yùn)行結(jié)束,將結(jié)果返回到A,B的調(diào)用記錄才會(huì)消失。如果函數(shù)B內(nèi)部還調(diào)用函數(shù)C,那就還有一個(gè)C的調(diào)用記錄棧,以此類推。所有的調(diào)用記錄,就形成一個(gè)"調(diào)用棧"(call stack)。
尾調(diào)用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調(diào)用記錄,因?yàn)檎{(diào)用位置、內(nèi)部變量等信息都不會(huì)再用到了,只要直接用內(nèi)層函數(shù)的調(diào)用記錄,取代外層函數(shù)的調(diào)用記錄就可以了。
function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3);
上面代碼中,如果函數(shù)g不是尾調(diào)用,函數(shù)f就需要保存內(nèi)部變量m和n的值、g的調(diào)用位置等信息。但由于調(diào)用g之后,函數(shù)f就結(jié)束了,所以執(zhí)行到最后一步,完全可以刪除 f() 的調(diào)用記錄,只保留 g(3) 的調(diào)用記錄。
這就叫做"尾調(diào)用優(yōu)化"(Tail call optimization),即只保留內(nèi)層函數(shù)的調(diào)用記錄。如果所有函數(shù)都是尾調(diào)用,那么完全可以做到每次執(zhí)行時(shí),調(diào)用記錄只有一項(xiàng),這將大大節(jié)省內(nèi)存。這就是"尾調(diào)用優(yōu)化"的意義。
三、尾遞歸
函數(shù)調(diào)用自身,稱為遞歸。如果尾調(diào)用自身,就稱為尾遞歸。
遞歸非常耗費(fèi)內(nèi)存,因?yàn)樾枰瑫r(shí)保存成千上百個(gè)調(diào)用記錄,很容易發(fā)生"棧溢出"錯(cuò)誤(stack overflow)。但對(duì)于尾遞歸來(lái)說(shuō),由于只存在一個(gè)調(diào)用記錄,所以永遠(yuǎn)不會(huì)發(fā)生"棧溢出"錯(cuò)誤。
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120
上面代碼是一個(gè)階乘函數(shù),計(jì)算n的階乘,最多需要保存n個(gè)調(diào)用記錄,復(fù)雜度 O(n) 。
如果改寫(xiě)成尾遞歸,只保留一個(gè)調(diào)用記錄,復(fù)雜度 O(1) 。
function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
由此可見(jiàn),"尾調(diào)用優(yōu)化"對(duì)遞歸操作意義重大,所以一些函數(shù)式編程語(yǔ)言將其寫(xiě)入了語(yǔ)言規(guī)格。ES6也是如此,第一次明確規(guī)定,所有 ECMAScript 的實(shí)現(xiàn),都必須部署"尾調(diào)用優(yōu)化"。這就是說(shuō),在 ES6 中,只要使用尾遞歸,就不會(huì)發(fā)生棧溢出,相對(duì)節(jié)省內(nèi)存。
四、遞歸函數(shù)的改寫(xiě)
尾遞歸的實(shí)現(xiàn),往往需要改寫(xiě)遞歸函數(shù),確保最后一步只調(diào)用自身。做到這一點(diǎn)的方法,就是把所有用到的內(nèi)部變量改寫(xiě)成函數(shù)的參數(shù)。比如上面的例子,階乘函數(shù) factorial 需要用到一個(gè)中間變量 total ,那就把這個(gè)中間變量改寫(xiě)成函數(shù)的參數(shù)。這樣做的缺點(diǎn)就是不太直觀,第一眼很難看出來(lái),為什么計(jì)算5的階乘,需要傳入兩個(gè)參數(shù)5和1?
兩個(gè)方法可以解決這個(gè)問(wèn)題。方法一是在尾遞歸函數(shù)之外,再提供一個(gè)正常形式的函數(shù)。
function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } function factorial(n) { return tailFactorial(n, 1); } factorial(5) // 120
上面代碼通過(guò)一個(gè)正常形式的階乘函數(shù) factorial ,調(diào)用尾遞歸函數(shù) tailFactorial ,看起來(lái)就正常多了。
函數(shù)式編程有一個(gè)概念,叫做柯里化(currying),意思是將多參數(shù)的函數(shù)轉(zhuǎn)換成單參數(shù)的形式。這里也可以使用柯里化。
function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } const factorial = currying(tailFactorial, 1); factorial(5) // 120
上面代碼通過(guò)柯里化,將尾遞歸函數(shù) tailFactorial 變?yōu)橹唤邮?個(gè)參數(shù)的 factorial 。
第二種方法就簡(jiǎn)單多了,就是采用ES6的函數(shù)默認(rèn)值。
function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120
上面代碼中,參數(shù) total 有默認(rèn)值1,所以調(diào)用時(shí)不用提供這個(gè)值。
總結(jié)一下,遞歸本質(zhì)上是一種循環(huán)操作。純粹的函數(shù)式編程語(yǔ)言沒(méi)有循環(huán)操作命令,所有的循環(huán)都用遞歸實(shí)現(xiàn),這就是為什么尾遞歸對(duì)這些語(yǔ)言極其重要。對(duì)于其他支持"尾調(diào)用優(yōu)化"的語(yǔ)言(比如Lua,ES6),只需要知道循環(huán)可以用遞歸代替,而一旦使用遞歸,就最好使用尾遞歸。
([說(shuō)明] 本文摘自我寫(xiě)的《ECMAScript 6入門》)
五、嚴(yán)格模式
ES6的尾調(diào)用優(yōu)化只在嚴(yán)格模式下開(kāi)啟,正常模式是無(wú)效的。
這是因?yàn)樵谡DJ较?,函?shù)內(nèi)部有兩個(gè)變量,可以跟蹤函數(shù)的調(diào)用棧。
- arguments:返回調(diào)用時(shí)函數(shù)的參數(shù)。
- func.caller:返回調(diào)用當(dāng)前函數(shù)的那個(gè)函數(shù)。
尾調(diào)用優(yōu)化發(fā)生時(shí),函數(shù)的調(diào)用棧會(huì)改寫(xiě),因此上面兩個(gè)變量就會(huì)失真。嚴(yán)格模式禁用這兩個(gè)變量,所以尾調(diào)用模式僅在嚴(yán)格模式下生效。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Javascript前端優(yōu)化代碼
- 通過(guò)循環(huán)優(yōu)化 JavaScript 程序
- 淺析JavaScript異步代碼優(yōu)化
- JavaScript關(guān)于提高網(wǎng)站性能的幾點(diǎn)建議(一)
- JavaScript提高網(wǎng)站性能優(yōu)化的建議(二)
- JS 網(wǎng)站性能優(yōu)化筆記
- 網(wǎng)站性能提高實(shí)戰(zhàn)經(jīng)驗(yàn)點(diǎn)滴記錄
- 詳解網(wǎng)站中圖片日常使用以及優(yōu)化手法
- 利用javascript解決圖片縮放及其優(yōu)化的代碼
- 圖片該如何優(yōu)化來(lái)提高網(wǎng)站性能
相關(guān)文章
echarts餅圖labelLine線的長(zhǎng)度自適應(yīng)設(shè)置
這篇文章主要給大家介紹了關(guān)于echarts餅圖labelLine線的長(zhǎng)度自適應(yīng)設(shè)置的相關(guān)資料,在echarts中,餅圖的標(biāo)簽線可以通過(guò)設(shè)置 labelLine屬性來(lái)自定義位置,需要的朋友可以參考下2023-08-08那些項(xiàng)目中常見(jiàn)的TypeScript錯(cuò)誤總結(jié)
這篇文章主要給大家總結(jié)介紹了一些項(xiàng)目中常見(jiàn)的TypeScript錯(cuò)誤的相關(guān)資料,如果你想查看所有的錯(cuò)誤信息和錯(cuò)誤碼,可以瀏覽TypeScript的源代碼倉(cāng)庫(kù),當(dāng)然隨著?ts?版本的更新,官網(wǎng)也會(huì)逐漸增加更多新的類型錯(cuò)誤,需要的朋友可以參考下2022-03-03JavaScript的new date等日期函數(shù)在safari中遇到的坑
safari中對(duì)于JavaScript的new Date函數(shù)的支持有一個(gè)比較奇怪的問(wèn)題,帶著這個(gè)奇怪的問(wèn)題我們通過(guò)本文一起學(xué)習(xí)吧2016-10-10JavaScript?排他思想的具體實(shí)現(xiàn)
排他思想的算法就是排除掉其他的,本文主要介紹了JavaScript?排他思想的實(shí)現(xiàn),以及介紹了兩個(gè)示例,感興趣的可以了解一下2021-11-11layui 監(jiān)聽(tīng)彈窗關(guān)閉并刷新父級(jí)table的場(chǎng)景分析
這篇文章主要介紹了layui 監(jiān)聽(tīng)彈窗關(guān)閉并刷新父級(jí)table的場(chǎng)景分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-07-07JS實(shí)現(xiàn)Date日期格式的本地化的方法小結(jié)
為了更好的更新多語(yǔ)言日期的顯示,所以希望實(shí)現(xiàn)日期的本地化格式顯示要求,常規(guī)的特殊字符型格式化無(wú)法滿足顯示要求,這里整理了幾種我思考實(shí)現(xiàn)的本地化實(shí)現(xiàn)功能2024-06-06javascript簡(jiǎn)易畫(huà)板開(kāi)發(fā)
這篇文章主要為大家詳細(xì)介紹了javascript簡(jiǎn)易畫(huà)板開(kāi)發(fā)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10