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.5 实战:类型转换面试题精讲](#4.5 实战:类型转换面试题精讲)
-
- [面试题 1:以下代码的输出是什么?](#面试题 1:以下代码的输出是什么?)
- [面试题 2:如何让 `a == 1 && a == 2 && a == 3` 为 `true`?](#面试题 2:如何让
a == 1 && a == 2 && a == 3为true?) - [面试题 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()三大显式转换函数的完整规则 - 理解隐式类型转换的触发时机与底层算法(
ToPrimitive、ToNumber、ToString、ToBoolean) - 深入理解
==与===的本质区别 - 了解
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(非零数字)
记忆口诀 :
false、0、-0、0n、""、null、undefined、NaN是 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 中最容易出错的运算符之一,因为它既能做数字加法 ,也能做字符串拼接。
规则:
- 如果任一操作数是字符串,则将另一个操作数转为字符串,执行拼接。
- 如果任一操作数是对象,先调用
ToPrimitive将其转为原始值。 - 否则,两个操作数都转为数字,执行加法。
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 条件判断中的隐式转换
if、while、三元运算符 ? : 等需要布尔值的场景,会自动调用 ToBoolean 进行转换。
javascript
// 这些值在条件判断中都是 falsy
if (0) { /* 不会执行 */ }
if ('') { /* 不会执行 */ }
if (null) { /* 不会执行 */ }
if (undefined) { /* 不会执行 */ }
if (NaN) { /* 不会执行 */ }
// 这些值在条件判断中都是 truthy
if ('0') { /* 会执行 */ }
if ([]) { /* 会执行 */ }
if ({}) { /* 会执行 */ }
if (-1) { /* 会执行 */ }
常见 Bug :
if (obj)只能判断obj是否为null或undefined,无法判断是否为空对象{}。要判断空对象,需要使用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 选择建议
| 对比 | == 宽松相等 |
=== 严格相等 |
|---|---|---|
| 类型转换 | 会 | 不会 |
NaN 与 NaN |
false |
false |
+0 与 -0 |
true |
true |
null 与 undefined |
true |
false |
| 对象比较 | 引用地址 | 引用地址 |
| 推荐程度 | 仅 == null 判空时使用 |
始终推荐使用 |
最佳实践 :始终使用
===进行比较。唯一的例外是用value == null同时判断null和undefined,这等价于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 == 3 为 true?
答案 :利用对象的 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。[] == false为true,但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() | 严格解析,无法解析返回 NaN,null → 0,undefined → NaN |
| String() | 几乎所有值都能成功转换,对象调用 toString() |
| Boolean() | 8 个 falsy 值(false、0、-0、0n、""、null、undefined、NaN),其余全 truthy |
| 隐式转换 | + 遇字符串则拼接,-*/% 强制转数字,条件判断走 ToBoolean |
| ToPrimitive | 对象转原始值的底层算法,Symbol.toPrimitive 优先级最高 |
== |
会做类型转换,规则复杂,容易产生反直觉结果 |
=== |
不做类型转换,类型不同直接 false,始终推荐使用 |
Object.is() |
修复 NaN === NaN 和 +0 === -0 两个边界问题 |
一句话总结 :JavaScript 的类型转换看似灵活,实则有严格的规则体系。掌握
ToPrimitive、ToNumber、ToString、ToBoolean四套转换算法,就能解释所有"诡异"的行为。日常开发中坚持使用===,类型转换交给显式调用,代码会更安全、更可读。
下篇预告 :第5章:运算符与表达式 --- 我们将学习 JavaScript 的各种运算符、表达式求值规则、短路求值、空值合并运算符以及运算符优先级等核心概念。
作者 :xhmico | CSDN 博客 :https://blog.csdn.net/xhmico
参考资源 :The Modern JavaScript Tutorial | MDN Web Docs | ECMAScript 规范