JavaScript 的一个显著特点就是灵活。灵活的反面就是猝不及防否坑多,其中一个典型的例子就是一直被诟病的数据类型隐式转换。先来看一个极端的例子,如下:
(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]]
// "sb"
这就是隐式转换的成果。为什么会有这样的输出呢?这里先不做过过多解释,先从基础入手来进行分析。
MDN这样介绍过JavaScript 的特点:JavaScript是一种弱类型,或者说是一种动态语言。这意味着你不用提前声明变量的数据类型,在程序运行过程中,变量的数据类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据。
let a = 123 // number类型
a = 'abc' // string类型
a = () => {} // 对象类型
ECMAScript 标准定义了 7 种数据类型,不同形式的值对应的类型不一样,如果我们想将值从一种类型转换为另一种类型就需要类型转换。
JS 的类型转换一共分两种:显示类型转换 和 隐式类型转换。显示转换是指在原始值和包装对象或者运算符结合后,显式的改变了类型。隐式转换是指在代码执行过程中,通过运算符运算或语句执行等操作,隐式的改变了类型。
显示类型转换是通过 JS 提供的一些函数或运算符,可以直接将类型进行转换。
String()
将值转换为字符串基本类型,Number()
将值转换为数字基本类型,Boolean()
将值转换为布尔基本类型。请注意它们前面没有new关键字,并不创建封装对象。
// 字符串
Number('') // 0
Number(' 12 ') // 12
Number('1 1') // NaN
Number('12text') // NaN
// 布尔值
Number(true) // 1
Number(false) // 0
// undefined
Number(undefined) // NaN
// null
Number(null) // 0
值 | 转数字Number() |
---|---|
undefined | NaN |
null | 0 |
true | 1 |
false | 0 |
''(空字符串) | 0 |
' 11 '(字符串头尾有空格) | 11 |
'1 1'(空格在中间/字符串中含有非数字类型字符) | NaN |
999999999999999999999(极小和极大的数字) | 1e+21 |
011(非10进制数) | 9 |
+0 | 0 |
-0 | -0 |
NaN | NaN |
// undefined
String(undefined) // 'undefined'
// null
String(null) // 'null'
// 布尔值
String(true) // 'true'
String(false) // 'false'
// 数字
String(01) // '1'
String(011) // '9'
String(123) // '123'
String(999999999999999999999) // "1e+21"
注意:Number类定义的toString()方法可以接受表示转换基数的可选参数,如果不指定此参数,转换规则将是基于十进进制。
值 | 转字符串 String() |
---|---|
undefined | "undefined" |
null | "null" |
true | "true" |
false | "false" |
''(空字符串) | "" |
' 11 '(字符串头尾有空格) | " 11 " |
'1 1'(空格在中间/字符串中含有非数字类型字符) | "1 1" |
999999999999999999999(极小和极大的数字) | "1e+21"(指数形式) |
011(非10进制数) | "9" |
+0 | "0" |
-0 | "0" |
NaN | "NaN" |
// 字符串
Boolean('') // false
Boolean('123') // true
// 数字
Boolean(0) // false
Boolean(NaN) // false
Boolean(1) // true
// undefined
Boolean(undefined) // false
// null
Boolean(null) // false
// 对象
Boolean({}) // true 非null对象
注意:undefined、null、false、+0、-0、NaN、""只有这些
toBoolean()是
false,其余都为
true
值 | 转布尔值Boolean() |
---|---|
undefined | false |
null | false |
true | true |
false | false |
''(空字符串) | false |
' 11 '(字符串头尾有空格) | true |
'1 1'(空格在中间/字符串中含有非数字类型字符) | true |
999999999999999999999(极小和极大的数字) | true |
011(非10进制数) | true |
+0 | false |
-0 | false |
NaN | false |
// 数组
String({}) // "[object Object]"
String([1, 2]) // "1,2" String([1, 2]) === [1, 2].join()
String([1, undefined, 2]) // "1,,2"
// 函数
String(function() {}) // "function() {}"
String(class A{}) // "class A{}"
// 日期时间函数
String(new Date()) // "Mon Dec 07 2020 17:00:03 GMT+0800 (中国标准时间)"
// 正则表达式
String(/\s/) // "/\s/"
String(new RegExp(/\s+/)) // "/\s+/"
值 | 转字符串 String() |
---|---|
{} | "[object Object]" |
[] (空数组) | "" |
[1234] (只有一个纯数字元素的数组) | "1234" |
[1, 2] | "1,2"(arr.join() ) |
[1, undefined, 2](数组的某一项是null/undefined) | "1,,2" |
function(){} (函数对象) |
"function(){}"(定义函数的代码) |
new Date() (日期类) |
"Mon Dec 07 2020 17:00:03 GMT+0800 (中国标准时间)" |
/\s/ new (RegExp(/\s/)) (正则对象) |
"/\s/"(正则对象字面量的字符) |
// 数组
Number([]) // 0
Number([1]) // 1
Number([1, 2]) // NaN
// 日期时间函数
Number(new Date()) // 1607334260466时间戳
// 对象
Number({}) // NaN
// 正则
Number(new (RegExp(/\s/))) // NaN
值 | 转数字 Number() |
---|---|
{} | NaN |
[] (空数组) | 0 |
[1234] (只有一个纯数字元素的数组) | 1234 |
[1, 2] | NaN |
[1, undefined, 2](数组的某一项是null/undefined) | NaN |
function(){} (函数对象) |
NaN |
new Date() (日期类) |
1607334260466 (时间戳) |
/\s/ new (RegExp(/\s/)) (正则对象) |
NaN |
+"3.14" // 3.14
上例中+"3.14"是+运算符的一元形式(即只有一个操作数)。+运算符显式地将 ”3.14“ 转换为数字,而非数字加法运算(也不是字符串拼接)。
拓展:
一元运算符 - 和 + 一样,会反转数字的符号位。由于 -- 会被当作递减运算符来处理,所以我们不能使用 -- 来撤销反转,而应该像 - -"3.14"这样,在中间加一个空格。
尽量不要把一元运算符 + (还有 - )和其他运算符放在一起使用。
一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳。
var time = new Date()
+time
Javascript 按位取反位运算符 (位非)~ ,对一个表达式执行位非(求非)运算。如 ~1 = -2 ; ~2 = -3 ; ~99 = -100;
~ 运算符查看表达式的二进制表示形式的值,并执行位非运算。
~x 大致等同于 -(x+1)
~5 // -6
/*
5 二进制 101,补满 32位
00000000000000000000000000000101
按位取反
11111111111111111111111111111010
由于32位开头第一个是1,所以这是一个负数,将二进制转换成负数,需要先反码
00000000000000000000000000000101
之后,再+1
00000000000000000000000000000110
转换成十进制为6,加上符号变成负数 -6
*/
在JavaScript中有些函数用-1来代表执行失败,用大于等于0的值来代表函数执行成功。比如,indexOf()方法在字符串中搜索指定的字符串,如果找到就返回子字符串的位置,否则返回-1。
var a = "Hello World"
if(a.indexOf("lo") != -1){
// 找到匹配
}
if(a.indexOf("ol") == -1){
// 没有找到匹配
}
~和indexOf()一起可以将结果强制类型转换为真/假值,如果indexOf()返回-1,~将其转换为假值0,其他情况一律转换为真值。
var a = "Hello World"
~a.indexOf("lo") // -4 ==>真值
if(~a.indexOf("lo")){
// 找到匹配
}
~a.indexOf("ol") // 0 ==>假值
if(!~a.indexOf("ol")){
// 没有找到匹配
}
~~x
能将值截除为一个整数,~~只适用于数字,更重要的是它对负数的处理与Math.floor()不同。
Math.floor(-49.6) // -50
~~-49.6 //-49
parseInt()
解析字符串中的数字和Number()
将字符串强制类型转换为数字的返回结果都是数字。但是解析和转换两者之间还是有明显的差别。比如:
var a = "42"
var b = "42px"
Number(a) //42
parseInt(a) //42
Number(b) //NaN
parseInt(b) //42
parseInt()
解析允许字符串中含有非数字字符,比如"42px",解析按从左到右的顺序,如果遇到非数字字符“p”就停止。而Number()
转换不允许出现非数字字符,否则会失败返回NaN。
解析字符串中的浮点数可以使用parseFloat()函数。从ES5开始parseInt()默认转换为十进制数,除非指定第二个参数作为基数。
不要忘了parseInt()针对的是字符串,向parseInt()传递数字和其他类型的参数是没有用的。非字符串会首先被强制类型转换为字符串,应该避免向parseInt()传递非字符串参数。
parseInt(1/0,19) //18
parseInt(1/0,19) 最后的结果是18,而非报错,因为parseInt(1/0,19)实际上是parseInt("Infinity",19)。基数19,它的有效数字字符范围是0-9和a-i(区分大小写),以19为基数时,第一个字符"I"值为18,而第二个字符"n"不是一个有效的数字字符,解析到此为止,和"42px"中"p"一样。
这才来到本文的重点隐式转换,也是坑了无数前端的一个地方。
隐式转换潜藏在代码中的很多地方,主要涉及: + -
、 == 和 ===
、if()
举个栗子:
1 + '1' // 11
1 + true // 2
1 + false // 1
1 + undefined // NaN
'sunshine' + true // sunshinetrue
我们发现当+运算计算 string 类型和其他数据类型相加时,其他数据类型都会转换为 string 类型;而在其它情况下都会转换为 number 类型,但是 undefiend 类型会转换为 NaN,相加结果也是 NaN。
在+运算符两侧,如果存在引用数据类型,比如对象,那又会遵循怎样的一套转换规则呢?
再举个栗子:
{} + true // [Object Object] true
由上可得出结论,当使用 + 运算符时,如果存在引用数据类型,那么它将会被转换为基本类型之后再进行运算。这就涉及对象的转换规则。
对于加法操作,+运算符在 JS 语法解析中存在二义性。
- 如果+运算符两边都是 number 类型,则其规则如下:
- 如果 +运算符两边存在 NaN,则结果为 NaN
- 如果是 Infinity + (-Infinity),则结果为NaN
- 如果 -Infinity + (-Infinity),则结果是 -Infinity
- 如果 Infinity + (-Infinity),则结果是 NaN
- 如果 +运算符两边有或至少一个是字符串,则其规则如下:
- 如果 +运算符两边都是字符串,则执行字符串拼接操作
- 如果 +运算符两边只有一个是字符串,则将另外的值转换为字符串,再执行字符串拼接操作
- 如果 +运算符两边有一个是对象,则调用 valueOf 或 toString 方法取得值,将其转换为基本类型在进行字符串拼接
对于多数情况来说,对象隐式转换成字符串或数字,其实调用了一个叫做ToPrimitive(obj,preferredType)的内部方法来干这件事情,此抽象方法将对象值转换为相应的基本类型值。
在调用这个方法转换的时候,除了date对象走转换数字流程(即preferredType值是number)优先调用 valueOf(),其他走的都是转字符流程(即preferredType值是string)优先调用toString()。用一句话解释就是:这个对象倾向转换成什么,就会优先调用哪个方法。
- 调用 obj.valueOf(),如果执行结果是原始值,返回之;
- 否则为对象,调用 obj.toString(),如果执行结果是原始值,返回之;
- 否则抛异常。
- obj.toString(),如果执行结果是原始值,返回之;
- 否则为对象,调用 obj.valueOf(),如果执行结果是原始值,返回之;
- 否则抛异常。
常用内置对象调用toString()
和valueOf()
的返回情况:
类型 | toString | valueOf |
---|---|---|
Object | "[object 类型名]" | 对象本身 |
String | 字符串值 | 字符串值 |
Number | 返回数值的字符串表示。还可返回以指定进制表示的字符串,默认10进制 | 数字值 |
Boolean | "true" / "false" | Boolean 值 |
Array | 每个元素转换为字符串,用英文逗号作为分隔符进行拼接 | 数组本身 |
Date | 日期的文本表示,格式为Wed Jun 05 2019 18:22:32 GMT+0800 (中国标准时间) | 返回时间戳,等同于调用getTime() |
Function | 函数的文本表示 | 函数本身 |
RegExp | 正则的文本表示 | 正则本身 |
另一方面,valueOf 及 toString方法是可以被重写的,示例如下:
const foo = {
toString() {
return 'sunshine'
},
valueOf() {
return 1
}
}
console.log(String(foo))
console.log(1 + foo) // 2
以上我们对foo对象的 valueOf 和toString 进行了重写。
- 第一个console 打印结果为 sunshine。这里就涉及隐式转换,在console 打印的时候,倾向于使用 foo 对象的 toString 方法,将 foo 转换为基本数据类型,以打印结果。
- 第二个console 打印结果为 2,这时候的隐式转换则倾向于使用 foo 对象的 valueOf 方法,将 foo 转换为基本数据类型,以执行相加操作。
相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换。
- if()语句中的条件判断表达式
- for(..; ..; ..)语句中的条件判断表达式
- while()和do .. while()
- ? : 中的条件判断表达式
- 逻辑运算符||和&&左边的操作数
ES5规范中说到:&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
以如果条件判断左边的操作数or表达式的结果为前提,对于||来说,如果结果为true就返回第一个操作数的值,如果为false就返回第二个操作数的值。对于&&来说,如果结果为true就返回第二个操作数的值,如果为false就返回第一个操作数的值。
下面是一个十分常见的||的用法,也许你已经用过但却并未完全理解:
function foo(a,b) {
a = a||"hello"
b = b||"world"
console.log(a + '' + b)
}
foo() // "hello world"
再来看看&&,有一种用法开发人员不常见,然而JavaScript代码压缩工具常用。就是如果第一个操作数为真值,则&&运算符选择第二个操作数作为返回值,这也叫做“守护运算符”,即前面的表达式为后面的表达式把关。
function foo() {
console.log(a)
}
var a = 42
a && foo()
ES6中引入了Symbol类型,它的类型转换有一个坑。ES6允许从Symbol到字符串的显式转换,然而隐式转换会产生错误,例如:
var s1 = Symbol("cool")
String(s1) // "Symbol(cool)"
var s2 = Symbol("not cool")
s2 + '' // TypeError
Symbol不能够被转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式都是true)。
常见的误区是“==检查值是否相等,===检查值和类型是否相等”,正确的解释是:“==允许在相等比较中进行强制类型转换,而===不允许”。事实上,==和===都会检查操作数的类型,区别在于操作数类型不同时它们的处理方式不同。
ES5规范11.9.3节的“抽象相等比较算法”定义了==运算符的行为。该算法简单而又全面,涵盖了所有可能出现的类型组合,以及它们进行强制类型转换的方式。
比较运算x==y, 其中x和 y是值,产生true或者false。这样的比较按如下方式进行:
- 若Type(x)与Type(y)相同, 则 a. 若Type(x)为Undefined, 返回true。 b. 若Type(x)为Null, 返回true。 c. 若Type(x)为Number, 则 i. 若x为NaN, 返回false。 ii. 若y为NaN, 返回false。 iii. 若x与y为相等数值, 返回true。 iv. 若x 为 +0 且 y为−0, 返回true。 v. 若x 为 −0 且 y为+0, 返回true。 vi. 返回false。 d. 若Type(x)为String, 则当x和y为完全相同的字符序列(长度相等且相同字符在相同位置)时返回true。 否则, 返回false。 e. 若Type(x)为Boolean, 当x和y为同为true或者同为false时返回true。 否则, 返回false。 f. 当x和y为引用同一对象时返回true。否则,返回false。
- 若x为null且y为undefined, 返回true。
- 若x为undefined且y为null, 返回true。
- 若Type(x) 为 Number 且 Type(y)为String, 返回comparison x == ToNumber(y)的结果。
- 若Type(x) 为 String 且 Type(y)为Number,返回比较ToNumber(x) == y的结果。
- 若Type(x)为Boolean, 返回比较ToNumber(x) == y的结果。
- 若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果。
- 若Type(x)为String或Number,且Type(y)为Object,返回比较x == ToPrimitive(y)的结果。
- 若Type(x)为Object且Type(y)为String或Number, 返回比较ToPrimitive(x) == y的结果。 10. 返回false。
主要分为 x 和 y 类型相同和类型不同的情况,类型相同时没有类型转换,类型不同时
- x, y 为 null、undefined 两者中一个,返回true
- x、y为 Number 和 String 类型时,则转换为 Number 类型比较。
- 有 Boolean 类型时,Boolean 转化为 Number 类型比较。
- 一个 Object 类型,一个 String 或 Number 类型,将 Object 类型进行原始转换(
ToPrimitive
)后,按上面流程进行原始值比较。
var a = 42
var b = '42'
a == b // true
根据第4条规则返回x == ToNumber(y)的结果:a==b是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。具体怎么转换?这就需要匹配前文的“抽象相等比较算法”,寻找适应的转换规则。
==最容易出错的一个地方是true和false与其他类型之间的相等比较。
var a = '42'
var b = true
a == b // false
结果是false,这让人很容易掉坑里。如果严格按照“抽象相等比较算法”,这个结果也就是意料之中的。
根据第7条规则,若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果,即返回'42' == 1,结果为false。
很奇怪吧?所以切记:无论什么情况下都不要使用== true和== false。
"0" == false // true
false == 0 // true
false == "" // true
false == [] // true
"" == 0 // true
"" == [] // true
0 == [] // true
其中有4种情况涉及== false,之前我们说过应该避免,所以还剩下后面3种。
这些特殊情况会导致各种问题,使用中要多加小心。我们要对==两边的值认真推敲,以下两个原则可以让我们有效地避免出错。
- 如果两边的值中有true或者false,千万不要使用==
- 如果两边的值中有[]、""、或者0,尽量不要使用==
隐式强制转换在部分情况下确实很危险,为了安全起见就要使用===
在==中 null 和 undefined 相等,这也就是说在 == 中 null 和 undefined 是一回事,可以相互进行隐式强制类型转换。
null == undefined
掌握“抽象相等比较算法”,读者可以自行推倒为什么[]==![]返回true。
var a = {b:42}
var b = {b:43}
a < b // false
a == b // false
a > b // false
a <= b // true
a >= b // true
如果a < b和a == b结果为false,为什么a <= b和a >= b的结果会是true呢?
因为根据规范a <= b被处理为b < a,然后将结果反转。因为b < a的结果为false,所以a <= b的结果为true。
这可能与我们设想的大相径庭,即<=应该是“小于或者等于”,实际上,JavaScript中<=是“不大于”的意思,即a <= b被处理为 !(b < a)。
另外,规范设定NaN既不大于也不小于任何其他值。
最后,相信聪明的你仔细阅读完本文之后一定掌握了 JS 类型转换相关的知识,试着解答下面的面试题来检验自己的掌握程度吧!
答案:
function onlyOne() {
var sum = 0
for (var i=0;i<arguments.length;i++){
if(arguments[i]){
sum += arguments[i]
}
}
return sum == 1
}
var a = true
var b = false
onlyOne(b,a,b,b,b,b) // true
无论使用隐式转换还是显式转换,我们都可以通过修改onlyTwo()或者onlyFive()来处理更加复杂的情况,只需要将最后的条件判断从改为2或5。这比加入一大堆&&和||表达式要简洁得多。
+true
+[]
+new Date()
[] == false
!![]
0 == '\n'
!+[]+[]+![]
答案:
+true // 1
+[] //0
+new Date() // 当前时间的时间戳
[] == false // true == 两边都转为数字进行比较,而不是[]转为布尔值与 false 比较,即 0 == 0
!![] // true [] => true => !true => !!true
0 == '\n' // true ""、"\n"(或者" "等其他空格组合)等空字符串被ToNumber强制类型转换为0。
!+[]+[]+![] // "truefalse"
//首先第一个 + 左边不是数值,所以它是一元运算符,将后边跟着的 [] 转化为数字 0
//同时,最后一个 [] 左边是 ! 运算符,将 [] 转化为布尔值并取反,为 false
//转化后的结果为 !0 + [] + false
//!0 结果为 true,[] 转化为 "",所以结果变为 true + "" + false
//因为 第一个 + 右边有字符串,所以变为"true" + false
//最终结果为 "truefalse"
答案:根据全文自行分析,哈哈哈~
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。
想阅读更多优质文章、可我的微信公众号【阳姐讲前端】,每天推送高质量文章,我们一起交流成长。
如果你觉得这篇内容对你有启发,我想请你帮个小忙:
-
点击「在看」,让更多的人也能看到这篇内容
-
关注公众号「阳姐讲前端」,持续为你推荐精选好文
-
欢迎扫描下方二维码加我微信,拉你进群,长期交流学习....