之前写了一篇文章简单介绍过使用“==“进行比较时被操作数的类型转换规则,但看了些题目后觉得对JS的隐性数据类型转换还不够熟悉,这里再补充总结一下。
本文完全是参考阮一峰老师JS教程数据转换和运算符章节:JS数据类型转换 ,JS运算符。个人认为阮一峰老师讲得非常清晰,建议各位有需要直接阅读。本文仅作自己记录总结。
为什么JS支持隐性数据类型转换
隐性数据类型转换在JS中非常普遍,在很多场景下都会发生。虽然这让JS很灵活,但也因此引入了较为复杂的转换规则和一些潜在的逻辑风险。个人感觉这样是得不偿失的,因为增加了开发人员的记忆负担。为什么JS会如此普遍地支持隐性类型转换,这里引用一下阮一峰老师文章的解释:
JavaScript 是一种动态类型语言, 变量没有类型限制,可以随时赋予任意值
(在JS中),变量的类型没法在编译阶段就知道,必须等到运行时才能知道
虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。
个人理解是:因为JS变量类型是不确定的(在编译期间不可确定)。但运算符对其操作数的变量类型是有限制的,为避免JS脚本在执行过程中因变量类型问题而大量报错,所以在会进行隐性的数据类型转换。
JS隐式类型转换场景
- 运算符操作时转换。因为各运算符对运算子有数据类型要求,当运算子类型要求达不到时,会进行类型转换
- 条件判断语句中,如if(),while(),圆括号中期望的是布尔值,如果不是布尔值,则会使用Boolean将其转换为布尔值
- 内置函数。这个严格来说我觉得可能不算隐式类型转换,但还是在这备注一下。如alert函数,要求第一个参数是字符串,如果不是字符串,则会使用String方法??将其转换为字符串。所以alert是无法展示完整的对象的。数组和自定义函数可以完整显示
强制类型转换方法
既然本文说的是隐式类型转换,那为什么会提及强制类型转换方法。因为JS中多数隐式类型转换场景内里使用的都是下面介绍的强制类型转换方法
Number()
使用
Number
函数,可以将任意类型的值转化成数值。下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。
- 原始类型值
// 数值:转换后还是原来的值
Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值。
Number('324') // 324
//Number方法会省略字符串前导和后缀空格
Number(" 23 \t") //23
// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN
// 空字符串转为0
Number('') // 0
// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0
// undefined:转成 NaN
Number(undefined) // NaN
// null:转成0
Number(null) // 0
复制代码
- 对象
Number({}) //NaN
Number([]) //0
Number([1]) //1复制代码
这个Number背后的转换规则有关,详见上文阮一峰老师的文章
第一步,调用对象自身的
valueOf
方法。如果返回原始类型的值,则直接对该值使用Number
函数,不再进行后续步骤。第二步,如果
valueOf
方法返回的还是对象,则改为调用对象自身的toString
方法。如果toString
方法返回原始类型的值,则对该值使用Number
函数,不再进行后续步骤。第三步,如果
toString
方法返回的是对象,就报错。
String()
和Number一样,分为原始值和对象类型
原始值类型- 数值:转为相应的字符串。
- 字符串:转换后还是原来的值。
- 布尔值:true转为字符串"true",false转为字符串"false"。
- undefined:转为字符串"undefined"。
- null:转为字符串"null"。
String
方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
String
方法背后的转换规则,与Number
方法基本相同,只是互换了valueOf
方法和toString
方法的执行顺序。
先调用对象自身的
toString
方法。如果返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。如果
toString
方法返回的是对象,再调用原对象的valueOf
方法。如果valueOf
方法返回原始类型的值,则对该值使用String
函数,不再进行以下步骤。如果
valueOf
方法返回的是对象,就报错。
Boolean
目前我所知的,JS中有六个假值,即Boolean()会转换为false。除这六个假值外其他都转换为true
null
undefined
一个对象类型
document.all
一个字符串类型
""
两个数值类型
0,+0,-0
NaN复制代码
各运算符期望的操作数类型
js中有很多种运算符,但会发生隐性类型转换且应用场景广泛的主要可以分为三类
算术运算符
加法运算符
加法运算符在算术运算符中是非常特殊的一个,因为其具有重载的特性。所谓重载即根据运算子的不同而有不同的行为。加法运算符有两种行为:
- 两边运算子只要其中一个是字符串,则另外一个运算子如果是非字符串会被隐性转为字符串(效果同使用String()转换),此时加法运算符实际上是执行字符串拼接运算。原始类型数据转换为字符串的规则如下:
"s" + 1 ==> "s" + String(1) //"s1"
String(null) //"null"
String(undefined) //"undefined"
String(false) //"false"
String(true) //true复制代码
- 两边运算子均不是字符串(也均不是对象),则执行加法运算。若有运算子不是数值类型,则先转换为数值类型(转换效果同Number)再进行加法运算。原始类型转换为数值的规则如下
1 + true ==> 1 + Number(true) //2
Number(true) //1
Number(false) //0
Number(null) //0
Number(undefined) //NaN
复制代码
- 如果有运算子是对象,则会先将其转为原始值再相加(不一定会被转为字符串)。对象转为原始值规则为:首先,自动调用对象的
valueOf
方法(Date对象除外,Date对象会先调用自身的toString方法)。如果对象的valueOf方法返回值不是原始类型的值,则再调用返回值的toString
方法(如果对象的toString方法返回的仍不是原始类型值,则报错)。
各数据类型对应valueOf()和toString()的转换规则
[].valueOf() //[]
true.valueOf() //true
(new Date()).valueOf //1558192343693
function fn() {
console.log(1)
}
fn.valueOf() //ƒ fn() {console.log(1)} //自定义函数返回完整的函数表现
alert.valueOf() //ƒ alert() { [native code] } 内置函数返回不同于自定义函数
i = 1;i.valueOf() //i
obj = {};obj.valueOf() //{}
str = ""
str.valueOf() //""
toString转换规则
引用类型转换规则:
obj = {}
obj.toString //"[object Object]"
Math.toString() //"[object Math]" 其他类型的对象
数组重写了toString
arr = [] //空数组
arr.toString() //""
arr1 = [1] //数组只有一个元素
arr1.toString() //"1"
arr2 = [1.2, 2] //数组不止一个元素
arr2.toString() //"1.2,2" 效果类似join(",")
函数重写了toString
function fn() {console.log(1)}
fn.toString() //"function fn() {console.log(1)}"
alert.toString() //"function alert() { [native code] }" 内置函数
原始类型。原始类型的转换规则比较简单,即将原始类型值直接转换为对应值的字符串
str = "a"
str.toString() //"a"
num = 1
num.toString() //"1"
bool = true;
bool.toString() //"true"
obj = {}
obj.valueOf.call(null) //Uncaught TypeError: Cannot convert undefined or null to object
obj.toString.call(null) //"[object Null]"
obj.toString.call(undefined) //"[object Undefined]"复制代码
总结:
- 引用类型中,函数和数组都重写了toString方法。数组的toString方法效果类似join(",")
- 函数和数组都未重写valueOf方法
- 不可以以任何形式调用null,undefined的valueOf方法,但可以以call的形式调用其toString方法
ex:运算子相加
1 + {} ==> 1 + {}.valueOf().toString() //"1[object Object]"
obj = {
valueOf: function() {
console.log("trigger valueOf"); //该行会被打印
return 1
}
toString: function() {
console.log("trigger toString"); //该行不会被打印,因为valueOf返回原始类型,所以toString不会被触发
return "toString"
}
}
1 + obj //2
(new Date()).valueOf() //1558230138200
1 + (new Date()) //"1Sun May 19 2019 09:42:38 GMT+0800 (中国标准时间)"
Date对象会先调用自身的toString方法复制代码
其他算术运算符
除加法运算符外,其他算术运算符的规则就比较简单了。它们均要求两侧运算子未数值类型,如果非数值类型则会使用类型Number()将运算子转换为数值类型再运算
+true //+1 数值运算符
true - false //1
true - [] //1
true - [1] //0
true - [1,2] //NaN复制代码
比较运算符
JavaScript 一共提供了8个比较运算符。
>
大于运算符<
小于运算符<=
小于或等于运算符>=
大于或等于运算符==
相等运算符===
严格相等运算符!=
不相等运算符!==
严格不相等运算符
这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小
非相等运算符:字符串
JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。
即如果第一个字符即比较出结果,则不需要再比较下去
"cat" > "dog" //false
"2" > "100" //true
"2.0" > "2" //true 第一个字符相等的情况下比较第二个字符,"."码点比空字符码点大,空字符码点为0x00复制代码
非相等运算符:非字符串,非对象
- 原始值之间的比较。先使用Number将非数值类型的转换为数值类型再比较
1 > true ==> 1 > Number(true) //false
true > "a" ==> Number(true) > Number("a") //false
1 > null //true
1 > undefined //false NaN与任何值比较都是返回NaN复制代码
非相等运算符:对象
如果运算子是对象,会转为原始类型的值,再进行比较。
对象转换成原始类型的值,算法是先调用
valueOf
方法;如果返回的还是对象,再接着调用toString
方法
可见逻辑基本是同拼接运算符。
"2" > [11]
等同于:"2" > [11].valueOf().toString(),等同于"2" > "11"
两边都是字符串,比较字符串码点。第一位即比较出结果
obj1 = { x: 1 }
obj2 = { x: 2 }
obj1 >= obj2
等同于:obj1.valueOf().toString() >= obj2.valueOf().toString()
"[object Object]" >= "[object Object]" //true
"[object Object]" >= obj2 //true复制代码
总结:
- 当两边运算子都是字符串时,比较的是字符的码点
- 当两边运算子不都是字符串,则原始值运算子(即使是字符串)使用Number方法转换为数值。引用类型会先使用valueOf,若valueOf返回的不是原始值,则再使用toString转换。若返回的还不是原始值,则报错
所以非相等运算符到最后都是原始值之间的比较。
严格相等运算符(===)
该运算符比较的时候不会发生类型转换
相等运算符(==)
- 原始值,两边数据类型相同。不发生类型转换,效果同严格相等运算符
- undefined,null。undefined和null与其他类型值比较时,不发生类型转换,结果为false。undefined,null相互比较时,结果为true
- 原始值,两边数据类型不相同,非null或undefined。使用Number方法将非数值类型转换为数值类型再比较大小
- 两运算子均为对象,比较指针
- 运算子一方为对象一方为原始值。若另一运算子是字符串类型,会使用String方法转换对象。String方法转换对象的算法是先调用对象的toString方法,若返回不是原始类型,则再调用valueOf方法。若返回还不是原始类型,则报错。若另一运算子是数值或布尔类型,则会调用Number方法。Number方法也是调用valueOf和toString方法,只不过是先调用valueOf方法再调用toString方法。
[] == "" //true
String([]) ==> [].toString ==> ""
[] == 1
Number([]) ==> [].valueOf() 返回[],不是原始类型值,于是再调用toString
[].toString() ==> "" 返回的是空字符串,此时两边运算子都是原始类型值,使用Number方法将空字符串转换为数值类型
Number("") ==> 0
0 == 1 //false复制代码