类型转换
装箱转换
在 ES 时期,如果按照 JS 高级程序设计过去的描述,String
,Number
,Boolean
,Symbol
这些属于基本包装类型,也就是当使用它们的值调用方法时,会创建一个该类型的对象,从而能使用原型上的方法。
var str = 'test';
var strSub = str.substring(2);
// 这个装箱的过程是这样的
var str = new String('test'); //利用String类型构造函数创建对象
var strSub = str.substring(2); //使用对象从原型上继承的方法
str = null; //销毁
需要注意的一点是,将基本类型转换成为包装类型的操作只存在当前语句的执行期间,JS 引擎会在使用完毕后立即销毁对象,所以无法为基本类型的值添加属性。
var str = 'test';
str.prop = 'string';
console.log(str.prop); // undefined
如果在代码中使用new
调用基本类型的构造函数,实际创造出来的就是一个对象,它不是基本类型的值。使用typeof
会得到"object"
的结果;并且能为其添加属性。
var str = new String('');
console.log(typeof str); // object
str.prop = 'string';
console.log(str.prop); // "string"
ToObject
ES 规范文档里并没有拆箱装箱这种说法,但是描述了ToObject
这个抽象方法,这个方法在面对原始值类型的时候,会将其包装成一个对象,如下表所示:
根据 ES 规范文档的描述,大部分Object
以及 Object.prototype
上的方法,都需要显式调用ToObject
来转换;另外比较隐含的一处是[Runtime Semantics: Evaluation]这部分,这部分属于规范中抽象方法部分,是描述成员属性和方法解析标识符的规则。
从Reference Specification Type一章可以了解到一个标识符其实包含三部分绑定内容:
- base value component:这个值本身的数据,它可以是 undefined, an Object, a Boolean, a String, a Symbol, a Number, a BigInt, 或者 Environment Record,也就是 this;
- the referenced name component:被 base value component 调用的属性和方法;
- Boolean strict reference flag:当前语法环境是否使用的是严格模式,true/false
当发生[CallExpression.IdentifierName]这种形式的调用属性或者方法时,会调用 GetValue 这个抽象操作去解析标识符,步骤如下:
- 判断是否是 IsPropertyReference 绑定标识符,IsPropertyReference 的规则是如果 base value component 是 Object 或者 HasPrimitiveBase 为 true 就返回 false;而 HasPrimitiveBase 表示 base value component 是 Boolean, String, Symbol, BigInt, or Number 之一类型就返回 true,所以 IsPropertyReference 的判定规则就是 Object,Boolean, String, Symbol, BigInt, or Number 其中类型之一就返回 true,所以原始值类型自然符合这个条件
- 再判断原始值类型符合后,就会执行 ToObject 操作了,所以本质上,现在的 JS 还是会进行隐式的包装类型的操作
从 ES 规范来看,隐式的装箱发生在很多情况下,涉及到 GetValue 的地方很多,常见的for
循环也会涉及,使用Function.prototype.call/apply
等也会涉及 ToObject
ToPrimitive
ToPrimitive(input, preferredType)
@param input 对象
@param preferredType 要转换的基本类型,默认是 number
ES 规范文档中描述了抽象的ToPrimitive
这个方法,是将对象类型转换成原始值类型的操作,也就是拆箱。
根据 ES 规范文档一大串的描述,归纳下来ToPrimitive
转换一般只会得到String
或者Number
两种类型的原始值,其规则如下:
如果
input
是七种原始值类型之一,直接返回,它根本不会走这个方法;如果
input
是Date
类型,由于其原型上重写了@@toPrimitive
方法,按照Date.prototype.[@@toPrimitive]
这个方法执行,并且默认是转换成字符串
console.log(new Date()[Symbol.toPrimitive]('default'));
console.log(new Date()[Symbol.toPrimitive]('string'));
// "Sat Jul 25 2020 16:29:09 GMT+0800 (Hong Kong Standard Time)"
console.log(new Date()[Symbol.toPrimitive]('number'));
// 1595665749397
- 对于其他对象,如果有
@@toPrimitive
这个方法,就获取其值并判断是否为原始值类型,是的话就返回结果;不是就TypeError
- 其实大部分内置对象都没有
@@toPrimitive
这个方法,那么他们只能走下一步去执行OrdinaryToPrimitive
这步
OrdinaryToPrimitive
将普通对象转成String
或者Number
。
这个方法是配合ToPrimitive
使用的,ToPrimitive
会为该方法指定要转换的类型是 string 还是 number:
- 如果是 string,那么先尝试调用 toString,返回的结果如果是原始值类型就作为结果,否则继续尝试 valueOf,如果最够都得不到原始值,则
TypeError
; - 如果是 number,则和 string 相反,先尝试 valueOf,后尝试 toString,直到其中之一返回原始值
@@toPrimitive
@@toPrimitive(hint)
@param hint "string" / "number" / "default"
@@toPrimitive
是一个抽象的方法,是在对象类型转换为原始值时最先被调用的方法,只会接收三个参数 —— "string" / "number" / "default"
。
一般情况下,只有两种对象转原始值会调用这个方法:
Date
类型转原始值,Date
类型的原型对象上定义了该方法,Date.prototype[@@toPrimitive]
,可将Date
类型转换成String
或者Number
console.log(new Date()[Symbol.toPrimitive]('default'));
console.log(new Date()[Symbol.toPrimitive]('string'));
// "Sat Jul 25 2020 16:29:09 GMT+0800 (Hong Kong Standard Time)"
console.log(new Date()[Symbol.toPrimitive]('number'));
// 1595665749397
- 自定义的
Object
类型对象,并通过Symbol.toPrimitive
属性实现了自己的@@toPrimitive
方法,例如:
obj[Symbol.toPrimitive] = function(hint) {
// 返回一个原始值
// hint = "string"、"number" 和 "default" 中的一个
};
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
},
};
2 * obj; // 246
3 + obj; // '3default'
obj == 'default'; // true
String(obj); // 'str'
ToString
类型 | 结果 |
---|---|
Undefined | "undefined" |
Null | "null" |
Boolean | "true" / "false" |
Symbol | TypeError |
BigInt | 和 Number 表现一致 |
Object | 将对象转原始值ToPrimitive(object, hint String) ;然后再根据原始值的类型按照本表进行转换 |
对于Number
类型转字符串,ES 文档描述的很复杂,总结如下:
NaN
返回"NaN"
;- 正负 0,都返回
0
; - 正无穷,返回
Infinity
; - 如果是负值,数字部分
ToString
再和负号-
做字符串连接
ToNumber
对于原始值类型ToNumber
根据不同类型会直接返回下表中指定的值;
对于Object
及其它内置对象,ToNumber
会首先进行ToPrimitive(object,number)
操作转化成原始值,这里指定了ToPrimitive
转换原始值的类型为 number
操作数类型 | 转换结果 |
---|---|
Undefined | NaN |
Null | +0 |
Boolean | true = 1 ; false = 0 |
Number | 直接返回 Number |
Symbol | TypeError |
BigInt | TypeError |
String | 不包含数字的字符串(十六进制除外),直接NaN 如果字符串中含有 [U+D800, U+DFFF] 之间的码点字符,则直接转换成NaN ;如果字符串中数字和字母串接在一起,也是直接 NaN 其他情况如下文具体分析 |
Object | 执行ToPrimitive(object,number) 转换成原始值;然后再根据原始值的类型进行上述转换 |
对于String
类型转数字,需要满足数字字符串形式的Number
类型词法组合才能转成一个数字,也就是以下形式组合在一起,或者单独一条都能转成数字,规则如下:
- 只包含空字符串;空白字符,换行符等都会被转成
+0
* - 包含七种空白字符
- 包含四种换行符
- 十进制数字形式,这里可以出现小数点,也只能在这里用,如果其他地方使用了
.
构不成一下形式,就直接NaN
- 无符号十进制数字形式
Infinity
[0, 9]
组成的整数[0, 9]
数字组成的小数形式[0, 9].[0, 9]
,例如 2.5,3.6- 没有整数部分的小数形式
.[0, 9]
,例如.8
- 严格遵守规则的科学计数法形式,即
e
后面必须是整数,什么都没有或者小数都会NaN
- 无符号十进制数字形式
+
无符号十进制数字形式-
无符号十进制数字形式
0b
/0B
开头 +0
和1
两个数字组成的二进制形式,会将其转换成十进制0o
/0O
开头 +[0, 7]
之间数字组成的八进制形式,会将其转换成十进制0x
/0X
开头 +[0, F]
组成的十六进制形式,会将其转换成十进制
ToBoolean
类型 | 结果 |
---|---|
Undefined | false |
Null | false |
Number | ±0,NaN => false 其它都是 true |
String | 空字符串 => false 其它都是 true |
Symbol | true |
BigInt | 0n => false 其它都是 true |
Object | true |
假值
JS 的假值只有以下 7 个:
null
undefined
空字符串""
±0
NaN
0n