看了一天的官方文档,终于搞懂了 JS 类型转换机制

JS 数据类型的转换机制是是学习 JS 过程中的一大难点,一度成为初学者面临的巨大挑战,这一复杂性往往源于 JS 的灵活性和动态性,使得在不同的上下文中,同一个值可能被解释为不同的数据类型。

本文将针从底层出发,借助官方文档,深度剖析 JS 类型转换机制。

JS 中的数据类型

首先我们回忆一下 JS 中的数据类型。

js 复制代码
//基本数据类型
let num = 123  //数字
let str = 'hello world'  //字符串
let flag = true  //布尔
let un = undefined  //未定义
let nu = null  //空值
let bigint = 9007199254740991n  //大整数
let sy = Symbol('symbol')  //符号

//引用数据类型
var person = { //对象
    name: "John",
    age: 30, 
    isStudent: false, 
    hobbies: ["reading", "coding"] 
};
var numbers = [1, 2, 3, 4, 5]; //数组
function foo() { //函数
    return "你好!掘友";
}
//等等

类型转换

JS 类型转换主要分为两种:隐式类型转换显式类型转换

1. 隐式类型转换:在隐式类型转换中,JS 引擎自动地将一种数据类型转换为另一种类型,通常发生在运算或比较的过程中。这种转换是隐式的,开发者不需要明确地进行操作,而是由 JS 引擎在必要的时候自动完成。

2. 显式类型转换:显式类型转换是由开发者明确指定的类型转换,通过调用相应的转换函数或使用一些特定的语法进行。这种转换是开发者有意识地进行的,用于确保数据在特定上下文中具有期望的类型。

看到这你可能还是一头雾水,举个例子:

js 复制代码
var a = 12; 
var b = a + ""; //隐式类型转换 
var c = String(a); //显式类型转换

在这里,隐式强制类型转换发生在变量 a 被赋值给变量 b 的过程中。通过将 a 与一个空字符串 "" 相加,JS 引擎会自动将 a 的值转换为字符串,然后与空字符串拼接在一起。这是一种常见的将数字转换为字符串的方式,是隐式的,因为你并没有明确指示进行类型转换。

而显式强制类型转换通过调用 String() 函数来实现。你明确告诉 JS 引擎将变量 a 的值转换为字符串。这种方式更加明显,开发者有意识地进行了类型转换。

总体而言,这两种方式都能将数字 12 转换为字符串,但发生的时机和方式略有不同。隐式转换是在某些操作中自动进行的,而显式转换是由开发者明确调用的。

下面我们分别对这两种类型转换展开分析。

显示转换

我们通常使用官方定义的 Number()String()Boolean() 等函数进行显示转换,这些函数提供了明确的方式将值转换为特定的数据类型。

原始值转其他类型

1. 原始值转换成布尔值

js 复制代码
console.log(Boolean()); // false
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
console.log(Boolean(-1)); // true
console.log(Boolean(undefined)); // false
console.log(Boolean(null)); // false
console.log(Boolean('123')); // true
console.log(Boolean('')); // false
console.log(Boolean(' ')); // true

见官方文档 9.2:Annotated ES5 9.2

简而言之:

  • false0''nullundefined以及NaN转换为false
  • 其他所有值转换为true

2. 原始值转换成数字

js 复制代码
console.log(Number('123')); //123
console.log(Number('abc')); //NaN
console.log(Number()); //0
console.log(Number(true)); //1
console.log(Number(false)); //0
console.log(Number(null)); //0
console.log(Number(undefined)); //NaN

见官方文档 9.3:Annotated ES5 9.3

大概的意思就是:

  • 如果是数字字符串,则转换为对应的数字。
  • 如果是非数字字符串或无法解析为数字的字符串,则转换为 NaN
  • 布尔值 true 被转换为 1false 被转换为 0
  • null 被转换为 0
  • undefined 被转换为 NaN

其中,对象转数字规则没有罗列,我们将在后文对 对象类型(Object)转数字类型进行着重分析。

3. 原始值转换成字符串

js 复制代码
console.log(String()) //''
console.log(String(123)) //'123'
console.log(String(NaN)) //'NaN'
console.log(String(undefined)) //'undefined'
console.log(String(null)) //'null'
console.log(String(true)) //'true'
  • 空值或没有提供参数时,返回空字符串。
  • 数字转字符串:直接将数字转换为对应的字符串形式。
  • NaN 被转换为字符串 'NaN'
  • 布尔值转字符串:true 转换为 "true"false 转换为 "false"
  • null 转换为字符串 "null"
  • undefined 转换为 "undefined"

