[你不知道的JavaScript(中)] 强制类型转换

169 阅读13分钟

值类型转换

将值从一种类型显式的转为另外一种类型通常称之为类型转换,如果是隐式的,则一般称为强制类型转换。JavaScript中的强制类型转换总是返回基本类型,不会返回函数、对象等复杂类型。显式类型转换发生在编译阶段,而隐式类型转换是动态的,所以发生在运行时。

const num = 666

const str1 = String(num) // 显式
const str2 = '' + num // 隐式

抽象值操作

介绍显式和隐式类型转换之前,我们需要掌握字符串、数字和布尔值之间的类型转换。

ToString

const str = '666'
const num = 666
const bool = true
const emptyObj = {}
const obj = {a: 1}
const bigint = 100000000000000000000000000000000000000000n
const symbol = Symbol(666)
const nullValue = null
const undefinedValue = undefined
const func = () => 666
const func1 = 
const obj = {toString: () => 777}
const arr = [1,2,3]
const emptyArr = []

console.log(str.toString()) // 字符串的666
console.log(num.toString()) // 字符串的666
console.log(bool.toString()) // 字符串的true
console.log(emptyObj.toString()) // [object Object]
console.log(obj.toString()) // 777
console.log(bigint.toString()) // 100000000000000000000000000000000000000000
console.log(symbol.toString()) // Symbol(666)
console.log(nullValue+'') // 字符串的 null
console.log(undefinedValue+'') // 字符串的undefined
console.log(func.toString()) // () => 666
console.log(func1.toString()) // function() {return 666}
console.log(arr.toString()) // 1,2,3
console.log(emptyArr.toString()) // 空字符串

从上面我们可以看到基础类型和都比较符合我们的直觉。对于对象来说,如果没有定义toString方法,就会调用本身的toString方法,返回[object Object],如果定义了,则返回自定义的返回值。而函数的toString方法则是返回了函数定义的字符串。数组的toString方法则从新定义了,他会通过,来连接每一个项,有点类似于arr.join(',')

JSON的字符串化

工具函数JSON.stringify在将JSON转为字符串时也使用了ToString,不过这个严格意义和显式隐式类型转换都没关系,只不过也牵涉了ToString的规则,所以顺带讲一下。

对于绝大部分简单值来说,JSON的字符串化和toString的效果是一致的,只不过序列化的结果总是字符串

const str = '666'
const num = 666
const bool = true
console.log(JSON.stringify(str)) // 带引号的 "666"
console.log(JSON.stringify(num)) // 字符串 666
console.log(JSON.stringify(bool)) // 字符串true

对于undefinedfunctionsymbol等类型以及循环引用的对象都不符合JSON结构标准,其他支持JSON的语言可能无法处理他们,所以包含这些类型对象的JSON都是不安全的JSON。JSON.stringify在遇到undefinedfunctionsymbol等对象时会忽略这些对象,如果出现在数组中,则使用null来替代以保持单元位置不变。

console.log(JSON.stringify(undefined)) // undefined
console.log(JSON.stringify(() => 666)) // undefined
console.log(JSON.stringify([1, undefined, () => 666, 777])) // [1,null,null,777]

包含循环引用的对象执行JSON.stringify会出错。

如果对象中定义了toJSON方法,则JSON字符串化的时候会首先调用这个函数,然后将这个函数的返回值进行序列化。

const obj = {
    a: 111,
    toJSON: () => ({b: 777})
}

console.log(JSON.stringify(obj)) // {"b":777}

所以我们对一个JSON对象进行字符串化之前需要做特殊处理,或者过滤掉无法字符串化的对象时,可以使用这个toJSON函数进行预处理。

