大家好,我是前端西瓜哥。本文講解如何使用瀏覽器提供的工具進行 JS 代碼的斷點調試。
debugger
在代碼中需要打斷點的地方,加上 debugger,表示一個斷點。瀏覽器代碼執行到該位置時,會停下來,進入調試模式。
示例代碼:
function a() {
let a_var = 'a';
b(a_var);
}
function b(a_var_from_a) {
debugger;
console.log(global_var);
let b_var = 'b';
c();
}
function c() {
let c_var = 'c';
}
let module_var = 'module';
var global_var = 'global';
a();
整體就是調用 a,然后 a 調用 b,b 調用 c,然后有各種作用域的變量。
記得打開開發者工具面板,沒打開的話,debugger 會靜默失敗。
下面是斷點后的樣子。
現在是 a 函數調用了 b 函數,b 函數調用的時候用 debugger 加了個斷點,于是我們就停在這里了。
此時上下文狀態和調用站會保留著,方便我們排查是什么分支導致變量狀態錯誤,比如一個錯誤的條件判斷,讓一個為 null 的變量沒能變成一個普通對象,導致訪問它的屬性報錯。
手動打斷點
在對應的行號點一下就可以了,相當于加了個 debugger 關鍵字。
刷新頁面后,手動打的斷點還會保留。
調用棧信息
首先是函數調用棧信息。
調用的起始端是一個匿名函數,沒有名字的函數都會顯示 anonymouse,這里是 script 最外層的直接調用,所以沒有名字。我本人建議多給匿名函數起個名字,可讀性會更好。但如果你有起名困難癥,不起也好。
匿名函數,然后調用了函數 a,函數 a 再調用了函數 b,然后停下了。之后還會調用 c。
看到 b 旁邊的藍色箭頭沒,它表示我們 正在觀測哪個函數的上下文,默認會選擇棧頂。
你可以用光標選擇你要觀測的函數下的變量,并且會高亮對應的代碼。
作用域
我們看看某個函數的函數作用域的上下文。
找到 Scope 這個標簽頁,目前我們可以看到有三種類型:Local、Script 和 Global。
首先是 Local,本地作用域。這里對應 b 函數的上下文,可以看到(1)傳入的變量,(2)函數內部聲明的變量,以及 (3)this 值。
然后是 Script,表示一個模塊文件的最外層變量,和全局變量不同,只能被模塊文件內的文件的代碼訪問。
最后是 Global,全局作用域。
再補充一個比較常見的閉包作用域。如果一個函數是通過閉包產生的,那在 Local 和 Script 還會有一個閉包作用域。
在函數中訪問一個變量,其實就是沿著這條鏈路去查找最先找到的那個,如果找不到就會拿到 undefined。
當然除了這些,還有不少,比如塊作用域(Block)、捕獲作用域(Catch Block)、Eval 作用域、With 塊作用域等,篇幅原因,不一一介紹了。
執行下一步
實際我們還要看看代碼是否如預期進入我們希望的分支并拿到正確的值,所以需要 讓代碼一點點執行下去,觀察狀態的變化。
瀏覽器提供了各種執行下一步代碼的方式。
我們一個個過一遍。
Resume 恢復腳本執行
首先是最左邊這個 矩形+三角形 的藍色按鈕,它表示 結束當前斷點,恢復腳本運行。
但如果往下執行,又遇到一個斷點,那又會進入調試模式。
于是在長循環的情況下,就出不來了(悲)。
這時候惱羞成怒的西瓜哥有個辦法,長按這個按鈕,然后出現一個停止按鈕,點它會 結束所有的斷點。
或者更常見的做法是,只在特定判斷條件下的打斷點,比如:
todoItems.forEach(item => {
// item 不可能為 null,我們來看看發生甚么事了
if (!item) {
debugger;
}
// ...
})
Step over 跳過下一個函數
然后是跳過下一個函數。就是遇到下一個要執行的函數,就不進去了,執行完它繼續往下運行。
為什么要跳過函數?因為函數里面有很多代碼,或者里面又調用了其他函數,要走好久才能回到當前函數。
如果我們只是想看當前函數的完整邏輯,那就跳過其下的函數執行。
Step into 進入下一個函數
如果走著走著遇到一個函數,進入這個函數。
注意,瀏覽器環境自帶的 api 方法是進不去的。
Step out 跳出當前函數
如果你不想再看當前函數的執行了,想回到調用它的函數,就可以選擇這個方式。
Step 下一步
就是普通的下一步,它會嚴格遵守代碼的執行順序,比較常用。
Step into 和 Step 的區別
Step into 和 Step 在大體的表現上有些相同,遇到函數是會進入函數內部的,但在異步代碼下,行為有一些不同。
Step into 在遇到一個異步代碼的回調函數,會直接掛起并讓后面的同步代碼繼續跑,直到這個異步函數被執行,然后進入這個函數。
而 Step 則符合代碼的執行順序,先執行后面的同步代碼,然后再執行異步函數。
我們用一個實例演示一下。代碼為:
window.onclick = () => {
debugger;
setTimeout(() => {
console.log('inside');
console.log('p1', performance.now() / 1000);
}, 2000);
console.log('p2', performance.now() / 1000);
console.log('p3', performance.now() / 1000);
};
Step into 的表現:
可以看到,Step into 會等待異步函數被執行,才進入到函數內部,然后停在它的首行。
然后是 Step:
Step 遵循正常的代碼執行過程順序:先走完同步代碼,然后再進入異步代碼。
直接跳到某一行
我們可能想直接跳到中間的一連串邏輯,直接走到后面的某一行,對此我們可以手動跳轉。
具體做法是行號右鍵選擇 continue to here。
需要注意,這個地方必須是和當前位置在同一個函數下,否則會等價于執行了 Resume。
其他
關閉斷點功能
關閉斷點功能(deactivate breakpoint)。
開啟這個,斷點在打開開發者工具的條件下無效。
上一篇文章西瓜哥說了一個用定時器不斷執行 debugger 的方式,防止別人點點點看代碼是怎么執行的。但這只能防小白,我們把這個開了就無視 debugger 關鍵字了。
報錯時斷點
代碼報錯時,我們希望知道報錯那瞬間的上下文。
此時我們可以開啟這個功能,在報錯且沒有被捕獲時,瀏覽器會給你打一個斷點,然后你可以看看哪個變量出了問題。
還可以勾選這個 Pause on caught exceptions,效果是錯誤被捕獲時,打斷點:
結尾
光說不練假把式,西瓜哥建議你自己嘗試一番。
編程是一個實操性很強的學科,要自己動手調試,這樣才能更好地理解掌握。