JavaScript 里其实藏着不少被严重低估的特性用对了一个项目少写 500 行代码真不是夸张。我先说个场景从接口返回的对象里嵌套取几个字段你得先判空再一个个赋值拿到一组用户数据想提取所有订单你下意识写两层 forEach判断字符串里有没有某个关键词你还在用 indexOf -1……这些都是可以两行解决的。今天我想结合自己在业务代码里的实际体会盘点 10 个常见但没被用透的 JS 特性每个都附带等价的旧写法对比看完你就能直接用到项目里。1. 数组与迭代真正省掉循环的三板斧数组是 JavaScript 里最常用的集合类型但大多数人翻来覆去只会map、filter、forEach。碰到数组扁平化、生成序列、类数组转换这类需求第一反应还是写 for 循环。下面两个方法能帮你把大量临时变量和循环代码直接删掉。1.1 用 flatMap 替代 filter 加 map 的双重遍历先看一个非常常见的业务场景后端返回一个订单列表每个订单里又有商品数组你想把所有商品扁平化提取出来。老一点的写法是这样的const orders [ { id: 1, items: [{ name: A }, { name: B }] }, { id: 2, items: [{ name: C }] } ]; // 传统写法双层循环 const allItems []; orders.forEach(order { order.items.forEach(item { allItems.push(item); }); });用flatMap一句话就结束了const allItems orders.flatMap(order order.items);flatMap()的逻辑是先执行map()再把结果整体拍平一层。它和map().flat()等价但只遍历一次语义也更清晰。更妙的是你可以用flatMap同时实现filter和map的效果返回空数组代表“过滤掉”返回[value]代表“保留并转换”。比如筛出库存大于 0 的商品并只保留名称const products [ { name: 鼠标, stock: 3 }, { name: 键盘, stock: 0 }, { name: 耳机, stock: 5 } ]; // 传统写法filter map const available products .filter(p p.stock 0) .map(p p.name); // flatMap 写法 const available products.flatMap(p p.stock 0 ? [p.name] : [] );不过有两个点需要留意。第一flatMap只拍平一层如果嵌套三层以上还是得用flat(2)或者递归。第二如果只是想把二维数组彻底展开成一维数组[].concat(...arr)也可以但遇到特别大的数组性能一般flatMap更合适。我在实际项目里做数据清洗管道时特别喜欢用flatMap因为它让“一对多映射”这种逻辑变得非常直观而且不会产生中间变量。看到嵌套循环第一时间想想能不能用flatMap往往能省好几行。1.2 用 Array.from 生成序列和转换类数组Array.from是一个“看似普通、实则很全能”的静态方法。它有两个核心能力把类数组/可迭代对象转成真数组生成指定长度的序列。很多前端拿到arguments或NodeList后第一反应是Array.prototype.slice.call(...)其实用Array.from(arguments)更直接。更重要的是Array.from可以接受第二个参数 mapFn相当于转换和生成一步到位。// 生成 0~99 的数组不用写 for 循环 const numbers Array.from({ length: 100 }, (_, i) i); // Set 去重后顺便做一遍转换 const uniqueDoubled Array.from(new Set([1, 1, 2, 3]), x x * 2); // [2, 4, 6] // NodeList 转真数组批量修改样式 const items document.querySelectorAll(.item); Array.from(items, el el.classList.add(active));第二个参数mapFn和普通数组的map行为几乎一致只是在空数组上不会执行。还有个容易忽略的小细节Array.from在遇到需要“先创建一个占位数组再 map”的场景非常有用很多写算法题的同学喜欢Array(n).fill(0).map(...)实际上Array.from({ length: n }, fn)更符合语义还省了一次fill。Array.from最大的价值是消除“类数组需要写工具函数才能变成真数组”的心智负担。以前从querySelectorAll拿到集合并想调用filter必须写// 旧写法 const items [].filter.call(document.querySelectorAll(.item), el el.visible);现在只需要Array.from(...)就行。类似的把Set、Map、生成器迭代出来的数据转成普通数组也都用它。这个特性用熟了代码里会少很多“转换工具函数”。2. 对象与解构取值不再写防御代码对象取值是前端防御式编码的重灾区。很多人为了从嵌套对象里安全取出一个值会写if (obj obj.a obj.a.b)或者用obj.a || {}一层层兜底。这部分要分享的奇技淫巧能把这些防御代码浓缩成一行。2.1 解构赋值里的默认值与嵌套模式解构赋值很多人会但往往只用最基础的const { a, b } obj。真正值钱的是它的默认值、重命名和嵌套模式组合能极大减少赋值语句。比如用户信息接口可能给出一堆字段你要给每个字段设置默认值// 传统写法 const name user.name || 匿名; const age user.age || 18; const city (user.address user.address.city) || 未知;解构写法const { name 匿名, age 18, address: { city 未知 } {} } user;这里有两个关键点默认值只在值为undefined时生效如果字段是null默认值不会覆盖它对于嵌套对象要提前用 {}给address一个默认空对象否则user里没有address时会直接抛错。函数参数也可以用解构默认值并且给整个参数一个兜底// 不传任何参数也不会报错 function greet({ name 世界, age } {}) { console.log(你好${name}年龄 ${age}); } greet();数组解构还有一个“不够用了”的炫技点交换变量。以前要借助临时变量现在一行搞定let a 1, b 2; [a, b] [b, a];解构赋值看似基础但在实际业务中它把“逐字段手动赋值”变成了“声明式提取”我重构代码时经常把下面这种 20 行的取值代码压成 3 行const { data: { list [], total 0, page 1 } {}, code, msg } response;注意解构出的默认值如果是一个对象字面量每次取默认都会生成一个新的对象引用。如果你希望多个解构共享同一个默认对象应该先定义常量再引用。2.2 Object.entries 与 Object.fromEntries 的转换魔法这两个方法一进一出把对象和“键值对数组”打通了。很多复杂的对象处理因此变得非常简洁。最常见的是遍历对象const obj { a: 1, b: 2, c: 3 }; // 传统 for...in 还要 hasOwnProperty 判断 for (const key of Object.keys(obj)) { console.log(key, obj[key]); } // 现代写法 for (const [key, value] of Object.entries(obj)) { console.log(key, value); }Object.fromEntries是逆操作可以把数组、Map 转回对象。两者合起来就能像数组一样对对象进行 filter 和 map。比如过滤掉值为空字符串的字段const user { name: 张三, email: , age: 18 }; const cleaned Object.fromEntries( Object.entries(user).filter(([key, value]) value ! ) ); // { name: 张三, age: 18 }再比如反转对象的键值const obj { a: 1, b: 2 }; const reversed Object.fromEntries( Object.entries(obj).map(([key, value]) [value, key]) ); // { 1: a, 2: b }和Map互转也非常频繁// 对象转 Map const map new Map(Object.entries(obj)); // Map 转对象 const obj2 Object.fromEntries(map);这些操作以前都需要手写中间对象和循环现在两行解决。当你发现自己在写“把对象转成数组再转回对象”的代码时大概率就是这个魔法的主场。3. 字符串与 URL处理文本参数少写一堆工具函数字符串和 URL 解析是另一个“手写工具函数大户”。很多人明明需要判断开头结尾还在用indexOf明明要替换所有匹配项还在写正则明明要解析 query还在split()。原生 API 早就把这些活干完了。3.1 includes、startsWith、endsWith 与 replaceAll 的组合用法先说说最日常的“判断字符串是否包含某个子串”。我相信很多项目里还有这种代码if (str.indexOf(abc) ! -1) { ... }用includes阅读起来就舒服多了if (str.includes(abc)) { ... }类似的startsWith和endsWith可以替代手写slice或正则判断。比如判断文件后缀// 旧写法 if (fileName.slice(-4) .jpg) { ... } // 新写法 if (fileName.endsWith(.jpg)) { ... }replaceAll是一个容易被忽略但极其实用的方法。以前想全局替换字符串要么写正则/foo/g要么写拆分合并// 旧写法 str.replace(/foo/g, bar); // 或者 str.split(foo).join(bar); // 新写法 str.replaceAll(foo, bar);replaceAll的优点是第一个参数是普通字符串不需要考虑正则的转义问题。比如你要把文本里的所有$替换成以前必须先\\$现在直接传$。再推荐一个和多字符串匹配有关的方法matchAll。它可以一次性返回所有全局匹配的结果并且支持捕获组。比如你想提取一段文本里所有被{{ }}包住的变量名const text 你好 {{name}}你的验证码是 {{code}}; const matches [...text.matchAll(/\{\{\s*(\w)\s*\}\}/g)]; // [[{{name}}, name], [{{code}}, code]]matchAll返回的是一个迭代器用展开运算符或for...of就能拿到所有匹配。以前要手动while(exec)来循环现在干净多了。另外padStart/padEnd在格式化数字时特别好用比如时间补零const seconds 5; console.log(String(seconds).padStart(2, 0)); // 05这些字符串方法加在一起能帮你干掉一整类“文本处理工具函数”。下次再想抄 lodash 或自己封装判断前缀后缀的函数先看看原生 API 有没有。3.2 URLSearchParams别再手写 query 解析了URL query 的解析和拼接也是重灾区。老代码经常是// 从 ?page1size10 中取 page const queryStr location.search.replace(/^\?/, ); const params {}; queryStr.split().forEach(pair { const [key, value] pair.split(); params[key] decodeURIComponent(value); }); const page params.page;且不说这种写法面对重复参数、空值和特殊编码时容易出问题光是代码量就很离谱。用URLSearchParamsconst params new URLSearchParams(location.search); const page params.get(page); // 1 const tags params.getAll(tag); // 支持重复参数 const hasKeyword params.has(keyword); // boolean修改参数也非常方便params.set(page, 2); params.append(extra, hello); params.delete(unused); const newQuery params.toString(); // page2extrahello把对象拼成 query 字符串URLSearchParams也是一行搞定而且自动处理编码const obj { name: 张三, tags: [a, b] }; const qs new URLSearchParams(obj).toString(); // name%E5%BC%A0%E4%B8%89tagsa%2Cb注意URLSearchParams接收对象时数组值会被转换成逗号分隔的字符串不一定符合后端接口要求。如果后端要tagsatagsb那还是得用append多次。还可以配合new URL()直接操作某个完整链接的 queryconst url new URL(https://example.com/path?a1a2b3); url.searchParams.getAll(a); // [1, 2] url.searchParams.set(c, 4); url.toString(); // https://example.com/path?a1a2b3c4这个 API 在 Node.js 里也能用写爬虫、写接口调试工具时特别好使。我把项目里所有手写parseQuery和buildQuery的函数都删了全部换成URLSearchParams代码量直接少了几十行。4. 异步与并发模块加载更省心的新写法Promise 早就普及了但多状态并发处理和模块初始化这块很多人还停留在“Promise.all 一把梭”的阶段。遇到了某个请求失败就全局挂掉或者为了在模块顶部 await 一个异步操作而包一层 IIFE。下面两个特性能把这些问题简化不少。4.1 Promise.allSettled失败不再波及全体Promise.all有一个很经典的痛点只要其中一个 Promise 变成 rejected整体结果就会进入 rejected其他已经成功的值也拿不到了。这在批量请求、批量上传、批量校验这些场景下非常难受——你只是想收集所有请求的结果而不是希望“一个失败就全军覆没”。以前大家是怎么应对的给每个请求手动包一层catchconst results await Promise.all( urls.map(url fetch(url).catch(e ({ error: e })) ) ); results.forEach(r { if (r.error) { ... } });这样做的副作用是你丢失了标准 Promise 状态还得用自定义字段区分。Promise.allSettled就是为了解决这个问题const results await Promise.allSettled( urls.map(url fetch(url)) ); results.forEach(result { if (result.status fulfilled) { console.log(result.value); } else { console.log(result.reason); } });返回的每一项都有固定的status字段要么是fulfilled要么是rejected。status 为fulfilled时用value拿数据status 为rejected时用reason拿错误原因。不再需要手动设计返回结构逻辑也更统一。我实际用这个特性重写过一次批量上传功能几十个文件同时上传需要知道每个文件是成功还是失败失败了要单独打标记。用Promise.allSettled之后代码比之前用Promise.all catch 包装直观了一倍而且不容易漏掉失败分支。再补充一下和它同批推出的另外两个方法Promise.race返回第一个完成的结果无论成功还是失败常用于超时控制。Promise.any返回第一个成功的 Promise如果全部失败才走 rejected。这三个方法加上all基本覆盖了大部分并发编排场景。要记住一点使用allSettled之后千万不要理所当然地以为所有数组项都有.value必须先检查status。4.2 顶层 await模块初始化不用包 Promise这是一个很多前端可能还没用起来的特性。在 ES ModuleESM环境下你可以在模块顶层直接使用await不需要把代码包进async function再手动调用。举个例子你需要从接口读取一份配置文件然后基于它生成一些常量并导出// 传统写法 let config; export async function init() { config await fetchConfig(); return config; } // 调用方还得等 init() 完成才能用有了顶层awaitconst config await fetchConfig(); export const appName config.name; export const theme config.theme;这种写法的意义不只是少了一个包装函数更重要的是它改变了模块依赖的加载方式。如果有另一个模块import { appName } from ./config.js那么被依赖的模块会先完成顶层await之后导入方再执行。这样就天然有了依赖等待不需要手动做初始化同步。不过要注意几个前提只能在 ES Module 里用typemodule的script或 Node.js 的.mjs文件。顶层await会让模块加载被阻塞如果等待时间太长整个页面模块的加载都会变慢所以别在入口模块里无脑用而是应该放在真正需要异步初始化的叶子模块。这个特性非常适合做“配置加载”“动态语言包”“初始化 SDK”这类只执行一次的异步场景。5. 运算符与赋值一行搞定原本要写三行的逻辑运算符这块很多人觉得“没啥可学的”但 ES2020 以来的几个新运算符能把大量条件赋值和判空判断压缩成一行而且语义更接近口头描述。5.1 可选链 ?. 和空值合并 ??先看看它们要解决的问题。从嵌套对象安全取值以前是这样const city (user user.address user.address.city) || 未知;用可选链const city user?.address?.city ?? 未知;?.会在前面的值等于null或undefined时短路返回undefined而不会继续往下访问所以不会报错。它可以用于三种场景// 属性访问 user?.address?.city; // 方法调用有才调用 this.method?.(); // 数组下标 list?.[0];??则用于给null/undefined一个默认值。它和||的关键区别是||会把所有“假值”都替换掉包括0、、false而??只替换null和undefined。典型场景分页参数page query.page ?? 1如果用户传了page0??会保留0而||会改成1保证是 bug。这两个运算符组合后的读法很接近自然语言const total response?.data?.total ?? 0;在原生 JavaScript 里写业务代码时我基本不会再写if (obj obj.a)这种长链了。需要留意的只有兼容性如果你的项目要支持非常老的浏览器比如 Android 5 以下 WebView可以考虑用 Babel/SWC 转译或者暂时忍一忍。5.2 逻辑赋值运算符 ??、||、这组运算符是逻辑条件和赋值操作的结合可以把“先判断再赋值”压缩到一行。它们分别是x || y如果x是假值则x y。x y如果x是真值则x y。x ?? y如果x是null或undefined则x y。最常见的用法是给对象设置默认值但不覆盖已有内容// 传统写法 if (this.config null) { this.config { retries: 3 }; } // 新写法 this.config ?? { retries: 3 };处理表单输入时想要把空字符串变成默认值// 传统写法 if (!user.name) { user.name 匿名; } // 新写法 user.name || 匿名;更适合在“有值才继续处理”的场景// 只有 items 存在时才做 map if (items) { items items.map(process); } // 等价于 items items.map(process);这里有个常见的陷阱??和||左侧的表达式不能是未声明的变量否则会直接抛 ReferenceError。它们适合给已经存在的对象属性或变量填充默认值。另外多个逻辑运算符组合时最好加上括号避免可读性降低。有一次我在写缓存逻辑时看到一段旧代码if (cache[key] undefined) { cache[key] defaultValue; }改成cache[key] ?? defaultValue;之后整个函数的代码量又少了一截。第一次看这种写法可能会觉得“这是什么鬼”一旦习惯了你就回不去了。写到这里有些人可能担心这些新语法在团队项目里“太超前”。其实主流的现代浏览器和 Node.js 14 都已经原生支持绝大多数特性配合构建工具转译也完全没有问题。我个人在重构老代码时会先从数组和字符串相关的方法入手因为接受度高、不容易出 bug等团队适应了再慢慢推可选链、空值合并和逻辑赋值实测下来代码量能减掉不少。与其继续维护一堆手写的解析函数和防御式判断不如早点把这些特性用起来让代码更像“在描述业务”而不是“在描述怎么拆字符串”。
企业数字化 ERP 产品动态
相关推荐
Qwen Coder Mac本地部署实战:从选型到落地的完整指南 我这周被问得最多的一个词就是 coder。有人问 coder 咋下载,有人问我 Qwen Coder 在 Mac 上怎么部署,还有人把 AI Coder 代码生成现状的讨论直接甩我脸上。先提一句,搜索 coder 的时候你还会看到 KH Coder 这种做文本挖掘的软件,那… · 2026/9/23 4:34:29
3道荣耀9价格高频面试题拆解从零搭建实战项目避坑 3道荣耀9价格高频面试题拆解从零搭建实战项目避坑 刚写完代码发现跑不起来?别慌。这是新手最常见的死穴。你会背语法,但不会搭项目。更扎心的是,面试时考官最爱拿【荣耀9价格】这种看似简单的业务逻辑考你。这其实是【高频面试题】里的经典陷阱。很多人… · 2026/9/23 4:34:29
3天搞懂阿兹海默算法:面试必问的实战避坑指南 3天搞懂阿兹海默算法:面试必问的实战避坑指南 看了一堆教程还是不会写项目?别慌,这不是你笨,是教程太“虚”。 很多兄弟在准备 面试必问 的高频算法题时,总觉得“阿兹海默”(注:此处借代复杂状态机或记忆化搜索类高难度算法,如LeetCode… · 2026/9/23 4:34:23
量子态原理图解:3个案例帮新手避坑 量子态原理图解:3个案例帮新手避坑 报错日志满屏红字,StackTrace 堆得让人头皮发麻,新手最容易在这里卡住。别慌,咱们把“量子态”这个听起来很玄的词,拆成市政公用工程微服务里的具体场景,用代码把坑填平。新手避坑的核心,不是背概念,而… · 2026/9/23 6:04:45
多模态AI大模型统一接入平台:架构设计与多模态适配实战 多模态AI这两年从“能看图的聊天框”一路卷到“能听会看还能动手”的智能体,身边做业务的朋友几乎都在问同一个问题:手里攒了七八个模型的API Key,写业务代码时到底该怎么接才不把自己坑死。我过去一年半先后在三个项目里落地过统一接入层&am… · 2026/9/23 6:04:39
图像去噪深度学习实战:卷积神经网络与残差学习全解析 简介:面向深度学习与图像处理方向学习者的一份高分大作业项目源码,完整实现了基于卷积神经网络的图像去噪算法研究,并附带四种传统去噪算法作为对照。项目中以DnCNN为核心,同时实现均值滤波、中值滤波、非局部均值(NLM… · 2026/9/23 6:04:33
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29