JSON.stringify还有两个可选参数。

  • replacer 用来指定哪些对象会被处理,哪些会被忽略。如果是个数组,则必须是个字符串数组,包含需要序列化的属性名,其他的则会被忽略。如果是个函数,则他会对对象本身调用一次,然后对对象的每一个属性都会调用一次,如果忽略某个值则返回undefined,否则返回对应的值。

    const obj = {
        a: 666,
        b: 777,
        d: 888
    }
    console.log(JSON.stringify(obj, ['a', 'b'])) // {"a":666,"b":777}
    console.log(JSON.stringify(obj, function(k,v) {
        console.log(k, v)
        // { a: 666, b: 777, d: 888 }
        // a 666
        // b 777
        // d 888
        // 输出四次,第一次是对象本身
        if (k !== 'a') {
            return v
        }
    })) // {"b":777,"d":888}
    
  • space 用来指定输出的缩进格式。如果是正整数,则指定每一级缩进的字符数,如果是字符串,则最前面的十个字符用于每一级的缩进。

    const obj = {
        a: 666,
        b: 777,
        d: 888
    }
    console.log(JSON.stringify(obj, null, 4))
    // {
    //     "a": 666,
    //     "b": 777,
    //     "d": 888
    // }
    
    console.log(JSON.stringify(obj, null, '-------------------------------'))
    // {
    // ----------"a": 666,
    // ----------"b": 777,
    // ----------"d": 888
    // }
    
    

ToNumber

有时候我们需要将非数值当做数字来用,比如数学运算。

对于基础类型,true会转为1,false会转为0,undefined转为NaNnull会转为0,Symbol则会报错。

对于复杂对象,抽象操作 ToPremitive(ES5规范9.1节) 规定了首先会检查该值是否有valueOf的方法,如果有,就对这方法返回 基本类型 ,就用改基本类型进行数值转回,如果没有则检查是否有toString方法,使用toString方法的返回值进行类型转换,如果都没有,则报错。

```js
const obj = {
    a: 1,
    valueOf() {
        return '666';
    }
}

console.log(Number(obj)) // 666
```

ToBoolean

JavaScript规范中规定了以下的值是假值,可以被强制转换为false

  • false
  • ''
  • null
  • undefined
  • +0-0NaN

逻辑上说,除开假值以外的都是真值,但是JavaScript并没有给出对应规范,所以我们可以这么理解就行。

假值对象

刚才我们说了,除了假值以外的值都是真值,那么为啥还会有假值对象这种说法呢?你以为是Boolean(false)String('')Number(0)吗?

非也!!!

const a = new Boolean(false)
const b = new String('')
const c = new Number(0)

console.log(Boolean(a), Boolean(b), Boolean(c)) // true true true

JavaScript中会出现假值对象,不是JavaScript语言的范畴,而是浏览器在某些特定的情况下,在JavaScript的语法基础上加的一些东西,这些看起来跟对象没啥区别(都有属性等等),但是强制类型转为布尔值的时候会返回 false

console.log(Boolean(document.all)) // false

出现这些情况的原因是有些浏览器实现的对象,有可能在新版本的浏览器对象中被废弃,但是总有很多很多的应用以及旧浏览器依旧在用,但是已经不想再新的版本里去支持它了,所以将这些不再支持的对象类型作为假值处理。

显式强制类型转换

字符串和数字之间的显式转换

字符串和数字之间的转换是通过StringNumber这两个内建函数来实现的。

const a = '777'
const b = Number(a) // 777

const c = 777
const d = String(c) // "777"

除此之外,也可以通过以下方式转换

const a = 777
const b = a.toString() // "777"

const c = "777"
const d = +c

a.toString是显式的,不过其中隐含了隐式转换,因为对于a这种基本类型没有toString这种方法,所以将a自动被JavaScript引擎封装为封装对象,所以包含了隐式转换。而+c是运算符的一元形式,他可以显式的将c转为数字,对于+c是显式还是隐式,取决于自己的理解,不过JavaScript开源社区一般把这个认为是显式强制类型转换。

将日期显式的转为数字

console.log(+new Date()) // 1637584991035

但是一般来说我们更推荐用 Date.now()或者new Date().getTime()来获取时间戳。

显式解析数字字符串

解析字符串和将字符串强制类型转换为数字的返回结果都是数字,但是这两者之间还是有一些区别的。

