编码与调试
编码与调试
你应该测试你的代码
所有代码都需要测试,没有测试的代码毫无价值
js里如何测试你的代码——直接在console里调用:
solution_02(1,2,3,4)
10
直接跑一下就可以。 以上面的测试为例,我们输入是参数(1,2,3,4),你预计的输出10。 一组输入和输出称为一个用例。
如何你得到的输出结果和用例的预期输出不同,就说明你代码有问题了。前提当然你用例正确。 测试用例是我们测试代码的重要方式,你们的OJ和我自动判分的方式,都使用这种方式来执行。
测试用例之所以如此重要,在于一点:跑测试用例会让代码跑起来。 注意我们的原则:任何代码,不跑就什么都不是 跑用例是最贴近代码实际运行的测试方式,因此他不可替代。 那么要怎么写测试用例,才能“又快又好”呢?
设计你的用例
基本原则
我们跑用例的目的是保证代码能真正跑起来以验证逻辑的正确性。编写用例的原则也是一样:尽可能跑遍你的代码。
这个原则是很容易做到的,因为你知道自己代码写成啥样,以上次的练习4为例:
let onClickGetFactors = function(){
let tag = document.getElementById("num");
let num = parseInt(tag.value);
let output = document.getElementById("output");
if((num<1) || (num>81)){
output.innerHTML = "输入错误,n的范围必须在[1,81]之间";
return;
}
let ret = solution_03(num);
if( ret == null ){
output.innerHTML = "无法拆分";
}
else{
output.innerHTML = num.toString()+"="+ret[0].toString()+"x"+ret[1].toString();
}
}
这是我的解法。很明显,这个函数要完成3个功能:
- 当num超出范围时,输出提示信息,告知范围不对
- 根据solution_03的返回值,返回null时,告知无法拆分。
- solution_03返回有效数值时,写入解法。
这意味着,我们至少应该测对应的三种情况,因此我们这样构造:
- 90
- 11
- 12
这样就保证三种情况都测到,才能保证你所有代码都能跑到,我们把这称为“测试覆盖率”。最好的测试覆盖率当然是100%,但有些时候,这做不到,那就应该尽量提高。
更全面覆盖
平时经常做测试的童鞋可能已经注意到了,第一种状况里有两个条件:(n<1)||(n>81))
,而我们的用例90,只针对(n>81)
这种情况。(n<1)
并没有测试到。 因此针对第一种情况,我们还应该加上(n<1)
的情况,比如测0、-1,这样测试才全面。
因为你测了90,并不能保证你这个组合逻辑的前一个逻辑是正确的,例如有人干脆就忘了写,那么你用90测试,就没法把这种错误找出来。
这是第二条原则:所有条件的组合,都要测试到。 这样我们针对三种情况,就分别有了4个用例:
- 90、-1
- 11
- 12
我们进一步考虑,假如用户不小心将(n>81)
写成(n>=81)
。那么这种错误用上面的4个用例也是找不出来的。
边界条件有比较大的概率出问题,应该为边界增加测试用例。
本例中,我们有两个边界:1和81,那么我们可以这样测试:针对边界1,我们测试0、1、2这三个数,针对81,我们测80,81、82这三个数字。也就是分别是边界外,边界内和边界上。这样我们用例的纠错程度进一步提高了。
总结一下
对于初学者,请记住这三个原则:
- 测试要跑遍所有代码,否则测试就是不完整的。
- 组合条件要单独测,保证每个条件都覆盖到。
- 边界条件要增加用例保证边界内外都测试到。
经过这些测试,我们不能说代码一定不存在问题,但我们可以说,我们大幅度降低了代码出问题的概率。
当然不管测试有多少条原则,如果你不做就什么都没得说了,不要想当然的认为代码就不会有问题。没有人可以保证写代码一定不出bug。我们往大了说,测试自己编写的代码是身为程序员一项重要的职业道德。
要怎么调你的代码?
写代码不能保证不出问题,出了问题,就要调试,那么应该如何调试你的代码呢?
原则一:代码量越少,定位和调试越容易
代码越少,越容易定位问题,等你写了一堆代码之后再发现问题,你面对几百行代码可能会无从下手。因此最好的做法是,写一点,测一下再写一点再测一下。 以上面的例子,你写的时候,可以这样写:
let onClickGetFactors = function(){
let tag = document.getElementById("num");
let num = parseInt(tag.value);
console.log(num);
}
我们要做的第一步,是从输入框里获取数字,以方便后面代入函数计算。如果你没把握,特别是对新手,功能划分越小你越省力。于是我们写下这三行代码,第三行代码console.log(num)
并不是真正有用的代码,但我们必须有输出来判断是否正确。不要小看这三行代码,你有一千种可能把这三行代码写错。比如很多人在第一行上会用错函数,又或者很多人会把词拼错。
写完之后,就要测试和调试,测试一个代码你需要在让他跑起来,调代码你也需要触发代码执行。我们这个函数可以由页面上的按钮直接触发。但对于那些不能由页面UI触发的函数来说,JavaScript也有很方便的方法:直接用浏览器的console来跑。
F12打开调试界面,直接在console里输入函数名调用就行了:
onClickGetFators()
注意你要正确的调用函数,必须带括号和参数(如果有的话)。
这样代码就可以直接跑起来了。
我要再强调一次:代码不跑就没有有任何意义。
原则二:先分析再定位、先定位再修改,不要猜
新手常常代码运行一出现错误,就随便找个地方改改,期望随手改一个代码就搞定。这是很不好的习惯:
- 瞎猫碰到死耗子的可能有多大?大概率不可能改对。
- 有可能你这修改只不过把你测试的用例搞对了,其他用例一样错。
- 即便行为对了,你也不知道为什么错,你收获不到经验
遇到错误了,请你:努力凭自己的能力找到问题并修改>>让别人帮你看分析问题在哪里并修改>>让别人告诉你要怎么改>>随便改
这几个方法对你的帮助,依次递减。 所以作为新手,请你:确保你知道自己每个错误的原因。
既然修改错误不能靠瞎蒙,定位问题也一样不能靠瞎蒙。 很多时候,你需要一些辅助手段来帮助你定位错误。请看后面的例子。
原则三:修改之后,该测试的还要测试
改完之后很重要的一步是,当然是测试一下能不能跑,但这个测试不要马虎,即便简单测试能过,也要尽量将之前的测试都过一下。这称为“回归测试”,这是为了避免:
- 你以为改对,其实改错。
- 你可能改对,但引入新错。
回归测试在测试中占有很重要的位置,请不要遗漏。
例子
我们看一个例子:
<input type="number" id="input"><button onclick="onClick01()">提交</button>
<p id="output"></p>
let onClick01 = function(){
let num = document.getElementByTagName("input").vaule;
document.getElementById("output").innerHTML = num.toString();
}
你跑程序,一点反应都没有。现在先别瞎猜:
第一步:收集现场信息
打开F12看控制台console的输出:

