DEBUG心得

这篇文章记录了笔者对于debug的心得。

Debug对于程序员来说应该是最普遍的一件事,如果你在试图写一些不熟悉的代码,那么估计有80%的时间都在debug。debug的方法可以说直接影响到你的编码效率。

先分享一次笔者的debug经历

1.网页不正常,vue渲染失败,控制台提示vue渲染时错误
2.发现这种报错只在IE11的iframe中出现
3.只在url带hash(如http://a.cn/b.html#xxx)的情况下报错
4.新建一个空白页a,使用vue渲染,url带hash,不报错
5.a页中复制问题页代码,url带hash,报错
6.删除a页中的大部分代码,每次只留一部分,发现只在有表单组件时报错
7.新建了一个input标签,删除其他表单组件,不报错
8.初步判断element-ui的表单组件和hash冲突
9.以hash为关键字搜索element-ui的js源码,发现两行相关代码,屏蔽之,错误依旧
10.格式化vue.min.js的代码,根据报错信息从控制台定位到相关代码
11.发现运行时document.activeElement值为“错误”
12.推测hash改变了activeElement的值,在a页中添加了hash对应id的元素,错误依旧
13.新建空白页b,打印activeElement的默认值,发现为html元素
14.在a页首部插入代码document.body.focus(),将activeElement设置为body,错误排除
15.在原问题页首部插入代码document.body.focus(),错误排除

成因:IE11的iframe子页src有hash的情况下,document.activeElement是一个非法值,而element-ui组件在运行时读取了它,导致错误。

方案:页首部插入代码document.body.focus(),将activeElement设置为body

结论:珍爱生命,远离IE!

为什么要自己DEBUG

我眼中的程序员有两种:咸鱼程序员和非咸鱼程序员。
咸鱼可能有着自己的擅长领域,却失去了探索和质疑的勇气。
他们不是创造者,而是使用者,所以遇到问题第一时间是看看这个产品有没有答疑的途径,而不是自己试图去解决。
如果你不想变成咸鱼,先改变一小步,自己去debug吧!

  • Bug是不可避免的,谁也不敢说自己写程序没bug,要不然要测试部门干嘛。
  • 排除bug的时候你可能会对编程语言的底层有更深的理解(堆栈、形参、运行时、时序等)。
  • 什么是大佬?大佬就是你踩过足够多的坑,知道去避开它们,而菜鸟不会。

关于咸鱼,这里不是说不应该去找答疑,而是强调失去了自我答疑的能力。

工欲善其事,必先利其器

相信我,好的debug工具能使你事半功倍。
首先,你需要IDE。
虽然现在的脚本语言逐渐流行,很多代码不需要复杂的环境配置就可以编辑,但是IDE依然是不可或缺的东西,专业的工具做专业的事,它会有一大堆适合你编码的工具,从源头上减少bug产生的概率。

IDE会更好的对你的代码进行省查,代码补全和错误提示都会提高你的代码质量。编辑器也可以实现,但是编辑器不是以一个项目来组织代码,效果相对的会差很多。

debug阶段,你也需要合适的工具。
不要满足于console.log()!
不要满足于console.log()!
不要满足于console.log()!
重要的事情说三遍。
在代码中临时插入控制台输出来调试代码真的是很低效的行为。

  • 控制台输出往往是字符串格式,碰到复杂的数据结构,还是很难看懂,更别说一些变量不能被无损转化为字符串。
  • 临时插入的代码,你还要记得去删除它。
  • 一行输出,并不能查看上下文和运行的堆栈结构(除非你打印更多的输出。。。)

对于js,你应该使用谷歌浏览器的F12或火狐的firebug;对于php,你应该使用xdebug。我相信任何语言都有不错的debug手段,如果它没有,社区会自制出来的。

快速定位

要尽快的定位到你的bug在哪一行,以及为什么会产生bug。
我们对一个程序出错的第一反应大概就是,卧槽,程序崩了。沮丧是没用的,快速平复心情去解决吧~

1.从外到内。从程序的输入开始试错,弄清楚在什么输入下会触发这个问题,进而找出与这个输入相关的代码。
2.从内到外。借助行号提示,单步执行等线索,直接定位到问题代码,然后查看堆栈调用顺序,找出源头。

这两种方法都是很有效的,大部分情况是结合使用。需要注意的是,有的bug代码并不会报错,只是输出不符合预期,此时第一种办法更有效;有的bug调用层次较深,逻辑复杂,联想不到与输入的关联,则适合第二种办法。

从源头解决

Bug的解决要从源头做起。某些咸鱼喜欢全部try...catch...搞定,这是对自己的项目相当不负责任的做法。

如果是笔误性的错误,如单词拼写错误,修复之后要考虑是不是这个单词太难写了,为什么IDE没有自动提示,可不可以取别名,打包成别的函数等等;如果反复在某处出现拼写错误和修改遗漏,你就该考虑此处是不是该用更好的代码结构,减少机械化抄写的部分;甚至...你真的太累了,需要给自己放个假先。

对于逻辑错误,应该从更源头的地方解决。例如代码a/bb=0的时候错误,你应该在输入b的时候检查不允许为0,而不是仅仅try{a/b}catch{...}(不嫌麻烦的话,可以两步都做)。反思bug的源头帮助你更好地预防其他bug,你可能会发现不合理的需求、不合理的算法和优化的灵感。

小技巧

递归函数的DEBUG

递归函数往往并不是那么容易去理解,因为它会反复调用自身,导致你很难去理清楚它的时序和运行时上下文。
我们来看一下这个经典的斐波那契递归函数

<script>
        var fib=function (n) {
            var value= n<=2?1:fib(n-1)+fib(n-2);
            return value;
        };
        console.log(fib(5))
</script>

假如你试图弄清它的递归过程,也许会在return value;插一句console.log(value),于是会得到以下结果

1
1
2
1
3
1
1
2
5

还是不太明白,对么?因为这串数字不符合我们的理解顺序。我们知道5是3+2得到的,而3和2的位置在上面这串数字中很奇怪。
但其实我们把fib函数稍加改进,情况就会好很多了。

<script>
        var fib=function (n,level) {
            console.log(level+'计算开始'+n);
            var value= n<=2?1:fib(n-1,level+'--')+fib(n-2,level+'--');
            console.log(level+'fib('+n+')='+value);
            console.log(level+'计算完成'+n);
            return value;
        };
        fib(5,'');
</script>
计算开始5
--计算开始4
----计算开始3
------计算开始2
------fib(2)=1
------计算完成2
------计算开始1
------fib(1)=1
------计算完成1
----fib(3)=2
----计算完成3
----计算开始2
----fib(2)=1
----计算完成2
--fib(4)=3
--计算完成4
--计算开始3
----计算开始2
----fib(2)=1
----计算完成2
----计算开始1
----fib(1)=1
----计算完成1
--fib(3)=2
--计算完成3
fib(5)=5
计算完成5

注意到,我们标识出每次计算的开始和结束,加以缩进,就能比较容易的看出:fib(5)=fib(4)+fib(3),fib(3)=fib(2)+fib(1),原来fib(n)函数总是返回fib(n-1)+fib(n-2);

这里其实就是记录了函数的运行时(runtime)、上下文(context)和调用层级(stack)。而且对于此类debug场景,单步断点的效果并不如打表法好,工具需要在合适的使用场景才能发挥全力。

更好的测试用例

在正式测试之前,程序员往往会对每个模块进行简单的测试,如何设计出能测试出bug的测试用例呢?
1.输入逼近临界点。数据边界往往是最容易出错的地方,比如数组元素的第一个和最后一个,空数组和满数组,循环结构的第一次和最后一次等。
2.覆盖尽可能多的条件分支。
例如下面的代码,你一定要用IE和非IE浏览器都测试一遍。

if(isIE){
    alert('这是IE浏览器,小心!')
}else{
    alert('这不是IE浏览器,安心~')
}

3.奇怪而苛刻的数据。如很大的数字,带小数点的数字,带特殊符号的字符串,重复的id字段等等。