☰
JavaScript运算符全解析:从隐式转换到位运算与避坑指南
2026/10/9 5:19:17 网站建设 项目流程

我在后台系统改一处权限判断时,被新同事反问得很不舒服:“JavaScript运算符不是入门第一课吗?还有什么好讲的?”我把线上事故清单打开给他看:库存数字被隐式转换加成了字符串、默认值用逻辑或结果把 0 和空字符串直接覆盖、位运算缺括号导致用户权限位被误判……他看完之后默默回去把“JS基础”那一栏重新刷了一遍。

这篇文章不是临时起意,而是我在 HoRain 云技术社区做公开分享的整理底稿,主题就挂在标题下面:JavaScript运算符全解析,从入门到精通。我想把它写成这样一份东西:新手能拿它当查漏补缺的索引,已经写了几年代码的人,也能在好几段里找到自己当年踩坑的影子。

可以先给一个不太严谨但很形象的定义:变量和值像面粉和水,运算符是揉面的手法。同样的原料,有人揉出能拉丝的面团,有人揉出一盆死面。在 JavaScript 里,运算符是语法最基础、也最容易“以为会了”的一部分,很多线上 bug 的根源,写到最后都是一行运算符表达式。

1. 从认知地图开始:JavaScript运算符到底有哪些

1.1 运算符的本质不是符号,而是运算协议

很多教程会把运算符讲成“一种符号”,这个说法会把初学者带偏。更准确的理解是:运算符是一套规则协议,它决定了两件事,一是怎么取出操作数的值,二是按什么规则产出新值,必要时还要做类型转换、触发副作用。

比如“+”号,它在 JavaScript 里不是单纯的数学加号。两个数字操作数的时候做加法,只要有一个操作数是字符串,就倾向于做字符串拼接。这条规则从一个层面解释了为什么1 + "2"会得到"12"而不是3。如果只是把“+”当成“加法符号”,第一次碰到这种输出就会懵。

从另一个层面看,运算符也是表达式求值顺序的锚点。为什么(a + b) * c和a + b * c的结果不同?因为运算符优先级决定了哪些先算。为什么a = b = c能一次给多个变量赋值?因为赋值运算符是右结合。这些都不是靠死记硬背能长期记住的,而是要在写代码、看报错、review 别人代码的过程里反复验证。

我在带团队的时候常说一句话:如果你能在一个报错信息里迅速判断“这是隐式类型转换导致的,还是优先级导致的,还是短路求值导致的”,你对 JavaScript 运算符的理解就已经超过一半的人了。

1.2 一张速查表看懂运算符家族

JavaScript 的运算符比很多人以为的要多得多。下面这张表不追求覆盖 ECMA-262 里每一个冷门条目,而是把日常开发里真正会频繁遇到的类别先串起来,每个类别后面都附一个真实落点。

类别常见运算符最常见的应用场景
算术运算符+ - * / % ** ++ --数值计算、累加器、取余分页、幂运算
赋值运算符= += -= *= /= %= **= <<= >>= >>>= &= ^= |=变量更新、状态叠加
比较运算符== != === !== > < >= <=条件判断、排序、边界比较
逻辑运算符&& || !短路取值、条件渲染、判空
位运算符& | ^ ~ << >> >>>权限位、状态标志、二进制处理
三元运算符? :简单二选一赋值
空值合并与可选链?? ?.安全读取深层属性、默认值
类型相关运算符typeof instanceof in delete void类型判断、属性检测、清理属性
逗号运算符,for 循环、精简表达式
下标与调用[] ()属性访问、函数调用

这里要特别说一句:运算符“入门”容易,是因为大多数开发者日常只用算术、比较、逻辑、赋值四类;“精通”难,是因为隐式转换、优先级、结合性、返回值类型这些隐藏规则,经常联合起来制造诡异问题。

2. 算术运算符与赋值运算符:从日常计算到隐式转换陷阱

