尾呼叫優化

2021-07-25 16:55:38 字數 3266 閱讀 2014

尾呼叫的概念非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另乙個函式。

function

f(x)

上面**中,函式f的最後一步是呼叫函式g,這就叫尾呼叫。

以下兩種情況,都不屬於尾呼叫。

// 情況一

function

f(x)

// 情況二

function

f(x)

上面**中,情況一是呼叫函式g之後,還有別的操作,所以不屬於尾呼叫,即使語義完全一樣。情況二也屬於呼叫後還有操作,即使寫在一行內。

尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。

function

f(x)

return

n(x)

;}

上面**中,函式m和n都屬於尾呼叫,因為它們都是函式f的最後一步操作。

尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。

我們知道,函式呼叫會在記憶體形成乙個"呼叫記錄",又稱"呼叫幀"(call frame),儲存呼叫位置和內部變數等資訊。如果在函式a的內部呼叫函式b,那麼在a的呼叫記錄上方,還會形成乙個b的呼叫記錄。等到b執行結束,將結果返回到a,b的呼叫記錄才會消失。如果函式b內部還呼叫函式c,那就還有乙個c的呼叫記錄棧,以此類推。所有的呼叫記錄,就形成乙個"呼叫棧"(call stack)。

尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫記錄,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫記錄,取代外層函式的呼叫記錄就可以了。

functionf()

f();// 等同於

functionf()

f();// 等同於g(3

);

上面**中,如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除 f() 的呼叫記錄,只保留 g(3) 的呼叫記錄。

這就叫做"尾呼叫優化"(tail call optimization),即只保留內層函式的呼叫記錄。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫記錄只有一項,這將大大節省記憶體。這就是"尾呼叫優化"的意義。

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。

遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫記錄,很容易發生"棧溢位"錯誤(stack overflow)。但對於尾遞迴來說,由於只存在乙個呼叫記錄,所以永遠不會發生"棧溢位"錯誤。

function

factorial(

n)factorial(5)

// 120

上面**是乙個階乘函式,計算n的階乘,最多需要儲存n個呼叫記錄,複雜度 o(n) 。

如果改寫成尾遞迴,只保留乙個呼叫記錄,複雜度 o(1) 。

function

factorial(

n, total)

factorial(5,

1)// 120

由此可見,"尾呼叫優化"對遞迴操作意義重大,所以一些函式式程式語言將其寫入了語言規格。es6也是如此,第一次明確規定,所有 ecmascript 的實現,都必須部署"尾呼叫優化"。這就是說,在 es6 中,只要使用尾遞迴,就不會發生棧溢位,相對節省記憶體。

尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只呼叫自身。做到這一點的方法,就是把所有用到的內部變數改寫成函式的引數。比如上面的例子,階乘函式 factorial 需要用到乙個中間變數 total ,那就把這個中間變數改寫成函式的引數。這樣做的缺點就是不太直觀,第一眼很難看出來,為什麼計算5的階乘,需要傳入兩個引數5和1?

兩個方法可以解決這個問題。方法一是在尾遞迴函式之外,再提供乙個正常形式的函式。

function

tailfactorial(

n, total)

function

factorial(

n)factorial(5)

// 120

上面**通過乙個正常形式的階乘函式 factorial ,呼叫尾遞迴函式 tailfactorial ,看起來就正常多了。

函式式程式設計有乙個概念,叫做柯里化(currying),意思是將多引數的函式轉換成單引數的形式。這裡也可以使用柯里化。

function

currying(

fn, n);}

function

tailfactorial(

n, total)

const factorial =

currying(

tailfactorial,1)

;factorial(5)

// 120

上面**通過柯里化,將尾遞迴函式 tailfactorial 變為只接受1個引數的 factorial 。

第二種方法就簡單多了,就是採用es6的函式預設值。

function

factorial(

n, total =1)

factorial(5)

// 120

上面**中,引數 total 有預設值1,所以呼叫時不用提供這個值。

總結一下,遞迴本質上是一種迴圈操作。純粹的函式式程式語言沒有迴圈操作命令,所有的迴圈都用遞迴實現,這就是為什麼尾遞迴對這些語言極其重要。對於其他支援"尾呼叫優化"的語言(比如lua,es6),只需要知道迴圈可以用遞迴代替,而一旦使用遞迴,就最好使用尾遞迴。

([說明] 本文摘自我寫的《ecmascript 6入門》)

es6的尾呼叫優化只在嚴格模式下開啟,正常模式是無效的。

這是因為在正常模式下,函式內部有兩個變數,可以跟蹤函式的呼叫棧。

尾呼叫優化發生時,函式的呼叫棧會改寫,因此上面兩個變數就會失真。嚴格模式禁用這兩個變數,所以尾呼叫模式僅在嚴格模式下生效。

尾呼叫優化

尾呼叫 tail call 是函式式程式設計的乙個重要概念,本文介紹它的含義和用法。一 什麼是尾呼叫?尾呼叫的概念非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另乙個函式。function f x 上面 中,函式f的最後一步是呼叫函式g,這就叫尾呼叫。以下兩種情況,都不屬於尾呼叫。情況一...

尾呼叫優化

本文 日期 2015年4月10日 尾呼叫 tail call 是函式式程式設計的乙個重要概念,本文介紹它的含義和用法。尾呼叫的概念非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另乙個函式。function f x 上面 中,函式f的最後一步是呼叫函式g,這就叫尾呼叫。以下兩種情況,都不屬...

尾呼叫優化

尾呼叫優化其實在阮一峰老師的es6中已經講過了,但很多人可能並沒有重視它,它同樣會經常出現在筆試面試中,典型應用為斐波那契數列的優化,所以在這裡我單獨拿出來說一下。尾呼叫是函式式程式設計的乙個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另乙個函式。function f x...