溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript錯誤和調(diào)用棧常識都有哪些

發(fā)布時間:2021-11-16 17:16:58 來源:億速云 閱讀:105 作者:柒染 欄目:web開發(fā)

JavaScript錯誤和調(diào)用棧常識都有哪些,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。

大多數(shù)工程師可能并沒留意過 JS 中錯誤對象、錯誤堆棧的細(xì)節(jié),即使他們每天的日常工作會面臨不少的報錯,部分同學(xué)甚至在 console  的錯誤面前一臉懵逼,不知道從何開始排查,如果你對本文講解的內(nèi)容有系統(tǒng)的了解,就會從容很多。而錯誤堆棧清理能讓你有效去掉噪音信息,聚焦在真正重要的地方,此外,如果理解了  Error 的各種屬性到底是什么,你就能更好的利用他。

接下來,我們就直奔主題。

調(diào)用棧的工作機制

在探討 JS 中的錯誤之前,我們必須理解調(diào)用棧(Call Stack)的工作機制,其實這個機制非常簡單,如果你對這個已經(jīng)一清二楚了,可以直接跳過這部分內(nèi)容。

簡單的說:函數(shù)被調(diào)用時,就會被加入到調(diào)用棧頂部,執(zhí)行結(jié)束之后,就會從調(diào)用棧頂部移除該函數(shù),這種數(shù)據(jù)結(jié)構(gòu)的關(guān)鍵在于后進先出,即大家所熟知的 LIFO。比如,當(dāng)我們在函數(shù) y 內(nèi)部調(diào)用函數(shù) x 的時候,調(diào)用棧從下往上的順序就是 y -> x 。

我們再舉個代碼實例:

function c() {console.log('c');
}function b() {console.log('b');
    c();
}function a() {console.log('a');
    b();
}

a();

這段代碼運行時,首先 a 會被加入到調(diào)用棧的頂部,然后,因為 a 內(nèi)部調(diào)用了 b,緊接著 b 被加入到調(diào)用棧的頂部,當(dāng) b 內(nèi)部調(diào)用 c  的時候也是類似的。在調(diào)用 c的時候,我們的調(diào)用棧從下往上會是這樣的順序:a -> b -> c。在 c 執(zhí)行完畢之后,c  被從調(diào)用棧中移除,控制流回到 b 上,調(diào)用棧會變成:a -> b,然后 b 執(zhí)行完之后,調(diào)用棧會變成:a,當(dāng) a  執(zhí)行完,也會被從調(diào)用棧移除。

為了更好的說明調(diào)用棧的工作機制,我們對上面的代碼稍作改動,使用 console.trace 來把當(dāng)前的調(diào)用棧輸出到 console 中,你可以認(rèn)為console.trace 打印出來的調(diào)用棧的每一行出現(xiàn)的原因是它下面的那行調(diào)用而引起的。

function c() {console.log('c');console.trace();
}function b() {console.log('b');
    c();
}function a() {console.log('a');
    b();
}

a();

當(dāng)我們在 Node.js 的 REPL 中運行這段代碼,會得到如下的結(jié)果:

Traceat c (repl:3:9)
    at b (repl:3:1)
    at a (repl:3:1)
    at repl:1:1 // <-- 從這行往下的內(nèi)容可以忽略,因為這些都是 Node 內(nèi)部的東西at realRunInThisContextScript (vm.js:22:35)
    at sigintHandlersWrap (vm.js:98:12)
    at ContextifyScript.Script.runInThisContext (vm.js:24:12)
    at REPLServer.defaultEval (repl.js:313:29)
    at bound (domain.js:280:14)
    at REPLServer.runBound [as eval] (domain.js:293:12)

顯而易見,當(dāng)我們在 c 內(nèi)部調(diào)用 console.trace 的時候,調(diào)用棧從下往上的結(jié)構(gòu)是:a -> b -> c。如果把代碼再稍作改動,在 b 中 c 執(zhí)行完之后調(diào)用,如下:

function c() {console.log('c');
}function b() {console.log('b');
    c();console.trace();
}function a() {console.log('a');
    b();
}

a();

通過輸出結(jié)果可以看到,此時打印的調(diào)用棧從下往上是:a -> b,已經(jīng)沒有 c 了,因為 c 執(zhí)行完之后就從調(diào)用棧移除了。

