== 的每一步都写在规范里: ToPrimitive → 偏好拼接还是数字 → 逐分支可比 — 乱但不神秘
JS 只有 7 种类型(6 原始 + object), 但运算符会自作主张地把类型掰到能算为止 — 这就是隐式转换。它不神秘: 每一步都写在规范里。ToPrimitive 负责"对象怎么变原始值"(自定义钩子 → valueOf → toString), + 看到字符串就拼接、否则转数字, 其他算术符只走数字, == 在类型不同时按固定优先级掰扯。
工程上的结论反而简单: 用 === 消灭歧义, 用显式转换表达意图。需要判空用 x == null 这一处约定俗成; 需要判 NaN 用 Number.isNaN; 涉及钱就别用浮点。真正的高手不是背下 []+{} 的答案, 而是让代码里根本不会出现这种表达式。
typeof null; // → 'object' 历史遗留 typeof NaN; // → 'number' typeof undeclared; // → 'undefined' 不抛错
Symbol.toPrimitive 优先, 然后 valueOf, 再 toString; hint 是 number/string/default。 const o = { valueOf: () => 10, toString: () => 'ten' }; o + 1; // → 11 default 偏向 valueOf `${o}`; // → 'ten' string hint 偏向 toString
Boolean('false'); // → true 非空字符串恒真 Boolean([]); // → true Boolean(0n); // → false BigInt 零是假值
'1' == true; // → true true→1, '1'→1 '' == 0; // → true ''→0 null == 0; // → false 特判: null 只等于 undefined
Number.isNaN。 Number.isNaN(NaN); // → true Number('abc'); // → NaN 而不是报错 NaN === NaN; // → false
Object.is 能区分且能判 NaN。 Object.is(-0, 0); // → false Object.is(NaN, NaN); // → true === 都做不到 (1/-0) === -Infinity; // → true 探测 -0 的老办法
Number('42px'); // → NaN 严格 parseInt('42px'); // → 42 宽松 parseInt('08'); // → 8 必须 parseInt(s, 10) 防意外
[1,2] + ''; // → '1,2' ({}) + ''; // → '[object Object]' [[]] + [[]]; // → '' 两边都 join 成空串
0 || 10; // → 10 0 被误伤 0 ?? 10; // → 0 只兜 null/undefined '' ?? 'd'; // → ''
0.1 + 0.2; // → 0.30000000000000004 Math.round(0.1*100 + 0.2*100); // → 30 分化思路 (0.1 + 0.2).toFixed(2); // → '0.30' 展示层修
JSON.stringify({ a: undefined, b: null });
// → {"b":null} undefined 键直接消失!1n + 1; // → TypeError 1n + BigInt(1); // → 2n '' + Symbol(); // → TypeError
输入框拿到的永远是字符串, "0" 被当 falsy 跳过校验是高频事故。统一走"转数字 + Number.isNaN"管线:
function parseAmount(raw) { const n = Number(raw); // 严格: '12a' 直接 NaN if (Number.isNaN(n) || n <= 0) { return { ok: false, msg: '金额必须为正数' }; } return { ok: true, cents: Math.round(n * 100) }; } parseAmount('0'); // → { ok:false } "0" 不再被 falsy 误判跳过
订单合计 0.1+0.2 事故。前端只做展示层换算, 金额运算全部走"分":
const prices = [1990, 350, 100]; // 单位: 分 const totalCents = prices.reduce((s, p) => s + p, 0); const display = (totalCents / 100).toFixed(2); // '24.40' // 折扣等乘法后必须取整回分: const discounted = Math.round(totalCents * 0.85); // 永远是整数分
环境变量/query 全是字符串, Boolean('false') 是 true。专门的解析函数:
function parseBool(v) { if (typeof v === 'boolean') return v; return ['1', 'true', 'yes', 'on'].includes(String(v).toLowerCase()); } parseBool(process.env.DEBUG); // 'false' → false ✓ // 反例: Boolean('false') → true, 线上 DEBUG 永远开着
可选参数"没传"与"显式传 null"都要兜住, 双重判断啰嗦, 约定俗成用 ==:
function create(name, opts) { if (opts == null) opts = {}; // 同时兜 undefined 与 null // 等价于 opts === undefined || opts === null, 但短 // 现代写法: opts ??= {} (逻辑空赋值) }
filter+indexOf 去重遇到 NaN 全乱(NaN!==NaN), Set 的 SameValueZero 连 NaN 都能正确去重:
const dirty = [1, NaN, 2, NaN, 1]; [...new Set(dirty)]; // → [1, NaN, 2] ✓ NaN 只留一个 dirty.filter((v, i) => dirty.indexOf(v) === i); // → [1, NaN, NaN, 2] indexOf 永远找不到第二个 NaN
组件依赖比较用 Object.is 语义: NaN 状态不再误判"没变", -0/0 切换也能触发渲染:
Object.is(NaN, NaN); // → true setState(NaN) 不再无限渲染 Object.is(-0, 0); // → false 温度跨零点的场景能正确更新 // 自己写 deepEqual 时, 叶子节点比较也用 Object.is
new Date('2024-03-01') 按 UTC 解析, '2024/03/01' 按本地时区 — 差 8 小时的事故:
new Date('2024-03-01').getHours(); // 8 (UTC 零点 = 东八 8 点) new Date('2024/03/01').getHours(); // 0 (本地零点) // 对: 统一手动拆解或显式带时区 new Date('2024-03-01T00:00:00+08:00'); // 意图明确
同一笔订单从两条链路来, id 一个是 number 一个是 string, 幂等表出现两条记录。入口统一规范化:
const normId = (id) => String(id); // 统一字符串键 const seen = new Set(); function handleOrder(o) { const k = normId(o.id); if (seen.has(k)) return { dup: true }; seen.add(k); return process(o); } handleOrder({ id: 1001 }); handleOrder({ id: '1001' }); // → 第二次被识别为重复 ✓
日志/埋点把对象直接塞进模板串, 落库全是 [object Object]。显式序列化:
// 错: log(`payload=${payload}`); // → payload=[object Object] // 对: log(`payload=${JSON.stringify(payload)}`); // 更稳: 结构化日志直接传对象, 由日志库序列化 logger.info('order_submit', { payload });
深度配置 { timeout: 0 } 被 || 默认值覆盖成 3000, 0 语义丢失。?? 保住合法假值:
const cfg = { timeout: 0, prefix: '', retry: null }; cfg.timeout ?? 3000; // → 0 合法的"不等"被保留 cfg.prefix ?? '/api'; // → '' 空前缀是合法配置 cfg.retry ?? 3; // → 3 null 才用默认 // 组合: a?.b?.c ?? default 一行完成深取值+默认
Number() 后再算。 '1' + 1; // → '11' '1' - 1; // → 0 // 对: Number('1') + 1; // → 2 意图明确
JSON.stringify。 [] + {}; // → '[object Object]'
{} + []; // 语句开头 {} 被当块 → 0 (控制台差异)v === null 或 Object.is。 typeof null === 'object'; // true, 但 null 不是对象 // 对: v === null;
Number.isNaN / Object.is。 arr.indexOf(NaN); // → -1 永远找不到 arr.includes(NaN); // → true includes 用 SameValueZero
Boolean('false'); // → true Boolean('0'); // → true
0.1 + 0.2 === 0.3; // → false Math.abs(0.1+0.2-0.3) < Number.EPSILON; // → true 误差容忍比较
??。 volume ?? 50; // 0 音量被保留 timeout ?? 3000; // 0 超时被保留
new Boolean(false) ? a : b; // 走 a! 对象是真值 typeof new Number(1); // → 'object'
parseInt('08'); // 多数现代引擎 8, 但别赌 parseInt('08', 10); // → 8 明确十进制
'' == 0; '0' == 0; [] == 0; [] == ''; // 全 true // 生产代码里看到 == 就是 code smell
null >= 0; // → true null→0 null == 0; // → false 特判 null > 0; // → false
JSON.stringify({ a: undefined }); // → '{}'
JSON.stringify({ a: null }); // → '{"a":null}'new Date('2024-03-01'); // UTC 零点 new Date(2024, 2, 1); // 本地零点, 月份从 0 起!
new Date(2024, 2, 1); // → 2024-03-01 2 表示三月
String(sym) 或 sym.description。 '' + Symbol('id'); // → TypeError String(Symbol('id')); // → 'Symbol(id)'
1n + 1; // → TypeError 1n + BigInt(1); // → 2n
Boolean(a || b) 或 !!。 const has = user.tag || ''; // has 是 string 不是 boolean // 对: const has = !!(user.tag);
Number.isNaN。 isNaN('abc'); // → true 'abc'→NaN Number.isNaN('abc'); // → false 不是 NaN 类型才 true
`data: ${obj}`; // → data: [object Object] `data: ${JSON.stringify(obj)}`; // ✓
Array.from({length}, fn)。 new Array(3).map(fn); // → [empty × 3] map 不跑 Array.from({ length: 3 }, fn); // → [f(0), f(1), f(2)]