文章來源于公眾號:前端UpUp ,作者:TianTianUp
前言
經??吹疥P于尾遞歸
這三個詞,遞歸很多時候,都離不開我們,廢話不多說,這次我們梳理一遍關于遞歸那些事。
在這里關于遞歸,這里就不贅述了,有興趣的可以去查一查資料。
需要了解如何優(yōu)化尾遞歸的話,我們需要從最開始講起。
- 什么是尾調用
- 什么是尾遞歸
- 如何優(yōu)化尾遞歸
尾調用
從字面理解,自然而言就是在函數的尾部返回一個函數的調用,通常來說,指的是函數執(zhí)行的最后一步。
舉個例子
const fn = () => f1() || f2()
// 這里的話, f2函數有可能是尾調用,f1不可能是尾調用
為什么f1函數不是呢,我們看這個函數的等價形式
const fn = function () {
const flag = f1()
if(flag) {
return flag
} else {
return f2()
}
}
似乎寫到這里,根據尾調用定義,我們就明白了,只有f2函數是在尾部調用。
說到這里,為什么要說尾調用呢?我們事先想一想傳統(tǒng)的遞歸,典型的就是首先執(zhí)行遞歸調用,然后根據這個遞歸的返回值并結算結果,那么傳統(tǒng)的遞歸缺點有哪些呢
- 效率低,占內存。
- 如果遞歸鏈過長,可能會stack overflow
那么我們是不是可以做優(yōu)化呢,這就可以涉及上面提到的尾調用,它的原理是啥呢
按照阮一峰老師在es6的函數擴展中的解釋就是:函數調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數
A
的內部調用函數B
,那么在A
的調用幀上方,還會形成一個B
的調用幀。等到B
運行結束,將結果返回到A
,B
的調用幀才會消失。如果函數B
內部還調用函數C
,那就還有一個C
的調用幀,以此類推。所有的調用幀,就形成一個“調用?!保╟all stack)。
這里的“調用幀”和“調用?!?,說的應該就是“執(zhí)行環(huán)境”和“調用棧”。因為尾調用時函數的最后一部操作,所以不再需要保留外層的調用幀,而是直接取代外層的調用幀,所以可以起到一個優(yōu)化的作用。
從上述的描述中,我們視乎可以理解成
- 它的原理類似于當編譯器檢測到一個函數調用是尾遞歸時,它會覆蓋當前的活動記錄而不是在函數棧中創(chuàng)建一個新的
調用記錄
。 - 這樣子,我們也可以理解成,不同的語言編譯器或者是解釋器做了
尾遞歸優(yōu)化
,才讓它不會爆棧。
既然是這樣子的話,尾遞歸的優(yōu)化,取決于瀏覽器,那具體有哪些主流瀏覽器支持呢
safari 和火狐,有興趣的可以去了解一下,可以寫個斐波那契數列數列驗證一下。
手動優(yōu)化
既然我們知道了,很多瀏覽器對于尾遞歸的優(yōu)化支持的瀏覽器并不多,那你會好奇,當我們使用尾遞歸進行優(yōu)化的時候,依然出現棧溢出
的錯誤,那么我們如何解決呢?
我在網上看到一個不錯的方案,采用的是蹦床函數
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
那么如何使用呢
我們拿最常見的斐波那契數列來說吧
function fibonacci(n) {
if (n === 0) return 0
if (n === 1) return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}
根據上面的式子,我們可以將其寫成迭代形式,用一個變量去緩存它的值
function fibonacci (n, ac1 = 0, ac2 = 1) {
return n