值类型转换
将值从一种类型显式的转为另外一种类型通常称之为类型转换,如果是隐式的,则一般称为强制类型转换。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
对于undefined
、function
、symbol
等类型以及循环引用的对象都不符合JSON结构标准,其他支持JSON的语言可能无法处理他们,所以包含这些类型对象的JSON都是不安全的JSON。JSON.stringify
在遇到undefined
、function
、symbol
等对象时会忽略这些对象,如果出现在数组中,则使用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
转为NaN
,null
会转为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
、-0
、NaN
逻辑上说,除开假值以外的都是真值,但是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
出现这些情况的原因是有些浏览器实现的对象,有可能在新版本的浏览器对象中被废弃,但是总有很多很多的应用以及旧浏览器依旧在用,但是已经不想再新的版本里去支持它了,所以将这些不再支持的对象类型作为假值处理。
显式强制类型转换
字符串和数字之间的显式转换
字符串和数字之间的转换是通过String
和Number
这两个内建函数来实现的。
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
。
显式转换为布尔值
和前面的String
、Number
一样,显式转换为布尔值的是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,2
和3,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
转为1
,false
转为0
隐式强制类型转为布尔值
以下几种情况会将布尔值隐式强制类型转换。
if (...)
语句for (...;...;...)
语句中的判断表达式,也就是第二个参数while(...)
和do...while(...)
... ? ... : ...
三元表达式的条件判断- 逻辑运算符
||
和&&
左边的操作数(作为条件判断)
以上情况,非布尔值会被隐式的转换为布尔值。
||
和 &&
对于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
和一种规定null
和undefined
相等,但是这两者和其他任何值都不相等
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<=b
和a>=b
成立了呢?我们的直觉是<=
是小于或者等于,但是在JavaScript中,这其实会被解释为不大于,也就是!(a<b)
,那么(a<b)
为false,那么!false
自然就是true啦,反之亦然。