Trace
    at b (repl:4:9)
    at a (repl:3:1)
    at repl:1:1  // <-- 從這行往下的內(nèi)容可以忽略,因為這些都是 Node 內(nèi)部的東西at realRunInThisContextScript (vm.js:22:35)
    at sigintHandlersWrap (vm.js:98:12)
    at ContextifyScript.Script.runInThisContext (vm.js:24:12)
    at REPLServer.defaultEval (repl.js:313:29)
    at bound (domain.js:280:14)
    at REPLServer.runBound [as eval] (domain.js:293:12)
    at REPLServer.onLine (repl.js:513:10)

再總結(jié)下調(diào)用棧的工作機制:調(diào)用函數(shù)的時候,會被推到調(diào)用棧的頂部,而執(zhí)行完畢之后,就會從調(diào)用棧移除。

Error 對象及錯誤處理

當(dāng)代碼中發(fā)生錯誤時,我們通常會拋出一個 Error 對象。Error 對象可以作為擴展和創(chuàng)建自定義錯誤類型的原型。Error 對象的 prototype 具有以下屬性:

  • constructor &ndash; 負(fù)責(zé)該實例的原型構(gòu)造函數(shù);

  • message &ndash; 錯誤信息;

  • name &ndash; 錯誤的名字;

上面都是標(biāo)準(zhǔn)屬性,有些 JS 運行環(huán)境還提供了標(biāo)準(zhǔn)屬性之外的屬性,如 Node.js、Firefox、Chrome、Edge、IE 10、Opera 和 Safari 6+ 中會有 stack 屬性,它包含了錯誤代碼的調(diào)用棧,接下來我們簡稱錯誤堆棧。錯誤堆棧包含了產(chǎn)生該錯誤時完整的調(diào)用棧信息。如果您想了解更多關(guān)于 Error 對象的非標(biāo)準(zhǔn)屬性,我強烈建議你閱讀 MDN 的這篇文章。

拋出錯誤時,你必須使用 throw 關(guān)鍵字。為了捕獲拋出的錯誤,則必須使用 try catch 語句把可能出錯的代碼塊包起來,catch  的時候可以接收一個參數(shù),該參數(shù)就是被拋出的錯誤。與 Java 中類似,JS 中也可以在 try catch 語句之后有  finally,不論前面代碼是否拋出錯誤 finally 里面的代碼都會執(zhí)行,這種語言的常見用途有:在 finally 中做些清理的工作。

此外,你可以使用沒有 catch 的 try 語句,但是后面必須跟上 finally,這意味著我們可以使用三種不同形式的 try 語句:

  • try &hellip; catch

  • try &hellip; finally

  • try &hellip; catch &hellip; finally

try 語句還可以嵌套在 try 語句中,比如:

try {try {throw new Error('Nested error.'); // 這里的錯誤會被自己緊接著的 catch 捕獲} catch (nestedErr) {console.log('Nested catch'); // 這里會運行}
} catch (err) {console.log('This will not run.');  // 這里不會運行}

try 語句也可以嵌套在 catch 和 finally 語句中,比如下面的兩個例子:

try {throw new Error('First error');
} catch (err) {console.log('First catch running');try {throw new Error('Second error');
    } catch (nestedErr) {console.log('Second catch running.');
    }
}
try {console.log('The try block is running...');
} finally {try {throw new Error('Error inside finally.');
    } catch (err) {console.log('Caught an error inside the finally block.');
    }
}

同樣需要注意的是,你可以拋出不是 Error 對象的任意值。這可能看起來很酷,但在工程上卻是強烈不建議的做法。如果恰巧你需要處理錯誤的調(diào)用棧信息和其他有意義的元數(shù)據(jù),拋出非 Error 對象的錯誤會讓你的處境很尷尬。

假如我們有如下的代碼:

function runWithoutThrowing(func) {try {
        func();
    } catch (e) {console.log('There was an error, but I will not throw it.');console.log('The error\'s message was: ' + e.message)
    }
}function funcThatThrowsError() {throw new TypeError('I am a TypeError.');
}

runWithoutThrowing(funcThatThrowsError);

如果 runWithoutThrowing 的調(diào)用者傳入的函數(shù)都能拋出 Error 對象,這段代碼不會有任何問題,如果他們拋出了字符串那就有問題了,比如:

function runWithoutThrowing(func) {try {
        func();
    } catch (e) {console.log('There was an error, but I will not throw it.');console.log('The error\'s message was: ' + e.message)
    }
}function funcThatThrowsString() {throw 'I am a String.';
}

runWithoutThrowing(funcThatThrowsString);

這段代碼運行時,runWithoutThrowing 中的第 2 次 console.log 會拋出錯誤,因為 e.message  是未定義的。這些看起來似乎沒什么大不了的,但如果你的代碼需要使用 Error  對象的某些特定屬性,那么你就需要做很多額外的工作來確保一切正常。如果你拋出的值不是 Error 對象,你就不會拿到錯誤相關(guān)的重要信息,比如  stack,雖然這個屬性在部分 JS 運行環(huán)境中才會有。

Error 對象也可以向其他對象那樣使用,你可以不用拋出錯誤,而只是把錯誤傳遞出去,Node.js 中的錯誤優(yōu)先回調(diào)就是這種做法的典型范例,比如 Node.js 中的 fs.readdir 函數(shù):

const fs = require('fs');

fs.readdir('/example/i-do-not-exist', function callback(err, dirs) {if (err) {// `readdir` will throw an error because that directory does not exist// We will now be able to use the error object passed by it in our callback functionconsole.log('Error Message: ' + err.message);console.log('See? We can use Errors without using try statements.');
    } else {console.log(dirs);
    }
});

此外,Error 對象還可以用于 Promise.reject 的時候,這樣可以更容易的處理 Promise 失敗,比如下面的例子:

new Promise(function(resolve, reject) {
    reject(new Error('The promise was rejected.'));
}).then(function() {console.log('I am an error.');
}).catch(function(err) {if (err instanceof Error) {console.log('The promise was rejected with an error.');console.log('Error Message: ' + err.message);
    }
});

錯誤堆棧的裁剪

Node.js 才支持這個特性,通過 Error.captureStackTrace  來實現(xiàn),Error.captureStackTrace 接收一個 object 作為第 1 個參數(shù),以及可選的 function 作為第 2  個參數(shù)。其作用是捕獲當(dāng)前的調(diào)用棧并對其進行裁剪,捕獲到的調(diào)用棧會記錄在第 1 個參數(shù)的 stack 屬性上,裁剪的參照點是第 2  個參數(shù),也就是說,此函數(shù)之前的調(diào)用會被記錄到調(diào)用棧上面,而之后的不會。

讓我們用代碼來說明,首先,把當(dāng)前的調(diào)用棧捕獲并放到 myObj 上:

const myObj = {};function c() {
}function b() {// 把當(dāng)前調(diào)用棧寫到 myObj 上Error.captureStackTrace(myObj);
    c();
}function a() {
    b();
}// 調(diào)用函數(shù) aa();// 打印 myObj.stackconsole.log(myObj.stack);// 輸出會是這樣//    at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack//    at a (repl:2:1)//    at repl:1:1 <-- Node internals below this line//    at realRunInThisContextScript (vm.js:22:35)//    at sigintHandlersWrap (vm.js:98:12)//    at ContextifyScript.Script.runInThisContext (vm.js:24:12)//    at REPLServer.defaultEval (repl.js:313:29)//    at bound (domain.js:280:14)//    at REPLServer.runBound [as eval] (domain.js:293:12)//    at REPLServer.onLine (repl.js:513:10)

上面的調(diào)用棧中只有 a -> b,因為我們在 b 調(diào)用 c 之前就捕獲了調(diào)用?!,F(xiàn)在對上面的代碼稍作修改,然后看看會發(fā)生什么:

const myObj = {};function d() {// 我們把當(dāng)前調(diào)用棧存儲到 myObj 上,但是會去掉 b 和 b 之后的部分Error.captureStackTrace(myObj, b);
}function c() {
    d();
}function b() {
    c();
}function a() {
    b();
}// 執(zhí)行代碼a();// 打印 myObj.stackconsole.log(myObj.stack);// 輸出如下//    at a (repl:2:1) <-- As you can see here we only get frames before `b` was called//    at repl:1:1 <-- Node internals below this line//    at realRunInThisContextScript (vm.js:22:35)//    at sigintHandlersWrap (vm.js:98:12)//    at ContextifyScript.Script.runInThisContext (vm.js:24:12)//    at REPLServer.defaultEval (repl.js:313:29)//    at bound (domain.js:280:14)//    at REPLServer.runBound [as eval] (domain.js:293:12)//    at REPLServer.onLine (repl.js:513:10)//    at emitOne (events.js:101:20)

在這段代碼里面,因為我們在調(diào)用 Error.captureStackTrace 的時候傳入了 b,這樣 b 之后的調(diào)用棧都會被隱藏。

現(xiàn)在你可能會問,知道這些到底有啥用?如果你想對用戶隱藏跟他業(yè)務(wù)無關(guān)的錯誤堆棧(比如某個庫的內(nèi)部實現(xiàn))就可以試用這個技巧。

看完上述內(nèi)容,你們掌握J(rèn)avaScript錯誤和調(diào)用棧常識都有哪些的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI