JavaScript

网页 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 则符合代码的执行顺序,先执行后面的同步代码,然后再执行异步函数。