☰
JavaScript运算符分类与优先级:从单目、双目到三目的实战解析
2026/10/6 4:19:25 网站建设 项目流程

Javascript 里所谓的单目运算符、双目运算符、多目运算符,乍一听像是教材里才会出现的术语,但只要你写过一行 JS 代码,其实就已经在用它们了。一个表达式由操作数和运算符组成,运算符需要“管”几个操作数,就决定了它是单目还是双目。我做了十几年前端,在代码评审里见过太多人被一行看似简单的表达式问住:++i和i++到底怎么算?a && b || c到底先算谁?三目嵌套三目为什么每次都被同事喷?归根结底,就是这三类运算符没吃透。

这篇文章不是教科书式的罗列,我把日常开发里真正用得上的运算符用法、容易踩的坑、以及排查问题的思路全部拆开讲一遍。适合刚入门想打好基础的新人,也适合写了几年 JS 但老在某些边缘 case 上翻车的同学。看完你至少能回答一个问题:运算符的分类不是考试知识点,而是你读懂别人代码、写出更稳代码的底层工具。

1. 先把概念理清楚:运算符、操作数和“目”的关系

1.1 什么是操作数,什么是运算符

先做一个最简单的区分。1 + 2这个表达式里,1和2是操作数,+是运算符。操作数就是被运算的东西,运算符就是定义“怎么算”的规则。这个概念看着简单,但很多人一碰到复杂表达式就糊涂,原因是他没有下意识地把表达式拆成“操作数 + 运算符 + 操作数”的组合。

举个例子,a + b * c怎么拆?按运算符的优先级,b * c先结合,这里的*需要两个操作数b和c,所以它是双目;+也需要两个操作数a和(b * c),它也是双目。拆完之后你就知道,优先级本质上是在决定“哪些操作数先成为另一个运算符的操作数”。

1.2 按操作数数量分类的完整逻辑

  • 单目运算符(一元运算符):只需要一个操作数,比如!true、-x、++i。
  • 双目运算符(二元运算符):需要两个操作数,比如a + b、x === y、p && q。
  • 多目运算符(三元运算符):需要三个操作数,Javascript 里最常见也几乎是唯一的就是条件表达式cond ? expr1 : expr2。

这里要提醒一句:很多人把“三目运算符”直接等同于? :,严格说“三目”是按操作数数量分类的统称,? :只是 Javascript 里的一个具体三目运算符。但因为语言里只有这一个三目的,所以大家日常口语里“三目运算符”和“条件表达式”就划等号了。这个说法问题不大,但心里要清楚分类的维度是“操作数个数”,不是“运算符名字”。

2. 单目运算符:一个操作数也能玩出花来

2.1 单目运算符全家福

我先把 Javascript 里常用到的单目运算符整理成一张表,你对照着看,后面再逐个拆:

运算符名称作用典型示例
+一元正号转数字、数值符号+ "123"得到数字 123
-一元负号取负、转数字- "42"得到 -42
++自增操作数加 1i++或++i
--自减操作数减 1i--或--i
!逻辑非取布尔值的反!true得到 false
~按位非按位取反~5得到 -6
typeof类型检测返回操作数类型字符串typeof x
void空值运算返回 undefinedvoid 0
delete删除属性删除对象属性或数组元素delete obj.name
await等待等待 Promise 完成await promise

这张表里,+和-是同一个符号,但它作为单目和双目的时候,行为完全不一样。很多人就是在这里开始懵的。+做双目是加法或字符串拼接,做单目是把操作数往数字方向转换。

2.2 一元正号+和一元负号-:除了符号,它们还能做类型转换

一元正号最实用的场景是把字符串转成数字。比如后端接口返回了一个"2024"字符串,你想参与数值运算,直接+"2024"就行。它和Number("2024")效果一样,但写法更短。我经常在需要快速解析数据的时候用它。

const str = "12.5"; const num = +str; // 12.5,number 类型 const neg = -str; // -12.5 console.log(typeof num); // "number"

要注意的是,如果字符串不能完整转成数字,结果就是NaN。+"abc"得到NaN,+null得到 0,+undefined得到NaN,+""得到 0。这几个边界值我在代码评审里看到过无数回,特别是+""等于 0 这件事,经常让一些判断逻辑产生匪夷所思的结果。

一元负号除了取反,同样也会触发类型转换。-"5"是 -5,不是字符串。所以你要是拿-"5" + 1去算,结果是 -4,因为-"5"已经被转成数字了。

2.3i++与++i:前后置运算的差别到底在哪

这个知识点的考察频率极高,但实际开发里重复踩坑的频率也不低。区别就一句话:i++先返回原值再加 1,++i先加 1 再返回新值。

let a = 1; const b = a++; // b 是 1,a 变成 2 let c = 1; const d = ++c; // c 变成 2,d 是 2

这个差异在单独一行i++的时候没有区别,一旦你把自增运算嵌进表达式里,结果就不一样了。最常见的坑是循环里写arr[i++] = i这种自以为很聪明的写法,行为诡异不说,可读性还差。我的建议很简单:自增自减永远单独成行,不要嵌进表达式。这不是学院派的洁癖,是长期维护代码的人用血泪换来的规矩。你三个月后回来看别人的代码,看到return i++,你真的得愣一下才知道返回的是几。

还有一个隐藏坑:++和--只能用于变量或对象属性,不能用于数字字面量。5++直接报语法错误,因为数字字面量不允许赋值。这个规则底层逻辑是自增自减本质是“赋值给自身”,字面量没有存储位置,自然不能自增。

2.4typeof、void、delete、await:几个容易忽略的单目

typeof不是函数,它后面可以不带括号。typeof x和typeof(x)都合法,但很多人误以为它是函数。它返回的结果是字符串:"string"、"number"、"boolean"、"undefined"、"object"、"function"、"symbol"、"bigint"。注意没有"null",typeof null返回"object",这是语言历史遗留 bug,后面我会专门讲。

void这个运算符平时用得少,但有一个场景非常经典:在a标签的href里写javascript:void(0),用来让点击不触发页面跳转。它的作用是计算一个表达式然后返回undefined。void 0就是获取undefined的一种常见写法,因为在某些旧环境里undefined可能被局部变量覆盖,void 0永远安全。

delete用于删除对象的属性,成功或失败返回布尔值。但它删除不了var声明的变量,也删除不了不可配置的属性。删除数组元素时,它不会改变数组length,只会把那个位置变成空槽。这个行为经常让人误以为数组长度缩短了,实际上没有,遍历的时候会出现undefined。

await得在async函数里用,它作为单目运算符的语义是“右边的 Promise 完成后再继续”,本质上是一个暂停点。

2.5 单目运算的实战心得与坑

我在实际项目里总结出几条关于单目运算符的经验:

第一个,~和indexOf的组合。在老式 ES5 代码里,判断一个数组或字符串是否包含某个值时,大家会写str.indexOf("x") >= 0,更“老练”的写法是~str.indexOf("x")。原理是indexOf找不到时返回 -1,~-1是 0,0 是 falsy;找到时返回 >=0 的数,取反之后变成负数或 1,都是 truthy。这个技巧现在用includes完全可以替代,但老代码里到处都是,看懂它是基本素养。

第二个,!和!!的用法。!!value是“强制转布尔”的简写。第一个!把值转成布尔并取反,第二个!再取反回来,结果就是Boolean(value)。我建议代码里尽量用Boolean(value)而不是!!value,因为!!虽然简洁,但对新手阅读很不友好,评审时容易被追问。当然,你自己看老代码得能看懂。

第三个,别滥用一元正号做类型转换。虽然+"123"很香,但在可读性要求高的业务代码里,Number("123")更直白。我的原则是:自己写的工具函数可以用简写,团队共用的业务代码用语义明确的写法。

3. 双目运算符:日常写代码的大头

3.1 算术运算符:+的特殊之处

双目运算符是 Javascript 里最庞大的家族,日常写代码 90% 都在跟它们打交道。先看算术类:+、-、*、/、%、**。

**是 ES2016 加入的幂运算符,2 ** 10等于 1024,不用再写Math.pow(2, 10)。%是取余,注意它是取余不是取模,结果的正负跟被除数一致。-7 % 3在 Javascript 里是 -1,不是 2。这个数学细节在日历计算、分页算法里很容易踩。

+的特殊之处在于它是双重身份:两边都是数字就是加法,只要有一边是字符串,就变成字符串拼接。这就是著名的“类型转换陷阱”:

console.log(1 + 2 + "3"); // "33",先 1+2=3,再 3+"3"="33" console.log("1" + 2 + 3); // "123",从左到右一直拼接 console.log(1 + "2" + 3); // "123"