见官方文档 9.8:Annotated ES5 9.8

4. 原始值转对象

原始数据类型可以通过其对应的包装对象来实现转换,例如:

js 复制代码
console.log(Object(true)) //[Boolean: true]
console.log(Object(123)) //[Number: 123]
console.log(Object('123')) //[String: '123']
console.log(Object(undefined)) //{}
console.log(Object(null)) //{}

见官方文档 9.9:Annotated ES5 9.9

  • 布尔值、数字、字符串分别被转换为对应的 Boolean、Number、String 对象。
  • 对于 undefined,使用 Object() 转换为一个空对象 {}
  • 对于 null,同样使用 Object() 转换为一个空对象 {}

以上就是原始值转换数据类型的官方解释,我们不用过多考虑,只需要无脑接受就好。

当我们要用到的时候,查下这个表就行了。

因为官方没有给NullUndefined定义显示转换的方法,所以我们暂且不聊。

然而以上这些内容都不是本文的重点,我们的重点是对象转换成原始值的过程,好戏才刚刚开始。

对象转原始值

对象转NullUndefined没有意义,所以我们只聊对象转字符串、数字和布尔值。

1. 对象转数字

这时候我们再打开官方文档,回到之前铺垫的位置,如果转换的对象为对象类型的话,会进行红框中的操作。

大概意思是,你传入一个值,经过ToPrimitive这样一个函数的调用并返回一个值,这个值会被转换成数字类型,我们可以通过文档(Annotated ES5 9.3)进入到ToPrimitive这个函数内部。

看不懂,出国留学一下······

意思很明了,当你传入的是基本数据类型的时候,不会进行转换。唯独传入的是对象(Object)的时候才会有进行转换,它会调用对象的 [[DefaultValue]] 内部方法,于是我们跟随文档,跳到 8.12.8 。

翻译就不给大家看了,直接告诉大家它什么意思吧。

ToPrimitive(obj, Number) 是 ECMAScript 规范中描述的一种抽象操作,用于将给定对象 obj 转换为一个原始值,在进行数据转换的时候才会触发。这里我们目标是要将对象转换成数字,所以第二个参数是Number,字符串则是String

下面是 ToPrimitive(obj, Number) 的执行步骤:

  1. 如果 obj 是基本类型:

    • 如果 obj 已经是基本类型(例如数字、字符串、布尔值等),那么直接返回 obj
  2. 否则,调用 valueOf 方法:

    • 如果 obj 不是基本类型,即它是一个对象,首先会尝试调用 valueOf 方法。
    • 如果 valueOf 方法存在并返回一个原始值,那么返回这个原始值。
  3. 否则,调用 toString 方法:

    • 如果 valueOf 方法不存在,或者它返回的仍然是一个对象,那么接下来会调用 toString 方法。
    • 如果 toString 方法存在并返回一个原始值,那么返回这个原始值。
  4. 否则,报错:

    • 如果 toString 方法也不存在,或者它返回的仍然是一个对象,那么就无法将对象转换为原始值,此时会抛出一个错误。

简化一下:

ToPrimitive(obj, Number) ==> Number({})

  1. 如果 obj 是基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果得到原始值,则返回
  3. 否则,调用 toString 方法,如果得到原始值,则返回
  4. 否则,报错

这就相当于一个公式,当我们在将一个对象转换成数字类型的时候,直接套就行。

举个例子:

js 复制代码
a = {}
console.log(Number(a)); //NaN

开始套公式:

  1. 如果 a 是基本类型:

    • a 是一个对象,不是基本类型。
  2. 调用 valueOf 方法:

    • 尝试调用 valueOf 方法,看是否可以获取到原始值。
    • 由于 a 是一个空对象 {},而普通的对象(没有重写 valueOf 方法的情况下)的 valueOf 方法会返回对象本身,而不是一个原始值。
  3. 调用 toString 方法:

    • 由于 valueOf 方法没有返回原始值,接下来调用 toString 方法。
    • 对于普通的空对象 {}toString 方法通常会返回 "[object Object]" 字符串。

调用toString方法的时候得到了原始值字符串"[object Object]",于是ToPrimitive这个函数将其返回,最后就相当于 Number("[object Object]") 执行了这段代码。在 JS 引擎内部,ToPrimitive这个函数相当于进行了将 Number(a) 转化成了 Number("[object Object]") ,相当于将字符串转换成数字,所以最终打印结果为 NaN

