大家好,我是前端西瓜哥。本文讲解如何使用浏览器提供的工具进行 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
,效果是错误被捕获时,打断点:
结尾
光说不练假把式,西瓜哥建议你自己尝试一番。
编程是一个实操性很强的学科,要自己动手调试,这样才能更好地理解掌握。
我是前端西瓜哥,欢迎关注我,学习更多前端知识。