-、*、/没有这个问题,它们只做数值运算,会把操作数强制转数字。所以"12" - 3结果是 9,"12" - "3"也是 9。这类隐式转换让人又爱又恨,我每次看到后端接口返回的数字被 JSON 解析成字符串,然后跟前端数字直接===比较失败的时候,都建议团队规范:所有可能来自外部的数字,进来第一件事就做一次显式Number()转换。

3.2 比较运算符:宽松相等与严格相等

比较运算符分两组,一组是关系比较>、<、>=、<=,一组是相等比较==、===、!=、!==。

关系比较的转换规则比较隐蔽。当两边都是字符串时,会按字典序比较;一边是字符串一边是数字时,字符串转数字再比较。这里有个经典坑:"10" < "9"是 true,因为字符串比较时先比第一位字符,'1'比'9'小。你要是想按数值比较,就得手动转数字。

==和===的核心区别:==比较时允许类型转换,===要求类型和值都相等。我这些年看过的团队规范几乎清一色要求禁用==,因为宽松相等在复杂数据类型比较时行为非常反直觉:

null == undefined // true null === undefined // false "" == 0 // true "0" == false // true

这些结果虽然背后有完整的规范依据,但业务代码里没人愿意每次都想一遍转换规则。我的建议和绝大多数团队一致:能写===就写===,把==留给极少数明确需要隐式转换的场景。

3.3 逻辑运算符:短路求值才是灵魂

&&、||、??这三个逻辑运算符的行为,本质上不是“返回布尔值”,而是短路求值。

&&从左到右,遇到 falsy 就停下来返回这个值,否则一直算到最后一个。||从左到右,遇到 truthy 就停下来返回这个值,否则返回最后一个。??是专门用来处理 nullish 的,只要遇到null或undefined就返回右侧值。

这三者的区别在实际业务里太重要了:

const name = user.name || "匿名用户"; // 空字符串也会被兜底 const nick = user.nick ?? "匿名用户"; // 只有 null/undefined 才兜底 const maybe = a && b && c; // 某个 falsy 就返回它

我见过一个线上事故:用户把昵称设置成了空字符串,结果页面显示“匿名用户”。原因就是代码用了||,而空字符串是 falsy。改成??之后,空字符串就能正常显示。这种问题在“用户输入允许为空字符串”的场景里特别常见,所以??出现之后,取默认值的第一选择应该是它,而不是||。

&&还经常被用来做条件渲染和函数调用,比如isAdmin && handleAdmin()。这种做法简洁,但可读性确实不如if语句。我的态度是:&&做表达式求值没问题,做控制流要谨慎,team 里有人看不习惯就统一改用if。

3.4 位运算符:平时用得少但非常好用

位运算符有&、|、^、<<、>>、>>>,它们把操作数先转成 32 位整数再逐位运算。业务代码里确实少,但有几个场景非常实用。

第一个是权限标志位。定义权限常量,用二进制位表示:

const READ = 1; // 001 const WRITE = 2; // 010 const EXEC = 4; // 100 let perm = 0; perm |= READ; // 追加读权限 perm |= WRITE; // 追加写权限 const canRead = (perm & READ) !== 0; // 检查权限

这个方案比一堆布尔变量清爽得多,而且扩展方便。第二个是~~技巧,~~是连续两次按位取反,效果是快速取整,~~3.9是 3。但要注意它不是四舍五入,而是截断,且只能处理 32 位范围内的数字。大数或负数边界不推荐用,老老实实用Math.trunc。

第三个是<<用来做快速乘除 2 的幂,x << 1相当于x * 2。这个优化在现代 JS 引擎里意义已经不大了,可读性还差,我基本不在业务代码里写。

3.5 赋值运算符与逗号运算符

赋值运算符=、+=、-=、*=、/=、%=、**=这些没什么好说的,唯一要注意的是赋值表达式本身也有值,而且结合方向是从右到左。a = b = 1会先把 1 赋给 b,再把表达式b = 1的值 1 赋给 a。这个特性可以链式赋值,但也会在某些边界情况下让人困惑。

逗号运算符,可能是最容易被忽视的双目运算符。它从左到右逐个计算每个表达式,返回最后一个表达式的值:

let x = (1, 2, 3); // x 是 3,前两个表达式被算了但结果丢弃

