正儿八经地写JavaScript之调试

前端的业务需求比较繁复,开发场景也比较多,因此掌握一些调试技巧是很有必要的,下面总结了日常工作中用到的一些调试技巧。

<!--more-->

参考:

1. 输出调试

利用浏览器和控制台来调试应该是最基本的调试方法了。

1.1. alert

alert是浏览器提供的一个全局函数,会通过弹窗的形式显示传入参数调用toString方法之后的值,一般来讲,可以用来直观的调试变量的值,遗憾的是默认对象形式的值只返回毫无参考意义的[object Object],虽然我们可以通过修改原型方法来编辑想要的输出结果。

function Foo(a, b){

    this.a = a;
    this.b = b;
}
Foo.prototype.toString = function(){
    var str = "";
    for (var key in this){
        if(this.hasOwnProperty(key)){
            str += `${key}=>${this[key]};\n`;
        }
    }
    return str;
}

var foo = new Foo(100, 200)
alert(foo)

另外的一个问题是alert会阻塞程序执行,用户体验性非常糟糕(要是出现在线上估计会被骂死,所以上线前需要删除调试代码),因此很少会采用这个方法进行调试。

不过最近我发现了一个使用alert进行调试的场景,即window.onerroralert配合进行移动端真机调试。

window.onerror = function(err, url, line){
    alert(err + "," + url + "," + line)    
}

实际上,onerror回调函数接受五个参数,参数详情参考MDN文档

我们可以通过这几个参数复现控制台出现的错误提示信息,在某些场景下这是很有用的,比如使用模拟器调试完全没问题的页面,在真机上却报错(通常是某个奇怪的兼容性问题),由于无法在真机上查看控制台,因此通过alert复现错误信息可以快速帮助定位错误。

提到window.onerror事件,我们还可以通过注册全局的处理函数来生成网页错误日志,以便监控和调试网页错误。这会在后面的日志调试同提到。

1.2. console

讲道理,在控制台输出变量,追踪程序流程并进行调试怕是最常用的调试方法了,而这其中用到的基本上都是console.log方法。实际上console对象提供了多种方法来方便我们进行调试。 我们来试一试:

console.log(console)

大概有二十多种方法~~是不是很感人,老规矩,MDN文档

console对象常用的的方法有:

  • log,输出字符串,类似于C语言的printf函数。这也是最常用的方法,没有之一。类似的方法还有info,warn,error等,以更直观的形式输出字符串(比如红色字体,加个感叹号啥的)~
  • time传入一个标识符并开启定时器,timeEnd会结束对应标识符的定时器,计算并输出这段代码的执行时间,可以用来测试代码效率
  • dir,table等方法可以格式化输出对应的数据
  • trace,打印函数调用栈

突然想起《孔乙己》里面的“你知道茴字有四种写法吗”的梗,但是这里绝对不含有讽刺的意味,反之,合理利用这些函数可以更直观地追踪代码及调试。

1.3. log4js

不论是使用alert还是console,必定会遇见的一个问题是:必须在上线前手动移除调试代码,这是因为调试只发生在开发过程中;然后遇见需要修复BUG的时候,我们又必须重新插入调试代码,这是一个很蛋疼的问题。庆幸的是,log4js帮我们解决了这个问题。

log4js解决“需要手动移除调试代码”的办法是为调试代码划分等级。通过设置logger.level等级,可以屏蔽低等级级的调试代码,在源码的level.js文件可以发现

const defaultLevels = {
    ALL: new Level(Number.MIN_VALUE, 'ALL', 'grey'),
    TRACE: new Level(5000, 'TRACE', 'blue'),
    DEBUG: new Level(10000, 'DEBUG', 'cyan'),
    INFO: new Level(20000, 'INFO', 'green'),
    WARN: new Level(30000, 'WARN', 'yellow'),
    ERROR: new Level(40000, 'ERROR', 'red'),
    FATAL: new Level(50000, 'FATAL', 'magenta'),
    MARK: new Level(9007199254740992, 'MARK', 'grey'), // 2^53
    OFF: new Level(Number.MAX_VALUE, 'OFF', 'grey')
};

比如

let log4js = require("log4js"),
    logger = log4js.getLogger();

logger.level = "info";

logger.info("info wtf");
logger.debug("debug wtf"); // 由于debug的等级低于info,因此该调试代码不会输出

也就是说,只需要在上线的时候设置logger.level = "off",整个世界是安静了~

遗憾的是log4js貌似并没有浏览器版本,这里推荐另外一个调试框架js-logging。 除了配置略有差异之外(js-logging通过构造参数进行配置),其他的使用方法与log4js基本类似,包括等级划分

let logger1 = Logging.colorConsole({
    level: "warning"
});
logger1.debug('Hello World!');

logger1.info('Hello World!');

2. 日志调试

开发后台应用时,更常用的做法是使用日志调试。在NodeJS中,可以通过将程序异常记录在磁盘的日志文件,从而记录服务器错误并调试代码。由于后台语言基本上都可以访问文件系统,所以调试日志可以方便保存和传递,这种调试方法更加灵活和方便。

当然我们可以调用原始的fs库函数来实现将变量格式化并输出到文件中

let fs = require("fs");
var a = 100;
// 这里可以对文档内容进行格式化
fs.writeFile("1.txm", a, "utf-8", function (err) {
    if (err) throw err;
})

当然,在业务代码中写满这些调试代码会显得十分丑陋,此外频繁的文件IO也会影响程序的性能,因此还是封装一下比较实在,log4js恰好也提供了这个功能,只需要修改输出方式即可

