🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

10 個被嚴重低估的 JS 特性,直接少寫 500 行程式碼

前言

最近逛 Reddit 的時候,看到一個關於最被低估的 JavaScript 特性的討論,我對此進行了總結,和大家分享一下。

1. Set:陣列去重 + 快速查找,比 filter 快 3 倍

提到陣列去重,很多人第一反應是 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("滾動了"));

2. Object.entries () + Object.fromEntries ():物件陣列互轉神器

以前想遍歷物件,要用 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: "男"}

3. ?? 與 ??=:比 || 靈活

|| 設置默認值時,會把 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}

4. Intl API:原生國際化 API

很多人會用 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);

5. Intersection Observer:圖片懶加載 + 滾動加載,不卡主執行緒

傳統我們用 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"));

6. Promise.allSettled ():批量請求不 “掛掉”,比 Promise.all 更實用

如果使用 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"]
});

7. element.closest ():向上找父元素最安全的方式

傳統如果想找某個元素的父元素,比如點擊列表項找列表,需要使用 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");
});

8. URL + URLSearchParams:處理 URL 方便多了

傳統解析 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頁

9. for...of 循環:比 forEach 靈活,還支援 break 和 continue

我們都知道,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...
}

10. 顶层 await:模組異步初始化

以前在 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 以後會成為基本操作。


原文出處:https://juejin.cn/post/7568153532014559267


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝20   💬4   ❤️6
635
🥈
我愛JS
📝1   💬4   ❤️2
46
🥉
酷豪
1
#5
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付