原文地址
遞歸概念很簡單,「本身調用本身」(下面以函數爲例)。git
在分析遞歸以前,須要瞭解下 JavaScript 中「壓棧」(call stack
) 概念。github
棧是什麼?能夠理解是在內存中某一塊區域,這個區域比喻成一個箱子,你往箱子裏放些東西,這動做就是壓棧。因此最早放下去的東西在箱子最底下,最後放下去的在箱子最上面。把東西從箱子中拿出來能夠理解爲*出棧。chrome
因此得出結論,咱們有個習慣,拿東西是從上面往下拿,最早放下去的東西在箱子的最底下,最後才能拿到。數組
在 JavaScript 中,調用函數時,都會發生壓棧行爲,遇到含 return
關鍵字的句子或執行結束後,纔會發生出棧(pop)。瀏覽器
來看個例子,這個段代碼執行順序是怎樣的?app
function fn1() { return 'this is fn1' } function fn2() { fn3() return 'this is fn2' } function fn3() { let arr = ['apple', 'banana', 'orange'] return arr.length } function fn() { fn1() fn2() console.log('All fn are done') } fn()
上面發生了一系列壓棧出棧的行爲:函數
fn
執行,fn
先壓棧,放在棧(箱子)最底下fn1
,fn1
壓棧在 fn
上面fn1
執行,遇到 return
關鍵字,返回 this is fn1
,fn1
出棧,箱子裏如今只剩下 fn
fn
內部,fn1
執行完後,函數 fn2
開始執行,fn2
壓棧,可是在 fn2
內部,遇到 fn3
,開始執行 fn3
,因此 fn3
壓棧fn3
<= fn2
<= fn
,fn
在最底下fn3
內部遇到 return
關鍵字的句子,fn3
執行完結束後出棧,回到函數 fn2
中 ,fn2
也是遇到 return
關鍵字,繼續出棧。fn
了,回到函數 fn
中,執行 console.log('All fn are done'
) 語句後,fn
出棧。上面步驟容易把人繞暈,下面是流程圖:this
再看下在 chrome 瀏覽器環境下執行:spa
先看下簡單的 JavaScript 遞歸code
function sumRange(num) { if (num === 1) return 1; return num + sumRange(num - 1) } sumRange(3) // 6
上面代碼執行順序:
sumRange(3)
執行,sumRange(3)
壓棧,遇到 return
關鍵字,但這裏還立刻不能出棧,由於調用了 sumRange(num - 1)
即 sumRange(2)
sumRange(2)
壓棧,以此類推,sumRange(1)
壓棧,sumRange(1)
出棧返回 1,sumRange(2)
出棧,返回 2,sumRange(3)
出棧3 + 2 + 1
結果爲 6看流程圖
因此,遞歸也是個壓棧出棧的過程。
遞歸能夠用非遞歸表示,下面是上面遞歸例子等價執行
// for 循環 function multiple(num) { let total = 1; for (let i = num; i > 1; i--) { total *= i } return total } multiple(3)
若是上面例子修改一下,以下:
function multiple(num) { if (num === 1) console.log(1) return num * multiple(num - 1) } multiple(3) // Error: Maximum call stack size exceeded
上面代碼第一行沒有 return
關鍵字句子,由於遞歸沒有終止條件,因此會一直壓棧,形成內存泄漏。
遞歸容易出錯點
好了,以上就是 JavaScript 方式遞歸的概念。
下面是練習題。
true
,若是不等,返回 false
。function reverse(str) { if(str.length <= 1) return str; return reverse(str.slice(1)) + str[0]; } reverse('abc')
function isPalindrome(str){ if(str.length === 1) return true; if(str.length === 2) return str[0] === str[1]; if(str[0] === str.slice(-1)) return isPalindrome(str.slice(1,-1)) return false; } var str = 'abba' isPalindrome(str)
function flatten (oldArr) { var newArr = [] for(var i = 0; i < oldArr.length; i++){ if(Array.isArray(oldArr[i])){ newArr = newArr.concat(flatten(oldArr[i])) } else { newArr.push(oldArr[i]) } } return newArr; } flatten([1,[2,[3,4]],5])
function nestedEvenSum(obj, sum=0) { for (var key in obj) { if (typeof obj[key] === 'object'){ sum += nestedEvenSum(obj[key]); } else if (typeof obj[key] === 'number' && obj[key] % 2 === 0){ sum += obj[key]; } } return sum; } nestedEvenSum({c: 4,d: {a: 2, b:3}})
function collectStrings(obj) { let newArr = [] for (let key in obj) { if (typeof obj[key] === 'string') { newArr.push(obj[key]) } else if(typeof obj[key] === 'object' && !Array.isArray(obj[key])) { newArr = newArr.concat(collectStrings(obj[key])) } } return newArr } var obj = { a: '1', b: { c: 2, d: 'dd' } } collectStrings(obj)
遞歸精髓是,每每要先想好常規部分是怎樣的,在考慮遞歸部分,下面是 JavaScript 遞歸經常使用到的方法
slice
, concat
slice
, substr
, substring
Object.assign