浅析js中的类型转换

前言

众所周知, js 是一门弱类型或者说是动态语言。变量没有类型限制,能够随时赋予任意值。javascript

虽然变量的数据类型是不肯定的,可是各类运算符对数据类型是有要求的。若是运算符发现,运算值的类型与预期不符,就会自动转换类型。java

4 + '1' // '41'

上述代码中,数值 4和字符串'1'相加,结果为'41',在运算过程当中,JavaScript 将数值4自动转换为了字符串'4',相似的转换在 JavaScript 中很常见,咱们有必要了解下背后的转换原理。express

js数据类型

js 中的数据类型分两大类:数组

  • 简单数据类型(也称为基本数据类型):Undefined、Null 、Boolean、Number 和 String
  • 复杂数据类型:Object

总共 6 种数据类型(ES6 新增了 Symobal 类型,本文不讨论),相较于其余的语言,js 中的数据类型好像不足以表示全部数据类型。可是,因为 js 的数据类型具备动态性,所以没有再定义其余数据类型的必要了。浏览器

在类型转换过程当中简单数据类型又称为原始值(原始类型)函数

强制转换

JavaScript 内置三个转型函数,可使用Number()String()Boolean(),手动将各类类型的值,分别转换成数字、字符串或者布尔值。工具

Number()

其实把非数值转换为数值的函数除了Number()parseInt()parseFloat()也常常用到。转型函数Number()能够用于任何数据类型,然后面两个专门用于把字符串转换为数值。测试

原始类型值
  • 布尔值,true false 分别被转换为10
  • 数字值,原样返回
  • null 值,返回0
  • undefined,返回NaN
  • 字符串eslint

    • 若是是空字符串,返回0
    • 若是是字符串中只包含数字(包括前面带正负号的状况),则将其转换为十进制数值(例如"123"转换为123),前面的零会被忽略(例如"011"转换为11
    • 若是字符串中是有效的浮点数,如"1.2",返回1.2(前面的零会被忽略,例如"01.2"转换为1.2
    • 若是是有效的十六进制格式,如"0xf",则将其转换为相同大小的十进制整数值
    • 其余格式的字符串,则返回 NaN
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

  1. 调用对象自身的valueOf()方法,若是返回原始类型的值,则直接对该值使用Number函数,再也不进行后续步骤。
  2. 若是valueOf()方法返回对象,则调用对象自身的toString()方法。若是toString()方法返回原始类型的值,则直接对该值使用Number()函数,再也不进行后续步骤。
  3. 若是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'
parseInt()

由于Number()函数在转换字符串时比较复杂并且不太适用,所以在处理字符串转换为整数的时候更经常使用的是parseInt()函数。parseInt()函数在转换字符串时,更多的是看起其是否符合数值模式。它会忽略字符串前面的空格,直至找到第一个非空格字符。

  • 若是第一个字符不是数字字符或者负号,parseInt()就会返回NaN;所以,用parseInt()转换空字符串会返回NaNNumber()对空字符串返回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中,也会提示指定第二个参数)
parseFloat()

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

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

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()方法的执行顺序。

  1. 先调用对象自身的toString()方法。若是返回原始类型的值,则对该值使用String()函数,再也不进行如下步骤。
  2. 若是toString()方法返回的是对象,再调用对象的valueOf()方法。若是valueOf()方法返回原始类型的值,则对该值使用String()函数,再也不进行如下步骤。
  3. 若是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()函数进行显式转换。

ToBoolean

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()函数。

ToString

JavaScript 遇到预期为字符串的地方,就会将非字符串值自动转为字符串。字符串的自动转换,主要在字符串的加法运算,当一个值为字符串,另外一个值为非字符串时,非字符串会自动转换。

自动转换通常会调用非字符串的toString()方法转换为字符串(nullundefined没有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 (){}'

ToNumber

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会被转换为0undefined会被转换为NaN

NaN,即非数值,是一个特殊的数值,用于表示一个将要返回数值而未返回数值的状况。关于这个值,须要注意两点,第一是任何涉及到与 NaN 的运算操做,都会返回 NaN;第二是 NaN 不等于自身(即 NaN === NaN 为 false),检测 NaN 须要使用专用的 isNaN 函数。

相等操做符

==在比较时,若是被比较的两个值类型不一样,那么系统会自动转换成相同类型再比较。

==并非严格的比较,只是比较二者的数值。

在比较不一样的数据类型时,有下面几种状况:

  • 若是有一个操做数为布尔值,这在比较以前先将其转换为数值——false转换为0true转换为1
  • 若是有一个操做数为字符串,另外一个操做数为数值,在比较以前先将字符串转换为数值。
  • 若是有一个操做数为对象,另外一个操做数不是,则调用对象的valueOf()方法或toString()方法,用获得的基本类型值按照起那么的规则比较。
  • 若是两个操做符都为对象,这比较它们是否是同一个对象,若是都指向同一个对象,则为true(比较引用)
  • 只要有一个操做数为NaN,一概返回false
  • nullundefined比较,不会进行转换,直接返回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()以后才继续执行的,咱们有必要总结下toString()的返回值。

下面是各类类型的返回状况:

  • 数值返回当前数值的字符串形式
  • 字符串直接返回
  • 布尔值返回当前值的字符串形式
  • nullundefined没有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()方法了。

最后,看几个有意思的类型转换例子

  1. +new Date()

    这个返回时间戳的方法相信你们都常常用到,这里+运算符的规则和Number()转型函数是相同的,对于对象首先会先调用它的valueOf()方法,Date类型的valueOf()方法返回当前时间的数字值表示(即时间戳),返回结果为数值,不会继续下一步调用toSting()方法,直接对当前返回值使用Number()仍是返回时间戳

  2. (!+[]+[]+![]).length = 9

    xxx.length,粗略一看有length属性的就字符串和数组了,因此前面的结果确定是这两个其中一种。这里是类型转换,不可能出现转换为数组的状况,那么只多是字符串,下面咱们分析一下。

    首先拆开来看,就是!+[][]![]的相加操做

    !+[]会进行以下转换

    [].toString() // ''
    +'' // 0
    !0 // true

    都转换完成后,就是true''false的相加操做,结果为'truefalse',最后结果就是9

  3. [] + {} 和 {} + []

    先说 [] + {} 。一个数组加一个对象。

    []{}valueOf()都返回对象自身,因此都会调用 toString(),最后的结果是字符串串接。[].toString() 返回空字符串,({}).toString() 返回'[object Object]'。最后的结果就是'[object Object]'

    而后说 {} + [] ,看上去应该和上面同样。可是 {} 除了表示一个对象以外,也能够表示一个空的block

    在 [] + {} 中,[] 被解析为数组,所以后续的 + 被解析为加法运算符,而 {} 就解析为对象。但在 {} + [] 中,开头的{} 被解析为空的 block,随后的 + 被解析为正号运算符,最后的结果就是0。即实际上成了:

{} // empty block 
   +[] // 0

相信很多人在遇到类型转换尤为是自动转换的时候,都是靠猜的。如今看了这么多底层的转换规则,应该或多或少的对其都有了必定的了解了。

相关文章
相关标签/搜索