js中的类型转换
前言
在js的运算中,经常涉及到数据类型的转换,有时候是显示转换,有时是隐式转换,本章就来捋一捋各种数据类型之间的转换规则。,先看一道题:
let result = 100 + true + 21.2 + null + undefined + "cheny" + [] + null + 9 + false;
// result应该是?你知道最终的答案吗?
最终会输出'NaNchenynull9false',题目考察的知识点是数据类型隐式转换,想要做对这道题,我们必须了解 JavaScript 在转换的时候,会遵循哪些规则。
例子分析
上面的例子我们来一点点的分析,从左往右一点点的看(先来看为什么得到这个结果,具体的细节先不关注):
100 + true的结果是什么?这里的
true会被隐式地转换为数字1,所以得到的结果为101101 + 21.2 + null的结果是什么?101 + 21.2做正常的加法,得到122.2,null被隐式地转换为数字0,所以最终的结果是122.2122.2 + undefined的结果是什么?undefined会尝试去转换为数字与122.2做加法,但是undefined无法转换为数字,会被隐式的转换为NaN,任何数字与NaN做加法都会返回NaN,所以最终的结果是NaNNaN + "cheny"的结果是什么?右侧是字符串
"cheny",所以左侧的NaN会被隐式的转换为字符串"NaN",最后的结果是字符串拼接的结果,"NaNcheny""NaNcheny" + []的结果是什么?[]空数组会被隐士的转换为空字符串'',所以最终的结果仍为,"NaNcheny""NaNcheny" + nullnull会隐式的转换为字符串"null",所以最终的结果为,"NaNchenynull""NaNchenynull" + 9 + false数字
9会被隐式的转换为字符串"9",布尔fasle也会被隐式的转换为字符串"false",所以最终的结果为,"NaNchenynull9false"
所以输出的最终结果为:"NaNchenynull9false"。
基本类型之间的转换
字符串string
基本类型转换为字符串是最明显的,我们可以使用String(value)显示的将一个值转换为字符串,比如:
let value = true;
console.log(typeof value); // boolean
value = String(value); // 现在,值是一个字符串形式的 "true"
console.log(typeof value); // string当在做加法运算时,如果左侧是一个字符串,右侧不是字符串时,会隐式的将右侧的值转换为字符串做拼接,比如
let str = 'abc' + true
console.log(str) // abctrue
console.log(typeof str); // string小结
基本类型转string类型是比较明显的
undefined ➡ string
js// undefined -> "undefined"null ➡ string
js// null -> "null"number ➡ string
js// 123 -> "123" // 5.6 -> "5.6" // 0 -> "0" // -567 -> "-567"bigint ➡ string
js// 123n -> "123" // 5.6n -> "5.6" // 0n -> "0" // -567n -> "-567"boolean ➡ string
js// true -> "true" // false -> "false"symbol ➡ string
js// Symbol() -> "Symbol()" // Symbol(123) -> "Symbol(123)" // Symbol('123') -> "Symbol('123')"
数字number
在算术函数和表达式中,会自动进行 number 类型转换。
比如,当把除法 / 用于非 number 类型:
let a = "6" / "2"
console.log(a); // 3
console.log(typeof a); // number
let b = "abc" / "2"
console.log(b); // NaN我们也可以使用 Number(value) 显式地将这个 value 转换为 number 类型。
let str = "123";
console.log(typeof str); // string
let num = Number(str); // 变成 number 类型 123
console.log(typeof num); // number当我们从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。
如果该字符串不是一个有效的数字,转换的结果会是 NaN。例如:
let age = Number("an arbitrary string instead of a number");
console.log(age); // NaN,转换失败小结
基本类型转number类型:
undefined ➡ number
js// undefined -> NaNnull ➡ number
js// null -> 0string➡ number
js// "" -> 0 // " " -> 0 // " 123" -> 123 // " 123 " -> 123 // " 123 4" -> NaN // " 123a" -> NaN去掉首尾空格后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为
0。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回NaN。bigint ➡ number
js// 123n -> 123 // 5.6n -> 5.6 // 0n -> 0 // -567n -> -567注意:number的取值范围在
(-(2^53), 2^53)之间,如果bigint的值在这个范围之外时转换为number,多余的位会被截断,因此我们应该谨慎进行此类转换,比如:js// 9007199254740992 2^53 // 900719925474099222n // 900719925474099200 // Number(900719925474099222n) -> 900719925474099200boolean ➡ number
js// true -> 1 // false -> 0symbol ➡ number
symbol类型无法转换位number,会报错。
js// Number(Symbol()) -> Uncaught TypeError: Cannot convert a Symbol value to a number
👨请注意
null和undefined在这有点不同:null变成数字0,undefined变成NaN。
布尔boolean
布尔(boolean)类型转换是最简单的一个。
它发生在逻辑运算中(稍后我们将进行条件判断和其他类似的东西),但是也可以通过调用 Boolean(value) 显式地进行转换。
转换规则如下:
- 直观上为“空”的值(如
0、空字符串、null、undefined和NaN)将变为false。 - 其他值变成
true。
比如:
console.log( Boolean(1) ); // true
console.log( Boolean(0) ); // false
console.log( Boolean("hello") ); // true
console.log( Boolean("") ); // false👦请注意:包含 0 的字符串
"0"是truejsconsole.log( Boolean("0") ); // true console.log( Boolean(" ") ); // 空白,也是 true(任何非空字符串都是 true)
小结
基本类型转boolean类型:
undefined ➡ boolean
js// undefined -> falsenull ➡ boolean
js// null -> falsestring➡ boolean
js// "" -> false // " " -> true // "0" -> truenumber➡ boolean
js// 0 -> false // 1 -> true // 2 -> true // NaN -> false除了数值
0转换位boolean时为false,其它number都为true,另外特殊记忆一下NaN,它变布尔时也返回false。bigint➡ boolean
js// 0n -> false // 1n -> true // 2n -> truesymbol ➡ number
js// Symbol() -> true // Symbol(123) -> truesymbol类型转boolean都为true
对象到原始值的转换
转换规则
Symbol.toPrimitive
对象到原始类型的转换其实跟它的Symbol.toPrimitive方法有关,该方法接收一个形参hint,有三种可能情况
hint === string
当期望得到的值是一个字符串时,传入
Symbol.toPrimitive方法形参hint的值就为string。比如:
alert(obj),会默认的隐式转换obj为字符串。或者将对象作为属性键,anotherObj[obj] = 123(因为对象的属性值只能为字符串或者symbol,当对象作为一个个属性时,会隐式转换为字符串)。js// 输出 alert(obj); // 将对象作为属性键 anotherObj[obj] = 123;hint === number
当期望得到的值是一个数字时,传入
Symbol.toPrimitive方法形参hint的值就为number。比如:显示转换为某个对象为
number,Number(obj),或者数学运算+obj,将obj转换为number。再或者> <对对象进行比较时。js// 显式转换 let num = Number(obj); // 数学运算(除了二元加法) let n = +obj; // 一元加法 let delta = date1 - date2; // 小于/大于的比较 let greater = user1 > user2;hint === default
当期望得到的值不确定时,传入
Symbol.toPrimitive方法形参hint的值就为default。比如:
+运算符可以作为数值相加,也可以作为字符串拼接,obj + obj。另外在做==比较时,也会做类型转换,如果对象被用于与字符串、数字、或者Symbol比较时,到底进行哪种转换也不确定,因此此时的hint也是default。js// 二元加法使用默认 hint let total = obj1 + obj2; // obj == number 使用默认 hint if (user == 1) { ... };像
<和>这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default”。这是历史原因。实际上,我们没有必要记住这些奇特的细节,除了一种情况(Date对象)之外,所有内建对象都以和"number"相同的方式实现"default"转换。我们也可以这样做。
注意:没有
hint === boolean的情况,所有对象在转换为boolean类型时,都会返回true如果我们将
"default"和"number"视为相同,就像大多数内建函数一样,那么就只有两种转换了。
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)—— 带有 symbol 键Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"—— 尝试obj.toString()和obj.valueOf(),无论哪个存在。 - 否则,如果 hint 是
"number"或"default"—— 尝试obj.valueOf()和obj.toString(),无论哪个存在。
一个例子
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// 转换演示:
// hint string
console.log(String(user)); // hint: string -> {name: "John"}
// hint number
console.log(+user); // hint: number -> 1000
// hint default
console.log(user + 500); // hint: default -> 1500上述例子中,有个对象user,手动实现了它的Symbol.toPrimitive方法,
String(user),显示的将user转换为字符串,所以hint === 'string',调用Symbol.toPrimitive方法,最终返回字符串{name: 'John'}+user,隐式地将转换转换为number,所以hint === 'number',调用Symbol.toPrimitive方法,返回this.money,所以输出数值1000user + 500,并不知道user的期望值,所以hint === 'default',调用Symbol.toPrimitive方法,返回this.money,所以输出数值1500
toString/valueOf
那么,假如在上古时代,没有Symbol时,对象是按什么规则转换成原始类型的呢?其实是通过对象的两个方法实现转换的。toString()和valueOf()。
如果没有 Symbol.toPrimitive,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:
- 对于 “string” hint,
toString -> valueOf。 - 其他情况,
valueOf -> toString。
注意:这些方法必须返回一个原始值。如果
toString或valueOf返回了一个对象,那么返回值会被忽略。
默认情况下,普通对象具有 toString 和 valueOf 方法:
toString方法返回一个字符串"[object Object]"。valueOf方法返回对象自身。
比如:
let user = { name: "John" };
console.log(user.toString()); // [object Object]
console.log(String(user)); // [object Object]
console.log(user.valueOf() === user); // true因为默认的valueOf返回的就是对象本身,所以我们可以忽略它,具体是为什么,那就是历史原因,咱也不知道,可以假装它不存在就行了。
一个小例子
let user = {
name: "John",
money: 1000,
// 对于 hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// 对于 hint="number" 或 "default"
valueOf() {
return this.money;
}
};
// hint string
console.log(String(user)); // toString -> {name: "John"}
// hint number
console.log(+user); // valueOf -> 1000
// hint default
console.log(user + 500); // valueOf -> 1500上面的找个例子中,并没有实现它的Symbol.toPrimitive,而是实现了它的toString()和valueOf(),所以,
String(user),当打印它的值时,显示转换为字符串,因此转换顺序为先toString,然后valueOf,调用toString()时,返回了基本类型string,其值为'{name: John}'这里你可以尝试着将
toString返回一个对象试试,最终会被忽略,走valueOf的返回值1000+user,隐式转换为number,先走valueOf(),再走toString(),因为valueOf()有原始类型的返回值1000,所以直接返回1000user + 500,并不知道需要将user转换为那种值,所以仍然先走valueOf(),再走toString(),因为valueOf()有原始类型的返回值1000,所以直接返回1000
通常我们希望有一个“全能”的地方来处理所有原始转换。在这种情况下,我们可以只实现
toString,就像这样:jslet user = { name: "John", toString() { return this.name; } }; console.log(String(user)); // toString -> John console.log(user + 500); // toString -> John500
返回类型
关于所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回 “hint” 的原始值。
没有限制 toString() 是否返回字符串,或 Symbol.toPrimitive 方法是否为 hint “number” 返回数字。
唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象。
❗历史原因
由于历史原因,如果
toString或valueOf返回一个对象,则不会出现 error,但是这种值会被忽略(就像这种方法根本不存在)。这是因为在 JavaScript 语言发展初期,没有很好的 “error” 的概念。相反,
Symbol.toPrimitive必须 返回一个原始值,否则就会出现 error。
进一步的转换
我们知道,许多运算符和函数执行类型转换,例如乘法 * 将操作数转换为数字。
如果我们将对象作为参数传递,则会出现两个阶段:
- 对象被转换为原始值(通过前面我们描述的规则)。
- 如果生成的原始值的类型不正确,则继续进行转换。
例如:
let obj = {
// toString 在没有其他方法的情况下处理所有转换
toString() {
return "2";
}
};
console.log(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。- 乘法
obj * 2首先将对象转换为原始值(字符串 “2”)。 - 之后
"2" * 2变为2 * 2(字符串被转换为数字)。
二元加法在同样的情况下会将其连接成字符串,因为它更愿意接受字符串:
let obj = {
toString() {
return "2";
}
};
console.log(obj + 2); // 22("2" + 2)被转换为原始值字符串 => 级联一些小例子
例子1:对象转string
// String([]) === ''
// String(['']) === ''
// String([1,2,3]) === '1,2,3'
// String([1,,3]) === '1,,3'
// String([1,undefined,3]) === '1,,3'
// String([1,null,3]) === '1,,3'
// String([1,'',3]) === '1,,3'
// String({}) === '[object Object]'
// String({name: 'John'}) === '[object Object]'例子2 对象转number
其实也是先转成string,然后通过string转number
console.log(Number([])); // 0
console.log(Number([''])); // 0
console.log(Number([' '])); // 0
console.log(Number([1, 2, 3])); // NaN
console.log(Number({})) // NaN总结
js中有显示类型转换和隐式类型转换
- 显示类型转换:比如
String(value)、Number(value)、Boolean(value)等显示的将目标值转换为期望的类型。 - 隐式类型转换:比如js中的一些运算,
'abc' + 123,会隐式的将123转换为字符串'123',然后再进行字符串拼接。
类型转换规则
基本类型之间的转换
转换为
string比较简单,
true->'true'、0->'0'、undefined=>'undefined'、Symbol(123)->'Symbol(123)'等转换为
number字符串转
number时,会去除两边的空格,然后看中间的值是否可以转换成number,如果去除完前后的空格后为空字符串,那就转换为0,如果不能转换为数值,就转换为NaN。注意:
bigint转number时要注意下精度,如果超出了number的存储范围,会被截断一部分。numer的取值返回在(-2^53, 2^53)之间。symbol类型无法转换为number。boolean的true变1,false变0。另外需要特殊记一下
undefined和null,undefined会转换成NaN,null会变为0转换为
booleanundefined、null、0、''、NaN变boolean时为false,其余的都为true。
对象到原始值之间的转换
转换规则如下:
有
Symbol.toPrimitive方法时,按照该方法转换当需要转换为
string时,hint === string当需要转换为
number时,hint === number当需要转换的类型值不确定时,
hint === defaulthint === number与hint === default其实可以看成相同的一类,也就是说如果不走
hint === string的转换逻辑,那就直接走hint === number||default的逻辑。
当没有
Symbol.toPrimitive方法时,遵循下面的转换规则,会去找对象的toString和valueOf方法- 当需要转换为
string时,即hint === string,先找toString,再找valueOf - 当需要转换为别的类型时,即
hint === number || defalut,先找valueOf,再找toString
❗注意:在
Symbol.toPrimitive中的转换规则里,如果返回值不是原始类型,会直接报错,TypeError: Cannot convert object to primitive value。而当没有Symbol.toPrimitive时,toString或者valueOf的返回值如果不是原始类型会直接忽略。另外,数字和字符串都可以使用
> <进行比较,这时走的hint === number,是历史遗留下来的,记住就行了。比如:当有
Symbol.toPrimitive时jslet a = { [Symbol.toPrimitive](hint) { console.log(`hint: ${hint}`); return hint === 'string' ? 'hello' : 123 } } let b = { [Symbol.toPrimitive](hint) { console.log(`hint: ${hint}`); return hint === 'string' ? 'hello' : 456 } } console.log(a < b); // hint: number // hint: number // true 因为 123 < 456当没有Symbol.toPrimitive`时
jslet a = { toString() { return 'hello' }, valueOf() { console.log('hint: number'); return 123 } } let b = { toString() { return 'hello' }, valueOf() { console.log('hint: number'); return 456 } } // 走number时,先找valueOf 再找toString console.log(a < b); // hint: number // hint: number // true 因为 123 < 456
- 当需要转换为
在实践中,为了便于进行日志记录或调试,对于所有能够返回一种“可读性好”的对象的表达形式的转换,只实现以
obj.toString()作为全能转换的方法就够了。
参考
https://juejin.cn/post/6956170676327677966#heading-0