请先把英语练熟。
Uncaught TypeError说明这是一个未被捕获和处理的类型错误。后面是具体的错误信息,他告诉你document.getElementByTagName不是一个函数。
最后的index.js:2告诉你这个错误在index.js的第二行,你点击可以在调试器里查看到代码的位置。

最下方的两行是调用栈,你可以从里面看出函数是如何调用的。越下面,调用层次越高,也就是越远离现场。越上方,调用层次越低,也就是越接近现场。也就是说,这个调用关系是这样: index.html第12行HTMLButtonElement.onclick函数被首先调用。
在onclick函数里调用了onClick01,index.js第2行出现了问题。
这些都是非常有用的信息,跟警察破案,医生诊断一样,都要先收集足够的信息才能做出判断。
第二步:定位问题原因
注意,推测和定位原因是所有步骤中最具创造力和艺术性的步骤,很多时候是需要灵光一闪的——这点仅限对老手而言。对于新手,没什么好说的,所有灵感都扎根于经验,先老老实实积累你的经验。
如果你第一次遇到这样的问题,而且代码经验不足,你可能无法一下就推测出问题的原因,你有下面的几个办法:
- 百度或者其他搜索引擎。请注意选择合适的内容,比如这个例子,你搜索第一行就可以得到不错的结果StackOverFlow。这个页面的内容是最精确的。但你也有可能搜不到,此时你就要懂得策略,比如这里的document.getElementByTagName是一个比较具体的内容,你可以尝试去掉,只搜索Uncaught TypeError: is not a function
- 对照已有的,正确的代码。这里你不要太过于相信自己的眼睛,要尽量用工具来对:比如你可以将正确的代码和错误的代码放到同一个文本编辑器里,放在上下两行,一眼就能看出你写错函数了。
- 另一种比较常用的方式是,把正确的代码copy过来,在别人的代码上改,如果你运行后发现该错误消除了,那么一般就是你笔误了。你可以用ctrl+z返回一下,复制出来再用工具对比。
- 猜,没错猜着试一下。你把这行代码直接在console里输出打一下:注意手动输入,因为console窗口有函数提示功能,可以避免笔误。如果功能正常,还是一样,复制出来,用工具对比。
如果一切顺利,此时你应该已经知道问题的原因了,你写错了函数名,应该是getElemenetsByTagName。笔误是初学者非常常见的错误。
第三步:修改重新运行
修改之后,继续测试,还是不行,有些童鞋到这里就不行了,好一点的会觉得,我是不是改错了?不好一点的,会觉得你浏览器是不是针对我啊?或者,我电脑是不是坏掉了啊? 拜托:对菜鸟来说,只有你错,程序和电脑不会错!请相信你的电脑。
到这里,先不要急,我们还是按部就班来:
继续打开F12:

首先我们发现出现的错误变了,连行号也变了,说明我们前一个问题解决了。
我们做的修改并不是没有改进,至少向前进了一步。
看错误信息:“无法获取undefined的toString属性”。
这个做何解呢?
我们看代码:
document.getElementById("output").innerHTML = num.toString();
这行出的问题,说无法获得undefined的toString属性,意思就是num是undefined,所以你无法获得num的.toString函数来执行。 验证一下你的观点,很简单,只要在执行前我们打印一下num就可以了:
console.log(num);
document.getElementById("output").innerHTML = num.toString();
我们看,输出了undefined。
这就是我们前面说的必要的辅助手段。把你怀疑的问题打印出来,有助于你定位问题。
当然你也可以调试,断在这行之前,看num的取值:

右侧Watch里可以看到num:undefined。
注意代码执行的位置,也就是图中蓝色背景行的位置。如果你当前代码处在第2行,就不正确了。因为处于某一行时,表明下一步会运行该行,换言之,本行尚未执行。那么num=undefined就是合理的。但当代码运行到第三行,说明第二行执行的结果就不符合预期。
对于菜鸟,不要怕麻烦,多打印输出,多调试。你所有付出的东西都会得到回报。
第四步:拆分逻辑
我们可以断定问题一定还在第一行,但什么原因导致我们还需要推测。
我们先观察这行代码:
let num = document.getElementsByTagName("input").vaule;
我们先调用document.getElementsByTagName("input")
来获得input对象,然后用.value
来获得其属性。这在逻辑上没有问题的。
但这段代码就是有问题,要如何定位?
我们可以看到这行代码运行了两个功能,必然是这两个问题中的一个,或者两个都有问题。这个逻辑有点复杂了,我们不好下手。这跟前面第一个原则是相通的:代码(逻辑)越复杂,错误越难定位。
一行代码如果包含比较复杂的逻辑,会不利于调试和定位问题,初学者不要学。代码除了正确、好读之外,可维护性也是很重要的因素。对初学者,初次编码时,我会建议把可维护性放在正确性前面。
你可能会觉得正确性排后面难以接受,事实上,你只要做好可维护性(可调试性),早晚代码都可以调到正确。反之则不然。
先让代码好调,调正确之后,再好好改成好读的
回到这个例子上,我们需要将该行拆开,触类旁通之下,下一行也应该拆分:
let onClick01 = function(){
let tag_input = document.getElementsByTagName("input");
console.log(tag_input);
let num = tag_input.vaule;
console.log(num);
let tag_output = document.getElementById("output")
console.log(tag_output)
tag_output.innerHTML = num.toString();
}
顺便加上console.log来辅助打印诊断信息。
第五步:继续定位
继续跑:

有问题已在我们预料之中。不过我们获得了3个输出。把输出和代码对照起来分析:
let onClick01 = function(){
let tag_input = document.getElementsByTagName("input");
console.log(tag_input); // 第一个输出
let num = tag_input.vaule;
console.log(num); // 第二个输出
let tag_output = document.getElementById("output")
console.log(tag_output) // 第三个输出
tag_output.innerHTML = num.toString();
}
因此:
- 第一个输出的是
tag_input
,他等于HTMLCollection[input#input, input:input#input]
- 第二个输出的是
num
,他等于undefined
(意料之中) - 第三个输出是
tag_output
,他等于<p id="output"></p>
发现什么问题没有?
tag_input
和tag_output
是同一种东西,都是某个页面元素,但他们的值却是不一样的!
所以,收集尽量多的数据、认真观察和分析才是定位错误的必由之路。此外,注意对比相同和类似代码的执行结果,会非常有助于定位问题。
从出问题的位置,以及输出来看,我们基本可以断定是tag_input
的问题。那么是什么问题?又回到似曾相识的场景了:1百度 2对照 3copy 4尝试console调用。
我直接说答案,document.getElementsByTagName
这个函数,返回的是一个数组(其实并不是,是一个HTMLCollection
对象,但你可以把它当数组用)。因为他是个s,因为这几个带s的函数都会返回复数个对象,所以必须返回数组。只有ById函数,因为id是唯一的(这已经是第二次说了)。
事实上如果你不是第一次遇到相同的问题,当你看到HTMLCollection
时就应该立刻意识到问题的原因了。任何精巧的定位步骤和手段,在你犯错后积累的经验面前都一钱不值。
到此为止了
可能的话,请你把本次的所有内容手做一遍。 事实上到目前为止,依然还有问题没有找到。 你可以再尝试找一下。