能運行,不代表它是對的:五個潛伏在正常功能下的 JavaScript 錯誤
JavaScript 的動態(tài)性和復(fù)雜性意味著,代碼雖然表面上正常運行,但一些深層次、隱蔽的陷阱往往讓人意想不到,梳理了幾個 JavaScript 開發(fā)中難以發(fā)現(xiàn)的隱蔽錯誤,旨在幫助我們寫出更健壯、更可預(yù)測的代碼。
來看下那些潛伏在代碼中的“惡魔細節(jié)”。

1. async/await 的隱式陷阱:忘記 try...catch
async/await 極大地改善了異步代碼的可讀性,但它也帶來了一個隱蔽的風險:未被捕獲的 Promise reject 會變成一個靜默的、未處理的異常。
錯誤場景:
async function fetchData() {
// 如果 API 返回 404 或 500,這個 Promise 會被 reject
const data = await fetch('https://api.example.com/data');
console.log('數(shù)據(jù)處理完成', data); // 這一行永遠不會執(zhí)行
}
// 調(diào)用函數(shù),但沒有處理潛在的錯誤
fetchData();
console.log('程序繼續(xù)執(zhí)行'); // 程序會繼續(xù),但錯誤被“吞掉”了問題根源:await 只是一個語法糖,它會暫停 async 函數(shù)的執(zhí)行,等待 Promise 解決。如果 Promise被 reject,await 會將其作為異常拋出。如果沒有 try...catch 塊來捕獲這個異常,它就會沿著調(diào)用棧向上傳播,最終成為一個 unhandledrejection。
正確姿勢:始終用 try...catch 包裹 await 表達式,或者在調(diào)用鏈的更高層級進行捕獲。
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('數(shù)據(jù)處理完成', data);
} catch (error) {
console.error('獲取數(shù)據(jù)失敗:', error);
}
}
fetchData();2. Promise.all 的“一榮俱榮,一損俱損”
當需要并行處理多個 Promise 時,Promise.all 是首選。但高手有時會忘記它的“快速失敗”(Fail-Fast)特性。
錯誤場景:假設(shè)我們需要獲取用戶詳情和他的帖子列表,即使獲取帖子失敗,也希望看到用戶詳情。
async function getUserProfile(userId) {
try {
const [user, posts] = await Promise.all([
api.fetchUser(userId), // 這個成功了
api.fetchPosts(userId) // 但這個因為網(wǎng)絡(luò)問題失敗了
]);
renderProfile(user, posts);
} catch (error) {
// 整個 block 都失敗了,我們連 user 數(shù)據(jù)都拿不到
console.error('獲取用戶 Profile 失敗', error);
}
}問題根源:Promise.all 的設(shè)計是,只要其中任何一個 Promise被 reject,整個 Promise.all 就會立即 reject,并返回那個失敗的原因,而不會等待其他 Promise 完成。
正確姿勢:使用 Promise.allSettled。它會等待所有 Promise 都有結(jié)果(無論是 fulfilled 還是 rejected),然后返回一個包含每個 Promise 狀態(tài)和結(jié)果(或原因)的對象數(shù)組。
async function getUserProfile(userId) {
const results = await Promise.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. 數(shù)組迭代中的意外突變
在 forEach 或 for...of 循環(huán)中直接修改(增加或刪除)數(shù)組本身,是導(dǎo)致不可預(yù)測行為的常見原因。
錯誤場景:從數(shù)組中移除所有偶數(shù)。

問題根源:當你使用 splice 刪除一個元素時,數(shù)組的長度和后續(xù)元素的索引都會發(fā)生變化。但 forEach 的迭代過程并不會根據(jù)這個變化來調(diào)整它的內(nèi)部計數(shù)器,導(dǎo)致它跳過緊跟在被刪除元素后面的那個元素。
正確姿勢:不要在迭代中修改原數(shù)組。最好的方法是創(chuàng)建一個新數(shù)組。

如果確實需要在原地修改,請使用反向循環(huán)。

4. 閉包的記憶陷阱與內(nèi)存泄漏
閉包是 JavaScript 的強大特性,但也是內(nèi)存泄漏的主要來源之一,尤其是在處理 DOM 事件監(jiān)聽時。
錯誤場景:

問題根源:即使 element 從 DOM 中移除,只要事件監(jiān)聽器沒有被顯式地移除(removeEventListener),這個監(jiān)聽器(閉包)就依然存在,并且它會一直引用著 heavyObject。這導(dǎo)致 heavyObject 和 element 都無法被垃圾回收器回收。
正確姿勢:在組件卸載或元素銷毀時,務(wù)必清理事件監(jiān)聽器。

5. 對象的深拷貝與淺拷貝之謎
這是一個永恒的話題。即使是資深開發(fā)者,也可能在不經(jīng)意間對嵌套對象進行了淺拷貝,導(dǎo)致了意想不到的副作用。
錯誤場景:

問題根源:Object.assign() 和展開語法 ... 都只執(zhí)行淺拷貝。它們會創(chuàng)建一個新的頂層對象,但如果屬性值是對象或數(shù)組,它們只會復(fù)制引用,而不是值。
正確姿勢:對于深層嵌套的對象,需要使用深拷貝,關(guān)于深拷貝可參考前文《一行代碼實現(xiàn)深拷貝?別再用 JSON.stringify 了!》。
簡單場景 (無函數(shù)、undefined、Symbol等):const deepCopiedProfile = structuredClone(userProfile); // JSON.parse(JSON.stringify(userProfile));復(fù)雜場景: 使用成熟的庫,如 Lodash 的 _.cloneDeep()。
JavaScript 是一門看似簡單實則充滿細節(jié)的語言,當我們開始對這些“小問題”變得敏感時,代碼質(zhì)量和開發(fā)效率必將邁上一個新的臺階。
























