JavaScript 运算符完全指南

前言

如果把 JavaScript 比作一门语言,变量是名词,函数是句子,那么运算符就是动词------它决定了值与值之间发生什么:相加、比较、赋值、取反。我们每天都在写 +===,但很少有人能说清楚 0 || 100 ?? 10 到底差在哪,[] == ![] 为什么是 true,typeof null 为什么返回 'object'

这些"灵异事件"没有一个是 bug。它们全部是 ECMAScript 规范里白纸黑字定义好的行为,只是与人的直觉不一致。而直觉不一致的地方,就是线上事故的高发区:一个用 || 设置默认值的音量配置,把用户精心调好的 0 悄悄改回了 50;一个用 delete 删数组元素的循环,让后续的 forEach 莫名其妙跳了一格。


一、地基:类型转换机制

JS 是动态弱类型语言,几乎所有运算符在执行前都会对操作数做隐式转换。不理解转换规则,就只能靠背"经典面试题";理解了规则,任何诡异表达式都可以拿纸笔一步步推导出来。

规范定义了几个核心的抽象操作(Abstract Operations),它们不是可以调用的函数,而是规范描述行为用的记号。

1.1 ToPrimitive:对象变原始值

ToPrimitive(input, hint) 接收一个 hint 参数,取值有三种:numberstringdefault。转换流程是:

  1. 如果对象定义了 Symbol.toPrimitive 方法,直接调用它,hint 作为参数传入,结束;
  2. 否则,按顺序尝试 valueOf()toString()------但 hint 为 string 时顺序对调,先试 toString
  3. 第一个返回原始值的方法,其结果就是答案;两个都不返回原始值,抛 TypeError。
js 复制代码
const obj = {
  valueOf() { return 42; },
  toString() { return 'str'; }
};

obj + 1;        // 43。+ 用 default hint,优先 valueOf
`${obj}`;       // 'str'。模板字符串用 string hint
Number(obj);    // 42
String(obj);    // 'str'

hint 为 default 时按 number 的顺序处理,所以 +==< 这些运算符触发的转换里,valueOf 排在前面。

日期对象是唯一的特例。Date.prototype[Symbol.toPrimitive] 在 default hint 下返回字符串而不是数字,这就是为什么:

js 复制代码
new Date(0) + 1;
// 'Thu Jan 01 1970 ...1'------字符串拼接,不是时间戳加一
Number(new Date(0));  // 0,number hint 下老老实实返回时间戳

数组的 valueOf 返回自身(不是原始值),于是落到 toString,也就是 join(',')

js 复制代码
[] + [];          // '':两个空数组各自变成 '',拼接还是 ''
[1, 2] + [3, 4];  // '1,23,4'

1.2 ToNumber:一切变数字

输入 结果
undefined NaN
null 0
true / false 1 / 0
数字字符串 对应数值:'0x1A' → 26,' 12 ' → 12,'1e3' → 1000
非法数字字符串 NaN'12px' → NaN,'1,2' → NaN
空字符串 '' 0
Symbol 抛 TypeError
对象 先 ToPrimitive(number),再对结果递归 ToNumber

对象转数字这条递归路径解释了一堆现象:

js 复制代码
+[];        // 0:[] → '' → 0
+['5'];     // 5:['5'] → '5' → 5
+[1, 2];    // NaN:[1,2] → '1,2' → NaN
+{};        // NaN:{} → '[object Object]' → NaN
+null;      // 0
+undefined; // NaN

1.3 ToBoolean:只有八个 falsy

转布尔的规则最简单,也最需要背下来------falsy 值一共只有八个

复制代码
false、0、-0、0n、''、null、undefined、NaN

其余一切都是 truthy。高频踩坑的"意外 truthy":

js 复制代码
Boolean([]);          // true!空数组
Boolean({});          // true!空对象
Boolean('0');         // true!非空字符串
Boolean('false');     // true
Boolean(' ');         // true!空格也是非空
Boolean(new Boolean(false)); // true!包装对象是对象

最后一条尤其阴险:new Boolean(false) 创建的是对象,对象一律 truthy,尽管它"包着" false。永远不要用 new Boolean/Number/String,要转换就用函数形式的 Boolean(x)

1.4 ToInt32 与 ToNumeric

位运算专用:ToInt32 把值转成 32 位有符号整数,null、undefined、NaN 都变 0,小数向零截断,超出 ±2³¹ 的数按模回绕。BigInt 的位运算则走 ToBigInt

算术运算的总入口是 ToNumeric:BigInt 走 BigInt 路径,其余走 ToNumber 路径。BigInt 和 Number 不能直接混算,1n + 1 抛 TypeError,这是规范故意设置的防线------两种数字的精度语义不同,混算的结果几乎必然不符合任何一方的预期,不如强制开发者显式转换。

有了这四组规则,前文提过的 [] + {} 已经可以完整推导:

复制代码
[] → ToPrimitive(default) → valueOf 返回自身(非原始值) → toString → join → ''
{} → ToPrimitive(default) → '[object Object]'
两侧有字符串 → 走拼接分支 → '[object Object]'

没有任何玄学。下面进入运算符正题。


二、算术运算符

算术运算符包括 +-*/%**++--,以及一元 + 和一元 -

2.1 加法 +:一身兼两职

+ 是 JS 中唯一具有双重语义的二元运算符。求值分两步:先对左右操作数各执行一次 ToPrimitive(hint 为 default),然后只要有一方的原始类型是字符串,就走字符串拼接;否则双方 ToNumber 后数值相加

注意判断发生在转换之后,这是理解所有怪异加法的关键:

js 复制代码
1 + 2;           // 3
1 + '2';         // '12','2' 本身就是字符串
1 + true;        // 2,true → 1
1 + null;        // 1,null → 0
1 + undefined;   // NaN,undefined → NaN,NaN 传染一切
1 + NaN;         // NaN
'' + 0;          // '0',最常用的"数字转字符串"手段

{} + [] 是流传最广的整活题。在浏览器控制台把它当语句敲进去,结果是 0------因为行首的 {} 被解析为空代码块 ,实际执行的是 +[],一元加号把空数组转成数字 0。但写成 ({} + [])、放在赋值右侧、或在 Node REPL 里执行,{} 就是对象字面量了,结果是 '[object Object]'。差异出在语法解析层,与运算符本身无关。这个例子的教训是:同一串字符在语句位置和表达式位置的解析可能完全不同

2.2 一元加号:最短的类型转换

一元 + 的语义就是对操作数执行 ToNumber:

js 复制代码
+'42';       // 42
+'';         // 0
+'12px';     // NaN
+true;       // 1

