进阶篇-数据类型转换
进阶篇-数据类型转换
数据类型转换
我们先看一段代码,了解下大致的情况。
1'123' == 123 // false or true?
2'' == null // false or true?
3'' == 0 // false or true?
4[] == 0 // false or true?
5[] == '' // false or true?
6[] == ![] // false or true?
7null == undefined // false or true?
8Number(null) // 返回什么?
9Number('') // 返回什么?
10parseInt(''); // 返回什么?
11{}+10 // 返回什么?
12let obj = {
13 [Symbol.toPrimitive]() {
14 return 200;
15 },
16 valueOf() {
17 return 300;
18 },
19 toString() {
20 return 'Hello';
21 }
22}
23console.log(obj + 200); // 这里打印出来是多少?
首先我们要知道,在
JS
中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
转Boolean
1Boolean(0) //false
2Boolean(null) //false
3Boolean(undefined) //false
4Boolean(NaN) //false
5Boolean(1) //true
6Boolean(13) //true
7Boolean('12') //true
在条件判断时,除了
undefined
,null
,false
,NaN
,''
,0
,-0
,其他所有值都转为true
,包括所有对象
对象转原始类型
对象在转换类型的时候,会调用内置的
[[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般来说如下
- 如果已经是原始类型了,那就不需要转换了
- 调用
x.valueOf()
,如果转换为基础类型,就返回转换的值 - 调用
x.toString()
,如果转换为基础类型,就返回转换的值 - 如果都没有返回原始类型,就会报错
当然你也可以重写
Symbol.toPrimitive
,该方法在转原始类型时调用优先级最高。
1let a = {
2 valueOf() {
3 return 0
4 },
5 toString() {
6 return '1'
7 },
8 [Symbol.toPrimitive]() {
9 return 2
10 }
11}
121 + a // => 3
四则运算符
它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
11 + '1' // '11'
2true + true // 2
34 + [1,2,3] // "41,2,3"
- 对于第一行代码来说,触发特点一,所以将数字
1
转换为字符串,得到结果'11'
- 对于第二行代码来说,触发特点二,所以将
true
转为数字1
- 对于第三行代码来说,触发特点二,所以将数组通过
toString
转为字符串1,2,3
,得到结果41,2,3
另外对于加法还需要注意这个表达式
'a' + + 'b'
1'a' + + 'b' // -> "aNaN"
- 因为
+ 'b'
等于NaN
,所以结果为"aNaN"
,你可能也会在一些代码中看到过+ '1'
的形式来快速获取number
类型。 - 那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
14 * '3' // 12
24 * [] // 0
34 * [1, 2] // NaN
比较运算符
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
1let a = {
2 valueOf() {
3 return 0
4 },
5 toString() {
6 return '1'
7 }
8}
9a > -1 // true
在以上代码中,因为
a
是对象,所以会通过valueOf
转换为原始类型再比较值。
强制类型转换
强制类型转换方式包括
Number()
、parseInt()
、parseFloat()
、toString()
、String()
、Boolean()
,这几种方法都比较类似
Number()
方法的强制转换规则- 如果是布尔值,
true
和false
分别被转换为1
和0
; - 如果是数字,返回自身;
- 如果是
null
,返回0
; - 如果是
undefined
,返回NaN
; - 如果是字符串,遵循以下规则:如果字符串中只包含数字(或者是
0X / 0x
开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为0
;如果不是以上格式的字符串,均返回 NaN; - 如果是
Symbol
,抛出错误; - 如果是对象,并且部署了
[Symbol.toPrimitive]
,那么调用此方法,否则调用对象的valueOf()
方法,然后依据前面的规则转换返回的值;如果转换的结果是NaN
,则调用对象的toString()
方法,再次依照前面的顺序转换返回对应的值。
1Number(true); // 1
2Number(false); // 0
3Number('0111'); //111
4Number(null); //0
5Number(''); //0
6Number('1a'); //NaN
7Number(-0X11); //-17
8Number('0X11') //17
Object 的转换规则
对象转换的规则,会先调用内置的
[ToPrimitive]
函数,其规则逻辑如下:
- 如果部署了
Symbol.toPrimitive
方法,优先调用再返回; - 调用
valueOf()
,如果转换为基础类型,则返回; - 调用
toString()
,如果转换为基础类型,则返回; - 如果都没有返回基础类型,会报错。
1var obj = {
2 value: 1,
3 valueOf() {
4 return 2;
5 },
6 toString() {
7 return '3'
8 },
9 [Symbol.toPrimitive]() {
10 return 4
11 }
12}
13console.log(obj + 1); // 输出5
14// 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为3;如果valueOf也去掉,则调用toString返回'31'(字符串拼接)
15// 再看两个特殊的case:
1610 + {}
17// "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则C
18[1,2,undefined,4,5] + 10
19// "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则
'==' 的隐式类型转换规则
- 如果类型相同,无须进行类型转换;
- 如果其中一个操作值是
null
或者undefined
,那么另一个操作符必须为null
或者undefined
,才会返回true
,否则都返回false
; - 如果其中一个是
Symbol
类型,那么返回false
; - 两个操作值如果为
string
和 number 类型,那么就会将字符串转换为number
; - 如果一个操作值是
boolean
,那么转换成number
; - 如果一个操作值为
object
且另一方为string
、number
或者symbol
,就会把object
转为原始类型再进行判断(调用object
的valueOf/toString
方法进行转换)。
1null == undefined // true 规则2
2null == 0 // false 规则2
3'' == null // false 规则2
4'' == 0 // true 规则4 字符串转隐式转换成Number之后再对比
5'123' == 123 // true 规则4 字符串转隐式转换成Number之后再对比
60 == false // true e规则 布尔型隐式转换成Number之后再对比
71 == true // true e规则 布尔型隐式转换成Number之后再对比
8var a = {
9 value: 0,
10 valueOf: function() {
11 this.value++;
12 return this.value;
13 }
14};
15// 注意这里a又可以等于1、2、3
16console.log(a == 1 && a == 2 && a ==3); //true f规则 Object隐式转换
17// 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
'+' 的隐式类型转换规则
'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。
- 如果其中有一个是字符串,另外一个是
undefined
、null
或布尔型,则调用toString()
方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接。 - 如果其中有一个是数字,另外一个是
undefined
、null
、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。 - 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接
11 + 2 // 3 常规情况
2'1' + '2' // '12' 常规情况
3// 下面看一下特殊情况
4'1' + undefined // "1undefined" 规则1,undefined转换字符串
5'1' + null // "1null" 规则1,null转换字符串
6'1' + true // "1true" 规则1,true转换字符串
7'1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
81 + undefined // NaN 规则2,undefined转换数字相加NaN
91 + null // 1 规则2,null转换为0
101 + true // 2 规则2,true转换为1,二者相加为2
111 + 1n // 错误 不能把BigInt和Number类型直接混合相加
12'1' + 3 // '13' 规则3,字符串拼接
整体来看,如果数据中有字符串,JavaScript 类型转换还是更倾向于转换成字符串,因为第三条规则中可以看到,在字符串和数字相加的过程中最后返回的还是字符串,这里需要关注一下
null 和 undefined 的区别?
- 首先
Undefined
和Null
都是基本数据类型,这两个基本数据类型分别都只有一个值,就是undefined
和null
。 undefined
代表的含义是未定义,null
代表的含义是空对象(其实不是真的对象,请看下面的注意!)。一般变量声明了但还没有定义的时候会返回undefined
,null
主要用于赋值给一些可能会返回对象的变量,作为初始化。
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
- undefined 在 js 中不是一个保留字,这意味着我们可以使用
undefined
来作为一个变量名,这样的做法是非常危险的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的undefined
值,比如说void 0
。 - 当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
# 2 This
不同情况的调用,
this
指向分别如何。顺带可以提一下es6
中箭头函数没有this
,arguments
,super
等,这些只依赖包含箭头函数最接近的函数
我们先来看几个函数调用的场景
1function foo() {
2 console.log(this.a)
3}
4var a = 1
5foo()
6
7const obj = {
8 a: 2,
9 foo: foo
10}
11obj.foo()
12
13const c = new foo()
- 对于直接调用
foo
来说,不管foo
函数被放在了什么地方,this
一定是window
- 对于
obj.foo()
来说,我们只需要记住,谁调用了函数,谁就是this
,所以在这个场景下foo
函数中的this
就是obj
对象 - 对于
new
的方式来说,this
被永远绑定在了c
上面,不会被任何方式改变this
说完了以上几种情况,其实很多代码中的
this
应该就没什么问题了,下面让我们看看箭头函数中的this
1function a() {
2 return () => {
3 return () => {
4 console.log(this)
5 }
6 }
7}
8console.log(a()()())
- 首先箭头函数其实是没有
this
的,箭头函数中的this
只取决包裹箭头函数的第一个普通函数的this
。在这个例子中,因为包裹箭头函数的第一个普通函数是a
,所以此时的this
是window
。另外对箭头函数使用bind
这类函数是无效的。 - 最后种情况也就是
bind
这些改变上下文的API
了,对于这些函数来说,this
取决于第一个参数,如果第一个参数为空,那么就是window
。 - 那么说到
bind
,不知道大家是否考虑过,如果对一个函数进行多次bind
,那么上下文会是什么呢?
1let a = {}
2let fn = function () { console.log(this) }
3fn.bind().bind(a)() // => ?
如果你认为输出结果是
a
,那么你就错了,其实我们可以把上述代码转换成另一种形式
1// fn.bind().bind(a) 等于
2let fn2 = function fn1() {
3 return function() {
4 return fn.apply()
5 }.apply(a)
6}
7fn2()
可以从上述代码中发现,不管我们给函数
bind
几次,fn
中的this
永远由第一次bind
决定,所以结果永远是window
1let a = { name: 'poetries' }
2function foo() {
3 console.log(this.name)
4}
5foo.bind(a)() // => 'poetries'
以上就是
this
的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定this
最终指向哪里。
首先,
new
的方式优先级最高,接下来是bind
这些函数,然后是obj.foo()
这种调用方式,最后是foo
这种调用方式,同时,箭头函数的this
一旦被绑定,就不会再被任何方式所改变。