// 这里是官方的文档demo配置
log4js.configure({
    appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
    categories: { default: { appenders: ['cheese'], level: 'error' } }
});

此时执行程序,将会在根目录下得到cheese.log的日志文件,里面的内容即对应的输出结果,同理,log4jslevel同样生效,也就是说,我们可以通过设置等级控制日志文件的输出。

也许你会说,我们在开发过程中,直接通过控制台输出结果进行调试不久可以了吗,为何还要输出文件文件然后去检查日志呢?在某些环境下,使用控制台会比较麻烦(如果后台采用PHP进行开发~),此外在即使是程序在线上运行的时候,也可以通过输出的日志来检测代码的异常。有必要的时候,甚至可以通过发送邮件的形式提示线上环境异常。貌似现在的云服务器报警形式比较先进,这些东西可能用不上了。

3. 断点调试

前面的输出调试或日志调试,无非是通过通过检查变量值来对代码进行调试,频繁地编写调试代码也会有些繁琐,因此我猜你会喜欢上断点调试

断点(Breakpoint),除错(Debug)设定断点可以让程式执行到该行程式时停住,借此观察程式到断点位置时,其变数、暂存器、I/O等相关的变数内容,有助于深入了解程式运作的机制,发现、排除程序错误的根源。

学习断点调试主要可分为三部分:

  • 如何打断点
  • 如何分析断点
  • 按顺序逐步执行程序

3.1. 打断点

添加断点也可分为三种情景

  • 通过手动在代码中加入debugger关键字
  • 通过浏览器的Sources面板,通过点击行号添加断点
  • 通过IDE(我这里只用过WebStrom),通过点击行号断点,这里需要对WebStrom进行配置,参考WebStorm强大的调试JavaScript功能

断点在本质上本手动去插入console代码输出变量值并没有区别:都是为了对应位置变量的内容,调试程序的流程而已。不同的是,结合工具(浏览器或IDE)强大的分析能力,通过断点获取的变量内容更直观有效率。

3.2. 分析断点

以下面这段代码举例

<script type="text/javascript">
    (function(){
        var a = 100;

        function foo (){
            a = 200;
            var b = 100;
        }

        foo();
        debugger;

        // 不需要下面的调试代码了~
        console.log(a);
        console.log(b);
    })()
</script>

3.2.1. Chrome

当代码在浏览器中执行时,会提示Paused in debugger,打开开发者工具,切换到Sources面板,在源码面板会发现高亮标出的断点,其右侧就是该断点对应的变量信息(记得调整开发者工具的尺寸,也可能在下方)。这里点击Local折叠栏,可以发现类似于下列的信息

a:200
foo: foo()
this: Window

这样通过断点就可以知道程序运行到这时所有的局部变量了,这里表示此处的局部变量a的值是200,由于bfoo函数内部的局部变量,因此不会出现这里。 此外,如果不想在右侧的折叠面板去去找某个变量,也可以直接将鼠标放在源码的某个变量上,稍等就会出现变量相关的信息,这个技巧十分有用~

细心的你可能会发现有一个叫做Closure的面板,没错,这正是用来追踪闭包变量的。不过Chrome这里的闭包与我们常规理解的闭包有一点差异,以上面这段代码为例,按照之前的理解,这里的foo函数没有以返回值的形式传出,因此只是一个局部函数,也就没有形成闭包。而在这里的Closure的面板,却发现了a=200的信息,即Chrome认为foo使用了自由变量,因此这里已经形成了闭包。这只是对于闭包产生时机的一些差异,需要注意这个小细节否则可能会有点疑惑。

3.3. webstrom

在对WebStrom进行配置之后,只需要右键执行Debug xx.html即可进行调试,程序在执行到对应的断点时会弹出Debugger面板,在下面的Variables选项卡中也可以找到跟浏览器类似的折叠栏,从而分析对应的数据内容,具体操作大同小异,这里就不谈了。

当然,如果只是跟Chrome调试流程一样,我们也没有必要来折腾IDE的调试方法。Nodejs环境的代码是不能直接运行在浏览器中的,而WebStrom提供了跟调试浏览器代码类似的操作,这才是最主要的。这比满屏幕的console要舒服的多

3.4. 顺序执行

在断点代码分析的时候可以看见一排方向不同的箭头,鼠标移动上去会发现F8,F10,F11Shift + F11的快捷键提示,他们分别对应的是

  • F8,从断点处恢复运行,直至遇见下一个断点或程序错误
  • F10,按行执行代码,遇到自定义函数也当成一个语句执行,而不会进入函数内部
  • F11,同按行执行代码,区别在于遇见函数会进入函数内部并继续按行执行
  • Shift + F11,跳出当前正在调试的函数

逐步执行代码,相当于每行都在进行断点调试,也就是说我们可以查看整个程序的执行过程中所有变量的值,也可以函数调用栈。这在调试代码或者阅读源码的时候是非常有用的(比如实例化一个Vue实例到底发生了什么~)

关于顺序执行,上面引入的参考资料中有比较详细的介绍,这里就不献丑了,总之,按顺序逐行调试代码是一个必须学会的技巧。

3.5. 异常断点

在《Android开发权威指南》中还提到了一种异常断点的调试方法,这是通过IDE设置需要捕捉的异常类型然后快速定位到异常抛出的代码行,在 JavaScript中貌似没找到类似的调试方法

4. 最后

这里简单整理了一下JavaScript的调试方法,单就浏览器而言,Chrome本身的调试功能就已经很健全了,比如DOM断点帧动画等,科学调试可以节省很多时间,减少加班时长,这在今后需要更加注意。

想到之前有同事说我调代码快,其实我找BUG全靠猜,只是猜的比较准,哈哈...