2. 对象转字符串

与对象转数字类似,唯一的区别就是转字符串时在ToPrimitive函数中是先调用 toString 方法,后调用 typeOf 方法。

ToPrimitive(obj, String) ==> String({})

  1. 如果 obj 是基本类型,直接返回
  2. 否则,调用 toString 方法,如果得到原始值,则返回
  3. 否则,调用 valueOf 方法,如果得到原始值,则返回
  4. 否则,报错

与对象转数字用法一致,区别在于调用 toString 方法和 valueOf 方法的顺序,区分这一点就行了。

3. 对象转布尔

js 复制代码
console.log(Boolean({})); // true
console.log(Boolean([])); // true

对象转换为布尔值时,结果总是 true


隐式转换

隐式转换通常发生在一些运算符操作中,其中一些运算符会触发 JS 引擎自动进行类型转换以满足操作的要求。下面我们简单聊一下 + 这个运算符。

一元运算 + a

官方文档(Annotated ES5 11.4.6)这样描述:

如果遇到 + 号,且它是当作一元运算符来使用,那么 + 号后面的数据一定要被强制转化为 Number 类型。

举个例子:

js 复制代码
console.log(+'1'); //1

这个例子中, + 后面接的是数字字符串,则转换成对应数字,所以打印结果为1,没问题。

那么 + 后面接的是对象类型呢,我们再举个例子:

js 复制代码
console.log(+{}); //NaN

这个例子中, + 后面接的是一个空对象,那么对象怎么转换成Number呢,是不是要走ToPrimitive函数那一套哇,直接套公式就行了。

我直接搬过来:

ToPrimitive(obj, Number) ==> Number({})

  1. 如果 obj 是基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果得到原始值,则返回
  3. 否则,调用 toString 方法,如果得到原始值,则返回
  4. 否则,报错

我们前面已经走过一遍流程了,这个ToPrimitive函数最后会返回一个"[object Object]"字符串,所以最终就相当于 + 后面接的是一个字符串,则转换成 NaN ,打印结果也为 NaN,没问题。

我们也可以试着模拟一下整个过程:

js 复制代码
// +{} 的过程模拟
console.log(+{}); //0
console.log({}.valueOf()); //{}
console.log({}.toString()); //"[object Object]"
//返回"[object Object]"字符串
console.log(Number("[object Object]")); //NaN

二元运算 a + b

官方文档(Annotated ES5 11.6.1)这样描述:

二元运算符 + 可以执行字符串连接,也可以执行数字加法。简而言之,将 + 左右两边的数据分别用ToPrimitive函数调用一遍,如果返回值中有一个是字符串,则进行字符串拼接;否则进行加法运算。

即:

lprim + rprim == ToPrimitive(v1) + ToPrimitive(v2)

  1. 当 + 两边有一个是字符串,则按字符串进行拼接
  2. 否则,转到 number 进行计算

举个例子:

js 复制代码
console.log(1 + '1'); //'11'

我们分别让数字1和字符串'1'走一遍ToPrimitive,分别观察它们的返回值,发现'1'是数字字符串,在 valueOftoString 方法后返回的还是'1'本身,即进行字符串拼接,得到打印结果'11'

当你理解透彻后很容易得到:

js 复制代码
console.log(1 + 'null'); //1
console.log([] + []); //''
console.log([] + {}); //'[object Object]'
console.log({} + {}); //'[object Object][object Object]'

最后

当你读到这段话时,你已经跨过了JS类型转换这一座大山!

希望这篇文章能够为你提供帮助,如果你还有不懂之处,可以反复阅读,或者在评论区留言,学习是一个循序渐进,敢于试错的过程,我们顶峰相见!

我的Gitee: CodeSpace (gitee.com)

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 "点赞 收藏+关注" ,感谢支持!!

相关推荐
博客zhu虎康7 分钟前
ElementUI 的 form 表单校验
前端·javascript·elementui
敲啊敲952736 分钟前
5.npm包
前端·npm·node.js
贵州晓智信息科技41 分钟前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
CodeClimb1 小时前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
咸鱼翻面儿1 小时前
Javascript异步,这次我真弄懂了!!!
javascript
brrdg_sefg1 小时前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_748230941 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_589568101 小时前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈2 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss