这个标题我盯了很久——"JavaScript同样的意思,更巧的写法"。在项目里Review过很多次代码,经常能看到同一个业务需求,有人写得啰里啰嗦,有人三五行搞定,逻辑还更清楚。说实话,写JavaScript这十几年,我越来越觉得:代码不在于写得多,而在于表达得准。同样的意思,换一种写法,可能就少几个变量、少几次循环、少一层嵌套,别人读起来也省力气。
这篇文章我不打算堆技巧清单,而是换个思路:从几个最常见的编码场景出发,把"笨写法"和"巧写法"摆在一起,讲清楚为什么后面这种更值得用,以及在什么情况下必须避开它。适合刚入门前端的朋友建立好习惯,也适合写了两三年JS想再打磨一下代码的同学。
1. 先搞清楚:什么样的"巧"才值得学
1.1 巧写的本质是表达效率,不是炫技
我见过有些人写代码,特别喜欢堆运算符、嵌套三元表达式,一行代码恨不得塞进十个逻辑。那种代码确实"巧",但属于巧过头了,读起来像在解谜题。我理解的"巧写",核心是用更少的符号表达同样的语义,同时保持甚至提升可读性。
举个例子,判断一个值是否存在,然后给它赋默认值:
// 笨写法 let name; if (user && user.name !== undefined && user.name !== null) { name = user.name; } else { name = '匿名用户'; }换成巧写:
const name = user?.name ?? '匿名用户';两种写法意思完全一样,但第二种明显更"巧"。恰当运用可选链和空值合并,代码量从五行缩到一行,而且逻辑意图一眼能看出来——"如果user.name存在就用它,否则用匿名用户"。这种巧写是在减少噪音,突出语义,这才是值得学的方向。
1.2 一个判断标准:别人读你的代码需要几秒
我评价一段代码好不好,经常用"阅读耗时"来衡量。同样一段功能,A写法别人读30秒才懂,B写法5秒就懂,那B就是更巧的写法。这和代码行数关系不大,关键在于是否符合人的直觉思考方式。
举个例子,过滤数组中的无效值:
// 常规思路:for循环加if判断 const result = []; for (let i = 0; i < data.length; i++) { if (data[i] && data[i].value !== '') { result.push(data[i]); } }巧写:
const result = data.filter(item => item && item.value !== '');第二种方式直接描述"我要什么"——过滤出满足条件的项,而不是"怎么循环、怎么判断、怎么添加"。人的大脑处理"目标描述"比处理"过程指令"快得多。这个原则贯穿所有巧写的底层逻辑。
1.3 巧写的前提:你已经懂了笨写法
有个事情得说在前面——我推荐巧写,但不建议新手一开始就追求巧。解构赋值、展开运算符、高阶函数这些能力,如果基础不牢,很容易写成看起来简洁但逻辑有问题的代码。我的建议是:先能写出能运行的代码,再思考怎么写更巧。当你已经理解for循环怎么走、this怎么绑定、闭包怎么回事之后,巧写才有意义。否则就是在沙子上盖楼。
2. 条件与逻辑表达式的巧写:少两个变量,省三行判断
2.1 短路求值:&& 和 || 不只是逻辑运算符
很多同学用逻辑运算符只做布尔判断,其实它们真正的威力在于短路求值——左侧表达式的结果已经能决定整体结果时,右侧表达式根本不会执行。利用这个特性,可以替代很多简单的if语句。
比如,只有在用户已登录时才显示欢迎信息:
// 笨写法 if (isLogin) { showWelcome(); }巧写:
isLogin && showWelcome();再比如,取配置对象里的主题颜色,没有就用默认值:
// 笨写法 let themeColor; if (config.themeColor) { themeColor = config.themeColor; } else { themeColor = 'blue'; }巧写:
const themeColor = config.themeColor || 'blue';为什么能用||?因为JavaScript里config.themeColor如果是空字符串、0、null、undefined、NaN、false,都会被转成false,此时就会取右侧的'blue'。这个机制叫真值判断。但注意,如果用||处理空字符串,恰恰会掉坑——比如用户把主题色设为空字符串是有意为之,结果被替换成了默认蓝色。所以处理这种情况需要看场景。
2.2 空值合并运算符 ??:把 null 和 undefined 单独拎出来
||有个明显的缺点:它把0、''、false都当成"假值"。某些场景下,0是合法值,比如分数、数量、金额。这时候用||就会出问题:
const quantity = input.quantity || 1; // 用户输入 0,结果变成 1,这就不对了巧写应该用空值合并运算符:
const quantity = input.quantity ?? 1; // 只有 null 或 undefined 时才会取 1,0 会正常保留??的语义是"只有左侧是 null 或 undefined 时才取右侧"。这是 ES2020 引入的特性,现在主流浏览器和Node.js都支持。实际开发中,我处理表单数值、接口返回的默认值时,基本都用??而不是||。
2.3 三元表达式的分层写法:嵌套有度
三元运算符condition ? a : b本身就很简洁,适合if-else只有两个分支的场景。但嵌套三层以上就开始影响阅读了。
// 这种嵌套,虽然逻辑对,但读起来很痛苦 const type = user.type === 'admin' ? '管理员' : user.type === 'editor' ? '编辑' : user.type === 'viewer' ? '访客' : '未知';三层以内的三元链,如果格式对齐好,还能接受。超过三层,我宁可写成函数加switch或映射表:
const TYPE_MAP = { admin: '管理员', editor: '编辑', viewer: '访客', }; const type = TYPE_MAP[user.type] ?? '未知';映射表的写法,本质也是一种巧写——用数据结构代替条件分支。当判断逻辑是"根据某个值查对应结果"时,查找表永远比写一堆if/switch更清晰,而且扩展起来只需加一行配置。
2.4 条件判断里的小技巧:includes 替代多个或条件
再分享一个高频场景:判断某个值是否等于多个候选值之一。常见写法:
if (status === 'pending' || status === 'processing' || status === 'partial') { // 做某件事 }巧写:
if (['pending', 'processing', 'partial'].includes(status)) { // 做某件事 }后者的优势很明显:候选值集中在一个地方,既清晰又方便维护。要增加一个状态,只需改数组。这其实就是"数据驱动逻辑"的思路,在很多场景下能大幅简化代码。
3. 数组与字符串处理的巧写:用声明式替代命令式
3.1 map/filter/reduce:把循环写成流水线
JavaScript数组的三件套map、filter、reduce是我日常最常用的巧写工具。它们的核心思想是声明式编程——你只需要告诉程序"做什么",而不是"怎么做"。
比如,获取用户列表里所有已激活用户的邮箱地址:
// 笨写法:for循环 const emails = []; for (let i = 0; i < users.length; i++) { if (users[i].isActive) { emails.push(users[i].email); } }巧写:链式调用
const emails = users .filter(user => user.isActive) .map(user => user.email);filter负责筛选,map负责转换,每一步只做一件事。这就是流水线式的写法。对比一下:for循环把"怎么遍历、怎么判断、怎么取地址、怎么塞进数组"全塞在同一个块里,读代码的人要自己拆解;而链式写法直接告诉你"先筛再取",语义清清楚楚。
reduce很多人觉得难,其实它就是把"遍历并累积结果"这个动作抽象出来。比如计算购物车总价:
// 笨写法 let total = 0; for (const item of cart) { total += item.price * item.count; }巧写:
const total = cart.reduce((sum, item) => sum + item.price * item.count, 0);我见过不少同学纠结 reduce 的第二个参数,这里说清楚:reduce的第一个参数是回调,回调的第一个参数是"累积值",第二个参数是"当前项";第二个参数(0)是累积值的初始值。每次回调返回的值会成为下一次的累积值,最后一次回调的返回值就是整个reduce的结果。
3.2 展开运算符:复制、合并、传参的万能钥匙
展开运算符...是ES6里我最离不开的特性之一。它能把数组、对象"摊开",在很多场景下替代concat、Object.assign和手动传参。
数组复制:
const copy = [...original]; // 注意是浅拷贝,嵌套对象还是引用数组合并:
const merged = [...arr1, ...arr2, ...arr3];函数传参:
const result = Math.max(...nums); // 替代 Math.max.apply(null, nums)对象合并(ES2018起支持):
const merged = { ...defaults, ...userConfig };对象合并这个用法特别实用。默认配置和用户配置合并,不需要手写循环或者Object.assign。注意后面的对象覆盖前面的同名属性。我经常用这个特性实现配置合并、状态更新。
3.3 数组去重:Set 和展开运算符的组合拳
数组去重是个经典需求。不同数据规模有不同的巧写方式。
简单类型去重:
const unique = [...new Set(arr)];这个写法利用了 Set 天然不允许重复的特性。几万个元素的数组也能跑得很快。但如果数组里是对象,Set的去重就失效了,因为对象引用不同,需要自己定义去重规则:
// 按 id 字段去重 const unique = [...new Map(arr.map(item => [item.id, item])).values()];这段稍微复杂,但原理不難:arr.map先把数组变成[id, item]键值对数组,传给Map构造器会以id为键去重,.values()取出值,展开成新数组。这属于进阶巧写,建议在理解 Map 后再使用。
3.4 字符串处理的几个实用巧写
字符串判空、补全、截断是高频需求。很多写法能少写几行。
判断字符串是否为空字符串或纯空格:
// 笨写法 if (str === '' || str.trim() === '') { ... } // 巧写 if (!str.trim()) { ... }str.trim()去掉首尾空格后如果是空字符串,!取反就是true。同理,判断字符串是否有内容就直接写str.trim()——注意这样返回的是字符串,用在if条件里没问题。
字符串补全:
// 把数字补成两位,比如 3 -> 03 const padded = String(num).padStart(2, '0');截断字符串并加省略号:
const truncate = str.length > 20 ? str.slice(0, 18) + '...' : str;这些都是小写法,但积累多了,代码整体就干净很多。
4. 对象与解构赋值的巧写:少取十几次属性
4.1 解构赋值:从对象里取值的最短路径
对象解构是我向所有前端朋友推荐的第一优化点。看一个典型的场景——从配置对象里取出几个字段:
// 笨写法 const host = config.host; const port = config.port; const timeout = config.timeout; const retries = config.retries;巧写:
const { host, port, timeout, retries } = config;这不仅仅省了行数,更重要的是声明和赋值在一起,变量的来源一目了然。配合默认值,还能顺便处理字段缺失的情况:
const { host = 'localhost', port = 8080 } = config;如果变量名需要和属性名不同,也支持重命名:
const { host: serverHost, port: serverPort } = config;解构数组也一样,特别是搭配函数返回值:
const [first, second, ...rest] = arr;4.2 函数参数解构:调用方更轻松
函数参数用解构,调用时的体验会好很多。比如一个初始化用户信息的函数:
// 笨写法:调用方要记住参数顺序 function initUser(name, age, email, isAdmin) { ... } initUser('张三', 25, 'zhangsan@example.com', true); // 巧写:参数对象化 function initUser({ name, age, email, isAdmin = false }) { ... } initUser({ name: '张三', age: 25, email: 'zhangsan@example.com', isAdmin: true });第二种打法,调用时不担心顺序错,新增参数也不用改调用处,而且能用默认值。这个习惯对写公共函数、组件props尤其重要。
4.3 属性简写与计算属性名:让对象定义更流畅
ES6的属性简写,当对象属性名和变量名一致时可以省略:
// 笨写法 const obj = { name: name, age: age }; // 巧写 const obj = { name, age };这在函数返回对象时特别有用:
// 直接返回对象,不用写两遍变量名 function getUser(id) { const name = ...; const age = ...; return { name, age }; }计算属性名则允许在定义对象时使用动态键名:
const key = 'dynamic_' + id; const obj = { [key]: value };这两个特性很小,但组合起来能写出非常流畅的对象构建代码。特别在处理表单数据、接口字段映射时很顺手。
4.4 对象合并和更新:不可变更新风格
处理React状态或者遵循函数式编程习惯时,我们经常需要"不可变地更新对象"——不修改原对象,而是生成新对象。展开运算符是首选:
// 更新用户信息,保留原有字段 const updatedUser = { ...user, age: 26 }; // 等价于 Object.assign({}, user, { age: 26 })删除某个属性也有巧写(利用解构):
const { password, ...safeUser } = user; // safeUser 是去掉 password 之后的对象password被单独解构出来,剩下的safeUser不包含它。这种写法在向后端提交数据前剔除敏感字段时很有用。
5. 函数与异步处理的巧写:从回调地狱到清晰时序
5.1 箭头函数:不只是写法短,还解决 this 的问题
箭头函数让函数定义变得极其简洁:
// 笨写法 const sum = function(a, b) { return a + b; }; // 巧写 const sum = (a, b) => a + b;更关键的是,箭头函数不绑定自己的this,它继承定义时外层作用域的this。所以以前需要var self = this或者bind(this)的场景,现在直接用箭头函数就行。
比如定时器里访问当前对象:
// 笨写法:this 指向变化,需要保存 const self = this; setTimeout(function() { self.doSomething(); }, 1000); // 巧写:箭头函数保留外层 this setTimeout(() => { this.doSomething(); }, 1000);箭头函数还有一个作用:隐式返回。单表达式函数可以把return省略,用圆括号包裹返回对象:
const getUserLabel = user => ({ label: user.name, value: user.id });这里圆括号是必要的——如果不加圆括号,{}会被解析成函数体而不是对象字面量,这是个高频踩坑点。
5.2 async/await:把异步流程写成同步思维
Promise链式写法已经比回调好很多,但遇到复杂的先后依赖时,链式也会很绕。async/await是更巧的写法:
// Promise 链 fetchData() .then(result => process(result)) .then(final => display(final)) .catch(err => handleError(err));巧写:
async function render() { try { const result = await fetchData(); const final = process(result); display(final); } catch (err) { handleError(err); } }后者的可读性强在哪?它把异步流程拍成了"顺序执行"的形式,不需要then回调层层嵌套。特别是有多个接口要求先后顺序执行时,async/await能避免回调地狱:
// 串行请求:先拿用户信息,再根据角色拉权限 const user = await fetchUser(id); const permissions = await fetchPermissions(user.role);如果几个接口没有依赖关系,应该用Promise.all并行执行,这个取舍值得在代码注释里说明,避免别人误改成串行拖慢性能。
5.3 Promise.all / allSettled:并发处理的高效写法
并发处理多个异步任务,如果不需要按顺序,用Promise.all明显更高效。
// 多个图片并行加载 const results = await Promise.all([ loadImage('a.jpg'), loadImage('b.jpg'), loadImage('c.jpg'), ]);但Promise.all有一个问题:只要其中一个失败,整个Promise就变成 rejected,其他任务的结果会丢失。如果希望每个任务各自给出结果,用Promise.allSettled:
const results = await Promise.allSettled([task1(), task2()]); // 每个 result 都有 status 字段: 'fulfilled' 或 'rejected'数据上报场景我就常用allSettled——某个上报失败不应该影响其他的。这两个方法选对场景,代码既巧又稳。
5.4 柯里化与函数式小技巧:让函数更灵活
柯里化是函数式编程里一个有意思的巧写思路:把接受多个参数的函数改写成"接受一个参数,返回接受剩余参数的函数"。用箭头函数表达特别简洁:
// 普通函数 function add(a, b, c) { return a + b + c; } // 柯里化 const add = a => b => c => a + b + c; // 调用 add(1)(2)(3); // 6实际项目里,柯里化的典型应用是做"固定前置配置":
const multiply = factor => num => num * factor; const double = multiply(2); const triple = multiply(3); console.log(double(10)); // 20 console.log(triple(10)); // 30这种写法把"倍率"这个参数提取出去,生成新的专用函数。代码复用性强,语义也清楚。不过柯里化不是银弹,函数参数一多,箭头嵌套会深到影响阅读。我一般在两到三个参数时用它,多了就不值当了。
6. 巧写不等于炫技:这些坑我替你踩过了
6.1 可读性永远排在"短"前面
前面说了那么多巧写,但必须重申一个底线:如果一段巧写代码让人看不懂,那就不是巧写,是反巧写。判断标准很简单——把你写的代码拿给一个不太熟这个项目的同事看,看他能否很快读懂。
举例,有人喜欢用双重取反!!做布尔转换:
const isActive = !!user.active;这个写法确实很短,但新手看到会愣一下。如果团队里大家都习惯它,可以用;如果团队水平参差,我宁可写Boolean(user.active),语义更直白。同样道理,嵌套三层以上的三元表达式、复杂的位运算(如~~num取整),除非有注释说明,否则尽量别用。
在执行效率上,巧写如果没有用到性能更差的操作(比如在循环里反复创建数组),性能差异基本可以忽略。真正要防的是为了"写得巧"而在热循环里做低效操作,比如循环体内用filter或者展开运算符复制大数组。代码在运行时是有成本的,巧写不能以牺牲复杂度为代价。
6.2 常见误区与错误用法速查
我整理了一份开发中经常遇到的误用清单,都是踩过坑或者Review见过的问题:
| 场景 | 容易错的写法 | 问题说明 | 推荐写法 |
|---|---|---|---|
| 空值默认 | `value | '默认'` | |
| 字符串去空格后判断 | str === '' | 漏掉纯空格情况 | !str.trim() |
| 数组去重(对象数组) | [...new Set(arr)] | 对象按引用去重,无效 | 用Map按关键字段去重 |
| 可选链搭配 | `obj?.a | '默认'` | |
| 箭头函数返回对象 | x => { a: 1 } | 被解析成函数体 | x => ({ a: 1 }) |
| 解构嵌套过深 | const {a: {b: {c}}} = obj | 某一层为空就报错 | 分层解构,逐层判空 |
map里做复杂副作用 | arr.map(item => 删除item) | 应该用forEach表达副作用语义 | arr.forEach(...) |
6.3 根据团队和项目情况选合适的"巧度"
最后一个是团队协作层面的建议。巧写的接受度要结合团队实际情况:核心逻辑模块、工具函数库、一个人维护的老项目,可以更大胆地用新特性、函数式风格的写法;而多人协作的业务代码、给新人维护的模块,最好偏向更显式的写法。
我自己有个原则:公共函数的默认参数、合法值的空值处理、数组幂等操作,这三类场景无脑用巧写;复杂业务逻辑、异步串联、深度嵌套数据转换,一定写清楚步骤、注释、中间变量名起得直白。这两类不是对立的——同样的功能,你可以先按"笨办法"把逻辑跑通,再重构提炼。
比如处理一个复杂的接口返回数据,我先用for循环理清每一步,确认无误后再改成链式调用。这不是浪费时间,反而是在用"笨写法"验证逻辑、用"巧写法"提升可读性。聪明人写代码,从来不嫌弃先从笨开始。
结语前的一点体会
回头再看标题说的"同样的意思,更巧的写法"——十几个年头写下来,我最大的感受是:巧写不是为了显得自己厉害,而是为了让代码接近思维本身。人的脑子里转的念头是"过滤掉没激活的用户,再拿邮箱",链式写法直接表达这个念头;如果你脑子里要转一遍"从0开始循环、判断、取属性、push数组",代码就会多一层思考负担。
新手阶段,别急着把所有代码都写得花哨,先把基础循环、条件、对象操作练扎实,再用本文这些技巧逐步重构。写代码和写文章一样,第一稿允许啰嗦,关键在于你心里知道有一段更干净的表达在那儿等着你。等你下一次看见一个五行的if判断时,能想到??加一行搞定,说明你已经在用巧写的思维看问题了。