动态

详情 返回 返回

10 個被嚴重低估的 JS 特性,直接少寫 500 行代碼 - 动态 详情

前言

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

Hi,我是冴羽,學習前端,歡迎圍觀我的“網頁版朋友圈”、踏上“前端大佬成長之路”。

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

user avatar songminzh 头像
点赞 1 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.