众所周知, js 是一门弱类型或者说是动态语言。变量没有类型限制,能够随时赋予任意值。javascript
虽然变量的数据类型是不肯定的,可是各类运算符对数据类型是有要求的。若是运算符发现,运算值的类型与预期不符,就会自动转换类型。java
4 + '1' // '41'
上述代码中,数值 4
和字符串'1'
相加,结果为'41'
,在运算过程当中,JavaScript 将数值4
自动转换为了字符串'4'
,相似的转换在 JavaScript 中很常见,咱们有必要了解下背后的转换原理。express
js 中的数据类型分两大类:数组
总共 6 种数据类型(ES6 新增了 Symobal 类型,本文不讨论),相较于其余的语言,js 中的数据类型好像不足以表示全部数据类型。可是,因为 js 的数据类型具备动态性,所以没有再定义其余数据类型的必要了。浏览器
在类型转换过程当中简单数据类型又称为原始值(原始类型)函数
JavaScript 内置三个转型函数,可使用Number()
、String()
和Boolean()
,手动将各类类型的值,分别转换成数字、字符串或者布尔值。工具
其实把非数值转换为数值的函数除了Number()
,parseInt()
和parseFloat()
也常常用到。转型函数Number()
能够用于任何数据类型,然后面两个专门用于把字符串转换为数值。测试
true
和false
分别被转换为1
和0
null
值,返回0
undefined
,返回NaN
字符串eslint
0
"123"
转换为123
),前面的零会被忽略(例如"011"
转换为11
)"1.2"
,返回1.2
(前面的零会被忽略,例如"01.2"
转换为1.2
)"0xf"
,则将其转换为相同大小的十进制整数值Number(true) // 1 Number(false) // 0 Number(10) // 10 Number(null) // 0 Number(undefined) // NaN Number('') // 0 Number('10') // 10 Number('010') // 10 Number('1.2') // 1.2 Number('01.2') // 1.2 Number('0xf') // 15 Number('hello') // NaN
对象类型的转换稍微有点复杂:code
valueOf()
方法,若是返回原始类型的值,则直接对该值使用Number
函数,再也不进行后续步骤。valueOf()
方法返回对象,则调用对象自身的toString()
方法。若是toString()
方法返回原始类型的值,则直接对该值使用Number()
函数,再也不进行后续步骤。toString()
方法返回的是对象,则报错var obj = {a: 1} Number(obj) // NaN // 等同于下面步骤 if (typeof obj.valueOf() === 'object') { Number(obj.toString()) } else { Number(obj.valueOf()) }
上面代码中,首先调用valueOf()
,结果返回对象自己;继续调用toString()
,返回字符串[object Object]
,对此字符串调用Number()
,返回NaN
。
大多数状况下,对象的valueOf()
方法返回对象自身,因此通常会调用toString()
方法,而toString()
方法通常返回对象的类型字符串(例如[object Object]
),因此Number()
的参数是对象时,通常返回NaN
。
若是toString()
方法返回的不是原始类型的值,就会报错,咱们能够重写对象的toString()
方法:
var obj = { valueOf: function() { return {} }, toString: function() { return {} } } Number(obj) // TypeError: Cannot convert object to primitive value
数组类型的状况有点不同,对于空数组,转换为0
;对于只有一个成员(且该成员为可以被Number()
转换的值即转换结果不为NaN
)的数组,转换为该成员的值;其余状况下的数组,转换为NaN
。
Number([]) // 0 Number([10]) // 10 Number([null]) // 0 Number([[10]]) // 10
数组类型和通常对象的转换结果不同,主要是由于数组的toString()
方法内部重写了,直接调用不会返回[object Array]
,而是返回每一个成员(会先转换成字符串)拼接成的字符串,中间以逗号分隔。
[10].toString() // '10' [null].toString() // '' [1, 10].toString() // '1,10'
由于Number()
函数在转换字符串时比较复杂并且不太适用,所以在处理字符串转换为整数的时候更经常使用的是parseInt()
函数。parseInt()
函数在转换字符串时,更多的是看起其是否符合数值模式。它会忽略字符串前面的空格,直至找到第一个非空格字符。
parseInt()
就会返回NaN
;所以,用parseInt()
转换空字符串会返回NaN
(Number()
对空字符串返回0
)。parseInt()
会继续解析第二个字符,直到解析完全部后续字符或者遇到了一个非数字字符。例如,'123abc'
会被转换为1234
,由于'abc'
会被彻底忽略;'22.5'
会被转换为22
,由于小数点不是有效的数字字符。parseInt()
也可以识别出各类整数格式(十进制、八进制、十六进制)。若是字符串以'0x'
开头且后面是数字字符,就会当成一个十六进制整数;若是字符串以0
开头且后面是数字字符,则会将其当作八进制整数。parseInt('123abc') // 123 parseInt('') // NaN parseInt('a12') // NaN parseInt('0xa') // 10(十六进制数) parseInt('12.5') // 12 parseInt('070') // 56(八进制数)
上述代码中parseInt('070')
在浏览器中测试会返回10
,前面的零会被忽略掉。其实parseInt()
能够传入第二个参数,转换的基数(想要以什么进制来转换)
parseInt('0xaf') // 175 parseInt('af') // NaN // af 原本是有效的十六进制数值,若是指定第二个参数为16,使用十六进制来解析,那么能够省略前面的 0x parseInt('af', 16) // 175
若是不指定基数,parseInt()
会自行肯定如何解析输入的字符串,为了不没必要要的错误,应该老是传入第二个参数,明确指定基数。
通常状况下,解析的都是十进制数值,应该始终将 10 做为第二个参数(在语法检查工具 ESLint中,也会提示指定第二个参数)
与parseInt()
函数相似,parseFloat()
也是从第一个字符开始解析每一个字符,一直解析到最后一个字符。若是碰见一个无效的浮点数字字符,就会中止解析,输出结果。字符串中的第一个小数点是有效的,而第二个小数点就是无效的了,所以它后面的字符都会被忽略。例如,'12.34.5'
会被转换为12.34
。
parseFloat()
会忽略最前面的零,且它只能解析十进制值(没有第二个参数)。
parseFloat('123abc') // 123 parseFloat('0xa') // 0 parseFloat('12.3') // 12.3 parseFloat('12.3.4') // 12.3 parseFloat('012.3') // 12.3
Boolean()
函数能够将任意类型的值转为布尔值。至于返回的这个值是true
仍是false
,取决于要转换值的数据类型及其实际值。下表给出了各类数据类型及其对应的转换规则。
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | ""(空字符串) |
Number | 任何非零数字值(包括无穷大) | 0(包括-0和+0)和NaN |
Object | 任何对象 | null |
Undefined | - | undefined |
须要注意的是,全部空对象的转换结果都是true
,并且布尔对象表示的false
值(new Boolean(false)
)的转换结果也是true
。
Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true
String()
函数能够将任意类型的值转化成字符串,转换规则以下。
true
转换为字符串'true'
,false
转换为字符串'false'
undefined
,转为字符串'undefined'
null
,转为字符串'null'
String(123) // '123' String('abc') // 'abc' String(true) // 'true' String(undefined) // 'undefined' String(null) // 'null'
String()
函数的参数若是是对象,返回一个类型字符串;若是是数组,返回该数组的字符串形式。
String()
函数背后的转换规则,与Number()
函数基本相同,区别是互换了valueOf()
方法和toString()
方法的执行顺序。
toString()
方法。若是返回原始类型的值,则对该值使用String()
函数,再也不进行如下步骤。toString()
方法返回的是对象,再调用对象的valueOf()
方法。若是valueOf()
方法返回原始类型的值,则对该值使用String()
函数,再也不进行如下步骤。valueOf()
方法返回的是对象,就报错。String({name: 1}) // '[object Object]' // 等同于 String({name: 1}.toString()) String('[object Object]') // '[object Object]'
上面代码先调用对象的toString()
方法,会返回字符串'[object Object]'
,而后对该值使用String()
函数,不会再调用valueOf()
方法,输出'[object Object]'
。
若是toString()方
法和valueOf()
方法,返回的都是对象,就会报错,能够重写对象的这两个方法来验证:
var obj = { valueOf: function() { return {} }, toString: function() { return {} } } String(obj) // TypeError: Cannot convert object to primitive value
类型转换中一个难点就是自动转换(也称隐式转换),因为在一些操做符下其类型会自动转换,这使得 js 尤为灵活,但同时又难以理解。
JavaScript 会自动转换数据类型的状况主要有三种:
不一样类型的数据互相运算
1 == '1' // true 1 + 'a' // '1a'
条件控制语句的条件表达式为非布尔值类型
if ('a') { console.log('success') } // 'success'
对非数值类型的值使用一元运算符(+
和-
)
+{a: 1} // NaN -[1] // -1
自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。好比,某个位置预期为字符串,就调用String()
函数进行转换。若是该位置便可以是字符串,也多是数值,那么默认转为数值。
因为自动转换具备不肯定性,并且不易排错,建议在预期为布尔值、数值、字符串的地方,所有使用Boolean()
、Number()
和String()
函数进行显式转换。
JavaScript 遇到预期为布尔值的地方(好比if
语句的条件部分),就会将非布尔值自动转换为布尔值,自动调用Boolean()
函数,其转换规则在上面已经说过。
这些转换规则对于条件类语句很是重要,在运行过程当中,会自动执行相应的Boolean
转换。
var str = 'abc' if (str) { console.log('success') } // 'success' str && 'def' // 'def'
运行上面代码,就会输出字符串success
,由于字符串abc
被自动转换成了对应的Boolean
值(true)。因为存在这种自动执行的Boolean
转换,所以在条件类语句中知道是什么类型的变量很是重要。
常见的自动转换布尔值还有下面两种:
// 三目运算符 expression ? true : false // 双重取反 !!expression
上面两种写法,内部其实仍是调用的Boolean()
函数。
JavaScript 遇到预期为字符串的地方,就会将非字符串值自动转为字符串。字符串的自动转换,主要在字符串的加法运算,当一个值为字符串,另外一个值为非字符串时,非字符串会自动转换。
自动转换通常会调用非字符串的toString()
方法转换为字符串(null
和undefined
没有toString()
方法,调用String()
方法转换为'null'
和'undefined'
),转换完成获得相应的字符串值,而后就是执行加法运算,即字符串的拼接操做了。
'1' + 2 // '12' '1' + true // '1true' '1' + false // '1false' '1' + {} // '1[object Object]' // 数组的 toString 方法通过重写,返回每一个成员以逗号分隔拼接成的字符串 // 下面一行等同于, '1' + '', 结果为 '1' '1' + [] // '1' '1' + function (){} // '1function (){}' '1' + undefined // '1undefined' '1' + null // '1null'
上面代码须要注意的是字符串与函数的加法运算,会先调用函数的toString()
方法,它始终返回当前函数的代码(看起来就像源代码通常)。
(function (){}).toString() // 'function (){}' (function foo(a){ return a + b }).toString() // 'function foo(a){ return a + b }'
因此转换以后就是'1'
和'function (){}'
的拼接操做,结果为'1function (){}'
。
JavaScript 遇到预期为数值的地方,就会将非数值自动转换为数值,系统内部会自动调用Number()
函数。
除了加法运算符(+
)有可能(可能会是字符串拼接的状况)把自动转换,其余运算符都会把非数值自动转成数值。Number()
函数的转换规则前面已经说过,下面看看几个例子:
'3' - '2' // 1 '3' * '2' // 6 true - 1 // 0 false - 1 // -1 '1' - 1 // 0 '3' * [] // 0 false / '3' // 0 'a' - 1 // NaN null + 1 // 1 undefined + 1 // NaN
上面代码中须要注意的是,null
会被转换为0
,undefined
会被转换为NaN
NaN,即非数值,是一个特殊的数值,用于表示一个将要返回数值而未返回数值的状况。关于这个值,须要注意两点,第一是任何涉及到与 NaN 的运算操做,都会返回 NaN;第二是 NaN 不等于自身(即 NaN === NaN 为 false),检测 NaN 须要使用专用的 isNaN 函数。
==
在比较时,若是被比较的两个值类型不一样,那么系统会自动转换成相同类型再比较。
==
并非严格的比较,只是比较二者的数值。
在比较不一样的数据类型时,有下面几种状况:
false
转换为0
,true
转换为1
。valueOf()
方法或toString()
方法,用获得的基本类型值按照起那么的规则比较。true
(比较引用)NaN
,一概返回false
null
和undefined
比较,不会进行转换,直接返回true
下面看看几个例子:
false == 0 // true true == 1 // true true == 3 // false '5' == 5 // true var o = {name: 'foo'} o == 1 // false o == '[object Object]' // true,前面的 o 对象转换为基础类型值 '[object Object]' undefined == 0 // false null == 0 // false null == undefined // true 'NaN' == NaN // false 1 == NaN // false NaN == NaN // false
在开发时,在遇到不肯定是字符串仍是数值时,可能会使用==
来比较,这样就能够不用手动强制转换了。但其实这是偷懒的表现,仍是规规矩矩的先强制转换后再比较,这样代码看上去逻辑会更清晰(若是配置了语法检测工具eslint
,它的建议就是一直使用===
而不使用==
,避免自动转换)
前面提到了这么多转换规则,有不少都是调用的toString()
以后才继续执行的,咱们有必要总结下toString()
的返回值。
下面是各类类型的返回状况:
null
和undefined
没有toString()
方法,通常使用String()
,分别返回'null'
和'undefined'
'[object Object]'
,'[object Math]'
)function foo(){}
返回'function foo(){}'
(各浏览器返回可能有差别)join()
方法),例如:[1, 2, 3]
返回'1,2,3'
new Date('2018-10-01 12:12:12').toString()
返回'Mon Oct 01 2018 12:12:12 GMT+0800 (中国标准时间)'
。但其实在类型转换中Date
类型通常是调用valueOf()
,由于它的valueOf()
方法返回当前时间的数字值表示(即时间戳),返回结果已是基础类型值了,不会再调用toString()
方法了。最后,看几个有意思的类型转换例子
这个返回时间戳的方法相信你们都常常用到,这里+
运算符的规则和Number()
转型函数是相同的,对于对象首先会先调用它的valueOf()
方法,Date
类型的valueOf()
方法返回当前时间的数字值表示(即时间戳),返回结果为数值,不会继续下一步调用toSting()
方法,直接对当前返回值使用Number()
仍是返回时间戳
(!+[]+[]+![]).length = 9
xxx.length,粗略一看有length
属性的就字符串和数组了,因此前面的结果确定是这两个其中一种。这里是类型转换,不可能出现转换为数组的状况,那么只多是字符串,下面咱们分析一下。
首先拆开来看,就是!+[]
、[]
、![]
的相加操做
!+[]
会进行以下转换
[].toString() // '' +'' // 0 !0 // true
都转换完成后,就是true
、''
、false
的相加操做,结果为'truefalse'
,最后结果就是9
先说 [] + {} 。一个数组加一个对象。
[]
和 {}
的 valueOf()
都返回对象自身,因此都会调用 toString()
,最后的结果是字符串串接。[].toString()
返回空字符串,({}).toString()
返回'[object Object]'
。最后的结果就是'[object Object]'
。
而后说 {} + [] ,看上去应该和上面同样。可是 {}
除了表示一个对象以外,也能够表示一个空的block
。
在 [] + {} 中,[]
被解析为数组,所以后续的 +
被解析为加法运算符,而 {}
就解析为对象。但在 {} + []
中,开头的{}
被解析为空的 block
,随后的 +
被解析为正号运算符,最后的结果就是0
。即实际上成了:
{} // empty block +[] // 0
相信很多人在遇到类型转换尤为是自动转换的时候,都是靠猜的。如今看了这么多底层的转换规则,应该或多或少的对其都有了必定的了解了。