2.1 “+”号的双重身份:加法还是拼接

我在面试里经常让候选人现场写出下面几个表达式的结果,能全对的并不多:

console.log("1" + 2); // "12" console.log(1 + "2"); // "12" console.log(1 + 2 + "3"); // "33" console.log("1" + 2 + 3); // "123" console.log("5" - 2); // 3 console.log("5" * "2"); // 10

前四个看着绕,其实规律很简单:+只要遇到字符串,就把另一个操作数转成字符串做拼接。但注意执行顺序是从左到右,所以1 + 2 + "3"先把 1 和 2 相加得到 3,再与"3"拼接成"33"。而"1" + 2 + 3里第一个操作数就是字符串,后面全部跟着拼接,得到"123"。

后两个才是真正值得警惕的:-、*、/、%这些运算符没有字符串语义,所以 JavaScript 会尝试把字符串转换成数字。"5" - 2就成了数字减法,结果是3。这种隐式转换在业务代码里非常危险,尤其是从表单、接口数据里拿到“看起来像数字”的字符串时。

我在实际项目中遇到最多的场景是金额计算。接口返回的是字符串"19.90",前端直接"19.90" + 0.1,结果不是19.9,而是"19.900.1"。筛了好半天,最后发现是加号拼接。

经验:需要数值相加时,先显式转换,再相加。不要依赖隐式转换。

2.2 一元正号与数值转换的其他姿势

除了Number(),还有一个很容易被忽略的转换工具:一元正号+。它放在操作数前面,会强制把操作数转成数字。

const price = "19.90"; const total = +price + 0.1; console.log(total); // 19.999999999999996 (浮点精度问题,后面再说)

一元负号-也一样,但它还会改变符号。写代码时看到+str这种写法,不要觉得奇怪,它是非常常见的数字转换技巧。不过我建议在团队里统一风格:如果是核心逻辑,写Number(str)可比+str好读得多。

2.3 自增自减、取余、指数运算里的边界情况

自增++和自减--是历史悠久的运算符,规则不复杂:前置版本先改变变量值,再返回新值;后置版本先返回旧值,再改变变量值。

let i = 1; console.log(i++); // 1 console.log(i); // 2 let j = 1; console.log(++j); // 2 console.log(j); // 2

这个差异在 for 循环里影响不大,但在同一个表达式里多次修改同一个变量时,就成了灾难。比如let k = 1; const result = k++ + k++;各浏览器的结果一致,但人脑读起来非常费劲。我在代码评审里遇到这类写法,只有一个处理原则:拆开,别秀。

取余运算%也藏着一个分歧点。JavaScript 里-5 % 2的结果是-1,而数学上余数更接近1。很多从 Python 或数学场景转过来的开发者会在这里栽跟头。如果需要“总是非负”的余数,可以自己加一个校正:

const mod = (n, m) => ((n % m) + m) % m; console.log(mod(-5, 2)); // 1

指数运算符**是 ES2016 加入的,优先级很高,而且是右结合:

console.log(2 ** 3 ** 2); // 512,因为先算 3 ** 2 = 9,再算 2 ** 9

更麻烦的是,一元负号和指数运算符放一起时,直接写-2 ** 2是语法错误,必须写成(-2) ** 2。这个细节看似冷门,但在配置计算、图形代码里偶尔会出现。

2.4 浮点精度:0.1 加 0.2 不等于 0.3

讲算术运算符,不可能绕过浮点精度。0.1 + 0.2在 JavaScript 里得到0.30000000000000004,原因是二进制浮点数无法精确表示某些十进制小数。

实际工程里处理方式一般有三种:

  1. 金额用整数最小单位计算,比如“分”,而不是“元”;
  2. 显示层用toFixed(2)保留两位小数;
  3. 需要高精度计算时使用专门库。

我之前在某项目中做过一套优惠计算,最初直接用浮点累加,用户下单金额偶尔多一分钱或少一分钱。后来把所有金额统一转成“分”整数再计算,问题彻底消失。

