共计 8113 个字符,预计需要花费 21 分钟才能阅读完成。
本文首发于集体博客 clloz.com,欢送拜访。
前言
JavaScript
中的类型转换是一个十分让人头大的内容,其实咱们平时的编码个别会尽量避免让本人陷入不确定的类型转换中。然而很多时候面试会考查这方面的常识,并且搞清楚类型转换的机制可能让咱们在遇到一些奇葩问题的时候晓得产生了什么。咱们不肯定要记住所有的类型转换的可能性,只有记住一些罕用的,以及如何进行查问即可。
装箱拆箱
在探讨具体的类型转换场景之前,咱们先来说一下装箱拆箱操作。在这之前你应该温习一下 JavaScript
中对于数据类型的常识,你能够看我的这一篇文章:JS 数据类型和判断办法。
装箱 wrapper
在 JavaScript
中目前共有八种数据类型 Undefined, Null, Number, String, Boolean, BigInt, Symbol, Object
。除了 Object
其余都是根本数据类型(primitive values
,也称原始值,原始类型)。所谓根本数据类型就是它们是一种即非对象也没有属性和办法的数据,根本类型间接代表了最底层的语言实现。
所有根本类型的值都是不可扭转的。但须要留神的是,根本类型自身和一个赋值为根本类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被扭转。即根本类型值能够被替换,但不能被扭转。比方,JavaScript
中对字符串的操作肯定返回了一个新字符串,原始字符串并没有被扭转。
既然如此,为什么咱们还能在 Number
或者 String
上应用办法呢?这就引出了 JavaScript
中的根本包装类型(primitive wrapper types
,也成为原始包装类型),因为咱们有在根本类型上频繁操作的需要(比方 String
的截取,Number
的格局转换等),所以 JavaScript
也为根本类型内置了一系列的 API
。然而只有对象能力应用办法,所以 JavaScript
就用根本包装类型来让根本类型可能领有属性和办法。除了 null
和 undefined
之外,所有根本类型都有其对应的包装对象:
String
为字符串根本类型。Number
为数值根本类型。BigInt
为大整数根本类型。Boolean
为布尔根本类型。Symbol
为字面量根本类型。
其中最重要的就是 String
,Number
和 Boolean
三种原始包装类型,也是咱们本文重点探讨的内容。
这些类型与其余援用类型类似,但同时也具备与各自的根本类型相应的非凡行为。实际上,每当读取一个根本类型值的时候,后盾就会创立一个对应的根本包装类型的对象,从而让咱们 可能调用一些办法来操作这些数据。看上面的例子:
let str1 = "clloz";
let str2 = str1.substring(2);
下面的代码中咱们创立了一个根本类型的字符串 str1
,而后咱们调用了 str1
的 substring
办法,从逻辑上来讲根本类型不应该有办法的。实际上 JavaScript
在背地为咱们创立了一个根本包装类型,大抵过程如下:
let temp = new String("clloz");
let str2 = temp.substring(2);
temp = null;
援用类型与根本包装类型的次要区别就是对象的生存期。应用 new 操作符创立的援用类型的实例,在执行流来到以后作用域之前都始终保留在内存中。而主动创立的根本包装类型的对象,则只存在于一 行代码的执行霎时,而后立刻被销毁。这意味着咱们不能在运行时为根本类型值增加属性和办法。
let str1 = "clloz";
str1.color = "red";
console.log(str1.color); // undefined
个别状况下,咱们不须要手动进行装箱操作,因为装箱后的根本类型就变成了一个对象,typeof
将返回 object
,在转换为 Boolean
的时候也会转换成 true
,比方 Boolean(new Boolean(false))
将返回 true
。咱们只须要依据本人的需要来创立根本类型即可,将是否须要装箱的判断交给引擎,一般来说咱们能在代码中优化的内容,引擎肯定会帮咱们进行优化。
最初说一说进行装箱的几种办法,这些办法对除了 null
和 undefined
的根本类型都无效(null
和 undefined
没有原生构造函数,因为它们并不需要 API
):
- 用
new
操作符调用对应类型的构造函数。 -
应用
Object
函数,带不带new
都能够。Object()
构造函数将会依据参数的不同做以下操作:- 如果给定值是
null
或undefined
,将会创立并返回一个空对象 - 如果传进去的是一个根本类型的值,则会结构其包装类型的对象
- 如果传进去的是援用类型的值,依然会返回这个值,经他们复制的变量保有和源对象雷同的援用地址
- 当以非构造函数模式被调用时,
Object
的行为等同于new Object()
。
- 如果给定值是
- 利用
call
。
let a = 2;
console.log(typeof a) //number
let t = (function(){return this;}).call(a)
console.log(typeof t) //object
拆箱 toPrimitive
装箱的操作是为了让咱们可能应用一些为根本类型内置的 API
。但有时咱们也须要对对象进行拆箱操作,比方当咱们进行四则运算,进行比拟等逻辑运算,等等。
在 JavaScript
规范中,规定了 ToPrimitive
函数,它是对象类型到根本类型的转换(即,拆箱转换)。拆箱转换会尝试调用 valueOf
和 toString
来取得拆箱后的根本类型。如果 valueOf
和 toString
都不存在,或者没有返回根本类型,则会产生类型谬误 TypeError
。String
的拆箱转换会优先调用 toString
。在 ES6
之后,还容许对象通过显式指定 @@toPrimitive
Symbol
来笼罩原有的行为。
这里为了让大家彻底明确拆箱的机制,咱们间接把 ECMAScript2021 规范中的定义拿过去解读一下:
我次要讲一下 2
中的步骤:
a
:获取input
的@@toPrimitive
办法,input
是一个对象。-
b
:如果@@toPrimitive
不是undefined
,而后i
:如果@@toPrimitive
办法中没有指定第二个参数,那么hint
设为default
。ii
:如果第二个参数是string
,那么hint
设为string
。iii
:如果第二个参数是number
,那么hint
设为number
。iv
:以input
和hint
为参数调用@@toPrimitive
办法。v
:如果执行后果不是一个对象,那么返回后果。vi
:如果执行后果是一个对象,抛出TypeError
。
c
:如果没有定义@@toPrimitive
办法,并且没有指定preferredType
,那么preferredType
设为number
。d
:返回OrdinaryToPrimitive(input, preferredType)
所以当咱们没有指定 @@toPrimitive
办法的时候,就是执行 OrdinaryToPrimitive(input, preferredType)
,该函数定义如下:
它承受两个参数 O
和 hint
,也就是咱们下面 d
步骤中的 input
和 preferredType
。次要步骤如下:
hint
必须是string
或者number
的一种。- 如果
hint
是string
,就按顺序调用对象的toString
和valueOf
办法,如果调用后后果不是对象则返回。 - 如果
hint
是number
,就按顺序调用对象的valueOf
和toString
办法,如果调用后后果不是对象则返回。
其实逻辑还是比拟清晰,并没有很简单,最初在说一说 toPrimitive
中的 b
状况。@@toPrimitive
办法就是让咱们自定义拆箱的规定,而不是依据规范的规定进行,咱们能够依据本人的需要定制拆箱的规定。@@
结尾的名字是规范中的 Well-Known Symbols,他们是内置的 Symbol
,作为属性的 key
。在 ES2016
引入 Symbol
后咱们曾经能够拜访这些 Symbol
,比方 @@match
,@@matchAll
等等,咱们在编码中能够间接应用 String.prototype.match
和 String.prototype.matchAll
来调用,他们在引擎外部即调用的 Symbol
对应的办法。@@
是在规范文档中的名字,咱们在 JavaScript
编码中应用的名字是将 @@
替换为 Symbol.
,所以咱们给对象增加 @@toPrimitive
属性就是增加一个 Symbol.toPrimitive
属性。当引擎调用 @@toPrimitive
的时候就会找到咱们定义的办法。
// 一个没有提供 Symbol.toPrimitive 属性的对象,参加运算时的输入后果
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); //"[object Object]"
// 接上面申明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输入后果
var obj2 = {[Symbol.toPrimitive](hint) {if (hint == "number") {return 10;}
if (hint == "string") {return "hello";}
return true;
}
};
console.log(+obj2); // 10 -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); //"true"-- hint 参数值是"default"
内置 Symbol
参考 Symbol – MDN
类型转换
当初咱们曾经晓得装箱和拆箱的规定,也就是把握了类型转换的工具,剩下的只有搞清楚哪个场景用哪个工具进行转换即可。
显式强制类型转换
在探讨隐式强制类型转换之前,咱们先讨论一下显式强制类型转换。
所谓 显式强制类型转换 指的就是咱们间接调用 Number()
,String()
和 Boolean()
构造函数(不带 new
)对一个值进行类型转换。咱们还是来解读规范文档。
留神一点,规范文档中的蓝色的办法后面的
!
不是取反的意思,你能够忽视掉,就当做执行前面的办法就能够。
Number
下面的截图就是对 Number
构造函数的定义,内容很简略:
- 如果不是用
new
调用的,则返回ToNumeric(value)
的值,value
是咱们传入的值,如果没有传入value
,那么就返回+0
。 - 如果是用
new
调用,则生成根本包装类型对象。
ToNumeric
的定义如下:
表格非常清晰,我就不解读了。表格中没有说的是 String
,String
转 Number
在规范中定义了十分长的内容,我集体了解就是不合乎 JavaScript
格局的 string
返回 NaN
,其余返回对应的数字。所谓的合乎格局就包含 0o
或 0
结尾的八进制,0x
结尾的十六进制,0b
结尾的二进制,迷信计数法等。
String
还是从规范解读:
- 如果不是
new
调用String
构造函数,返回ToString(value)
。 - 如果是以
new
调用String
构造函数,返回根本包装类型对象。
ToStrong
定义如下:
这当中 Number::String
在规范中定义比较复杂,应该是进行了严格的数学定义,咱们按咱们失常的了解就能够了。-0,-0
都是 0
,NaN
返回 "NaN"
。
Boolean
- 如果不是
new
调用Boolean
构造函数,返回ToBoolean(value)
。 - 如果是以
new
调用Boolean
构造函数,返回根本包装类型对象。
隐式强制类型转换
隐式强制类型转换可能是更让人头疼的一部分,其实只有搞清楚规范,隐式的转换也是用的咱们下面看到的那些办法进行转换的,咱们也不用记分明每一个规定,只有晓得到哪里去查,还有编码中防止一些会出问题的转换。我这里就找出一些咱们比拟常见的隐式转换的场景对规范进行解读。
算数运算符
在规范中所有的算数运算符最初都是由上面这个办法执行的 lval
即操作符右边的值,opText
即操作符,rval
即操作符左边的值:
-
如果操作符是
+
- 计算
ToPrimitive(lval)
赋值给lprim
- 计算
ToPrimitive(rval)
赋值给rprim
-
如果
lprim
或rprim
中有一个类型是String
- 计算
ToString(lprim)
赋值给lstr
- 计算
ToString(rprim)
赋值给rstr
- 拼接
lstr
和rstr
并返回
- 计算
- 将
lprim
赋值给lval
- 将
rprim
赋值给rval
- 计算
- 计算
ToNumeric(lval)
,赋值给lnum
- 计算
ToNumeric(rval)
,赋值给rnum
- 如果
Type(lnum)
和Type(rnum)
不同,抛出一个TypeError
- 进行算数运算
咱们能够看到这段定义中的办法都是咱们在下面显示转换中介绍过的办法。在算数操作符中 ToPrimitive()
是并没有传入 hint
的,所以就用默认 number
,所以在算术运算的类型转换中,总是先调用 valueOf
,后调用 toString()
。
一元操作符
一元操作符的定义都非常简单,这里就不贴图了,间接给一个总结,如果你想看相干定义点击 ECMAScript 2021 – Unary Operators
++
->ToNumeric
--
->ToNumeric
+
->ToNumeric
-
->ToNumeric
~
->ToNumeric
!
->ToBoolean
这里再给大家举个例子 'a' + + 'a'
(留神两个加号不能相连)失去的后果是 aNaN
,因为第二个 +
作为一元操作符,调用 ToNumber()
最初的后果是 NaN
。而后执行 'a' + NaN
,就是回到算术运算符的定义,有一个是 String
两个都转成 String
而后返回拼接的字符串,所以最初的后果是 aNaN
。你也能够找一些例子进行验证。
关系运算符
所有的关系运算符(<, >, <=, >=
)的后果都是依据 Abstract Relational Comparison
的返回值计算,所以咱们先着重剖析这个办法,看下图。因为在规范中对立用小于号,所以用 leftFirst
示意是大于操作符还是小于操作符,true
则为小于关系符,false
则为大于关系符。
咱们能够看到第一部就是进行拆箱操作,hint
为 number
,也就是先调用 valueOf
,在调用 toString
。
当两个操作数 operand
都是字符串的时候,会调用一个办法 IsStringPrefix(a, b)
来计算结果。这个办法的意思就是:比方判断 a<b
的后果,就是判断 a
是不是 b
的一个前缀,就是 a
加上另一个字符串能形成 b
,如果能,则返回 true
;如果 b
是 a
的前缀,则返回 false
,所以 'cl' < 'clloz'
会返回 true
。如果不存在前缀关系,则进行 code unit
比拟,在 JavaScript
中是 UTF-16
编码,从最低位开始进行码点比拟雷同则进入下一位,如果能找到一位是 a
的码点小于 b
则返回 true
否则返回 false
。个别的字符串咱们只有依据扩大 ASCII
进行比拟即可。
console.log('cllob' < 'clloc') //true
console.log('cllob' < 'clloa') //false
console.log('clloba' < 'cllob') /false
bigInt
咱们就跳过,因为使用不是很多。咱们间接进入上面的 ToNumeric
,将两个操作数都进行 ToNumeric
,如果失去的后果类型雷同,则调用对应类型的 T::lessThan
. ToNumeric
的后果要么是 Number
要么是 BigInt
,要么抛错,所以咱们只有看 Number::lessThan(x, y)
的定义即可:
- 如果
x
是NaN
,返回undefined
。 - 如果
y
是NaN
,返回undefined
。 - 如果
x
和y
是雷同的数值,返回false
。 - 如果
x
和y
一个是+0
一个是-0
返回false
。 - 如果
x
和y
任意一个为 $\pm \infty$,返回false
。 - 其余状况进行数值比拟(非零并且不是无穷),
x < y
返回true
,否则返回false
。
留神,失去的 Abstract Relational Comparison
的返回值不是最终的后果 。对于 <, >
来说,如果 Abstract Relational Comparison
返回值是 undefined
,则则返回 false
,否则间接返回 Abstract Relational Comparison
的返回值。对于 <=, >=
,如果 Abstract Relational Comparison
的返回值是 true
或 undefined
,则返回 false
,否则返回 true
。
这里可能有同学纳闷 <=
和 >=
的逻辑是不是错了,Abstract Relational Comparison
的返回值是 true
应该返回 true
,这里规范外面是将 <=, >=
的 leftFirst
绝对于 <, >
去了一个相同,这样能放弃 lessThan
中的逻辑最简略,即 <, >
为 false
的时候 <=, >=
为 true
;<, >
为 true
的时候 <=, >=
为 false
。否则因为有第三条相等规定在,逻辑会比较复杂。具体的定义看 Relation-Operators -ECMAScript
上面来几个例子:
console.log(null < -0) //false null 被转为 +0,和 -0 进行 lessThan 返回 false,所以最终后果为 false
console.log(NaN < 10) //false 只有有 NaN,lessThan 的后果就是 undefined,对于 < 和 > 来说 undefined 最初返回 false
console.log(NaN <= 10) //false 对于 <= 和 >= 来说,undefined 就是 false
相等操作符
相等操作符有四个 ==, !=, ===, !==
,定义在 Equality-Operators – ECMAScript,其中最要害的就是两个办法:Abstract Equality Comparison
和 Strict Equality Comparison
,前者是双等号的办法,后者是全等号的办法。定义间下图:
咱们能够看到两个办法的定义长度齐全不同 。双等号能够算作是 JavaScript
中的一个设计失误,十分不倡议应用。这里我就说一说全等好的定义。
- 如果两个操作数的类型不同,返回
false
。 -
如果两个操作数的类型都是
Number
,调用Number::equal(x, y)
办法,返回办法的返回值(BigInt
不探讨)。该办法定义在 Number::equal – ECMAScript- 两个操作数有一个是
NaN
,返回false
。 - 两个操作数是同一个数值,返回
true
。 - 两个操作数别离是
+0
和-0
,返回true
。 - 下面的条件都不满足,返回
false
。
- 两个操作数有一个是
-
如果类型不是
Number
或BigInt
,则返回SameValueNonNumeric(x, y)
的返回值。- 断言:两者不是
Number
和BigInt
- 断言:两者类型雷同
- 如果
x
类型是Undefined
,返回true
。 - 如果
x
类型是Null
,返回true
。 - 如果
x
类型是String
,则必须x
和y
的所有码点序列完全一致才返回true
,否则返回false
。 - 如果
x
类型是Boolean
,则必须x
和y
同为true
或false
才返回true
,否则返回false
。 - 如果
x
类型是Symbol
,则必须x
和y
是同一个Symbol
才返回true
,否则返回false
。 - 如果
x
和y
是同一个对象,返回true
,否则返回false
。
- 断言:两者不是
相等操作符到这里就讲完了,双等号我没有认真看,因为我从来不用,也不倡议大家应用。如果你有趣味能够仔细阅读一下图片中的规范定义。
总结
这篇文章应该将双等号以外的绝大多数类型转换的状况都说分明了,而且是依据规范来讲的,还是比拟清晰的。其实整个思路理下来也不是十分的简单,所以有时候就是 Just Do It!
。心愿这篇文章给你带来帮忙,如果有谬误的中央,欢送斧正。