
1. 先搞清楚JS运算符到底有多少种先说个我自己的经历。刚工作那会儿我接手一个老项目线上有个诡异bug一个判断用户是否登录的逻辑明明条件都满足了但页面就是不走下一步。我查了半天最后发现是同事写代码时把if (userId 12345)写成了赋值而不是相等比较。当时我整个人是懵的——这代码居然能通过语法检查还跑得挺正常——因为赋值的返回值是12345在布尔上下文里永远是truthy所以条件恒为真bug就这样潜伏了两个月。这不算什么高端技巧但它让我意识到运算符这套东西表面上人人会用但坑全藏在细节里。JavaScript里的运算符官方文档列出来大概有四十多种按功能分成几大类赋值运算符、算术运算符、比较运算符、逻辑运算符、位运算符、条件三元运算符、逗号运算符、一元运算符还有一些比较少见的像typeof、instanceof、in、delete这类。说实话日常开发中90%的时间我们只用其中十几个但剩下的那些恰恰是遇到疑难杂症时的突破口。我建议所有前端新人把运算符当成表达式的地基来学因为JS里几乎一切东西都是表达式而表达式由操作数和运算符构成。你写的每一行代码本质上都是在用运算符组装值。地基不稳后面学闭包、学this、学框架原理都会觉得隔了一层。2. 赋值运算符从到链式赋值再到解构赋值2.1 不是数学里的等号很多从数学思维转过来的朋友第一个要纠正的观念就是是赋值而不是相等。它做的事情是把右边的值计算出来存到左边的变量/属性里而且整个表达式的返回值就是被赋的那个值。这个返回被赋的值的特性导致了上面那个buguserId 12345这个表达式本身的值是12345而12345是truthy所以if判断恒为true。解决办法很简单——平时写比较用写赋值就单独一行。但这还引出一个好玩的东西链式赋值。let a, b, c; a b c 0;这段代码的含义是把0赋给c然后把c的值赋给b再把b的值赋给a。因为赋值运算符的结合性是从右到左所以链式赋值能正常工作。注意链式赋值里所有变量会被赋成同一个值如果你以为它是依次赋不同值那就理解错了。2.2 复合赋值运算符、-、*、/、%、**复合赋值是先运算再赋值的语法糖比如a 1等价于a a 1。JS里支持运算符等价写法说明a a b加法或字符串拼接-a a - b减法*a a * b乘法/a a / b除法%a a % b取余**a a ** b幂运算a a b左移a a b右移a a b无符号右移a a b按位与|a a | b按位或^a a ^ b按位异或a a b逻辑与赋值ES2021||a a || b逻辑或赋值ES2021??a a ?? b空值合并赋值ES2021这里要注意一个细节复合赋值的两边只要有一个是字符串结果就是字符串拼接。所以a 1; a 1;后a是11不是2。这个几乎是面试必考题也是实际开发里最容易产生隐式bug的点之一。我自己的习惯是做数值累加之前先确保变量是Number类型比如用Number()包一层或者用parseInt明确转换。2.3 解构赋值一行代码交换变量如果你还在用temp a; a b; b temp来交换两个变量的值那一定要试试解构赋值let a 1, b 2; [a, b] [b, a];这只是解构的冰山一角。对象解构同样强大尤其是从接口返回的数据里提取字段时const { code, data: { list, total } } response;比逐行写const list response.data.list舒服太多了。但解构赋值有个坑如果右侧的值是null或undefined直接报错。所以解构response.data之前最好先做一层判空或者用默认值兜底const { data {} } response; const { list [] } data;这样即使接口返回异常页面也不至于白屏。2.4 扩展运算符与把一个数组的值加到另一个数组热搜里有个很典型的场景js怎么用扩展运算符把一个数组里面的值都添加到另外一个数组。答案其实很简单const arr1 [1, 2, 3]; const arr2 [4, 5, 6]; const merged [...arr1, ...arr2]; // merged: [1, 2, 3, 4, 5, 6]如果不想新建数组而是想在原数组上追加可以配合pusharr1.push(...arr2);这里要注意push(...arr2)是把arr2的每个元素逐个 push 进去效果等同于arr1.push(4, 5, 6)。如果直接写arr1.push(arr2)那你得到的是[1, 2, 3, [4, 5, 6]]——一个嵌套数组这是新手常犯的错误。扩展运算符本质上是展开可迭代对象所以字符串也能展开[...hello]; // [h, e, l, l, o]这个技巧在做字符统计、字符串去重时非常有用。2.5 赋值里容易被忽略的引用问题赋值运算符对基本类型number、string、boolean、null、undefined、symbol、bigint是传值对对象类型object、array、function是传引用。这意味着const a { name: 张三 }; const b a; b.name 李四; console.log(a.name); // 李四不少新手在这栽跟头以为b a是复制了一份结果修改b把a也改了。想要真正复制一个对象得用{...a}浅拷贝或者structuredClone(a)深拷贝。但浅拷贝只拷贝一层嵌套对象依然共享引用深拷贝在复杂场景下也有各种边界问题——这是另一个大话题但源头就是赋值运算符的引用传递特性理解了这个后面学状态管理、学React的setState不可变更新会顺畅很多。3. 算术运算符与比较运算符隐式类型转换是万恶之源3.1 号的二义性加法和拼接的纠缠号可能是JS里最人格分裂的运算符。它的行为取决于操作数类型如果两边都是数字就是数值加法。如果两边有任意一个是字符串就变成字符串拼接。如果两边是数组、对象会先调用toString()再决定。举几个真实场景console.log(1 1); // 2 console.log(1 1); // 11 console.log(1 1); // 11 console.log([] []); // 空字符串 console.log([] {}); // [object Object] console.log({} []); // 0不在控制台可能输出 [object Object]最后一个在浏览器控制台和Node里表现还可能不一样因为{}开头会被解析成代码块而不是对象字面量。这种混乱正是JS被吐槽最多的点。我的建议很直接做数值运算前用Number()、parseInt()、parseFloat()显式转换不要依赖隐式转换。parseInt(12px)返回12这种带单位解析在CSS相关计算里很有用但要注意parseInt(abc)返回NaN。3.2 取余、幂运算和自增自减取余运算符%在实际开发中经常用于判断奇偶、循环轮播图索引、分页页码计算等场景。它和数学上的模略有不同JS的%结果符号跟随被除数console.log(5 % 3); // 2 console.log(-5 % 3); // -2 console.log(5 % -3); // 2幂运算**是ES2016引入的替代了之前的Math.pow()console.log(2 ** 10); // 1024自增自减和--则要分清前置和后置的区别let a 1; let b a; // b 1, a 2先赋值再自增 let c a; // c 3, a 3先自增再赋值这个知识点面试必问实战中我见过有人为了炫技写arr[i]导致索引混乱的案例。我的原则是能拆成两行写的绝不用后置自增的返回值代码可读性永远比少写一行重要。3.3 与松散相等的魔鬼规则在JS里我几乎只用和!。为什么因为会做隐式类型转换规则复杂到难以记忆。比如console.log(0 ); // true console.log(0 0); // true console.log( 0); // false console.log(null undefined); // true console.log(false false); // false console.log(false 0); // true这些规则背后有一套抽象相等比较算法ToPrimitive ToNumber但说实话正常人记不住也不该记。唯一让我觉得还算合理的地方是判断null和undefinedif (value null) { /* 同时匹配 null 和 undefined */ }但这写法太隐晦我宁愿写成if (value null || value undefined)或者if (value null)加注释。除此之外一律这是ESLint规则eqeqeq的默认推荐也是我code review时必提的一条。3.4 比较运算符与NaN、Infinity的边界陷阱比较运算符、、、对数字和字符串的行为不同console.log(2 10); // true因为字符串按字典序比较2 1 console.log(2 10); // false因为10被转成数字10第一个结果很反直觉如果你在比较版本号或者字符串日期结果可能完全错误。我踩过这个坑在某个项目里比较订单号1001和99用比较得到99 1001是true导致排序全乱。之后我碰到字符串形式的数字一律先转Number。NaN是另一个大坑。它不等于任何东西甚至不等于自己console.log(NaN NaN); // false console.log(Number.isNaN(NaN)); // true判断一个值是不是NaN用Number.isNaN()不要用isNaN()后者会把字符串先转数字isNaN(abc)为true但Number.isNaN(abc)为false。Infinity和-Infinity是浮点溢出的结果在进行除法或大数据计算时要注意。4. 逻辑运算符、位运算符与空值合并不止是条件判断4.1 短路求值和||的高级玩法逻辑运算符、||、!不只是返回布尔值它们返回的是两个操作数之一。这是JS和前几代语言不太一样的地方也是很多高级写法的源头。a b如果a是 falsy直接返回a否则返回b。a || b如果a是 truthy直接返回a否则返回b。所以||常用来设置默认值const name user.name || 匿名用户;常用来做安全访问data data.list data.list.forEach(...)要注意的是如果a是0、、false这些falsy值a || 默认值会返回默认值。有些场景下你希望0是合法值用它做默认值逻辑就会出问题——比如展示价格时price || 免费当价格是0时显示免费或许没问题但如果是积分、库存这类0有实际含义的场景就要改用??空值合并运算符const count data.count ?? 0; // 只在 data.count 为 null/undefined 时取0??空值合并运算符是ES2020引入的它只对null和undefined做兜底对0、、false不做处理。这个区分非常实用。同样在ES2020引入的还有可选链?.const name user?.profile?.name;这一行相当于const name user user.profile ? user.profile.name : undefined;但在深层嵌套时?.的写法清晰得多。注意?.不是一个运算符严格说它是可选链运算符同样遵循短路原则。还有一个细节??不能和||、混用不加括号写a ?? b || c会直接报SyntaxError必须写成(a ?? b) || c。4.2 逻辑与赋值、||、??到底怎么用ES2021加的这三个逻辑赋值运算符其实就是短路求值 赋值的合体// 传统写法 if (a) { a b; } // 等价于 a b; // 传统写法 if (!a) { a b; } // 等价于 a || b; // 传统写法 if (a null || a undefined) { a b; } // 等价于 a ?? b;??在配置项合并、缓存初始化场景非常常用。比如const cache {}; function getCache(key) { cache[key] ?? createExpensiveData(); return cache[key]; }这比if (!cache[key]) { cache[key] ... }优雅多了。但要注意如果你想用0做缓存key且值为空串??正好只对null/undefined生效符合预期。4.3 位运算符从标志位到位掩码位运算符、|、^、~、、、在业务代码里用得不多但在底层库、混淆代码、权限系统里非常常见。它们把操作数转成32位整数然后按位操作。我举一个实用的例子用位掩码做权限组合。假设一个系统有三种权限读1、写2、删除4用二进制表示就是001、010、100。单个用户的权限可以用或运算组合const READ 1; // 001 const WRITE 2; // 010 const DELETE 4; // 100 let userPermission 0; userPermission | READ; // 001给读权限 userPermission | WRITE; // 011再给写权限 userPermission | DELETE; // 111三权都有 // 检查是否有写权限 if (userPermission WRITE) { // 有写权限 } // 移除写权限 userPermission ~WRITE;这种做法的好处是用一个小整数就能组合标记多个开关状态在二进制协议、状态机、游戏成就系统里非常常见。另外左移右移用于快速乘以/除以2的幂console.log(1 4); // 16相当于 1 * 2^4 console.log(32 2); // 8相当于 32 / 2^2但位运算有个坑JS位操作会把数字转成32位有符号整数超出范围会溢出。而且无符号右移会把负数变成很大的正数console.log(-1 0); // 4294967295这个特性偶尔被人用来写取整技巧比如~~3.9得到3。我不推荐在业务代码里用这种黑魔法因为可读性太差写Math.floor(3.9)多明白。4.4 枚举类型赋值与位掩码热搜里枚举类型赋值这个词在很多语言里是给枚举成员赋数值。JS原生没有枚举但可以用Object.freeze模拟const Color Object.freeze({ RED: 1, GREEN: 2, BLUE: 4 });如果枚举值恰好是2的幂就可以和位运算组合实现多选枚举比如上面权限的例子。这是我实际做过的方案一个活动报名系统类型字段包含线上、线下、直播、回放四种一个报名记录可以同时属于多种类型用位掩码存储一个整数就够了省得建关联表。5. 运算符优先级代码里最隐蔽的地雷5.1 从一张优先级表说起热搜里运算符优先级反复出现说明这是大家的痛点。JS运算符优先级本质上是决定表达式先算谁、后算谁的规则。优先级从高到低只看常用的优先级运算符说明最高() . [] new括号、成员访问、new高! ~ -- typeof一元运算符较高**幂运算右结合较高* / %乘法、除法、取余中 -加法、减法、字符串拼接中 位移中 in instanceof关系比较较低 ! !相等比较较低^|按位与/异或/或低||??逻辑运算更低?:三元条件最低 - ...赋值注意优先级高于||||高于??但??不能和它们混用。位运算中高于^^高于|。这个表我建议保存到书签里写复杂表达式时随时翻一下。5.2 结合性为什么右结合这么反直觉优先级解决的是先算哪个运算符结合性解决的是同等优先级时从左还是从右。大多数运算符是左结合a - b - c相当于(a - b) - c但有两个例外赋值运算符是右结合a b c相当于a (b c)。幂运算符是右结合2 ** 3 ** 2相当于2 ** (3 ** 2)结果是512不是64。这个右结合特性偶尔会让人写错。比如我见过2 ** 3 ** 2期望是64结果得到512的案例。解决方式很简单拿不准就加括号。多写括号不丢人反而是在保护代码的可读性。5.3 真实事故优先级导致的经典bug我印象最深的一个bug来自一个前端页面const isAdmin user.role admin || superadmin;写这段代码的人想表达如果角色是admin或superadmin就是管理员。但实际上||返回的是truthy值所以isAdmin永远是superadmintruthy这个判断恒为真。正确的是const isAdmin user.role admin || user.role superadmin;另一个类似的坑是把逻辑运算和相等比较混在一起if (a 1 || 2 || 3) { ... }这也是恒真的因为1 || 2 || 3结果是1然后a 1。正确写法是a 1 || a 2 || a 3。还有三元运算嵌套的优先级const result a ? b : c ? d : e;这个等价于a ? b : (c ? d : e)嵌套三元是右结合的。能读但可读性很差我一般拆成if/else。5.4 非JS场景里的优先级干扰热搜里还有C语言运算符优先级、python赋值、修改网卡优先级、需求优先级模型、python 服务 windows 下莫名变慢 优先级这些词。这里想提醒一下不同领域的优先级含义完全不同。C语言优先级表和JS有细微差别比如C的、|优先级比低JS的位运算更靠下网卡优先级、进程优先级属于操作系统范畴需求优先级属于管理模型。做JS开发时脑子里要时刻区分我刚说的优先级是运算符的不是别的。这看起来是废话但我真见过有人因为混淆概念在讨论需求优先级时引用运算符优先级表格——场面一度很尴尬。6. 框架实战与常见问题排查实录6.1 React/Vue中运算符的典型应用条件渲染React里常用于{isLoggedIn UserPanel /}但有一个经典坑——当isLoggedIn是0时页面上会渲染出一个0因为0是falsy值0 Component /返回的是0React会把这个数字渲染出来。解决办法是写成{!!isLoggedIn UserPanel /}或{isLoggedIn ? UserPanel / : null}。Vue模板里的运算符限制热搜词里有个很具体的坑非 h5 平台 :key 不支持逻辑运算符的兼容性问题。在uni-app或小程序里:key绑定如果写:keyitem.id || item.name在某些非H5平台编译会出问题。因为小程序模板的表达式解析能力和浏览器不一致复杂逻辑表达式在模板里可能被拒绝。我的解决方案把逻辑计算放到JS里用一个计算属性返回处理好的key值模板里只绑定一个变量。数组更新与扩展运算符React state 里更新数组推荐用展开运算符创建新数组setList(prev [...prev, newItem]);这也呼应了前面的...展开——把旧数组的元素全部展开再追加新元素。如果要用下标修改数组中的某一项记住不要直接赋值list[0] x而是setList(prev prev.map((item, i) i 0 ? x : item))。6.2 运算符相关常见问题速查表问题原因解决方案if (a b)永远为真赋值和相等比较搞混用赋值语句单独一行2 10返回 true字符串按字典序比较数字比较前先Number()NaN NaN是 falseNaN与任何值都不相等用Number.isNaN()0默认值 得到默认值{count} Com /渲染出 0返回falsy值本身用!!或三元表达式嵌套三元几乎读不懂三元运算右结合 嵌套改成 if/else 或 IIFE扩展运算符push成嵌套数组写成了arr.push(arr2)写arr.push(...arr2)对象浅拷贝修改影响原对象赋值传引用用{...obj}或structuredClone6.3 一条实用的ESLint防线想从源头减少运算符问题我推荐几条ESLint规则eqeqeq: 强制使用和!。no-sequences: 禁止使用逗号运算符(a 1, b 2)这种写法。no-mixed-operators: 不允许在没有括号的情况下混用不同优先级的运算符比如a b || c会报错要求写成(a b) || c。no-unneeded-ternary: 减少无必要的三元表达式。这些规则往.eslintrc里一加很多隐晦bug在开发阶段就被拦截了。我在团队里推了这套配置后代码review时关于运算符的争论直线下降——因为ESLint已经强制统一了写法我们只需要讨论业务逻辑。7. 一些额外想分享的实操习惯最后说几个我在实际开发中沉淀下来的习惯不一定写在规范文档里但对减少运算符问题很有帮助。第一不依赖记忆依赖括号和变量名。任何包含超过两种运算符的表达式要么加括号要么拆成中间变量。const isEligible (age 18) (score 60);比const isEligible age 18 score 60;更直观也完全规避了优先级问题。这不是畏手畏脚而是考虑到三个月后你可能不记得这里的运算细节。第二把魔法数字封装成有名字的常量。我见过太多代码里直接写if (status 4)这样的位判断4是什么含义毫无线索。改成const HAS_DELETE_PERMISSION 4;后含义就清楚了。运算符本身没错错的是把没有语义的数字直接暴露出来。第三利用console.log验证你的假设。我写复杂运算符表达式前经常先在控制台跑一遍确认结果是预期的。比如不确定2 10的结果直接打印一下比翻文档快多了。这也是一种测试驱动的思维。第四警觉falsy值的边界。JS的falsy值只有七个false、0、-0、0n、、null、undefined、NaN。看到||和时心里默念一遍操作数会不会落入这几个特殊情况。尤其是0和他们在业务中往往是合法值但falsy特性会在默认值逻辑里造成误判。运算符这个东西说难不难说简单也不简单。难的是它在每个表达式里都在默默工作一旦出错定位成本极高——因为它看起来不像是会出错的地方。但好消息是规则就那么多把优先级表、隐式转换规则、短路求值这几个核心吃透再配上一套ESLint规则兜底你踩过的坑大概率不会踩第二次。你在项目里遇到过什么奇怪的运算符bug或者有什么独特的避坑习惯欢迎在评论区聊聊没准别人也正被同样的问题卡住。