前言
JavaScript 的一个显著特点就是灵活,是一门弱语言。灵活的反面就是猝不及防的坑多,定义的变量可以改变类型,数据类型会进行隐式转换等一系列头皮发麻的操作,下面例子你知道答案是什么吗
(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]]
提示:以下是本篇文章正文内容,下面案例可供参考
一、类型转换的由来
MDN介绍过JavaScript 的特点:JavaScript是一种弱类型,或者说是一种动态语言。这意味着你不用提前声明变量的数据类型,在程序运行过程中,变量的数据类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据
let a = 123
a = 'abc'
a = () => {}
ECMAScript 标准定义了 7 种数据类型 object number boolean string undefined null symbol 不同形式的值对应的类型不一样,如果我们想将值从一种类型转换为另一种类型就需要类型转换
JS 的类型转换共有两种:显示类型转换 和 隐式类型转换
- 显示转换是指在原始值和包装对象或者运算符结合后,手动显式的改变了类型
- 隐式转换是指在代码执行过程中,通过运算符运算或语句执行等操作,js 引擎会自动隐式的改变类型
二、显示转换
显示类型转换是通过 JS 提供的一些函数或运算符,可以直接将类型进行转换
2.1 Number() String() Boolean()
- String() 将值转换为字符串基本类型
- Number()将值转换为数字基本类型
- Boolean()将值转换为布尔基本类型
- parseInt() 将值转换为数字基本类型
基本类型互相转换
Number('')
Number(' 12 ')
Number('1 1')
Number('12text')
Number(true)
Number(false)
Number([0])
Number([1,2,3])
Number(undefined)
Number(null)
值 | Number() |
---|
undefined | NaN | null | 0 | true | 1 | false | 0 | " " | 0 | " 11 "(字符串头尾有空格) | 11 | ‘1 1’(空格在中间/字符串中含有非数字类型字符) | NaN | 011(非10进制数) | 9 | +0 | 0 | NaN | NaN |
String(undefined)
String(null)
String(true)
String(false)
String(01)
String(011)
String(123)
String(999999999999999999999)
注意:Number类定义的 toString() 方法可以接受表示转换基数的可选参数,如果不指定此参数,转换规则将是默认基于十进进制
值 | String() |
---|
undefined | “undefined” | null | “null” | true | “true” | false | “false” | ‘’ "(空字符串) | " "(空字符串) | ’ 11 '(字符串头尾有空格) | " 11 " | ‘1 1’(空格在中间/字符串中含有非数字类型字符) | “1 1” | 011(非10进制数) | “9” | 0 | “0” | NaN | “NaN” |
任何类型与字符串相加都得到的是字符串
Boolean('')
Boolean('123')
Boolean(0)
Boolean(NaN)
Boolean(1)
Boolean(undefined)
Boolean(null)
Boolean({})
注意:undefined、null、false、+0、-0、NaN、"" 只有这些toBoolean()是 false,其余都为 true
引用类型转基本类型
String({})
String([1, 2])
String([1, undefined, 2])
String(function() {})
String(class A{})
String(new Date())
String(/\s/)
String(new RegExp(/\s+/))
值 | String() |
---|
{} | “[object Object]” | [] (空数组) | “” | [124] (只有一个纯数字元素的数组) | “124” | [1, 2] | “1,2”(相当于数组arr.join()方法) | [1, undefined, 2](数组的某一项是null/undefined) | “1,2” | function(){}(函数对象) | “function(){}”(定义函数的代码) | new Date()(日期类) | "Tue Dec 14 2021 22:59:34 GMT+0800 (中国标准时间)’ | /\s/ new (RegExp(/\s/))(正则对象) | “/\s/”(正则对象字面量的字符) |
Number([])
Number([1])
Number([1, 2])
Number(new Date())
Number({})
Number(new (RegExp(/\s/)))
值 | Number() |
---|
{} | NaN | [] (空数组) | 0 | [124] (只有一个纯数字元素的数组) | 124 | [1, 2] | NaN | [1, undefined, 2](数组的某一项是null/undefined) | NaN | function(){}(函数对象) | NaN | new Date()(日期类) | 1607334260466(时间戳) | /\s/ new (RegExp(/\s/))(正则对象) | NaN |
小结:对象转换位数字类型过程会调用对象.valueOf() 原始值
2.2 一元 + - 运算符
- "3.14"
- "22"
+"3.14"是+运算符的一元形式(即只有一个操作数)。+运算符显式地将 ”3.14“ 转换为数字,而非数字加法运算(也不是字符串拼接)
一元运算符 - 和 + 一样,会反转数字的符号位。由于 – 会被当作递减运算符来处理,所以我们不能使用 – 来撤销反转,而应该像 - -"3.14"这样,在中间加一个空格
尽量不要把一元运算符 + (还有 - )和其他运算符放在一起使用
var time = new Date()
+time
一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳
2.3 ~ 位运算符
- Javascript 按位取反位运算符 (位非)~ ,对一个表达式执行位非(求非)运算。如 ~1 = -2 ; ~2 = -3 ; ~99 = -100;
- 运算符查看表达式的二进制表示形式的值,并执行位非运算
~5
00000000000000000000000000000101
11111111111111111111111111111010
00000000000000000000000000000101
00000000000000000000000000000110
在 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")
if(~a.indexOf("lo")){
}
~a.indexOf("ol")
if(!~a.indexOf("ol")){
}
~~x 能将值截除为一个整数,~~只适用于数字,更重要的是它对负数的处理与Math.floor()不同, 一般也是用于取整操作性能比 Math.floor() 好
Math.floor(-49.6)
~~-49.6
parseInt() 解析字符串中的数字和Number()将字符串强制类型转换为数字的返回结果都是数字。但是解析和转换两者之间还是有明显的差别, 而且 parseInt() 第二个为可选参数可以转化为不同进制的数,默认是转化为十进制的数
var a = "42"
var b = "42px"
Number(a)
parseInt(a)
Number(b)
parseInt(b)
parseInt("010101",2)
parseInt() 解析允许字符串中含有非数字字符,比如"42px",解析按从左到右的顺序,如果遇到非数字字符“p”就停止。而Number()转换不允许出现非数字字符,否则会失败返回NaN
解析字符串中的浮点数可以使用parseFloat()函数。从ES5开始 parseInt() 默认转换为十进制数,除非指定第二个参数作为基数
parseInt()针对的是字符串,向parseInt()传递数字和其他类型的参数是没有用的。非字符串会首先被强制类型转换为字符串,应该避免向parseInt()传递非字符串参数
parseInt(1/0,19)
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'
1 + true
1 + false
1 + undefined
'sunshine' + true
由上面结果发现当 + 运算计算 string 类型和其他数据类型相加时,其他数据类型都会转换为 string 类型;而在其它情况下都会转换为 number 类型,但是 undefiend 类型会转换为 NaN,相加结果也是 NaN
引用类型
在+运算符两侧,如果存在引用数据类型,比如对象,数组等那又会遵循怎样的一套转换规则呢
{} + true
当使用 + 运算符时,如果存在引用数据类型,那么它将会被转换为基本类型之后再进行运算。这就涉及对象的转换规则
小结:
对于加法操作,+运算符在 JS 语法解析中存在二义性
- 如果 +运算符两边存在 NaN,则结果为 NaN
- 如果是 Infinity + (-Infinity),则结果为NaN
- 如果 -Infinity + (-Infinity),则结果是 -Infinity
- 如果 Infinity + (-Infinity),则结果是 NaN
如果 + 运算符两边有或至少一个是字符串,则其规则如下
- 如果 +运算符两边都是字符串,则执行字符串拼接操作
- 如果 +运算符两边只有一个是字符串,则将另外的值转换为字符串,再执行字符串拼接操
- 如果 +运算符两边有一个是对象,则调用 valueOf 或 toString 方法取得值,将其转换为基本类型在进行字符串拼接
- 字符串于任意类型最后得到的都是字符串
对象的隐式转换规则(ToPrimitive)
对于多数情况来说,对象隐式转换成字符串或数字,其实调用了一个叫做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)
上面代码则对foo对象的 valueOf 和toString 进行了重写
语句与逻辑运算符
相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换
- if()语句中的条件判断表达
- for(…; …; …)语句中的条件判断表达式
- while()和do … while()
- ? : 中的条件判断表达式(三目表达式)
- 逻辑运算符||和&&左边的操作数
注意:&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值
以如果条件判断左边的操作数or表达式的结果为前提,对于||来说,如果结果为true就返回第一个操作数的值,如果为false就返回第二个操作数的值。对于&&来说,如果结果为true就返回第二个操作数的值,如果为false就返回第一个操作数的值
|| 逻辑或运算符遇真则停,都是假返回逻辑或的最后的假值
function foo(a,b) {
a = a||"hello"
b = b||"world"
console.log(a + '' + b)
}
foo()
&& 逻辑或运算符遇假则停,都是真返回逻辑或的最后真值
function foo() {
console.log(a)
}
var a = 42
a && foo()
Symbol
ES6中引入了Symbol类型,它的类型转换有一个坑。ES6允许从Symbol到字符串的显式转换,然而隐式转换会产生错误
var s1 = Symbol("cool")
String(s1)
var s2 = Symbol("not cool")
s2 + ''
+ Symbol("11")
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的结果。 返回false
主要分为 x 和 y 类型相同和类型不同的情况,类型相同时没有类型转换,类型不同时
- x, y 为 null、undefined 两者中一个,返回true
- x、y为 Number 和 String 类型时,则转换为 Number 类型比较
- 有 Boolean 类型时,Boolean 转化为 Number 类型比较
- 一个 Object 类型,一个 String 或 Number 类型,将 Object 类型进行原始转换(ToPrimitive)后,按上面流程进行原始值比较
字符串VS数字
var a = 42
var b = '42'
a == b
根据第4条规则返回x == ToNumber(y)的结果:a==b是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。具体怎么转换?这就需要匹配前文的“抽象相等比较算法”,寻找适应的转换规则
其它类型 VS 布尔类型
==最容易出错的一个地方是true和false与其他类型之间的相等比较 (ps: 工作中最好少用双等判断条件)
var a = '42'
var b = true
a == b
结果是false,这让人很容易掉坑里。如果严格按照“抽象相等比较算法”,这个结果也就是意料之中的
根据第7条规则,若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果,即返回’42’ == 1,结果为 false
很奇怪吧?所以切记:无论什么情况下都不要使用== true和== false。
"0" == false
false == 0
false == ""
false == []
"" == 0
"" == []
0 == []
其中有4种情况涉及== false,之前我们说过应该避免,所以还剩下后面3种。
这些特殊情况会导致各种问题,使用中要多加小心。我们要对==两边的值认真推敲,以下两个原则可以让我们有效地避免出错
- 如果两边的值中有true或者false,千万不要使用==
- 如果两边的值中有[]、""、或者0,尽量不要使用==
隐式强制转换在部分情况下确实很危险,为了安全起见就要使用 === (全等)
null 和 undefined
在==中 null 和 undefined 相等,这也就是说在 == 中 null 和 undefined 是一回事,可以相互进行隐式强制类型转换
null == undefined
掌握“抽象相等比较算法”,读者可以自行推倒为什么 [] == ![]返回 true
< 和 <=
var a = {b:42}
var b = {b:43}
a < b
a == b
a > b
a <= b
a >= b
如果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既不大于也不小于任何其他值(六亲不认哈哈)
四、练习
- 实现一个函数,如果其中有且仅有一个参数为true,则函数返回true
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)
无论使用隐式转换还是显式转换,我们都可以通过修改onlyTwo()或者onlyFive()来处理更加复杂的情况,只需要将最后的条件判断从改为2或5。这比加入一大堆&&和||表达式要简洁得多
- 写出以下代码执行结果
+true
+[]
+new Date()
[] == false
!![]
0 == '\n'
!+[]+[]+![]
- 解释下面这张图的执行结果
|