它是所有数字转换手段里最短的,处理 DOM 输入值(+input.value)时很常见。缺点是意图不够显眼------一个前缀加号容易被看漏,也容易和二元加号混淆。团队项目中更推荐 Number(x),语义一目了然,行为完全等价(都走 ToNumber)。

顺带澄清一个常见误解:Number()parseInt() 不等价。Number('12px') 是 NaN,parseInt('12px') 是 12------parseInt 从左往右解析到第一个非法字符为止,Number 要求整个字符串是合法数字。解析用户输入的自由文本用 parseInt(记得传基数 10),做严格校验用 Number 或正则。

2.3 减法、乘法、除法 - * /

这三个没有歧义:操作数各自 ToNumber 后运算,结果永远是数字。字符串参与时会被强制转数字,与 + 形成鲜明对比:

js 复制代码
'10' - 5;     // 5
'10' * '2';   // 20
'a' - 1;      // NaN

除法要单独讲浮点精度。JS 只有一种数字类型------IEEE 754 双精度浮点数,于是:

js 复制代码
0.1 + 0.2;            // 0.30000000000000004
0.1 + 0.2 === 0.3;    // false

这不是 JS 的锅,Java、Python、C# 全都一样,是二进制无法精确表示 0.1 的固有问题。工程对策分场景:金额计算用整数(以分为单位)、用字符串保存小数、或用 decimal.js 这类库;普通业务里比较浮点数用误差范围 Math.abs(a - b) <= Number.EPSILON * Math.max(Math.abs(a), Math.abs(b)),简单场景 Math.abs(a - b) < 1e-9 也够用。整数在 Number.MAX_SAFE_INTEGER(2⁵³−1,约 9007 万亿)以内是精确的,超出的整数运算用 BigInt。

除零是合法操作,不抛错:

js 复制代码
1 / 0;    // Infinity
-1 / 0;   // -Infinity
0 / 0;    // NaN

2.4 取余 %

取余结果的符号跟随左操作数(被除数),这与数学上的"模运算"不同:

js 复制代码
7 % 3;     // 1
-7 % 3;    // -1,不是 2
7 % -3;    // 1
-7 % -3;   // -1

需要数学意义的非负模时用 ((a % b) + b) % b 修正。BigInt 也支持 %,且 BigInt 除法是向零取整(与 Number 的浮点除法不同,BigInt 的 / 直接丢弃小数部分:7n / 2n === 3n)。

另一个细节:n % 2 === 1 判断奇数对负数是错的-------3 % 2-1。稳妥写法是 n % 2 !== 0

2.5 幂运算 **(ES2016)

js 复制代码
2 ** 10;    // 1024,等价于 Math.pow(2, 10)
2 ** -1;    // 0.5
4 ** 0.5;   // 2,开平方

两个规范细节。第一,**右结合的:

js 复制代码
2 ** 3 ** 2;   // 512,即 2 ** (3 ** 2) = 2 ** 9,不是 (2 ** 3) ** 2 = 64

第二,一元运算符不能直接贴在底数前:

js 复制代码
-2 ** 2;     // SyntaxError!
-(2 ** 2);   // -4 ✅
(-2) ** 2;   // 4 ✅

这条禁令是 ES2016 讨论时有意为之的------Python 里 -2 ** 2 是 -4,有些语言是 4,与其替开发者选边站,不如直接报错强制表态。

2.6 自增自减 ++ --

前置与后置的区别在于表达式返回新值还是旧值:

js 复制代码
let a = 1;
let b = a++;   // b = 1,a = 2。后置:先取旧值,再自增
let c = 1;
let d = ++c;   // d = 2,c = 2。前置:先自增,再取新值

它们内部先执行 ToNumber,所以字符串也能用,且变量类型会改变:

js 复制代码
let s = '5';
s++;    // s 现在是数字 6,不再是字符串

由此可知 ++xx = x + 1 并不严格等价:前者做 ToNumber('5' 变 6),后者走完整的 + 逻辑('5' + 1'51')。对 const 变量、对象字面量、或任何非法赋值目标使用会直接报错。

还有一类必须远离的写法:在同一表达式里对同一变量多次读写,比如 arr[i++] = i++。规范虽然定义了求值顺序,但这种代码的可读性是灾难,且不同语言的顺序规则不同,迁移时必炸。一行只做一次修改,是底线。


三、赋值运算符

3.1 基本赋值 =

赋值表达式的值是被赋的那个值,且赋值右结合,链式赋值由此成立:

js 复制代码
let a, b;
a = b = 5;     // 解析为 a = (b = 5),a 和 b 都是 5

一个历史包袱:a = b = 5 中如果 b 从未声明,非严格模式下会悄悄创建全局变量;严格模式下抛 ReferenceError。ES Module 默认严格模式,现代项目踩不到,但读遗留代码时需要心里有数------这也正是当年无数全局变量污染的来源。

对象赋值传的是引用,这是"数据被莫名改掉"类问题的头号源头:

js 复制代码
const o1 = { n: 1 };
const o2 = o1;    // o2 和 o1 指向同一个对象
o2.n = 2;
o1.n;             // 2

注意 const 只锁定绑定不锁定内容:const 对象的属性照样可以改,o1 = {} 才会报错。

3.2 复合赋值

算术版:+=-=*=/=%=**=;位运算版:&=|=^=<<=>>=>>>=。都是 a = a op b 的简写。

+= 继承 + 的双重语义,是唯一需要小心的:

js 复制代码
let total = '1';
total += 2;    // '12',字符串拼接
let sum = 1;
sum += 2;      // 3

一个很少人知道的规范细节:a += b 只对 a 求值一次 ,而 a = a + b 求值两次。当左值是带副作用的表达式(函数调用、getter、arr[i++])时,两者行为不同。日常代码几乎不会踩到,但读复杂左值时值得留意。

3.3 逻辑赋值(ES2021):||= &&= ??=

这三个运算符的共同核心是短路:条件不满足时,右侧表达式根本不会执行。它们与展开形式的对应关系是:

js 复制代码
a ||= b;   // 等价于 a || (a = b),不是 a = (a || b)
a &&= b;   // 等价于 a && (a = b)
a ??= b;   // 等价于 a ?? (a = b)

写法上的差别不只是少敲几个字符------右侧是昂贵计算或有副作用的调用时,短路版本根本不执行它:

js 复制代码
config.theme ||= computeExpensiveTheme();   // theme 存在时,函数不调用

??= 是三者中最实用的,处理"给缺失的配置项补默认值"干净利落:

js 复制代码
const settings = { volume: 0 };
settings.volume ??= 50;     // 保持 0------用户明确设置的 0 是合法值 ✅
settings.muted ??= false;   // 补上 false
settings.lang ??= 'zh-CN';  // 补上 'zh-CN'

