1. 遞迴是啥?
遞迴概念很簡單,“自己呼叫自己”(下面以函式為例)。
在分析遞迴之前,需要了解下 JavaScript 中“壓棧”(call stack
) 概念。
2. 壓棧與出棧
棧是什麼?可以理解是在記憶體中某一塊區域,這個區域比喻成一個箱子,你往箱子裡放些東西,這動作就是壓棧。所以最先放下去的東西在箱子最底下,最後放下去的在箱子最上面。把東西從箱子中拿出來可以理解為出棧。
所以得出結論,我們有個習慣,拿東西是從上面往下拿,最先放下去的東西在箱子的最底下,最後才能拿到。
在 JavaScript 中,呼叫函式時,都會發生壓棧行為,遇到含 return
關鍵字的句子或執行結束後,才會發生出棧(pop)。
來看個例子,這個段程式碼執行順序是怎樣的?
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
先壓棧,放在棧(箱子)最底下 - 在函式 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
出棧。 - 現在棧中又為空了。
上面步驟容易把人繞暈,下面是流程圖:
再看下在 chrome 瀏覽器環境下執行:
3. 遞迴
先看下簡單的 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)
複製程式碼
4. 遞迴注意點
如果上面例子修改一下,如下:
function multiple(num) {
if (num === 1) console.log(1)
return num * multiple(num - 1)
}
multiple(3) // Error: Maximum call stack size exceeded
複製程式碼
上面程式碼第一行沒有 return
關鍵字句子,因為遞迴沒有終止條件,所以會一直壓棧,造成記憶體洩漏。
遞迴容易出錯點
- 沒有設定終止點
- 除了遞迴,其他部分的程式碼
- 什麼時候遞迴合適
好了,以上就是 JavaScript 方式遞迴的概念。
下面是練習題。
6. 練習題目
- 寫一個函式,接受一串字串,返回一個字串,這個字串是將原來字串倒過來。
- 寫一個函式,接受一串字串,同時從前後開始拿一位字元對比,如果兩個相等,返回
true
,如果不等,返回false
。 - 編寫一個函式,接受一個陣列,返回扁平化新陣列。
- 編寫一個函式,接受一個物件,這個物件值是偶數,則讓它們相加,返回這個總值。
- 編寫一個函式,接受一個物件,返回一個陣列,這個陣列包括物件裡所有的值是字串
7. 參考答案
參考1
function reverse(str) {
if(str.length <= 1) return str;
return reverse(str.slice(1)) + str[0];
}
reverse('abc')
複製程式碼
參考2
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)
複製程式碼
參考3
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])
複製程式碼
參考4
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}})
複製程式碼
參考5
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)
複製程式碼
5. 總結
遞迴精髓是,往往要先想好常規部分是怎樣的,在考慮遞迴部分,下面是 JavaScript 遞迴常用到的方法
- 陣列:
slice
,concat
- 字串:
slice
,substr
,substring
- 物件:
Object.assign