likes
comments
collection
share

JS隐式类型转换的秘密

作者站长头像
站长
· 阅读数 14

平时,我们使用模板字符串或者双等号(==)去判断是否相等的时候都会涉及到隐式类型转换的问题,下面我们来看看,学习并总结一下。

模板字符串

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) 是如何工作的。

  1. 判断入参类型,遍历一遍所有的 Primitive 类型:

    • 如果是 String,显然不用转换,直接返回;
    • 如果是 Symbol,抛出异常
    • 如果是 Undefined,就返回 “undefined”;
    • 如果是 Null,就返回 “null”;
    • 如果是 Boolean,就返回 “true” 或 “false”;
    • 如果是 Number 或者 BigInt,都转换成其 10 进制表示形式,这里面的细节不涉及类型转换,所以我们就不深究了,大家注意这里可能输出“NaN”、“Infinite”和科学记数法。
  2. 如果是非 Primitive 类型,也就是 Object,如何转换成字符串呢?答案是将参数带入到 ToPrimitive(arg, string)

ToPrimitive(input[, preferredType]) 用来将参数转换成 Primitive 类型,即非 Object。

通常来说,使用到 ToPrimitive 的场景,都是在参数已经被判定是 Object 的条件之下。下面我们也以此为前提条件来梳理它的原理。

我们来看一看第二个参数preferredType,这个参数是可选的,传入值为string和number这两个值。

preferredType 就是用来控制对象是偏向转换成哪种 Primitive 类型的。虽然它只能取值为数字和字符串,但并不限制 ToPrimitive 返回其他类型。

ToPrimitive 会先尝试取对象的一个方法,叫做 [Symbol.toPrimitive]

这个方法存在于对象本身或者原型链都可以,像下面这两种声明方式都是允许的:

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”:

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 中,逻辑是这样的:

  1. 如果 preferredType 等于 “string”,那么就会尝试依次调用对象的 toString 和 valueOf 方法,如果 toString 存在就不会调用 valueOf;
  2. 如果 preferredType 等于 “number”,那么就会尝试依次调用对象的 valueOf 和 toString 方法,如果 valueOf 存在就不会调用 toString;
  3. 如果返回值不是 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。也就是说,字符串和数字比较,是把字符串转换成数字,而不是把数字转换成字符串。以下代码可以作证:

15 == '0xF' // true
3 == '0b11' // true

如果 A 和 B 有一方是 Object,那么会把这个对象用 ToPrimitive 转换,再继续递归比较。注意,这里必须只有一方是 Object,如果双方都是,就会走到前面的 IsStrictlyEqual 分支去了。

var A = {
    valueOf() {
        return 1;
    },
};

var B = 1;

console.log(A == B); // true