JavaScript 中的 Array 如果要走訪,傳入的方法是 Promise 方法,並且要在走訪完畢之後做操作的話,確實是會有問題的,但是如果只是走訪完畢而已,之後沒有要做任何處理,不應該出現任何問題
為了避免重複程式碼,先將共用的部分列在這裡:
// --- 共用環境設定 ---
// 模擬一個會隨機延遲 0.5 到 1.5 秒的非同步函數 (類似 API 請求或外部的資料處理)
const fakeApi = (num, _delay = null) => {
return new Promise(resolve => {
const delay = _delay ?? (Math.random() * 1000 + 500);
setTimeout(() => {
resolve(num * 10); // 假設 API 會把數字乘以 10 傳回來
}, delay);
});
};
// 每次測試前重置的原始資料
const getMockData = () => [
{ id: 1, number: 1 },
{ id: 2, number: 2 },
{ id: 3, number: 3 }
];
先來看看異常程式碼
async function testForEach() {
console.log("=== 測試開始:forEach ===");
const list = getMockData();
list.forEach(async (item, index) => {
console.log(`[forEach 迴圈] 正在觸發第 ${index + 1} 筆...`);
item.number = await fakeApi(item.number);
console.log(`✅ [背景任務完成] 第 ${index + 1} 筆更新完畢!目前值: ${item.number}`);
});
// 這一行會比上面的 ✅ 更早印出來!
console.log("🚨 [主執行緒] forEach 迴圈下方的程式碼被執行了!");
console.log("此時的 list 狀態 (還是舊的):", JSON.stringify(list));
}
// 執行測試
testForEach();
運行測試:https://jsbin.com/sinowiy/7/edit?html,console
可以看到由于 forEach 執行完畢時, list 並沒有更新完畢,因此在 forEach 執行完畢後要立刻取 list 做操作自然是抓到舊的值,所以如果要在走訪完畢時操作 list 陣列,那有兩種方法可以做處理:
用 for/while 的方式走訪:
async function testForOf() {
console.log("=== 測試開始:for...of ===");
const list = getMockData();
for (const [index, item] of list.entries()) {
console.log(`[for...of 迴圈] 正在等待第 ${index + 1} 筆完成...`);
item.number = await fakeApi(item.number);
console.log(`✅ [單筆完成] 第 ${index + 1} 筆更新完畢!`);
}
// 這裡保證所有的 await 都已經老老實實地執行完了
console.log("🎯 [主執行緒] 所有任務循序完成!");
console.log("此時的 list 狀態 (已更新):", JSON.stringify(list));
}
// 執行測試
testForOf();
配合 map 做全部 Promise 的等待:
async function testPromiseAll() {
console.log("=== 測試開始:Promise.all ===");
const list = getMockData();
console.log("[主執行緒] 瞬間發射所有請求,並開始等待...");
// map 會回傳一個由 Promise 組成的陣列
const promises = list.map(async (item) => {
item.number = await fakeApi(item.number);
console.log(`✅ [背景任務完成] 某筆資料更新完畢!`);
});
// 用 Promise.all 把它們全部攔截下來,等大家都跑完才放行
await Promise.all(promises);
// 這裡保證所有的並發任務都已完成
console.log("🎯 [主執行緒] 所有並發任務統一完成!");
console.log("此時的 list 狀態 (已更新):", JSON.stringify(list));
}
// 執行測試
testPromiseAll();
這樣在走訪完畢時,取到的值才會是最新的,因為每次走訪的 Promise 都會等待完畢,後面要對 list 陣列操作的程式碼才會執行,自然也就不會有抓到舊值的問題.
完整運行展示:https://jsbin.com/sinowiy/edit?html,console
先說結論,不可能,如果有任何結果上有沒走訪完畢的情況 100% 是其他資料邏輯處理的問題,例如前面提到的 Promise 後,沒等待又對陣列做操作這種狀況就是很典型的例子
直接看異常程式碼:
// --- 模擬 API (可指定延遲時間) ---
const fakeApi = (id, delay) => new Promise(resolve => {
setTimeout(() => {
console.log(`[API 回傳] 第 ${id} 筆資料算好了!(耗時 ${delay}ms)`);
resolve(id * 10);
}, delay);
});
function testMemoryRaceCondition() {
console.log("=== 🎬 測試開始:記憶體脫節災難 ===");
// 1. 初始化狀態 (想像這是 Vue 裡面的 data 或 reactive)
const state = {
itemList: [
{ id: 1, number: 0 },
{ id: 2, number: 0 },
{ id: 3, number: 0 }
]
};
// 偷偷把「舊的」記憶體位址存起來,等等用來驗屍
const originalRef1 = state.itemList[0];
const originalRef3 = state.itemList[2];
// 2. 啟動 forEach 推土機
state.itemList.forEach(async (item, index) => {
// 第 1 筆 500ms 跑完,第 2、3 筆 1500ms 跑完
const delay = index === 0 ? 500 : 1500;
// 這裡產生了閉包,item 永遠鎖定了「舊的」物件參照
item.number = await fakeApi(item.id, delay);
console.log(`✅ [背景賦值] 成功將值寫入 id:${item.id} 的物件。`);
});
// 3. 模擬 1000ms 時,發生了陣列重賦值 (例如使用者點了某個過濾按鈕,又或者是另一個Promise)
setTimeout(() => {
console.log("🚨 [突發事件] 1000ms 到了!執行陣列重構與深拷貝...");
// 致命一擊:產生全新陣列與「全新的物件參照」
state.itemList = state.itemList.map(item => ({
...item,
isProcessed: true // 加上一個新屬性作為標記
}));
console.log("⚠️ state.itemList 已經被替換成【全新記憶體位址】的陣列!");
}, 1000);
// 4. 等待 2000ms 所有事情塵埃落定後,檢查最終狀態
setTimeout(() => {
console.log("=== 🛑 塵埃落定,檢查最終狀態 ===");
console.log("目前畫面綁定的資料 (state.itemList):", JSON.stringify(state.itemList, null, 2));
console.log("--- 🕵️♂️ 幕後真相 (那些被丟棄的舊物件) ---");
console.log("當初的舊物件 1 號 (早就改好了,且被成功拷貝):", originalRef1);
console.log("當初的舊物件 3 號 (剛才在背景苦苦改完,但無人關心):", originalRef3);
}, 2000);
}
// 執行測試
testMemoryRaceCondition();