const a = "42"
const b = "42px"

Number(a) // 42
parseInt(a) // 42

Number(b) // NaN
parseInt(b) // 42

解析允许字符串中出现非数字字符,从左往右,遇到非数字字符停下。而强制类型转换不允许出现非数字字符,否则会返回NaN

显式转换为布尔值

和前面的StringNumber 一样,显式转换为布尔值的是Boolean函数

const a = ""
const b = 0
const c = null
const d = undefined
const e = false

Boolean(a) // false
Boolean(b) // false
Boolean(c) // false
Boolean(d) // false
Boolean(e) // false

前面已经说过,除了这五个假值以外的都是真值,这里不再赘述。

和前面的+类似,!也被认为是显式转换为布尔值,并且取反。所以显式的转为布尔值的最常用的用法是!!

const a = ''
const b = 'ccc'

!!a // false
!!b // true

隐式强制类型转换

隐式强制类型转换是指那些隐蔽的强制类型转换,副作用也不明显。

字符串和数字之间的隐式强制类型转换

+既能用于加法运算,也能用于字符串拼接,那么JavaScript怎么判断是哪个操作呢?

const a = "42"
const b = "0"

const c = 42
const d = 0

console.log(a+b) // "420"
console.log(c+d) // 42

这里为啥会得到两个不同的结果呢?通常我们理解是某一个和某两个操作数都是字符串,所以+操作是字符串拼接。这里只答对了一半。

const a = [1,2]
const b = [3,4]

console.log(a+b) // "1,23,4"

根据ES5规范11.6.1节,如果某个操作数是字符串或者能通过以下步骤转换为字符串的话,+进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,改操作再调用[[DefaultValue]]

所以上面数组调用valueOf无法得到简单基本类型值,所以它转而调用了toString,因此上面两个变成了1,23,4,,所以拼接起来就成了1,23,4

如果是对象,则需要注意一下

const a = {
    valueOf: () => 42,
    toString: () => 4
}

console.log(a+'') // "42"
console.log(String(a)) // 4

所以我们可以将数字和空字符串""相加将其转为字符串

const c = '' + 42 // "42"

字符串转为数字

const a = "666" - 0

当然,用 a/1或者a*1都行。

布尔值到数字

console.log(1+true) // 2
console.log(1-false) // 1

通过运算可以把布尔值转为数值进行运算,true转为1false转为0

隐式强制类型转为布尔值

以下几种情况会将布尔值隐式强制类型转换。

  1. if (...) 语句
  2. for (...;...;...)语句中的判断表达式,也就是第二个参数
  3. while(...)do...while(...)
  4. ... ? ... : ... 三元表达式的条件判断
  5. 逻辑运算符||&& 左边的操作数(作为条件判断)

以上情况,非布尔值会被隐式的转换为布尔值。

||&&

对于JavaScript这门语言,||&&称之为逻辑运算符不如称之为选择器运算符更恰当。因为这两个运算符不返回布尔值,而是返回这两个操作数的其中一个。

const a = 'a' || 'c' // 'a'
const b = false || 'are you ok?' // are you ok?

const c = true && 666 // 666
const d = '' && 777 // ''

比如我们举个熟悉的例子就是在ES6之前不支持参数默认值,我们又需要一个默认值的时候,可以这么干

function add (a,b) {
    a = a || 1
    b = b || 1
    return a + b
}

&&也称之为‘把关’运算符,因为在支持?.之前,我们需要保证某个链式引用不报错,需要这样

const a = fetchData() //远程拿到的数据

a.b && a.b.c // 确保a.c存在时再进行链式调用,当然这里也有瑕疵,如果a.b不是对象...不过举个栗子就不纠结那么多了

需要注意一点

Symbol可以通过显示转换为字符串,但是隐式转换会报错,也不能转为数字(包括显示和隐式),但是又能转为布尔值(显示和隐式都是true)

