注,此文中一定要关注 [Symbol.toPrimitive],valueOf,toString 的调用顺序。
把其他类型转换为 number
基本有四种规则:
- Number(val) 显式转 number
- 隐式转 number
- parseInt(val, [radix])
- parseFloat(val)
显式转换成 number
Number 显式转换数字类型有以下几种情况:
- 字符串转换为数字:空字符串变 0,非有效数字变 NaN,
- 布尔转数字: true -> 1,false -> 0
- null -> 0, undefined => NaN
- 把 Symbol 转数字: 报错 Cannot convert a Symbol value to a number
- 把 BigInt 转数字: 10n -> 10, 如果超过安全数字,则按照科学记数法处理,比如 Number(666666665555555556666n) -> 666666665555555500000
- 把对象显式转换成数字:
1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 toString 获取原始值 还没取到原始值则报错,取到基于 Number 转换成数字,返回即可。
Number({ name: 'xx' }) // NaN
// 先调用 ({ name: 'xx' })[Symbol.toPrimitive] 方法,接收一个 hint 参数,此方法不存在于普通对象上
// 再调用 ({ name: 'xx' }).valueOf 得到 { name: 'xx' },不是原始值
// 再调用 ({ name: 'xx' }).toString 得到字符串 "[object Object]"
// Number("[object Object]"); 返回 NaN
Number([10]) // 10
// 先调用 [10][Symbol.toPrimitive] 方法,此方法不存在于数组对象上
// 再调用 [10].valueOf 得到 [10],不是原始值
// 再调用 [10].toString 得到字符串 "10"
// Number("10"); 返回 10
let time = new Date();
Number(time); // 1621757451241
// 先调用 time[Symbol.toPrimitive] 方法,发现有,就会调用该方法,传入 'number' 类型
// time[Symbol.toPrimitive]('number'); 返回 1621757451241
Number(new Number(10)) // 10
// 先调用 (new Number(10))[Symbol.toPrimitive] 方法,此方法不存在于数字对象上
// 再调用 new Number(10).valueOf 得到 10,是原始值
// Number(10); 返回 10
var obj = {
[Symbol.toPrimitive]() {
return '2'
},
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return '11'
}
}
+obj // 2 相当于 Number 调用
isNaN 会使用 Number 做显式类型转换。
隐式转换成 number
原始值隐式转数字: 原始值类型的隐式转换为数字,跟 Number 行为基本一致,唯一区别在 10n + 1 会报错,而不会先把 10n 转 10 与 1 相加(这是因为 js 不允许大数与普通数字进行运算而做的特殊处理)。 把对象隐式转换成数字: 1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 toString 获取原始值 还没取到原始值则报错,取到返回即可,不用 Number 包装。
- 数学运算中「除了一方为字符串的字符串拼接」,都会隐式转换成 number
- == 隐式转换成 number
- 等,下面再罗列这些规则。
10n == 1 // true
var obj = {
[Symbol.toPrimitive]() {
return false
},
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return '11'
}
}
1 + obj // => 1 + false => 1 + 0 = 1
let time = new Date();
time + 1
// Date 实例比较例外,不管是隐式转 number 还是 string, 都是会传入 hint 为 string 的类型
// 先调用 time[Symbol.toPrimitive] 方法,发现有,就会调用该方法,传入 'string' 类型
// time[Symbol.toPrimitive]('string'); 返回 "Thu May 27 2021 23:00:45 GMT+0800 (中国标准时间)1"
通过 parseInt、parseFloat 转换成数字
这两种方法一般用于手动转换。
- parseInt(val, [radix])
- parseFloat(val)
规则:val 值必须是一个字符串,如果不是则先转换成字符串,然后从字符串首位开始查找,把找到的有效数字字符最后转换成数字「如果一个有效数字都没找到,返回 NaN」; 遇到一个非有效数字字符,不论后面是否还有有效数字字符,都不再查找了; 两种方法的区别在于 parseFloat 可以多识别一个小数点,而且 parseInt 有第二个参数,我们下面细说;
// 小试牛刀
parseInt('10'); // 10
parseInt('10px'); // 10
Number('10px'); // NaN
parseInt('10px10'); // 10
parseInt('10.5px'); // 10
parseFloat('10.5px'); // 10.5
Number(null); // 0
parseInt(null); // -> parseInt('null') -> NaN
parseFloat(null); // NaN
一道面试题之 parseInt 的第二个参数
先来看一道经典面试题
let arr = [27.2, 0, '0013', '14px', 123];
arr = arr.map(parseInt);
console.log(arr);
parseInt 传递的第二个值是一个 radix 进制
- radix 不写或者写0,默认是10进制,「如果字符串是以 '0x' 开始的」,那么默认是16进制的。
- radix 取值范围:2 ~ 36,不在这个范围内,处理的结果都是 NaN。
- 在传递的字符串中,从左到右,找到符合 radix 进制的值(遇到不符合的则结束查找),把找到的值,看做 radix 进制,最后转换为 10 进制。
- 其他进制转 10 进制的规则(按权展开求和),比如 2 进制的数字 010,转为 10 进制就是 0 * 2^2 + 1 * 2^1 + 0 * 2^0 = 2,所以 parseInt('010', 2) 输出 2;
特殊说明,16进制输入字符串中的 A 代表 10, F 代表 15;比如 parseInt("1FA.37", 16) 输出 16进制的 "1FA",转 10 进制为:1 * 16^2 + 15 * 16^1 + 10 * 16^0 = 506
抛开 parseInt 不谈,我们单独把 3 进制表示的 10.21,转成 10 进制,它的按权展开求和为 1 * 3^1 + 0 * 3^0 + 2 * 3^-1 + 1 * 3^-2 = 3 + 0 + 2 * 1/3 + 1 * 1/ 3^2 = 34/9
parseInt(27.2, 0);
// => parseInt('27.2', 10);
// => 从左到右,找符合十进制的值,直到遇见不符合 10 进制的值,所以是 '27'
// => 把 '27' 看做 10 进制,转换为 10 进制 => 27
parseInt(0, 1);
// 进制 2 ~ 36 之间 => NaN
parseInt('0013', 2);
// => parseInt('0013', 2);
// => 从左到右,找符合 2 进制的值,直到遇见不符合 2 进制的值,所以是 '001'
// => 把 '001' 看做 2 进制,转换为 10 进制, 按权展开求和
// => 0 * 2^2 + 0 * 2^1 + 1 * 2^0 = 1
parseInt('14px', 3);
// => parseInt('14px', 3);
// => 从左到右,找符合 3 进制的值,直到遇见不符合 3 进制的值,所以是 '1'
// => 把 '1' 看做 3 进制,转换为 10 进制, 按权展开求和
// => 1 * 3^0 = 1
parseInt(123, 4);
// => parseInt(123, 4);
// => 从左到右,找符合 4 进制的值,直到遇见不符合 4 进制的值,所以是 '123'
// => 把 '123' 看做 4 进制,转换为 10 进制, 按权展开求和
// => 1 * 4^2 + 2 * 4^1 + 3 * 4^0 = 16 + 8 + 3 = 27
所以上面面试题的最终输出结果是 [27, NaN, 1, 1, 27]
把其他类型转字符串
基本有三种规则:
- String(val) 显式转 string
- 隐式转 string
- toString()方法调用
显式转字符串
- String()
原始值类型显式转换成字符串:基于引号包起来,BigInt 是去掉 n 对象类型显式转换成字符串: 1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 toString 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 valueOf 获取原始值 还没取到原始值则报错,取到原始值用 String 包装后返回。
// 注意区别
Number(10n); // 10
String(10n); // '10'
var obj = {
[Symbol.toPrimitive]() {
return false
},
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return 11
}
}
String(obj); // 'false'
var obj2 = {
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return 11
}
}
String(obj2); // '11'
隐式转字符串
原始值类型隐式转换成字符串:基于引号包起来,BigInt 是去掉 n 对象类型隐式转换成字符串: 1. 先调用对象的 Symbol.toPrimitive 这个方法获取原始值,如果不存在这个方法,↓ 2. 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值,↓ 3. 再调用对象的 toString 获取原始值 还没取到原始值则报错,取到原始值直接返回(不用 String 包装)。
var obj = {
[Symbol.toPrimitive]() {
return 2
},
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return 11
}
}
obj == '2' // true
obj + 3 // 5
var obj2 = {
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return 11
}
}
obj2 == '11' // false
'a' + obj2 // 'a10'
let time = new Date();
time + "a"
// Date 实例比较例外,不管是隐式转 number 还是 string, 都是会传入 hint 为 string 的类型
// 先调用 time[Symbol.toPrimitive] 方法,发现有,就会调用该方法,传入 'string' 类型
// time[Symbol.toPrimitive]('string'); 返回 "Thu May 27 2021 23:00:45 GMT+0800 (中国标准时间)a"
toString() 转字符串
原始值类型调用 toString 转字符串:基于引号包起来,BigInt 是去掉 n 对象类型调用 toSring 转字符串: 1. 直接调用对象的 toString 获取原始值,如果获取的值不是原始值,↓ 2. 再调用对象的 valueOf 获取原始值 还没取到原始值则报错,取到原始值直接返回(不用 String 包装)。
var obj = {
[Symbol.toPrimitive]() {
return 2
},
valueOf() {
console.log('valueof');
return '10'
},
toString() {
console.log('tostring');
return 11
}
}
obj.toSring(); // 11
总结
你肯定不想看上面又臭又长的总结 + 验证过程,够懂你吧。
// ----------- 隐式转换 -------------
1. 先调用自身 [Symbol.toPrimitive]
2. 再调用 valueOf
3. 最后调用 toString
4. 返回得到的原始值
> 注意点:隐式转换的 Date 类型有点奇怪,都会优先转 string 类型,也就是 new Date()[Symbol.toPrimitive]('string')
// ----------- 显式转换 -------------
Number:
1. 先调用自身 [Symbol.toPrimitive],传入 hint 为 number 类型
2. 再调用 valueOf
3. 最后调用 toString
4. 把得到的原始值通过 Number 转成数字类型返回
String:
1. 先调用自身 [Symbol.toPrimitive],传入 hint 为 default/string 类型
2. 再调用 toString
3. 最后调用 valueOf
4. 把得到的原始值通过 String 转成数字类型返回
toString 方法调用:
2. 先调用 toString
3. 再调用 valueOf
4. 返回得到的原始值
数学运算符的转换规则
此类运算中,如果一方为 NaN,则结果为 NaN
- 一方为字符串,会将另一方也隐式转为 string 进行字符串拼接
- 「除了一方为字符串的字符串拼接」,都会隐式转成 number 进行运算。
+a 或者 -a 中的符号并不是二元运算符「加法」「减法」,而是一个一元运算符,作用是将它后面的操作数转换成数字,和 Number 显式转换完全一样。
1 + 'a' // '1' + 'a' => '1a'
1 + {} // '1[object Object] 对象隐式转数字,推导过程如下
// ({}).toPrimitive? => ({}).valueOf? => ({}).toString() => '[object Object]'
var obj = {
toString() {
return 1;
}
}
1 + obj // 2
'1' + obj // '11'
var obj2 = {
valueOf() {
return 1;
},
toString() {
return 2;
}
}
1 + obj2 // 2
'1' + obj2 // '11'
var obj3 = {
[Symbol.toPrimitive]() {
return 0;
},
valueOf() {
return 1;
},
toString() {
return 2;
}
}
1 + obj3 // 1
'1' + obj3 // '10'
== 的转换规则
三等号不会进行隐式转换哦
- null 和 undefined 互相等,各自等。
- NaN 不等于 NaN。
- 数据类型相同,不需要转换,引用类型比较的是指针地址。
- 一方引用类型,一方字符串的话,会先把对象隐式转换成字符串作比较。
- 都隐式转换成 number 类型做比较。
console.log(1 == true)
// 1 == 1
console.log('1' == true)
// 都转数字 1 == 1
console.log('1' == 1);
// 1 == 1
console.log([] == false);
// 0 == 0
console.log([1, 2] == NaN);
// "1,2" == NaN -> Number("1, 2") == NaN -> NaN == NaN
console.log(['1'] == true);
// "1" == true -> Number("1") == Number(true) -> 1 == 1
console.log([] == ![])
// [] == false -> "" == 0 -> Number("") == 0 -> 0 == 0
console.log({} == !{})
// {} == false -> Number("[object Object]") == false -> NaN == 0
console.log({} == {})
// false 注意 当两边数据类型相同 不进行隐式转换,如果是复杂数据类型,对比的是指针
console.log([] == [])
// false 同上
console.log([1, 2] == '1,2'); // 对象和字符串比较 对象要转字符串
// [1, 2] => '1,2' 再做对比 为 true
a == 1 && a == 2 && a == 3
var a = ?;
// 实现如下判断
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
}
因为把对象转 number,我们会走 Symbol.toPrimitive, valueOf,再走 toString,最好 Number 包装返回值,所以改写对象的三个方法之一即可。
var a = {
i: 0,
[Symbol.toPrimitive]() {
return ++this.i
}
}
或者直接声明成数组
var arr = [1, 2, 3]
// 当然改 valueOf 方法也可以。
arr.toString = arr.shift;
或者直接劫持变量
var i = 0;
Object.defineProperty(window, 'a', {
get() {
return ++i;
}
});
腾讯的一道面试题
let result = 100 + true + 21.2 + null + undefined + 'Tencnet' + [] + null + 9 + false;
console.log(result);
// 100 + true + 21.2 + null + undefined = 122.2 + 0 + NaN = NaN
// NaN + 'Tencnet' = 'NaNTencnet';
// 'NaNTencnet' + [] + null + 9 + false = 'NaNTencnet' + '' + null + 9 + false = 'NaNTencnetnull9false'