B 币安 · 全球最大的加密货币交易所币安 注册 → AD OKX OKX · 全球领先的加密货币交易所OKX 注册 → AD
💻 编程基础 · 第8讲 / 10

调试与读懂错误——把红字变成线索

错误信息用“名称: 说明”和行号告诉你哪里出了什么问题。把信息读完,把值打印出来确认,再把假设一个个排除,大多数 bug 会比想象中更快被抓到。

⏱ 约22分钟 ✍️ 练习题 5 道 🧪 代码练习 4 题 更新 2026-10-09
🎯 本讲学习目标
  • 能从错误信息中找出错误名称、说明和行号,并说出发生了什么
  • 能区分 SyntaxError、ReferenceError、TypeError、RangeError 的常见原因
  • 能按“用 console.log 跟踪值、提出假设、验证”的顺序找出 bug
  • 能识别不报错但结果错误的 bug 和停不下来的循环,并做出最小复现向 AI 提问

1.错误信息是线索

刚开始写代码时,经常会冒出红色的错误。专业开发者也一样。区别在于:看到错误是慌了神,还是把它当线索来读。找出并修正错误的工作叫调试,第一步就是把信息从头到尾、一个字一个字地读完。信息里通常直接写着要修改的地方的名字。

JavaScript 的错误信息是 错误名称: 说明 的形式。例如 TypeError: Cannot read properties of undefined (reading 'length') 的意思是:“类型是 TypeError(值的类型不对),内容是想从 undefined 上读取 length”。如果是运行中发生的错误,这个练习场还会同时显示出错的是第几行。那一行就是出发点。

有两点需要知道。第一,说明文字因浏览器而略有不同。本讲中的信息是谷歌浏览器等所用的 V8 引擎的措辞,在其他浏览器里,同样的错误可能换一种说法。错误名称(TypeError 等)由语言规则规定,在大多数浏览器里都一样。第二,出错的那一行和犯错的那一行可能不是同一行。值在上面就被做错了,到下面使用它时才炸开,这种情况很常见。

一旦出错,执行就停在那一行。所以错误前面的 console.log 会输出,后面的不会。这个特点能告诉你“程序正常运行到了哪里”。下面的代码打印出第一行之后,在第3行停下。

在第3行停下——words[2] 是不存在的格子,所以是 undefined
const 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 list
price 紧前面,也就是上一行末尾漏了逗号
const item = {
  name: "苹果"
  price: 1500
};
输出
SyntaxError: Unexpected identifier 'price'
同一个名字声明了两次
let count = 0;
let count = 1;
输出
SyntaxError: Identifier 'count' has already been declared
用 JSON.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 defined
在声明之前使用
console.log(price);
const price = 1500;
输出
ReferenceError: Cannot access 'price' before initialization
在块外面看不见
for (let i = 0; i < 3; i++) {
  const last = i;
}
console.log(last);
输出
ReferenceError: last is not defined

4.`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 function
字符串没有 toFixed
const price = "1500";
console.log(typeof price);
console.log(price.toFixed(0));
输出
string
TypeError: price.toFixed is not a function
最常遇到的四种错误(信息为 V8 引擎的措辞)
错误名称含义常见信息先看哪里
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: -1
不报错却得到 NaN——差一错误
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. 第1步 写下预期和实际。预期:20,实际:NaN。没有错误信息,所以是只有结果出错的 bug。
  2. 第2步 假设:“加进去的值里混进了不是数字的东西”。在循环里打印 i 和 sales[i]。
  3. 第3步 看输出,i: 3 时值是 undefined。数组只有0到2号,循环却转到了3号。
  4. 第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 的是完整信息和短代码,并且不含机密信息

✍️ 练习题

请先自己作答,再展开“答案与解析”。

Q1. 在第一行加了 console.log("开始"),却什么都没输出,只出现了错误。最先应该怀疑哪种错误?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① SyntaxError

语法错误在运行之前、读取代码的阶段就被拦下,所以连第一行也不会执行。其他错误在出错那一行之前的部分都会执行。

Q2. TypeError: Cannot read properties of undefined (reading 'name') 的含义是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① .name 前面的值是 undefined

意思是想从 undefined 上读取 name。把点左边的值打印出来,再追溯它为什么成了 undefined。

Q3. 用 for (let i = 0; i <= arr.length; i++) 求 [10, 20, 30] 的和,会怎样?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① 不报错,得到 NaN

最后取出了不存在的格子 arr[3],加上了 undefined,而数字 + undefined 是 NaN。这是没有错误信息、只有结果出错的 bug。

Q4. 向 AI 询问错误时,最好的做法是?

⭕ 回答正确

❌ 再想一想

答案与解析
答案 ① 把完整的错误信息、出问题的短代码、预期和实际结果一起给它

信息原样给,代码缩减成最小复现再给。概括会丢失线索;机密信息和真实个人信息要删掉,换成假值。

Q5. 修一个停不下来的 while 循环时,要确认哪两点?

答案与解析
参考答案 确认条件里用到的变量在循环里是否真的在变化,以及这种变化是否朝着让条件变为假(结束)的方向。

例如 while (n > 0) 时,n 必须在循环里减小。这个练习场超过2秒就会停止运行,并提示超时。

🧪 代码练习

每段初始代码里都藏着一个 bug。先原样运行,读一读错误信息或错误的结果,找到原因后只改一处,让测试通过。

代码只在本浏览器内的隔离环境中运行,不会发送到服务器。无法访问网络,超过2秒会自动停止。改过的代码只保存在此浏览器中。按 Ctrl+Enter(⌘+Enter)运行,Tab 键输入两个空格(先按 Esc 再按 Tab 可跳到下一项)。

浏览器脚本未开启,无法运行代码,但题目、初始代码、自动检查项和参考答案都可以直接阅读。

1改正名字的拼写

函数 discount(price, rate) 接收价格 price 和折扣率 rate(例如 0.1 表示10%),返回打折后的价格。运行时会出现 ReferenceError。看着信息里出现的名字把它改好。

自动检查
  • discount(10000, 0.1)期望值 9000
  • discount(5000, 0.5)期望值 2500
  • discount(3000, 0)期望值 3000
  • discount(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])期望值 20
  • average([7])期望值 7
  • average([1,2])期望值 1.5
  • average([-5,5])期望值 0
  • average([])期望值 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 调试入门

达成全部学习目标后,请点击完成。

💻 编程基础

  1. 1什么是程序:把事情拆成顺序、条件和循环的思维
  2. 2值、变量与数据类型:给值贴上名字标签
  3. 3条件语句:根据情况走不同的路
  4. 4循环 — 让同一件事准确地重复多次
  5. 5函数 — 拆成一个个有名字的小机器
  6. 6数组与对象 — 装载和处理数据
  7. 7字符串与文本处理——截取、查找、替换文字
  8. 8调试与读懂错误——把红字变成线索
  9. 9简单的自动化示例——用代码做表格计算和文本整理
  10. 10阅读并检验 AI 写的代码——测试、安全、许可证与个人信息
📚 推荐阅读
📊Using AI for Spreadsheets, and Checking the Formulas→ 💻AI Coding Assistants: Security and Licence Risks→ 💬Practising Conversation in a Foreign Language with AI→ ⚙️Finding Work Tasks Worth Automating with AI→
← AI时代基础学力课程