const sym = Symbol('aaa')
console.log(String(sym)) // Symbol(aaa)
// console.log(''+sym) // TypeError
// console.log(Number(sym)) // TypeError
// console.log(+sym) // TypeError
console.log(Boolean(sym)) // true
console.log(!!sym) // true

宽松相等和严格相等

==表示宽松相等,===表示严格相等。一般都会说,宽松相等检查值是否相等,严格相等检查值和类型是否相等。但是准确来说这种说法不够严谨,正确的解释是 ==允许在相等比较中进行强类型转换,而===不允许

console.log("42" == 42) //true
console.log(true == 1) //true
console.log(false == 0) //true
console.log({valueOf () {return 100}} == 100) //true

console.log("42" === 42) //false
console.log(true === 1) //false
console.log(false === 0) //false
console.log({valueOf () {return 100}} === 100) //false

我们需要注意两种特殊情况

  • NaN不等于NaN
  • +0 等于 -0

和一种规定nullundefined相等,但是这两者和其他任何值都不相等

console.log(null==undefined) // true

对象和非对象之间的转换

首先,布尔值会被强制转为数字,然后再进行数字或者字符串与对象的对比,ES5规范11.9.3.8-9做了以下规定

  • type(x)为数字或字符串,type(y)为对象,则返回x == ToPrimitive(y)
  • type(x)为对象,type(y)为数字或字符串,则返回ToPrimitive(x) == y
const a = 42
const b = [42]

console.log(a == b) // true

这是因为[42]调用ToPrimitive抽象操作会返回"42",而"42"==42,所以返回true。

对于封装对象的拆箱也一样。

const a = 'abc'
const b = new String(a)

console.log(a==b) //true

一些特殊情况

console.log("0"==false) //true
console.log(false==0) //true
console.log(false=="") //true
console.log(false==[]) //true
console.log(""==0) //true
console.log(""==[]) //true
console.log(0==[]) //true

上面的几种情况都属于假阳(莫得假阴),他们明显属于不同的值和类型,但是因为强制类型转换,他们却相等了....

极端情况

console.log([] == ![]) // true

因为是酱紫的,![]的值为false,所以上面的等式变成了[] == false,通过上面特殊情况我们知道false == [],所以他们又变得相等了.....

类似的还有2==[2]""==[null], [2]会转为2,[null]会转为""

隐式类型转换有蛮多的坑,所以我们理应优先使用严格相等===,尽可能的避免使用宽松相等。但是我们也不能说完全不去了解宽松相等,这对于我们深入学习JavaScript还是非常有用的,只有知道了原理,才能知道我们日常为什么这么做,有啥好处。

抽象关系比较

a < b涉及隐式强制类型转换比较不引人注意,但是我们有必要深入学习一下。

比较双方首先调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则将双方强制类型转换为数字来进行比较。

例如:

const a = [42]
const b = ["43"]

console.log(a<b) // true

如果双方都没有被转为数字

const a = ["42"]
const b = ["043"]

console.log(a<b) // false

这是因为双方转为字符串"42""043",因为是字符串比较,所以是按顺序比较, "4"在字母顺序上大于"0",所以结果为false

下面还有个比较有意思的例子

const a = {a: 42}
const b = {a: 42}

console.log(a<b) //false
console.log(a==b) //false
console.log(a>b) //false

console.log(a<=b) //true
console.log(a>=b) //true

按照我们刚才的思路,a和b的字符串值都是[object Object],那么按理说a==b是成立的呀。我们需要记住,只有比较双方都不是同一种类型才会发生隐式强制类型转换,如果是相同类型,那么会直接比较值。 所以这里a和b都是对象,他们就直接比较值了,a和b明显不相等,任何单独创建而不是引用的值都不会相等。那么为啥a<=ba>=b成立了呢?我们的直觉是<=是小于或者等于,但是在JavaScript中,这其实会被解释为不大于,也就是!(a<b),那么(a<b)为false,那么!false自然就是true啦,反之亦然。

总结结束,感谢观看么么哒。希望通过学习能摆脱内卷吧。爷讨厌加班啊啊啊啊啊啊啊啊啊啊啊啊啊