一、Set: 数组去重 + 快速查找,比 filter 快3倍
提到数组去重,很多第一反应是 filter + indexOf,但是这种写法的时间复杂度是O(n²),而 Set 天生支持 “唯一值”,查找速度是 O(1),还能直接转数组。
示例:
用户 ID 去重
// 后端返回的重复用户ID列表constduplicateIds=[101,102,102,103,103,103];// 1行去重constuniqueIds=[...newSet(duplicateIds)];console.log(uniqueIds);// [101, 102, 103]避免重复绑定事件
constlistenedEvents=newSet();functionsafeAddEvent(eventName,handler){if(!listenedEvents.has(eventName)){window.addEventListener(eventName,handler);listenedEvents.add(eventName);// 标记已绑定}}// 调用2次也只会绑定1次 scroll 事件safeAddEvent("scroll",()=>console.log("滚动了"));safeAddEvent("scroll",()=>console.log("滚动了"));二、Object.entries() + Object.fromEntries():对象数组互转神器
以前想遍历对象,要用 for…in 循环,外加判断 hasOwnProperty;如果想把数组转成对象,只能手动写循环。这对组合直接一键搞定。
示例:
对象转数组 Object.entries()
Object.entries(obj) 会将对象的可枚举键值对转换为一个二维数组,格式为 [[key1, value1], [key2, value2], …]。
// 原始对象constobj={name:'张三',age:20,gender:'男'};// 对象转数组:Object.entries()constarr=Object.entries(obj);console.log(arr);// 输出:[ ['name', '张三'], ['age', 20], ['gender', '男'] ]特点:
- 只处理对象自身的可枚举属性(不包含继承的属性,如 toString)。
- 数组元素是 [键, 值] 的结构,键的类型为字符串(即使原对象键是数字,也会转为字符串)。
数组转对象
Object.fromEntries(arr) 会将一个二维数组(格式为 [[key1, value1], [key2, value2], …])转换为对应的对象,数组中的每个子数组的第一个元素作为对象的键,第二个元素作为值。
// 原始数组(格式必须是 [[key, value], ...])constarr=[['name','李四'],['age',25],['gender','女']];// 数组转对象:Object.fromEntries()constobj=Object.fromEntries(arr);console.log(obj);// 输出:{ name: '李四', age: 25, gender: '女' }注意:
- 数组的子数组必须包含至少两个元素(否则键对应的值为 undefined)。
- 若数组中存在重复的键,后面的键值对会覆盖前面的(对象键具有唯一性)。
示例 2:修改对象的键或值
constobj={name:'张三',age:20};// 1. 转数组后,修改键名和值constmodifiedArr=Object.entries(obj).map(([key,value])=>{if(key==='name')return['username',value];// 改键名if(key==='age')return[key,value+1];//改值return[key,value];});// modifiedArr: [ ['username', '张三'], ['age', 21] ]// 2. 转回对象constnewObj=Object.fromEntries(modifiedArr);console.log(newObj);// { username: '张三', age: 21 }筛选对象属性,过滤掉空值
// 后端返回的用户信息,包含空值字段constuserInfo={name:"张三",age:28,avatar:"",// 空值,需要过滤phone:"13800138000",};// 1. 转成[key,value]数组,过滤空值;2. 转回对象constfilteredUser=Object.fromEntries(Object.entries(userInfo).filter(([key,value])=>value!==""));console.log(filteredUser);// {name: "张三", age:28, phone: "13800138000"}URL 参数转对象(不用再写正则了)
// 地址栏的参数:?name=张三&age=28&gender=男constsearchStr=window.location.search.slice(1);// 直接转成对象,支持中文和特殊字符constparamObj=Object.fromEntries(newURLSearchParams(searchStr));console.log(paramObj);// {name: "张三", age: "28", gender: "男"}三、?? 与 ??=:比 || 靠谱
用 || 设置默认值时,会把 0、“”、false 这些 “有效假值” 当成空值。而 ?? 只判断 null/undefined。
示例:
处理用户输入的 “有效假值”
// 用户输入的数量( 0 是有效数值,不能替换)constuserInputCount=0;// 错误:会把 0 当成空值,返回 10constwrongCount=userInputCount||10;// 正确写法:只判断 null/undefined,返回 0constcorrectCount=userInputCount??10;console.log(wrongCount,correctCount);// 10, 0给对象补默认值(不会覆盖已有值)
// 前端传入的配置,可能缺少 retries 字段constrequestConfig={timeout:5000};// 只有当 retries 为 null/undefined 时,才赋值 3(不覆盖已有值)requestConfig.retries??=3;console.log(requestConfig);// {timeout:5000, retries:3}// 如果已有值,不会被覆盖constoldConfig={timeout:3000,retries:2};oldConfig.retries??=3;console.log(oldConfig);// {timeout:3000, retries:2}四、Intl API:原生国际化 API
很多人会用 moment.js 处理日期、货币格式化,但这个库体积特别大(压缩后也有几十 KB);而 Intl 是浏览器原生 API,支持货币、日期、数字的本地化,体积为 0,还能自动适配地区。
示例:
多语言货币格式化(适配中英文)
constprice=1234.56;// 人民币格式(自动加 ¥ 和千分位)constcnyPrice=newIntl.NumberFormat("zh-CN",{style:"currency",currency:"CNY",}).format(price);// 美元格式(自动加 $ 和千分位)constusdPrice=newIntl.NumberFormat("en-US",{style:"currency",currency:"USD",}).format(price);console.log(cnyPrice,usdPrice);// ¥1,234.56 $1,234.56日期本地化(不用手动拼接年月日)
constnow=newDate();// 中文日期:2025年11月3日 15:40:22constcnDate=newIntl.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 PMconstenDate=newIntl.DateTimeFormat("en-US",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",}).format(now);console.log(cnDate,enDate);五、Intersection Observer:图片懒加载 + 滚动加载,不卡主线程
传统我们用 scroll 事件 + getBoundingClientRect() 判断元素是否在视口,会频繁触发重排,导致页面卡顿;Intersection Observer API 是异步监听,不阻塞主线程,性能直接提升一大截。
示例:
图片懒加载(可用于优化首屏加载速度)```html
列表滚动加载更多(避免一次性加载过多数据)
<ulid="news-list"></ul><!-- 加载提示,放在列表底部 --><divid="load-more">加载中...</div>constloadObserver=newIntersectionObserver((entries)=>{if(entries[0].isIntersecting){// 当加载提示进入视口,请求下一页数据fetchNextPageData().then((data)=>{renderNews(data);// 渲染新列表项});}});// 监听加载提示元素loadObserver.observe(document.getElementById("load-more"));六、Promise.allSettled ():批量请求不 “挂掉”,比 Promise.all 更实用
如果使用 Promise.all,当批量请求时,只要有一个请求失败,Promise.all 就会直接 reject,其他成功的请求结果就拿不到了;而 allSettled 会等待所有请求完成,不管成功失败,还能分别处理结果。
示例:
批量获取用户信息 + 订单 + 消息(部分接口失败不影响整体)
// 3个并行请求,可能有失败的constrequestList=[fetch("/api/user/101"),// 成功fetch("/api/orders/101"),// 失败(比如订单不存在)fetch("/api/messages/101"),// 成功];// 等待所有请求完成,处理成功和的结果Promise.allSettled(requestList).then((results)=>{// 处理成功的请求constsuccessData=results.filter((res)=>res.status==="fulfilled").map((res)=>res.value.json());// 记录失败的请求(方便排查问题)constfailedRequests=results.filter((res)=>res.status==="rejected").map((res)=>res.reason.url);console.log("成功数据:",successData);console.log("失败接口:",failedRequests);// ["/api/orders/101"]});七、element.closest ():向上找父元素最安全的方式
传统如果想找某个元素的父元素,比如点击列表项找列表,需要使用 element.parentNode.parentNode,但一旦 DOM 结构变了,代码就崩了;closest() 会直接根据 CSS 选择器找最近的祖先元素,不管嵌套多少层。
示例:
点击列表项,给列表容器加高亮
<ulclass="user-list"><liclass="user-item">张三</li><liclass="user-item">李四</li></ul>document.querySelectorAll(".user-item").forEach((item)=>{item.addEventListener("click",(e)=>{// 找到最近的.user-list(不管中间嵌套多少层)constlist=e.target.closest(".user-list");list.classList.toggle("active");// 切换高亮});});输入框聚焦,给表单组加样式
<divclass="form-group"><label>用户名</label><inputtype="text"id="username"/></div>constusernameInput=document.getElementById("username");usernameInput.addEventListener("focus",(e)=>{// 找到最近的.form-group,加focused样式constformGroup=e.target.closest(".form-group");formGroup.classList.add("focused");});八、URL + URLSearchParams:处理 URL 方便多了
传统解析 URL 参数、修改参数,还要写复杂的正则表达式,有时还得处理中文编码问题;URL API 可以直接解析 URL 结构,URLSearchParams 可用于处理参数,支持增删改查,自动编码,方便多了。
示例:
解析 URL 参数(支持中文和特殊字符)
// 当前页面URL:https://xxx.com/user?name=张三&age=28&gender=男constcurrentUrl=newURL(window.location.href);// 获取参数console.log(currentUrl.searchParams.get("name"));// 张三console.log(currentUrl.hostname);// xxx.com(域名)console.log(currentUrl.pathname);// /user(路径)修改 URL 参数,跳转新页面
consturl=newURL("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=3window.location.href=url.href;// 跳转到第3页九、for…of 循环:比 forEach 灵活,还支持 break 和 continue
forEach 不能用 break 中断循环,也不能用 continue 跳过当前项。而 for…of 不仅支持中断,还能遍历数组、Set、Map、字符串,甚至获取索引。
示例:
遍历数组,找到目标值后中断
constproductList=[{id:1,name:"手机",price:5999},{id:2,name:"电脑",price:9999},{id:3,name:"平板",price:3999},];// 找价格大于8000的产品,找到后中断for(constproductofproductList){if(product.price>8000){console.log("找到高价产品:",product);// {id:2, name:"电脑", ...}break;// 中断循环,不用遍历剩下的}}遍历 Set,获取索引
constuniqueTags=newSet(["前端","JS","CSS"]);// 用 entries() 获取索引和值for(const[index,tag]of[...uniqueTags].entries()){console.log(`索引${index}:${tag}`);// 索引 0:前端,索引 1:JS...}十、顶层 await:模块异步初始化
以前在 ES 模块里想异步加载配置,必须写个 async 函数再调用;现在 top-level await 允许你在模块顶层直接用 await,其他模块导入时会自动等待,不用再手动处理异步。
示例:
模块初始化时加载配置
// config.js// 顶层直接 await,加载后端配置constresponse=awaitfetch("/api/config");exportconstappConfig=awaitresponse.json();// {baseUrl: "https://xxx.com", timeout: 5000}// api.js(导入 config.js,自动等待配置加载完成)import{appConfig}from"./config.js";// 直接用配置,不用关心异步exportconstapiClient={baseUrl:appConfig.baseUrl,get(url){returnfetch(`${this.baseUrl}${url}`,{timeout:appConfig.timeout});},};点击按钮动态加载组件(按需加载,减少首屏体积)
// 点击“图表”按钮,才加载图表组件document.getElementById("show-chart-btn").addEventListener("click",async()=>{// 动态导入图表模块,await 等待加载完成const{renderChart}=awaitimport("./chart-module.js");renderChart("#chart-container");// 渲染图表});结语
可以看到,以前我们依赖的第三方库,其实原生 API 早就能解决,比如用 Intl 替代 moment.js,用 Set 替代 lodash 的 uniq,用 Intersection Observer 替代懒加载,随着老旧的浏览器被 内容过长