基础篇:类型转换
类型转换(Type Conversion)是指将数据由一种类型变换为另一种类型。在 JavaScript 中,在一些运算类型与预期不符的情况下,会发生隐式转换,也可以用一些写法强制要求进行显式转换。
JavaScript 中有 7 种简单数据类型(也称为基本数据类型):数字(Number)、字符串(String)、布尔值(Boolean)、Undefined、Null、ES6 新增的 Symbol,以及 ES2020 起正式纳入的 BigInt。此外还有 1 种复杂数据类型(也称引用类型):对象(Object)。
强制类型转换
强制类型转换主要是使用 Number()、String()、Boolean() 三个函数,手动将各种类型的值分别转换成数字、字符串、布尔值。
Number()
Number 对象是经过封装的处理数值的对象,主要有两个作用:一是作为构造函数,创建数字对象;二是用来将任意类型的值转换为数值。
原始类型值
- 数值:返回该数值本身。控制台把极大或极小的数打成科学计数法,那是显示用的字符串,不是
Number()又转了一遍。 - 字符串:整段都能当成数值则转换;有任何一个字符不行就返回
NaN;空字符串(以及只含空白)转为0。 - 布尔值:
true转成1;false转成0。 - undefined:转为
NaN。 - null:转为
0。 - Symbol:不能转成数字,会抛 TypeError。
- BigInt:转为 Number,字面量末尾的
n不会出现。超出安全整数范围会丢精度。
Number(324) // 324
Number('324') // 324
Number('324abc') // NaN
Number('') // 0
Number(true) // 1
Number(false) // 0
Number(undefined) // NaN
Number(null) // 0
Number(Symbol('a')) // Uncaught TypeError: Cannot convert a Symbol value to a number
Number(1n) // 1
Number(9007199254740993n) // 9007199254740992,超出安全整数,精度丢失
Number() 函数将字符串转为数值,要比 parseInt() 函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为 NaN。
另外,parseInt() 和 Number() 函数都会自动过滤一个字符串前导和后缀的空格。
对象
简单的规则是:Number() 的参数是对象时,先按 hint "number" 转成原始值再 ToNumber。空数组、只含一个数字的数组、Date(valueOf 返回时间戳)都能得到有限数字;普通 {}、多元素数组通常是 NaN。
若对象定义了 Symbol.toPrimitive,会优先调用它(见下文)。否则按下面顺序。
Number({}) // NaN
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([]) // 0
Number([5]) // 5
Number(new Date()) // 1653321770387
具体规则:
- 调用对象自身的 valueOf() 方法。如果返回原始类型的值,则使用 Number() 函数转换该值。
- 如果 valueOf() 方法返回的还是对象,则改为调用对象自身的 toString() 方法;如果 toString() 方法返回原始类型的值,则使用 Number() 函数转换该值。
- 如果 toString() 方法返回的是对象,则报错。
const obj = {}
obj.valueOf() // {}
obj.toString() // '[object Object]'
Number('[object Object]') // NaN
另外,一个对象的 valueOf() 和 toString() 方法,是可以自定义的。
const obj = {
valueOf: function () { return {}; },
toString: function () { return {}; }
};
obj.valueOf() // {}
obj.toString() // {}
Number(obj) // Uncaught TypeError: Cannot convert object to primitive value
String()
String 是一个用于字符串或一个字符序列的构造函数,另外,还可以作普通函数使用,可以将任意类型的值转化成字符串。
原始类型值
- 数值:转换为相应的字符串。
- 字符串:返回原值。
- 布尔值:true 转换为字符串 "true";false 转换为字符串 "false"。
- undefined:转换为字符串 "undefined"。
- null:转换为字符串 "null"。
- Symbol:可以显式强制转换为字符串,然而隐式强制类型转换会产生错误。
- BigInt:转为对应的十进制字符串,字面量末尾的
n不会出现。
String(123) // '123'
String('abc') // 'abc'
String(true) // 'true'
String(undefined) // 'undefined'
String(null) // 'null'
String(Symbol('a')) // 'Symbol(a)'
'a' + Symbol('a') // Uncaught TypeError: Cannot convert a Symbol value to a string
String(BigInt(1)) // '1'
对象
String 函数的参数如果是对象,先按 hint "string" 转成原始值(优先 toString,见下文)。数组走 Array.prototype.toString,相当于 join(',')。
String({a: 1}) // '[object Object]'
String({}) // '[object Object]'
String([1, 2, 3]) // '1,2,3'
String([]) // ''
String(new Date()) // 'Tue May 24 2022 00:03:00 GMT+0800 (中国标准时间)'
具体转换规则(有 Symbol.toPrimitive 则先走它):
- 先调用对象自身的
toString()。如果返回原始类型的值,再交给String()。 - 如果
toString()返回的仍是对象,再调用valueOf();若得到原始类型,再交给String()。 - 如果
valueOf()返回的还是对象,则抛 TypeError。
Boolean()
Boolean 对象是一个布尔值的对象包装器,也可以用作普通函数,将任意类型的值转为布尔值。
如果参数已经是布尔值,则原样返回。下列值转为 false,其余(含非空字符串、非 0 的数字、对象)转为 true:
undefinednullfalse0(含-0、+0)、0nNaN''(空字符串)
Boolean(undefined) // false
Boolean(null) // false
Boolean(false) // false
Boolean(0) // false
Boolean(0n) // false
Boolean(NaN) // false
Boolean('') // false
注意: 普通对象(包括空对象、空数组)以及 Symbol 原始值转为布尔都是 true;连 new Boolean(false) 这种包装对象也是 true。Symbol 不是对象。
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
Boolean(Symbol('a')) // true
注意: 有一个特殊的对象,它转换成的布尔值是 false。
document.all // HTMLAllCollection(9) [html, head, meta, meta, title, body, div, script, script, viewport: meta]
Boolean(document.all) // false
隐式类型转换
隐式类型转换是以强制类型转换为基础的。
以下三种情况时,JavaScript 会自动转换数据类型:
不同类型的数据互相运算。
对非布尔值类型的数据求布尔值。
对非数值使用一元
+、-,以及减、乘、除、取余等算术运算。
自动转换的规则:这个位置需要什么类型,就按那种类型去转。二元 + 只要有一边已经是字符串(或 ToPrimitive 后得到字符串),另一边就转成字符串;其余算术运算转成数值。
比如,'' + a 走字符串拼接;+a 走 Number(a)。
由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用 Boolean()、Number() 和 String() 函数进行强制转换。
自动转换为布尔值
以下情况,JavaScript 会自动调用 Boolean() 函数:
- if 语句的条件部分
for (;;)语句中的条件判断表达式(第二个)while ()和do...while()循环中的条件判断表达式- 三元表达式(?:)
- 逻辑非运算符(
!) - 逻辑与
&&、逻辑或||判断条件时(返回值仍是原来的操作数,不是布尔值)
自动转换为字符串
以下情况,JavaScript 会自动调用 String() 函数:
字符串的加号运算符(+):当两个操作数,有一个是字符串,另一个为非字符串,则后者转为字符串。
'1' + 1 // '11'
'1' + true // '1true'
'1' + false // '1false'
'1' + {} // '1[object Object]'
'1' + [] // '1'
'1' + function () { } // '1function () { }'
'1' + undefined // '1undefined'
'1' + null // '1null'
自动转换为数值
以下情况,JavaScript 会自动调用 Number() 函数:
除了二元加号 + 在碰到字符串时会走拼接,其余算术运算符都会把操作数转成数值。
'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
null + 1 // 1
undefined + 1 // NaN
对象转基本类型
对象转基本类型有三种 hint(暗示)。规范规定:对象出现在需要原始值的场合(字符串拼接、alert、算术、比较等)时,会先转成原始值。
Hint
"string":需要字符串时,例如模板字符串、String(obj)、alert(obj)。"number":需要数字时,例如一元+、减乘除、关系运算符<、>、<=、>=。"default":运算符不偏向某一种时,主要是二元+和宽松相等==。普通对象把"default"按"number"处理;Date把"default"按"string"处理。
为了实现转换,JavaScript 会按这个顺序:
- 如果存在
Symbol.toPrimitive,所有 hint 都先走它。 - 否则,hint 是
"string":先toString(),再valueOf()。 - 否则(hint 是
"number"或"default"):先valueOf(),再toString()。
Symbol.toPrimitive
Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。
const user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
alert(user); // hint: string
(+user); // hint: number
(user + 500); // hint: default
唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象。
toString() 和 valueOf()
toString() 返回对象的字符串表示,valueOf() 返回指定对象的原始值。
如果没有 Symbol.toPrimitive,JavaScript 会找这两个方法:
- hint 为
"string":先toString(),没有再用valueOf()。 - 其他 hint:先
valueOf(),没有再用toString()。
这些方法必须返回原始值。若返回的仍是对象,这次结果会被丢掉,当作该方法不存在,继续试下一个。两个都给不出原始值,就抛 TypeError。
默认情况下,普通对象带有:
toString():返回"[object Object]"。valueOf():返回对象自身(不是原始值,因此对转换没有帮助)。
多数情况下,普通对象最终会落到 toString()。
四种相等性判断
相等(==)
相等运算符(==)检查其两个操作数是否相等,并返回布尔值。
相等运算符(==)会尝试强制类型转换并且比较不同类型的操作数,转换规则如下:
如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回 true。
如果一个操作数是 null,另一个操作数是 undefined,则返回 true。
一个操作数是
undefined或null:另一个也是null或undefined则为true;另一个是普通对象则为false。例外是带[[IsHTMLDDA]]的document.all,它与undefined、null的==都为true。如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
- 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
- 如果操作数之一是 Boolean,则 true 转换为1、false 转换为 0。
- 如果操作数之一是对象,另一个是数字或字符串,会先把对象转成原始值(
Symbol.toPrimitive,否则valueOf/toString),再继续比较。
如果操作数具有相同的类型,则将它们进行如下比较:
- String: 仅当两个操作数具有相同顺序的相同字符时才返回 true。
- Number:仅当两个操作数具有相同的值时才返回 true。+0 和 -0 视为相同的值,NaN 和任何值(包括 NaN 本身)返回 false。
- Boolean:仅当操作数为两个 true 或两个 false 时才返回 true。
以下表格为不同类型一一对应的转换规则:
备注: 表格纵向为被比较值 A,横向为被比较值 B。ToNum(A) 表示把 A 转成数字;ToPrim(A) 表示把 A 转成原始值。普通对象与 undefined、null 的 == 为 false;document.all 除外,见表示例。
| undefined | null | Number | String | Boolean | Object | |
|---|---|---|---|---|---|---|
| undefined | true | true | false | false | false | false |
| null | true | true | false | false | false | false |
| Number | false | false | A===B | A === ToNum(B) | A === ToNum(B) | A == ToPrim(B) |
| String | false | false | ToNum(A) === B | A===B | ToNum(A) === ToNum(B) | ToPrim(B) == A |
| Boolean | false | false | ToNum(A) === B | ToNum(A) === ToNum(B) | A===B | ToNum(A) == ToPrim(B) |
| Object | false | false | ToPrim(A) == B | ToPrim(A) == B | ToPrim(A) == ToNum(B) | A===B |
一些特殊的比较:
document.all
// HTMLAllCollection(10) [html, head, meta, meta, title, style, body, div, a, script, viewport: meta]
console.log(document.all == undefined) // true
console.log(document.all == null) // true
全等运算符(===)
全等运算符(===)会检查它的两个操作数是否相等,并且返回一个布尔值结果。与相等运算符不同,全等运算符总是认为不同类型的操作数不相等,也就是说,不会做隐式类型转换。
全等运算符(===)比较规则:
- 如果操作数的类型不同,则返回 false。
- 如果两个操作数都是对象,只有当它们指向同一个对象时才返回 true。
- 如果两个操作数都为 null,或者两个操作数都为 undefined,返回 true。
- 如果两个操作数有任意一个为 NaN,返回 false。
- 否则,比较两个操作数的值:
- 数字类型必须拥有相同的数值。+0 和 -0 会被认为是相同的值。
- 字符串类型必须拥有相同顺序的相同字符。
- 布尔值必须同时为
true或同时为false。
同值相等
判断两个值在任何情况下是否功能上相同,用 Object.is()。
与严格相等(===)只有两处不同:
+0不等于-0。NaN等于自身。
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
零值相等(SameValueZero)
与同值相等类似,但认为 +0 与 -0 相等;NaN 与 NaN 也相等。它既不是 ===(NaN === NaN 为 false),也不是 Object.is(Object.is(+0, -0) 为 false)。
SameValueZero 用于 Map、Set 的键或值比较,以及 Array.prototype.includes、TypedArray 的 includes。String.prototype.includes 是子串查找,不用这套算法。
+0 === -0 // true,这是 ===,还看不出和 Object.is 的差别
[NaN].includes(NaN) // true,includes 用 SameValueZero
NaN === NaN // false
const map = new Map();
map.set(+0, 1)
map.get(+0) // 1
map.get(-0) // 1,同一条键
一些例子
行首的 { 在语句里是块,不是对象。{} + {} 实际是空块加上一元 +{},结果是 NaN;写成 ({} + {}) 才是拼接两个对象。
![] // false
+[] // 0
+![] // 0
[] + [] // ''
({} + {}) // '[object Object][object Object]'
{} + {} // NaN。行首 {} 是空块,等价于 +{}
{} + [] // 0。等价于 +[]
{a:0} + 1 // 1。块里的标签 a,再加上 +1
[] + {} // '[object Object]'
[] + ![] // 'false'
![] + [] // 'false'
'' + {} // '[object Object]'
{} + '' // 0。等价于 +''
[]['map'] + [] // 以 'function map' 开头的函数源码字符串
[]['a'] + [] // 'undefined'
[][[]] + [] // 'undefined'
+!![] + [] // '1'
+!![] // 1
1 - {} // NaN
1 - [] // 1
true - 1 // 0
{} - 1 // -1。空块然后 -1
[] == ![] // true。[] == false → 0 == 0
+((+![])+(+!![])+[]+(+![])) // 10
(![]+[])[+[]] // 'f'。'false'[0]
(![]+[])[+!![]] // 'a'。'false'[1]
相关问题
(a==1 && a==2 && a==3) 能不能为 true?
可以。
因为 == 会做隐式转换。对象对上数字时,hint 是 "default",普通对象等价于先 valueOf() 再 toString()(有 Symbol.toPrimitive 则先走它)。改这几个方法就能让同一次比较每次得到不同的原始值。
const a = {
num: 0,
valueOf: function () {
return this.num += 1
}
};
console.log(a == 1 && a == 2 && a == 3) // true
let num = 0;
Function.prototype.toString = function () {
return ++num;
}
function a() { }
console.log(a == 1 && a == 2 && a == 3) // true
const a = {
[Symbol.toPrimitive]: (function (i) {
return function () { return ++i }
})(0)
};
console.log(a == 1 && a == 2 && a == 3) // true
完善 Cash 打印三个 101
要求只能在 class 里面增加代码:
class Cash {}
const a = new Cash(1)
const b = new Cash(100)
console.log(`${a.add(b)},${Cash.add(a,b)},${new Cash(a+b)}`) // '101,101,101'
首先,三个输出都经过隐式转换。a 和 b 是实例,new Cash(a + b) 说明构造函数也会接到 Cash 相加后的结果。若 toString 仍是默认的 '[object Object]',模板字符串对不上 101,所以要改 valueOf()、toString()(或 Symbol.toPrimitive)。
class Cash {
constructor(a) {
this.m = a
this.valueOf = function () { return a }
}
add(n) { return this.m + n }
toString() { return this.m }
static add(v1, v2) { return v1 + v2 }
}
const a = new Cash(1)
const b = new Cash(100)
console.log(`${a.add(b)},${Cash.add(a, b)},${new Cash(a + b)}`) // '101,101,101'