逗号运算常见于for循环的多变量更新,或者一些函数式写法。但它极其容易被误当成“数组里的逗号”或“参数列表里的逗号”,所以我强烈建议业务代码里不要用逗号运算符,要用就加括号,否则团队里容易闹出乌龙。

4. 多目运算符:平时说的三目条件表达式

4.1 基本写法

Javascript 的三目运算符只有? :这一种,语法是condition ? expr1 : expr2。如果 condition 为 truthy,整体表达式返回 expr1,否则返回 expr2。它是个表达式,意味着可以出现在赋值、参数、return 语句等几乎所有位置。

const status = isLogin ? "已登录" : "未登录"; const tip = count > 0 ? `剩余 ${count} 个` : "没有了";

三目的核心价值是在表达式语境里做条件选择。比如你要塞进模板字符串里,或者作为函数参数传进去,用if...else写不了那么干净,三目就非常顺手。这是它存在的主要理由。

4.2 嵌套三目:为什么我不建议这么写

嵌套三目就是cond1 ? a : (cond2 ? b : c)这种写法。从语法上它完全合法,但可读性直接崩塌。我举一个反例:

const label = type === "admin" ? "管理员" : type === "editor" ? "编辑" : "访客";

这行代码短,但读起来要花好几秒钟才理清层级。更麻烦的是,一旦后续要加第四个分支,要么继续嵌套更深,要么重构,几乎走向死胡同。我在评审里遇到嵌套三目,一般会要求改成switch或Map映射。

有一种例外:单个嵌套且层级清晰时,有些团队可以接受,但一定要加括号显式表达嵌套关系。括号除了消除歧义,更重要是告诉读者“这里是有意嵌套的”。不加括号的嵌套,和加了括号的嵌套,语义完全一样,但阅读成本差很远。

const label = type === "admin" ? "管理员" : (type === "editor" ? "编辑" : "访客");

不过即便如此,我个人的默认原则仍然是不嵌套。用Map更直白:

const labelMap = { admin: "管理员", editor: "编辑" }; const label = labelMap[type] ?? "访客";

4.3 三目替代方案与实际场景

最常用的替代方案有三个:if...else语句、Map查找、??兜底。三目适合“两个分支都是短表达式”的场景;分支较长或带语句逻辑时,改用if;分支多且都是取值场景,用Map。

还有一个常见纠结点:三目能不能把函数调用写在分支里?可以,但很多团队规范禁止有“副作用”的三目,因为三目强调的是“取值”,如果你在里面做赋值或者调用有副作用的函数,代码行为会变得难以追踪。我自己就踩过坑:在 React 的 JSX 里用三目条件渲染时,分支里写了带副作用的函数调用,结果每次渲染都触发,引发性能问题。三目分支尽量放纯表达式,有副作用的逻辑拆出去。

5. 优先级和结合性:真正决定表达式顺序的规则

5.1 一张表看清常见优先级

运算符优先级决定了先算谁。我挑业务代码里最常遇到的,从高到低排一张简化表:

优先级运算符说明
最高()[].分组和成员访问
高++--!~typeofvoid单目运算
中高**幂运算
中*/%乘除取余
中低+-加减
低<<>>>>>位移
更低<<=>>=关系比较
很低=====!=!==相等比较
极低&^``
更低&&||??逻辑运算
最低? :三目条件
最低=赋值赋值运算

这张表不是完整规范,但覆盖了绝大多数面试和审查场景。我特别想强调两个容易出错的点:

第一个,位运算符优先级低于相等比较,所以a & b === c不是你想的那样。它会被解析成a & (b === c),因为===优先。要判断位运算结果,必须写(a & b) === c。这个坑我见过不止一次。

第二个,??和&&、||混用默认是语法错误。a ?? b || c会直接报错,因为规范禁止 nullish 合并运算符与逻辑运算符无括号混用。原因就是这类表达式语义太容易混淆。想混用必须加括号。

5.2 结合性:从左到右还是从右到左

优先级解决“先算谁”,结合性解决“同一优先级时从哪边开始”。Javascript 里,算术运算、逻辑运算基本都是左结合,也就是说从左往右算;只有赋值运算、三目运算、幂运算是右结合。

右结合的例子:

a = b = c; // 等价于 a = (b = c),先给 b 赋值再给 a 赋值 cond ? x : cond2 ? y : z // 等价于 cond ? x : (cond2 ? y : z) 2 ** 3 ** 2 // 等价于 2 ** (3 ** 2),结果是 512,不是 64

