《You Dont Know JS中卷》(四) ---强制类型转换

60 阅读12分钟

强制类型转换

JS中的类型转换

类型转换发生在静态类型语言编译时,强制类型转换发生在动态类型语言的运行时.因此,js中的类型转换属于强制类型转换.

在本书中,作者将因为一些操作的副作用而引起的强制类型转换称为: 隐式强制类型转换这种类型转换是自动发生的.其他手动指定的类型转换称为显式强制类型转换

const a = 42;
const b = String(a); // 显示强制类型转换
const c = a + "";    // 隐式强制类型转换

基本的类型转换规则

在深入了解强制类型转换之前,需要先掌握: 字符串,布尔值,数字之间的类型转换规则.这些基本规则非常重要

ToPromitive

该操作表示将对象转换为基本数据类型(string,number,boolean,null,undefined,symbol,bigInt),具体方式为:

  1. 首先调用内置属性Symbol.toPrimitive,该方法如果存在必须返回一个原始值,
  2. 如果没有该方法,则调用valueOf()方法,将对象拆封获取原始值,
  3. 如果返回的不是原始值,则调用toString,获取基本数据类型的值,
    1. 对象重定义的toString方法会优先调用,否则调用Object.prototype.toSting
      1. 对象: 返回[object Object]
      2. 数组: 重写了toString方法: 将元素转换为字符串后使用,拼接起来
  4. 如果这两个方法都没有,或者返回的值都不是原始数据类型则抛出错误TypeError.

ToString

这个操作负责处理非字符串转换到字符串的强制类型转换,转换规则如下:

基本类型转换字符串
  1. null转换为"null"
  2. undefined转换为`"undefine
  3. true/false转换为"true"/"false"
  4. number: 54转换为"54",但对于极大极小的数字,会转换成指数形式的字符串
  5. 对象:首先通过ToPrimitive操作将对象转换为基本类型的值.转换完成后根据上述规则进行转换
JSON.stringify转换规则
  1. 所有安全的JSON值都可以通过JSON.stringify转换为字符串,不安全的值在转换过程中会进行忽略,包括:

    • undefined

    • function

    • symbol

    • 循环引用

  2. 第二个参数

    1. 指定对象序列化过程中哪些属性需要被排除,参数可以是数组或者函数
      1. 数组: 必须是字符串数组,表示需要处理的属性名称,不包含在数组中的属性不会进行处理
      2. 函数: 序列化时,对对象本身调用一次,随后对对象中的每一个属性调用一次,如果想排除某个属性,则返回undefined,参数(k:属性名,v:值)第一次调用时kundefined
  3. 第三个参数

    1. 指定输出的格式,正数: 表示缩进的字符数,字符串: 缩进位置会使用该字符串替换

ToNumber

这个操作负责处理非数字转换到数字的强制类型转换,转换规则如下:

  1. null: 0
  2. undefined: NaN
  3. true: 1
  4. false: 0
  5. 字符串:
    1. 空字符串或者代表空格的字符串如:'/n': 0
    2. 数字字符串: 对应的数字
    3. 其他字符串: NaN
  6. 对象:执行ToPrimitive操作,将对象转换为基本数据类型,然后根据上述操作进行转换

ToBoolean

这个操作负责处理非布尔值转换到不布尔值的强制类型转换,转换规则如下:

假值列表

  • null
  • undefined
  • +0,-0
  • ""
  • NaN
  • false

假值列表中的值在转换为布尔值时,转换为false,假值列表之外的值转换为布尔值时,转换为true

特别的: js中存在假值对象,如document.all,由DOM提供给js引擎,转换时转换为false,但这时个例

强制类型转换分类

章节开头已经提到过: 作者将强制类型转换分为显式隐式.这里聊一下个人理解:

  • 显式强制类型转换:手动明确在什么地方进行类型转换

    const num = 1;
    const str = String(num) // 明确表示在这里需要将`num`强制类型转换为`string`
    
  • 隐式强制类型转换: 由于一些操作的副作用引起的类型转换

    const num = 1;
    const str = num + '2' // 由于加法操作,根据加法操作运算规则,自动将num转换为字符串,然后执行字符串拼接操作
    

显式强制类型转换

下面来看一下不同场景下,显式强制类型转换有哪些操作

字符串与数字转换

  • Number(): 强制转换为数字

  • String(): 强制转换为字符串

  • +: 将字符串转换为数字

    • const a = '3';
      const b = +a; // b => 3
      

解析数字字符串

解析数字字符串和转换数字字符串式两个不同的概念,请注意不要混淆

js提供了从数字字符串中解析出可用数字的方法:

  • paserInt(): 从字符串中解析出整数
  • parseFloat():从字符串中解析出浮点数
const a = 42;
const b = '42px';
//从字符串中解析出数字
parseInt(a) // 42
paserInt(b) // 42
//将字符串转换为数字
Number(a) // 42
Number(b) // NaN

具体的用法请查看MDN,这里提到是因为这个两个方法在使用时涉及强制类型转换:接收的第一个参数必须是字符串,否则会强制转换为字符串,然后再解析

举个例子: 
parseInt(1 / 0, 19); //猜猜这里会返回什么?

答案是: 18

整个解析过程是这样的:
1.1/0求值为infinity
2.Infinity是字符串吗?显然不是,那么转换成字符串`Infinity`
3.根据第二个参数指定的机制,解析字符串`Infinity`,第一个字母为I,值为18,第二个字符为n,不在进制内,解析结束,返回18

下面还有些结果很奇怪,但分析起来又很合理的例子:

parseInt(0.00008) // 0
parseInt(0.00000008) //8(8来自8e-7)
parseInt([1] + []); // 1
parseInt(parseInt,16) // 15 (15来自functiuon的f)

显式转换为布尔值

  1. Boolean()
  2. 一元运算符:!

隐式强制类型转换

下面来看一下不同场景下,显式强制类型转换有哪些操作

字符串与数字

  • +号运算符,运算规则:

    • 如果一个操作数是string,或者能通过ToPrimitive操作转换为string,则执行字符串拼接操作
    • 否则执行数字加法
    3 + "" => '3'
    [1,2] + 3 => '1,23'
    
  • -号运算符,运算规则:

    • 将操作数转换为number进行计算

隐式转换为布尔值

  1. if()中的条件判断表达式
  2. for(...;...;...)中的条件判断表达式
  3. while(),do..while()中的条件判断表达式
  4. ? :中的条件判断表达式

上述情况中,代码执行时,会将表达式隐式转换为boolean,在进行判断,下面的|| ,&& 也会将操作数隐式转换为boolean,但其返回值有些特殊,因此单独拎出来说:

||, &&

逻辑运算符||,&&,在js中应该称为操作数选择运算符跟贴切,为什么呢?因为在js中他们返回的并不是布尔值,而是具体的操作数

  • ||: 对左侧操作数隐式转换为boolean,如果为真,返回左侧操作数的,如果为假,返回右侧操作数的
    • 常用场景: 为函数参数赋默认值,arg1 = arg2 || 默认值
  • &&: 对左侧操作数隐式转换为boolean,如果为真,返回右侧操作数的,如果为假返回左侧操作数的
    • 常用场景: 作为守护运算符,让前面的表达式为后面的表达式把关,如fn函数存在才能调用: fn && fn()

显然虽然转换你过程中涉及了隐式强制类型转换,但最终返回的确实表达式的值,并非布尔值

Symbol的强制类型转换

symbol在类型转换时有几个注意点:

  • symbol转换为 string 时,允许显式强制类型转换,不允许隐式强制类型转换

    const symbol = Symbol("这是个符号")
    String(symbol); // 'symbol(这是个符号)'
    symbol + "" // 报错
    
  • symbol不能转换为number

  • symbol转换为布尔值,永远是true

宽松相等和严格相等

宽松相等==与严格相等===的常见误区: ==检查值相等,===检查值和类型相等.这个解释是不准确的.正确的解释应该是: ==允许在相等比较中进行强制类型转换,而===不允许,

虽然==在日常开发中很少用到,但由于其中涉及隐式强制类型转换,并且作者也强调==虽然广受诟病,不能因噎废食,所以这里将详细探讨一下宽松相等在不同情况下的具体实现:

抽象相等

抽象相等比较算法定义了==在运算时的行为,比较规则如下:

  • 类型相同: 比较值是否相等,特殊的:
    • NaN不等于NaN
    • +0 等于 -0
  • 两个都是对象: 如果两个值指向同一个值,即视为相等,否则视为不相等,这里和严格相等的行为一致
  • 两个值类型不同: 进行隐式强制类型转换,将一个或两个转换成相同类型之后进行比较,具体比较规则视情况而定,共有以下几种情况:
    • 字符串和数字
    • 布尔值和其他类型
    • null,undefined,其他类型比较
    • 对象和非对象比较

下面将对上述最后一种情况: 两个值类型不同,进行详细讲解:

字符串和数字

规则: 将两者中的字符串进行ToNumber操作(上文中ToNumber详细讲解)转换为数字,然后进行比较

布尔值和其他类型

规则:将两者中的布尔值执行ToNumber操作转换为数字,然后进行比较,如果类型仍不相同,视不同情况继续转换:

"42" == true // false
比较过程如下:
1.类型不同,对true执行toNumber操作,"42" == 1
2.类型仍然不同,属于字符串和数字的比较,则对字符串执行ToNumber操作: 42 == 1
3.现在类型相同了,比较两个操作数是否相同,42不等于1返回false
null,nudefined和其他类型比较

规则: null,undefined之间相等,初次之外nullundefine与其他类型都不相等

null == undefined // true
null == "" // false
undefined == 0 // false
对象和非对象比较

规则:如果一个操作数为数字或字符串,则对对象执行ToPrimitive操作转换为基本数据类型进行比较

这里只提到数字和字符串是因为: 上文中已经说过与null,undefined,布尔值之间比较的规则了,如果是这几种类型,则按照该类型对应规则进行转换

const obj = Object('abc');
const abc = 'abc'

obj == abc //true
obj === abc //false

obj==abc比较过程如下:
1.obj是对象,abc为字符串,需要对对象进行`ToPromitice`操作
2.检查调用obj的valueOf方法,返回基本数据类型'abc',`ToPRimitive`操作完成: 'abc' == 'abc'
3.现在两者类型相同了,直接比较值

抽象关系比较

比较大小时a < b ,也涉及强制类型转换,而且js只有抽象关系比较,没有严格抽象关系比较,因此,抽象关系比较的转换规则,我们一定要熟悉.毕竟js中只有这一种比较方式

比较规则如下:

  1. 双方都是字符串: 按照字母顺序进行比较
  2. 其他情况: 先进行ToPrimitive操作,如果出现非字符串,则进行ToNumber转换为数字比较
[42] < ['43'] // true 两边执行ToPrimitive转换成基本数据类型 '42' < '43',都是字符串,然后按照字母顺序比较
{a:42} < {b:43} // false两边执行ToPrimitive转换成基本数据类型 '[object Object]' < '[object Object]',明显相同,返回false

>=和<=

根据规范a<=b被处理为b< a,然后将结果取反,因此a <= b表示的不是a小于等于b而是a不大于b,来看个有趣的情况:

const obj1 = {a:1};
const obj2 = {a:2};

obj1 < obj2 //false
obj1 > obj2 //false
obj1 == obj2 // false

obj1 <= obj2 //true
obj1 >= obj2 //true

仔细观察你会发现: obj1即不大于也不小于也不等于obj2,这就很奇怪,我们来分析以下比较过程:

  • obj1 < obj2 两边执行ToPrimitive操作,转换为: '[object Object]' < '[object Object]',显然应该返回false
  • obj1 > obj2 两边执行ToPrimitive操作,转换为: '[object Object]' > '[object Object]',显然应该返回false
  • obj1 == obj2 两边都是对象,比较两个变量的值是不是指向同一个,显然这两个是单独定义的,所以返回false
  • obj1 <= obj2,这个比较实际上是obj1 > obj2的值取反,上述obj1 > obj2比较结果为false,所以这里返回true
  • obj1 >= obj2,这个比较实际上是obj1 < obj2的值取反,所以返回true

比较关系符由于没有对应的严格关系比较,因此当类型不同的数据进行比较的时候,无法避免隐式强制类型转换的发生,在比较时,最好显示强制类型转换为同一种类型后再进行比较

总结

js的类型强制转换非常复杂,在处理时要清楚: 基础类型转换规则(ToPrimitive,ToNumber,ToString),在不同情况下这些基础规则运用的情况不同,因此处理时,要知其然知其所以然.