Skip to content

JavaScript 常见踩坑

JavaScript 开发中高频遇到的类型、异步和逻辑错误。


预防措施

  1. 使用严格模式:在文件顶部添加 'use strict',捕获更多潜在错误
  2. 启用 ESLint:配置规则禁止常见错误(如 no-implicit-globalseqeqeq
  3. 使用 TypeScript:类型检查可在编译时发现大部分类型错误
  4. 避免使用 var:统一使用 letconst,避免变量提升和作用域问题
  5. 异步操作必须处理错误:所有 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 个

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准