JavaScript 中的隐秘问题:那些容易踩的坑
这一篇收集 JavaScript 中那些容易让人困惑、出错的地方:
从类型转换的陷阱,到作用域和闭包的坑,再到原型链、this 绑定等让人头疼的问题。
类型转换的陷阱
== vs ===
== 会进行类型转换,=== 不会。但 == 的转换规则有时候很反直觉:
1// 这些都为 true
20 == false; // true
3'' == false; // true
4[] == false; // true
5[0] == false; // true
6null == undefined; // true
7
8// 但这些都是 false
90 === false; // false
10'' === false; // false
11[] === false; // false
12[0] === false; // false
13null === undefined; // false
14
15// 更奇怪的
16[] == ![]; // true
17[] == []; // false
18{} == {}; // false
建议:总是使用 === 和 !==,除非你真的需要类型转换。
数组和数字的转换
1[] + []; // ""
2[] + {}; // "[object Object]"
3{} + []; // 0 (在浏览器控制台)
4{} + {}; // "[object Object][object Object]"
5
6// 数组转数字
7Number([]); // 0
8Number([1]); // 1
9Number([1, 2]); // NaN
10
11// 数组转字符串
12String([]); // ""
13String([1]); // "1"
14String([1, 2]); // "1,2"
布尔值的转换
1// 这些都为 false(falsy 值)
2Boolean(0);
3Boolean('');
4Boolean(null);
5Boolean(undefined);
6Boolean(NaN);
7Boolean(false);
8
9// 其他所有值都是 true
10Boolean('0'); // true
11Boolean('false'); // true
12Boolean([]); // true
13Boolean({}); // true
parseInt 的陷阱
1parseInt('08'); // 8 (ES5+)
2parseInt('08', 10); // 8 (明确指定基数)
3
4// 但要注意
5parseInt('0x10'); // 16 (十六进制)
6parseInt('010'); // 10 (ES5+),但在旧浏览器可能是 8
7
8// 更奇怪的
9parseInt('123abc'); // 123 (会忽略非数字字符)
10parseInt('abc123'); // NaN
作用域和变量提升
var 的作用域问题
1for (var i = 0; i < 3; i++) {
2 setTimeout(() => {
3 console.log(i); // 输出 3, 3, 3
4 }, 100);
5}
6
7// 因为 var 是函数作用域,不是块作用域
8// 等价于
9var i;
10for (i = 0; i < 3; i++) {
11 setTimeout(() => {
12 console.log(i); // 所有闭包共享同一个 i
13 }, 100);
14}
15
16// 解决方案:使用 let
17for (let i = 0; i < 3; i++) {
18 setTimeout(() => {
19 console.log(i); // 输出 0, 1, 2
20 }, 100);
21}
变量提升(Hoisting)
1console.log(x); // undefined,不是 ReferenceError
2var x = 5;
3
4// 等价于
5var x;
6console.log(x);
7x = 5;
8
9// 但 let 和 const 有暂时性死区(TDZ)
10console.log(y); // ReferenceError: Cannot access 'y' before initialization
11let y = 5;
函数提升
1foo(); // "Hello"
2
3function foo() {
4 console.log('Hello');
5}
6
7// 但函数表达式不会提升
8bar(); // TypeError: bar is not a function
9var bar = function() {
10 console.log('World');
11};
this 绑定的陷阱
普通函数中的 this
1const obj = {
2 name: 'John',
3 greet: function() {
4 console.log(this.name);
5 }
6};
7
8obj.greet(); // "John"
9
10const greet = obj.greet;
11greet(); // undefined (在严格模式下) 或 window.name (非严格模式)
12
13// 解决方案:使用箭头函数或 bind
14const obj2 = {
15 name: 'John',
16 greet: () => {
17 console.log(this.name); // this 指向外层作用域
18 }
19};
20
21const obj3 = {
22 name: 'John',
23 greet: function() {
24 console.log(this.name);
25 }.bind(this)
26};
回调函数中的 this
1class Button {
2 constructor() {
3 this.text = 'Click me';
4 }
5
6 click() {
7 console.log(this.text);
8 }
9
10 setup() {
11 // 错误:this 丢失
12 document.addEventListener('click', this.click);
13
14 // 解决方案1:使用箭头函数
15 document.addEventListener('click', () => this.click());
16
17 // 解决方案2:使用 bind
18 document.addEventListener('click', this.click.bind(this));
19
20 // 解决方案3:在类中定义箭头函数
21 // click = () => { console.log(this.text); }
22 }
23}
箭头函数的 this
1const obj = {
2 name: 'John',
3 regular: function() {
4 console.log(this.name); // "John"
5
6 const inner = function() {
7 console.log(this.name); // undefined
8 };
9 inner();
10
11 const arrow = () => {
12 console.log(this.name); // "John" (继承外层 this)
13 };
14 arrow();
15 }
16};
数组的奇怪行为
稀疏数组
1const arr = [];
2arr[5] = 'five';
3console.log(arr.length); // 6
4console.log(arr); // [empty × 5, "five"]
5console.log(arr[0]); // undefined
6
7// 但这样遍历会有问题
8arr.forEach(item => {
9 console.log(item); // 只输出 "five"
10});
11
12// 使用 for...of 也会跳过空位
13for (const item of arr) {
14 console.log(item); // 只输出 "five"
15}
16
17// 使用传统 for 循环会输出 undefined
18for (let i = 0; i < arr.length; i++) {
19 console.log(arr[i]); // undefined, undefined, ..., "five"
20}
数组的 length 属性
1const arr = [1, 2, 3];
2arr.length = 5;
3console.log(arr); // [1, 2, 3, empty × 2]
4
5arr.length = 2;
6console.log(arr); // [1, 2] (元素被删除)
7
8arr.length = 0;
9console.log(arr); // [] (清空数组)
数组方法返回新数组还是修改原数组
1const arr = [1, 2, 3];
2
3// 返回新数组(不修改原数组)
4arr.map(x => x * 2); // [2, 4, 6]
5arr.filter(x => x > 1); // [2, 3]
6arr.slice(1); // [2, 3]
7arr.concat([4, 5]); // [1, 2, 3, 4, 5]
8
9// 修改原数组(返回修改后的数组)
10arr.push(4); // 返回 4 (新长度)
11arr.pop(); // 返回 4 (被删除的元素)
12arr.reverse(); // [3, 2, 1] (原数组被修改)
13arr.sort(); // [1, 2, 3] (原数组被修改)
数组的 sort 方法
1// sort 默认按字符串排序
2[10, 2, 1].sort(); // [1, 10, 2] (不是 [1, 2, 10]!)
3
4// 需要提供比较函数
5[10, 2, 1].sort((a, b) => a - b); // [1, 2, 10]
对象的陷阱
对象属性的访问
1const obj = {
2 '123': 'number key',
3 'true': 'boolean key',
4 null: 'null key',
5 undefined: 'undefined key'
6};
7
8// 这些键都会被转换为字符串
9console.log(obj[123]); // "number key"
10console.log(obj[true]); // "boolean key"
11console.log(obj[null]); // "null key"
12console.log(obj[undefined]); // "undefined key"
对象属性的枚举
1const obj = {
2 a: 1,
3 b: 2
4};
5
6Object.defineProperty(obj, 'c', {
7 value: 3,
8 enumerable: false
9});
10
11console.log(obj.c); // 3
12console.log(Object.keys(obj)); // ['a', 'b'] (不包含 c)
13
14// for...in 也会跳过不可枚举属性
15for (const key in obj) {
16 console.log(key); // 'a', 'b'
17}
对象比较
1{} == {}; // false
2{} === {}; // false
3
4// 对象比较的是引用,不是值
5const obj1 = { a: 1 };
6const obj2 = { a: 1 };
7const obj3 = obj1;
8
9console.log(obj1 === obj2); // false
10console.log(obj1 === obj3); // true
浮点数精度问题
10.1 + 0.2; // 0.30000000000000004
20.1 + 0.2 === 0.3; // false
3
4// 解决方案:使用小数位数比较
5function isEqual(a, b, epsilon = 0.0001) {
6 return Math.abs(a - b) < epsilon;
7}
8
9isEqual(0.1 + 0.2, 0.3); // true
10
11// 或者使用整数运算
12(0.1 * 10 + 0.2 * 10) / 10 === 0.3; // true
原型链的陷阱
原型污染
1const obj = {};
2obj.__proto__.polluted = 'yes';
3
4const obj2 = {};
5console.log(obj2.polluted); // "yes" (所有对象都被污染了)
6
7// 更安全的做法
8Object.setPrototypeOf(obj, { polluted: 'yes' });
9// 或者
10Object.create({ polluted: 'yes' });
原型链查找的性能
1// 在原型链上查找属性比直接属性慢
2function createObject() {
3 return { a: 1, b: 2, c: 3 };
4}
5
6function createObjectWithProto() {
7 const proto = { a: 1, b: 2, c: 3 };
8 return Object.create(proto);
9}
10
11// createObject 的性能通常更好
hasOwnProperty 的问题
1const obj = Object.create(null);
2obj.a = 1;
3
4// 错误:obj 没有 hasOwnProperty 方法
5obj.hasOwnProperty('a'); // TypeError
6
7// 解决方案
8Object.prototype.hasOwnProperty.call(obj, 'a'); // true
9// 或者
10Object.hasOwn(obj, 'a'); // true (ES2022)
异步相关的陷阱
循环中的异步
1// 错误:所有请求几乎同时完成,i 已经是 3
2for (var i = 0; i < 3; i++) {
3 setTimeout(() => {
4 console.log(i); // 输出 3, 3, 3
5 }, 100);
6}
7
8// 解决方案1:使用 let
9for (let i = 0; i < 3; i++) {
10 setTimeout(() => {
11 console.log(i); // 输出 0, 1, 2
12 }, 100);
13}
14
15// 解决方案2:使用闭包
16for (var i = 0; i < 3; i++) {
17 (function(j) {
18 setTimeout(() => {
19 console.log(j); // 输出 0, 1, 2
20 }, 100);
21 })(i);
22}
Promise 的错误处理
1// 错误:未捕获的错误
2Promise.resolve()
3 .then(() => {
4 throw new Error('Oops');
5 })
6 .then(() => {
7 console.log('不会执行');
8 });
9// UnhandledPromiseRejectionWarning
10
11// 正确:总是处理错误
12Promise.resolve()
13 .then(() => {
14 throw new Error('Oops');
15 })
16 .catch(error => {
17 console.error(error);
18 });
async/await 的错误处理
1// 错误:未捕获的错误
2async function fetchData() {
3 const response = await fetch('/api/data');
4 return response.json();
5}
6
7fetchData(); // 如果出错,会变成未处理的 Promise rejection
8
9// 正确:使用 try/catch 或 .catch()
10async function fetchData() {
11 try {
12 const response = await fetch('/api/data');
13 return response.json();
14 } catch (error) {
15 console.error(error);
16 throw error;
17 }
18}
19
20// 或者
21fetchData().catch(error => {
22 console.error(error);
23});
其他常见的坑
typeof 的陷阱
1typeof null; // "object" (历史遗留问题)
2typeof []; // "object"
3typeof new Date(); // "object"
4typeof /regex/; // "object"
5
6// 更准确的类型检查
7Object.prototype.toString.call(null); // "[object Null]"
8Object.prototype.toString.call([]); // "[object Array]"
9Object.prototype.toString.call(new Date()); // "[object Date]"
NaN 的问题
1NaN === NaN; // false
2NaN == NaN; // false
3
4// 检查 NaN
5Number.isNaN(NaN); // true
6Number.isNaN('abc'); // false
7
8// 注意 isNaN 和 Number.isNaN 的区别
9isNaN('abc'); // true (会先转换类型)
10Number.isNaN('abc'); // false (不会转换类型)
字符串和数字的运算
1'5' + 3; // "53" (字符串拼接)
2'5' - 3; // 2 (数字运算)
3'5' * 3; // 15 (数字运算)
4'5' / 3; // 1.666... (数字运算)
5
6// 但要注意
7+'5' + 3; // 8 (一元 + 转换为数字)
函数参数的默认值
1function foo(a = 1, b = a) {
2 return [a, b];
3}
4
5foo(); // [1, 1]
6foo(2); // [2, 2]
7foo(2, 3); // [2, 3]
8
9// 但要注意作用域
10function bar(a = b, b = 2) {
11 return [a, b];
12}
13
14bar(); // ReferenceError: Cannot access 'b' before initialization
解构赋值的陷阱
1// 解构的默认值只在 undefined 时生效
2function foo({ a = 1, b = 2 } = {}) {
3 return [a, b];
4}
5
6foo(); // [1, 2]
7foo({}); // [1, 2]
8foo({ a: 3 }); // [3, 2]
9foo({ a: 3, b: null }); // [3, null] (null 不是 undefined)
JSON.stringify 的陷阱
1JSON.stringify({ a: undefined, b: function() {}, c: Symbol('foo') });
2// "{}" (undefined、函数、Symbol 会被忽略)
3
4JSON.stringify({ a: NaN, b: Infinity, c: -Infinity });
5// "{"a":null,"b":null,"c":null}" (NaN 和 Infinity 会变成 null)
6
7// 自定义序列化
8JSON.stringify({ a: undefined, b: function() {} }, (key, value) => {
9 if (typeof value === 'function') {
10 return value.toString();
11 }
12 return value;
13});
正则表达式的全局标志
1const regex = /test/g;
2regex.test('test'); // true
3regex.test('test'); // false (lastIndex 已经移动)
4
5// 解决方案:每次创建新正则或重置 lastIndex
6/test/g.test('test'); // true
7/test/g.test('test'); // true
8
9// 或者
10regex.lastIndex = 0;
11regex.test('test'); // true
如何避免这些问题
1. 使用严格模式
1'use strict';
2
3// 严格模式下的一些行为更安全
4function foo() {
5 this.a = 1; // 在严格模式下,this 是 undefined,不会意外创建全局变量
6}
2. 使用现代语法
- 使用
let和const代替var - 使用箭头函数处理
this绑定 - 使用
===和!==代替==和!= - 使用模板字符串代替字符串拼接
3. 使用工具
- ESLint:检测潜在问题
- TypeScript:提供类型检查
- Prettier:统一代码格式
4. 理解 JavaScript 的特性
- 理解原型链、闭包、作用域
- 理解异步编程模型
- 理解类型转换规则
小结
JavaScript 中有很多容易踩的坑:
- 类型转换:
==的隐式转换、数组和对象的转换规则 - 作用域:
var的函数作用域、变量提升、暂时性死区 - this 绑定:普通函数和箭头函数的区别、回调中的 this 丢失
- 数组和对象:稀疏数组、length 属性、原型链
- 浮点数精度:0.1 + 0.2 不等于 0.3
- 异步编程:循环中的异步、错误处理
- 其他陷阱:
typeof null、NaN比较、JSON 序列化等
理解这些问题,可以帮助你写出更健壮的代码,避免在生产环境中遇到这些隐秘的 bug。