广州总部电话:020-85564311
广州总部电话:020-85564311
20年
互联网应用服务商
请输入搜索关键词
知识库 知识库

优网知识库

探索行业前沿,共享知识宝库

代码能跑,不代表它就是对的:5 个潜伏在常规下的 JavaScript Bug

发布日期:2025-09-01 15:36:25 浏览次数: 824 来源:OTT前端技术
推荐语
JavaScript开发者必看!揭秘5个隐藏在日常代码中的致命陷阱,让你的程序更健壮可靠。

核心内容:
1. 隐式陷阱:async/await未被捕获的Promise.reject风险
2. 类型转换:==与===的隐藏差异带来的问题
3. 作用域问题:闭包与变量提升的常见误区
小优 网站建设顾问
专业来源于二十年的积累,用心让我们做到更好!

代码能跑,不代表它就是对的:5 个潜伏在常规下的 JavaScript Bug

JavaScript 的动态性和复杂性意味着,即便代码在表面上能正常工作,其深层也可能隐藏着一些不易察的陷阱。这些陷阱往往出乎我们的意料。本文梳理了几个在 JavaScript 开发中难以被发现的隐藏 Bug,希望能帮助我们编写出更健壮、更可预测的代码。

让我们一起来看看那些潜伏在代码中的“魔鬼细节”。

1. 隐式的陷阱:被遗忘的 async/await 和 try...catch

async/await 极大地提升了异步代码的可读性,但它也引入了一个隐藏的风险:未被捕获的 Promise.reject 会变成一个静默的、未处理的异常。

错误场景:

