原文地址git
遞歸概念很簡單,「本身調用本身」(下面以函數爲例)。github
在分析遞歸以前,須要瞭解下 JavaScript 中「壓棧」(call stack
) 概念。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()
複製代碼
上面發生了一系列壓棧出棧的行爲:this
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
出棧。上面步驟容易把人繞暈,下面是流程圖:spa
再看下在 chrome 瀏覽器環境下執行:code
先看下簡單的 JavaScript 遞歸
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