Skip to content

Latest commit

 

History

History
executable file
·
680 lines (488 loc) · 26.2 KB

类型转换.md

File metadata and controls

executable file
·
680 lines (488 loc) · 26.2 KB

前言

JavaScript 的一个显著特点就是灵活。灵活的反面就是猝不及防否坑多,其中一个典型的例子就是一直被诟病的数据类型隐式转换。先来看一个极端的例子,如下:

(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]]
// "sb"

这就是隐式转换的成果。为什么会有这样的输出呢?这里先不做过过多解释,先从基础入手来进行分析。

类型转换的由来

MDN这样介绍过JavaScript 的特点:JavaScript是一种弱类型,或者说是一种动态语言。这意味着你不用提前声明变量的数据类型,在程序运行过程中,变量的数据类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据。

let a = 123 // number类型
a = 'abc' // string类型
a = () => {} // 对象类型

ECMAScript 标准定义了 7 种数据类型,不同形式的值对应的类型不一样,如果我们想将值从一种类型转换为另一种类型就需要类型转换。

JS 的类型转换一共分两种:显示类型转换 和 隐式类型转换。显示转换是指在原始值和包装对象或者运算符结合后,显式的改变了类型。隐式转换是指在代码执行过程中,通过运算符运算或语句执行等操作,隐式的改变了类型。

显示转换

显示类型转换是通过 JS 提供的一些函数或运算符,可以直接将类型进行转换。

1. Number() String() Boolean()

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

2. 一元 +-运算符

字符串转数字

+"3.14" // 3.14

上例中+"3.14"是+运算符的一元形式(即只有一个操作数)。+运算符显式地将 ”3.14“ 转换为数字,而非数字加法运算(也不是字符串拼接)。

拓展:

一元运算符 - 和 + 一样,会反转数字的符号位。由于 -- 会被当作递减运算符来处理,所以我们不能使用 -- 来撤销反转,而应该像 - -"3.14"这样,在中间加一个空格。

尽量不要把一元运算符 + (还有 - )和其他运算符放在一起使用。

日期时间转数字

一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳。

var time = new Date()
+time

3. ~ 位运算符

转数字

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")){
    // 没有找到匹配
}

4. ~~字位截除

~~x能将值截除为一个整数,~~只适用于数字,更重要的是它对负数的处理与Math.floor()不同。

Math.floor(-49.6)   // -50
~~-49.6 //-49

5. parseFloat() parseInt()

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 + '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 语法解析中存在二义性。

  1. 如果+运算符两边都是 number 类型,则其规则如下:
    • 如果 +运算符两边存在 NaN,则结果为 NaN
    • 如果是 Infinity + (-Infinity),则结果为NaN
    • 如果 -Infinity + (-Infinity),则结果是 -Infinity
    • 如果 Infinity + (-Infinity),则结果是 NaN
  2. 如果 +运算符两边有或至少一个是字符串,则其规则如下:
    • 如果 +运算符两边都是字符串,则执行字符串拼接操作
    • 如果 +运算符两边只有一个是字符串,则将另外的值转换为字符串,再执行字符串拼接操作
    • 如果 +运算符两边有一个是对象,则调用 valueOf 或 toString 方法取得值,将其转换为基本类型在进行字符串拼接

2. 对象的隐式转换规则(ToPrimitive)

对于多数情况来说,对象隐式转换成字符串或数字,其实调用了一个叫做ToPrimitive(obj,preferredType)的内部方法来干这件事情,此抽象方法将对象值转换为相应的基本类型值。

在调用这个方法转换的时候,除了date对象走转换数字流程(即preferredType值是number)优先调用 valueOf(),其他走的都是转字符流程(即preferredType值是string)优先调用toString()。用一句话解释就是:这个对象倾向转换成什么,就会优先调用哪个方法

转换数字流程:

  1. 调用 obj.valueOf(),如果执行结果是原始值,返回之;
  2. 否则为对象,调用 obj.toString(),如果执行结果是原始值,返回之;
  3. 否则抛异常。

转字符流程:

  1. obj.toString(),如果执行结果是原始值,返回之;
  2. 否则为对象,调用 obj.valueOf(),如果执行结果是原始值,返回之;
  3. 否则抛异常。

常用内置对象调用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 转换为基本数据类型,以执行相加操作。

3. 语句与逻辑运算符

相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换。

  • 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()

4. Symbol

ES6中引入了Symbol类型,它的类型转换有一个坑。ES6允许从Symbol到字符串的显式转换,然而隐式转换会产生错误,例如:

var s1 = Symbol("cool")
String(s1)  // "Symbol(cool)"
var s2 = Symbol("not cool")
s2 + '' // TypeError

Symbol不能够被转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式都是true)。

5. == 和 ===

常见的误区是“==检查值是否相等,===检查值和类型是否相等”,正确的解释是:“==允许在相等比较中进行强制类型转换,而===不允许”。事实上,==和===都会检查操作数的类型,区别在于操作数类型不同时它们的处理方式不同。

抽象相等比较算法

ES5规范11.9.3节的“抽象相等比较算法”定义了==运算符的行为。该算法简单而又全面,涵盖了所有可能出现的类型组合,以及它们进行强制类型转换的方式。

比较运算x==y, 其中x和 y是值,产生true或者false。这样的比较按如下方式进行:

  1. 若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。
  2. 若x为null且y为undefined, 返回true。
  3. 若x为undefined且y为null, 返回true。
  4. 若Type(x) 为 Number 且 Type(y)为String, 返回comparison x == ToNumber(y)的结果。
  5. 若Type(x) 为 String 且 Type(y)为Number,返回比较ToNumber(x) == y的结果。
  6. 若Type(x)为Boolean, 返回比较ToNumber(x) == y的结果。
  7. 若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果。
  8. 若Type(x)为String或Number,且Type(y)为Object,返回比较x == ToPrimitive(y)的结果。
  9. 若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)后,按上面流程进行原始值比较。

字符串VS数字

var a = 42
var b = '42'
a == b  // true

根据第4条规则返回x == ToNumber(y)的结果:a==b是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。具体怎么转换?这就需要匹配前文的“抽象相等比较算法”,寻找适应的转换规则。

其它类型 VS 布尔类型

==最容易出错的一个地方是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 是一回事,可以相互进行隐式强制类型转换。

null == undefined

掌握“抽象相等比较算法”,读者可以自行推倒为什么[]==![]返回true。

6. < 和 <=

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 类型转换相关的知识,试着解答下面的面试题来检验自己的掌握程度吧!

面试题

1. 实现一个函数,如果其中有且仅有一个参数为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)    // true

无论使用隐式转换还是显式转换,我们都可以通过修改onlyTwo()或者onlyFive()来处理更加复杂的情况,只需要将最后的条件判断从改为2或5。这比加入一大堆&&和||表达式要简洁得多。

2.写出以下代码执行结果

+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"

3. 解释下面这张图的执行结果

答案:根据全文自行分析,哈哈哈~

参考链接

  1. 【JS基础】类型转换知多少

  2. https://juejin.cn/post/6844904095774425101#heading-11

  3. JavaScript中的强制类型转换

最后

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

想阅读更多优质文章、可我的微信公众号【阳姐讲前端】,每天推送高质量文章,我们一起交流成长。

最后

如果你觉得这篇内容对你有启发,我想请你帮个小忙:

  1. 点击「在看」,让更多的人也能看到这篇内容

  2. 关注公众号「阳姐讲前端」,持续为你推荐精选好文

  3. 欢迎扫描下方二维码加我微信,拉你进群,长期交流学习....