1.错误信息是线索
刚开始写代码时,经常会冒出红色的错误。专业开发者也一样。区别在于:看到错误是慌了神,还是把它当线索来读。找出并修正错误的工作叫调试,第一步就是把信息从头到尾、一个字一个字地读完。信息里通常直接写着要修改的地方的名字。
JavaScript 的错误信息是 错误名称: 说明 的形式。例如 TypeError: Cannot read properties of undefined (reading 'length') 的意思是:“类型是 TypeError(值的类型不对),内容是想从 undefined 上读取 length”。如果是运行中发生的错误,这个练习场还会同时显示出错的是第几行。那一行就是出发点。
有两点需要知道。第一,说明文字因浏览器而略有不同。本讲中的信息是谷歌浏览器等所用的 V8 引擎的措辞,在其他浏览器里,同样的错误可能换一种说法。错误名称(TypeError 等)由语言规则规定,在大多数浏览器里都一样。第二,出错的那一行和犯错的那一行可能不是同一行。值在上面就被做错了,到下面使用它时才炸开,这种情况很常见。
一旦出错,执行就停在那一行。所以错误前面的 console.log 会输出,后面的不会。这个特点能告诉你“程序正常运行到了哪里”。下面的代码打印出第一行之后,在第3行停下。
words[2] 是不存在的格子,所以是 undefinedconst words = ["苹果", "梨"];
console.log(words[0].length);
console.log(words[2].length);
console.log("这里不会执行");2
TypeError: Cannot read properties of undefined (reading 'length')- 错误名称——是哪一类问题(
SyntaxError、ReferenceError、TypeError、RangeError……) - 说明——在做什么的时候卡住了(哪个名字、哪个属性)
- 行号——停在了哪里(犯错的地方可能在更上面)
2.`SyntaxError`——根本运行不起来的错误
SyntaxError 是语法错误。括号没闭合、漏了逗号、同一个名字声明了两次等,代码读不成 JavaScript 语句时就会出现。和其他错误不同,它在代码运行之前的读取阶段就被拦下,所以一行都不会执行。如果在最上面加了 console.log 也什么都没打印出来,就先怀疑语法错误。
这个练习场遇到语法错误时只显示信息,不显示行号,所以信息里的线索更加重要。missing ) after argument list 表示函数调用缺少右括号;Unexpected identifier 'price' 表示“price 出现在了不该出现的位置”,也就是它紧前面漏了逗号之类的东西。看到 Unexpected token 时,就检查那个符号紧前面的部分。如果信息里用引号标出了某个名字,用这个名字搜索就能很快找到位置。
语法错误大多出在配对不上的地方。检查每个左括号、左花括号、引号是否都有对应的另一半,把缩进对齐,配对错开的地方就会显露出来。编辑器高亮配对括号的功能也很有帮助。不要一口气写很长的代码,每写几行就运行一次,出现语法错误时只需要看刚写的那几行。
console.log("你好";SyntaxError: missing ) after argument listprice 紧前面,也就是上一行末尾漏了逗号const item = {
name: "苹果"
price: 1500
};SyntaxError: Unexpected identifier 'price'let count = 0;
let count = 1;SyntaxError: Identifier 'count' has already been declaredJSON.parse 读取 JSON 字符串时出现的错误,名称也是 SyntaxError。不过这不是代码的语法错了,而是“数据的语法”错了,而且发生在运行中,所以会显示行号。JSON 是不是坏了,贴进 JSON 格式化工具就能马上看出哪里不对。3.`ReferenceError`——没有这个名字
ReferenceError 是“找不到这个名字”的错误。最常见的原因是拼写错误。把 console 写成 Console,或者把 total 写成 totla,JavaScript 就会告诉你没有这个名字的变量。大小写不同也算不同的名字。把信息里 is not defined 前面的名字原样复制,到代码里搜索,拼写不同的地方很快就能看到。
第二个原因是“声明之前就使用”。用 let、const 创建的变量,如果在声明那一行之前使用,就会出现 Cannot access 'price' before initialization。第三个原因是作用域。第5讲讲过,在函数或 { } 块里创建的变量,在外面是看不见的。想在循环结束后使用循环内部创建的变量,就会出现 is not defined。
在像这个练习场一样以严格模式运行的代码里,不写 let 或 const 就给一个从未出现过的名字赋值,也会引发 ReferenceError。如果不是严格模式,这种失误会悄悄创建一个新的全局变量,变成更难查的 bug;严格模式则会直接把它作为错误报出来。
const total = 100;
console.log(totla);ReferenceError: totla is not definedconsole.log(price);
const price = 1500;ReferenceError: Cannot access 'price' before initializationfor (let i = 0; i < 3; i++) {
const last = i;
}
console.log(last);ReferenceError: last is not defined4.`TypeError`——这个值做不了这件事
TypeError 表示让一个值做了与它的类型不相符的事,是初学阶段遇到最多的错误。最典型的是 Cannot read properties of undefined (reading '…')。意思是 undefined 或 null 没有属性,却想用点(.)从它身上取东西。括号里的名字是想取的属性,而那个点紧左边的值就是 undefined。
关键在于“那个值为什么变成了 undefined”。JavaScript 取不存在的数组格子(arr[5])或不存在的属性(user.nmae)时并不报错,而是悄悄返回 undefined。直到再用一次这个 undefined,错误才会出现。所以要用 console.log 打印出错那一行点左边的值,再往上追溯它从哪里来。罪魁祸首通常是索引越界、属性名拼错,或者函数漏写 return 而返回了 undefined。
另一个常见的是 … is not a function,也就是用括号 () 调用了不是函数的东西。要么是方法名拼错(toUppercase),要么是对另一种类型的值调用了这个方法。例如 toFixed 是数字的方法,对字符串 "1500" 调用就会出这个错误。这和第2讲讲的类型转换问题是连在一起的。给用 const 创建的变量重新赋值时出现的 Assignment to constant variable. 也属于 TypeError。
undefined——错误在下一步才出现const user = { name: "小明" };
console.log(user.nmae);
console.log(user.nmae.length);undefined
TypeError: Cannot read properties of undefined (reading 'length')const s = "hello";
console.log(s.toUppercase());TypeError: s.toUppercase is not a functiontoFixedconst price = "1500";
console.log(typeof price);
console.log(price.toFixed(0));string
TypeError: price.toFixed is not a function| 错误名称 | 含义 | 常见信息 | 先看哪里 |
|---|---|---|---|
SyntaxError | 读不成语句 | missing ) after argument list | 括号、逗号、引号的配对 |
ReferenceError | 没有这个名字 | x is not defined | 拼写、大小写、声明位置、作用域 |
TypeError | 这个值做不了的事 | Cannot read properties of undefined | 点左边的值从哪里来 |
RangeError | 值超出了允许范围 | Maximum call stack size exceeded | 负数、过大的数、停不下来的递归 |
5.`RangeError`,以及不报错的 bug
RangeError 表示值的类型没错,但范围不对。像 "*".repeat(-1) 那样给负数次数,或者像 new Array(-1) 那样给负数长度,就会出现。函数没完没了地调用自己时会出现 Maximum call stack size exceeded,在谷歌浏览器等使用的 V8 引擎里这也是 RangeError(火狐浏览器对同样的情况报 InternalError: too much recursion)。函数调用自身的递归一定要有停止条件。
更棘手的是不报错的 bug:代码顺利跑完了,只是结果不对。第4讲讲过的差一错误(off-by-one)就是典型。用 i <= arr.length 循环,最后会取出不存在的格子,加上 undefined;数字加 undefined 不会报错,而是得到 NaN。除以 0 得到 Infinity,0 / 0 得到 NaN,都不会报错。结果里出现 NaN、undefined、Infinity 时,即使没有错误信息,也是 bug 的信号。
停不下来的循环也不会给出错误信息。如果 while 的条件永远为真,程序就会一直转下去。这个练习场在运行超过2秒还没结束时会强制停止,并提示超时。看到这个提示,就怀疑循环的结束条件。看条件里用到的变量在循环里是否真的在变,是否朝着接近结束的方向变化。在真实的浏览器标签页里运行死循环可能会让标签页卡住,所以第一次写循环时,先用次数很小的输入来测试。
function stars(n) {
return "*".repeat(n);
}
console.log(stars(3));
console.log(stars(-1));***
RangeError: Invalid count value: -1NaN——差一错误const nums = [10, 20, 30];
let sum = 0;
for (let i = 0; i <= nums.length; i++) {
sum += nums[i];
}
console.log(sum);
console.log(10 / 0, 0 / 0);NaN Infinity NaN
6.调试的顺序——打印、假设、缩小
调试有固定的顺序。先各用一句话写下“预期结果”和“实际结果”。接着对原因提出一个假设(例如:循环多转了一次),再做一个能验证它的最小实验。假设对了就修改,错了就划掉,换下一个假设。一次改好几处的话,就不知道到底是哪一处修好的,所以每次只改一处。
最顺手的实验工具是 console.log。把可疑位置的值打印出来,就能看出“脑子里的值”和“实际的值”从哪里开始分岔。在循环里把编号和值一起打印,再给值加上标签(console.log("i:", i)),输出混在一起也不会搞混。同时打印 typeof,就能区分 "1500"(字符串)和 1500(数字)这种看起来一样的值。找到问题后,把这些确认用的输出删掉。
代码很长时,就做一个最小复现:在问题仍然出现的前提下,把无关的行一行行删掉,留下能引发问题的最短代码。在缩减的过程中常常就看出了原因;向别人或 AI 提问时,短代码也能换来准确得多的回答。有时在讲解的过程中答案自己就冒出来了,所以把代码一行行大声讲出来的方法(小黄鸭调试法)也很流行。
NaN。该怎样找原因?const sales = [10, 20, 30];
let sum = 0;
for (let i = 0; i <= sales.length; i++) {
console.log("i:", i, "值:", sales[i]);
sum += sales[i];
}
console.log(sum / sales.length);i: 0 值: 10 i: 1 值: 20 i: 2 值: 30 i: 3 值: undefined NaN
- 第1步 写下预期和实际。预期:20,实际:
NaN。没有错误信息,所以是只有结果出错的 bug。 - 第2步 假设:“加进去的值里混进了不是数字的东西”。在循环里打印
i和sales[i]。 - 第3步 看输出,
i: 3时值是undefined。数组只有0到2号,循环却转到了3号。 - 第4步 把条件
i <= sales.length改成i < sales.length,再用空数组、只有一个元素的数组测试。
undefined。改成 < 就会得到20。7.向 AI 询问错误时要提供什么
AI 编程助手在解释错误信息、给出修改方向方面相当有用。不过它只能根据收到的信息给出相应准确的回答。与其说“不行,帮我改一下”,不如把完整的错误信息、出问题的最小代码、预期结果和实际结果、运行环境(浏览器、引擎)以及已经试过的办法一起给它,得到的回答会准确得多。错误信息不要概括,原样复制粘贴。一翻译或删减,重要的线索(名字、属性、行号)就没了。
粘贴之前也有必须过滤掉的东西。密码、API 密钥、访问令牌,以及真实客户的姓名、电话号码等个人信息,即使混在代码或数据里,也要删掉并换成假值。如果是公司或学校的代码,先确认那里的规定是否允许贴到外部 AI。第10讲会更详细地讲这一部分。
拿到 AI 修改过的代码,不要照单全收,要自己确认。亲自运行看看:原来的错误是否消失了,是否得到了预期结果,在空数组、0 这样的边界值上是否也正确。AI 有时会给出听起来合理却是错的解释,也会用只把错误藏起来的方式(例如把有问题的值一律换成0)来“修好”。请它解释为什么这样改,再判断这个解释和错误信息对不对得上。
- 完整的错误信息(名称、说明、行号)——原样复制
- 能重现问题的最短代码(最小复现)
- 预期结果和实际结果
- 运行环境(哪种浏览器、是否在这个练习场等)以及已经尝试过的办法
- 去掉密码、密钥和真实个人信息,换成假值
📌 要点总结
- 错误信息按“名称: 说明”和行号来读——说明文字因浏览器略有不同,但名称一样
SyntaxError在运行前就被拦下,一行都不执行;ReferenceError是名字问题,TypeError是值的类型问题,RangeError是范围问题undefined悄悄产生、过后才炸开——要追溯出错行里点左边的值是从哪里来的NaN、Infinity、停不下来的循环都是没有错误信息的 bug 信号——练习场超过2秒就会停止运行- 写下预期与实际 → 提一个假设 → 用
console.log验证 → 最小复现;给 AI 的是完整信息和短代码,并且不含机密信息
🧪 代码练习
每段初始代码里都藏着一个 bug。先原样运行,读一读错误信息或错误的结果,找到原因后只改一处,让测试通过。
代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过2秒会自动停止。改过的代码只保存在此浏览器中。按 Ctrl+Enter(⌘+Enter)运行,Tab 键输入两个空格(先按 Esc 再按 Tab 可跳到下一项)。
浏览器脚本未开启,无法运行代码,但题目、初始代码、自动检查项和参考答案都可以直接阅读。
1改正名字的拼写
函数 discount(price, rate) 接收价格 price 和折扣率 rate(例如 0.1 表示10%),返回打折后的价格。运行时会出现 ReferenceError。看着信息里出现的名字把它改好。
discount(10000, 0.1)期望值9000discount(5000, 0.5)期望值2500discount(3000, 0)期望值3000discount(0, 0.2)期望值0
💡 提示
prise is not defined——找到这个名字在哪里,再和参数名比较一下。
查看参考答案
function discount(price, rate) {
const off = price * rate;
return price - off;
}2修正不存在的方法
函数 shout(s) 把字符串 s 全部转成大写,再在末尾加上 "!" 后返回。运行时出现 TypeError: s.toUppercase is not a function。找到原因并改好。空字符串时返回 "!"。
shout("hi")期望值"HI!"shout("Hello World")期望值"HELLO WORLD!"shout("甲乙丙")期望值"甲乙丙!"shout("")期望值"!"
💡 提示
JavaScript 的名字区分大小写。把第7讲用过的方法名逐个字母对比一下。
查看参考答案
function shout(s) {
return s.toUpperCase() + "!";
}3修正得到 NaN 的平均值
函数 average(nums) 返回数字数组 nums 的平均值,空数组时返回 0。它不报错,但结果是 NaN。在循环里打印 i 和 nums[i],找出原因。
average([10,20,30])期望值20average([7])期望值7average([1,2])期望值1.5average([-5,5])期望值0average([])期望值0
💡 提示
在循环里加上 console.log("i:", i, nums[i]); 试试。最后打印出来的值是什么?
查看参考答案
function average(nums) {
if (nums.length === 0) return 0;
let sum = 0;
for (let i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum / nums.length;
}4修正停不下来的循环
函数 countDown(n) 接收正整数 n,返回从 n 倒数到1的数组。例:countDown(3) → [3, 2, 1],countDown(0) → []。初始代码停不下来,练习场会在2秒后停止运行。检查循环的结束条件并改好。
countDown(3)期望值[3,2,1]countDown(1)期望值[1]countDown(5)期望值[5,4,3,2,1]countDown(0)期望值[]
💡 提示
要让条件 n > 0 终有一天变成假,n 在循环里应该怎样变化?
查看参考答案
function countDown(n) {
const out = [];
while (n > 0) {
out.push(n);
n--;
}
return out;
}🤖 可以这样问 AI
复制后把 [ ] 部分换成你的情况。不要直接相信回答,请与正文对照核实。
看不懂错误信息时
我在[浏览器/练习场]里运行 JavaScript,出现了下面的错误:[原样粘贴完整的错误信息]。出问题的代码是:[最小代码]。请用通俗的话解释这个错误是什么意思,并按可能性从大到小告诉我,我的代码里哪一行的哪个值最可能是原因。先别直接给答案,先告诉我应该在哪里用 `console.log` 确认。
没有报错但结果不对时
下面这个函数运行时不报错,但结果不对。输入:[输入],预期结果:[预期],实际结果:[实际]。代码:[最小代码]。请提出2到3个原因假设,并告诉我验证每个假设应该把 `console.log` 放在哪里。给出修改后的代码时,请说明改了什么、为什么改,并附上边界值(空数组、0)的测试。
验证 AI 修改过的代码时
我想确认你改的代码是否真的解决了原来的问题。原来的错误:[信息],修改后的代码:[代码]。请逐行说明改动的理由,并自己检查一下这是不是只把错误藏起来的修改(例如把值一律换成0)。再举出3个可能让这段代码出错的输入。
🧰 相关工具
可以当场检查、整理本讲涉及的 JSON、正则表达式和文本的工具。请不要粘贴真实的个人信息或密码。
- MDN Web Docs — JavaScript 错误参考
- MDN Web Docs — SyntaxError、ReferenceError、TypeError、RangeError
- MDN Web Docs — 严格模式
- Chrome DevTools 文档 — JavaScript 调试入门
达成全部学习目标后,请点击完成。
此浏览器无法保存记录,仅在当前页面显示。