一文分享10个被严重低估的JS特性
作者:冴羽
前言
最近逛 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 以后会成为基操。
以上就是一文分享10个被严重低估的JS特性的详细内容,更多关于JS特性的资料请关注脚本之家其它相关文章!
