浅谈JavaScript的类型转换

这些是本人在 github.pages上写的博客,欢迎大家关注和纠错,本人会定期在github pages上更新。有想要深入了解的知识点可以留言。

概述

在 JavaScript 中,将一种值类型转换为另一种值类型,叫做类型转换,出于动态型语言的特性,类型转换发生在运行时阶段。这些转换在我们平时写的代码里无处不在,尽管我们没有注意,但是这些转换已经存在于我们的代码里了。像 if、for、while、==、===、+、- 等等语句中。

而在 JavaScript 中,有两种转换风格:隐式强制类型转换和显式强制类型转换。

举个栗子


    let str = 42 + '' // '42'  隐式
    let anotherStr = String(42) // '42'  显示

复制代码

下面,将会从类型转换的运行机制对转换值的机制进行深入分析

值转换的抽象操作

下面介绍一些抽象操作,ToString, ToNumber, ToBoolean, ToPrimitive 注意:这里的抽象操作不代表方法,而是对类型进行转换执行的一系列方法。

ToString

ToString 主要负责处理非字符串类型转换为字符串类型。我们将待转换的类型进行划分:

基本类型和对象类型

string 类型是 JS 中很特殊,也是最重要的基本类型,基本每个内置对象都实现了自身的 toString 方法。

基本类型值的操作很常规,都遵循着通用的规则。


    null -> 'null'
    undefined -> 'undefined'
    true -> 'true'
    21 -> '21'

复制代码

对普通的对象而言,机制就变得复杂起来。

对象自身调用 toString() 进行字符串化或者显示字符串化(如: String() )。

基本上 obj.toString() === String(obj) 过程如下:

  • 先检查该对象是否有 toString 方法。如果该对象自身重写了从 Object 原型链继承的 toString ,那么调用该重写的方法
  • 如果该对象自身没有 toString 方法。那么就调用原型链上游的 toString 方法,直到调用到 Object.prototype.toString() 方法。如果原型链上没有此方法或者 toString 方法被重写为普通属性,则抛出 TypeError 的错误

    let arr = [1, 2, 3]
    arr.toString() === String(arr) // '1,2,3'
    let obj = { name: 'jack' }
    obj.toString() === String(obj) // [object, Object]

复制代码

特殊情况:调用 String(obj) 时,如果对象的 toString 方法被重写为普通属性,则会退而求其次执行 valueOf 方法。如果执行的结果未返回基本类型,则会报错。


    let obj = {
        valueOf() {
            return '12'
        }
        toString: undefined
    }
    String(obj) // 12
    obj.valueOf = () => ({})
    String(obj) // TypeError:Cannot convert object to primitive value

复制代码

建议:不要轻易的重写对象属性的 valueOf 和 toString 方法。因为这两个属性涉及对象的表现形式。在类型转换中至关重要。

Object.prototype.toString() 方法返回的是内部属性 [[ class ]] 的值。如([objecct, Object], [object, Function])

ToPrimitive

该抽象操作为了将对象类型转换为基本类型,步骤如下:

  • 检查该值是否有 valueOf() 方法。如果有且返回基本类型值,则使用该值
  • 如果没有返回基本类型值或者没有该方法,就使用 toString 方法的返回值(如果存在)
  • 如果 valueOf 或者 toString 都不返回基本类型值 则会报错 TypeError

    let nullObj = Object.create(null)
    Number(nullObj) || String(nullObj) // VM3789:1 Uncaught TypeError: Cannot convert object to primitive value

    let obj = {
        valueOf: function() {
            return '12'
        }
    }

    Number(obj) // 12 经过的步骤 valueOf -> '12' -> 经过字符串转数字 -> 12
    Number([]) // 0 经过步骤 valueOf -> [] -> toString -> '' -> 经过字符串转数字 -> 0

复制代码

ToNumber

ToNumber 主要负责将其他类型转换为 number 类型

处理规则:

  • true 转换为 1,false 转换为 0
  • undefined 转换为 NaN
  • null 转换为 0
  • "" 转换为 0,其他字符串转换按照特定规则解析 如果能将其解析成数字类型,就解析成该值,否则为NaN
  • 对象类型先将其转换为基本类型( ToPrimitive ),然后再按上述步骤进行转换。

    Number('') // 0
    Number(true) // 1
    Number(false) // 0
    Number(undefined) // NaN
    Number(null) // 0
    Number('1d1') // NaN
    let obj = {
        valueOf() {
            return '142'
        }
        toString() {
            return 'obj self'
        }
    }
    Number(obj) // 经历过程 obj 先经过ToPrimitive抽象操作 valueOf -> '142' ->ToNumber -> 142
    obj.valueOf = null // 这个时候 valueOf 不是一个方法,所以直接调用 toString()
    Number(obj) // ToPrimitive 无 valueOf 方法 -> toString() -> NaN

复制代码

ToBoolean

假植(falsy)和真值,在 JavaScript 中,除了 true 和 false,还有其他一些列的真值和假值,这些真值和假植有区别于 true 和 false 总的来说,JavaScript中的值分为两类,真值和假值。因为 boolean 类型只有两个值,没有第三个值。

下面的这些值是假值,假值的布尔类型转换为false

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

除了以上列出的假值,其他全为真值。所以ToBoolean的操作也很简单。就是寻找以上假值中是否存在目标值。 存在即为false,不存在即为true

显式强制类型转换

数字、字符串和布尔值的强制类型转换

规则如下:

  • 数字的强制类型转换遵循以上所述的 ToNumber 抽象操作,通常所用方法是 Number(), 一元操作符+
  • 字符串的强制类型转换遵循以上所述的 ToString 抽象操作,通常所用方法是 String(), toString()
  • 布尔值的强制类型转换遵循以上所述的 ToBoolean 抽象操作,通常所有方法是 Boolean(), !!val

    Number('12') // 12
    +'12' // 12
    Number('') // 0
    +'' // 0
    Number({}) // NaN
    Number([]) // 0
    String(12) // '12'
    String({}) // '[object Object]'
    String([]) // ''
    Boolean('') // false
    Boolean(0) // false
    Boolean(1) // true
    !!1 // true
    !1 // false
    [1,2,'',undefined,0].filter(Boolean) -> [1,2]
    [1,3,'',undefined,0].filter(v -> !!v) -> [1,2]

复制代码

详解 parseInt 方法

parseInt 方法区别于 Number 方法。不同点有

  • parseInt 只处理字符串类型,如果接受的参数不是字符串类型,会先将其转化为字符串类型(执行 ToString 抽象操作)。
  • parseInt 支持转化为特定的数据类型,第二个参数就是特定的进制数据类型。第二个参数默认值为10,即十进制解析
  • parseInt 可以会从前至后依次按照第二个参数进制解析字符串,一旦解析到为 NaN 时。则结束解析。返回之前解析的结果。

注意:parseInt 要解析的参数,如果第一个参数以0开头,会按照八进制数据进行解析。0x 会按照十六进制进行解析。会覆盖默认的十进制解析。即使显式的指定十进制解析,也会进行覆盖。


    parseInt('112ssasd') // 112
    parseInt(true) // NaN
    parseInt(null) // NaN
    parseInt(112, 2) // 3 解析过程 112 按照二进制解析 二进制只能识别 0 和 1,所以 只能解析 11,二进制的结果为3
    parseInt(012) // 10
    parseInt(0x12) // 18

复制代码

常见的考题: [ 1,2,4 ].map(parseInt) 的结果,根据上述分析,显然不是[ 1,2,4 ]了。 下面的几种结果,停下来,思考一下,自己可以分析出来,也就对该方法彻底掌握了


    let noop = function() { }
    parseInt(noop, 15)
    parseInt(noop, 16)
    parseInt(1/0)
    parseInt(1/0, 19)

复制代码

隐式强制类型转换

字符串的隐式类型转换

二元运算符 + 是最重要的一个操作符,因为该运算符即可以作为两个数字进行相加,又可以作为字符串的连接符号。 现在,我们讨论作为连接符时的注意点以及相关规则。

规则:

先将 + 两侧的数据类型转化为基本数据类型(ToPrimitive 抽象操作),如果一边有字符串,那么此时,就作为连接符使用。

