Jamey's Jamey's
首页
导航站
  • 学习专栏

    • 《HTML》笔记
    • 《CSS》笔记
    • 《JavaScript》笔记
    • 《Vue》笔记
    • 《Git》笔记
    • 《规范》笔记
    • 《软技能》笔记
    • 《面试》笔记
    • 《持续集成&交付&部署》笔记
  • 踩坑专栏

    • 《Element-UI 实践系列》笔记
    • 《移动端 实践系列》笔记
    • 《综合》笔记
  • 配置专栏

    • 《环境系列》笔记
  • 极空间

    • Docker
  • 影视

    • movie
  • 编辑器笔记

    • 开发编辑器
  • 浏览器笔记

    • Chrome
  • Mac笔记

    • Mac
  • 跨界学习

    • 运营
  • 破解合集

    • 破解
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 书单
    • 关于

Jamey

首页
导航站
  • 学习专栏

    • 《HTML》笔记
    • 《CSS》笔记
    • 《JavaScript》笔记
    • 《Vue》笔记
    • 《Git》笔记
    • 《规范》笔记
    • 《软技能》笔记
    • 《面试》笔记
    • 《持续集成&交付&部署》笔记
  • 踩坑专栏

    • 《Element-UI 实践系列》笔记
    • 《移动端 实践系列》笔记
    • 《综合》笔记
  • 配置专栏

    • 《环境系列》笔记
  • 极空间

    • Docker
  • 影视

    • movie
  • 编辑器笔记

    • 开发编辑器
  • 浏览器笔记

    • Chrome
  • Mac笔记

    • Mac
  • 跨界学习

    • 运营
  • 破解合集

    • 破解
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 书单
    • 关于
  • 深入系列

    • 为什么要写?
    • JavaScript 深入之从原型到原型链
    • JavaScript 深入之词法作用域和动态作用域
    • JavaScript 深入之执行上下文栈
    • JavaScript 深入之变量对象
    • JavaScript 深入之作用域链
    • JavaScript 深入之从 ECMAScript 规范解读 this
    • JavaScript 深入之执行上下文
    • JavaScript 深入之闭包
    • JavaScript 深入之参数按值传递
    • JavaScript 深入之 call 和 apply 的模拟实现
    • JavaScript 深入之 bind 的模拟实现
    • JavaScript 深入系列之 new 的模拟实现
    • JavaScript 深入之类数组对象与 arguments
    • JavaScript 深入之创建对象的多种方式以及优缺点
    • JavaScript 深入之继承的多种方式以及优缺点
    • JavaScript 深入之浮点数精度
    • JavaScript 深入之头疼的类型转换(上)
    • JavaScript 深入之头疼的类型转换(下)
      • 📖. 前言
      • 一. 一元操作符 +
      • 二. 二元操作符 +
        • 1. 规范
        • 2. Null 与数字
        • 3. 数组与数组
        • 4. 数组与对象
      • 三. 注意
      • 四. == 相等
        • 1. 规范
        • 2. null 和 undefined
        • 3. 字符串与数字
        • 4. 布尔值和其他类型
        • 5. 对象与非对象
        • 6. 其他
      • 五. 其他
  • 专题系列

  • underscore系列

  • ES6系列

  • 模块化

  • 正则表达式

  • 单元测试

  • 微前端

  • 实用函数

  • Rollup

  • 解决方案

  • 《JavaScript》笔记
  • 深入系列
Jamey
2021-10-17
目录

JavaScript 深入之头疼的类型转换(下)

# JavaScript 深入之头疼的类型转换(下)

# 📖. 前言

举个例子:

console.log(1 + '1');
1

在 JavaScript 中,这是完全可以运行的,不过你有没有好奇,为什么 1 和 '1' 分属不同的数据类型,为什么就可以进行运算呢?

这其实是因为 JavaScript 自动的将数据类型进行了转换,我们通常称为隐式类型转换。但是我们都知道,+ 运算符既可以用于数字加法,也能用于字符串拼接,那在这个例子中,是将数字 1 转成字符串 '1',进行拼接运算? 还是将字符串 '1' 转成数字 1,进行加法运算呢?

先卖个关子,虽然估计你也知道答案。今天,我们就常见的隐式类型转化的场景进行介绍。

# 一. 一元操作符 +

console.log(+'1');
1

当 + 运算符作为一元操作符的时候,查看 ES5规范1.4.6 (opens new window) ,会调用 ToNumber 处理该值,相当于 Number('1'),最终结果返回数字 1。

那么下面的这些结果呢?

console.log(+[]);
console.log(+['1']);
console.log(+['1', '2', '3']);
console.log(+{});
1
2
3
4

既然是调用 ToNumber 方法,回想《JavaScript深入之头疼的类型转换(上)》 中的内容,当输入的值是对象的时候,先调用 ToPrimitive(input, Number) 方法, 执行的步骤是:

  1. 如果 obj 为基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

以 +[] 为例,[] 调用 valueOf 方法,返回一个空数组,因为不是原始值,调用 toString 方法,返回 ``。

得到返回值后,然后再调用 ToNumber 方法,"" 对应的返回值是 0,所以最终返回 0。

剩下的例子以此类推。结果是:

console.log(+['1']); // 1
console.log(+['1', '2', '3']); // NaN
console.log(+{}); // NaN
1
2
3

# 二. 二元操作符 +

# 1. 规范

现在 + 运算符又变成了二元操作符,毕竟它也是加减乘除中的加号

1 + '1' 我们知道答案是 '11',那 null + 1、[] + []、[] + {}、{} + {} 呢?

如果要了解这些运算的结果,不可避免的我们要从规范下手。

规范地址: http://es5.github.io/#x11.6.1 (opens new window)

不过这次就不直接大段大段的引用规范了,直接给大家讲简化后的内容。

到底当执行 + 运算的时候,会执行怎样的步骤呢?让我们根据规范 11.6.1 来捋一捋:

当计算 value1 + value2时:

  1. lprim = ToPrimitive(value1)
  2. rprim = ToPrimitive(value2)
  3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim) 的拼接结果
  4. 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

规范的内容就这样结束了。没有什么新的内容,ToString、ToNumber、ToPrimitive 都是在《JavaScript 深入之头疼的类型转换(上)》 中讲到过的内容, 所以我们直接进分析阶段:

让我们来举几个例子:

# 2. Null 与数字

console.log(null + 1);
1

按照规范的步骤进行分析:

  1. lprim = ToPrimitive(null) 因为null 是基本类型,直接返回,所以 lprim = null
  2. rprim = ToPrimitive(1) 因为 1 是基本类型,直接返回,所以 rprim = null
  3. lprim 和 rprim 都不是字符串
  4. 返回 ToNumber(null) 和 ToNumber(1) 的运算结果

接下来:

ToNumber(null) 的结果为0,(回想上篇 Number(null)),ToNumber(1) 的结果为 1

所以,null + 1 相当于 0 + 1,最终的结果为数字 1。

这个还算简单,看些稍微复杂的:

# 3. 数组与数组

console.log([] + []);
1

