之前學遞歸一直學的迷迷糊糊,感覺懂了又感覺沒懂,今天正好學習到了這一部分。
當函數解決一個任務時,在解決的過程中它可能會調用很多其他函數。當函數調用自身時,就是所謂的遞歸。
舉一個例子:
一個函數pow(x,n),計算x的n次方。
遞歸思路:
1 function pow(x, n) { 2 if (n == 1) { 3 return x; 4 } else { 5 return x * pow(x, n - 1); 6 } 7 } 8 9 alert( pow(2, 3) ); // 8
函數執行分為兩個分支:
1、如果n == 1,函數會立即產生明顯的結果,這叫做基礎的遞歸。
2、else,這個分支叫做一個遞歸步驟:將任務轉化為更簡單的行為(x的乘法)和更簡單的同類任務(帶有更小的n的pow運算)的調用。接下來的步驟將其進一步簡化,直到n到達1。
比如,為了計算 pow(2, 4)
,遞歸變體經過了下面幾個步驟:
pow(2, 4) = 2 * pow(2, 3)
pow(2, 3) = 2 * pow(2, 2)
pow(2, 2) = 2 * pow(2, 1)
pow(2, 1) = 2
到這里之前也是懂的,接下來研究一下遞歸調用的工作原理。
函數底層的工作原理:有關正在運行的函數的執行過程的相關信息被存貯在其 執行上下文中。
執行上下文是一個內部數據結構,它包含有關函數執行時的詳細細節:當前控制流所在的位置,當前的變量,this
的值(此處我們不使用它),以及其它的一些內部細節。
一個函數調用僅具有一個與其相關聯的執行上下文。
當一個函數進行嵌套調用時,將發生以下的事兒:
- 當前函數被暫停;
- 與它關聯的執行上下文被一個叫做 執行上下文堆棧 的特殊數據結構保存;
- 執行嵌套調用;
- 嵌套調用結束后,從堆棧中恢復之前的執行上下文,並從停止的位置恢復外部函數。
讓我們看看 pow(2, 3)
調用期間都發生了什么。
pow(2, 3)
在調用 pow(2, 3)
的開始,執行上下文(context)會存儲變量:x = 2, n = 3
,執行流程在函數的第 1
行。
我們將其描繪如下:
- Context: { x: 2, n: 3, at line 1 } call: pow(2, 3)
這是函數開始執行的時候。條件 n == 1
結果為 false,所以執行流程進入 if
的第二分支。
變量相同,但是行改變了,因此現在的上下文是:
- Context: { x: 2, n: 3, at line 5 } call:pow(2, 3)
為了計算 x * pow(x, n - 1)
,我們需要使用帶有新參數的新的 pow
子調用 pow(2, 2)
。
pow(2,2)
為了執行嵌套調用,JavaScript 會在 執行上下文堆棧 中記住當前的執行上下文。
這里我們調用相同的函數 pow
,所有函數的處理都是一樣的:
- 當前上下文被“記錄”在堆棧的頂部。
- 為子調用創建新的上下文。
- 當子調用結束后 —— 前一個上下文被從堆棧中彈出,並繼續執行。
下面是進入子調用 pow(2, 2)
時的上下文堆棧:
- Context: { x: 2, n: 2, at line 1 } pow(2, 2)
- Context: { x: 2, n: 3, at line 5 } pow(2, 3)
新的當前執行上下文位於頂部(粗體顯示),之前記住的上下文位於下方。
當我們完成子調用后 —— 很容易恢復上一個上下文,因為它既保留了變量,也保留了當時所在代碼的確切位置。
pow(2,1)
這是當pow(2,1)時,函數的執行上下文堆棧,現在的參數是x=2,n=1,
新的執行上下文被創建,前一個被壓入堆棧頂部:
- Context: { x: 2, n: 1, at line 1 } call:pow(2, 1)
- Context: { x: 2, n: 2, at line 5 } call:pow(2, 2)
- Context: { x: 2, n: 3, at line 5 } call:pow(2, 3)
此時,有2個舊的上下文和一個當前正在運行的pow(2,1)的上下文。
出口
在執行 pow(2, 1)
時,與之前的不同,條件 n == 1
為 true,因此 if
的第一個分支生效:
此時不再有更多的嵌套調用,所以函數結束,返回 2
。
函數完成后,就不再需要其執行上下文了,因此它被從內存中移除。前一個上下文恢復到堆棧的頂部:
- Context: { x: 2, n: 2, at line 5 }call: pow(2, 2)
- Context: { x: 2, n: 3, at line 5 }call: pow(2, 3)
恢復執行 pow(2, 2)
。它擁有子調用 pow(2, 1)
的結果,因此也可以完成 x * pow(x, n - 1)
的執行,並返回 4
。
然后,前一個上下文被恢復:
- Context: { x: 2, n: 3, at line 5 }call: pow(2, 3)
當它結束后,我們得到了結果 pow(2, 3) = 8
。
本示例中的遞歸深度為:3。
遞歸深度:最大的嵌套調用次數(包括首次)。
從上面我們可以看出,遞歸深度等於堆棧中上下文的最大數量。
使用遞歸時需要注意內存,上下文占用內存。
參考文獻:https://zh.javascript.info/recursion