2.5 赋值运算符的链式与解构赋值

赋值运算符=本身也有返回值,那就是右侧表达式的结果。所以可以写a = b = 5,含义是把5赋给b,再把b的值赋给a。结合右结合规则,这个表达式是从右往左执行的。

ES6 之后我更推荐解构赋值,它比链式赋值可读性好得多:

const [first, second] = [1, 2]; const { name, age } = user;

解构本质上也是“从右侧对象或数组里取出值,赋给左侧变量”,并不是新的运算符,但它和赋值运算符配合非常紧密。需要注意的是,如果右侧是null或undefined会直接报错,所以解构前最好做好判空。

ES2021 又引入了逻辑赋值运算符&&= ||= ??=,它们把逻辑运算符和赋值结合起来:

a ||= "default"; // 等价于 a || (a = "default") b &&= other; // b 为真时才把 other 赋给 b c ??= "fallback"; // c 为 null/undefined 时才赋默认值

这种写法简洁,但在团队里如果不统一,阅读成本会上升。我一般只在初始化场景里用。

3. 比较运算符:=== 和 == 到底差在哪里

3.1 宽松等于与严格相等的本质差异

这是 JavaScript 里最容易产生争议的内容之一。==会做隐式类型转换,===要求类型和值都相等。

先看几个非常容易让人产生误解的例子:

console.log(null == undefined); // true console.log(null === undefined); // false console.log(null == 0); // false console.log("" == 0); // true console.log("" == false); // true console.log([] == false); // true console.log([] == 0); // true console.log([1] == 1); // true

null == 0是 false 这行特别反直觉。从运行机制看,==有一个特殊的规则:null和undefined彼此相等,而且它们不会跟其他任何值做隐式转换比较。所以null == 0为 false,undefined == false也为 false。

我的编码规范很简单:默认全部使用===和!==。唯一允许用== null的场景,是同时判断null和undefined的时候:

