簡單易懂的現代魔法-遞歸

平時在前端開發中,好像也沒啥用到遞歸的地方。不過這並不表明遞歸不重要,若是你看過一些框架的源碼,就會常常見到它的影子:好比渲染虛擬DOM的render函數,webpack中require依賴分析,Koa2洋蔥式的中間件模型,其實都運用到了遞歸算法。javascript

博客原文前端

遞歸概念

那麼遞歸究竟是啥?先上兩張圖:java

圖1:webpack

圖2:web

遞歸,就是在運行的過程當中調用本身

咱們來看個最簡單的階乘函數:算法

5! = 5 * 4 * 3 * 2 * 1
function factorial(num) {
    if (num === 1) { // 基線條件
        return 1;
    }

    // 遞歸條件
    return num * factorial(num-1);
}

factorial(5);

一個常規的遞歸函數都有兩部分:數據結構

  1. 基線條件(if (num === 1)):保證函數再也不調用本身,避免無限循環
  2. 遞歸條件(num * factorial(num-1)):保證函數可以調用本身

調用棧

棧是一種先進後出的數據結構,它只有兩種操做,出棧和入棧框架

const nekopara = ['chocolat', 'Coconut'];
nekopara.push('vanilla'); // 入棧
nekopara.pop(); // 出棧

代碼在運行過程當中,會有一個叫作調用棧(call stack)的概念。函數

function greet(name) {
    console.log(`hello, ${name}!`)
    greet2(name);
    console.log(`getting ready to say bye`);
    bye();
}

function greet2(name) {
    console.log(`how are you, ${name}?`);
}

function bye() {
    console.log(`bye`);
}

greet('deepred');

調用greet('deepred')時,計算機會首先給該函數分配一塊內存,並將變量名name設置爲deepred性能

greet

每當調用函數時,都會分配一個內存塊並將涉及到的變量值存儲到內存中。

打印hello, deepred後,調用了greet2('deepred')。一樣,計算機再次分配了一塊內存,而且該內存塊位於第一個內存塊上面。

greet

調用棧的最上面表示當前運行的函數,如圖所示,如今正在運行的是greet2函數,打印輸出how are you, deepred?後,函數greet2執行完畢,棧頂的內存塊被彈出。

如今棧頂的內存塊又變回greet,這意味着咱們從greet2的函數中跳出,再次返回到了greet。

咱們在greet中調用了greet2時,greet只執行了一部分。

特別注意:調用另一個函數時,當前函數暫停而且處於未完成狀態,暫停函數的全部變量的值仍然在內存中

執行完greet2後,咱們回到了greet,並從離開的地方開始接着往下執行:首先打印getting ready to say bye,而後調用bye函數。

在棧頂添加了bye函數的內存塊後,開始執行bye函數,打印bye,而後函數返回,內存塊被彈出。

咱們又再次回到了greet中,此次沒有其餘要運行的代碼了,因而從greet函數中返回,內存塊被彈出,調用棧最後爲空。

完整的一次調用流程

greet

遞歸調用棧

遞歸一樣使用調用棧

咱們來分析下階乘fact(3)的調用棧

function fact(num) {
   if (num === 1) { 
       return 1;
   }
   return num * fact(num-1);
}

fact(3);

直接看圖:
greet

遞歸注意事項

遞歸會致使程序的性能變低

若是遞歸嵌套很深,那麼調用棧會很長,這將佔用大量內存,可能會致使棧溢出

相關文章
相關標籤/搜索