JavaScript类型显/隐式转换

前言

JavaScript 的一个显著特点就是灵活,是一门弱语言。灵活的反面就是猝不及防的坑多,定义的变量可以改变类型,数据类型会进行隐式转换等一系列头皮发麻的操作,下面例子你知道答案是什么吗
(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+[])[[~!+[]*~+[]]]  
// 答案是啥

类型转换的由来

MDN介绍过JavaScript 的特点:JavaScript是一种弱类型,或者说是一种动态语言。这意味着你不用提前声明变量的数据类型,在程序运行过程中,变量的数据类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据
let a = 123  // number类型
a = 'abc'    // string类型
a = () => {} // 对象类型
// a 在赋值中不断的切换类型且不报错

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

JS 的类型转换共有两种:显示类型转换 和 隐式类型转换

  • 显示转换是指在原始值和包装对象或者运算符结合后,手动显式的改变了类型
  • 隐式转换是指在代码执行过程中,通过运算符运算或语句执行等操作,js 引擎会自动隐式的改变类型

显示转换

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

Number() String() Boolean()

  • String() 将值转换为字符串基本类型
  • Number()将值转换为数字基本类型
  • Boolean()将值转换为布尔基本类型
  • parseInt() 将值转换为数字基本类型

基本类型互相转换

  • 转数字类型
// 字符串
Number('') // 0
Number(' 12 ') // 12
Number('1 1') // NaN
Number('12text') // NaN

// 布尔值
Number(true) // 1
Number(false) // 0

// 数组 
Number([0])  // 0
Number([1,2,3])  // NaN
// 注意转换数组长度位 1 时直接会忽视中括号,但是长度不为 1 时转换位 NaN

// undefined
Number(undefined) // NaN

// null
Number(null) // 0
Number()
undefinedNaN
null0
true1
false0
" "0
" 11 "(字符串头尾有空格)11
‘1 1’(空格在中间/字符串中含有非数字类型字符)NaN
011(非10进制数)9
+00
NaNNaN
  • 转字符类型
// 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”
011(非10进制数)“9”
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

引用类型转基本类型

  • 转字符类型
// 数组
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]”
[] (空数组)“”
[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([]) // 0
Number([1]) // 1
Number([1, 2]) // NaN

// 日期时间函数
Number(new Date()) // 1607334260466时间戳

// 对象
Number({}) // NaN

// 正则
Number(new (RegExp(/\s/))) // NaN
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() 原始值

一元 + - 运算符

  • 字符串转数字
 - "3.14" // 3.14
 - "22"   // 22

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


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


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

  • 日期时间转数字
var time = new Date()
+time  

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

~ 位运算符

  • 转数字
  1. Javascript 按位取反位运算符 (位非)~ ,对一个表达式执行位非(求非)运算。如 ~1 = -2 ; ~2 = -3 ; ~99 = -100;
  2. 运算符查看表达式的二进制表示形式的值,并执行位非运算
~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")){
    // 没有找到匹配
}
  • ~~字位截除

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

Math.floor(-49.6)   // -50
~~-49.6 //-49
  • parseFloat() parseInt()

parseInt() 解析字符串中的数字和Number()将字符串强制类型转换为数字的返回结果都是数字。但是解析和转换两者之间还是有明显的差别, 而且 parseInt() 第二个为可选参数可以转化为不同进制的数,默认是转化为十进制的数

var a = "42"
var b = "42px"

Number(a)   //42
parseInt(a) //42

Number(b)   //NaN
parseInt(b) //42

parseInt("010101",2) // 21

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' // 11
1 + true // 2
1 + false // 1
1 + undefined // NaN
'sunshine' + true // sunshinetrue

由上面结果发现当 + 运算计算 string 类型和其他数据类型相加时,其他数据类型都会转换为 string 类型;而在其它情况下都会转换为 number 类型,但是 undefiend 类型会转换为 NaN,相加结果也是 NaN

引用类型

在+运算符两侧,如果存在引用数据类型,比如对象,数组等那又会遵循怎样的一套转换规则呢

{} + true  // [Object Object] 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()的返回情况

类型toStringvalueOf
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

// 第一个console 打印结果为 sunshine。这里就涉及隐式转换,在console 打印的时候,倾向于使用 foo 对象的 toString 方法,将 foo 转换为基本数据类型
// 第二个console 打印结果为 2,这时候的隐式转换则倾向于使用 foo 对象的 valueOf 方法,将 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()   // "hello world"

// 逻辑或常用于赋默认值

&& 逻辑或运算符遇假则停,都是真返回逻辑或的最后真值

function foo() {
    console.log(a)
}
var a = 42 
a && foo()
// 逻辑与常用于简写条件判断,叫做“守护运算符”,即前面的表达式为后面的表达式把关

Symbol

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

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

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  // true

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

其它类型 VS 布尔类型

==最容易出错的一个地方是true和false与其他类型之间的相等比较 (ps: 工作中最好少用双等判断条件)

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

< 和 <=

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既不大于也不小于任何其他值(六亲不认哈哈)


练习

  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。这比加入一大堆&&和||表达式要简洁得多

  1. 写出以下代码执行结果
+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"
  1. 解释下面这张图的执行结果

在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值