1.某些時候遞歸能替換for循環
我們先看一下下面2個例子。
var arrList = [1,2,3,5,100,500,10000,10000,1000,10000002] //for循環測試 function forTest(){ console.time("for循環") for(let i in arrList){ console.log(((arrList[i] + arrList[i]) * 5 - arrList[i])/arrList[i]) } console.timeEnd("for循環") } //遞歸遍歷測試 function recursive() { console.time("遞歸遍歷") const testFun = function (i) { console.log(((arrList[i] + arrList[i]) * 5 - arrList[i])/arrList[i]) if(i == arrList.length - 1){ return } i++ testFun(i) } testFun(0) console.timeEnd("遞歸遍歷") } forTest() recursive()
運行結果:

可以看到,for循環去遍歷一個數組和用遞歸遍歷去遍歷同一個數組得到的結果一樣,耗時也幾乎相同。但是寫法上有很大區別。
遞歸特點
每個遞歸都有一個結束遞歸的條件,上例中的:if(i == arrList.length - 1){ return }
。每一個遞歸都會在函數內部把函數本身調用一次,但是函數在每次運行的時候,都會發生一些變化,用來觸發遞歸的結束,上例中,testFun
函數在內部調用了自己,並且每次調用i
的值會+1,最終觸發了結束條件,讓遞歸結束。
2.使用遞歸,可以輕松實現多級遍歷
我們先看下面的代碼:
var data = [ { name: "所有物品", children: [ { name: "水果", children: [{name: "蘋果", children: [{name: '青蘋果'}, {name: '紅蘋果'}]}] }, { name: '主食', children: [ {name: "米飯", children: [{name: '北方米飯'}, {name: '南方米飯'}]} ] }, { name: '生活用品', children: [ {name: "電腦類", children: [{name: '聯想電腦'}, {name: '蘋果電腦'}]}, {name: "工具類", children: [{name: "鋤頭"}, {name: "錘子"}]}, {name: "生活用品", children: [{name: "洗發水"}, {name: "沐浴露"}]} ] } ] }]
某些時候,坑逼后台讓我們遍歷上面的一個數組,最后在頁面上顯示沒一級的最后一個數據。就是下面的數據:
青蘋果;紅蘋果;北方米飯;南方米飯;聯想電腦;蘋果電腦;鋤頭;錘子;洗發水;沐浴露;
我們先用遍歷的方式來實現:
//普通遍歷實現 var forFunction = function () { var str = "" data.forEach(function(row){ row.children.forEach(function(row){ row.children.forEach(function(row){ row.children.forEach(function(row){ str += (row.name + ";") }) }) }) }) console.log(str) } forFunction() //輸出:青蘋果;紅蘋果;北方米飯;南方米飯;聯想電腦;蘋果電腦;鋤頭;錘子;洗發水;沐浴露;
可以看到,前端累死累死寫了4個forEach
實現了產品要的效果,這時候如果再加點別的什么邏輯,就很難弄了。這時候我們嘗試用遞歸的思想實現:
//遞歸遍歷實現 var recursiveFunction = function(){ var str = '' const getStr = function(list){ list.forEach(function(row){ if(row.children){ getStr(row.children) }else { str += row.name + ";" } }) } getStr(data) console.log(str) } recursiveFunction() //輸出:青蘋果;紅蘋果;北方米飯;南方米飯;聯想電腦;蘋果電腦;鋤頭;錘子;洗發水;沐浴露;
可以看到,遞歸的方式來實現的時候,我們只需要一個for循環,每次遍歷接受到的數據,通過判斷是否還有children
,沒有就代表是最后一級了,有就繼續把children
這個list
傳給函數繼續遍歷,最后就得到了我們想要的數據。
很明顯,
forEach
的遍歷的方式能實現多級的遍歷,並且數據格式可以靈活一些,但是遍歷的層級有限,而且對於未知層級的情況下,就無從下手了。
遞歸遍歷,理論上,只要內存夠用,你能實現任意層級的遍歷,但缺點也很明顯,沒一個層級里面需要有固定的數據格式,否則無法遍歷。
3.遞歸遍歷輕松實現多個異步結果的依次輸出
我們先看一下下面的需求,需要依次輸出1,2,3,4,5
,每個輸出中間間隔1s
。
我們先嘗試下面的方式:
//常規實現 var forTest = function () { console.time("for時間") for (let i = 0; i < 5; i++) { setTimeout(function () { console.log('for循環輸出:' + (i + 1)) if(i+ 1 === 5){ console.timeEnd("for時間") } }, 1000 * (i + 1)) } } forTest() //每隔一秒輸出了1,2,3,4,5
上面我們用let
當前作用域的特點實現了每隔1s輸出,其實可以看出,是一起執行的,但是由於間隔時間不一樣,導致結果是每隔一秒輸出的。
我們再用遞歸的方式實現:
//遞歸遍歷實現 var recursiveTest = function(){ console.time("遞歸時間") const test = function (i) { setTimeout(function () { i = i + 1 console.log('遞歸輸出:' + i) if(i < 5){ test(i) }else { console.timeEnd("遞歸時間") } }, 1000) } test(0) } recursiveTest() //每隔一秒輸出1,2,3,4,5
這里利用遞歸實現就很好理解了,在setTimeout
里面輸出了之后,再開始下一個1s的輸出。
最后我們看一下運行截圖:

通過截圖上的耗時來看:遞歸遍歷需要的時間更長,但是更好理解一下,而且不依賴es6的環境。
4.遞歸遍歷實現排序
var quickSort = function(arr) { if (arr.length <= 1) {//如果數組長度小於等於1無需判斷直接返回即可 return arr; } var pivotIndex = Math.floor(arr.length / 2);//取基准點 var pivot = arr.splice(pivotIndex, 1)[0];//取基准點的值,splice(index,1)函數可以返回數組中被刪除的那個數 var left = [];//存放比基准點小的數組 var right = [];//存放比基准點大的數組 for (var i = 0; i < arr.length; i++){ //遍歷數組,進行判斷分配 if (arr[i] < pivot) { left.push(arr[i]);//比基准點小的放在左邊數組 } else { right.push(arr[i]);//比基准點大的放在右邊數組 } } //遞歸執行以上操作,對左右兩個數組進行操作,直到數組長度為<=1; return quickSort(left).concat([pivot], quickSort(right)); }; var arr = [14, 50, 80, 7, 2, 2, 11]; console.log(quickSort(arr));
這是利用遞歸實現的快速排序,效率很高,有興趣的同學可以試試普通的遍歷實現,再進行比較。
5.總結
1.很多時候可以用遞歸代替循環,可以理解為遞歸是一種特殊的循環,但通常情況下不推薦這樣做。
2.遞歸一般是在函數里面把函數自己給調用一遍,通過每次調用改變條件,來結束循環。
3.遞歸在數據格式一致,在數據層級未知的情況下,比普通的遍歷更有優勢。
4.遞歸在異步的時候,更容易理解,且更容易實現,因為可以在異步的回調里面,調用自己來實現每次都能拿到異步的結果再進行其他操作。
5.遞歸實現的快速排序比普通遍歷實現的排序效率更好。