类型转换(下)
类型转换(下)
前言
举个例子:
在 JavaScript 中,这是完全可以运行的,不过你有没有好奇,为什么 1 和 '1' 分属不同的数据类型,为什么就可以进行运算呢?
这其实是因为 JavaScript 自动的将数据类型进行了转换,我们通常称为隐式类型转换。但是我们都知道,+
运算符既可以用于数字加法,也能用于字符串拼接,那在这个例子中,是将数字 1
转成字符串 '1'
,进行拼接运算?还是将字符串 '1'
转成数字 1
,进行加法运算呢?
先卖个关子,虽然估计你也知道答案。今天,我们就常见的隐式类型转化的场景进行介绍。
动态语言和静态语言
通常我们所说的动态语言、静态语言是指动态类型语言和静态类型语言。
动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,永远也不用给任何变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python和JavaScript就是一种典型的动态类型语言,其他的各种脚本语言如Ruby也多少属于动态类型语言。
静态类型语言:静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、JAVA等。
强类型定义语言和弱类型定义语言
强类型定义语言:强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。举个例子:如果你定义了一个整型变量a,那么程序根本不可能将a当作字符串类型处理。强类型定义语言是类型安全的语言。
弱类型定义语言:数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。
强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。另外,Typescript就是为了解决Javascript弱类型而衍生出的超集。
一元操作符 +
当 + 运算符作为一元操作符的时候,查看 ES5 规范 1.4.6,会调用 ToNumber
处理该值,相当于 Number('1')
,最终结果返回数字 1
。
那么下面的这些结果呢?
console.log(+[]);
console.log(+['1']);
console.log(+['1', '2', '3']);
console.log(+{});
既然是调用 ToNumber
方法,回想上一篇章中的内容,当输入的值是对象的时候,先调用 ToPrimitive(input, Number)
方法,执行的步骤是:
- 如果
obj
为基本类型,直接返回 - 否则,调用
valueOf
方法,如果返回一个原始值,则JavaScript
将其返回。 - 否则,调用
toString
方法,如果返回一个原始值,则JavaScript
将其返回。 - 否则,
JavaScript
抛出一个类型错误异常。
以 +[]
为例,[]
调用 valueOf
方法,返回一个空数组,因为不是原始值,调用 toString
方法,返回 ""
。
得到返回值后,然后再调用 ToNumber
方法,""
对应的返回值是 0
,所以最终返回 0
。
剩下的例子以此类推。结果是:
console.log(+['1']); // 1
console.log(+['1', '2', '3']); // NaN
console.log(+{}); // NaN
二元操作符 +
规范
现在 +
运算符又变成了二元操作符,毕竟它也是加减乘除中的加号
1 + '1'
我们知道答案是 '11',那 null + 1
、[] + []
、[] + {}
、{} + {}
呢?
如果要了解这些运算的结果,不可避免的我们要从规范下手。
规范地址:http://es5.github.io/#x11.6.1
不过这次就不直接大段大段的引用规范了,直接给大家讲简化后的内容。
到底当执行 +
运算的时候,会执行怎样的步骤呢?让我们根据规范11.6.1
来捋一捋:
当计算 value1 + value2 时:
- lprim = ToPrimitive(value1)
- rprim = ToPrimitive(value2)
- 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim) 的拼接结果,否则返回 ToNumber(lprim) 和 ToNumber(rprim) 的运算结果
规范的内容就这样结束了。没有什么新的内容,ToString
、ToNumber
、ToPrimitive
都是在上一篇章中讲到过的内容,所以我们直接进分析阶段:
举例
1.Null 与数字
按照规范的步骤进行分析:
- lprim = ToPrimitive(null) 因为 null 是基本类型,直接返回,所以 lprim = null
- rprim = ToPrimitive(1) 因为 1 是基本类型,直接返回,所以 rprim = null
- lprim 和 rprim 都不是字符串
- 返回 ToNumber(null) 和 ToNumber(1) 的运算结果
接下来:
ToNumber(null)
的结果为 0,(回想上篇 Number(null)),ToNumber(1)
的结果为 1
所以,null + 1
相当于 0 + 1
,最终的结果为数字 1
。
这个还算简单,看些稍微复杂的:
2. 数组与数组
依然按照规范:
- lprim = ToPrimitive([]),[] 是数组,相当于 ToPrimitive([], Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回空字符串 ""
- rprim 类似。
- lprim 和 rprim 都是字符串,执行拼接操作
所以,[] + []
相当于 "" + ""
,最终的结果是空字符串""
。
看个更复杂的:
3. 数组与对象
// 两者结果一致
console.log([] + {});
console.log({} + []);
按照规范:
- lprim = ToPrimitive([]),lprim = ""
- rprim = ToPrimitive({}),相当于调用 ToPrimitive({}, Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回 "[object Object]"
- lprim 和 rprim 都是字符串,执行拼接操作
所以,[] + {}
相当于 "" + "[object Object]"
,最终的结果是 "[object Object]"。
下面的例子,可以按照示例类推出结果:
console.log(1 + true);
console.log({} + {});
console.log(new Date(2017, 04, 21) + 1) // 这个知道是数字还是字符串类型就行
结果是:
console.log(1 + true); // 2
console.log({} + {}); // "[object Object][object Object]"
console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"
注意
以上的运算都是在 console.log
中进行,如果你直接在 Chrome
或者 Firebug
开发工具中的命令行直接输入,你也许会惊讶的看到一些结果的不同,比如:

我们刚才才说过 {} + []
的结果是 "[object Object]"
呐,这怎么变成了 0
了?
不急,我们尝试着加一个括号:

结果又变成了正确的值,这是为什么呢?
其实,在不加括号的时候,{}
被当成了一个独立的空代码块,所以 {} + []
变成了 +[]
,结果就变成了 0
同样的问题还出现在 {} + {}
上,而且火狐和谷歌的结果还不一样:
> {} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"
如果 {}
被当成一个独立的代码块,那么这句话相当于 +{}
,相当于 Number({})
,结果自然是 NaN
,可是 Chrome
却在这里返回了正确的值。
那为什么这里就返回了正确的值呢?我也不知道,欢迎解答~
== 相等
规范
"=="
用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。
关于使用 "==" 进行比较的时候,具体步骤可以查看规范 11.9.5:
当执行 x == y 时:
如果 x 与 y 是同一类型:
- x 是 Undefined,返回 true
- x 是 Null,返回 true
- x 是数字:
- x 是 NaN,返回 false
- y 是 NaN,返回 false
- x 与 y 相等,返回 true
- x 是 + 0,y 是 - 0,返回 true
- x 是 - 0,y 是 + 0,返回 true
- 返回 false
- x 是字符串,完全相等返回 true, 否则返回 false
- x 是布尔值,x 和 y 都是 true 或者 false,返回 true,否则返回 false
- x 和 y 指向同一个对象,返回 true,否则返回 false
x 是 null 并且 y 是 undefined,返回 true
x 是 undefined 并且 y 是 null,返回 true
x 是数字,y 是字符串,判断 x == ToNumber(y)
x 是字符串,y 是数字,判断 ToNumber(x) == y
x 是布尔值,判断 ToNumber(x) == y
y 是布尔值,判断 x ==ToNumber(y)
x 是字符串或者数字,y 是对象,判断 x == ToPrimitive(y)
x 是对象,y 是字符串或者数字,判断 ToPrimitive(x) == y
返回 false
觉得看规范判断太复杂?我们来分几种情况来看:
1. null 和 undefined
console.log(null == undefined);
看规范第 2、3 步:
- x 是 null 并且 y 是 undefined,返回 true
- x 是 undefined 并且 y 是 null,返回 true
所以例子的结果自然为 true
。
在编写判断对象的类型 type
函数时,如果输入值是 undefined
,就返回字符串 undefined
,如果是 null
,就返回字符串 null
。
如果是你,你会怎么写呢?
下面是 jQuery 的写法:
function type(obj) {
if (obj == null) {
return obj + '';
}
...
}
2. 字符串与数字
console.log('1' == 1);
结果肯定是 true,问题在于是字符串转化成了数字和数字比较还是数字转换成了字符串和字符串比较呢?
看规范第 4、5 步:
4.x 是数字,y 是字符串,判断 x == ToNumber(y)
5.x 是字符串,y 是数字,判断 ToNumber(x) == y
结果很明显,都是转换成数字后再进行比较
3. 布尔值和其他类型
console.log(true == '2')
当要判断的一方出现 false
的时候,往往最容易出错,比如上面这个例子,凭直觉应该是 true
,毕竟 Boolean('2')
的结果可是 true,但这道题的结果却是 false。
归根到底,还是要看规范,规范第 6、7 步:
6.x 是布尔值,判断 ToNumber(x) == y
7.y 是布尔值,判断 x ==ToNumber(y)
当一方出现布尔值的时候,就会对这一方的值进行 ToNumber 处理,也就是说 true 会被转化成 1,
true == '2'
就相当于 1 == '2'
就相当于 1 == 2
,结果自然是 false
。
所以当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == true
和 xx == false
的写法。
比如:
// 不建议
if (a == true) {}
// 建议
if (a) {}
// 更好
if (!!a) {}
4. 对象与非对象
console.log( 42 == ['42'])
看规范第 8、9 步:
- x 是字符串或者数字,y 是对象,判断 x == ToPrimitive(y)
- x 是对象,y 是字符串或者数字,判断 ToPrimitive(x) == y
以这个例子为例,会使用 ToPrimitive
处理 ['42']
,调用valueOf
,返回对象本身,再调用 toString
,返回 '42'
,所以
42 == ['42']
相当于 42 == '42'
相当于42 == 42
,结果为 true
。
到此为止,我们已经看完了第 2、3、4、5、6、7、8、9 步,其他的一概返回 false。
其他
再多举几个例子进行分析:
console.log(false == undefined)
false == undefined
相当于 0 == undefined
不符合上面的情形,执行最后一步 返回 false
false == []
相当于 0 == []
相当于 0 == ''
相当于 0 == 0
,结果返回 true
首先会执行 ![]
操作,转换成 false,相当于 [] == false
相当于 [] == 0
相当于 '' == 0
相当于 0 == 0
,结果返回 true
最后再举一些会让人踩坑的例子:
console.log(false == "0")
console.log(false == 0)
console.log(false == "")
console.log("" == 0)
console.log("" == [])
console.log([] == 0)
console.log("" == [null])
console.log(0 == "\n")
console.log([] == 0)
以上均返回 true
逻辑操作符(!、&&、||)
逻辑非(!)操作符首先通过Boolean()函数将它的操作值转换为布尔值,然后求反。
!!value //常常用这种方式转换为对应boolean值
逻辑与(&&)操作符,如果一个操作值不是布尔值时,遵循以下规则进行转换:
- 如果第一个操作数经Boolean()转换后为true,则返回第二个操作值,否则返回第一个值(不是Boolean()转换后的值)
- 如果有一个操作值为null,返回null
- 如果有一个操作值为NaN,返回NaN
- 如果有一个操作值为undefined,返回undefined
1&&2 //2
0&&2 //0
null&&2 //null
2&&undefined //undefined
NaN&&undefined //NaN 返回第一个(可以理解为第一个boolean为false,所以返回第一个)
逻辑或(||)操作符,如果一个操作值不是布尔值,遵循以下规则:
- 如果第一个操作值经Boolean()转换后为false,则返回第二个操作值,否则返回第一个操作值(不是Boolean()转换后的值)
- 对于undefined、null和NaN的处理规则与逻辑与(&&)相反
1||2 //1
0||2 //2
null||2 //2
2||undefined //2
NaN||undefined //undefined 返回第二个(可以理解为第一个boolean为false,所以返回第二个)
关系操作符(<, >
,<=
, >=
)
与上述操作符一样,关系操作符的操作值也可以是任意类型的,所以使用非数值类型参与比较时也需要系统进行隐式类型转换:
- 如果两个操作值都是数值,则进行数值比较
- 如果两个操作值都是字符串,则比较字符串对应的字符编码值
- 如果只有一个操作值是数值,则将另一个操作值转换为数值,进行数值比较
- 如果一个操作数是对象,则调用ToPrimitive,得到的结果按照前面的规则执行比较
- 如果一个操作值是布尔值,则将其转换为数值,再进行比较
"A">"a" //false
"c">"b" //true
1>"2" //false
[0]>=0 //true
false<=0 //true
NaN>1 //false
其他类型转换
下面列举的情况都会发生布尔值隐式类型转换:
- if () 语句中的条件判断表达式;
- for ( … ; … ; … ) 语句中的条件判断表达式(第二个);
- while () 和 do…while() 循环中的条件判断表达式;
- ? : 中的条件判断表达式
更多