async function fetchData({
// 如果 API 返回 404 或 500 错误,这个 Promise 将会是 rejected 状态。
const data = await fetch("https://api.example.com/data");
// 下面这行代码将永远不会执行
console.log("数据处理完成", data);
}

// 调用了函数,但没有处理潜在的错误。
fetchData();
// 程序会继续执行,但前面发生的错误就像被“吞掉”了一样。
console.log("程序继续执行");

问题根源await 仅仅是一个语法糖,它会暂停 async 函数的执行,等待 Promise 被解决(resolved)。如果这个 Promise 变成了 rejected 状态,await 会将其作为异常抛出。如果没有 try...catch 块来捕获这个异常,它就会沿着调用栈向上传播,最终成为一个 unhandledrejection(未处理的拒绝)。

正确姿势:

始终使用 try...catch 来包裹 await 表达式,或在调用链的更高层级进行捕获。

async function fetchData({
try {
    // 等待 fetch 请求完成
    const response = await fetch("https://api.example.com/data");

    // 检查 HTTP 响应状态是否 ok
    if (!response.ok) {
      // 如果不 ok,手动抛出一个错误,这样可以被 catch 捕获
      thrownewError(`HTTP 错误! 状态: ${response.status}`);
    }

    // 解析 JSON 数据
    const data = await response.json();
    console.log("数据处理完成", data);
  } catch (error) {
    // 如果 try 块中的任何 await 失败或抛出错误,都将在这里捕获
    console.error("获取数据失败:", error);
  }
}

fetchData();

2. “一荣俱荣,一损俱损”的 Promise.all

当需要并行处理多个 Promise 时,Promise.all 是首选。但即便是专家,有时也会忘记它的“快速失败”(fail-fast)特性。

错误场景:

假设我们需要获取用户详情和他的帖子列表,并且希望即便帖子列表获取失败,我们依然能看到用户详情。

async function getUserProfile(userId{
try {
    // Promise.all 等待两个 Promise 都完成
    const [user, posts] = awaitPromise.all([
      api.fetchUser(userId), // 这个成功了
      api.fetchPosts(userId), // 但这个因为网络问题失败了
    ]);
    // 由于一个失败,整个 Promise.all 都会失败,这里不会执行
    renderProfile(user, posts);
  } catch (error) {
    // 整个代码块都失败了,我们连用户信息都拿不到。
    console.error("获取用户资料失败", error);
  }
}

问题根源Promise.all 的设计是,只要其中任何一个 Promise 变成 rejected,整个 Promise.all 就会立即以该失败原因为由 reject,而不会等待其他的 Promise 完成。

正确姿势:

使用 Promise.allSettled。它会等待所有的 Promise 都有一个结果(无论是 fulfilled 还是 rejected),然后返回一个包含每个 Promise 状态和结果(或原因)的对象数组。

async function getUserProfile(userId{
// 使用 allSettled,它会等待所有 promise 完成,无论成功还是失败
const results = awaitPromise.allSettled([api.fetchUser(userId), api.fetchPosts(userId)]);

// 从结果数组中分别获取用户和帖子的结果
const userResult = results[0];
const postsResult = results[1];

// 检查获取用户的结果状态
if (userResult.status === "fulfilled") {
    // 即便帖子获取失败,我们依然可以渲染用户信息
    renderUser(userResult.value);
  } else {
    // 如果获取用户失败,记录错误
    console.error("获取用户失败:", userResult.reason);
  }

// 检查获取帖子的结果状态
if (postsResult.status === "fulfilled") {
    // 如果成功,渲染帖子
    renderPosts(postsResult.value);
  } else {
    // 如果失败,记录错误
    console.error("获取帖子失败:", postsResult.reason);
  }
}

3. 数组迭代中的意外突变

在 forEach 或 for...of 循环中直接修改(增加或删除)数组本身,是导致不可预测行为的常见原因。

错误场景:

从数组中移除所有偶数。

let numbers = [123456];

numbers.forEach((num, index) => {
  if (num % 2 === 0) {
    // 灾难开始了!在迭代中修改数组
    // 当 index=1 (值为2) 被删除时,原数组变为 [1, 3, 4, 5, 6]。
    // 下一次迭代,index 变为 2,访问的是新数组的第3个元素 (值为4)。
    // 元素 3 被跳过了!
    numbers.splice(index, 1);
  }
});

// 输出结果是 [1, 3, 5, 6] <--- 6 被跳过了!
console.log(numbers);

问题根源:当你使用 splice 删除一个元素时,数组的长度和后续元素的索引都发生了改变。然而,forEach 的迭代过程并不会根据这种变化来调整其内部计数器,这导致它跳过了被移除元素紧随其后的那个元素。

正确姿势:

不要在迭代中修改原数组。最佳实践是创建一个新数组。

let numbers = [123456];

// 使用 filter,这是函数式编程的最佳实践,它返回一个新数组,不修改原数组
const oddNumbers = numbers.filter((num) => num % 2 !== 0);

// 输出: [1, 3, 5]
console.log(oddNumbers);

如果确实需要原地修改,请使用反向循环

let numbers = [123456];
// 从后向前遍历,即使删除了元素,也不会影响尚未遍历的元素的索引
for (let i = numbers.length - 1; i >= 0; i--) {
  if (numbers[i] % 2 === 0) {
    numbers.splice(i, 1);
  }
}
// 输出: [1, 3, 5]
console.log(numbers);

4. 闭包的内存陷阱与内存泄漏

闭包是 JavaScript 的一个强大特性,但也是内存泄漏的主要来源之一,尤其是在处理 DOM 事件监听时。

错误场景:

function setupHeavyObjectListener({
// 假设这是一个包含大量数据的“重”对象
const heavyObject = {
    /* 一个包含大量数据的对象 */
  };
const element = document.getElementById("my-element");

  element.addEventListener("click", () => {
    // 这个匿名函数形成了一个闭包,持有了对 heavyObject 的引用
    console.log("Clicked!", heavyObject.someProperty);
  });

// 假设 element 稍后从 DOM 中被移除了
// element = null;
}

setupHeavyObjectListener();

问题根源:即使 element 从 DOM 中被移除,只要事件监听器没有被显式地移除(使用 removeEventListener),这个监听器(闭包)就依然存在,并且它会永远引用着 heavyObject。这导致 heavyObject 和 element 都无法被垃圾回收器回收。

正确姿势:

在组件卸载或元素销毁时,总是清理事件监听器。

function setupHeavyObjectListener({
const heavyObject = {
    /* ... */
  };
const element = document.getElementById("my-element");

// 将监听器函数单独定义,以便之后可以引用它来移除监听
const handleClick = () => {
    console.log("Clicked!", heavyObject.someProperty);
  };

  element.addEventListener("click", handleClick);

// 返回一个清理函数,这是“钩子”或组件生命周期中常用的模式
return() => {
    element.removeEventListener("click", handleClick);
    // 确认监听器已被清理
    console.log("监听器已清理!");
    // 此后,handleClick 闭包和它引用的 heavyObject 就可以被垃圾回收了
  };
}

const cleanup = setupHeavyObjectListener();

// 在未来的某个时刻,例如组件卸载时调用
// cleanup();

5. 对象深浅拷贝之谜

这是一个永恒的话题。即使是经验丰富的开发者,也可能在不经意间对嵌套对象进行了浅拷贝,从而导致意料之外的副作用。

错误场景:

const userProfile = {
name"Alex",
settings: {
    theme"dark",
    notificationstrue,
  },
};

// 使用展开语法来“拷贝”
const updatedProfile = { ...userProfile };

// 修改新对象的嵌套属性
updatedProfile.settings.theme = "light";

// 原来的对象也被修改了!
// 输出: 'light'
console.log(userProfile.settings.theme);

问题根源:展开语法 (...) 和 Object.assign() 都只执行浅拷贝。它们会创建一个新的顶层对象,但如果属性值是对象或数组,它们只会复制引用,而不是值本身。

正确姿势:

对于深层嵌套的对象,需要使用深拷贝

简单场景(对象不包含 functionundefinedSymbol 等):可以使用 structuredClone(推荐的现代方法) 或 JSON.parse(JSON.stringify())

// 使用 structuredClone 进行标准化的深拷贝
const deepCopiedProfile = structuredClone(userProfile);

// 或者使用 JSON 魔术方法,但要注意其局限性
// const deepCopiedProfile = JSON.parse(JSON.stringify(userProfile));

// 修改深拷贝后的对象
deepCopiedProfile.settings.theme = "light";

// 原对象不会受到影响
// 输出: 'dark'
console.log(userProfile.settings.theme);

复杂场景:使用成熟的库,如 Lodash 的 _.cloneDeep()


JavaScript 是一门看似简单却充满细节的语言,当我们开始对这些“小问题”变得敏感时,代码质量和开发效率必将迈上一个新的台阶。



优网科技,优秀企业首选的互联网供应服务商

优网科技秉承"专业团队、品质服务" 的经营理念,诚信务实的服务了近万家客户,成为众多世界500强、集团和上市公司的长期合作伙伴!

优网科技成立于2001年,擅长网站建设、网站与各类业务系统深度整合,致力于提供完善的企业互联网解决方案。优网科技提供PC端网站建设(品牌展示型、官方门户型、营销商务型、电子商务型、信息门户型、微信小程序定制开发、移动端应用(手机站APP开发)、微信定制开发(微信官网、微信商城、企业微信)等一系列互联网应用服务。


我要投稿

姓名

文章链接

提交即表示你已阅读并同意《个人信息保护声明》

专属顾问 专属顾问
扫码咨询您的优网专属顾问!
专属顾问
马上咨询
联系专属顾问
联系专属顾问
联系专属顾问
扫一扫马上咨询
扫一扫马上咨询

扫一扫马上咨询

和我们在线交谈!