Skip to content

JavaScript 常见错误

本章重点介绍 JavaScript 开发者,特别是初学者可能遇到的常见陷阱和误解。了解这些问题可以节省大量的调试时间。

1. 使用赋值运算符 (=) 代替比较运算符 (== 或 ===)

Section titled “1. 使用赋值运算符 (=) 代替比较运算符 (== 或 ===)”

在 if 语句或循环中,意外地使用赋值运算符 (=) 代替比较运算符(== 用于宽松相等,=== 用于严格相等)是一个常见的错误。

赋值表达式 (x = value) 的结果是 value。如果 value 是真值 (truthy),if 块就会执行;如果是假值 (falsy),则不执行。

let x = 0;
// Mistake: Assignment instead of comparison
// 错误:使用了赋值运算符而不是比较运算符
if (x = 10) { // This assigns 10 to x, and 10 is truthy
// 这会将 10 赋值给 x,而 10 是真值
console.log("This will run, but x is now 10, not 0!");
// 这行会执行,但 x 现在是 10,而不是 0!
}
let y = 0;
// Correct: Comparison
// 正确:使用了比较运算符
if (y === 0) { // This correctly checks if y is equal to 0
// 这正确地检查了 y 是否等于 0
console.log("This will run because y is indeed 0.");
// 这行会执行,因为 y 确实是 0。
}
// Another mistake example
// 另一个错误示例
let z = 0;
if (z = 0) { // Assigns 0 to z; 0 is falsy
// 将 0 赋值给 z;0 是假值
console.log("This will NOT run.");
// 这行不会执行。
}

最佳实践:始终使用严格相等 (===) 或严格不等 (!==) 进行比较,以避免意外的类型转换 (type coercion) 问题(见下一点)。

2. 误解宽松相等 (==) 与严格相等 (===) 比较

Section titled “2. 误解宽松相等 (==) 与严格相等 (===) 比较”

JavaScript 有两组相等运算符:

  • ==(宽松相等)和 !=(宽松不等):如果操作数的类型不同,会执行类型转换后再进行比较。这可能导致不直观的结果。
  • ===(严格相等)和 !==(严格不等):比较值和类型,不执行类型转换。通常更安全,更推荐使用。

宽松比较的陷阱:

console.log(10 == "10"); // true (string "10" is coerced to number 10)
// true (字符串 "10" 被转换为数字 10)
console.log(0 == false); // true (false is coerced to 0)
// true (false 被转换为 0)
console.log('' == false); // true (empty string coerced to 0, false to 0)
// true (空字符串被转换为 0,false 也转换为 0)
console.log(null == undefined); // true (special case in loose equality)
// true (宽松相等中的特殊情况)

严格比较更具可预测性:

console.log(10 === "10"); // false (number vs. string)
// false (数字 vs. 字符串)
console.log(0 === false); // false (number vs. boolean)
// false (数字 vs. 布尔值)
console.log('' === false); // false (string vs. boolean)
// false (字符串 vs. 布尔值)
console.log(null === undefined); // false (different types)
// false (类型不同)

记住:switch 语句使用严格比较 (===) 来匹配其 case。

let val = 10;
switch (val) {
case "10": // This case will not match because val (number 10) !== "10" (string)
// 这个 case 不会匹配,因为 val (数字 10) !== "10" (字符串)
console.log("Matched string 10");
break;
case 10: // This case will match
// 这个 case 会匹配
console.log("Matched number 10");
break;
}

3. 混淆加法 (+) 与字符串拼接 (+)

Section titled “3. 混淆加法 (+) 与字符串拼接 (+)”

+ 运算符既用于数字相加,也用于字符串拼接。如果其中一个操作数是字符串,JavaScript 会将另一个操作数转换为字符串并执行拼接。

let num1 = 10;
let num2 = 5;
console.log(num1 + num2); // Output: 15 (numerical addition)
// 输出:15 (数字相加)
let str1 = "10";
let str2 = "5";
console.log(str1 + str2); // Output: "105" (string concatenation)
// 输出:"105" (字符串拼接)
console.log(num1 + str2); // Output: "105" (num1 is converted to string "10")
// 输出:"105" (num1 被转换为字符串 "10")

这在处理表单输入时尤为棘手,因为表单输入通常是字符串。如果需要进行算术运算,请使用 parseInt()、parseFloat() 或一元加号运算符 (+) 将字符串转换为数字。

let inputVal = "20";
// let sum = inputVal + 10; // "2010"
// let sum = inputVal + 10; // 结果是 "2010"
let sum = Number(inputVal) + 10; // 30 (or parseInt(inputVal) + 10)
// 结果是 30 (或使用 parseInt(inputVal) + 10)
console.log(sum);

JavaScript 数字存储为 64 位 IEEE 754 浮点数。这种格式可能导致十进制算术运算的精度问题,这是许多编程语言中的常见问题。

let x = 0.1;
let y = 0.2;
let z = x + y;
console.log(z); // Output might be 0.30000000000000004, not exactly 0.3
// 输出可能是 0.30000000000000004,而不是精确的 0.3
if (z === 0.3) { // This comparison might fail
// 这个比较可能失败
console.log("z is 0.3");
} else {
console.log("z is NOT exactly 0.3 due to precision: " + z);
// 由于精度问题,z 不是精确的 0.3:加上 z 的值
}

解决方案:

  • 对于比较,检查绝对差值是否在一个很小的容差范围内 (epsilon):Math.abs(z - 0.3) < 0.000001。
  • 对于显示,使用 toFixed():z.toFixed(2) 会得到 “0.30”。
  • 对于重要的财务计算,考虑使用处理任意精度十进制算术的库,或使用整数进行计算(例如,将货币存储为分)。

旧的写法(使用反斜杠 \ - 容易出错且过时):

// const oldMultiline = "This is a very long \
// string that continues on the next line."; // This works but is not recommended
// 这行代码可以工作,但不推荐使用

现代写法(模板字面量 - ES6+):

模板字面量(使用反引号 `)是创建多行字符串和嵌入表达式的最佳方式。

const modernMultiline = `This is a very long
string that naturally
continues on multiple lines.`;
console.log(modernMultiline);

6. 分号 misplaced (自动分号插入 - ASI)

Section titled “6. 分号 misplaced (自动分号插入 - ASI)”

JavaScript 使用自动分号插入 (Automatic Semicolon Insertion, ASI) 在省略但预期需要分号的地方自动插入分号。虽然方便,但如果理解不透彻,可能导致意外行为。

示例:if 条件后 misplaced 分号。

let condition = false;
if (condition); { // Semicolon here ends the if statement prematurely!
// 这里的分号提前结束了 if 语句!
// This block will ALWAYS execute, regardless of 'condition'.
// 无论 'condition' 是什么,这个块总会执行。
console.log("This block runs unexpectedly!");
// 这个块意外地运行了!
}

最佳实践:尽管在许多地方是可选的,但在语句末尾一致地使用分号可以防止与 ASI 相关的 bug 并提高代码清晰度。

如果 return 语句的返回值放在新的一行且没有紧跟 return 关键字,ASI 可能导致问题。

function getObject() {
return // ASI inserts a semicolon here!
// ASI 会在这里插入一个分号!
{
name: "Problematic Object"
};
}
console.log(getObject()); // Output: undefined, not the object
// 输出:undefined,而不是对象

由于 ASI 在 return 后插入了分号,函数实际上变成了 return;,因此返回了 undefined。下面的对象字面量作为返回值永远不会被访问到。

正确写法:

function getCorrectObject() {
return {
name: "Correct Object"
};
}
// Or: return (
// {
// name: "Correct Object"
// }
// );
// 或者:return (
// {
// name: "Correct Object"
// }
// );
console.log(getCorrectObject().name); // Output: Correct Object
// 输出:Correct Object

将返回值的起始大括号或圆括号放在与 return 同一行。

8. 使用命名索引(字符串键)访问数组

Section titled “8. 使用命名索引(字符串键)访问数组”

JavaScript 数组是使用数字索引(0、1、2 等)访问的。使用字符串键在数组上设置值不会向数组添加元素;它是在数组对象上设置属性。

let myArray = [];
myArray[0] = "Apple";
myArray["fruitName"] = "Banana"; // This sets a property, not an array element
// 这设置了一个属性,而不是数组元素
console.log(myArray.length); // Output: 1 (only "Apple" is an element)
// 输出:1 (只有 "Apple" 是一个元素)
console.log(myArray[0]); // Output: "Apple"
// 输出:"Apple"
console.log(myArray.fruitName); // Output: "Banana" (accessed as a property)
// 输出:"Banana" (作为属性访问)
console.log(myArray[1]); // Output: undefined
// 输出:undefined
// Array methods like forEach will ignore 'fruitName'
// 像 forEach 这样的数组方法会忽略 'fruitName'
myArray.forEach(item => console.log(item)); // Logs only "Apple"
// 只会打印 "Apple"

对于键值对,请使用普通对象 ({}) 或 Map 对象。

const myMap = new Map();
myMap.set("fruitName", "Cherry");
console.log(myMap.get("fruitName")); // Output: Cherry
// 输出:Cherry

9. 数组和对象字面量中的尾随逗号 (Trailing Commas)

Section titled “9. 数组和对象字面量中的尾随逗号 (Trailing Commas)”

历史上,尾随逗号(最后一个元素/属性后的逗号)在某些旧的 JavaScript 引擎(尤其是 IE)中存在问题。

// Older problematic example for some engines:
// 某些引擎中旧的、有问题的情况:
// const points = [40, 100, 1, ];
// const person = { firstName: "John", lastName: "Doe", age: 46, };

现代 JavaScript (ES5 及更高版本) 完全支持在数组和对象字面量中使用尾随逗号。现在它们被认为是好的实践,因为它们能使版本控制差异 (version control diffs) 更清晰(添加新项目时只改变一行)。

// Modern and good practice:
// 现代且好的实践:
const validPoints = [
40,
100,
1, // Trailing comma is fine
// 尾随逗号是允许的
];
const validPerson = {
firstName: "John",
lastName: "Doe",
age: 46, // Trailing comma is fine
// 尾随逗号是允许的
};

尾随逗号在 ES2017+ 中也允许用于函数参数列表和调用参数。

undefined 和 null 是 JavaScript 中两个不同的原始类型,尽管有时它们被交替使用或混淆。

  • undefined: 通常表示变量已声明但尚未赋值,或者对象属性/数组元素不存在。它是 JavaScript 未初始化变量的默认值。
  • null: 表示明确地表示没有对象值。它必须由程序员显式赋值。
let uninitializedVar;
console.log(uninitializedVar); // undefined
// console.log(未初始化的变量); // undefined
let emptyObject = null;
console.log(emptyObject); // null
// console.log(空对象); // null
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (this is a historical quirk of JavaScript)
// console.log(typeof null); // "object" (这是 JavaScript 的一个历史遗留问题)
console.log(null == undefined); // true (loose equality)
// console.log(null == undefined); // true (宽松相等)
console.log(null === undefined); // false (strict equality)
// console.log(null === undefined); // false (严格相等)

检查对象或变量是否未初始化或明确为 null 时,通常最好同时检查两者,或根据上下文具体判断:

let myObj = null; // or could be undefined
// let myObj = null; // 或者可能是 undefined
// To check if myObj has a value (is not null or undefined):
// 检查 myObj 是否有值(即不是 null 或 undefined):
if (myObj != null) { // Loose check for both null and undefined
// 对 null 和 undefined 的宽松检查
// ...
}
// To check specifically for null:
// 专门检查是否为 null:
if (myObj === null) {
// ...
}
// To check if an object property exists, use `in` operator or `hasOwnProperty` or check against `undefined`.
// 检查对象属性是否存在,使用 `in` 操作符、`hasOwnProperty` 或检查是否为 `undefined`。
const data = {};
if (data.property === undefined) { /* ... */ }

11. 误解变量作用域 (var vs. let/const)

Section titled “11. 误解变量作用域 (var vs. let/const)”

在 ES6 之前,var 是声明变量的唯一方式。var 具有函数作用域或全局作用域,而不是块级作用域。这意味着用 var 在块(例如 if 语句或 for 循环)内部声明的变量可以在该块外部访问,在其包含函数内部。

function scopeTestVar() {
if (true) {
var x = 10; // 'x' is function-scoped
// 'x' 具有函数作用域
}
console.log(x); // Output: 10 (x is accessible here)
// 输出:10 (这里可以访问 x)
for (var i = 0; i < 3; i++) {
// 'i' is function-scoped
// 'i' 具有函数作用域
}
console.log(i); // Output: 3 (i is accessible here and its value is 3 after loop)
// 输出:3 (这里可以访问 i,并且循环结束后它的值是 3)
return i;
}
scopeTestVar();

ES6 引入了 let 和 const,它们具有块级作用域 (block scope)。这意味着它们只能在其定义所在的块 ({...}) 内部访问。这通常更直观,有助于防止 bug。

function scopeTestLet() {
if (true) {
let y = 20; // 'y' is block-scoped
// 'y' 具有块级作用域
console.log(y); // Output: 20
// 输出:20
}
// console.log(y); // Error: y is not defined here
// console.log(y); // 错误:y 在这里未定义
for (let j = 0; j < 3; j++) {
// 'j' is block-scoped for each iteration
// 'j' 对于每次迭代都具有块级作用域
console.log(j); // 0, 1, 2
}
// console.log(j); // Error: j is not defined here
// console.log(j); // 错误:j 在这里未定义
// return j; // This would also be an error
// return j; // 这里也会报错
}
scopeTestLet();

最佳实践:在现代 JavaScript 中,优先使用 let 和 const 而不是 var,以利用块级作用域并避免常见的 var 相关问题,例如意外的变量提升 (hoisting) 或重复声明。