今天你学会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》(中)。

相关推荐
Lotzinfly2 小时前
8 个经过实战检验的 Promise 奇淫技巧你需要掌握😏😏😏
前端·javascript·面试
小桥风满袖3 小时前
极简三分钟ES6 - ES9中对象扩展
前端·javascript
城中的雾3 小时前
HarmonyOS应用拉起系列(三):如何直接拉起腾讯/百度/高德地图进行导航
前端·javascript·harmonyos
Mintopia3 小时前
Next 全栈之 API 测试:Supertest 与 MSW 双雄记 🥷⚔️
前端·javascript·next.js
鹏多多3 小时前
纯前端人脸识别利器:face-api.js手把手深入解析教学
前端·javascript·人工智能
江城开朗的豌豆4 小时前
从生命周期到useEffect:我的React函数组件进化之旅
前端·javascript·react.js
江城开朗的豌豆4 小时前
React组件传值:轻松掌握React组件通信秘籍
前端·javascript·react.js
Sailing4 小时前
别再放任用户乱填 IP 了!一套前端 IP 与 CIDR 校验的高效方案
前端·javascript·面试
遂心_18 小时前
JavaScript 函数参数传递机制:一道经典面试题解析
前端·javascript