如果换成 ||=volume 会被错误覆盖成 50,用户的静音设置凭空丢失。这个例子值得反复强调:凡是 0、空串、false 属于合法业务值的字段,默认值逻辑必须走 ??。分页页码、计数器、开关状态、允许为空的文本框,全都属于此类。

&&= 的典型场景是"存在才修改":

js 复制代码
let user = null;
user &&= { ...user, lastLogin: Date.now() };   // user 为 null 时右侧不求值,不报错

四、比较运算符

这是规范中最庞大的一章,也是实际项目事故率最高的区域。

4.1 关系运算符 < > <= >=

比较逻辑分两条路径:双方 ToPrimitive 后都是字符串 时,按 Unicode 码元逐字符比较(字典序);其他情况,双方 ToNumeric 后按数值比较(BigInt 与 Number 混合比较在这四个运算符下是合法的,规范专门定义了跨类型数值比较)。

js 复制代码
'a' < 'b';      // true
'Z' < 'a';      // true,'Z'(90) 在 'a'(97) 前面------大写整体排在小写前
'10' < '9';     // true!双方都是字符串,逐字符比,'1'(49) < '9'(57)
'10' < 9;       // false,右侧是数字,'10' 转数值 10

'10' < '9' 坑过无数对字符串排序掉以轻心的人------数据库里 VARCHAR 类型的数字字段排序、版本号比较、文件名排序,全是重灾区。凡是需要数值语义,先确保两边真的是数字;版本号这种 1.9.0 < 1.10.0 的场景则要拆段比较。

null 和 undefined 参与大小比较时行为分裂,是高频面试点,也是真实的坑:

js 复制代码
null >= 0;       // true
null > 0;        // false
null <= 0;       // true
undefined >= 0;  // false
undefined > 0;   // false
undefined < 0;   // false

前两条看似矛盾,其实完全自洽。规范里 >= 的定义是"先做小于比较,若为 false 且结果不是 undefined 则返回 true",即 null >= 0 等价于 !(null < 0)null < 0 走 ToNumber 得 0 < 0 为 false,取反就是 true。而 null > 0 直接算 0 > 0,false。undefined 转数字是 NaN,NaN 参与任何大小比较都是 false,所以三行全 false。

结论很实际:大小比较前先判空,不要让 null/undefined 混进来

4.2 宽松相等 ==:抽象相等算法

== 的行为由 Abstract Equality Comparison 定义。规则可以压缩成六条,按判断顺序:

  1. 类型相同 :完全按 === 的规则走;
  2. null 与 undefined :互相相等,且不与任何别的值相等------包括 0、''、false;
  3. 数字对字符串:字符串 ToNumber 后重新比较;
  4. 有布尔参与:布尔先 ToNumber(true→1,false→0),重新比较;
  5. 对象对原始值(bigint 对 number 也在此列):对象先 ToPrimitive,重新比较;
  6. 其余组合(如 Symbol 对数字):一律 false。
js 复制代码
null == undefined;   // true(规则2)
null == 0;           // false(规则2:null 绝不转数字)
'' == 0;             // true(规则3:'' → 0)
'0' == false;        // true(规则4:false → 0;规则3:'0' → 0)
[] == false;         // true(同上:[] → '' → 0)
[] == ![];           // true(见下方推导)
NaN == NaN;          // false,NaN 参与任何相等比较都是 false

[] == ![] 值得完整推一遍,它串起了三条规则:

复制代码
第一步:! 的优先级高于 ==,先算 ![]
        [] 是对象 → ToBoolean 为 true → 取反得 false
        表达式变为 [] == false
第二步:规则4,布尔转数字 → [] == 0
第三步:规则5,对象转原始值 → '' == 0
第四步:规则3,字符串转数字 → 0 == 0
第五步:类型相同值相同 → true

每一步都严格遵循规范。背结论没有意义,会推导才能以不变应万变。

== 在工程上唯一被广泛认可的用法,是一行同时判 null 和 undefined

js 复制代码
if (value == null) {
  // 当且仅当 value 是 null 或 undefined 时进入
}

它精确等价于 value === null || value === undefined,更短且不引入任何歧义------因为规则 2 保证了 null/undefined 不会和其他任何值宽松相等。ESLint 的 eqeqeq 规则开 "smart"["always", {"null": "ignore"}] 选项时专门放行这一种写法。除此之外,没有理由使用 ==

4.3 严格相等 ===

类型不同直接 false;类型相同再比值。对象比引用:

js 复制代码
1 === '1';            // false,类型不同
null === undefined;   // false
NaN === NaN;          // false ⚠️
+0 === -0;            // true ⚠️
{} === {};            // false,两个不同对象
const a = {}; a === a; // true,同一引用

两个 ⚠️ 是 === 仅有的"不完美"处,都源于 IEEE 754。

NaN 的判断用 Number.isNaN(x)

js 复制代码
Number.isNaN(NaN);      // true
Number.isNaN('abc');    // false ✅ 不转换,只对真正的 NaN 返回 true
isNaN('abc');           // true ⚠️ 全局 isNaN 先做 ToNumber,'abc' → NaN,语义完全不同

全局 isNaN 的实际含义是"这个值能不能转成数字",和名字表达的"这个值是不是 NaN"是两回事。永远用 Number.isNaN。另一个技巧 x !== x 利用了 NaN 不等于自身的特性,功能正确但可读性差,只配在压缩产物里出现。

+0-0=== 眼里相等,绝大多数场景无差别。区别只在:1/0 得 Infinity 而 1/-0 得 -Infinity;Object.is 能区分它们;(-0).toString()'0',负号会悄悄消失,这在需要保号的数学库中是要专门处理的问题。

4.4 Object.is:同值相等(SameValue)

ES2015 引入,对应规范的 SameValue 算法,与 === 只有两处分歧------恰好修复上面两个 ⚠️:

js 复制代码
Object.is(NaN, NaN);   // true
Object.is(+0, -0);     // false
Object.is(1, '1');     // false,与 === 一致
Object.is({}, {});     // false,与 === 一致

业务代码直接用它的机会不多(判 NaN 用 Number.isNaN 更语义化),但它在库层面无处不在------React 的 useState 内部就是用 Object.is 比较新旧 state 来决定是否触发重渲染。你给 state 连续赋两个 NaN,React 不会无限重渲,就是它的功劳。

4.5 第四种相等:SameValueZero

规范里还有一种相等算法值得知道:SameValueZero,与 Object.is 的唯一区别是认为 +0 和 -0 相等 (NaN 等于自身这点与 Object.is 相同)。它是 Map/Set 的键去重、Array.prototype.includes 的判断标准:

js 复制代码
[NaN].includes(NaN);        // true!includes 用 SameValueZero
[NaN].indexOf(NaN);         // -1!indexOf 用严格相等
new Set([NaN, NaN]).size;   // 1,NaN 被视为同一个键
new Set([0, -0]).size;      // 1,±0 也被视为同一个键

includes 能找到 NaN 而 indexOf 找不到,这个差异的根源就在算法选型。四种相等性对照:

比较 == === Object.is (SameValue) SameValueZero
NaN vs NaN false false true true
+0 vs -0 true true false true
null vs undefined true false false false
1 vs '1' true false false false
使用者 宽松相等 严格相等 React state、Object.is Map/Set/includes

选型原则一句话:默认 ===;判空可用 == null??;需要精确处理 NaN 或 ±0 时用 Object.is;集合类 API 的行为由 SameValueZero 决定,知道即可


五、逻辑运算符

5.1 短路与返回值:&&|| 不返回布尔

这是 JS 与 Java、C 的重要差异。&&|| 返回的是决定结果的那个操作数本身

  • a && b:a 为 falsy 返回 a,否则返回 b;
  • a || b:a 为 truthy 返回 a,否则返回 b。
js 复制代码
'hi' && 'there';     // 'there'
0 && 'there';        // 0,左侧已经决定结果
null || 'fallback';  // 'fallback'
'a' || 'b';          // 'a'
1 && 2 && 3;         // 3,链式取最后一个
null && 2 && 3;      // null,短路在第一环

求值从左往右,一旦结果可确定立即停止,右侧完全不会执行------这就是短路。它带来两种经典用法。

一是防御性访问,可选链出现之前的标准写法:

js 复制代码
if (response && response.data && response.data.items) { ... }

二是 React 条件渲染:

jsx 复制代码
{isLogin && <Profile />}

第二种用法藏着前端最著名的事故之一:如果 isLogin 是数字,比如购物车数量 count,那么 count && <Cart /> 在 count 为 0 时返回 0 ,而 React 会把数字 0 渲染到页面上------用户看到一个孤零零的"0"挂在导航栏。根因就是 && 返回操作数本身而非布尔。修复方式任选其一:

jsx 复制代码
{count > 0 && <Cart />}      // 让左侧是真正的布尔表达式
{!!count && <Cart />}        // 显式转布尔
{count ? <Cart /> : null}    // 三元

5.2 逻辑非 ! 与双重取反 !!

! 先对操作数做 ToBoolean 再取反,返回值一定是布尔。!! 于是成为把任意值规范化为布尔的惯用法,与 Boolean(x) 完全等价:

js 复制代码
!0;      // true
![];     // false------空数组是 truthy
!!'abc'; // true

!!xBoolean(x) 选哪个纯属风格问题,团队统一即可。需要精确控制转换语义时(比如配合 getter 或 Proxy)用 Boolean() 更显式。

5.3 空值合并 ??(ES2020)

?? 只在左侧为 nullundefined 时返回右侧,其余所有 falsy 值(0、''、false、NaN)一律原样保留:

js 复制代码
0 ?? 'default';      // 0
'' ?? 'default';     // ''
false ?? true;       // false
null ?? 'default';   // 'default'
undefined ?? 'x';    // 'x'
NaN ?? 'default';    // NaN

它同样短路:

js 复制代码
'ok' ?? expensive();   // expensive 不执行
null ?? expensive();   // 执行

??|| 的选型,用一个真实场景就能说清。假设做表单回显:

js 复制代码
// 用户的 nickname 可能故意设置为空字符串
const display = user.nickname || '匿名用户';   // ⚠️ 空昵称被顶替成"匿名用户"
const display = user.nickname ?? '匿名用户';   // 仅 null/undefined 才用兜底

|| 的语义是"左侧不可用就换右侧",?? 的语义是"左侧缺失才补右侧"。取默认值几乎总是后者的语义 。只有当业务确实认为 0/''/false 等同于"未提供"时(比较少见),才用 ||

一条硬性语法限制:?? 不能与 &&|| 不加括号地混用:

js 复制代码
a || b ?? c;     // SyntaxError
(a || b) ?? c;   // ✅
a || (b ?? c);   // ✅

这是规范有意设计------三种逻辑的短路语义各不相同,裸混写出的表达式连作者自己都未必说得清结果,不如强制加括号表态。&&|| 混用没有这个限制(历史上太多存量代码),但 lint 会提醒你加括号。


六、位运算符

位运算把操作数经 ToInt32 转成 32 位有符号整数后按位运算,结果转回 64 位浮点数。这意味着它只适用于 ±2³¹−1 范围内的整数,更大的数会被截断回绕。

6.1 七个运算符

