遞迴尾呼叫優化

wade3po發表於2019-04-16

之前分享過遞迴,其中有一個優化就是尾呼叫。

先明確尾呼叫的概念:

尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,就是指某個函式的最後一步是return呼叫另一個函式。

function fn() {  

  return gn()

}
複製程式碼

下面幾種不是尾呼叫:

function fn() {  

  return gn() + 10//呼叫之後又賦值

}

function fn() {  

  gn()//沒有return,或者說是return了undefined

}

function fn() {  

  var gnn = gn();  

  return gnn//呼叫後還有操作

}
複製程式碼

注意,是最後一步操作,不是放在最末尾,下面也算尾呼叫:

function fn(a) {  

  if(a > 10){ 

   return gn(10)  

  }else{    

    return gn(20)  

  }  

  return gn()

}

複製程式碼

之前分享過呼叫棧,如果不是尾呼叫,那麼會生成一個呼叫棧,直到棧頂的執行完畢,才會釋放之前形成的呼叫棧的記憶體。尾呼叫因為是最後一步操作,所以不需要保留之前的棧,也就不需要儲存之前的記憶體,就是遞迴裡面計算階乘那兩個函式。

注意,並不是所有的函式都能尾呼叫優化,要看你這個函式需不需要使用某些上個函式的變數或者什麼的。

尾呼叫優化其實很大一部分就是遞迴函式在使用,因為遞迴函式呼叫的時候非常耗費記憶體,可能需要儲存成百上千呼叫棧,很容易記憶體溢位。如果是尾遞迴就只有一個呼叫棧,能把複雜度O(n)的變成O(1)。

至於怎麼改寫遞迴變成可以使用尾呼叫就比較複雜了,需要根據不同函式去修改。比如阮一峰大神的例子:

function sum(x, y) {

  if (y > 0) {

    return sum(x + 1, y - 1);

  } else {

    return x;

  }

}

sum(1, 100000)
複製程式碼

改成:

function sum(x, y) {

  if (y > 0) {

    return sum.bind(null, x + 1, y - 1);

  } else {

    return x;

  }

}
複製程式碼

然後使用蹦床函式:

function trampoline(f) {

  while (f && f instanceof Function) {

    f = f();

  }

  return f;

}
複製程式碼

執行:

trampoline(sum(1, 100000))
複製程式碼

你會發現,很多遞迴函式都能改成類似的,然後使用蹦床函式實現尾呼叫優化。

而ES6對尾呼叫有什麼優化?就是函式預設值,在一些場景下,比如階乘的遞迴,採用預設值實現尾遞迴優化。

遞迴尾呼叫優化

相關文章