依然按照规范:

  1. lprim = ToPrimitive([]),[]是数组,相当于 ToPrimitive([], Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回空字符串 ""
  2. rprim 类似。
  3. lprim 和 rprim 都是字符串,执行拼接操作

所以,[] + [] 相当于 "" + "",最终的结果是空字符串 ""。

看个更复杂的:

# 4. 数组与对象

// 两者结果一致
console.log([] + {});
console.log({} + []);
1
2
3

按照规范:

  1. lprim = ToPrimitive([]),lprim = ""
  2. rprim = ToPrimitive({}),相当于调用 ToPrimitive({}, Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回 "[object Object]"
  3. lprim 和 rprim 都是字符串,执行拼接操作

所以,[] + {} 相当于 "" + "[object Object]",最终的结果是 "[object Object]"。

下面的例子,可以按照示例类推出结果:

console.log(1 + true);
console.log({} + {});
console.log(new Date(2017, 04, 21) + 1) // 这个知道是数字还是字符串类型就行
1
2
3

结果是:

console.log(1 + true); // 2
console.log({} + {}); // "[object Object][object Object]"
console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"
1
2
3

# 三. 注意

以上的运算都是在 console.log 中进行,如果你直接在 Chrome 或者 Firebug 开发工具中的命令行直接输入,你也许会惊讶的看到一些结果的不同,比如:

javascript_03-13_04

我们刚才才说过 {} + [] 的结果是 "[object Object]" 呐,这怎么变成了 0 了?

不急,我们尝试着加一个括号:

javascript_03-13_05

结果又变成了正确的值,这是为什么呢?

其实,在不加括号的时候,{} 被当成了一个独立的空代码块,所以 {} + [] 变成了 +[],结果就变成了 0。

同样的问题还出现在 {} + {} 上,而且火狐和谷歌的结果还不一样:

> {} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"
1
2
3

如果 {} 被当成一个独立的代码块,那么这句话相当于 +{},相当于 Number({}),结果自然是 NaN,可是 Chrome 却在这里返回了正确的值。

那为什么这里就返回了正确的值呢?我也不知道,欢迎解答~

# 四. == 相等

# 1. 规范

"==" 用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。

关于使用 "==" 进行比较的时候,具体步骤可以查看 规范11.9.5 (opens new window) :

当执行 x == y 时:

  1. 如果 x 与 y 是同一类型:
    i. x 是 Undefined,返回 true
    ii. x 是 Null,返回 true
    iii. x 是数字:
        a. x 是 NaN,返回 false
        b. y 是 NaN,返回 false
        x 与 y 相等,返回 true
        x 是 +0,y 是 -0,返回 true
        x 是 -0,y 是 +0,返回 true
        返回 false iv. x 是字符串,完全相等返回 true,否则返回 false v. x 是布尔值,x 和 y 都是 true 或者 false,返回 true,否则返回 false vi. x 和 y 指向同一个对象,返回 true,否则返回 false
  2. x 是 null 并且 y 是 undefined,返回 true
  3. x 是 undefined 并且y是 null,返回 true
  4. x 是数字,y 是字符串,判断 x == ToNumber(y)
  5. x 是字符串,y 是数字,判断 ToNumber(x) == y
  6. x 是布尔值,判断 ToNumber(x) == y
  7. y 是布尔值,判断 x ==ToNumber(y)
  8. x 是字符串或者数字,y 是对象,判断 x == ToPrimitive(y)
  9. x 是对象,y 是字符串或者数字,判断 ToPrimitive(x) == y
  10. 返回 false

觉得看规范判断太复杂?我们来分几种情况来看:

# 2. null 和 undefined

console.log(null == undefined);
1

看规范第2、3步:

  1. x 是 null 并且 y 是 undefined,返回 true
  1. x 是 undefined 并且 y 是 null,返回 true

所以例子的结果自然为 true。

这时候,我们可以回想在 《JavaScript专题之类型判断(上)》 中见过的一段 demo,就是编写判断对象的类型 type 函数时,如果输入值是 undefined,就返回字符串 undefined,如果是 null,就返回字符串 null。

如果是你,你会怎么写呢?

下面是 jQuery 的写法:

function type(obj) {
  if (obj == null) {
    return obj + '';
  }
  ...
}
1
2
3
4
5
6

# 3. 字符串与数字

console.log('1' == 1);
1

结果肯定是true,问题在于是字符串转化成了数字和数字比较还是数字转换成了字符串和字符串比较呢?

看规范第4、5步:

  1. x 是数字,y 是字符串,判断 x == ToNumber(y)
  1. x 是字符串,y 是数字,判断 ToNumber(x) == y

结果很明显,都是转换成数字后再进行比较

# 4. 布尔值和其他类型

console.log(true == '2');
1

当要判断的一方出现 false 的时候,往往最容易出错,比如上面这个例子,凭直觉应该是 true,毕竟 Boolean('2') 的结果可是 true,但这道题的结果却是 false。

归根到底,还是要看规范,规范第6、7步:

  1. x 是布尔值,判断 ToNumber(x) == y
  1. y 是布尔值,判断 x ==ToNumber(y)

当一方出现布尔值的时候,就会对这一方的值进行 ToNumber 处理,也就是说 true 会被转化成 1,

true == '2' 就相当于 1 == '2' 就相当于 1 == 2,结果自然是 false。

所以当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == true 和 xx == false 的写法。

比如:

// 不建议
if (a == true) {};

// 建议
if (a) {};
// 更好
if (!!a) {};
1
2
3
4
5
6
7

# 5. 对象与非对象

console.log( 42 == ['42']);
1

看规范第8、9步:

  1. x 不是字符串或者数字,y 是对象,判断 x == ToPrimitive(y)
  1. x 是对象,y 不是字符串或者数字,判断 ToPrimitive(x) == y

以这个例子为例,会使用 ToPrimitive 处理 ['42'],调用 valueOf,返回对象本身,再调用 toString,返回 '42',所以

42 == ['42'] 相当于 42 == '42' 相当于 42 == 42,结果为 true。

到此为止,我们已经看完了第2、3、4、5、6、7、8、9步,其他的一概返回 false。

# 6. 其他

再多举几个例子进行分析:

console.log(false == undefined);
1

false == undefined 相当于 0 == undefined 不符合上面的情形,执行最后一步 返回 false

console.log(false == []);
1

false == [] 相当于 0 == [] 相当于 0 == '' 相当于 0 == 0,结果返回 true

console.log([] == ![]);
1

首先会执行 ![] 操作,转换成 false,相当于 [] == false 相当于 [] == 0 相当于 '' == 0 相当于 0 == 0,结果返回 true

最后再举一些会让人踩坑的例子:

console.log(false == "0");
console.log(false == 0);
console.log(false == "");

console.log("" == 0);
console.log("" == []);

console.log([] == 0);

console.log("" == [null]);
console.log(0 == "\n");
console.log([] == 0);
1
2
3
4
5
6
7
8
9
10
11
12

以上均返回 true

# 五. 其他

除了这两种情形之外,其实还有很多情形会发生隐式类型转换,比如 if、? :、&& 等情况,但相对来说,比较简单,就不再讲解。

#JavaScript 深入
上次更新: 2022/07/08, 18:18:02
JavaScript 深入之头疼的类型转换(上)
写在前面

← JavaScript 深入之头疼的类型转换(上) 写在前面→

Theme by Vdoing | Copyright © 2017-2023 Jamey | blog 闽ICP备19022664号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式