JavaScript|第4章:类型转换

JavaScript|第4章:类型转换

类型转换

  • JavaScript|第4章:类型转换
    • 引言
    • [4.1 显式类型转换(强制类型转换)](#4.1 显式类型转换(强制类型转换))
      • [4.1.1 Number():转换为数字](#4.1.1 Number():转换为数字)
      • [4.1.2 String():转换为字符串](#4.1.2 String():转换为字符串)
      • [4.1.3 Boolean():转换为布尔值](#4.1.3 Boolean():转换为布尔值)
    • [4.2 隐式类型转换(自动类型转换)](#4.2 隐式类型转换(自动类型转换))
      • [4.2.1 触发隐式转换的场景](#4.2.1 触发隐式转换的场景)
      • [4.2.2 ToPrimitive 抽象算法](#4.2.2 ToPrimitive 抽象算法)
      • [4.2.3 + 运算符的类型转换陷阱](#4.2.3 + 运算符的类型转换陷阱)
      • [4.2.4 - * / % 运算符](#4.2.4 - * / % 运算符)
      • [4.2.5 条件判断中的隐式转换](#4.2.5 条件判断中的隐式转换)
      • [4.2.6 隐式转换汇总表](#4.2.6 隐式转换汇总表)
    • [4.3 == vs === 的本质区别](#4.3 == vs === 的本质区别)
      • [4.3.1 === 严格相等](#4.3.1 === 严格相等)
      • [4.3.2 == 宽松相等](#4.3.2 == 宽松相等)
      • [4.3.3 == 的传递性问题](#4.3.3 == 的传递性问题)
      • [4.3.4 选择建议](#4.3.4 选择建议)
    • [4.4 Object.is() 的特殊用途](#4.4 Object.is() 的特殊用途)
      • [4.4.1 === 的两个"缺陷"](#4.4.1 === 的两个"缺陷")
      • [4.4.2 Object.is() 的行为](#4.4.2 Object.is() 的行为)
      • [4.4.3 三种相等性比较汇总](#4.4.3 三种相等性比较汇总)
      • [4.4.4 实际应用场景](#4.4.4 实际应用场景)
    • [4.5 实战:类型转换面试题精讲](#4.5 实战:类型转换面试题精讲)
      • [面试题 1:以下代码的输出是什么?](#面试题 1:以下代码的输出是什么?)
      • [面试题 2:如何让 `a == 1 && a == 2 && a == 3` 为 `true`?](#面试题 2:如何让 a == 1 && a == 2 && a == 3true?)
      • [面试题 3:以下表达式的结果分别是什么?](#面试题 3:以下表达式的结果分别是什么?)
      • [面试题 4:`typeof` 与类型转换的关系](#面试题 4:typeof 与类型转换的关系)
      • [面试题 5:以下代码的输出顺序是什么?](#面试题 5:以下代码的输出顺序是什么?)
      • [面试题 6:如何实现一个深比较函数?](#面试题 6:如何实现一个深比较函数?)
    • 本章小结

上篇回顾第3章:变量与数据类型

下篇预告第5章:运算符与表达式


引言

在上一章中,我们学习了 JavaScript 的七种原始类型和引用类型。但 JavaScript 是一门弱类型语言------它不会要求你在使用数据之前先确认类型,而是在运行时自动帮你完成类型之间的转换。

这种"灵活"是一把双刃剑。一方面,它让代码写起来更简洁;另一方面,它也是无数诡异 Bug 的源头。你一定见过这样的代码:

javascript 复制代码
console.log([] + {});      // "[object Object]"
console.log({} + []);      // 0(在浏览器控制台中)
console.log('5' - 3);      // 2
console.log('5' + 3);      // "53"
console.log(true + true);  // 2
console.log(null == undefined); // true

如果你觉得上面这些结果"不可理喻",那恰恰说明你需要系统学习类型转换的规则。

本章目标

  • 掌握 Number()String()Boolean() 三大显式转换函数的完整规则
  • 理解隐式类型转换的触发时机与底层算法(ToPrimitiveToNumberToStringToBoolean
  • 深入理解 ===== 的本质区别
  • 了解 Object.is() 解决的边界问题
  • 通过面试题巩固所有知识点

4.1 显式类型转换(强制类型转换)

显式类型转换是指开发者主动调用 内置函数,将一个值转换为另一种类型。JavaScript 提供了三个核心的类型转换函数:Number()String()Boolean()

4.1.1 Number():转换为数字

Number() 将任意类型的值转换为 number 类型。它的规则比较严格,遇到无法解析的值一律返回 NaN

原始类型的转换规则

javascript 复制代码
// 数字 → 数字(原样返回)
console.log(Number(42));        // 42

// 字符串 → 数字(整体解析,不允许非数字字符)
console.log(Number('123'));     // 123
console.log(Number('12.5'));    // 12.5
console.log(Number(''));        // 0(空字符串转为 0)
console.log(Number(' '));       // 0(纯空白字符串也转为 0)
console.log(Number('12px'));    // NaN(包含非数字字符)
console.log(Number('12 34'));   // NaN(中间有空格)

// 布尔值 → 数字
console.log(Number(true));      // 1
console.log(Number(false));     // 0

// undefined → NaN
console.log(Number(undefined)); // NaN

// null → 0
console.log(Number(null));      // 0

对象类型的转换规则

对象转换为数字时,JavaScript 引擎会先调用对象的 valueOf()toString() 方法获取原始值,再将该原始值转为数字。

javascript 复制代码
// 数组
console.log(Number([]));        // 0([].toString() → "",Number("") → 0)
console.log(Number([42]));      // 42([42].toString() → "42")
console.log(Number([1, 2]));    // NaN([1,2].toString() → "1,2")

// 对象
console.log(Number({}));        // NaN({}.toString() → "[object Object]")

// Date 对象
console.log(Number(new Date(0))); // 0(Date 的 valueOf 返回时间戳)

底层流程 :对象 → ToPrimitive(hint: "number") → 优先调用 valueOf(),若返回的不是原始值,再调用 toString(),最后将得到的原始值用 Number() 转换。

parseInt() 与 parseFloat()

Number() 不同,parseInt()parseFloat()从左到右逐字符解析,遇到非数字字符就停止。

javascript 复制代码
console.log(parseInt('12px'));     // 12(Number() 会返回 NaN)
console.log(parseInt('0x1F'));     // 31(默认识别十六进制)
console.log(parseInt('10', 2));    // 2(按二进制解析 "10")
console.log(parseFloat('3.14abc')); // 3.14
console.log(parseFloat('abc'));    // NaN
方法 空字符串 纯空白 非数字开头 数字+单位
Number() 0 0 NaN NaN
parseInt() NaN NaN NaN 数字部分
parseFloat() NaN NaN NaN 数字部分

4.1.2 String():转换为字符串

String() 的转换规则相对简单------几乎任何值都能成功转换为字符串。

javascript 复制代码
// 数字 → 字符串
console.log(String(42));         // "42"
console.log(String(3.14));       // "3.14"
console.log(String(NaN));        // "NaN"
console.log(String(Infinity));   // "Infinity"

// 布尔值 → 字符串
console.log(String(true));       // "true"
console.log(String(false));      // "false"

// null 和 undefined → 字符串
console.log(String(null));       // "null"
console.log(String(undefined));  // "undefined"

// 对象 → 字符串(调用 toString())
console.log(String([1, 2, 3]));  // "1,2,3"
console.log(String({name: 'Alice'})); // "[object Object]"

对象的 toString() 行为

javascript 复制代码
const user = {
  name: 'Alice',
  toString() {
    return `User: ${this.name}`;
  }
};

console.log(String(user)); // "User: Alice"

如果对象自定义了 toString() 方法,String() 会优先使用它。数组的 toString() 会将元素用逗号连接。

4.1.3 Boolean():转换为布尔值

Boolean() 的转换规则最为简洁:只有 6 个 falsy 值 会被转换为 false,其余全部为 true

六个 falsy 值

javascript 复制代码
console.log(Boolean(false));     // false
console.log(Boolean(0));         // false
console.log(Boolean(-0));        // false
console.log(Boolean(0n));        // false(BigInt 的零)
console.log(Boolean(''));        // false(空字符串)
console.log(Boolean(null));      // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN));       // false

容易误判的 truthy 值

javascript 复制代码
console.log(Boolean('0'));       // true(非空字符串)
console.log(Boolean('false'));   // true(非空字符串)
console.log(Boolean([]));        // true(空数组也是 truthy)
console.log(Boolean({}));        // true(空对象也是 truthy)
console.log(Boolean(new Boolean(false))); // true(对象永远是 truthy)
console.log(Boolean(' '));       // true(包含空格的字符串)
console.log(Boolean(-1));        // true(非零数字)

记忆口诀false0-00n""nullundefinedNaN 是 falsy,其余全是 truthy。特别注意 "0"[] 是 truthy。

快捷转换技巧

javascript 复制代码
// 使用双感叹号快速转换为布尔值
console.log(!!'hello');  // true
console.log(!!0);        // false
console.log(!![]);       // true
console.log(!!null);     // false

4.2 隐式类型转换(自动类型转换)

隐式类型转换是 JavaScript 引擎在执行某些操作时自动触发的类型转换。它是很多诡异行为的根源,也是面试中的高频考点。

4.2.1 触发隐式转换的场景

JavaScript 中有三大类场景会触发隐式类型转换:
#mermaid-svg-spFfH1JZVhPgS9GI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-spFfH1JZVhPgS9GI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-spFfH1JZVhPgS9GI .error-icon{fill:#552222;}#mermaid-svg-spFfH1JZVhPgS9GI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-spFfH1JZVhPgS9GI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-spFfH1JZVhPgS9GI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-spFfH1JZVhPgS9GI .marker.cross{stroke:#333333;}#mermaid-svg-spFfH1JZVhPgS9GI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-spFfH1JZVhPgS9GI p{margin:0;}#mermaid-svg-spFfH1JZVhPgS9GI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-spFfH1JZVhPgS9GI .cluster-label text{fill:#333;}#mermaid-svg-spFfH1JZVhPgS9GI .cluster-label span{color:#333;}#mermaid-svg-spFfH1JZVhPgS9GI .cluster-label span p{background-color:transparent;}#mermaid-svg-spFfH1JZVhPgS9GI .label text,#mermaid-svg-spFfH1JZVhPgS9GI span{fill:#333;color:#333;}#mermaid-svg-spFfH1JZVhPgS9GI .node rect,#mermaid-svg-spFfH1JZVhPgS9GI .node circle,#mermaid-svg-spFfH1JZVhPgS9GI .node ellipse,#mermaid-svg-spFfH1JZVhPgS9GI .node polygon,#mermaid-svg-spFfH1JZVhPgS9GI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-spFfH1JZVhPgS9GI .rough-node .label text,#mermaid-svg-spFfH1JZVhPgS9GI .node .label text,#mermaid-svg-spFfH1JZVhPgS9GI .image-shape .label,#mermaid-svg-spFfH1JZVhPgS9GI .icon-shape .label{text-anchor:middle;}#mermaid-svg-spFfH1JZVhPgS9GI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-spFfH1JZVhPgS9GI .rough-node .label,#mermaid-svg-spFfH1JZVhPgS9GI .node .label,#mermaid-svg-spFfH1JZVhPgS9GI .image-shape .label,#mermaid-svg-spFfH1JZVhPgS9GI .icon-shape .label{text-align:center;}#mermaid-svg-spFfH1JZVhPgS9GI .node.clickable{cursor:pointer;}#mermaid-svg-spFfH1JZVhPgS9GI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-spFfH1JZVhPgS9GI .arrowheadPath{fill:#333333;}#mermaid-svg-spFfH1JZVhPgS9GI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-spFfH1JZVhPgS9GI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-spFfH1JZVhPgS9GI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-spFfH1JZVhPgS9GI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-spFfH1JZVhPgS9GI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-spFfH1JZVhPgS9GI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-spFfH1JZVhPgS9GI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-spFfH1JZVhPgS9GI .cluster text{fill:#333;}#mermaid-svg-spFfH1JZVhPgS9GI .cluster span{color:#333;}#mermaid-svg-spFfH1JZVhPgS9GI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-spFfH1JZVhPgS9GI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-spFfH1JZVhPgS9GI rect.text{fill:none;stroke-width:0;}#mermaid-svg-spFfH1JZVhPgS9GI .icon-shape,#mermaid-svg-spFfH1JZVhPgS9GI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-spFfH1JZVhPgS9GI .icon-shape p,#mermaid-svg-spFfH1JZVhPgS9GI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-spFfH1JZVhPgS9GI .icon-shape .label rect,#mermaid-svg-spFfH1JZVhPgS9GI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-spFfH1JZVhPgS9GI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-spFfH1JZVhPgS9GI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-spFfH1JZVhPgS9GI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 隐式类型转换触发场景
算术运算
比较运算
逻辑运算与条件判断

  • 运算符(字符串拼接 vs 数字加法)
  • * / % 运算符(强制转数字)
    == 宽松相等
    < > <= >= 比较
    if / while / ? : 条件
    && || ! 逻辑运算

4.2.2 ToPrimitive 抽象算法

当 JavaScript 需要将对象转换为原始值时,会调用内部的 ToPrimitive 算法。这是理解隐式转换的关键。

javascript 复制代码
// ToPrimitive 的调用过程(伪代码)
function ToPrimitive(obj, hint) {
  // hint 可以是 "number"、"string" 或 "default"
  
  // 1. 如果 obj[Symbol.toPrimitive] 存在,调用它
  if (obj[Symbol.toPrimitive]) {
    const result = obj[Symbol.toPrimitive](hint);
    if (typeof result !== 'object') return result;
    throw TypeError;
  }
  
  // 2. 如果没有 Symbol.toPrimitive:
  if (hint === 'string') {
    // 先 toString(),再 valueOf()
    return toStringFirst(obj);
  } else {
    // 先 valueOf(),再 toString()
    return valueOfFirst(obj);
  }
}

实际示例

javascript 复制代码
const obj = {
  valueOf() {
    console.log('valueOf called');
    return 42;
  },
  toString() {
    console.log('toString called');
    return 'hello';
  }
};

// 数字上下文:先调用 valueOf
console.log(obj + 1);  // valueOf called → 43

// 字符串上下文:先调用 toString
console.log(`value: ${obj}`); // toString called → "value: hello"

4.2.3 + 运算符的类型转换陷阱

+ 运算符是 JavaScript 中最容易出错的运算符之一,因为它既能做数字加法 ,也能做字符串拼接

规则

  1. 如果任一操作数是字符串,则将另一个操作数转为字符串,执行拼接。
  2. 如果任一操作数是对象,先调用 ToPrimitive 将其转为原始值。
  3. 否则,两个操作数都转为数字,执行加法。
javascript 复制代码
// 数字加法
console.log(1 + 2);          // 3

// 字符串拼接(有一个操作数是字符串)
console.log('1' + 2);        // "12"
console.log(1 + '2');        // "12"

// 布尔值参与加法
console.log(true + 1);       // 2(true → 1)
console.log(false + 1);      // 1(false → 0)

// null 和 undefined
console.log(null + 1);       // 1(null → 0)
console.log(undefined + 1);  // NaN(undefined → NaN)

经典陷阱

javascript 复制代码
// 对象 + 原始值
console.log([] + 1);         // "1"([].toString() → "","" + 1 → "1")
console.log([2] + 1);        // "21"([2].toString() → "2","2" + 1 → "21")
console.log({} + []);        // 0 或 "[object Object]"(取决于执行环境)

// 一元 + 运算符(强制转数字)
console.log(+'42');          // 42
console.log(+true);          // 1
console.log(+[]);            // 0
console.log(+{});            // NaN
console.log(+'');            // 0

注意{} 在语句开头会被解析为空代码块(block),而非空对象。所以 {} + [] 在浏览器控制台中等价于 +[],结果为 0。但在表达式上下文中(如 console.log({} + [])),{} 是对象字面量,结果为 "[object Object]"

4.2.4 - * / % 运算符

+ 不同,减、乘、除、取模运算符会强制将两个操作数转为数字

javascript 复制代码
console.log('5' - 3);        // 2("5" → 5)
console.log('5' * 2);        // 10
console.log('6' / 2);        // 3
console.log('10' % 3);       // 1
console.log('abc' - 1);      // NaN("abc" 无法转为数字)
console.log([] - 1);         // -1([].toString() → "","" → 0,0 - 1 = -1)
console.log([5] - 1);        // 4([5].toString() → "5",5 - 1 = 4)

4.2.5 条件判断中的隐式转换

ifwhile、三元运算符 ? : 等需要布尔值的场景,会自动调用 ToBoolean 进行转换。

javascript 复制代码
// 这些值在条件判断中都是 falsy
if (0) { /* 不会执行 */ }
if ('') { /* 不会执行 */ }
if (null) { /* 不会执行 */ }
if (undefined) { /* 不会执行 */ }
if (NaN) { /* 不会执行 */ }

// 这些值在条件判断中都是 truthy
if ('0') { /* 会执行 */ }
if ([]) { /* 会执行 */ }
if ({}) { /* 会执行 */ }
if (-1) { /* 会执行 */ }

常见 Bugif (obj) 只能判断 obj 是否为 nullundefined,无法判断是否为空对象 {}。要判断空对象,需要使用 Object.keys(obj).length === 0

4.2.6 隐式转换汇总表

原始值 ToNumber ToString ToBoolean
undefined NaN "undefined" false
null 0 "null" false
true 1 "true" true
false 0 "false" false
""(空字符串) 0 "" false
"0" 0 "0" true
"abc" NaN "abc" true
0 0 "0" false
NaN NaN "NaN" false
Infinity Infinity "Infinity" true
[](空数组) 0 "" true
[42] 42 "42" true
{}(空对象) NaN "[object Object]" true

4.3 == vs === 的本质区别

==(宽松相等)和 ===(严格相等)的区别是 JavaScript 面试中出现频率最高的问题之一。

4.3.1 === 严格相等

=== 的规则非常简单:不做任何类型转换 ,类型不同直接返回 false,类型相同再比较值。

javascript 复制代码
console.log(1 === 1);           // true
console.log(1 === '1');         // false(类型不同)
console.log(0 === false);       // false(类型不同)
console.log(null === undefined); // false(类型不同)
console.log(NaN === NaN);       // false(NaN 不等于任何值,包括自身)
console.log(+0 === -0);         // true(+0 和 -0 被视为相等)

对象比较

javascript 复制代码
const a = {};
const b = {};
const c = a;

console.log(a === b);  // false(不同的对象引用)
console.log(a === c);  // true(同一个对象引用)

=== 比较的是引用地址,不是内容。两个内容完全相同的对象,只要不是同一个引用,=== 就返回 false

4.3.2 == 宽松相等

== 在比较之前,会先尝试类型转换,将两个操作数转换为相同类型后再比较。这正是很多"诡异"结果的来源。

== 的转换规则
#mermaid-svg-gVLYpQBp7e5jHIGF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gVLYpQBp7e5jHIGF .error-icon{fill:#552222;}#mermaid-svg-gVLYpQBp7e5jHIGF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gVLYpQBp7e5jHIGF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gVLYpQBp7e5jHIGF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gVLYpQBp7e5jHIGF .marker.cross{stroke:#333333;}#mermaid-svg-gVLYpQBp7e5jHIGF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gVLYpQBp7e5jHIGF p{margin:0;}#mermaid-svg-gVLYpQBp7e5jHIGF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF .cluster-label text{fill:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF .cluster-label span{color:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF .cluster-label span p{background-color:transparent;}#mermaid-svg-gVLYpQBp7e5jHIGF .label text,#mermaid-svg-gVLYpQBp7e5jHIGF span{fill:#333;color:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF .node rect,#mermaid-svg-gVLYpQBp7e5jHIGF .node circle,#mermaid-svg-gVLYpQBp7e5jHIGF .node ellipse,#mermaid-svg-gVLYpQBp7e5jHIGF .node polygon,#mermaid-svg-gVLYpQBp7e5jHIGF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gVLYpQBp7e5jHIGF .rough-node .label text,#mermaid-svg-gVLYpQBp7e5jHIGF .node .label text,#mermaid-svg-gVLYpQBp7e5jHIGF .image-shape .label,#mermaid-svg-gVLYpQBp7e5jHIGF .icon-shape .label{text-anchor:middle;}#mermaid-svg-gVLYpQBp7e5jHIGF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gVLYpQBp7e5jHIGF .rough-node .label,#mermaid-svg-gVLYpQBp7e5jHIGF .node .label,#mermaid-svg-gVLYpQBp7e5jHIGF .image-shape .label,#mermaid-svg-gVLYpQBp7e5jHIGF .icon-shape .label{text-align:center;}#mermaid-svg-gVLYpQBp7e5jHIGF .node.clickable{cursor:pointer;}#mermaid-svg-gVLYpQBp7e5jHIGF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gVLYpQBp7e5jHIGF .arrowheadPath{fill:#333333;}#mermaid-svg-gVLYpQBp7e5jHIGF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gVLYpQBp7e5jHIGF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gVLYpQBp7e5jHIGF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gVLYpQBp7e5jHIGF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gVLYpQBp7e5jHIGF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gVLYpQBp7e5jHIGF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gVLYpQBp7e5jHIGF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gVLYpQBp7e5jHIGF .cluster text{fill:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF .cluster span{color:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gVLYpQBp7e5jHIGF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gVLYpQBp7e5jHIGF rect.text{fill:none;stroke-width:0;}#mermaid-svg-gVLYpQBp7e5jHIGF .icon-shape,#mermaid-svg-gVLYpQBp7e5jHIGF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gVLYpQBp7e5jHIGF .icon-shape p,#mermaid-svg-gVLYpQBp7e5jHIGF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gVLYpQBp7e5jHIGF .icon-shape .label rect,#mermaid-svg-gVLYpQBp7e5jHIGF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gVLYpQBp7e5jHIGF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gVLYpQBp7e5jHIGF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gVLYpQBp7e5jHIGF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是









a == b
类型相同?
直接用 === 比较
一个是 null

一个是 undefined?
返回 true
一个是数字

一个是字符串?
字符串转数字后再比
一个是布尔值?
布尔值转数字后再比
一个是对象

一个是原始值?
对象转原始值后再比
返回 false

规则详解与示例

javascript 复制代码
// 规则1:null 和 undefined 互相宽松相等
console.log(null == undefined);    // true
console.log(null == 0);           // false(null 不会转为 0)
console.log(undefined == 0);      // false(undefined 不会转为 NaN 再比)
console.log(null == false);       // false

// 规则2:数字与字符串比较 → 字符串先转数字
console.log(1 == '1');            // true("1" → 1)
console.log(0 == '');             // true("" → 0)
console.log(2 == ' 2 ');          // true(" 2 " → 2)

// 规则3:布尔值参与比较 → 布尔值先转数字
console.log(true == 1);           // true(true → 1)
console.log(false == 0);          // true(false → 0)
console.log(true == '1');         // true(true → 1,"1" → 1)
console.log(false == '');         // true(false → 0,"" → 0)

// 规则4:对象与原始值比较 → 对象先 ToPrimitive
console.log([1] == 1);            // true([1].toString() → "1","1" → 1)
console.log([1, 2] == '1,2');     // true([1,2].toString() → "1,2")
console.log([] == false);         // true([] → "" → 0,false → 0)
console.log([] == ![]);           // true(经典面试题,见下方分析)

经典面试题:[] == ![] 为什么是 true

javascript 复制代码
// 逐步推导
// 1. ![] → false([] 是 truthy,取反后为 false)
// 2. [] == false
// 3. false 是布尔值 → 转为数字 0
// 4. [] == 0
// 5. [] 是对象 → ToPrimitive → [].toString() → ""
// 6. "" == 0
// 7. "" 是字符串 → 转为数字 0
// 8. 0 == 0 → true

4.3.3 == 的传递性问题

== 不具备传递性,这是它被广泛诟病的原因之一。

javascript 复制代码
console.log('' == 0);   // true("" → 0)
console.log(0 == []);   // true([] → "" → 0)
console.log('' == []);  // true([] → "" → "" == "")

// 但:
console.log('' == '0'); // false(同为字符串,直接比较)
console.log(0 == '0');  // true("0" → 0)

4.3.4 选择建议

对比 == 宽松相等 === 严格相等
类型转换 不会
NaNNaN false false
+0-0 true true
nullundefined true false
对象比较 引用地址 引用地址
推荐程度 == null 判空时使用 始终推荐使用

最佳实践 :始终使用 === 进行比较。唯一的例外是用 value == null 同时判断 nullundefined,这等价于 value === null || value === undefined

javascript 复制代码
// 推荐:使用 == null 判断空值
function processUser(user) {
  if (user == null) {
    // 同时覆盖 null 和 undefined
    return 'No user provided';
  }
  return `Hello, ${user.name}`;
}

4.4 Object.is() 的特殊用途

Object.is() 是 ES6 引入的方法,用于比较两个值是否完全相同 。它在 === 的基础上修复了两个边界问题。

4.4.1 === 的两个"缺陷"

javascript 复制代码
// 缺陷1:NaN 不等于自身
console.log(NaN === NaN);   // false

// 缺陷2:+0 和 -0 被视为相等
console.log(+0 === -0);     // true
console.log(1 / +0);        // Infinity
console.log(1 / -0);        // -Infinity(+0 和 -0 实际含义不同)

4.4.2 Object.is() 的行为

Object.is() 使用的是 SameValue 算法,规则如下:

javascript 复制代码
// 修复 NaN
console.log(Object.is(NaN, NaN));   // true

// 修复 +0 和 -0
console.log(Object.is(+0, -0));     // false

// 其他情况与 === 一致
console.log(Object.is(1, 1));       // true
console.log(Object.is('a', 'a'));   // true
console.log(Object.is(null, null)); // true
console.log(Object.is(undefined, undefined)); // true
console.log(Object.is({}, {}));     // false(不同引用)
console.log(Object.is(true, 1));    // false(不做类型转换)

4.4.3 三种相等性比较汇总

比较 == === Object.is()
类型转换
NaN == NaN false false true
+0 == -0 true true false
null == undefined true false false
算法名称 Abstract Equality Strict Equality SameValue

4.4.4 实际应用场景

场景1:需要精确判断 NaN

javascript 复制代码
// 以前需要 Number.isNaN()
console.log(Number.isNaN(NaN));     // true

// 现在 Object.is() 也可以
console.log(Object.is(NaN, NaN));   // true

// 注意区分:全局 isNaN() 会先做类型转换
console.log(isNaN('abc'));          // true("abc" → NaN)
console.log(Number.isNaN('abc'));   // false("abc" 不是 NaN)

场景2:区分 +0 和 -0

javascript 复制代码
function reciprocal(n) {
  if (Object.is(n, -0)) {
    return -Infinity;
  }
  return 1 / n;
}

console.log(reciprocal(0));  // Infinity
console.log(reciprocal(-0)); // -Infinity

场景3:React 中的状态比较

React 在内部使用 Object.is() 来比较 state 是否发生变化,以决定是否需要重新渲染组件。

javascript 复制代码
// React 的 useState 内部使用 Object.is 进行比较
const [count, setCount] = useState(0);
setCount(0); // 不会触发重新渲染(Object.is(0, 0) → true)
setCount(NaN); // 连续调用 setCount(NaN) 只有第一次会触发渲染

4.5 实战:类型转换面试题精讲

面试题 1:以下代码的输出是什么?

javascript 复制代码
console.log([] + []);
console.log([] + {});
console.log({} + []);
console.log({} + {});

答案与解析

javascript 复制代码
console.log([] + []);
// [].toString() → "","" + "" → ""
// 输出:""

console.log([] + {});
// [].toString() → "",{}.toString() → "[object Object]"
// "" + "[object Object]" → "[object Object]"
// 输出:"[object Object]"

console.log({} + []);
// 在语句开头,{} 被解析为空代码块
// +[] → 0
// 输出:0(浏览器控制台)
// 在表达式中:"[object Object]"

console.log({} + {});
// 同理,{} 被解析为空代码块
// +{} → NaN
// 输出:NaN(浏览器控制台)

面试题 2:如何让 a == 1 && a == 2 && a == 3true

答案 :利用对象的 ToPrimitive 机制,每次比较时返回不同的值。

javascript 复制代码
const a = {
  value: 1,
  valueOf() {
    return this.value++;
  }
};

console.log(a == 1 && a == 2 && a == 3); // true

解析 :每次 a == N 比较时,JavaScript 引擎都会调用 a.valueOf() 获取原始值。第一次调用返回 1,第二次返回 2,第三次返回 3,所以三次宽松相等比较全部为 true

也可以用 Symbol.toPrimitive 实现更精确的控制:

javascript 复制代码
const a = {
  [Symbol.toPrimitive](hint) {
    return this.value++;
  },
  value: 1
};

console.log(a == 1 && a == 2 && a == 3); // true

面试题 3:以下表达式的结果分别是什么?

javascript 复制代码
console.log(0 == false);
console.log('' == false);
console.log([] == false);
console.log(null == false);
console.log(undefined == false);

答案

javascript 复制代码
console.log(0 == false);       // true(false → 0)
console.log('' == false);      // true("" → 0,false → 0)
console.log([] == false);      // true([] → "" → 0,false → 0)
console.log(null == false);    // false(null 只 == undefined,不转其他类型)
console.log(undefined == false); // false(undefined 同理)

启示 :这就是为什么在条件判断中不应该用 == false[] == falsetrue,但 if ([]) 也进入执行(因为 [] 是 truthy),逻辑上自相矛盾。

面试题 4:typeof 与类型转换的关系

javascript 复制代码
console.log(typeof NaN);
console.log(typeof null);
console.log(typeof []);
console.log(typeof function(){});
console.log(NaN === NaN);
console.log(null === undefined);

答案

javascript 复制代码
console.log(typeof NaN);           // "number"
console.log(typeof null);          // "object"(历史 Bug)
console.log(typeof []);            // "object"
console.log(typeof function(){});  // "function"
console.log(NaN === NaN);          // false
console.log(null === undefined);   // false

面试题 5:以下代码的输出顺序是什么?

javascript 复制代码
const obj = {
  [Symbol.toPrimitive](hint) {
    console.log(`toPrimitive: ${hint}`);
    return hint === 'number' ? 42 : 'hello';
  },
  valueOf() {
    console.log('valueOf');
    return 100;
  },
  toString() {
    console.log('toString');
    return 'world';
  }
};

console.log(obj + 1);
console.log(obj - 1);
console.log(`${obj}`);

答案

javascript 复制代码
console.log(obj + 1);
// toPrimitive: default(+ 运算符 hint 为 "default")
// 输出:hello1("hello" + 1 → "hello1")

console.log(obj - 1);
// toPrimitive: number(- 运算符 hint 为 "number")
// 输出:41(42 - 1 = 41)

console.log(`${obj}`);
// toPrimitive: string(模板字符串 hint 为 "string")
// 输出:hello(直接返回 "hello")

关键点Symbol.toPrimitive 的优先级高于 valueOf()toString()。一旦定义了 Symbol.toPrimitive,后两者就不会被调用。

面试题 6:如何实现一个深比较函数?

javascript 复制代码
function deepEqual(a, b) {
  // 1. 使用 Object.is 处理 NaN、+0/-0
  if (Object.is(a, b)) return true;

  // 2. 非对象或 null 直接返回 false
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
    return false;
  }

  // 3. 获取键名并比较
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);

  if (keysA.length !== keysB.length) return false;

  // 4. 递归比较每个属性
  for (const key of keysA) {
    if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
      return false;
    }
  }

  return true;
}

// 测试
console.log(deepEqual({ x: 1, y: 2 }, { x: 1, y: 2 })); // true
console.log(deepEqual({ x: 1, y: 2 }, { x: 1, y: 3 })); // false
console.log(deepEqual([1, [2, 3]], [1, [2, 3]]));         // true
console.log(deepEqual(NaN, NaN));                          // true

本章小结

知识点 要点
Number() 严格解析,无法解析返回 NaNnull0undefinedNaN
String() 几乎所有值都能成功转换,对象调用 toString()
Boolean() 8 个 falsy 值(false0-00n""nullundefinedNaN),其余全 truthy
隐式转换 + 遇字符串则拼接,-*/% 强制转数字,条件判断走 ToBoolean
ToPrimitive 对象转原始值的底层算法,Symbol.toPrimitive 优先级最高
== 会做类型转换,规则复杂,容易产生反直觉结果
=== 不做类型转换,类型不同直接 false始终推荐使用
Object.is() 修复 NaN === NaN+0 === -0 两个边界问题

一句话总结 :JavaScript 的类型转换看似灵活,实则有严格的规则体系。掌握 ToPrimitiveToNumberToStringToBoolean 四套转换算法,就能解释所有"诡异"的行为。日常开发中坚持使用 ===,类型转换交给显式调用,代码会更安全、更可读。


下篇预告第5章:运算符与表达式 --- 我们将学习 JavaScript 的各种运算符、表达式求值规则、短路求值、空值合并运算符以及运算符优先级等核心概念。


作者 :xhmico | CSDN 博客https://blog.csdn.net/xhmico

参考资源The Modern JavaScript Tutorial | MDN Web Docs | ECMAScript 规范

相关推荐
聪慧的水蜜桃1 小时前
【YFIOs】用C#开发硬件之设备上云
开发语言·c#
yqcoder1 小时前
httpOnly 是什么,又有什么用?
开发语言·前端·javascript
烬羽2 小时前
还在手动拼路径、写回调地狱?一文吃透 Node.js 的 path 和 fs
javascript·程序员·node.js
mONESY2 小时前
LangChain JS 高性能并行执行解析、工具调用底层封装
javascript
wuqingshun3141592 小时前
重写equals而不重写hashCode,会出什么问题?
java·开发语言
飞猪~2 小时前
LangChain python 版本 第一集
开发语言·python·langchain
迅速的自行车3 小时前
从一段模板说起
前端·javascript·vue.js
YIAN3 小时前
LangChain JS 工具调用实战:基于 DeepSeek-V4-Flash 实现本地文件读取 AI 代码助手(完整可运行源码)
javascript·langchain·前端框架
尼斯湖皮皮怪3 小时前
iceCoder 记忆系统深度分析
前端·javascript