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
相关推荐
cyadyx30 分钟前
Vite 比 Webpack 构建效率更高
前端·webpack·node.js·vite
mCell2 小时前
AI 时代 SVG 画图的潜力
前端·agent·svg
我命由我123458 小时前
CesiumJS 笔记 - 获取容器中心点、Cartesian3 clone 方法、修改 Cartesian3 对象的高度
前端·javascript·css·前端框架·html·html5·js
Hopebearer_9 小时前
页面突然只剩 DOM?一次静态资源版本错配排查
前端·部署
抱抱宝9 小时前
Agent-study项目教程(03):手写 Mini-ReAct Agent(不依赖框架)
javascript·人工智能·gpt·react.js·prompt·agent
东风破_9 小时前
ESLint 是什么?为什么你的项目需要它?
前端·后端·代码规范
用户938515635079 小时前
Next.js 笔记系统(二):Redis 数据服务与侧边栏组件拆分实战
javascript·全栈
圣殿骑士-Khtangc11 小时前
Go字符串高效拼接性能对比与底层原理分析
服务器·前端·golang
BigTopOne11 小时前
【ijkplayer】 硬解码流程
前端
kyriewen12 小时前
DeepSeek Harness开源第一天我就上手了——和Claude Code的差距比想象中大
前端·ai编程·deepseek