We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
应该将JavaScript中的类型转换区分为:显式强制类型转换(发生在编译阶段)、隐式强制类型转换(发生在运行时)
强制类型转换总是返回基本类型值,不会返回对象或函数。为基本类型值(除 object )封装一个相应类型的对象,并非严格意义上的强制类型转换
object
var a = 42 var b = a + '' // 隐式强制类型转换 var c = String(a) // 显式强制类型转换
除了字面意思意外,两种转换在行为特征上也有一定差别
ToString
基本类型值的字符串化规则为:
null 转化为 'null'
null
'null'
undefined 转化为 'undefined'
undefined
'undefined'
true 转化为 'true'
true
'true'
数字遵循通用规则,不过极大或极小的数字会使用指数形式
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 a.toString() // '1.07e+21'
对于普通对象来说,除非自行定义,否则会调用 Object.prototype.toString() 返回内部属性 [[Class]] 的值
Object.prototype.toString()
[[Class]]
数组的 toString() 方法重新定义过
toString()
[1, 2, 3].toString() // '1,2,3'
工具函数 JSON.stringify() 在将JSON对象序列化为字符串时也会调用 toString() ,但是它并非是严格意义上的强制类型转换。对于大多数简单值来说,JSON.stringify() 和 toString() 效果相同
JSON.stringify()
JSON.stringify('aa') // ""aa"" JSON.stringify(42) // "42" JSON.stringify(null) // "null" JSON.stringify(true) // "true"
所有安全的JSON值(JSON-safe)都可以用 JSON.stringify() 字符串化。对于不安全的JSON值( undefined function symbol 循环引用的对象 ):
undefined function symbol 循环引用的对象
在对象中遇到 undefined function symbol 会自动忽略,在数组中遇到则返回 null
undefined function symbol
JSON.stringify(undefined) // undefined JSON.stringify(() => {}) // undefined JSON.stringify([1, undefined, () => {}, 4]) // "[1,null,null,4]" JSON.stringify({ a: 1, b: () => {}, c: undefined }) // "{"a":1}"
对包含循环引用的对象,则会报错
如果要对含有非法JSON值的对象做字符串化,或者对象中的某些值无法被序列化时,需要定义 toJSON() 方法来返回一个安全的JSON值,JSON.stringify() 会先调用该方法并用它的返回值做序列化操作(toJSON() 应该返回一个可以被字符串化的安全的JSON值,而不是返回一个字符串)
toJSON()
var o = {} var a = { b: 42, c: o, d: () => {} } // 创建一个循环引用 o.e = a JSON.stringify(a) // TypeError: Converting circular structure to JSON a.toJSON = function() { return { b: this.b } } JSON.stringify(a) // "{"b":42}"
JSON.stringify() 可以传递两个可选参数
replacer :如果是数组,必须是字符串数组,包含需要序列化的属性名,其余属性会被忽略;如果是函数,会对对象本身调用一次,然后对对象中的每个属性各调用一次。如果要忽略某个键就返回 undefined ,否则返回指定的值。同时,如果遇到键对应的值也是对象,则递归调用
replacer
var a = { b: 42, c: '42', d: [1, 2, 3], e: { b: 1, c: 2 } } JSON.stringify(a, ['b', 'c']) // "{"b":42,"c":"42"}" JSON.stringify(a, function (k, v) { if (k !== 'c') return v }) // "{"b":42,"d":[1,2,3],"e":{"b":1}}"
space :用来指定缩进格式,为整数时指定每一级缩进的字符数,为字符串时前十个字符被用于每一级的缩进
space
var a = { b: 42, c: '42', d: [1, 2, 3], } JSON.stringify(a, null, 3) // "{ // "b": 42, // "c": "42", // "d": [ // 1, // 2, // 3 // ] // }" JSON.stringify(a, null, '-----') // "{ // -----"b": 42, // -----"c": "42", // -----"d": [ // ----------1, // ----------2, // ----------3 // -----] // }"
ToNumber
如果需要将非数字值当做数字使用,遵循以下规则:
1
false
0
NaN
ToPrimitive
为了将值转化为相应的基本类型值,首先会去检查该值是否有 valueOf() 方法,如果有且返回基本类型值,会使用该基本类型值进行强制类型转换。如果没有则使用 toString() 的返回值来进行强制类型转换。如果这两种方式都不返回基本类型值,会产生 TypeError 错误
valueOf()
TypeError
使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null ,不存在 valueOf() 和 toString() 方法,因此无法进行强制类型转换
Object.create(null)
[[Prototype]]
var a = { valueOf: () => '42', } var b = { toString: () => '42', } var c = [4, 2] c.toString = function () { return this.join('') } Number(a) // 42 Number(b) // 42 Number(c) // 42 Number('') // 0 Number([]) // 0 Number(['abc']) // NaN
ToBoolean
Javascript规范具体定义了一小撮可以被强制类型转换为 false 的值,其余的则都是 true
假值列表:
+0 -0 NaN
''
字符串和数字的互相转换是通过 String() 和 Number() 两个内建函数来实现的
String()
Number()
var a = 42 var b = String(a) // '42' var c = '3.14' var d = Number(c) // 3.14 // 注意这里没有new关键字,不会创建封装对象
一些其他的方式:
+ :
+
var a = 42 var b = a.toString() // '42' var c = '3.14' var d = +c // 3.14 // 不推荐用一元运算符来显式强制类型转换,会导致代码难以理解 // 同时 + 还可以将日期对象转换为时间戳,同样不推荐 var e = new Date('Mon, 18 Aug 2014 08:53:06 CDT') +e // 1408369986000
~:位操作符会通过抽象操作 ToInt32 强制操作数使用32位格式,位非 ~x 基本等于 -(x+1) 。对于正数,位反后拿到的是补码,需要转换为原码。对于负数,先取补码再位反。(MDN例子)
~
ToInt32
~x
-(x+1)
补码的存在是为了正确计算负数的加法(借助溢出),以8位有符号位数字举例 正数的补码是其本身,负数的补码符号位不变,其余按位取反并+1 补码转换为原码同样是符号位不变,其余按位取反并+1 原码 补码 1 + (-2) 00000001+100000101000011 = -3 00000001+1111111011111111 = -1 1 + (-1) 00000001+100000011000010 = -2 00000001+1111111100000000 = 0(溢出8位,从0开始)
补码的存在是为了正确计算负数的加法(借助溢出),以8位有符号位数字举例
正数的补码是其本身,负数的补码符号位不变,其余按位取反并+1
补码转换为原码同样是符号位不变,其余按位取反并+1
在 -(x+1) 中唯一能得到 0 的 x 值是 -1,这是个哨位值,通常被赋予特殊含义,例如字符串 indexOf() 方法在没有找到相应字符串时返回 -1
x
-1
indexOf()
var a = 'Hello World' if (a.indexOf('lo') >= 0) { // 写法不好,抽象渗漏(暴露底层实现细节) } if (a.indexOf('lo') != -1) { // 写法不好,抽象渗漏(暴露底层实现细节) } if (~a.indexOf('lo')) { // 将结果强制类型转换为真假值! }
解析字符串中的数字,和将字符串强制类型转换为数字是有区别的:解析允许字符串中含有非数字字符,而转换不允许
var a = '42' var b = '42px' Number(a) // 42 parseInt(a) // 42 Number(b) // NaN parseInt(b) // 42
ES5之前,parseInt() 如果不传递第二个参数指定进制,会自行根据字符串的第一个字符来决定进制。除此之外,不要传递非字符串的其他参数,如果传入参数非字符串,会先隐式转换为字符串
parseInt()
parseInt(1 / 0, 19) // parseInt('Infinity', 19) 第一个字符为'I',以19位基数时值为18,第二个字符不是有效数字,解析结束 parseInt(0.000008) // 0 parseInt(0.0000008) // 8 '8e-7' parseInt(false, 16) // 250 'fa' = 250 parseInt(parseInt, 16) // 15 'f' 来自于function () {...} parseInt('0x10') // 16 parseInit('103', 2) // 2 到3停止,因为3不是有效的二进制数字
Boolean() (不带new)是显式的 ToBoolean 强制类型转换
Boolean()
var a = '0' var b = [] var c = {} var d = '' var e = 0 var f = null var g Boolean(a) // true Boolean(b) // true Boolean(c) // true Boolean(d) // false Boolean(e) // false Boolean(f) // false Boolean(g) // false
但这种方式并不常用,一元运算符 ! 也可以将值显式强制类型转换为布尔值,因为 ! 还会反转真假值,因此最常用的方式是 !! ,在 if 语句中,会自动隐式的进行 ToBoolean 转换,建议用显式的方式让代码更易理解
!
!!
if
因人而异,对于自己来说不够明显的强制类型转换都可以称作隐式强制类型转换,会导致代码晦涩难懂。但从另一个角度来看,也可以减少代码的冗余,让代码更简洁
根据ES5规范,如果 + 的某个操作数是字符串或者能够通过以下步骤转换为字符串,则进行拼接操作:
[[DefaultValue]]
否则执行数字加法
var a = '42' var b = '0' a + b // '420' var c = 42 var d = 0 c + d // 42 var e = [1, 2] var f = [3, 4] e + f // '1,23,4' 对数组进行ToPrimitive抽象操作,发现valueOf()方法不能返回基本类型值,转而使用toString()
通过 + 空字符串来隐式强制类型转换数字为字符串的场景非常多见,但和显式强制类型转换略有不同:
a + ''
a
String(a)
var a = { valueOf: () => 42, toString: () => 4 } a + '' // '42' String(a) // '4'
相应的,- * / 都是数字运算符,因此会将操作数强制类型转化为数字。
- * /
var a = 1 var b = '2' b - a // 1 a * b // 2 b / a // 2 var c = [3] var d = [1] c - d // 2 c和d首先被转化为字符串,然后再转化为数字
将某些复杂的布尔逻辑转化为数字加法的时候可以派上大用场,但是情况非常罕见
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) // true onlyOne(b, a, b, b, b, b) // true
if()
for(..; ..; ..)
while()
do..while()
?:
||
&&
对于javascript中的逻辑运算符(实际上更像是操作数选择器运算符),并不是返回一个布尔值,而是返回两个操作数中的一个。 || 和 && 首先对第一个操作数进行 ToBoolean 抽象操作,然后再执行条件判断。对于 || ,如果第一个操作数判断结果为 true 则返回第一个操作数的值,为 false 则返回第二个操作数的值。而对于 && ,如果第一个操作数判断结果为 true 就返回第二个操作数的值,为 false 则返回第一个操作数的值
之所以我们在 if 或其他条件判断语句中可以用 && 和 ||,是因为这些语句本身会做隐式强制类型转换
常见的误区:“ == 检查值是否相等,=== 检查值和类型是否相等”。正确的解释是:“ == 允许在相等比较中进行强制类型转换,而 === 不允许。”延伸出来的性能结论:实际上 == 比 === 在比较过程中做的事情更多(相比误区说法),所以性能上确实会慢上一点点(可以忽略不计)
==
===
ES5规范如下:
+0
-0
Type(x)
Type(y)
x == ToNumber(y)
ToNumber(x) == y
x == ToPrimitive(y)
更改对象原型
Number.prototype.valueOf = () => 3 new Number(2) == 3 // true // 因为数字对象拆封涉及到ToPrimitive操作
假值的相等比较
false == 0 // true // false优先转换为0,即 0 == 0 false == '0' // true // false优先转换为0,即 0 == '0',两个操作数分别是数字和字符串,转换为 0 == Number('0') false == '' // true // false优先转换为0,即 0 == '',两个操作数分别是数字和字符串,转换为 0 == Number('') false == [] // true // false优先转换为0,[]通过ToPrimitive转换为'' [] == ![] // true // 右侧进行了布尔值的显式强制类型转换,即 [] == false
总结:
0 '' []
根据ES5规范,首先对比较双方调用 ToPrimitive 操作
这里原本书中的例子有问题
var a = 42 var b = '43' a < b // true (42 < 43) var a = ['42'] var b = ['043'] a < b // false ('42' < '043',在字母顺序上,0小于4)
需要注意,根据规范,a <= b 会被处理成 b < a 再将结果反转。所以实际上JavaScript中 <= 是不大于的意思(而不是通常理解的小于等于),>= 同理。这会产生一些悖论:
a <= b
b < a
<=
>=
var a = {} var b = {} a < b // false 都是 '[object object]' a == b // false 指向地址不是一致的! a > b // false a <= b // true a >= b // true
相等比较有严格相等,但是关系比较却没有严格关系比较,想要避免在关系比较中出现隐式强制类型转换,只能确保比较两者类型相同,别无他法
The text was updated successfully, but these errors were encountered:
No branches or pull requests
值类型转换
应该将JavaScript中的类型转换区分为:显式强制类型转换(发生在编译阶段)、隐式强制类型转换(发生在运行时)
强制类型转换总是返回基本类型值,不会返回对象或函数。为基本类型值(除
object
)封装一个相应类型的对象,并非严格意义上的强制类型转换除了字面意思意外,两种转换在行为特征上也有一定差别
抽象值操作
ToString
基本类型值的字符串化规则为:
null
转化为'null'
undefined
转化为'undefined'
true
转化为'true'
数字遵循通用规则,不过极大或极小的数字会使用指数形式
对于普通对象来说,除非自行定义,否则会调用
Object.prototype.toString()
返回内部属性[[Class]]
的值数组的
toString()
方法重新定义过工具函数
JSON.stringify()
在将JSON对象序列化为字符串时也会调用toString()
,但是它并非是严格意义上的强制类型转换。对于大多数简单值来说,JSON.stringify()
和toString()
效果相同所有安全的JSON值(JSON-safe)都可以用
JSON.stringify()
字符串化。对于不安全的JSON值(undefined function symbol 循环引用的对象
):在对象中遇到
undefined function symbol
会自动忽略,在数组中遇到则返回null
对包含循环引用的对象,则会报错
如果要对含有非法JSON值的对象做字符串化,或者对象中的某些值无法被序列化时,需要定义
toJSON()
方法来返回一个安全的JSON值,JSON.stringify()
会先调用该方法并用它的返回值做序列化操作(toJSON()
应该返回一个可以被字符串化的安全的JSON值,而不是返回一个字符串)JSON.stringify()
可以传递两个可选参数replacer
:如果是数组,必须是字符串数组,包含需要序列化的属性名,其余属性会被忽略;如果是函数,会对对象本身调用一次,然后对对象中的每个属性各调用一次。如果要忽略某个键就返回undefined
,否则返回指定的值。同时,如果遇到键对应的值也是对象,则递归调用space
:用来指定缩进格式,为整数时指定每一级缩进的字符数,为字符串时前十个字符被用于每一级的缩进ToNumber
如果需要将非数字值当做数字使用,遵循以下规则:
true
转化为1
false
转化为0
undefined
转化为NaN
null
转化为0
NaN
(报错?)ToPrimitive
为了将值转化为相应的基本类型值,首先会去检查该值是否有
valueOf()
方法,如果有且返回基本类型值,会使用该基本类型值进行强制类型转换。如果没有则使用toString()
的返回值来进行强制类型转换。如果这两种方式都不返回基本类型值,会产生TypeError
错误ToBoolean
Javascript规范具体定义了一小撮可以被强制类型转换为
false
的值,其余的则都是true
假值列表:
undefined
null
false
+0 -0 NaN
''
显式强制类型转换
字符串和数字
字符串和数字的互相转换是通过
String()
和Number()
两个内建函数来实现的一些其他的方式:
+
:~
:位操作符会通过抽象操作ToInt32
强制操作数使用32位格式,位非~x
基本等于-(x+1)
。对于正数,位反后拿到的是补码,需要转换为原码。对于负数,先取补码再位反。(MDN例子)在
-(x+1)
中唯一能得到0
的x
值是-1
,这是个哨位值,通常被赋予特殊含义,例如字符串indexOf()
方法在没有找到相应字符串时返回-1
显式解析数字字符串
解析字符串中的数字,和将字符串强制类型转换为数字是有区别的:解析允许字符串中含有非数字字符,而转换不允许
ES5之前,
parseInt()
如果不传递第二个参数指定进制,会自行根据字符串的第一个字符来决定进制。除此之外,不要传递非字符串的其他参数,如果传入参数非字符串,会先隐式转换为字符串显式转换为布尔值
Boolean()
(不带new)是显式的ToBoolean
强制类型转换但这种方式并不常用,一元运算符
!
也可以将值显式强制类型转换为布尔值,因为!
还会反转真假值,因此最常用的方式是!!
,在if
语句中,会自动隐式的进行ToBoolean
转换,建议用显式的方式让代码更易理解隐式强制类型转换
因人而异,对于自己来说不够明显的强制类型转换都可以称作隐式强制类型转换,会导致代码晦涩难懂。但从另一个角度来看,也可以减少代码的冗余,让代码更简洁
字符串和数字之间的隐式强制类型转换
根据ES5规范,如果
+
的某个操作数是字符串或者能够通过以下步骤转换为字符串,则进行拼接操作:ToPrimitive
抽象操作[[DefaultValue]]
否则执行数字加法
通过
+
空字符串来隐式强制类型转换数字为字符串的场景非常多见,但和显式强制类型转换略有不同:a + ''
依据ToPrimitive
抽象操作规则,先对a
调用valueOf()
方法,然后通过ToString
抽象操作将返回值转换为字符串String(a)
则是直接调用ToString
抽象操作相应的,
- * /
都是数字运算符,因此会将操作数强制类型转化为数字。布尔值到数字的隐式强制类型转换
将某些复杂的布尔逻辑转化为数字加法的时候可以派上大用场,但是情况非常罕见
隐式强制类型转换为布尔值
if()
语句中的条件判断表达式for(..; ..; ..)
语句中的条件判断表达式(第二个)while()
和do..while()
循环中的条件判断表达式?:
语句中的条件判断表达式||
和&&
的左操作数对于javascript中的逻辑运算符(实际上更像是操作数选择器运算符),并不是返回一个布尔值,而是返回两个操作数中的一个。
||
和&&
首先对第一个操作数进行ToBoolean
抽象操作,然后再执行条件判断。对于||
,如果第一个操作数判断结果为true
则返回第一个操作数的值,为false
则返回第二个操作数的值。而对于&&
,如果第一个操作数判断结果为true
就返回第二个操作数的值,为false
则返回第一个操作数的值宽松相等和严格相等
常见的误区:“
==
检查值是否相等,===
检查值和类型是否相等”。正确的解释是:“==
允许在相等比较中进行强制类型转换,而===
不允许。”延伸出来的性能结论:实际上==
比===
在比较过程中做的事情更多(相比误区说法),所以性能上确实会慢上一点点(可以忽略不计)抽象相等
ES5规范如下:
NaN
不等于NaN
,+0
等于-0
)==
在比较两个不同类型的值时,会发生隐式强制类型转换,将其中之一或两者都转换为相同类型再比较,转换规则如下:Type(x)
是数字,Type(y)
是字符串,则返回x == ToNumber(y)
的结果,反之亦然Type(x)
是布尔值,则返回ToNumber(x) == y
(Type(y)
亦然),因此不要使用布尔值的宽松相等,会和预期完全不符null
和undefined
在比较中可以相互强制类型转换,两者等价Type(x)
是字符串或数字,Type(y)
对象,则返回x == ToPrimitive(y)
的结果,反之亦然比较少见的情况
更改对象原型
假值的相等比较
总结:
==
0 '' []
,尽量不要使用==
==
(个人理解)抽象关系比较
根据ES5规范,首先对比较双方调用
ToPrimitive
操作ToNumber
将比较双方强制类型转换为数字比较需要注意,根据规范,
a <= b
会被处理成b < a
再将结果反转。所以实际上JavaScript中<=
是不大于的意思(而不是通常理解的小于等于),>=
同理。这会产生一些悖论:相等比较有严格相等,但是关系比较却没有严格关系比较,想要避免在关系比较中出现隐式强制类型转换,只能确保比较两者类型相同,别无他法
The text was updated successfully, but these errors were encountered: