最近在重读《JavaScript 高级程序设计》, 读到数据类型这一节, 想到了 JavaScript 里令程序员抓狂的一个问题 -- 类型转换. 因为 JS 是一门弱类型的语言, 在运行时系统会根据需要进行类型转换, 而类型转换的规则又令人迷惑. 于是写篇博文尝试自己总结来加深印象.
基本概念
首先, 我们知道 JavaScript 里有 7 种数据类型:
boolean number null string symbol undefined object
object 称为引用类型, 其余的数据类型统称为 "基本类型".
显示强制类型转换
转换为 Boolean 类型
布尔值的强制类型转换使用的方法主要有: Boolean() . 其实布尔值的转换规则很好记住, 因为转换后为 false 的值有限, 只有下列几种:
null undefined false +0 -0 NaN ""
转换为 Number 类型
数字的强制类型转换使用的方法主要有: Number() parseInt() parseFloat() 和一元操作符.
Number() 函数的转换规则如下:
如果是 Boolean 值, true 和 false 将分别被转换为 1 和 0.
如果是数字值, 只是简单的传入和返回.
如果是 null 值, 返回 0.
如果是 undefined , 返回 NaN .
如果是字符串, 遵循下列规则:
如果字符串中只包含数字(包括前面带正号或负号的情况), 则将其转换为十进制数值, 即 "1" 会变成 1, "123" 会变成 123, 而 "011" 会变成 11(注意: 前导的零被忽略了);
如果字符串中包含有效的浮点格式, 如 "1.1" , 则将其转换为对应的浮点数值(同样, 也会忽略前导零);
如果字符串中包含有效的十六进制格式, 例如 "0xf" , 则将其转换为相同大小的十进制整数值;
如果字符串是空的(不包含任何字符), 则将其转换为 0;
如果字符串中包含除上述格式之外的字符, 则将其转换为 NaN .
细说 parseInt
parseInt() 只处理字符串类型, 如果接受的参数不是字符串类型, 会先将其转换为字符串类型(稍后介绍字符串的强制转换)
parseInt() 函数在转换字符串时, 更多的是看其是否符合数值模式. 它会忽略字符串前面的空格, 直至找到第一个非空格字符. 如果第一个字符不是数字字符或者负号, parseInt() 就会返回 NaN . 如果第一个字符是数字字符, parseInt() 会继续解析第二个字符, 直到解析完所有后续字符或者遇到了一个非数字字符. 例:
- var num1 = parseInt("123iuuan"); // 123(字母不是数字字符, 被忽略)
- var num2 = parseInt(""); // NaN
- var num3 = parseInt("0xA"); // 10(十六进制数)
- var num4 = parseInt(22.5); // 22)(小数点并不是有效的数字字符)
parseInt() 函数可以接收两个参数, 第一个参数是需转换字符串, 第二个参数是转换是使用的基数(即多少进制), 例如:
- var num1 = parseInt("AF", 16); //175
- var num2 = parseInt("AF"); //NaN
当指定基数时, 字符串可以被成功转换, 而第二个转换时, 按之前说的转换规则, 第一个字符不是数字字符, 所以直接返回了 NaN.
对于同一个字符串, 如果指定的基数不同, 转换的结果也会受影响, 例如:
- var num1 = parseInt("10", 2); //2 (按二进制解析)
- var num2 = parseInt("10", 8); //8 (按八进制解析)
- var num3 = parseInt("10", 10); //10 (按十进制解析)
- var num4 = parseInt("10", 16); //16 (按十六进制解析)
综上所述, 当不指定基数时, parseInt() 会自行决定如何解析输入的字符串, 所以为了避免错误的解析, 使用 parseInt() 时都应该指定基数.
转换为 String 类型
要把一个值转换为一个字符串有两种方式, 第一种是使用 toString() 方法, 除了 null 和 undefined 之外, 其余的数据类型都有这个方法, 它返回相应值的字符串表现. 在调用数值的 toString() 方法时, 可以传递一个参数: 输出数值的基数. 默认的输出值与指定基数 10 时的输出值相同.
- var iuuan = true;
- alert(iuuan.toString()); // 'true'
- var num = 7;
- alert(num.toString()); // '7'
- alert(num.toString(2)); // '111'
- alert(num.toString(10)); // '7'
在不知道要转换的值是不是 null 或 undefined 的情况下, 还可以使用转型函数 String() , 这个函数能够将任何类型的值转换为字符串.
当值有 toString() 方法是, 调用该方法并返回结果;
值是 null 时, 返回 "null";
值是 undefined 时, 返回 "undefined".
- var value1 = 10;
- var value2 = true;
- var value3 = null;
- var value4;
- alert(String(value1)); // "10"
- alert(String(value2)); // "true"
- alert(String(value3)); // "null"
- alert(String(value4)); // "undefined"
对象转换为基本类型
1, 对象转换为布尔值时, 根据上文所说的 Boolean() 假值可知, 转换后所有的对象都为 true;
2, 对象转换为字符串:
判断对象是否有 toString() 方法, 如果有 toString() 方法且返回的结果是基本类型值, 就返回这个结果并转换为字符串;
如果对象没有 toString 方法或者该方法返回的不是原始值, 就判断该对象是否有 valueOf 方法. 如果存在 valueOf 方法且返回值是基本类型值, 就返回并转换为字符串;
否则就抛出错误.
- var objtostring1 = {
- //toString 返回基本类型值
- toString:function(){
- return null
- }
- }
- var objtostring2 = {
- //toString 方法返回不是基本类型值, valueOf 返回基本类型值
- toString:function(){
- return {}
- },
- valueOf:function(){
- return undefined
- }
- }
- var objtostring3 = {
- //toString 方法返回不是基本类型值, valueOf 返回的也不是基本类型值
- toString:function(){
- return {}
- },
- valueOf:function(){
- return {}
- }
- }
- String(objtostring1); //'null'
- String(objtostring2); //'undefined'
- String(objtostring3); //Uncaught TypeError: Cannot convert object to primitive value
3, 对象转换为数值:
对象转换为数值的操作与转换为字符串基本相似, 只是转换时先调用 valueOf , 不存在或返回值不是基本类型值时, 再调用 toString 方法.
- var objtonum1 = {
- //valueOf 返回基本类型值
- valueOf:function(){
- return null
- }
- }
- var objtonum2 = {
- //valueOf 方法返回不是基本类型值, toString 返回基本类型值
- valueOf:function(){
- return {}
- },
- toString:function(){
- return 1
- }
- }
- var objtonum3 = {
- //valueOf 方法返回不是基本类型值, toString 返回的也不是基本类型值
- valueOf:function(){
- return {}
- },
- toString:function(){
- return {}
- }
- }
- Number(objtonum1); //0 null 转换为数值后为 0
- Number(objtonum2); //1
- Number(objtonum3); //Uncaught TypeError: Cannot convert object to primitive value
隐式强制类型转换
与显示类型转换使用函数方法不同, 隐式类型转换发生在是使用操作符或者语句中间.
+ 操作符
当 + 操作符作为一元操作符时, 对非数值进行 Number() 转型函数一样的转换;
- var s1 = "01",s2 = "1.1",s3 = "z";,b = false,f = 1.1;
- var o = {
- valueOf: function() {
- return -1;
- }
- };
- s1 = +s1; // 值变成数值 1
- s2 = +s2; // 值变成数值 1.1
- s3 = +s3; // 值变成 NaN
- b = +b; // 值变成数值 0
- f = +f; // 值未变, 仍然是 1.1
- o = +o; // 值变成数值 - 1
当 + 操作符作为加法运算符时, 会应用如下规则:
如果两个操作数都是字符串, 则进行简单的字符串拼接;
如果只有一个操作数是字符串, 则将另一个转换为字符串再进行拼接, 转换为字符串的操作与显示转换时规则相同;
如果有一个操作数是对象, 数值或布尔值, 则调用它们的 toString 方法取得相应的字符串值, 然后再应用前面关于字符串的规则
- var s1 = "01",s2 = "1.1",b = false,f = 1.1;
- var o = {
- valueOf: function() {
- return -1;
- }
- };
- s1 + s2 //'011.1'
- s1 + b //'01false'
- s2 + f //'1.11.1'
- s1 + o //'01-1'
- 操作符
当 - 操作符作为一元操作符时, 对非数值进行 Number() 转型函数一样的转换之后再取负;
- var s1 = "01",s2 = "1.1",s3 = "z";,b = false,f = 1.1;
- var o = {
- valueOf: function() {
- return -1;
- }
- };
- s1 = -s1; // 值变成了数值 - 1
- s2 = -s2; // 值变成了数值 - 1.1
- s3 = -s3; // 值变成了 NaN
- b = -b; // 值变成了数值 0
- f = -f; // 变成了 - 1.1
- o = -o; // 值变成了数值 1
当 - 操作符作为加法运算符时, 会应用如下规则:
如果操作数存在非数值的基本类型, 则先转换为数值在进行减法计算;
如果操作数中存在对象, 则按照对象转换为数值的规则将对象转换为数值后进行减法计算.
布尔操作符
逻辑非 !
逻辑非操作符会将它的操作数转换为一个布尔值, 然后再对其求反. 所以使用两个逻辑非操作符, 实际上会模拟 Boolean() 转型函数的行为.
逻辑与 && 和逻辑或 ||
这两个操作符产生的值不是必须为 Boolean 类型, 产生的值始终未两个运算表达式的结果之一.
对于逻辑与 && 来说, 如果第一个操作数条件判断为 false 就返回该操作数的值, 否则就返回第二个操作数的值.
对于逻辑或 || 来说, 如果第一个操作数条件判断为 true 就返回该操作数的值, 否则就返回第二个操作数的值.
看个例子:
- var a = 'hello',b = '';
- a && b; // '' a 是真值, 所以返回 b
- b && a; // '' b 是假值, 所以直接返回 b, 不对 a 进行判断
- a || b; // 'hello' a 是真值, 所以直接返回 a
- b || a; // 'hello' b 是假值, 所以返回 a
可以看得出来, 两个操作符在执行时都有一个特点: 当第一个操作数能决定操作结果时, 则不会对第二个操作数进行判断, 并且直接返回第一个操作数的值. 这种操作又称为短路操作.
非严格相等 ==
等操作符比较两个值是否相等, 在比较前将两个被比较的值转换为相同类型. 在转换后(等式的一边或两边都可能被转换), 最终的比较方式等同于全等操作符 === 的比较方式.
ECMAScript5 文档中关于非严格相等的比较算法, 列出了有 11 中情况, 文中就不一一列出了, 可以自行去文档查看学习: 抽象相等比较算法 http://yanhaijing.com/es5/#203
这里说明一下 ToPrimitive 操作, 这个操作是 ECMAScript 运行时系统进行自动类型转换的一种抽象操作, 用于将对象类型转换为基本类型, 转换规则如下:
检查该值是否有 valueOf 方法. 如果有且返回基本类型值, 则使用该值;
如果没有就使用 toString 方法的返回值 (如果存在) 来进行强制类型转换;
如果 valueOf 或者 toString 都不返回基本类型值, 则会报错 TypeError.
如此绕的一串规则, 不如来看几个例子:
- 7 == '7' // true 字符串与数字比较时, 字符串转数值后比较
- 1 == true // true 操作数中有布尔值, 布尔值转数值后比较, true 为 1
- 7 == true // false 原理同上相当于 7 == 1
- [] == 0 // true []先调用 valueOf, 返回值非基本类型, 再调用 toString, 返回为'', 空字符串转数值后为 0
- [] == [] // false 作为引用类型, 内存地址不同
总结起来就是一下几条:
null 和 undefined 互相比较时, 结果为 true, 其余任何类型与这两个值比较都为 false;
操作数中存在数值, 则将另一个操作数转换为数值再比较;
操作数中没有数值但有字符串, 则将另一个操作数转换为字符串再比较;
操作数中的布尔值都转换为数值. 非基本类型都先进行 ToPrimitive 操作, 按上述三条顺序进行比较.
比较关系符
同样的, 文档中的规则非常长, 就不列出来了, 抽象关系比较算法 http://yanhaijing.com/es5/#197
- // 两边均为字符串
- '7'> '20'; // true 按字符编码进行比较
- // 两边不全是字符串
- 7> '20'; // false 字符串转为数值后进行比较
- // 两边全不是基本类型
- [7]> [20]; // true 数组调用 valueOf 返回非基本类型, 再调用 toString 方法返回字符串.
- var obj = {
- },obj1 = {
- };
- obj> obj1; // false
总结起来, 比较关系符的类型转换比较规则就是:
如果操作数中存在非基本类型, 先进行 ToPrimitive 操作;
ToPrimitive 操作转换后如果操作数出现数值, 那么将操作数转换为数值进行比较;
ToPrimitive 操作转换后如果操作数均为字符串, 那么按照字符编码值进行比较.
最后来说说 obj>= obj1 的特殊现象
- var obj = {
- },obj1 = {
- };
- obj <obj1; // false
- obj == obj1; // false
- obj> obj1; // false
- obj>= obj1; // true
- obj <= obj1; // true
前面三个结果不难理解, 非严格相等判断时, 均为空对象, 但引用地址不同, 返回 false. 比较两个对象时, 先进行 ToPrimitive 操作, 均返回 ''[object Object]'', 所以不存在大小关系, 也返回 false. 那为什么 a <= b 和 a>= b 的结果返回的是 true 呢?
因为根据规范, a <= b 实际上执行的是 !(a> b), 即我们理解的<= 是 "小于或等于", 但 JavaScript 执行的是 "不大于" 的操作, 所以 a> b 为 false, 那么 a <= b 自然为 true 了.
结语
JavaScript 作为一门弱类型语言, 其中的类型转换规则总结起来真是让人头疼. 当然, 熟练掌握这些规则, 并不是为了在实际开发中写出这些晦涩代码, 而是通过理解, 使得我们能够避免在代码编写的过程中避免触碰到不必要的类型转换, 提高代码的稳定性和可维护性.
笔者作为前端菜鸟, 文中如有错误, 欢迎指出, 共同交流, 进步.
参考链接
《JavaScript 高级程序设计》
MDN JavaScript 参考文档
ECMAScript5.1 中文版 http://yanhaijing.com/es5/#about
来源: https://juejin.im/post/5c4aaf5651882525a67c7810