超越異步/等待:高級工程師常用的十種 JS/TS 高級技巧
基礎教程來回炒冷飯、永遠停在 async/await 和“手寫一個 Promise”的階段?
下面這 10 個高級 TypeScript / JavaScript 技巧,正好填上那塊空白:從 Promise 編排、內存安全緩存、二進制處理 到 異步調優,全部來自真實生產事故的復盤,而不是紙上談兵。
這些模式曾經實打實地:
- 把某線上服務的內存泄漏下降了 38%
- 讓數據庫成本直接砍掉 60%
- 把某批處理鏈路提速到原來的 3.2 倍
無論是滿天飛的“懸空 Promise”,還是一踩就炸的連接池,這些“冷門但好用”的實戰套路,足夠改變工程師構建健壯系統的方式。
1. 用 void 守住 Async IIFE:給“即刻執行”的異步一個出口
場景問題:立即執行的 async 函數(Async IIFE),如果返回的 Promise 沒有被顯式處理,經常會在啟動流程里埋下“懸空 Promise”的隱患——內存泄漏、未捕獲異常統統算在這一類頭上。
// ? 風險寫法 —— 返回了一個沒人管的 Promise
(async () => {
await initializeApp();
})();
// ? 安全寫法 —— 明確丟棄返回值
void (async () => {
await initializeApp();
})();void 的作用,就是顯式丟棄 async IIFE 返回的 Promise:
- 避免 TypeScript / Lint 一直提醒“未處理的 Promise”
- 讓“我就是要 fire-and-forget”這件事寫在代碼語義里
- 特別適合做:應用啟動階段的“一次性異步操作”,比如日志預熱、配置加載、緩存預熱等
凡是“需要跑,但不需要再被業務邏輯 await 回來”的異步啟動邏輯,都值得加上一層 void 作為“態度聲明”。
2. 高精度異步計時:把 console.time 的活交給 Performance API
console.time() 對粗略統計夠用,但到了需要微秒級精準的時候,就有點捉襟見肘了。 這時可以用瀏覽器/Node 都支持的 Performance API 來做異步計時。
const measureAsync = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
performance.mark(`${name}-start`);
try {
return await fn();
} finally {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const [entry] = performance.getEntriesByName(name);
console.log(`?? ${name}: ${entry.duration.toFixed(3)}ms`);
performance.clearMarks();
}
};
// 使用方式:
await measureAsync("DatabaseTransaction", () =>
db.transaction(complexQuery)
);好處不只有一條 console.log:
- 會在 Performance 時間線里留下標記, Chrome DevTools 的 Performance 面板可以直接看到每一段異步的耗時和分布
- 統一包裝業務操作,方便后續批量審計“哪段邏輯始終慢”
- 不需要額外引依賴,就能拿到比較專業的性能數據
適合掛在:數據庫事務、外部接口、復雜計算、批量導入這類關鍵路徑上。
3. 把 AbortController 當“異步調度器”,而不是只給 fetch 用
AbortController 不只是給 fetch 設計的。 一旦把它當成一個通用信號器,任何 Promise 都可以掛在這根“取消總線”上。
const createCancellablePool = (promises: Promise<any>[], signal: AbortSignal) => {
return Promise.all(
promises.map(
p =>
new Promise((resolve, reject) => {
// 監聽中斷信號
signal.addEventListener("abort", () =>
reject(new DOMException("Cancelled", "AbortError"))
);
// 正常完成
p.then(resolve).catch(reject);
})
)
);
};
// 使用方式:
const controller = new AbortController();
// 2 秒后取消全部任務
setTimeout(() => controller.abort(), 2000);
await createCancellablePool(
[analyticsSync(), cacheHydration()],
controller.signal
);關鍵用例:路由切換 / 頁面跳轉。
- 用戶離開當前頁面時, 后臺跑著的埋點上報、緩存預熱、預取數據統統可以優雅地中斷
- 既節省流量和算力,又避免“舊頁面的異步結果,誤寫進新頁面 state”這種詭異 bug
實踐里,前端“路由級取消”基本都可以用 AbortController 搭個公共基座。
4. 用異步生成器做“懶加載 Promise 流”:一邊拉一邊算
一次性把所有 Promise 全部 await 完,再統一處理結果,是最常見也最容易吃內存的大坑。
更穩的做法:用 async function* 生成一個“懶處理的異步流”,只在需要的時候才繼續往前拉。
async function* streamResults<T>(urls: string[]) {
for (const url of urls) {
const response = await fetch(url);
yield (response.json() as unknown) as T;
}
}
// 按需消費
const videoStream = streamResults<Video>(videoUrls);
for await (const video of videoStream) {
if (shouldStopProcessing(video)) break;
renderPreview(video);
}這種模式的兩個隱性紅利:
- 處理超大規模數據(比如 TB 級日志、批量視頻元信息)時,內存占用保持接近常數,不再隨著數據量飆升
- 一旦業務條件滿足(比如“找到目標”、“錯誤達到上限”), 可以提前 break,不浪費后續網絡和計算資源
凡是之前寫成 Promise.all(urls.map(...)) 又擔心爆內存的地方,都值得升級成這樣的“異步流式消費”。
5. 玩轉 TypedArray:用二進制把 JS 的“腳本感”打掉
要和 WebAssembly、WebGL、WebSocket 二進制協議打交道時,ArrayBuffer 和各類 TypedArray 是繞不過去的。
下面這個模式,是高效拼接多個二進制塊的常見寫法:
const mergeBuffers = (buffers: ArrayBuffer[]) => {
const total = buffers.reduce((sum, b) => sum + b.byteLength, 0);
const result = new Uint8Array(total);
let offset = 0;
buffers.forEach(buffer => {
result.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
});
return result.buffer;
};
// Bonus:零拷貝切片
const sliceWithoutCopy = (buffer: ArrayBuffer, start: number, end: number) => {
return buffer.slice(start, end);
};適用場景包括:
- WebAssembly 模塊之間的內存互通
- WebGL 紋理、頂點數據的合并和切割
- WebSocket 二進制數據幀的組裝與拆分
相比常規的 Buffer.concat/Array.concat,TypedArray 在性能和內存可控性上更接近“系統語言”。
6. 用 Error cause 把“錯誤鏈路”串起來
異步調用層級一多,錯誤信息只看頂層 message,基本等于什么都沒看見。 好用的錯誤處理模式不應只是“拋出”,而是要把因果關系串起來。
async function processOrder() {
try {
await validatePayment();
} catch (err) {
// 包裝一層業務語義,同時保留根因
throw new OrderError("Payment failed", { cause: err });
}
}
try {
await processOrder();
} catch (e: any) {
console.error("Root cause:", e.cause); // 原始校驗錯誤
Sentry.captureException(e, { extra: { cause: e.cause } });
}好處非常現實:
- 日志里能看到“業務錯誤 + 技術根因”雙維度信息
- 分布式鏈路追蹤時,可以用
cause串起跨服務的錯誤軌跡 - 前臺可以根據
cause決定是否提示更多細節,后臺則留足排查信息
微服務 / Serverless 架構里,錯誤鏈幾乎是復盤事故的唯一生命線。
7. 安全枚舉屬性:別再讓原型鏈“順手帶口鍋”
在工具函數里做對象枚舉時,如果直接上 for...in 或沒想清楚原型鏈,很容易被“原型污染”陰一把。
更穩的方式,是基于 屬性描述符 做一次顯式過濾:
const getSafeKeys = (obj: object) => {
return Object.entries(Object.getOwnPropertyDescriptors(obj))
.filter(([_, desc]) => desc.enumerable)
.map(([key]) => key);
};
// 原型為 null 的純凈字典
const safeDict = Object.create(null);
(safeDict as any).data = "test";
console.log(getSafeKeys(safeDict)); // ["data"]
// 不會受到原型鏈屬性影響在安全敏感環境下(例如處理外部 JSON、模板變量、動態配置), 安全枚舉可以:
- 避免被注入
__proto__、constructor等危險屬性 - 確保工具函數對“自有屬性”負責,不吃原型鏈的虧
對長期維護的工具庫,先把枚舉方式換掉,是最便宜的一次安全加固。
8. Promise 池并發控制:給并行任務設一個“保險絲”
一次 Promise.all 把幾百個請求丟給數據庫或外部服務,很容易一腳踩爆連接池,整條鏈路全掛。
可以用一個簡潔的 PromisePool 做“有上限的并行執行器”:
class PromisePool {
private running = 0;
private queue: (() => void)[] = [];
constructor(private concurrency: number) {}
async run<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
const execute = async () => {
this.running++;
try {
resolve(await task());
} catch (err) {
reject(err);
} finally {
this.running--;
this.next();
}
};
this.queue.push(execute);
this.next();
});
}
private next() {
while (this.queue.length && this.running < this.concurrency) {
this.queue.shift()!();
}
}
}
// 使用方式:
const pool = new PromisePool(3); // 比如:DB 最大連接數 3
await pool.run(() => generateReport());好處非常直白:
- 把“每次最多并發多少個任務”固化到代碼里
- 防止 serverless / 短生命周期函數在高峰期把下游服務打掛
- 對第三方 API 有 QPS 限制時,也可以當“節流器”用
在多租戶、多區域、多實例場景下,PromisePool 往往是撐住系統的一條底線。
9. 用 Proxy 做“可觀察 Promise”:狀態查詢不再另開一份 state
很多 UI 代碼里,為了展示“pending / done / error”狀態,會額外維護一堆布爾量,其實 Promise 自己就可以被“觀察”。
function trackPromise<T>(promise: Promise<T>) {
const state = {
status: "pending" as "pending" | "fulfilled" | "rejected",
value: null as T | null
};
const proxy = new Proxy(promise, {
get(target, prop) {
if (prop === "status") return state.status;
if (prop === "value") return state.value;
return Reflect.get(target, prop);
}
});
promise
.then(result => {
state.status = "fulfilled";
state.value = result;
})
.catch(() => {
state.status = "rejected";
});
return proxy as Promise<T> & { status: typeof state.status; value: T | null };
}
// 使用方式:
const dataPromise = trackPromise(fetch("/api/data"));
// React 組件示例:
useEffect(() => {
dataPromise.then(data => {
console.log(`State: ${dataPromise.status}`); // "fulfilled"
});
}, []);UI 側獲益:
- 不需要再聲明一堆
isLoading/hasError狀態變量 - 直接從 Promise 上讀
.status和.value即可渲染對應界面 - 非 React 場景(例如終端 TUI、Node 服務日志)同樣適用
本質上就是:在不破壞 Promise 接口的前提下,給它掛上可觀測的狀態層。
10. 用 WeakRef 做“自動清理”的緩存:不手動刪也不亂占內存
緩存是性能優化必備,但配不好就會變成“慢性內存泄漏”。 WeakRef + FinalizationRegistry 提供了一種“對象一被 GC,緩存就自動失效”的模式:
class TemporaryCache {
private cache = new Map<string, WeakRef<object>>();
private cleanup = new FinalizationRegistry((key: string) => {
this.cache.delete(key);
});
set(key: string, value: object) {
this.cache.set(key, new WeakRef(value));
this.cleanup.register(value, key, value);
}
get(key: string): object | undefined {
const ref = this.cache.get(key);
return ref?.deref();
}
}
// 使用方式:
const cache = new TemporaryCache();
cache.set("user:123", heavyUserObject);
// 當 heavyUserObject 被 GC 回收時
// 對應的 cache entry 會自動被移除適合用在:
- 大對象(圖像、DOM 快照、解析后的 AST、復雜計算結果)的短期緩存
- 希望“內存吃緊時自動丟棄舊值”的場景
- 需要減少“手動清理緩存”邏輯的復雜系統
這種緩存更像一種“盡力而為”的加速器:有就用,沒有也不會影響正確性。
核心收獲小結
這一系列技巧,其實勾勒出了 JS/TS 從“腳本語言”到“系統語言”的那條路:
- 計時精度:異步測量優先用 Performance API,而不是只靠
console.time() - 取消模型:把 AbortController 用到 fetch 之外,統一管理異步任務的“生死”
- 內存管理:使用 WeakRef 和 FinalizationRegistry 做不會越堆越大的緩存
- 錯誤上下文:通過
cause串起錯誤鏈,方便追蹤根因 - 并發控制:用 PromisePool 控制資源,保護數據庫和第三方服務
- 二進制效率:用 TypedArray 原生處理二進制數據,向系統級能力靠攏
- 安全反射:用屬性描述符安全枚舉對象,防御原型污染
- 狀態觀測:用 Proxy 讓 Promise 自帶“狀態標記”,簡化 UI 狀態機
- 懶執行管道:用異步生成器按需處理數據流,避免“一次性吃光內存”
- 值丟棄語義:在 fire-and-forget 場景使用
void,明確承認“這是個不會被 await 的 Promise”
這些模式不需要一次性全部上場。 只要在合適的場景,解決真實的性能和穩定性問題,JS/TS 工程就能從“能跑起來”,穩步升級到“經得起流量和時間考驗”。
























