基础篇:类型转换
类型转换(Type Conversion)是指将数据由一种类型变换为另一种类型。在 JavaScript 中,在一些运算类型与预期不符的情况下,会发生隐式转换,也可以用一些写法强制要求进行显式转换。
JavaScript 中有 7 种简单数据类型(也称为基本数据类型):数字(Number)、字符串(String)、布尔值(Boolean)、 Undefined、Null,还有 ES6 新增的 Symbole,以及处于第 4 阶段的 BigInt。此外,还有 1 种复杂数据类型(也称引用类型)—对象(Object)。
强制类型转换
强制类型转换主要是使用 Number()、String()、Boolean() 三个函数,手动将各种类型的值,分别转换成数字、字符串、布尔值。
Number()
Number 对象是经过封装的处理数值的对象,主要有两个作用:一是作为构造函数,创建数字对象;二是用来将任意类型的值转换为数值。
原始类型值
- 数值:返回原值。如果是极小和极大的数字,则会返回指数形式。
- 字符串:可以被解析,则转换为相应的数值;不可以被解析为数值,返回 NaN;空字符串转为 0。
- 布尔值:true 转成 1;false 转成 0。
- undefined:转成为 NaN。
- null:转成为 0。
- Symbol:不能转换为数字,会报错。
- BigInt:转换为数值,末尾 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() 函数将字符串转为数值,要比 parseInt() 函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为 NaN。
另外,parseInt() 和 Number() 函数都会自动过滤一个字符串前导和后缀的空格。
对象
简单的规则是,Number() 函数的参数是对象时,将返回 NaN,除非是空数组或者只有一个数值的数组。
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 函数的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
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 (中国标准时间)'
具体转换规则:
先调用对象自身的 toString() 方法。如果返回原始类型的值,则使用 String() 函数转换该值。
如果 toString() 方法返回的是对象,再调用原对象的 valueOf() 方法。如果 valueOf() 方法返回原始类型的值,则使用 String() 函数转换该值。
如果 valueOf() 方法返回的是对象,则报错。
Boolean()
Boolean 对象是一个布尔值的对象包装器,也可以用作普通函数,将任意类型的值转为布尔值。
如果是布尔值,则原值返回,仅以下五个值的转换为 false,其他的值全部为 true:
- undefined
- null
- 0(包含 -0、+0、BigInt(0))
- NaN
- ''
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
注意: 所有对象(包括空对象、Symbol 值)的转换都是 true,甚至连 false 对应的布尔对象 new Boolean(false) 也是。
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 会自动转换数据类型:
不同类型的数据互相运算。
对非布尔值类型的数据求布尔值。
对非数值类型的值使用一元运算符(即 +、-)。
自动转换的规则:预期什么类型的值,就调用该类型的转换函数。如果该位置既可以是字符串,也可能是数值,那么默认转为数值。
比如,'' + a,调用 String(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(暗示、示意)。ECMAScript 规范定义:当一个对象被用在需要原始值的上下文中时,例如,在比较 alert 或数学运算中,对象会被转换为原始值。
Hint
- string: 对象到字符串的转换。当对期望一个字符串的对象执行操作,如,alert 函数中的参数。
- number:对象到数字的转换。如,进行数学运算时。
- default:在少数情况下发生,当运算符 “不确定” 期望值的类型时,如二元加法 +、不严格相等 ==、比较运算符 <、>。
为了实现转换,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 将尝试寻找 toString() 和 valueOf() 方法:
- 对于 “string” hint:如果 toString() 存在,优先调用 toString();否则调用 valueOf()。
- 对于其他 hint:如果 valueOf() 存在,优先调用 valueOf();否则调用则 toStrin()。
这些方法必须返回一个原始值。如果 toString() 或 valueOf() 返回了一个对象,那么返回值会被忽略(和没有方法时相同)。
默认情况下,普通对象具有 toString() 和 valueOf() 方法:
- toString() 方法返回一个字符串 "[object Object]"。
- valueOf() 方法返回对象自身。
一般地,toString() 作为 “全能” 的方法来处理转换。
ES6 的四种相等性判断
相等(==)
相等运算符(==)检查其两个操作数是否相等,并返回布尔值。
相等运算符(==)会尝试强制类型转换并且比较不同类型的操作数,转换规则如下:
如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回 true。
如果一个操作数是 null,另一个操作数是 undefined,则返回 true。
一个操作数是 undefined 或者 null,如果另一个操作数是 Number、String、Boolean ,则返回 false;如果另一个操作数是对象,则返回
!Boolean(object)(特殊对象 document.all,查看下方示例)。如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
- 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
- 如果操作数之一是 Boolean,则 true 转换为1、false 转换为 0。
- 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的 valueOf() 和 toString() 方法将对象转换为原始值。
如果操作数具有相同的类型,则将它们进行如下比较:
- String: 仅当两个操作数具有相同顺序的相同字符时才返回 true。
- Number:仅当两个操作数具有相同的值时才返回 true。+0 和 -0 视为相同的值,NaN 和任何值(包括 NaN 本身)返回 false。
- Boolean:仅当操作数为两个 true 或两个 false 时才返回 true。
以下表格为不同类型一一对应的转换规则:
备注: 表格纵向为被比较值 A,横向为被比较值 B;ToNum(A) 表示将 A 转换为数字;ToPrim(A) 表示调用 A.toString() 和 A.valueOf() 方法,将 A 转换为原始值;IsFalsy(A) 表示 A 是否为假值。
| undefined | null | Number | String | Boolean | Object | |
|---|---|---|---|---|---|---|
| undefined | true | true | false | false | false | IsFalsy(B) |
| null | true | true | false | false | false | IsFalsy(B) |
| 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 | IsFalsy(A) | IsFalsy(A) | ToPrim(A) == B | ToPrim(A) == B | ToPrim(A) == ToNumB | 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
零值相等
与同值相等类似,不过会认为 +0 与 -0 相等。
+0 === -0 // true
零值相等使用场景:%TypedArray% 和 ArrayBuffer 构造函数、以及 Map、Set 操作, 并将用于 ES2016/ES7 中的 String.prototype.includes 方法。
const map = new Map();
const obj = { a: 'a' }
map.set(+0, 1)
map.get(+0) // 1
map.get(-0) // 1
一些例子
![] // false
+[] // 0
+![] // 0
[] + [] // '
{} + {} // '[object Object][object Object]'
{} + [] // 0
{a:0} + 1 // 1
[] + {} // '[object Object]'
[] + ![] // 'false'
{} + [] // 0
![] + [] // 'false'
'' + {} // '[object Object]'
{} + '' // 0
[]['map'] + [] // 'function map() { }'
[]['a'] + [] // 'undefined'
[][[]] + [] // 'undefined'
+!![] + [] // '1'
+!![] // 1
1 - {} // NaN
1 - [] // 1
true - 1 // 0
{} - 1 // -1
[]==![] // true
+((+![])+(+!![])+[]+(+![])) // 10
(![]+[])[+[]] // 'f'。'false'[0]
(![]+[])[+!![]] // 'a'。'false'[1]
相关问题
(a==1 && a==2 && a==3) 能不能为 true?
事实上是可以的。
因为在 == 比较的情况下,会进行类型的隐式转换。如果参数是对象,先调用 valueOf() 方法,再调用 toString() 方法。因此,只要改变原生的 valueOf() 方法或者 toString() 方法就可以达到效果。
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 出来的对象,由 new Cash(a+b) 可以看出构造函数传入的也是两个 Cash 的实例对象。那么 new 出来的结果肯定不是简简单单的一个对象,不然就是被转换成字符串 '[object Object]',但是又不得不以对象类型出现,那就只能魔改隐式转换用到的toString() 和 valueOf() 方法。
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'