if (value == null) { // 同时覆盖 value === null && value === undefined }

这个技巧非常实用,而且避免了typeof value === "undefined" || value === null这种冗余表达。

3.2 关系比较和字符串排序的隐藏规则

关系运算符>、<、>=、<=同样有类型转换问题。两个数字比较没问题;数字和字符串比较时,字符串会转成数字;两个字符串比较时,则会逐字符按 Unicode 编码比较:

console.log("2" > "10"); // true,因为 "2" 的编码比 "1" 大 console.log(2 > "10"); // false,因为 "10" 被转成了数字 10 console.log("b" > "a"); // true

正因为字符串比较是逐字符的,才导致一个著名现象:Array.prototype.sort()默认会把所有元素转成字符串再排序。

const arr = [10, 9, 100, 2]; arr.sort(); console.log(arr); // [10, 100, 2, 9]

想要真正按数字排序,必须传比较函数:

arr.sort((a, b) => a - b); console.log(arr); // [2, 9, 10, 100]

对中文排序不要直接用localeCompare之外的方式。默认字符串排序是按 Unicode 编码来的,中文的编码顺序和拼音顺序并不一致,直接用 sort 排中文等于随机排。

3.3 比较运算符与数组/对象的隐式转换

当操作数里有对象或数组时,比较运算符会先尝试把对象转成原始值。这个转换过程一般会调用对象的valueOf(),如果返回的不是原始值,再尝试toString(),所以[] == 0为 true,因为空数组先转成空字符串"",再转成数字0。

这个机制相当底层,但了解它能帮你调试很多“怪异”结果。我处理此类问题的原则是:不要写依赖隐式转换的比较表达式。遇到数组和对象比较,先明确取的是什么值,再比较,比如arr.length === 0、obj.id === targetId。

4. 逻辑运算符与短路求值:不只是 true 和 false

4.1 逻辑运算符返回的不一定是布尔值

初学者最容易误以为&&和||一定返回布尔值。实际上,它们在执行短路求值之后,返回的是某个操作数本身的值。

a || b的规则是:如果a是真值,返回a;否则返回b。a && b的规则是:如果a是假值,返回a;否则返回b。

所以:

console.log(0 || "默认值"); // "默认值" console.log("有值" || "默认值"); // "有值" console.log(1 && 2); // 2 console.log(0 && 2); // 0

理解了这一点,就能看懂很多框架源码里的写法:

const name = user.name || "游客"; const isLogin = user && user.token;

user && user.token在user为null或undefined时直接返回user,避免继续读取user.token报错;在user存在时返回user.token。这正是短路求值的价值:通过一个表达式同时完成判空和取值。

4.2 短路求值在业务代码里的三种常见场景

第一个场景是条件渲染。React 里很常见:

{isLogin && <Dashboard />}

isLogin为 false 时,false就直接作为表达式结果,不会渲染出任何内容。

第二个场景是累加或初始化。比如统计购物车总价:

const totalPrice = cart.items.reduce((sum, item) => sum + item.price, 0);

这里不多说逻辑运算符,但它依赖每次迭代把数值赋给累加器。

第三个场景是给参数设置默认值。在 ES6 之前,常见写法是:

function fetchData(url, options) { options = options || {}; }

这种方式有一个坑:如果调用方传入0、""、false这类假值,也会被替换成默认值。如果业务上确实允许""或0作为有效参数,就不能用||,要用 ES2020 的空值合并运算符??。这个在后面的特殊运算符章节单独展开。

4.3 双重否定是一种类型转换技巧

逻辑非!会把操作数转成布尔值再取反。写两个!,就得到“把任意值转成布尔值”的效果:

console.log(!!"hello"); // true console.log(!!0); // false console.log(!!undefined); // false console.log(!![]); // true

!!不是独立运算符,只是两次逻辑非。它比Boolean(value)短,也比Boolean(value)更容易混在一长串表达式里。我平时更推荐Boolean(value),因为意图更明确。不过阅读老代码、压缩代码时,还是会经常看到!!,至少要能读懂。

这里还要注意逻辑非的优先级。很多新手会以为!user.age >= 18是“用户的年龄不小于 18”,但实际运算顺序是(!user.age) >= 18,先取反得到一个布尔值,再和数字 18 比较。布尔值false和数字比较时会转成0,结果就是0 >= 18,永远是 false。

经典坑位:想表达“年龄不小于 18”,应该写成!(user.age < 18)或者user.age >= 18。

4.4 逻辑运算符的优先级比你想的低

不少人都知道&&优先级高于||,所以true || false && false会先算false && false,结果是true。但真正到业务表达式里,经常因为混合使用导致误判。

我最推荐的做法是:混合&&和||时一定要加括号。不是因为优先级背不下来,而是因为括号能让未来读代码的人不产生误解。写代码不只是写给解释器,更是写给人看。

5. 位运算符实战:从权限标志位到二进制技巧

5.1 位运算先把数字变成 32 位二进制

位运算符包括&、|、^、~、<<、>>、>>>,它们操作的是数字的二进制表示。JavaScript 的位运算会把操作数先转成 32 位有符号整数,再按位计算,最后返回一个 32 位有符号整数。

这意味着位运算不适合直接处理很大的数字,超过 2^31 - 1 的数值参与位运算时可能被截断。这个限制平时很少遇到,但权限系统里如果权限值设计得很大,就需要注意。

最常见的权限判断写法是“位标志”模式。假设一个系统有三类操作权限:读、写、删,分别定义为:

const READ = 1; // 二进制 001 const WRITE = 2; // 二进制 010 const DELETE = 4; // 二进制 100

用户权限存成一个数字,比如5,二进制是101,代表有读和删的权限。判断有没有写权限时:

const userPermission = 5; console.log((userPermission & WRITE) === WRITE); // false,因为 101 & 010 = 0

给用户加权限:

const newPermission = userPermission | DELETE; // 101 | 100 = 111

去掉某个权限:

const removeRead = userPermission & ~READ; // 101 & ~001 = 100

这种方式的优点是只用一个数字就能表达组合状态,数据库存储、缓存传输都非常方便。我在后台管理系统的角色权限模块里用过这套方案,几十种权限叠加起来,代码依然很清晰。

5.2 几个高频但容易被误用的位运算小技巧

第一个是快速判断奇偶:

const isEven = (n) => (n & 1) === 0;

第二个是用~~截断小数位:

console.log(~~12.99); // 12 console.log(~~-12.99); // -12

~~本质上不是数学取整,而是“按位取反再取反”,先把小数部分丢掉。它比Math.trunc()短,但可读性差。我建议在业务里写Math.trunc()。

第三个是判断一个数是不是 2 的幂:

const isPowerOfTwo = (n) => n > 0 && (n & (n - 1)) === 0;

第四个是交换两个变量,这是很多算法课程里的经典题目:

let a = 3; let b = 5; a ^= b; b ^= a; a ^= b;

这个技巧看起来很酷,但普通业务代码里直接用解构[a, b] = [b, a]更清晰。位运算交换只在极度追求底层的场景里才有意义。

5.3 位运算的优先级坑和可读性取舍

位运算符的优先级比比较运算符低,比逻辑运算符高但也不是最高。这里有一个非常经典的坑,也是我在代码评审里反复抓的问题。

有人想判断一个权限位是否包含某个标志位,写出了:

if (userPermission & READ === READ) { // ... }

这里的错误在于:===的优先级高于&,所以实际执行的是:

if (userPermission & (READ === READ)) { // ... }

READ === READ为 true,userPermission & true相当于userPermission & 1。如果用户权限恰好有最低位权限,这个判断就错误通过。

正确写法必须加括号:

if ((userPermission & READ) === READ) { // ... }

不要在这类表达式上省括号。权限判断关系到系统安全,宁可多写一点,也不要留下优先级隐患。

6. 特殊运算符:?. ?? typeof instanceof in void

6.1 空值合并运算符 ?? 与 || 的区别

空值合并运算符??是 ES2020 引入的,它只对null和undefined做“兜底”,不会像||那样把0、""、false也当成需要兜底的值。

看这段对比:

let value = 0; console.log(value || "默认值"); // "默认值" console.log(value ?? "默认值"); // 0

业务里最常见的坑是:配置项允许0,结果用||给默认值后,0永远没法生效。比如优惠金额为0表示不打折,但||会把0当成无值,导致错误地套用默认折扣。

还有一个语法细节:??不能和||或&&直接混合使用而不加括号,否则直接报语法错误。

a ?? b || c; // SyntaxError (a ?? b) || c; // 正确 a ?? (b || c); // 正确

我遇到很多次新手看文档写options.timeout ?? 3000 || fallback,要么报错,要么运行行为和预期不一致。我的习惯是:只要出现??,就把相关表达式整体用括号包起来。

6.2 可选链运算符 ?. 的三种形态

可选链?.也是 ES2020 引入的,作用是在访问属性、调用函数、读取数组下标之前自动判空。它只有三种形态:

obj?.prop // 属性访问 obj?.[expr] // 下标访问 obj?.(args) // 函数调用

举一个真实场景。接口返回结构可能是:

const user = { address: { city: "深圳" } }; console.log(user?.address?.city); // "深圳"

如果user是null,写user.address会抛 TypeError,但写成user?.address会安全返回undefined。

可选链也不会对“空字符串”“0”做兜底,它只关心null和undefined。这一点和??是同一套哲学。它们经常搭配使用:

const city = user?.address?.city ?? "未知";

这套组合基本取代了以前user && user.address && user.address.city || "未知"的写法,代码干净很多。

6.3 typeof、instanceof、in 和 delete 的边界情况

typeof用来判断值的类型,但它有一堆历史遗留问题,最大的就是:

console.log(typeof null); // "object"

这是 JavaScript 诞生初期的设计缺陷,直到今天也不能用typeof null === "null"判断 null。判断 null 直接用value === null最靠谱。

typeof对未声明的变量不会抛错,而是返回"undefined",这是它和直接访问变量的最大区别:

console.log(typeof notExist); // "undefined"

instanceof检查对象是否在某个构造函数的原型链上:

console.log([] instanceof Array); // true

但跨 realm(比如 iframe、不同窗口对象)的情况下,数组的Array构造函数可能不同,导致instanceof Array为 false。判断数组我建议直接用Array.isArray()。

in运算符用来检查某个属性是否存在于对象中,包括继承来的属性:

console.log("toString" in {}); // true

如果想只检查自身属性,用Object.prototype.hasOwnProperty.call(obj, key)或者Object.hasOwn(obj, key)。

delete运算符用来删除对象属性,它返回布尔值表示是否删除成功,但对局部变量使用基本上无效,甚至会带来困惑。我以往只在清空对象属性映射时用它。

6.4 void、逗号运算符和下标运算符

void运算符现在用得不多,它接受一个表达式并总是返回undefined。在前端领域,老式 HTML 里常见href="javascript:void(0)",目的是让点击链接不产生页面跳转。现代工程里这个写法已经比较少见了,但了解它还是有必要的,因为void 0曾经是安全的“取 undefined”的方式。

逗号运算符,会从左到右执行多个表达式,并返回最后一个表达式的值。最常见的场景是 for 循环:

for (let i = 0, j = 10; i < j; i++, j--) { console.log(i, j); }

也可以用来精简代码:

const result = (fn1(), fn2(), 42);

但业务代码里我强烈不建议这样写,可读性太差。

下标运算符[]本质是属性访问表达式。arr[0]、obj[key]里的key会先被计算,再作为属性名去读取。正因为如此,它能读取动态属性名。前端处理 map 结构时,经常用:

const typeName = ["info", "warning", "error"][type] || "info";

7. 运算符优先级与结合性:bug 重灾区

7.1 一张足够用的优先级简表

优先级是运算符最容易被忽略、也最容易产生连环 bug 的地方。我不会要求任何人背下《ECMAScript 规范》里几十行优先级,但下面这张精简表最好能熟记,它覆盖了 95% 的开发场景。

优先级说明运算符
高成员访问、函数调用、创建实例.[]()new
高一元运算符!~+-typeofvoiddelete++--
中高指数运算**
中高乘除取余*/%
中加减+-
中移位<<>>>>>
中关系比较<><=>=ininstanceof
中相等比较==!====!==
中位与、异或、或&^|
低逻辑与、或、空值合并&&||??
低三元? :
低赋值=+=-=&&=等
低逗号,

这张表至少要记住几个关键顺序:一元运算符的优先级高于乘除;相等比较的优先级高于位运算;逻辑运算符低于关系运算符;赋值运算符基本垫底。

正因为===的优先级高于&,才会出现第 5 章里那个权限判断的坑。同理,typeof a === "string"能正常运算,是因为typeof先执行,再与字符串比较。

7.2 结合性:赋值、三元、指数从右往左

优先级解决的是“谁先算”,结合性解决的是“同一个优先级的连写时怎么算”。绝大多数运算符从左到右,但有三类比较特殊:

赋值运算符从右到左,所以a = b = c能连续赋值。 三元运算符也是右结合,所以a ? b : c ? d : e等价于a ? b : (c ? d : e)。但嵌套三元可读性非常差,我建议只要出现第二层三元,就改成 if 分支。 指数运算符也是右结合,所以2 ** 3 ** 2先算右上角。

结合性问题最麻烦的地方是“看起来合法,但理解错”。我在代码评审里遇到过一个很典型的例子:

const result = condition1 ? value1 : condition2 ? value2 : value3;

这个表达式确实能跑,但第一眼读的时候,很容易理解为(condition1 ? value1 : condition2) ? value2 : value3。为了不让同事误读,我后来统一要求:嵌套三元一律拆成纯 if,或者加括号。

7.3 什么情况下必须加括号

从代码可维护性出发,我总结了几条硬性规则:

只要混合了位运算符和比较运算符,就加括号。比如权限判断,写成(permission & READ) === READ。 只要混合了??和||/&&,就加括号。这是语法强制要求。 只要表达式同时出现加减和移位,我会加括号。移位优先级比加减低,是不少人记反的地方。 只要三元表达式里有复杂条件,就先把条件存成命名变量,三元表达式本身只保留最简形式。

括号本身不是坏味道。真正的坏味道是“一会儿依赖优先级,一会儿依赖结合性”,让读代码的人被迫回忆运算符表。

8. 常见问题与避坑清单

8.1 高频坑速查表

下面这些案例全部来自真实代码场景,我按“问题现象-根因-正确姿势”整理成表。

常见问题产生原因正确姿势
金额相加得到字符串拼接接口返回字符串,+触发了拼接先用Number()转成数字再相加
0.1 + 0.2不等于0.3二进制浮点数精度限制用整数分、toFixed或精度库
权限判断永远为 true===优先级高于&(perm & READ) === READ
数组 sort 没有按数字排默认转字符串比较arr.sort((a, b) => a - b)
默认值把合法0覆盖使用了 `
读嵌套属性报错中间层为 null使用?.
NaN判断失败NaN === NaN为 false用Number.isNaN(value)
空字符串被判成无值用了 `

NaN值得单独说一下。NaN是唯一和自身不相等的值,所以value === NaN永远是 false。判断NaN要用Number.isNaN(value),或者写value !== value这种 hack。实际业务里,最常见的NaN来源是parseFloat解析失败、除法分母为 0、后端字段缺失直接做减法。排查这类问题的时候,第一步就是检查是不是产生了NaN。

8.2 一个真实的重构案例:权限位判断修复

我印象里最深的一次 bug 排查,就是在后台系统权限模块里。

当时的代码长这样:

if (userPermission & roleFlag === roleFlag) { // 执行某些高风险操作 }

这个代码从逻辑意图上看,是想判断userPermission是否包含roleFlag对应的位。但是由于优先级问题,实际执行的是:

if (userPermission & (roleFlag === roleFlag))

roleFlag === roleFlag是 true,表达式变成userPermission & 1。也就是说,只要用户拥有最低位权限,所有需要更高权限的操作都会放行。某次安全审查发现问题后,我们排查了半小时,最后定位在一行“看起来很合理”的表达式上。

修复很简单:

if ((userPermission & roleFlag) === roleFlag) { // 执行某些高风险操作 }

从那以后,我禁止团队里出现任何“位运算条件不加括号”的写法。并且把类似问题写进了代码评审检查单。

8.3 从入门到精通,我理解为“定位根因的速度”

写到最后,说说我对“精通”这两个字的理解。JavaScript 运算符不是考试知识点,它真正的衡量标准是:项目里出了诡异的数值 bug,你能不能快速判断是隐式转换、优先级、结合性、短路返回类型还是浮点精度的问题。

我现在带人做代码评审时,最常说的话是:“不是让你不用运算符,是让你在关键表达式上别省括号、别省转换、别省中间变量。”

有个小习惯分享给各位:每写完一行复杂表达式,问自己一个问题,旁边同事不查文档,能不能看懂这行在做什么?如果答案是不能,那就拆一行,加个变量名,或者加一对括号。运算符可以帮你写出很短的代码,但合格的工程师往往更关心代码能被下一个接手的人顺利读懂。

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

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

立即咨询