js遞歸函數解析:階乘示例

遞歸

程序調用自身的編程技巧稱爲遞歸( recursion)。遞歸做爲一種算法在程序設計語言中廣泛應用。 一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它通常把一個大型複雜的問題層層轉化爲一個與原問題相似的規模較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重複計算,大大地減少了程序的代碼量。
遞歸的能力在於用有限的語句來定義對象的無限集合。一般來說,遞歸需要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回。

來說一個經典的遞歸調用編程題——階乘

一個正整數的階乘(factorial)是所有小於及等於該數的正整數的積,並且0的階乘爲1。自然數n的階乘寫作n!。
亦即n!=1×2×3×…×(n-1)×n。
階乘亦可以遞歸方式定義:0!=1,n!=(n-1)!×n。

1、通過函數名自身遞歸調用

function factorial(num){
	if(num <= 1){
		return 1;
	}
	return num*factorial(num-1);
}
factorial(5); //120

執行過程:
①代碼執行factorial(5)–>進入函數,此時num=5;遞歸前進。
②執行5*factorial(4),此時代碼等待,執行factorial(4)–>進入函數,num=4;遞歸前進。
③執行4*factorial(3),此時代碼等待,執行factorial(3)–>進入函數,num=3;遞歸前進。
④執行3*factorial(2),此時代碼等待,執行factorial(2)–>進入函數,num=2;遞歸前進。
⑤執行2*factorial(1),此時代碼等待,執行factorial(1)–>進入函數,num=1;觸發邊界條件。
⑥執行if條件判斷語句,返回1;此時factorial(1)的執行結果是1;開始遞歸返回,按以下代碼順序依次執行,每次返回的結果與當前的num相乘。

2*factorial(1)-->2×1//2
3*factorial(2)-->3×2×1//6
4*factorial(3)-->4×3×2×1//24
5*factorial(4)-->5×4×3×2×1//120

最後的結果是120。
來看一下控制檯的debug:
在這裏插入圖片描述
這種通過函數名調用自身的方式存在一個問題:函數名是一個指向函數對象的指針,函數的執行與函數名factorial緊緊耦合在了一起,如果我們把函數名與函數對象本身的指向關係斷開,這種方式運行時將出現錯誤。如下代碼:

function factorial(num){
	if(num <= 1){
		return 1;
	}
	return num*factorial(num-1);
}
factorial(5); //120
//將factorial指針指向factorialNew
var factorialNew = factorial;
factorialNew(5); //120
//將factorial重新定義
factorial = function(){
	return 0;
};
factorialNew(5); //0

2、通過arguments.callee調用函數自身

callee 是 arguments 對象的一個屬性。它可以用於引用該函數的函數體內當前正在執行的函數。

function factorial(num){
	if(num <= 1){
		return 1;
	}
	return num*arguments.callee(num-1);
}
factorial(5); //120

在這個重寫後的 factorial() 函數的函數體內,沒有再引用函數名 factorial。這樣,無論引用函數時使用的是什麼名字,都可以保證正常完成遞歸調用。這時候如果重新將 factorial 賦值,結果還是120。如下代碼:

//將factorial指針指向factorialNew
var factorialNew = factorial;
factorialNew(5); //120
//將factorial重新定義
factorial = function(){
	return 0;
};
factorialNew(5); //120

在此,變量 factorialNew 獲得了 factorial 的值,實際上是在 factorialNew 上保存了 factorial 函數的引用。然後,我們又將一個返回數值0的函數賦值給 factorial 變量。如果像原來的 factorial() 那樣不使用 arguments.callee,調用 factorialNew()就會返回數值0。可是,在解除了函數體內的代碼與函數名的耦合狀態之後,factorialNew()就能夠正常地計算階乘,因爲它調用的是之前保存的引用。至於 factorial(),它現在只是一個返回數值0的函數。

參考:arguments.callee 屬性包含當前正在執行的函數

這種方式很好的解決了函數名指向變更時導致遞歸調用找不到自身的問題。
但是這種方式也不是很完美,因爲在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。

3、通過命名函數表達式來實現arguments.callee的效果

'use strict'
var factorialIIFE = (function() {
	return function factorial(num) {
		if (num <= 1) {
			return 1;
		} else {
			return num * factorial(num - 1);
		}
	}
})();
factorialIIFE(5); //120

var factorialNew = factorialIIFE;
factorialNew(5); //120

factorialIIFE = function(){
	return 0;
};
factorialNew(5); //120

在上面的代碼中,階乘函數factorial被封裝成了一個IIEF(立即調用的函數表達式);賦值給變量名爲factorialIIFE的變量。這裏函數執行的結果和使用arguments.callee執行的結果是一樣的。

這裏我理解的也不是很明白,但是還是要說一下我自己的理解思路,如果不正確,還請大家幫忙指正一下。

思路:因爲這裏使用的是函數名調用,所以使用IIEF將階乘函數factorial封裝爲一個閉包,而由於閉包的特性,factorial綁定了當前的函數作用域;factorial()引用的就是當前正在執行的函數。還記得本文第一部分的內容麼:函數名是一個指向函數對象的指針,函數的執行與函數名緊緊耦合在了一起;我們利用閉包將factorial與factorial的調用緊緊聯繫在一起,並將引用賦值給了factorialNew,所以即便將factorialIIFE重新賦值(引用並未修改),也無法改變factorialNew的調用結果。

發佈了44 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章