注意点:

  • 是将对象运用 ToPrimitive 抽象操作, 即先 valueOf ,然后 toString (如果有必要)
  • 有区别于 String(): String() 是直接寻找 toString 方法,如果未找到,寻找原型链上的方法。如果当前 toString 未被定义为方法,则调用valueOf。如果未能转化为基本类型,则报错typeError。

    let num = 1
    num + '' // '1'
    let bool = true
    bool + '' // 'true'
    let nul = null
    nul + '' // 'nul'
    let arr = [ 1,2,3 ]
    arr + '' // '1,2,3'
    let obj = {}
    obj + '' // '[object Object]'
    arr.valueOf = () => '111'
    arr + '' // '111'
    arr + 1 // '1111'
    1 + [1] // '11'

复制代码

数字的隐式类型转换

数字的隐式转换有多种,像二元操作符 + - * / % 都可以对类型进行数字的隐式类型转换,我们先来讨论特殊的 +

  • 二元操作符 + 如果有非基本类型的值,先转化为基本类型的值(ToPrimitive抽象操作)
  • 如果没有出现字符串,则使用数字相加进行运算,非数字类型参照上述规则 ToNumber 抽象操作转换。

    null + 1 // 1
    true + 1 // 2
    false + 1 // 1
    undefined + 1 // NaN
    let obj = {
        valueOf: function() {
            return 12
        }
    }
    obj + 1 // 13

复制代码

而其他的二元操作符 如: -、*、%、/ 都会对操作符两侧进行 ToNumber 抽象操作。相当于 Number(variable)


    1 - '12' // -11 相当于 1 - Number('12')
    1 * '12' // 12
    1 - [1] // 0
    1 / '12' // 0.08333333333333333
    1 % '12' // 1

复制代码

布尔值的隐式类型转换

布尔值的隐式类型转换规则很简单 参照上述的 ToBoolean 的抽象操作就可以了,除去列出的假值,其他均为真值

那么布尔值的应用场景有哪些呢?

  • 除去我们常用的 if 语句等循环判断语句之外。有filter
  • 还有我们之后要讨论的逻辑运算符

    之前的写法是显示的转换 如:
    let arr = [1, 2, 4, '', 0, undefined, null, [], {}]
    arr.filter(Boolean) // [1, 2, 4, [], {}]
    arr.filter(v => !!v) // [1, 2, 4, [], {}]
    现在我们可以写成隐式转换
    arr.filter(v => v) // [1, 2, 4, [], {}]

复制代码

|| 和 && 的抽象逻辑

很多人都认为 || 和 && 是返回布尔值的,这是一种误解。其实这两个运算符从来都不是用来返回布尔值的,相反,这是一种运算,可以返回任意类型的值。 接下来详细介绍这两个运算符。

|| 的使用方式 如 a || b ,运算规则如下

先对 a 进行抽象类型转换 (ToBoolean抽象操作),如果 a 是真值,那么直接返回 a ,否则返回 b


    let a = [], b = ''
    a || b // [] 因为 a 是真值,所以直接返回 a,不对 b 进行计算。
    b || a // [] 因为 b 是假值,所以直接返回 a

复制代码

&& 的使用方式 如 a && b,运算规则如下

先对 a 进行抽象类型转换(ToBoolean抽象操作),如果 a 是真值,那么直接返回 b,否则返回 a


    let a = [], b = ''
    a && b // '' 因为 a 是真值,所以直接返回 b
    b && a // '' 因为 b 是假值,所以直接返回 b,不对 a 进行计算

复制代码

所以 这两种运算符的性质决定了其又叫短路运算符

抽象相等(==)和 (===)

这两个操作符,我们平时在写代码时,用到的地方特别多,但是如何抉择?为什么推荐使用全等 ===,而不推荐使用 == ? 我们都知道 == 是经过类型转换之后再比较值是否相等。那么值转换到底是什么样的顺序?

下面将通过深入分析,为什么 === 比 == 要好。首先我们需要熟悉规则是什么?

== 的规则如下:

  • 对象与对象之间进行比较时,结果直接为false(前提是引用地址的内存地址不同)。
  • null 和 undefined 比较结果为true。与其他相比直接为false。
  • 非null 和 非 undefined 情况下,先将两边都转换为基本类型( 可能会经过ToPrimitive抽象操作 ),如果两边的数据类型都相同,则直接进行比较。
  • 两边的数据类型不相同的情况下,则对两边都进行 ToNumber 抽象操作。然后进行比较。

根据ES5规范 11.9.3.2-3规定。 null == undefined


    let arr = []
    arr == false // true 转换过程 [] -> ToPrimitive -> ''(这步还熟悉吧), 两边都 ToNumber '' -> 0, false -> 0
    arr == 0 // true
    1 == true // true
    2 == true // false 因为 true -> ToNumber -> 1 相当于 2 == 1 为false
    [1] == 1 // true
    [1] == [1] // false 
    [] == {} // false
    {} == [] // 会报 SyntaxError,为什么?

复制代码

针对上述的 {} == [] ,这里 JS 引擎会将 == 前面的大括号解析为块级作用域。所以会报语法错误


    相当于 
    {
        // some code 
    }
    == []

复制代码

所以改成 ({}) == [] 就可以查看结果了。

比较少见的情况:

  • 对象重写了 valueOf 方法,会返回意想不到的值。
  • [] == ![] 结果为 true。因为 ![] 会先进行 ToBoolean 抽象操作。结果就是 [] == false 很明显为true
  • 0 == [ null ] 结果为 true。因为 [ null ].valueOf() == [ null ] [ null ].toString() == '',所以结果也很明显

建议

  • 如果两边的值有 true 或者 false,避免使用 ==
  • 如果两边的值有 []、""、0,避免使用 ==
  • 我们应该在必要和保证安全的情况下使用显式的强制类型转换来保证程序的可靠性与可读性

总结:个人来看,抽象相等 == 用的好的话可以进行很多有趣的代码组合,前提是类型之间的互相切换我们已经很熟悉了。但是每次比较都可能会造成两侧的数据进行多次数据类型转换。性能和安全性,稳定性都不如 严格抽象全等 === 来的高。

抽象关系比较

最后,我们再简单介绍一下,> 、< 、>= 、<= 这几种情况。

先介绍下规则:

  • 先对操作符的两边进行 ToPrimitive 抽象操作(如果有必要的话)
  • 如果结果出现非字符串,那么将两边进行 ToNumber 抽象操作
  • 如果结果两边都是字符串类型,则按照字母顺序进行比较

    // 两边出现非字符串
    let arr = [12]
    arr < true // false
    arr < 13 // true
    // 两边出现字符串
    '042' < '12' // true
    let anotherArr = [042]
    let temp = [12]
    anotherArr < temp // false 为什么结果为false '042' < '12' 不是为 true 吗 ? 自己思考下 会得出答案的
    let obj = {}
    let obj1 = {}
    obj < obj1 // false 因为 '[object Object]' === '[object Object]'

复制代码

最后我们来讨论下 a <= b的情况。

举个栗子


    let obj = {}
    let obj1 = {}
    obj < obj1 // false
    obj == obj1 // false
    obj > obj1 // false
    // 上述3个的结果应该是没有任何问题
    obj <= obj1 // true ???
    obj >= obj1 // true ???

复制代码

意想不到的事情发生了是吧,这不是程序执行的问题,这个结果正符合规范的要求

  • 实际在 JavaScript 中, a <= b 会被执行为 !(a > b)
  • a >= b 同理为 b <= a。被执行为 !(b > a)

我们看刚才的例子


    obj <= obj1 会被执行为 !(obj > obj1) -> !false -> true
    obj >= obj1 会被执行为 !(obj < obj1) -> !false -> true

复制代码

这样,结果就很顺其自然了吧。

总结

今天介绍的类型转换,很多知识点都是参考 KYLE SIMPSON 著有的 YOU DONT KNOW JAVASCRIPT一书。部分知识参照 ES5规范。 然后根据日常的开发,尝试做的总结。类型转换基本就介绍完了。介绍这一知识的目的不是让我们在开发中写出这些生涩的代码,而是让我们透过写的代码,理解其运行的本质,这样,能让我们写出更好的代码。我们在学习的过程中,更加应该,知其然知其所以然。这样,我们写出来的代码才会又更高的可读性和稳定性。

如有理解错误或者表达不清楚的地方,欢迎一起交流。

猜你喜欢

转载自juejin.im/post/5c203d536fb9a049ef26960f