运算符 名称 示例 说明
& 按位与 13 & 75 1101 & 0111 = 0101
` ` 按位或 `13
^ 按位异或 13 ^ 710 相同为 0,不同为 1
~ 按位非 ~13-14 规律:~n === -(n + 1)
<< 左移 5 << 110 低位补 0,相当于 ×2
>> 有符号右移 -8 >> 1-4 高位补符号位,相当于除 2 向下取整
>>> 无符号右移 -1 >>> 2815 高位补 0,JS 特有

>>> 把 32 位补码当作无符号数解读,所以负数经它会变成很大的正数:

js 复制代码
-1 >>> 0;    // 4294967295(-1 的补码是全 1,按无符号读)
-5 >>> 1;    // 2147483645

它的正经用途是把任意数字规范化为无符号 32 位整数------处理 WebGL 索引、哈希值、Math.imul 结果时会用到。

ToInt32 对输入的宽容也值得知道:null → 0,undefined/NaN → 0,布尔 → 1/0,字符串先 ToNumber。所以 null & 1 得 0,不报错;'5' | 0 得 5。

6.2 实用场景与技巧的边界

位运算在现代前端代码里的正当用途集中在两处。

一是颜色与图形处理,RGB 通道打包在 24 位整数里:

js 复制代码
const color = 0xFF8800;
(color >> 16) & 0xFF;   // 255,红
(color >> 8) & 0xFF;    // 136,绿
color & 0xFF;           // 0,蓝
(r << 16) | (g << 8) | b;   // 反向打包

二是标志位(flags)管理,权限系统、状态机的经典手法:

js 复制代码
const READ = 1, WRITE = 2, EXEC = 4;
let perm = READ | WRITE;        // 组合:3
perm & WRITE;                    // 检测:非 0 表示有写权限
perm & ~WRITE;                   // 清除写权限:1
perm ^ READ;                     // 翻转读权限

至于网上流传的那些技巧,逐个说清楚为什么不推荐:

  • ~~x 取整:向零截断,对负数与 Math.floor 行为不同(~~-4.7 是 -4,Math.floor(-4.7) 是 -5),且超出 32 位就回绕出错。要取整用 Math.trunc(向零)或 Math.floor(向下),意图明确且无范围限制。
  • x | 0 取整:同上,多一层"这是在干嘛"的阅读成本。
  • a ^= b; b ^= a; a ^= b 交换变量:没有性能优势(V8 对 const t = a; a = b; b = t 优化得很好),对浮点数还会因精度丢失出错,且 a ^= b 在 a、b 是同一变量时结果为 0。用解构 [a, b] = [b, a]
  • n & 1 判奇偶:功能正确且确实比 % 快,但在业务代码里 n % 2 !== 0 的表达力完胜。

BigInt 支持 &|^~<<>>------注意没有 >>>,因为 BigInt 是任意精度有符号数,"无符号右移"没有意义。BigInt 与 Number 不能混合位运算,会抛 TypeError。


七、条件(三元)运算符

condition ? expr1 : expr2 是 JS 唯一的三目运算符,也是唯一能以表达式身份嵌入其他表达式的条件结构。它的价值在于把"根据条件选值"从四行 if/else 压缩成一行,且结果可以直接参与赋值、传参、返回、渲染:

js 复制代码
const label = score >= 60 ? '及格' : '不及格';
return isVIP ? price * 0.8 : price;
render(isLoading ? <Spinner /> : <Content />);

三元是右结合的,链式写法等价于右嵌套:

js 复制代码
const level = score >= 90 ? 'A'
            : score >= 80 ? 'B'
            : score >= 60 ? 'C'
            : 'D';
// 解析为 score >= 90 ? 'A' : (score >= 80 ? 'B' : (...))

像上面这样每档一行、缩进对齐的排版下,三四档的链式三元是可读的,本质上是 switch 的表达式版。再复杂就该换 if/else、switch 或查表(const map = { ... }; map[key] ?? fallback 往往比长链条更清晰)。

三条使用纪律:两个分支必须是表达式 ------breakcontinuelet 声明这些语句进不来,这既是限制也是保护,它把三元约束在"选值"这个它真正擅长的场景;分支里有副作用(赋值、调用)时读起来非常费劲,副作用交给 if/else;三元嵌套在另一个三元的分支 里勉强可读,嵌套在条件里没人能读懂,永远别写。


八、成员访问、可选链与属性检查

8.1 .[]

点访问要求属性名是合法标识符;方括号接受任意表达式,动态属性名只能用它:

js 复制代码
obj.prop;
obj['prop'];
obj[key];                 // key 是变量
obj[fields[i]];           // 任意表达式

对象键最终都是字符串或 Symbol,数字键会被转成字符串,所以 obj[1]obj['1'] 访问同一个属性。这一点对数组式对象(比如后端返回的 { "0": ..., "1": ... })尤其重要。

8.2 可选链 ?.(ES2020)

可选链把"访问前层层判空"的样板代码压缩成一个符号,三种形态:

js 复制代码
user?.profile?.address?.city;   // 属性访问
items?.[index]?.id;              // 索引访问
onSuccess?.();                   // 方法调用,方法不存在时返回 undefined 不报错

核心语义是短路 :链条中任何一环求值为 nullundefined,整个表达式立即返回 undefined,后续部分完全不执行------包括副作用:

js 复制代码
let a = null;
a?.b.c.d;                       // undefined,.b.c.d 整段跳过,不抛错
null?.[console.log('never')];   // 'never' 不会打印

使用上有四个容易误解的点。

第一,?. 只保护紧挨着它的那一环 在"起点为空"时的情形,链条中断后整段短路;但如果链条没断、中间某环恰好缺失,后面的裸 . 照样抛错。a?.b.c 中若 a 存在而 a.b 是 undefined,访问 .c 时 TypeError。拿不准就每环都写:a?.b?.c

第二,短路发生在 ?. 处,但整个"链段"作为一个单元------a?.b.c.d 在 a 为 null 时,b.c.d 全部不求值。所以 a?.b.ca?.b?.c 的差异只体现在"a 存在但 b 缺失"这一种情况。

第三,可选链不能出现在赋值左侧:a?.b = 1 是语法错误。它的定位是安全读取,不是条件写入。需要条件写入就老实写 if。

第四,区分不了"值为 undefined"和"对象不存在"。user?.name 返回 undefined 时,可能是 user 为 null,也可能是 name 本来就没设置。需要区分时自己判。

?.() 处理可选回调特别顺手,在组件库和 SDK 代码里高频出现:

js 复制代码
function submit(options = {}) {
  const result = doSubmit();
  options?.onSuccess?.(result);   // 回调可能存在也可能不存在,一行搞定
}

8.3 inObject.hasOwn

in 判断属性存在于对象自身或原型链上:

js 复制代码
const obj = { a: 1 };
'a' in obj;            // true
'toString' in obj;     // true!继承自 Object.prototype
'b' in obj;            // false
0 in [10, 20];         // true,数组的"属性"就是索引
2 in [10, 20];         // false
'length' in [10, 20];  // true

只想检查自身属性 ,用 ES2022 的 Object.hasOwn(obj, key)

js 复制代码
Object.hasOwn(obj, 'toString');   // false
Object.hasOwn(obj, 'a');          // true

它等价于 Object.prototype.hasOwnProperty.call(obj, key),但更安全------对象可能通过 Object.create(null) 没有原型(调 obj.hasOwnProperty 直接 TypeError),也可能恶意覆盖了 hasOwnProperty 属性。新代码一律用 Object.hasOwn

in 与直接访问比较的关键差异:能识别"属性存在但值为 undefined"

js 复制代码
const o = { x: undefined };
'x' in o;                  // true
o.x === undefined;         // true,但 o.y === undefined 也是 true,分不清
Object.hasOwn(o, 'x');     // true ✅

序列化、对象 diff、表单"用户是否动过这个字段"的判断,都依赖这个区分能力。

8.4 instanceof

instanceof 沿对象的原型链查找构造器的 prototype 是否出现:

js 复制代码
[] instanceof Array;         // true
[] instanceof Object;        // true,原型链包含 Object.prototype
new Date() instanceof Date;  // true
function f() {}
f instanceof Function;       // true
f instanceof Object;         // true,函数也是对象
'str' instanceof String;     // false!原始值不是对象

它有四个必须知道的局限。

跨 realm 失效 。iframe、Worker、Node 的 vm 各有独立的全局对象,iframe 里创建的数组与主窗口的 Array 构造器不是同一个,instanceof 直接失手:

js 复制代码
iframeArray instanceof Array;   // false
Array.isArray(iframeArray);     // true ✅

这正是"判断数组必须用 Array.isArray"的根本原因------它内部走 Object.prototype.toString 标签机制,跨 realm 可靠。jQuery 时代无数 bug 源于此。

原型可修改Object.setPrototypeOf(obj, X.prototype) 之后,instanceof 结果随之改变。它反映的是"当前原型链状态",不是"出生时的血统"。

Symbol.hasInstance 可接管。ES2015 起类可以自定义 instanceof 行为:

js 复制代码
class Even {
  static [Symbol.hasInstance](n) {
    return Number.isInteger(n) && n % 2 === 0;
  }
}
2 instanceof Even;   // true
3 instanceof Even;   // false

冷门,但读库源码时会遇到。

继承链上的语义class B extends Anew B() instanceof A 为 true------这通常是你想要的,但做精确类型判断时就是干扰项。

需要精确的运行时类型标签时,用 Object.prototype.toString.call(x)

js 复制代码
Object.prototype.toString.call([]);          // '[object Array]'
Object.prototype.toString.call(new Date());  // '[object Date]'
Object.prototype.toString.call(/re/);        // '[object RegExp]'
Object.prototype.toString.call(null);        // '[object Null]'(连 null 都能精确识别)

实践中各类判断的推荐手段:数组用 Array.isArray;null 用 === null;日期用 x instanceof Date(单 realm)或 Object.prototype.toString(跨 realm);普通对象用 typeof x === 'object' && x !== null;"是不是纯对象"(排除数组、日期等)则需要 Object.prototype.toString 或递归原型检查,视严格程度选型。


九、typeof、void、delete

9.1 typeof

返回类型的字符串标签,一共只有八种可能:

js 复制代码
typeof 42;              // 'number'
typeof 3.14;            // 'number'
typeof NaN;             // 'number'!NaN 是数字
typeof 'str';           // 'string'
typeof true;            // 'boolean'
typeof undefined;       // 'undefined'
typeof Symbol();        // 'symbol'
typeof 10n;             // 'bigint'
typeof function(){};    // 'function'
typeof (() => {});      // 'function',箭头函数也是
typeof class C {};      // 'function'!类在 typeof 眼里就是函数
typeof {};              // 'object'
typeof [];              // 'object'
typeof null;            // 'object' ⚠️
typeof undeclaredVar;   // 'undefined',对未声明变量不抛错(独有的安全性)

逐个说坑。

typeof null === 'object' 是 JS 第一版实现留下的 bug------当年值用"类型标签 + 数据"表示,对象的标签位是 000,而 null 在底层是全零地址,标签位恰好也是 000。后来规范讨论过修复提案,因破坏存量代码太多而放弃,它将永远存在。判 null 只认 x === null

无法区分对象子类型 :数组、日期、正则、Map、普通对象统统 'object'。这是 typeof 最大的盲区,对策见上一节(Array.isArrayObject.prototype.toString)。

函数独享 'function' 是规范特例------函数本质是对象,但 typeof 给它单开一个返回值。typeof x === 'function' 是判断可调用性的标准手段。

typeof 对 TDZ 变量会抛错。"typeof 永远不抛错"这句老话在 ES6 之后不严谨了:

js 复制代码
typeof x;   // ReferenceError: Cannot access 'x' before initialization
let x = 1;

let/const 声明的变量在声明语句之前处于暂时性死区,typeof 访问也会触发 ReferenceError。而 var 声明的变量被提升为 undefined,typeof 安全。对完全未声明 的变量 typeof 依然安全返回 'undefined'------早年特性检测就靠这个:typeof window.someLegacyAPI !== 'undefined'

9.2 void

对操作数求值,返回 undefined。操作数照常执行,只是结果被丢弃。现代代码中它有两个仍然活跃的用途:

丢弃 Promise,显式表达"故意不等待"

js 复制代码
void doAsync();   // 发起异步调用,明确不处理结果

TypeScript 生态的 @typescript-eslint/no-floating-promises 规则把 void 前缀作为"开发者已知晓并故意忽略"的官方逃生门。不写 void 的话,裸调用的 floating promise 会被 lint 拦下。

获取保证纯净的 undefined 。远古代码里 undefined 是全局对象的普通属性、可被覆盖(undefined = 'hacked' 在非严格模式的老浏览器真的可行),void 0 永远返回真 undefined。严格模式下 undefined 已不可写,这个用途过时了,但库代码和压缩产物里仍大量存在(void 0undefined 少三个字节)。

至于 <a href="javascript:void(0)">,历史用法,现代做法是用 <button>event.preventDefault()。另外注意 void 是一元运算符,优先级很高,void a + b 解析为 (void a) + b,得 NaN------又一个"不确定就加括号"的例子。

9.3 delete

移除对象的自身属性,返回布尔值:

js 复制代码
const obj = { a: 1 };
delete obj.a;           // true,obj 变为 {}
delete obj.nonexist;    // true!删不存在的东西也算"成功"
delete Math.PI;         // false,configurable: false 的属性删不掉
delete someVar;         // 删变量:严格模式 SyntaxError,非严格模式静默失败返回 false
delete obj?.a;          // ES2020 起合法,obj 为 null 时短路返回 true

五个要点:

  1. 只影响自身属性 。原型上有同名属性时,删除自身属性后访问会"穿透"到原型------删除不等于屏蔽:

    js 复制代码
    Object.prototype.x = 'inherited';
    const o = { x: 'own' };
    delete o.x;
    o.x;   // 'inherited'
  2. 删数组元素别用 delete 。它留下空洞而非缩短长度:

    js 复制代码
    const arr = [1, 2, 3];
    delete arr[1];   // [1, <1 empty item>, 3],length 仍是 3

    空洞会让 forEach/map/filter 跳过该位置,让 arr[1] 返回 undefined 但 1 in arr 为 false。原地删用 splice(i, 1),生成新数组用 filter

  3. 性能 。V8 里 delete 会让对象退出隐藏类(hidden class)优化路径,后续属性访问退化为字典模式。只想"清空值"且属性会保留时,赋 nullundefined 对引擎更友好------属性还在,对象形状不变。

  4. 返回值语义。true 不代表删掉了什么(本来不存在也 true);真正的失败(不可配置属性)返回 false,严格模式下直接抛 TypeError。

  5. 不可删除的东西 :变量声明、函数声明、configurable: false 的属性、用 Object.defineProperty 显式定义且不可配置的属性。


十、逗号、展开/剩余、分组及其他语法级运算符

10.1 逗号运算符 ,

从左到右依次求值,返回最后一个表达式的值:

js 复制代码
let x = (1, 2, 3);   // x = 3,前两个被求值后丢弃
let y = (sideEffect(), 'result');   // y = 'result',副作用先执行

必须区分:let a = 1, b = 2 里的逗号是声明分隔符f(x, y) 里的是实参分隔符[1, 2] 里的是数组元素分隔符------这些都不是逗号运算符。只有出现在表达式语境、把多个表达式串起来的那个逗号才是。

它如今几乎唯一的正当用途是 for 循环同步维护多个变量:

js 复制代码
for (let i = 0, j = n - 1; i < j; i++, j--) {
  [arr[i], arr[j]] = [arr[j], arr[i]];   // 双指针相向而行
}

逗号运算符优先级全场最低,a = 1, 2 解析为 (a = 1), 2 而非 a = (1, 2)

10.2 展开与剩余 ...

严格说是语法而非运算符,但与运算符体系紧密相关,且同一符号在不同位置语义相反,放在一起讲最清楚。

展开(Spread)位置------把可迭代对象或对象属性"摊开":

js 复制代码
// 数组字面量
const merged = [...arr1, ...arr2];
const copy = [...original];           // 浅拷贝
const withNew = [...arr, newItem];

// 函数调用实参
Math.max(...nums);
new Date(...parts);

// 对象字面量(ES2018)
const config = { ...defaults, ...userConfig };   // 后者覆盖前者

// 字符串可迭代
[...'abc'];   // ['a', 'b', 'c'],正确处理 Unicode 代理对

剩余(Rest)位置------把多个元素"收集":

js 复制代码
function sum(...nums) {                      // 收集实参为数组
  return nums.reduce((a, b) => a + b, 0);
}
const [first, ...others] = [1, 2, 3, 4];     // others = [2, 3, 4]
const { a, ...restObj } = { a: 1, b: 2, c: 3 };   // restObj = { b: 2, c: 3 }

对象剩余有个贴心的规范细节:剩余变量自动排除已解构的键,这正是"从 props 里剥离个别属性、其余透传"模式的基础:

js 复制代码
const { onClick, ...restProps } = props;
<Button {...restProps} />;   // onClick 已被剥离

三条注意事项:展开是 的,嵌套对象仍共享引用,深拷贝用 structuredClone()(现代浏览器与 Node 17+ 原生支持);对象展开会触发 getter{ ...obj } 拿到的是 getter 的返回值而非 getter 本身;剩余参数必须是函数的最后一个 参数,function f(...a, b) 是语法错误(但解构里 rest 同样必须在最后,规则一致)。

10.3 分组 ()

括号在运算符体系中扮演四种角色:

  1. 提升优先级、消除歧义------优先级焦虑的万能解药;
  2. 把对象字面量从语句位置拯救出来 :行首裸写 { a: 1 } 是带标签 a 的代码块,({ a: 1 }) 才是对象表达式。箭头函数返回对象字面量必须包裹:() => ({ a: 1 }),写成 () => { a: 1 } 会被解析为函数体加标签语句;
  3. 包裹箭头函数参数(a, b) => a + b,单参数可省 a => a,无参数写 ()
  4. IIFE(function () { ... })()

10.4 newnew.target

new Constructor(args) 执行构造流程:创建对象、链接原型、以新对象为 this 执行构造器、返回对象(构造器返回原始值时忽略之)。优先级上,带参数的 new 与函数调用同级且左结合,new Foo().bar() 解析为 (new Foo()).bar();不带参数时括号可省略(new Date 合法),但风格指南普遍要求写全 new Date()

new.target 是函数内的元属性:被 new 调用时指向实际使用的构造器,普通调用时为 undefined。两个经典用途------防御性检查和抽象基类:

js 复制代码
function Foo() {
  if (!new.target) throw new Error('Foo 必须用 new 调用');
}

class Shape {
  constructor() {
    if (new.target === Shape) throw new Error('抽象类不可直接实例化');
  }
}
class Circle extends Shape {}   // new Circle() 时 new.target 是 Circle,放行

10.5 awaityield

await 暂停 async 函数执行直到 Promise 落定,返回兑现值;Promise 被拒绝时异常在 await 处抛出,可以被 try/catch 捕获。它对非 Promise 值也合法,等效 Promise.resolve(value) 包一层------但这会多花微任务周期,性能敏感的循环里别对纯同步值 await。

使用位置限制:只能在 async 函数体内,或 ES Module 的顶层(Top-level await,ES2022)。

它的优先级坑前文提过,这里给出完整对策:

js 复制代码
await a + b;      // (await a) + b ------ 几乎必然不是你想要的
await (a + b);    // ✅ 需要整体等待就加括号

// 并发 vs 串行
const [x, y] = await Promise.all([fetchA(), fetchB()]);  // ✅ 并发,总耗时 = max
const m = await fetchA();
const n = await fetchB();   // ⚠️ 串行,总耗时 = 相加

yield 暂停生成器并产出值,yield* 委托给另一个可迭代对象或生成器:

js 复制代码
function* inner() { yield 1; yield 2; }
function* outer() {
  yield* inner();      // 委托生成器
  yield* [3, 4];       // 委托任意可迭代对象
}
[...outer()];          // [1, 2, 3, 4]

yield 还能接收 值------const v = yield x 中 v 是下次 next(v) 传入的参数,这是生成器双向通信的基础,也是早年 co 库实现 async/await 的原理。

10.6 其他值得点名的

动态 import() :函数形式(不是运算符,虽然长得像调用),返回 Promise,是代码分割的入口:const module = await import('./heavy.js')

标签语句label: for (...) 配合 break label / continue label 跳出多层循环。冷门但合法,是处理嵌套循环搜索的最直接手段。

in 在 for 循环里的形态for (const k in obj) 遍历可枚举属性(含原型链,通常要配 hasOwn 过滤);遍历数组用 for...of(按值)或普通索引循环,for...in 遍历数组拿到的是字符串索引且顺序无保证,是明确的反模式。


十一、优先级与结合性

11.1 完整优先级表

从高到低(MDN 编号体系,同级别从左到右结合,标注"右"的除外):

级别 运算符 说明 结合性
21 () 分组 ---
20 obj.prop obj?.prop obj[expr] new F(args) f(args) 成员访问与调用
19 new F(不带参数) ---
18 a++ a-- 后缀自增减 ---
17 ! ~ ++a --a +a -a typeof void delete await 一元
16 **
15 * / %
14 + -
13 << >> >>>
12 < <= > >= in instanceof 关系
11 == != === !== 相等
10 & 位与
9 ^ 位异或
8 ` ` 位或
7 && 逻辑与
6 ` `
5 ?? 空值合并
4 cond ? a : b 条件
3 = += **= &&= ` = ??=...yield yield*`
2 ...(对象/数组字面量中的展开) ---
1 , 逗号

几个结构性观察:一元运算符(17 级)整体高于一切二元运算;乘除模(15)高于加减(14)符合数学习惯;相等(11)低于关系(12),所以 a < b === c < d 碰巧符合直觉;逻辑三兄弟 &&(7) > ||(6) > ??(5) 依次递减;赋值(3)几乎垫底,所以 x = a + b * c 永远不用操心。

11.2 实际会踩的坑

typeof 先于加法

js 复制代码
typeof 1 + 2;      // 'number2'!先 typeof 1,再字符串拼接
typeof (1 + 2);    // 'number' ✅

一元负号撞上幂运算

js 复制代码
-2 ** 2;      // SyntaxError,规范直接禁止
-(2 ** 2);    // -4
(-2) ** 2;    // 4

&& 先于 ||

js 复制代码
a || b && c;       // a || (b && c)
(a || b) && c;     // 意图不同时必须显式括号

await 先于一切二元运算

js 复制代码
await fetch(a) + await fetch(b);
// (await fetch(a)) + (await fetch(b))
// Response + Response → '[object Response][object Response]'
// 不报错,静默产出垃圾字符串------最难查的那类 bug

三元右结合

js 复制代码
true ? 'a' : true ? 'b' : 'c';
// 解析为 true ? 'a' : (true ? 'b' : 'c'),结果 'a'

幂右结合

js 复制代码
2 ** 3 ** 2;   // 512,不是 64

赋值右结合

js 复制代码
a = b = c = 1;   // a = (b = (c = 1))

in/instanceof 与比较同级

js 复制代码
'x' in obj === true;   // ('x' in obj) === true,碰巧符合直觉,但别依赖

11.3 工程态度

优先级表没人应该背,也没人需要背。正确的态度是三句话:

  1. 不确定就加括号(a && b) || c 永远好于 a && b || c,哪怕两者等价------括号是给下一个读代码的人(包括三个月后的你)的礼物。
  2. 开 lint 兜底no-mixed-operators 强制混合运算符加括号;no-voidno-bitwiseno-plusplus 等规则按团队口味取舍;@typescript-eslint 系列对 await 优先级也有专项规则。
  3. 复杂就拆 。一个表达式复杂到需要查优先级表,说明它应该被拆成几个命名良好的中间变量。变量名本身就是文档,const isAdmin = user?.role === 'admin' 永远好过把这段逻辑内联进三层三元里。

十二、陷阱总清单

把全文的坑收拢成一张速查表:

# 陷阱 表现 对策
1 + 意外拼接 1 + '2''12' 运算前确认类型;显式 Number() / String()
2 == 隐式转换 [] == ![] 为 true 一律 ===;判空专用 == null
3 typeof null 'object' 判 null 用 x === null
4 typeof 分不清对象子类型 数组/日期都是 'object' Array.isArrayObject.prototype.toString
5 typeof 撞 TDZ typeof x 抛 ReferenceError let/const 声明前不访问
6 NaN 不等于自身 NaN === NaN 为 false Number.isNaN(),不是全局 isNaN
7 indexOf 找不到 NaN [NaN].indexOf(NaN) 为 -1 includes
8 ` ` 吞掉合法 falsy
9 数组是 truthy if ([]) 为真,![] 为假 判数组内容用 length
10 && 渲染出 0 {count && <C/>} 页面出现 0 左侧先转布尔或用三元
11 浮点精度 0.1 + 0.2 !== 0.3 金额用整数分 / BigInt / decimal 库
12 '10' < '9' 字符串按码元比较 数值比较前显式转数字
13 delete arr[i] 产生空洞,length 不变 splice / filter
14 instanceof 跨 realm iframe 数组判 false Array.isArray
15 可选链只护一段 a?.b.c 在 b 缺失时抛错 拿不准就每环 ?.
16 a?.b = 1 SyntaxError 可选链不能做赋值目标
17 await a + b await 优先级高于 + await (a + b) 或分别 await
18 null 参与大小比较 null > 0 false 但 null >= 0 true 比较前先判空
19 parseInt 缺基数 遗留环境八进制歧义 永远 parseInt(str, 10)
20 -2 ** 2 SyntaxError 显式括号表态
21 typeof 1 + 2 'number2' typeof (1 + 2)
22 {} + [] 结果看环境 控制台 0,表达式里是字符串 理解语句/表达式位置的解析差异
23 ** 右结合 2 ** 3 ** 2 是 512 不是 64 拿不准加括号
24 展开是浅拷贝 嵌套对象仍共享引用 深拷贝用 structuredClone

十三、实践清单

最后落到日常开发的操作层面。

默认选择 。相等用 ===;判 null/undefined 用 x == nullx ?? fallback;默认值赋值用 ??=;防御访问用 ?.;布尔规范化用 Boolean()!!;数字转换用 Number()parseInt(s, 10);数组判断用 Array.isArray;自身属性判断用 Object.hasOwn;NaN 判断用 Number.isNaN

ESLint 配置建议eqeqeq: ["error", "smart"](放行 == null);no-mixed-operators: "error";TypeScript 项目加 @typescript-eslint/no-floating-promises(配合 void 前缀逃生门);no-bitwise 按团队情况选择------图形、权限位项目关掉,纯业务项目开启防炫技。

BigInt 备忘 。字面量后缀 n9007199254740993n);与 Number 不能混算(1n + 1 抛 TypeError),显式转换 Number(bigintValue) / BigInt(numberValue);除法向零取整(7n / 2n === 3n);不支持 >>>typeof 10n === 'bigint'。超过 2⁵³−1 的 ID(后端返回的雪花 ID 就该用字符串或 BigInt 接,用 Number 接必丢精度)。

Code review 重点扫描位 。任何出现 ==(非 null 判断)、|| 做默认值、+ 混合类型、delete、位运算、多环属性访问的地方,都值得多看一眼。这五类覆盖了绝大多数运算符相关事故。

相关推荐
广州灵眸科技有限公司1 小时前
瑞芯微(EASY EAI)RV1126B display
开发语言·数据库·人工智能·科技·嵌入式硬件
极客互动API1 小时前
企业微信 AI 智能客服实战:Spring Boot+Vue 接入豆包、扣子、DeepSeek
java·人工智能·微信·企业微信
今天AI了吗1 小时前
Codex使用技巧:深度解析 Plan Mode 与 Goal Mode
java·网络·人工智能·架构·java-ee
Huangxy__1 小时前
java+ai 全栈项目
java·开发语言
Nil2081 小时前
leetcode 79单词搜索
开发语言·c#
Blockchina1 小时前
AI Agent 写权限安全吗?Codex、Claude Code 与 GitHub Copilot 最小权限实战指南
开发语言·php
阿维的博客日记1 小时前
Example API指南2
java·mybatis
ctlover1 小时前
网络编程和多线程
开发语言·网络·python
神仙别闹1 小时前
基于C#实现(WinForm)文件管理系统
java·开发语言·c#