跳到主要内容

类型转换

装箱转换

在 ES 时期,如果按照 JS 高级程序设计过去的描述,StringNumberBooleanSymbol这些属于基本包装类型,也就是当使用它们的值调用方法时,会创建一个该类型的对象,从而能使用原型上的方法。

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

ToObject ( argument )

Runtime Semantics: Evaluation

ES 规范文档里并没有拆箱装箱这种说法,但是描述了ToObject 这个抽象方法,这个方法在面对原始值类型的时候,会将其包装成一个对象,如下表所示:

image-20200726184636575

根据 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

image-20200727163210265

ToPrimitive

ToPrimitive(input, preferredType)

@param input 对象

@param preferredType 要转换的基本类型,默认是 number

ES 规范文档中描述了抽象的ToPrimitive这个方法,是将对象类型转换成原始值类型的操作,也就是拆箱。

根据 ES 规范文档一大串的描述,归纳下来ToPrimitive转换一般只会得到String或者Number两种类型的原始值,其规则如下:

  • 如果input是七种原始值类型之一,直接返回,它根本不会走这个方法;

  • 如果inputDate类型,由于其原型上重写了@@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,直到其中之一返回原始值

image-20200725153918261

@@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

ECMA - ToString

类型结果
Undefined"undefined"
Null"null"
Boolean"true" / "false"
SymbolTypeError
BigInt和 Number 表现一致
Object将对象转原始值ToPrimitive(object, hint String);然后再根据原始值的类型按照本表进行转换

对于Number类型转字符串,ES 文档描述的很复杂,总结如下:

  • NaN返回"NaN"
  • 正负 0,都返回0
  • 正无穷,返回Infinity
  • 如果是负值,数字部分ToString再和负号-做字符串连接

ToNumber

ECMA - ToNumber

对于原始值类型ToNumber根据不同类型会直接返回下表中指定的值;

对于Object及其它内置对象,ToNumber会首先进行ToPrimitive(object,number)操作转化成原始值,这里指定了ToPrimitive转换原始值的类型为 number

操作数类型转换结果
UndefinedNaN
Null+0
Booleantrue = 1 ; false = 0
Number直接返回 Number
SymbolTypeError
BigIntTypeError
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
  • image-20200629230620793
    • +无符号十进制数字形式
    • -无符号十进制数字形式
  • 0b/ 0B开头 + 01两个数字组成的二进制形式,会将其转换成十进制
  • 0o / 0O开头 + [0, 7]之间数字组成的八进制形式,会将其转换成十进制
  • 0x / 0X开头 + [0, F]组成的十六进制形式,会将其转换成十进制

image-20200728145244262

ToBoolean

ToBoolean

类型结果
Undefinedfalse
Nullfalse
Number±0,NaN => false
其它都是 true
String空字符串 => false
其它都是 true
Symboltrue
BigInt0n => false
其它都是 true
Objecttrue

假值

JS 的假值只有以下 7 个:

null
undefined
空字符串""
±0
NaN
0n