幂运算右结合这件事很容易被忽略,2 ** 3 ** 2绝大多数人凭直觉认为是 64,实际是 512。这再次说明一个道理:不要依赖记忆去理解复杂表达式,加括号永远是对的。

5.3 我的代码规范:优先级永远让位给可读性

这些规则我在项目里总结成一句话:不要在团队代码里考验别人对优先级表的记忆。

  • 混用逻辑运算符时,用括号标明意图,(a || b) && c比a || b && c好一万倍。
  • 位运算符号必须加括号,除非单目运算。
  • 三目嵌套必须加括号,或者干脆别嵌套。
  • 幂运算和负数一起用的时候要小心,-2 ** 2是语法错误,得写(-2) ** 2或者-(2 ** 2),语义不同。

优先级规则是用来让你理解别人代码的,不是用来让你写出难懂代码的。别把“我懂优先级”当成“我可以不写括号”的资本。

6. 排查运算符问题的实战经验

6.1typeof null === "object"这个老梗

这个坑来自 Javascript 诞生初期的实现:null在底层被表示为一个全 0 的对象指针,所以typeof null返回"object"。这个行为从来没改过,也不打算改,因为改了就破坏现有代码。

实际经验是:判断是否为 null 要用value === null,不要依赖 typeof。要区分“对象里的空”和“真的没有”,可以配合??和== null来处理。value == null这个写法比较特殊,它同时覆盖null和undefined,因为宽松相等的规则规定null == undefined是 true。在“判断值为空”的场景,这是少数我觉得==比===合适的用法,但为了降低理解成本,更多的团队会写value === null || value === undefined。

6.2a + b是加法还是字符串拼接

这是排查经历里最常见的一个类型问题。两个变量相加,你以为在做加法,结果出来一个字符串,或者反过来。原因就是+的双重身份。

我的排查经验很直接:先把两端类型打出来看。命令行里跑typeof a, typeof b,立刻就能定位。如果一个是数字一个是字符串,那结果必然是拼接。业务代码里要避免这种不确定性,最好统一先把输入转成预期类型:

const total = Number(item.price) + Number(item.count) * Number(item.unitPrice);

别嫌麻烦。我曾经排查过一个问题,列表合计金额时多了一位数,最后发现是某个字段被接口返回成了字符串,price + amount直接做了拼接。从那以后,涉及金额计算的入口全部强制显式转换。

6.3??和||别再傻傻分不清楚

这两个运算符都能提供默认值,但触发条件不同:||在左侧为任何 falsy 值时都触发,??只在左侧为null或undefined时触发。

0 || "default"; // "default" 0 ?? "default"; // 0 "" || "default"; // "default" "" ?? "default"; // "" false || "default"; // "default" false ?? "default"; // false

如果你处理的是数字 0、空字符串、false这些合法值,用||就会把它们吞掉。我的经验法则是:只要默认值本身不是布尔或数值兜底,一律优先??。精确表达式里也要小心,??的优先级略高于||,但两边混用会语法报错,所以一旦出现多个默认值逻辑,括号必须安排上。

6.4 定位运算符问题的方法论

遇到运算符相关的 bug,我的排查顺序一般是这样的:

第一,把复杂表达式拆开,一行只保留一个运算,看每一步结果是否符合预期。很多人不看拆解结果直接猜,效率极低。

第二,用括号把你觉得的“意图”显式写出来,然后对比结果。比如你觉得a || b && c应该是(a || b) && c,写出来跑一下,立刻发现和原表达式结果不同,就知道自己记错了优先级。

第三,用console.log或者浏览器调试器的断点,在表达式前后打印操作数的类型和值。这种问题 90% 出在类型上,打印一次类型,问题就暴露了。

第四,遇到NaN相关的结果,先检查是不是有某个操作数转数字失败了。NaN有个特点:NaN === NaN是 false,判断要用Number.isNaN(value)。

运算符这个主题,写到最后其实就一句话:理解分类只是起点,真正的能力是能准确地预测代码的行为。你掌握了单目、双目、多目的区别,掌握了优先级和隐式转换的规则,就能在别人还在调试的时候,一眼看出问题出在哪个运算符上。我个人在实际操作里最大的体会是,与其记住所有边界情况,不如养成加括号、用严格相等、对输入做显式转换的习惯——这些习惯带来的收益,远比背十遍规则表更实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询