共计 6422 个字符,预计需要花费 17 分钟才能阅读完成。
也许大家会觉得 js 强制类型转换与操作符知道个大概就够了。是的,对于搬砖工来说,够了。但是作为一个有追求的前端,我深信只有掌握了别人不愿意掌握的东西,才能让自己变得更强大更有竞争力。
小纲老师
- 类型转换规则
- 一元操作符
- 加性操作符
- 乘性操作符
- 布尔操作符
- 相等操作符
- 关系操作符
- 其他操作符(条件操作符、位操作符、赋值操作符、逗号操作符等)
- 总结
类型转换规则
或许大家不喜欢隐式类型转换,觉得这东西太没人性。但是你有没有想过,这也许正是 js 语言的独特之处?我认同 kyle 大佬说的,如果你彻底掌握了隐式类型转换,那么对你来说,它就是“显式”类型转换了。
之所以先讲类型转换,是因为在操作符运算中涉及了大量的隐式类型转换。
0. ToPrimitive
抽象操作 ToPrimitive 用于将引用类型转为原始类型。实现细节比较复杂,有兴趣的童鞋可以参考这里。
// 模拟一个对象的转基本类型操作 ToPrimitive
var o = {};
o[Symbol.toPrimitive] = function(hint) {console.log(hint) //hint 字符串至为 string number default 中的一个
if (hint == "default" || hint == "number") {if (o.valueOf && typeof(o.valueof()) != 'object') {return o.valueOf()
} else {return o.toString()
}
} else {if (o.toString && typeof(o.toString()) != 'object') {return o.toString()
} else {return o.valueOf()
}
}
}
String(o) // string
Number(o) // number
1+o // default
1-o // number
o++ // number
++o // number
规则如下:
- 如果传入参数是
string
(目前只有调用 String() 函数是执行这个顺序):首先检查该值是否有 toString()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就检查该值是否有 valueOf()方法。如果有并且返回基本类型值就使用该回值来进行强制类型转换,如果没有或者返回的不是基本类型值,就抛出错误。 - 如果传入参数是
number/default
(常见强制类型转换都是这个顺序):首先检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就检查该值是否有 toString()方法。如果有并且返回基本类型值就使用该回值来进行强制类型转换,如果没有或者返回的不是基本类型值,就抛出错误。
1. ToString
抽象操作 ToString,负责处理非字符串到字符串的强制类型转换。当需要一个值的字符串形式,就会进行 ToString 类型转换。
String()函数就会执行抽象操作 ToString,遵循下列转换规则:
- 如果值是基本类型,则直接转为字符串。如果是引用类型,则执行 ToPrimitive 抽象操作;
- 如果值是 null,则返回 ”null”;
- 如果值是 undefined,则返回 ”undefined”。
String() // ''String(0) //'0'String(true) //'true'String(null) //'null'String(undefined) //'undefined'String(Symbol('asdf')) //"Symbol('asdf')"String({}) //'[Object object]'// 数组的默认 toString() 方法经过了重新定义,将所有单元字符串化以后再用"," 连接起来
String([]) // ''String([1,2,3,4,'asdf']) //'1,2,3,4,asdf'
2. ToNumber
抽象操作 ToNumber,负责处理非数字到数字的强制类型转换。
Number()执行抽象操作 ToNumber,函数的转换规则如下。
- 如果是 Boolean 值,true 和 false 将分别被转换为 1 和 0。
- 如果是数字值,只是简单的传入和返回。
- 如果是 null 值,返回 0。
- 如果是 undefined,返回 NaN。
- 如果是字符串:如果字符串是空的(不包含任何字符),则将其转换为 0;如果含非数字,则将其转换为 NaN。
- 如果是对象,则执行 ToPrimitive 抽象操作,返回基本类型再按照以上规则处理。
Number() // 0
Number('') // 0
Number(' ') // 0
Number('0') // 0
Number('asdf') // NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN 与 null 不同,需要注意
// 对象会先通过抽象操作 ToPrimitive 转为基本类型,然后再转数字
Number({}) // NaN
Number([]) // 0
Number(['']) // 0
Number([' ']) // 0
Number(['0']) // 0
Number([1,2]) // NaN
3. ToBoolean
抽象操作 ToBoolean,负责处理非布尔值到布尔值的强制类型转换。
转换为 boolean 类型是最为简单的一个。转换规则如下:
(1) 可以被强制类型转换为 false
的值
- undefined
- null
- false
- +0、-0 和 NaN
- “”
(2) 其他值会被被强制类型转换为 true
这里有一个概念需要先理解:js 的操作符和操作数组成了表达式,表达式必定会返回一个值。无论是一元操作
++a
,还是布尔操作[] || false
,都会返回一个值。另外关于 js 运算符优先级请参阅 MDN 的:运算符优先级。
一元操作符
// 假设存在变量 a
+a // 一元加操作符
-a // 一元减操作符
++a // 前置递增操作符
--a // 前置递减操作符
a++ // 后置递增操作符
a-- // 后置递减操作符
一元操作符指的是只能操作一个值的操作符,区别与加性操作符可以操作两个值(如a+b
)。
1. 一元加减操作符
一元加操作符 +
用于非数字的强制类型转换,作用等同于Number()
。如:
+'1.1' // 1.1
+'asdf' // NaN
+true // 1
+false // 0
+null // 0
+undefined // NaN
+{} // NaN
+[] // 0
+new Date() // 1556258367546
一元减操作符 -
行为与 +
类似,只不过最终运算结果是负数。如 -true
结果是-1
。
2. 递增递减操作符
不同于一元加减操作符,递增递减操作符只能作用于 number
类型。若用于其他类型会直接抛错。
// 前置递增
var a = 57;
var b = ++a;
console.log(b); // 58
// 后置递增
var a = 57;
var b = a++;
console.log(b); // 57
前置递增和后置递增的区别在于,前置递增 ++a
的返回值是增加 1
的,而后置递增 a++
的返回值是不增加的。
递减和递增规则一样,不再废话。
加性操作符
1. 加法操作符+
+
操作符常用于数学的计算和字符串的拼接,规则如下:
- 如果两个操作符都是数值,执行常规的加法计算
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来;
- 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来。
- 如果有一个操作数是对象,则执行抽象操作 ToPrimitive(先 valueOf 再 toString)取的返回值,然后再应用前面关于字符串的规则。
- 对于 undefined 和 null,则分别调用 String()函数并取得字符串 ”undefined” 和 ”null”。
1+1 // 2
NaN+1 // NaN
'asdf'+'ghjk' // 'asdfghjk'
1+1+'1' // '21'
[]+1 // 1
null+undefined // 'nullundefined'
[]+{}+1 // '[Object object]1' 实际执行:''+'[Object object]'+1
{}+[]+1 // 1 {}位于行首会被解释为代码块,此处代码块被忽略,因此实际执行:+[]+1,结果为数字 1
2. 减法操作符-
- 如果两个操作符都是数值,执行常规的加法计算
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果有一个操作数是字符串、布尔值、null 或 undefined,则先在后台调用 Number()函数将其转换为数值,然后再根据前面的规则执行减法计算。如果转换的结果是 NaN,则减法的结果就是 NaN;
- 如果有一个操作数是对象,则执行抽象操作 ToPrimitive,先调用对象的 valueOf()方法以取得表示该对象的数值。如果得到的值是 NaN,则减法的结果就是 NaN。如果对象没有 valueOf()方法或者返回的不是基本类型值,则调用其 toString()方法并将得到的字符串转换为数值。
1-1 // 0
NaN-1 // NaN
10-true-null // 9
10-true-undefined // NaN
[]-1 // 0
['11']-11 // 0
11-{} // NaN
乘性操作符
乘性操作符包括乘法*
、除法/
、除余(求模)%
。规则如下:
- 如果两个操作符都是数值,执行常规乘除求模;
- 如果有一个操作数是 NaN,则结果是 NaN;
- 如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值,然后再应用上面的规则。
数值计算较为特殊的如下:
Infinity*0 // NaN
Infinity/Infinity // NaN
0/0 // NaN
Infinity%a // NaN a 为任意数值
a%0 // NaN a 为任意数值
布尔操作符
1. 逻辑非 !
逻辑非操作符会将任意值转换为一个布尔值,转换规则和 Boolean()
函数相反。连续使用两个逻辑非操作符,等同于调用了 Boolean()
。常见有大牛写代码用!!isTrue
来代替 Boolean(isTrue)
函数。
!undefined // true
!!undefined // false
!NaN // true
!!NaN // false
!1234 // false
!!1234 // true
!'' // true
!!'' // false
2. 逻辑或 ||
短路操作:如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值。
逻辑或操作符是短路操作,如果第一个操作数的求值结果(布尔求值,下同)为 true
,则直接返回第一个操作数,不再对第二个操作数求值。如果第一个操作符求职结果为false
,则返回第二个操作数。因此,常见大神写代码isExist || getIsExist()
,就是利用的短路操作,如果isExist
求值结果为 true,就不再执行 getExist()
函数。
[] || 0 // [] 对象(包括数组、函数等)的求值结果永远为 `true`,直接返回这个对象
0 || [] // []
1 || [] // 1
NaN || 0 // 0
3. 逻辑与 &&
逻辑与操作属于短路操作,即如果第一个操作数求值结果为 false
,则直接返回第一个操作数,那么就不会再对第二个操作数求值。如果第一个操作数求值为true
,则返回第二个操作数。可以用来做条件限制obj && obj.value
。只有obj
对象存在了,才会取 obj.value
值。
0 && true // 0
null && [] // null
NaN && null // NaN
[] && {} // {}
需要注意布尔操作符存在优先级:! > && > ||
:
null || !2 || 3 && 4 // ?????? 你知道结果吗?实际上,代码相当于下面一行
null || (!2) || (3 && 4) // 4
相等操作符
相等操作符有 == != === !==
四个,其中相等和不相等实行先转换类型再比较,全等和不全等实行仅比较而不转换类型。相等操作符返回布尔值 true
或false
。
1. 相等和不相等
不同类型操作数比较规则如下:
- 先判断是否在对比 null 和 undefined,是的话就会返回 true。null 和 undefined 不相等于其他任何值。
- 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number;
- 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断;
- 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。
[] == ![] // true
/* 首先,布尔操作符! 优先级更高,所以被转变为:[] == false
* 其次,操作数存在布尔值 false,将布尔值转为数字:[] == 0
* 再次,操作数 [] 是对象,转为原始类型(先调用 valueOf(),得到的还是[],再调用 toString(),得到空字符串 ''):'' == 0
* 最后,字符串和数字比较,转为数字:0 == 0
*/
NaN == NaN // false NaN 不等于任何值
null == undefined // true
null == 0 // false
undefined == 0 // false
全等和不全等
全等和不全等在比较之前不转换类型,所以相对简单:
null === undefined // false
'1' === 1 // false
0 === false // false
[] === [] // false 引用类型比较相等性还要看是否指向同一个内存地址
NaN === NaN // false NaN 比较特殊,不等于自身
关系操作符
关系操作符小于(<)、大于(>)、小于等于(<=)和大于等于(>=)比较两个值的大小,返回一个布尔值。当有一个操作数是非数值时,就会发生类型转换:
- 如果两个操作数都是数值,则执行数值比较。
- 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值。
- 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较。
- 如果一个操作数是对象,则执行 ToPrimitive 转为基本类型(先 valueOf 再 toString)。
- 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较。
'23' <'3' // true 比较的是字符编码值
'23' < 3 // false 执行规则 3
NaN > 0 // false NaN 比较总会返回 false
null >= 0 // true 执行规则 3,注意 null 相等性比较和关系比较不一样
undefined >= 0 //false undefined 执行关系比较会转化为 NaN,总是返回 false
条件操作符
1. 条件操作符
三元表达式就是由条件操作符 ? :
组成:
a > b ? a : b; // 如果 ? 前的操作求值为 true,则返回 a,否则返回 b
2. 赋值操作符
js 中等号 =
用于赋值操作,var a = 1
就是把值 1
赋值给变量a
。可以和+ - * / %
构成复合赋值:
a += b // 等同于 a = a + b
a -= b // 等同于 a = a - b
a *= b // 等同于 a = a * b
a /= b // 等同于 a = a / b
a %= b // 等同于 a = a % b
3. 逗号操作符
逗号操作符常用于一条语句声明多个变量:var a = 1, b = 2, c;
4. 位操作符
js 中数值是以 64 位格式储存的,前 32 位是整数,后 32 位是小数。位操作符会将 64 位的值转为 32 位的,所以位操作符会强制将浮点数转为整数。下面说几个常用的位操作符:
- 位操作符
~
:~x
相当于-(x+1)
,可以用来代替indexOf
作为判断条件。~str.indexOf('asdf')
相当于str.indexOf('asdf')>-1
; - 位操作符
|
:可用于将值截除为一个 32 位整数。1.11 | 0
执行结果是1
总结
js 的类型转换虽然很让人头疼,但并不是无迹可寻。只要掌握了规则,就能够按规则判断出来类型到底会如何转换。而规则中很重要的一部分是,引用类型到基本类型的转换是通过 ToPrimitive
抽象操作完成的。掌握了 ToPrimitive
抽象操作,就掌握了类型转换的核心规则。
类型转换是很常见和很常用的,虽然规则多了点,但却值得去努力学习。