平时,我们使用模板字符串或者双等号(==)去判断是否相等的时候都会涉及到隐式类型转换的问题,下面我们来看看,学习并总结一下。
模板字符串
js
const name="1in";
console.log(`name is ${name}`);
上述就是使用模板字符串的例子。
但是,有没有发现一个问题,如果我们定义的变量不是一个string类型,而是number类型,这个时候就会产生隐式类型转换的操作。
通过调用toString方法去实现隐式类型转化。
一般的对象,如果原型链能够上溯到 Object.prototype
,那么就可以调用 toString()
实例函数。有的对象会重载这个函数,比如 Date,甚至像 Number 的 toString 还带有一个参数。
但是在模板字符串中,并不是调用变量的 toString
方法,这样不安全,毕竟变量可能不是广义上的对象(null、undefined),而且 toString 也可以被重载为不返回字符串类型。
这里就要用到 ECMAScript 规范定义的一个内部函数了,叫做 ToString()
。没错,规范已经盘点好了各种类型转换的需求,其他的还有 ToBoolean、ToNumber、ToObject,甚至还有一些场景化的转换,比如 ToLength、ToPropertyKey、ToIndex,还有一个相当重要的 ToPrimitive
我们看 ToString(arg)
是如何工作的。
-
判断入参类型,遍历一遍所有的
Primitive
类型:- 如果是 String,显然不用转换,直接返回;
- 如果是 Symbol,
抛出异常
; - 如果是 Undefined,就返回 "undefined";
- 如果是 Null,就返回 "null";
- 如果是 Boolean,就返回 "true" 或 "false";
- 如果是 Number 或者 BigInt,都转换成其 10 进制表示形式,这里面的细节不涉及类型转换,所以我们就不深究了,大家注意这里可能输出"NaN"、"Infinite"和科学记数法。
-
如果是非 Primitive 类型,也就是 Object,如何转换成字符串呢?答案是将参数带入到
ToPrimitive(arg, string)
。
ToPrimitive(input[, preferredType])
用来将参数转换成 Primitive
类型,即非 Object。
通常来说,使用到 ToPrimitive 的场景,都是在参数已经被判定是 Object 的条件之下。下面我们也以此为前提条件来梳理它的原理。
我们来看一看第二个参数preferredType
,这个参数是可选的,传入值为string和number这两个值。
preferredType
就是用来控制对象是偏向转换成哪种 Primitive 类型的。虽然它只能取值为数字和字符串,但并不限制 ToPrimitive 返回其他类型。
ToPrimitive
会先尝试取对象的一个方法,叫做 [Symbol.toPrimitive]
。
这个方法存在于对象本身或者原型链都可以,像下面这两种声明方式都是允许的:
js
const name = {
[Symbol.toPrimitive](hint: "default" | "number" | "string") {}
};
class name {
[Symbol.toPrimitive](hint: "default" | "number" | "string") {}
}
它的参数 hint
事实上就是 preferredType
因此,Symbol.toPrimitive
的引入相当于把内部方法 ToPrimitive
外包给了开发者去定义。ToString
在调用 ToPrimitive
的时候,preferredType
用的是 "string",因此下面的 hint
就是 "string":
js
var foo = {
[Symbol.toPrimitive](hint) {
switch(hint) {
case "number":
return 67;
case "string":
default:
return "foo"
}
}
};
console.log(`${foo}`); // "foo"
注意,[Symbol.toPrimitive]
必须返回一个 Primitive 类型,如果不是的话,就会抛出异常。在 ToString
的场景下,该返回值还会递归传入到 ToString
,确保最终生成一个字符串。
一般的hint默认传值为string。
当这个对象没有[Symbol.toPrimitive]
方法时,hint的默认传值就会偏向于numnber,但是这个时候就不是调用[Symbol.toPrimitive]
方法了,而是调用OrdinaryToPrimitive(O, preferredType)
方法。
在 OrdinaryToPrimitive
中,逻辑是这样的:
- 如果
preferredType
等于 "string",那么就会尝试依次调用对象的 toString 和 valueOf 方法,如果 toString 存在就不会调用 valueOf; - 如果
preferredType
等于 "number",那么就会尝试依次调用对象的 valueOf 和 toString 方法,如果 valueOf 存在就不会调用 toString; - 如果返回值不是 Primitive 类型,抛出异常。
对于一般的对象来说,其 toString 和 valueOf 都会上溯到原型对象 Object.prototype
中。
事实上,很多规范内置的对象类型,都对 toString 进行了重载,比如 Number、BigInt、Array、Error、Symbol、RegExp、Boolean、Date。因此,它们转换成字符串的时候,压根走不到 Object.prototype.toString
。
相等判断(==)
判断 A 和 B 的类型,如果相同,则转 IsStrictlyEqual(A, B)
,可见如果类型相同,==
与 ===
是等价的。
如果 A 和 B,一个是 String,一个是 Number,那么把 String 传入 toNumber()
,再和另一边共同传入 IsLooselyEqual
。也就是说,字符串和数字比较,是把字符串转换成数字,而不是把数字转换成字符串。以下代码可以作证:
js
15 == '0xF' // true
3 == '0b11' // true
如果 A 和 B 有一方是 Object,那么会把这个对象用 ToPrimitive
转换,再继续递归比较。注意,这里必须只有一方 是 Object,如果双方都是,就会走到前面的 IsStrictlyEqual
分支去了。
js
var A = {
valueOf() {
return 1;
},
};
var B = 1;
console.log(A == B); // true