
1. 先从“运算符到底是什么”说起——一次搞懂分类关系1.1 我们为什么需要运算符从一次手工计算讲起我刚开始学编程那阵总觉得“运算符”是个特别虚的概念老师一上来就讲“算术运算符”“赋值运算符”“逻辑运算符”听得头大总觉得这不就是加减乘除吗有什么好讲的。直到后来自己写代码被一个优先级坑到加班才明白当初没搞懂的东西迟早是要还的。先说人话运算符就是告诉程序“拿数据做什么操作”的符号。比如是加法是赋值是比较大小。它本身不产生数据它消费数据、产生新数据。你写的每一行代码几乎都在跟运算符打交道。为了讲清楚“运算符的关系”我习惯先抛出一个最经典的例子let result 2 3 * 4; console.log(result); // 结果是 14不是 20如果从左往右硬算2 3 5再5 * 4 20。但几乎所有语言都会告诉你答案是14因为乘法的优先级高于加法程序先算3 * 4 12再算2 12 14。就是这样一个简单例子牵扯出了三个核心概念运算符的分类一元、二元、三元、运算符的优先级、运算符的结合性。这三个概念是“运算符”这个主题的三根柱子今天我把它们串在一起讲清楚。1.2 按操作数数量分一元、二元、三元到底按什么切操作数是什么就是运算符“作用”的对象。比如a b里a和b就是操作数是运算符。按操作数的个数来分运算符就能切成一元、二元、三元这是最直观、也是面试和考试里最高频的分类方式。一元运算符只接收一个操作数。典型代表有正号和负号--x表示取 x 的相反数自增和自减--i等价于i i 1i--等价于i i - 1逻辑非!!true结果是false把布尔值取反按位取反~对整数的二进制位逐位取反typeof、void、delete在 JavaScript 里也属于一元运算符举个例子let a 5; let b -a; // b -5负号是一元运算符 let c !false; // c true逻辑非 let d a; // a 先自增变成 6再赋值给 d所以 a6, d6这里有一个新手很容易踩的坑a和a虽然都是自增但返回值不一样。a前置自增是先加再返回a后置自增是先返回再加。这在复合表达式里会产生完全不同的结果。二元运算符接收两个操作数这是最常见的一类绝大多数运算符都属于二元类算术运算符、-、*、/、%取余比较运算符、、、、、、!、!逻辑运算符逻辑与、||逻辑或赋值运算符、、-、*、/、%位运算符、|、^、、、比如total price * quantity shipping里面*和各自接收两个操作数完成计算都是二元运算。三元运算符接收三个操作数严格来说编程里常用的三元运算符只有一个就是条件运算符语法是条件 ? 表达式A : 表达式B它的执行逻辑是先判断“条件”是否为真如果为真执行表达式A否则执行表达式B。比如let age 20; let type age 18 ? adult : minor; console.log(type); // adult人话翻译age 18成立就给type赋adult不成立就给type赋minor。它本质上是if...else的简写但因为它是一个“表达式”可以嵌在别的表达式里用这是它比if...else更灵活的地方。理解了一元、二元、三元这个概念再看运算符的时候你就不会觉得它是一个个零散的小点了。你再遇到一个没见过的运算符第一反应是问自己它是几元的它接收几个操作数它对操作数的类型有什么要求这样你就能快速掌握一个新运算符的用法。1.3 运算符家族的大盘点从算术到赋值到比较到逻辑到位运算我整理知识体系有个习惯一定要把一个“家族”的所有成员摆出来看一遍才知道它们之间的关系。运算符也一样虽然每种语言的具体符号有差异但大类基本一致。我把它们在表格里列出来方便你对照复习。类别常见运算符说明典型例子算术运算符 - * / %数学计算%是取余数7 % 3 1赋值运算符 - * / %把右边的值算好赋给左边x 3等价于x x 3比较运算符 ! !比较大小或相等关系返回布尔值5 3结果是true逻辑运算符 || !多个条件的组合判断true false结果是false位运算符 | ^ ~ 对整数的二进制位运算5 3结果是1字符串运算符字符串拼接Hello World条件运算符?:三元根据条件返回两个值之一a b ? a : b一元运算符 -- ! ~ - typeof只操作一个值!true结果是false逗号运算符,依次执行多个表达式返回最后一个(a1, b2, ab)结果是3这里面我想单独强调一下位运算符。虽然日常业务代码里用得不多但它在权限系统、状态位合并、加密算法、图形处理里都是核心工具。比如 Linux 权限里的755本质就是二进制位的组合用位或|可以把多个权限合并用位与可以判断是否有某个权限。const READ 4; // 二进制 100 const WRITE 2; // 二进制 010 const EXEC 1; // 二进制 001 let permission READ | WRITE; // 6二进制 110表示有读和写权限 console.log(permission READ); // 4非0说明有读权限 console.log(permission EXEC); // 0说明没有执行权限逻辑运算符和||也不只是返回布尔值那么简单。在 JavaScript 里和||会返回操作数的原始值而不是强制转成布尔。这就是为什么很多老代码里会出现let name user.name || Anonymous这种写法——当user.name是空值或undefined时||会返回后面那个默认值。这类“运算符背后的语言特性”恰恰是理解一门语言的关键。2. 运算符优先级别让“默认顺序”毁掉你的结果2.1 优先级到底是什么以及为什么不能只靠背优先级解决的是“多个运算符同时出现时先算谁”的问题。它跟小学数学里的“先乘除后加减”是同一个意思但编程里的运算符远比数学符号多所以优先级的层级也更多。我先给你一个核心心法不要把优先级当成一个需要死记硬背的东西而是要把它当成一个“万一不确定就加括号”的提醒。括号()是优先级最高的运算符之一你只要把想先算的表达式用括号包起来就一定能保证执行顺序符合预期。举个例子我见过有人写代码判断“分数在 60 到 80 之间”他这么写let score 75; if (60 score 80) { console.log(及格且不算优秀); }这段代码在 Python 里是对的Python 支持链式比较但在 JavaScript 和很多其他语言里是错的。JS 会先把60 score计算出来结果是true然后拿true去跟80比较true被转成数字11 80是true所以无论 score 是多少这个条件的返回值都是true。正确的写法应该是if (score 60 score 80) { console.log(及格且不算优秀); }这个案例能说明两件事第一不同语言的优先级规则有差异第二如果你想表达“区间判断”最稳妥的方式永远是用把两个比较表达式连起来而不是依赖语言的默认规则。2.2 结合性同样优先级时从左到右还是从右到左只有优先级还不够因为当两个运算符的优先级相同时程序需要额外的规则来决定先算谁。这个规则就叫结合性。结合性分两种左结合从左往右算和右结合从右往左算。说人话就是同一优先级连续出现时程序是从左往右“拿”操作数还是从右往左“拿”操作数。绝大多数运算符都是左结合比如加减法10 - 3 - 2等价于(10 - 3) - 2 5先算左边的减法。但有几个特殊的右结合情况我单独拿出来讲赋值运算符是右结合。a b 5等价于a (b 5)先把5赋给b再把b的值赋给a。如果你不理解右结合就会困惑“凭什么b先被赋值”。记住在赋值表达式链里最右边的赋值最先执行。一元运算符是右结合。!!x等价于!(!x)先取反一次再取反一次抵消回原本的布尔值。幂运算符是右结合在支持**的语言里。2 ** 3 ** 2等价于2 ** (3 ** 2) 2 ** 9 512如果左结合就会变成(2 ** 3) ** 2 8 ** 2 64结果天差地别。别问为什么幂要右结合数学上幂运算本来就是从右往左上标算的程序语言只是忠实还原了这个习惯。结合性这个概念在写长表达式时最容易翻车但在日常开发中最好的策略依然是别把表达式写太长长就拆行拆不开就加括号。2.3 一张能直接抄的优先级速查表下面这张表按照从高到低的优先级排列覆盖最常用的运算符。我把它们分组每组之间是不同级别组内相同级别的运算符按结合性运算。优先级分组运算符说明最高()[].?.括号、下标、属性访问、可选链一元 -- ! ~ - typeof void自增自减、逻辑非、按位取反、正负号算术乘除* / %乘法、除法、取余算术加减 -加法、减法、字符串拼接移位 位左移、右移、无符号右移比较 大小比较相等 ! !相等性判断位与按位与位异或^按位异或位或|按位或逻辑与逻辑与短路逻辑或||逻辑或短路空值合并??左侧为 null/undefined 时取右侧条件三元?:三目运算赋值 - * / % | ^赋值系最低,逗号运算符我强调三件事第一逻辑与的优先级比逻辑或||高。所以a || b c等价于a || (b c)。这两个运算符写在同一行时是最容易产生误读的场景之一建议加括号明确意图。第二三元运算符?:的优先级非常低只比赋值高。如果你要把三元表达式嵌到字符串模板里或者和其他表达式拼接最好用括号包起来let price 100; let tip price 50 ? expensive : cheap; console.log(This item is (price 50 ? expensive : cheap));第三括号永远是终极武器。我在写代码的时候只要表达式超过两个运算符就会习惯性加括号。不是因为我记不住优先级而是因为半年之后你回头看代码括号能让你三秒读懂当时的逻辑而不需要去查优先级表。代码是写给人看的不是写给机器看的。3. 实操把概念变成看得见的代码3.1 JavaScript环境下的最小演示从日拱一卒开始理论讲再多不如直接跑一遍代码。我先用 JavaScript 做一个“最小可运行”的演示你把代码复制到浏览器控制台F12 打开开发者工具切到 Console 标签或者任意 JS 在线运行环境里就能跑。// 一元运算符演示 let n 5; console.log(-n); // -5 console.log(n); // 6n 变成 6 console.log(n); // 6但 n 变成 7这里很容易看错 console.log(n); // 7 console.log(!true); // false // 二元运算符演示 let a 10; let b 3; console.log(a b); // 13 console.log(a - b); // 7 console.log(a * b); // 30 console.log(a / b); // 3.3333... console.log(a % b); // 110 除以 3 余 1 // 三元运算符演示 let score 85; let grade score 60 ? Pass : Fail; console.log(grade); // Pass // 优先级演示 let result1 2 3 * 4; // 14乘法先算 let result2 (2 3) * 4; // 20括号改变顺序 let result3 10 - 2 - 3; // 5左结合 let result4 2 ** 3 ** 2; // 512幂右结合 console.log(result1, result2, result3, result4); // 赋值结合性 let x, y; x y 5; // y 先被赋值 5然后 x 被赋值为 5 console.log(x, y); // 5 5我建议你亲手把这段代码敲一遍然后针对每一行输出都问自己“为什么”。尤其是n和n那两行如果输出把你搞晕了说明你对“后置自增先返回再加”这个规则还没形成肌肉记忆多敲几次就有感觉了。3.2 三目运算符的实战用法不只是 if 的简写很多人学三目运算符之后只会在简单的二选一场景里用它比如上面的及格判断。但实际上三目运算符最有价值的用法是当作一个表达式嵌在别的代码结构里这是if...else做不到的。举个实际例子。假设你要根据用户等级计算折扣等级分三档VIP 打 8 折普通用户打 95 折新用户打 9 折。用if...else写是这样let level vip; let discount; if (level vip) { discount 0.8; } else if (level new) { discount 0.9; } else { discount 0.95; }用三目运算符嵌套写let level vip; let discount level vip ? 0.8 : (level new ? 0.9 : 0.95);嵌套三目表达式的可读性说实话并不好我个人是不太建议超过两层的嵌套。但如果你想用在“返回语句”或者“赋值语句”里它确实比if...else干净利落得多。比如 React 组件里根据状态渲染不同内容三目是标配写法return ( div {isLoggedIn ? UserPanel / : LoginButton /} /div );再强调一次三目运算符是“表达式”不是“语句”。表达式可以被赋值、被拼接、被作为函数参数传入。而if...else是语句不能直接赋值。理解这个区别你就知道什么时候该用三目、什么时候该用if...else了。3.3 多语言横向对比C、Java、PHP、JavaScript 的小差异同一个运算符在不同语言里的行为并不完全一致我挑几个高频坑点对比一下。相等运算符的差异语言的规则是否存在C比较数值不同类型会隐式转换无Java基本类型比值引用类型比地址无用.equals()比值JavaScript比较时会做类型转换1 1为true存在比较值和类型PHP比较时会做类型转换1 1为true存在不转换Python无与之分直接比值无JavaScript 里最经典的坑是0 false返回true false也返回true。如果你不想被隐式转换坑一律用和!只在确定类型时才用。幂运算符的支持Python 用**JavaScript 用**ES2016 起C 和 Java 没有幂运算符用Math.pow()PHP 用**PHP 5.6 起取余运算对负数的处理JavaScript、Java、C 的%结果符号跟随被除数-7 % 3结果是-1Python 的%结果符号跟随除数-7 % 3结果是2这个差异很反直觉但如果是做算法题或者金融计算这个差异能直接导致结果错误。逻辑与短路求值和||在几乎所有语言里都有“短路”特性——左边结果已经能确定整个表达式的值时右边不再执行。但在这个基础上有细微差别比如 Java 里和|作为位运算时不会短路作为逻辑运算时||才短路。而 JavaScript 的和||不但短路还会返回操作数的原值。这些差异虽然细碎但排查深层 bug 时非常关键。PowerShell 的命令链运算符很多人不知道PowerShell 直到 7.0 版本才开始支持和||作为命令链运算符。在 PowerShell 5.1 及更早版本里是不被支持的要实现“上一条命令成功才执行下一条”得用;加上$?判断。这种跨语言的版本差异正是我们在排查报错时最容易忽略的一类问题。你上网搜的用法看到别人写的代码在自己的环境里跑不通往往不是代码错了而是版本或语言环境的差异。4. 容易翻车的 7 个坑与实用排查技巧4.1 等号三兄弟、、的经典混淆这是新手最常见、也最难自查的坑。是赋值是宽松相等是严格相等。在 C 语言里赋值表达式本身有值所以容易写出if (x 5)这种“永远为真”的代码——它把 5 赋给 x然后判断 x 的真假非零即真。在 JavaScript、Java、C# 这类语言里if (x 5)会被编译器警告但在 JavaScript 里不会报错只会给出一个你可能注意不到的 warning。排查时最好的习惯是把常量写在左边写成if (5 x)。这样一来如果你手滑写成了if (5 x)编译器会直接报错因为不能给常量赋值。这个习惯在团队协作里尤其有用。它不会改变代码逻辑但能在代码评审和编译阶段帮你拦截一大批“赋值当比较”的失误。4.2 取余和负数数学直觉不一定是程序直觉取余运算符%在很多语言里的结果跟数学上“取模”不完全一样。程序员常说的“余数”有两条规则一是符号跟随被除数二是结果满足a (a / b) * b a % b。console.log(-7 % 3); // -1 console.log(7 % -3); // 1 console.log(-7 % -3); // -1如果你需要“取余结果永远是正数”的效果可以自己加一步处理((a % b) b) % b。这个技巧在做循环数组索引、哈希散列、日期换算时非常实用。比如数组长度为 5你要往回退 3 格(index - 3 5) % 5就能保证结果落在0到4之间不会出现负数索引。4.3 逻辑运算符的短路求值别把有副作用的代码写在右边和||的短路特性虽然好但也会埋雷。看这段代码function foo() { console.log(foo executed); return true; } let a false; let result a foo(); // 因为 a 是 false整个表达式已经确定为 false console.log(result); // falsefoo executed 不会打印如果你在右侧写了i这种有副作用的代码而左侧恰好是false那么右侧代码根本不会执行i不会增加。这在业务代码里会导致数据不一致。排查方法是如果一个表达式内部包含函数调用或自增自减一定把它单独拆到一行别藏在逻辑表达式里。4.4 连续赋值的结合性陷阱右结合的赋值运算符让a b 5能正常工作但也带来另一个坑如果你本意是给两个变量赋两个不同的值写成a b 5不是问题但写a b c 5时必须清楚这行代码会让a、b、c三个变量全部变成5。这在初始化多个变量时很常见没问题。但有些人误以为a b c 1是“a 和 b 都赋值为 c1”这其实没问题它确实如此。真正会翻车的是对象或引用类型。在 JavaScript 里let a b {}会让a和b指向同一个对象修改a的某个属性b也会跟着变。这不是运算符的问题而是引用类型的特性。排查这类 bug 时先画出变量和对象之间的引用关系会比凭空读代码快得多。4.5 混合类型比较隐式转换的连锁反应JavaScript 里5 3结果是字符串53因为遇到字符串就优先做拼接但5 - 3结果是数字2因为-只支持数学运算字符串会被转成数字。同一个符号既做加法又做拼接是很多人调试半天的根源。console.log(5 3); // 53 console.log(5 - 3); // 2 console.log(5 * 2); // 10 console.log(5 3 2); // 532从左往右拼接 console.log(5 3 2); // 82先数字加再拼接处理这类问题的原则很简单不要依赖隐式转换显式转换写清楚。想拼接字符串就写${5}${3}${2}想算数字就写Number(5) 3。代码里多写几个转换函数不会让性能变差但能让你少掉很多头发。4.6 括号万能法三个可复用的表达式规范我总结了三条表达式书写规范可以显著降低优先级带来的 bug表达式中超过两个不同优先级的运算符就加括号。三元表达式、逻辑表达式、赋值表达式混合使用时每个独立语义都用括号包住。任何“自己不确定”的表达式查文档不如直接加括号括号不会影响性能。举一个实战例子我想写“如果分数存在且大于等于 60就显示及格”let score null; let message score score 60 ? Pass : Fail;这个表达式有个隐藏 bug当score是null时score本身被当作false表达式返回Fail看着没问题。但当score是0时0 ...返回0因为短路返回原值不是布尔值也不是字符串赋值给message就会出现诡异的“message 是数字 0”的情况。更好写法是let message score ! null score 60 ? Pass : Fail;这个例子说明表达式的语义一定要“显式”别依赖隐式的真假转换来省字。4.7 排查工具与方法把优先级问题可视化出了优先级相关的 bug光靠肉眼读代码不一定能找出来我常用的三板斧第一招逐步拆分法。把复杂表达式拆成多个中间变量每一步都打印到控制台。比如let step1 2 3; let step2 step1 * 4; let step3 step2 20;这样一步步看到哪一步结果不对就锁定是哪个运算符的问题。第二招括号对比法。给表达式的每个二元运算都加上括号看结果是否变化。如果加了括号之后结果变了说明原来的优先级理解有误顺藤摸瓜就能找到问题。第三招AST 或在线解析工具。像 JavaScript 可以用astexplorer.net看解析器是怎么理解你的表达式的它会用树状结构画出优先级非常直观。Python 可以用ast模块打印语法树C 系可以用 Compiler Explorer 看汇编。这个方法适合排查极其难缠的表达式优先级问题。5. 从项目复盘到编程习惯运算符学习的一点进阶体会运算符这块内容在学校里可能只是一节课、一章作业但实际工作中每次代码评审都会遇到。我给你分享几个我踩过坑之后养成的习惯希望能让你少走弯路。第一写代码时默认加括号不加括号时问自己“这行真的没有歧义吗”我见过太多生产事故的根源就是少了一对括号。程序不会替你猜意图它只会机械地按规则执行。你的意图通过括号表达代码才真正符合你的预期。第二学习新语言时运算符差异要单独立一个“快查表”。C、Java、JavaScript、Python、PHP、PowerShell 我都用过每种语言在运算符上都有细微差别。靠脑子记不靠谱用笔记或者文档记录遇到这个语言再调出来查。比如我上面列出的“取余对负数符号的处理”和“相等运算符的类型转换规则”每一列都是一个真实的坑。第三表达式的时间复杂度不取决于长度但表达式的可读性取决于长度。我见过有人用一整行三元、逻辑运算符、位运算拼出一个复杂的权限判断代码评审时大家看了半天都说不清它到底在干嘛。后来拆成 5 行逻辑一目了然。写代码的重点永远是“让下一个人容易读懂”而不是“证明你能把逻辑写在一行里”。第四学会区分“表达式”和“语句”是运算符之上的一个元能力。表达式有值语句没有值。赋值、加法、函数调用都是表达式if...else、for、while是语句。三目运算符之所以强大因为它硬生生把“条件判断”变成了表达式。有了这层认知你再看任何运算符都能建立更准确的直觉。第五优先级问题在代码评审中是最容易忽略但最值得查的。我 review 同事代码时只要看到一行里混了三种以上运算符节奏就会放慢逐个确认。多数时候不是同事写错了而是我也需要对照优先级表确认一遍。这种小心不是不信任彼此而是我知道“运算符优先级”这个知识点的反直觉程度远远超过它看起来的样子。