關於堆棧的溢出問題,在Javascript日常開發中很常見,Google了下,相關問題還是比較多的。本文旨在描述如何解決此類問題。 首先看一個實例(當然你可以使用更容易的方式實現,這里我們僅探討遞歸):
function isEven (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return isEven(Math.abs(num) - 2);
}
//Outputs: true
console.log(isEven(10));
//Outputs: false
console.log(isEven(9));
當我們把參數改成10000時,運行下例會發生堆棧溢出:
function isEven (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return isEven(Math.abs(num) - 2);
}
//不同的javascript引擎報錯可能不同
//Outputs: Uncaught RangeError: Maximum call stack size exceeded
console.log(isEven(10000));
原因是每次執行代碼時,都會分配一定尺寸的棧空間(Windows系統中為1M),每次方法調用時都會在棧里儲存一定信息(如參數、局部變量、返回值等等),這些信息再少也會占用一定空間,成千上萬個此類空間累積起來,自然就超過線程的棧空間了。那么如何解決此類問題?
使用閉包:
function isEven (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return function() {
return isEven(Math.abs(num) - 2);
}
}
//Outputs: true
console.log(isEven(4)()());
此時每次調用時,返回一個匿名函數,匿名函數執行相關的參數和局部變量將會釋放,不會額外增加堆棧大小。
優化調用:
上例調用比較麻煩,優化如下:
function isEven (num) {
if (num === 0) {
return true;
}
if (num === 1) {
return false;
}
return function() {
return isEven(Math.abs(num) - 2);
}
}
function trampoline (func, arg) {
var value = func(arg);
while(typeof value === "function") {
value = value();
}
return value;
}
//Outputs: true
console.log(trampoline(isEven, 10000));
//Outputs: false
console.log(trampoline(isEven, 10001));
現在我們可以解決堆棧溢出問題了,但是不是感覺每次tarmpoline(isEven, 1000)這種調用方式不是很好,我們可以使用bind來綁定:
function isEven(n) {
/**
* [isEvenInner 遞歸]
* @param {[type]} num [description]
* @return {Boolean} [description]
*/
function isEvenInner (n) {
if (n === 0) {
return true;
}
if (n === 1) {
return false;
}
return function() {
return isEvenInner(Math.abs(n) - 2);
}
}
/**
* [trampoline 迭代]
* @param {[type]} func [description]
* @param {[type]} arg [description]
* @return {[type]} [description]
*/
function trampoline (func, arg) {
var value = func(arg);
while(typeof value === "function") {
value = value();
}
return value;
}
return trampoline.bind(null, isEvenInner)(n);
}
//Outputs: true
console.log(isEven(10000));
//Outputs: false
console.log(isEven(10001));
雖然上例實現了我們想要的效果,但是trampoline函數還是有一定的局限性:
1.假設你只傳遞一個參數給遞歸函數
value = func(arg); 修改為 value = func.apply(func, arg);
2.假設最后的返回值不是一個函數 關於更健壯性的實現,請看underscore-contrib中源碼。
感謝您的閱讀,文中不妥之處還望批評指正,文章已同步至個人博客如果你有好的建議,歡迎留言,么么噠!
轉載聲明:
本文標題:Javascript中遞歸造成的堆棧溢出及解決方案
本文鏈接:http://www.zuojj.com/archives/1115.html,轉載請注明轉自Benjamin-專注前端開發和用戶體驗
