Skip to content

10 个被严重低估的 JS 特性,直接少写 500 行代码 #376

Description

@mqyqingfeng

前言

最近逛 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 以后会成为基操。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions