JS隐式类型转换的秘密

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

模板字符串

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

  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]

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

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

  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。也就是说,字符串和数字比较,是把字符串转换成数字,而不是把数字转换成字符串。以下代码可以作证:

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
相关推荐
蓝天白云下遛狗17 分钟前
goole chrome变更默认搜索引擎为百度
前端·chrome
come1123440 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志1 小时前
TypeScript结构化类型初探
javascript
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘2 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl023 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang3 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景3 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui