今天你学会JS的类型转换了吗?

最近在读《你不知道的 Javascript》,在变量部分,觉得内容很是有趣,工作中其实每天都在使用变量,但有些骚操作(奇技淫巧)看到后还是想写出来与各位分享一下。

先从一个判断语句开始说起

js 复制代码
let a = 3;
let b = [3];
a == b; // 这个语句会输出什么?

上面这个语句如果在实际开发中,相信各位肯定不会用 == 来进行判断。 ===== 的区别是: == 会对比较类型进行强制的类型转换,而 === 不会,所以平时我们总是说 == 是宽松的比较,=== 是严格的相等。

js 复制代码
let a = "45";
let b = 45;
a == b; // true
a === b; // false

这里再说明一点,== 是怎样的转换逻辑?我们知道 == 比较会自动进行强制类型转换,这种转换遵循的是什么规则?

摘录一下书中的引用

  1. 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
  2. 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
  3. 如果 Type(x) 是字符串/数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果
  4. 如果 Type(y) 是对象,Type(x) 是字符串/数字,则返回 ToPrimitive(x) == y 的结果

所以总结来说,如果比较的是基本数据类型,会先转换为数字进行比较,如果是对象比较,则会通过 ToPrimitive 进行隐式转换。这里提到的 ToNumber 在书中被称为「抽象操作」,在「抽象操作」中,不仅有 ToNumber,还有 ToString, ToBoolean。话已到此,我们再开一个分支先来讲讲这些抽象操作的作用及用法。

分支:抽象操作

在 ES5 中定义了一些「抽象操作」,其作用是内部"虚拟"的一些操作或者方法,无法直接调用这些行为,是一种隐式的类型转换,接下来会介绍几种常见的抽象操作。

  • ToString :处理非字符串到字符串的转换。有一些常见的规则,例如 null -> 'null', undefined -> 'undefined',如果是普通的对象,toString() 会调用内部属性 [[class]] 的值,如 "[object Object]" 所以你可以自定义(复写)对象的 toString() 方法,以便能在 ToString 过程中输出你想要的字符串值。

    • 没什么用的小知识 💡 JSON.stringify() 同样会调用 toString() 方法进行 JSON 对象的序列化,对于一些内置的「非安全」值会有特殊的处理,例如在数组中有 null、undefined 值会直接输出 null 作为替代。 如果你在对象中定义了 toJSON() 方法,那么 JSON.stringify() 时,会先调用 toJSON 得到正确的 JSON 对象后再进行序列化处理。 ⚠️ toJSON 需要返回一个对象而不是字符串。

      js 复制代码
      let o = {};
      let a = {
        b: 42,
        c: o,
        d: function () {},
      };
      o.e = a; // 创建循环引用,此时 a 对象中包含了「非安全」值,此时 JSON.stringify() 会报错
      a.toJSON = function () {
        return { b: this.b };
      };
      JSON.stringify(a); // '{"b": 42}'
  • ToNumber : 处理非数字值到数字值的转换。对于 ToString 的抽象操作是通过 toString() 方法来实现,那么 ToNumber 是通过什么方法实现呢? 对于 ToNumber ,JS 会首先检查对象是否含有 valueOf 方法,如果没有则检查是否含有 toString 方法,然后对其中某个函数返回的值进行强制类型转换,如果这两个方法都没有返回的「基本类型」值,则会输出 TypeError 错误。

    js 复制代码
    var a = {
      valueOf: () => {
        return "45";
      },
    };
    Number(a); // 45
  • ToBoolean : 处理非布尔类型的值的转换。这个转换在实际开发过程中使用较多,常见的假值包含如下几种: undefined, null, false, +0, -0, NaN, ""。对象一般都会被强制转换为 true,也有例外。

    • 没什么用的小知识 💡 document.all 是 JS 中的一个假值对象,因为它在现代浏览器中已经被废除,经常通过 if (document.all) 来判读是否在 IE 浏览器环境。

回到主线

所以我们在来检查之前的问题,下述判断会输出什么?

js 复制代码
let a = 3;
let b = [3];
a == b; // 这个语句会输出什么?

我们知道非严格相等 == 会进行隐式的类型转换(抽象操作),所以对于上述比较,左侧 a 就是数字无需转换,右侧 b 是数组,对于数组来说,会进行 ToNumber 的隐式转换,调用数组的 ToPrimitive 方法(先检查 valueOf,再检查 toString)对于数组来说,toString 方法被重新定义过了,故等式右侧会被转换为 3。所以上述比较返回了 true

js 复制代码
let a = [1, 2, 3];
String(a); // '1,2,3'

延伸

所以我们之前提到的 ToPrimitive 的含义是什么?即 ECMASCript 规范中定义的「抽象操作」,按照字面理解即返回原始值 。在执行 ToPrimitive 操作时,遵循下面的顺序

  1. 如果有 valueOf 方法,则优先调用该方法返回原始值
  2. 否则,则检查是否有 toString 方法,调用该方法返回原始值
  3. 否则,则直接返回类型错误 所以 ToPrimitive 实质是担任了将对象转换为原始值的作用,这里就不得不提「拆箱」和「装箱」的概念了。

分支:对象的「拆箱」和「装箱」

「装箱」:boxing,顾名思义,就是打包的意思,这里可以理解为将属性和方法打包 「拆箱」:unboxing,同理,拆除包装,还原为之前的状态 这两种行为我们在日常开发中经常用到,概括来说: 「装箱」即将基本类型的值转换为对应的对象 「拆箱」则是将对象转换为基本类型

js 复制代码
let a = "123"; // JS 会自动为 a 进行装箱操作,这样可以像「字符串对象」一样使用变量 a
a.length; // 3
a.toUpperCase(); // "ABC"

上面的例子中,JS 会自动帮我们进行装箱操作,那么我们可以自己进行装箱吗?

  • 没什么用的小知识 💡

    js 复制代码
    let a = new Boolean(false);
    if (!a) {
      console.log("可以执行到这儿么?");
    }

    上面代码是无法进行到分支中的,我们自己对 false 进行了装箱操作,a 此刻变成了一个对象,在 if 条件中进行了隐式的类型转换,而此时 Boolean(a) === true。 所以在实际开发中,我们应该优先使用 JS 自动的装箱逻辑,相对于自己进行装箱,JS 的装箱性能也较优,也可以规避一些不太明显的错误。 对于「拆箱」实质就是 ToPrimitive 的过程,对于要使用到基本类型的地方,都会进行隐式的「拆箱」过程

返回主线

思考如下代码:

js 复制代码
let a = "abc";
let b = Object(a);

a == b; // true
a === b; // false

这里 Object 函数的作用类似「装箱」,将变量 a 封装为对象,所以在宽松比较 == 时,会进行 ToPrimitive 逻辑,此时 b 会被隐式转换为 'abc',所以 a == b 等式成立。

  • 没什么用的小知识 💡

    js 复制代码
    let a = null;
    let b = Object(null);
    a == b; // false

    由于 null 没有对象的包装对象,所以 b 此刻是一个普通的对象,a == b 等式不成立。所以同理可以推断 undefined, NaN 这种特殊的字段也会是相同结果。

延伸

我们在日常开发中,经常会使用 || 的方式设置默认值

js 复制代码
let a = c || "default value";

上述语句中,如果 cToPrimitivefalse/undefined/null 等假值时,就会使用 || 右侧的值来为 a 赋值。

题外话

js 复制代码
let a = c && b;
// 等效为
if (c) {
  b;
}

对于 && 来说,相当于是逻辑判断的简略写法。一些压缩代码工具压缩后,常见的逻辑判断会被转换为 xx && yy

总结和引用

JS 中由于灵活的类型声明,导致了隐式转换几乎遍布于整个代码中,日常开发有些驾轻就熟的使用技巧多理解些原理也会更加从容,有些知识是常看常新的。 欢迎大家互相交流。

本文中例子大部分引用于《你不知道的 JavaScript》(中)。

相关推荐
前端之虎陈随易1 小时前
2年没用Nodejs了,Bun很香
linux·前端·javascript·vue.js·typescript
好运的阿财2 小时前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
XiYang-DING3 小时前
JavaScript
开发语言·javascript·ecmascript
空中海3 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
空中海4 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
空中海4 小时前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
杨超凡5 小时前
豆包收费了?我特么自己用“意念”搓了一个!
javascript
threelab6 小时前
Three.js 咖啡杯烟雾效果 | 三维可视化 / AI 提示词
开发语言·javascript·人工智能
Heo6 小时前
14_React 中的更新队列 updateQueue
前端·javascript·面试