JavaScript 常见踩坑
JavaScript 开发中高频遇到的类型、异步和逻辑错误。
预防措施
- 使用严格模式:在文件顶部添加
'use strict',捕获更多潜在错误 - 启用 ESLint:配置规则禁止常见错误(如
no-implicit-globals、eqeqeq) - 使用 TypeScript:类型检查可在编译时发现大部分类型错误
- 避免使用 var:统一使用
let和const,避免变量提升和作用域问题 - 异步操作必须处理错误:所有 Promise 和 async 函数都要有 catch 处理
1. typeof null === 'object'
js
typeof null // 'object'(JS 历史遗留 bug,无法修复)
typeof undefined // 'undefined'
typeof [] // 'object'(数组也是对象)
typeof {} // 'object'
// ✅ 正确判空(使用严格相等 ===)
value === null // 判断是否为 null
value === undefined // 判断是否为 undefined
// ✅ 判断数组(typeof [] 会返回 'object',所以用 Array.isArray)
Array.isArray(value) // 返回 true/false
// ✅ 判断类型(最可靠的方式)
Object.prototype.toString.call(value) // 返回 '[object Array]'、'[object Date]' 等2. 浮点数精度问题
js
// 浮点数精度问题:JS 使用 IEEE 754 双精度浮点数,无法精确表示某些小数
0.1 + 0.2 === 0.3 // false(实际结果为 0.30000000000000004)
0.1 * 100 // 10.000000000000001
// ✅ 解决方案:用误差范围比较
// Number.EPSILON 是 ES6 特性,IE 不支持,需 polyfill
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true(Number.EPSILON 是最小精度)
// ✅ 金额计算用整数(避免浮点误差)
const price = 19.9 * 100 // 1990(分)
const total = price * 3 // 5970(分)
const display = total / 100 // 59.7(元)
// ✅ 用 toFixed 限制小数位(返回字符串,需注意精度)
(0.1 + 0.2).toFixed(2) // '0.30'(字符串!)
Number((0.1 + 0.2).toFixed(2)) // 0.3(转回数字)3. this 指向丢失
js
const obj = {
name: '张三',
greet() {
console.log(this.name)
}
}
// ❌ 方法赋值给变量后调用,this 丢失
const fn = obj.greet
fn() // 严格模式:undefined(this 是 undefined)
// 非严格模式:undefined(this 是 window,window.name 为 undefined)
// ❌ 箭头函数不适用于对象方法(this 继承外层作用域,不是对象本身)
const obj2 = {
name: '张三',
greet: () => {
console.log(this.name) // undefined(this 是外层作用域,不是 obj2)
}
}
// ✅ bind 绑定(创建一个 this 永久绑定到 obj 的新函数)
const fn2 = obj.greet.bind(obj)
fn2() // '张三'
// ✅ call / apply(立即执行,指定 this)
obj.greet.call(obj) // '张三'
obj.greet.apply(obj) // '张三'
// ✅ 箭头函数适合回调场景(继承外层 this,避免丢失)
const obj3 = {
name: '张三',
greet() {
setTimeout(() => {
console.log(this.name) // ✅ '张三'(箭头函数继承 greet 的 this)
}, 100)
}
}
// ✅ Vue 中不要用箭头函数定义 methods(this 指向问题)
export default {
methods: {
// ❌ handleClick: () => { this.xxx }
handleClick() { this.xxx } // ✅
}
}4. 闭包陷阱(循环中的异步)
什么是闭包(Closure)? 闭包是指函数可以"记住"并访问它被创建时所在的词法作用域,即使函数在该作用域之外执行。 在循环中,闭包会捕获变量的引用而非值,导致异步回调执行时读到的是同一个变量。
js
// ❌ var 没有块级作用域,循环结束后 i 变为 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 所有回调共享同一个 i
}
// 输出:3, 3, 3
// ✅ let 有块级作用域,每次循环创建新的 i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100) // 每次迭代有自己的 i
}
// 输出:0, 1, 2
// ✅ 立即执行函数(IIFE)创建独立作用域
for (var i = 0; i < 3; i++) {
((j) => { // j 是 i 的副本
setTimeout(() => console.log(j), 100)
})(i)
}5. async/await 错误处理
js
// ❌ 没有错误处理,请求失败会抛出未捕获的异常
async function fetchUser() {
const res = await fetch('/api/user')
return res.json()
}
// ✅ try/catch
async function fetchUser() {
try {
const res = await fetch('/api/user')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json()
} catch (err) {
console.error('请求失败:', err)
throw err
}
}
// ✅ 或用 .catch()
const [err, data] = await fetchUser().then(
d => [null, d],
e => [e, null]
)6. 深拷贝问题
js
// ❌ JSON.parse(JSON.stringify()) 有局限(以下注释为 JSON 序列化的行为)
const obj = {
date: new Date(), // 变成字符串
regex: /abc/gi, // 变成空对象 {}
fn: () => {}, // 丢失
undef: undefined, // 丢失
nan: NaN, // 变成 null(JSON 序列化行为)
infinity: Infinity, // 变成 null(JSON 序列化行为)
map: new Map(), // 变成空对象
set: new Set(), // 变成空对象
}
const copy = JSON.parse(JSON.stringify(obj)) // 数据丢失
// ✅ structuredClone(现代浏览器,保留 NaN/Infinity/Map/Set/Date/RegExp)
const copy2 = structuredClone(obj)
// ✅ lodash 深拷贝
import { cloneDeep } from 'lodash-es'
const copy3 = cloneDeep(obj)7. 对象比较(引用 vs 值)
js
// 对象比较的是引用
{} === {} // false
[] === [] // false
// ✅ 浅比较
JSON.stringify(a) === JSON.stringify(b) // 顺序必须一致
// ✅ 深比较用 lodash
import { isEqual } from 'lodash-es'
isEqual(a, b)8. parseInt 的坑
js
parseInt('010') // 旧浏览器按八进制解析得 8,现代浏览器按十进制得 10
parseInt('0xff') // 255(按十六进制解析)
// ✅ 始终指定进制
parseInt('010', 10) // 10
parseInt('0xff', 16) // 255
// ✅ 或用 Number()
Number('08') // 8
Number('0xff') // NaN
+'08' // 8(一元加号)9. for...in 遍历原型链
什么是原型链(Prototype Chain)? JavaScript 中每个对象都有一个内部链接指向另一个对象,称为它的原型。当访问对象的属性时,如果对象本身没有,JS 会沿着原型链向上查找,直到找到或到达 null。
js
const obj = { a: 1, b: 2 }
// ❌ for...in 会遍历原型链上的属性(包括继承的属性)
for (const key in obj) {
console.log(key) // 'a', 'b', 可能还有继承的属性
}
// ✅ 用 hasOwnProperty 过滤(只保留对象自身的属性)
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key) // 只输出 'a', 'b'
}
}
// ✅ 或用 Object.keys / Object.entries(只返回自身可枚举属性)
Object.keys(obj).forEach(key => console.log(key))
Object.entries(obj).forEach(([key, val]) => console.log(key, val))10. Promise 并发控制
什么是并发(Concurrency)? 并发是指多个异步任务在同一时间段内"同时"执行。在 JavaScript 中,由于单线程特性,实际上是通过事件循环在多个任务之间快速切换实现的"伪并发"。
什么是并发控制?为什么要控制并发数?
并发控制就是限制同时执行的异步任务数量。
为什么要控制?
- 批量上传图片:同时上传 100 张图片,浏览器会卡死,服务器也会限流
- 批量请求接口:同时发 100 个请求,浏览器并发连接数有限(通常 6-8 个),多余的会排队等待
- 爬虫/数据抓取:并发太高会被目标网站封 IP
不控制的后果:
- 浏览器卡死或崩溃
- 服务器返回 429(Too Many Requests)
- 网络拥塞,所有请求都变慢
js
// ❌ 同时发 100 个请求,可能卡死
const results = await Promise.all(
urls.map(url => fetch(url)) // 所有请求同时发出
)
// ✅ 限制并发数(简易版)
async function asyncPool(limit, items, fn) {
const results = [] // 存储所有结果
const executing = new Set() // 存储正在执行的 Promise
for (const [index, item] of items.entries()) {
const p = Promise.resolve().then(() => fn(item, index)) // 创建 Promise
results.push(p) // 将 Promise 加入结果数组
executing.add(p) // 将 Promise 加入执行池
const clean = () => executing.delete(p) // 完成后从执行池移除
p.then(clean, clean) // 无论成功失败都清理
if (executing.size >= limit) { // 如果达到并发上限
await Promise.race(executing) // 等待任意一个完成
}
}
return Promise.all(results) // 等待所有完成
}
// 使用:最多 3 个并发
const data = await asyncPool(3, urls, url => fetch(url).then(r => r.json()))11. 事件委托中获取目标元素
什么是事件委托?为什么要用事件委托?
事件委托就是把事件监听器绑定在父元素上,而不是每个子元素上。
为什么要用事件委托?
- 减少内存占用:100 个列表项只需要 1 个事件监听器,而不是 100 个
- 动态添加的子元素也能响应事件:新添加的列表项自动继承父元素的事件监听
- 代码更简洁:不需要遍历所有子元素绑定事件
什么时候用?
- 列表、表格等有大量子元素的场景
- 子元素会动态增删的场景
js
// ❌ e.target 可能是子元素
document.getElementById('list').addEventListener('click', (e) => {
console.log(e.target) // 可能是 <span>、<a> 等子元素
})
// ✅ 用 closest 查找匹配的祖先元素
document.getElementById('list').addEventListener('click', (e) => {
const item = e.target.closest('.list-item')
if (item) {
console.log(item.dataset.id)
}
})12. localStorage 的限制
js
// ❌ localStorage 不能存非字符串
localStorage.setItem('key', { a: 1 }) // 存入 '[object Object]'
localStorage.getItem('key') // '[object Object]'
// ✅ JSON 序列化
localStorage.setItem('key', JSON.stringify({ a: 1 }))
const data = JSON.parse(localStorage.getItem('key'))
// ⚠️ 容量限制约 5MB(各浏览器不同)
// ⚠️ 不能存特殊对象(Date、RegExp、函数等会丢失)真实场景
场景 1:循环中异步回调拿到错误的值
- 问题:点击列表项时,总是输出最后一项的索引
- 原因:使用
var声明循环变量,所有回调共享同一个变量引用 - 解决:改用
let声明循环变量,每次迭代创建新的作用域
场景 2:金额计算出现 0.000000001 误差
- 问题:购物车合计
0.1 + 0.2显示为0.30000000000000004 - 原因:JavaScript 浮点数精度问题,二进制无法精确表示某些十进制小数
- 解决:金额计算用整数(分),最后再转为小数(元)
场景 3:批量上传图片导致页面卡死
- 问题:用户选择 200 张图片同时上传,浏览器卡死
- 原因:未做并发控制,200 个请求同时发出
- 解决:使用并发控制函数,限制同时上传数量为 3-5 个