最近逛 Reddit 的時候,看到一個關於最被低估的 JavaScript 特性的討論,我對此進行了總結,和大家分享一下。
提到陣列去重,很多人第一反應是 filter + indexOf,但這種寫法的時間複雜度是 O (n²),而 Set 天生支援 “唯一值”,查找速度是 O (1),還能直接轉陣列。
舉個例子:
使用者 ID 去重:
// 後端返回的重複使用者 ID 列表
const duplicateIds = [101, 102, 102, 103, 103, 103];
// 1 行去重
const uniqueIds = [...new Set(duplicateIds)];
console.log(uniqueIds); // [101,102,103]
避免重複綁定事件:
const listenedEvents = new Set();
// 封裝事件綁定函數,防止同一事件重複綁定
function safeAddEvent(eventName, handler) {
if (!listenedEvents.has(eventName)) {
window.addEventListener(eventName, handler);
listenedEvents.add(eventName); // 標記已綁定
}
}
// 調用 2 次也只會綁定 1 次 scroll 事件
safeAddEvent("scroll", () => console.log("滾動了"));
safeAddEvent("scroll", () => console.log("滾動了"));
以前想遍歷物件,要用 for...in 循環,外加判斷 hasOwnProperty;如果想把陣列轉成物件,只能手動寫循環。這對組合直接一鍵搞定。
舉個例子:
篩選物件屬性,過濾掉空值:
// 後端返回的使用者資訊,包含空值字段
const userInfo = {
name: "張三",
age: 28,
avatar: "", // 空值,需要過濾
phone: "13800138000",
};
// 1. 轉成[key,value]陣列,過濾空值;2. 轉回物件
const filteredUser = Object.fromEntries(Object.entries(userInfo).filter(([key, value]) => value !== ""));
console.log(filteredUser); // {name: "張三", age:28, phone: "13800138000"}
URL 參數轉物件(不用再寫正則了):
// 地址欄的參數:?name=張三&age=28&gender=男
const searchStr = window.location.search.slice(1);
// 直接轉成物件,支援中文和特殊字符
const paramObj = Object.fromEntries(new URLSearchParams(searchStr));
console.log(paramObj); // {name: "張三", age: "28", gender: "男"}
用 || 設置默認值時,會把 0、""、false這些 “有效假值” 當成空值。比如使用者輸入 0(表示數量),count || 10會返回 10,但這裡其實應該返回 0。而??只判斷 null/undefined。
舉個例子:
處理使用者輸入的 “有效假值”:
// 使用者輸入的數量( 0 是有效數值,不能替換)
const userInputCount = 0;
// 錯誤寫法:會把 0 當成空值,返回 10
const wrongCount = userInputCount || 10;
// 正確寫法:只判斷 null/undefined,返回 0
const correctCount = userInputCount ?? 10;
console.log(wrongCount, correctCount); // 10, 0
給物件補默認值(不會覆蓋已有值):
// 前端傳入的配置,可能缺少 retries 字段
const requestConfig = { timeout: 5000 };
// 只有當 retries 為 null/undefined 時,才賦值 3(不覆蓋已有值)
requestConfig.retries ??= 3;
console.log(requestConfig); // {timeout:5000, retries:3}
// 如果已有值,不會被覆蓋
const oldConfig = { timeout: 3000, retries: 2 };
oldConfig.retries ??= 3;
console.log(oldConfig); // {timeout:3000, retries:2}
很多人會用 moment.js 處理日期、貨幣格式化,但這個庫體積特別大(壓縮後也有幾十 KB);而 Intl 是瀏覽器原生 API,支援貨幣、日期、數字的本地化,體積為 0,還能自動適配地區。
舉個例子:
多語言貨幣格式化(適配中英文):
const price = 1234.56;
// 人民幣格式(自動加 ¥ 和千分位)
const cnyPrice = new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
}).format(price);
// 美元格式(自動加 $ 和千分位)
const usdPrice = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
console.log(cnyPrice, usdPrice); // ¥1,234.56 $1,234.56
日期本地化(不用手動拼接年月日):
const now = new Date();
// 中文日期:2025年11月3日 15:40:22
const cnDate = new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(now);
// 英文日期:November 3, 2025, 03:40:22 PM
const enDate = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(now);
console.log(cnDate, enDate);
傳統我們用 scroll事件 + getBoundingClientRect()判斷元素是否在視口,會頻繁觸發重排,導致頁面卡頓;Intersection ObserverAPI 是異步監聽,不阻塞主執行緒,性能直接提升一大截。
舉個例子:
圖片懶加載(可用於優化首屏加載速度):
<!-- 用data-src存真實圖片地址,src放佔位圖 -->
<img data-src="https://xxx.com/real-img.jpg" src="placeholder.jpg" class="lazy-img" />
// 初始化觀察者
const lazyObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// 當圖片進入視口
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 加載真實圖片
lazyObserver.unobserve(img); // 加載後停止監聽
}
});
});
// 給所有懶加載圖片添加監聽
document.querySelectorAll(".lazy-img").forEach((img) => {
lazyObserver.observe(img);
});
列表滾動加載更多(避免一次性加載過多數據):
<ul id="news-list"></ul>
<!-- 加載提示,放在列表底部 -->
<div id="load-more">加載中...</div>
const loadObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// 當加載提示進入視口,請求下一頁數據
fetchNextPageData().then((data) => {
renderNews(data); // 渲染新列表項
});
}
});
// 監聽加載提示元素
loadObserver.observe(document.getElementById("load-more"));
如果使用 Promise.all,當批量請求時,只要有一個請求失敗,Promise.all 就會直接 reject,其他成功的請求結果就拿不到了;而 allSettled 會等待所有請求完成,不管成功失敗,還能分別處理結果。
舉個例子:
批量獲取使用者資訊 + 訂單 + 消息(部分接口失敗不影響整體):
// 3個並行請求,可能有失敗的
const requestList = [
fetch("/api/user/101"), // 成功
fetch("/api/orders/101"), // 失敗(比如訂單不存在)
fetch("/api/messages/101"), // 成功
];
// 等待所有請求完成,處理成功和失敗的結果
Promise.allSettled(requestList).then((results) => {
// 處理成功的請求
const successData = results.filter((res) => res.status === "fulfilled").map((res) => res.value.json());
// 記錄失敗的請求(方便排查問題)
const failedRequests = results.filter((res) => res.status === "rejected").map((res) => res.reason.url);
console.log("成功數據:", successData);
console.log("失敗接口:", failedRequests); // ["/api/orders/101"]
});
傳統如果想找某個元素的父元素,比如點擊列表項找列表,需要使用 element.parentNode.parentNode,但一旦 DOM 結構變了,程式碼就崩了;closest() 會直接根據 CSS 選擇器找最近的祖先元素,不管嵌套多少層。
舉個例子:
點擊列表項,給列表容器加高亮:
<ul class="user-list">
<li class="user-item">張三</li>
<li class="user-item">李四</li>
</ul>
document.querySelectorAll(".user-item").forEach((item) => {
item.addEventListener("click", (e) => {
// 找到最近的.user-list(不管中間嵌套多少層)
const list = e.target.closest(".user-list");
list.classList.toggle("active"); // 切換高亮
});
});
輸入框聚焦,給表單組加樣式:
<div class="form-group">
<label>使用者名稱</label>
<input type="text" id="username" />
</div>
const usernameInput = document.getElementById("username");
usernameInput.addEventListener("focus", (e) => {
// 找到最近的.form-group,加focused樣式
const formGroup = e.target.closest(".form-group");
formGroup.classList.add("focused");
});
傳統解析 URL 參數、修改參數,還要寫複雜的正則表達式,有時還得處理中文編碼問題;當然我們會直接引入第三方庫來處理,但畢竟還要引入多餘的苦,其實 URL API 可以直接解析 URL 結構,URLSearchParams 可用於處理參數,支援增刪改查,自動編碼,方便多了。
解析 URL 參數(支援中文和特殊字符):
// 當前頁面URL:https://xxx.com/user?name=張三&age=28&gender=男
const currentUrl = new URL(window.location.href);
// 獲取參數
console.log(currentUrl.searchParams.get("name")); // 張三
console.log(currentUrl.hostname); // xxx.com(域名)
console.log(currentUrl.pathname); // /user(路徑)
修改 URL 參數,跳轉新頁面:
const url = new URL("https://xxx.com/list");
// 添加參數
url.searchParams.append("page", 2);
url.searchParams.append("size", 10);
// 修改參數
url.searchParams.set("page", 3);
// 刪除參數
url.searchParams.delete("size");
console.log(url.href); // https://xxx.com/list?page=3
window.location.href = url.href; // 跳轉到第3頁
我們都知道,forEach 不能用 break中斷循環,也不能用 continue跳過當前項。而for...of不僅支援中斷,還能遍歷陣列、Set、Map、字串,甚至獲取索引。
舉個例子:
遍歷陣列,找到目標值後中斷:
const productList = [
{ id: 1, name: "手機", price: 5999 },
{ id: 2, name: "電腦", price: 9999 },
{ id: 3, name: "平板", price: 3999 },
];
// 找價格大於8000的產品,找到後中斷
for (const product of productList) {
if (product.price > 8000) {
console.log("找到高價產品:", product); // {id:2, name:"電腦", ...}
break; // 中斷循環,不用遍歷剩下的
}
}
遍歷 Set,獲取索引:
const uniqueTags = new Set(["前端", "JS", "CSS"]);
// 用 entries() 獲取索引和值
for (const [index, tag] of [...uniqueTags].entries()) {
console.log(`索引${index}:${tag}`); // 索引 0:前端,索引 1:JS...
}
以前在 ES 模組裡想異步加載配置,必須寫個 async 函數再調用;現在 top-level await 允許你在模組頂層直接用 await,其他模組導入時會自動等待,不用再手動處理異步。
舉個例子:
模組初始化時加載配置:
// config.js
// 頂層直接 await,加載後端配置
const response = await fetch("/api/config");
export const appConfig = await response.json(); // {baseUrl: "https://xxx.com", timeout: 5000}
// api.js(導入 config.js,自動等待配置加載完成)
import { appConfig } from "./config.js";
// 直接用配置,不用關心異步
export const apiClient = {
baseUrl: appConfig.baseUrl,
get(url) {
return fetch(`${this.baseUrl}${url}`, { timeout: appConfig.timeout });
},
};
點擊按鈕動態加載組件(按需加載,減少首屏體積):
// 點擊“圖表”按鈕,才加載圖表組件
document.getElementById("show-chart-btn").addEventListener("click", async () => {
// 動態導入圖表模組,await 等待加載完成
const { renderChart } = await import("./chart-module.js");
renderChart("#chart-container"); // 渲染圖表
});
可以看到,以前我們依賴的第三方庫,其實原生 API 早就能解決,比如用 Intl 替代 moment.js,用 Set 替代 lodash 的 uniq,用 Intersection Observer 替代懶加載,隨著舊的瀏覽器被淘汰,兼容性越來越好,這些 API 以後會成為基本操作。