深入理解 JavaScript 数据类型转换

365 阅读12分钟

对于前端开发人员来说,可能问起 JS 数据类型,大家觉得都很简单,不就 七种 类型嘛: string, number, boolean, null, undefined, symbol, object。这个大家是都知道的,但是可能问的详细一些,有些地方咱们可能就不太清楚了。

JS 中的数据类型

JS 中共有七种内置的数据类型,分为 基本类型对象类型

基本类型

基本类型共有六种:

  • string: 字符串
  • numberr: 数字
  • boolean: 布尔值
  • symbol: 符号
  • null: 空值
  • undefined:未定义

注意:

  1. string, number, boolean, null, undefined 这五种类型称为 原始类型(Primitive), 表示不能再细分下去的类型。
  2. symbol 是 ES6 中新增的数据类型,它是独一无二的值,通过 Symbol 函数调用生成,它生成的值永远不会相等, 由于生成的 symbol 值为原始类型,并且 Symbol 函数不是一个构造器,所以不能通过 new Symbol() 的方式生成,还会报错 ‘Uncaught TypeError: Symbol is not a constructor’。
  3. null 和 undefined 是两个特殊的类型,他们的值唯一,就是其本身

对象类型

对象类型又叫引用类型,array 和 function 是对象的子类型。对象类型和基本类型不同的是他们的值存储的是引用地址(内存地址),不是真正的值,所以对象类型的值是可变的。

JS 弱类型语言

JS 为什么是 弱类型语言呢?是因为 JS 变量声明的时候不需要预先确定类型,值的类型就是变量的类型,值的类型变了,变量的类型就跟着变了。这就是说上一秒钟是 string 类型,下一秒钟就可能变成 number 类型或 boolean 类型或其他类型,这一过程中发生了强制类型转换。虽然 JS 的这种弱类型语言 不需要预先确定类型的特性给我们带来了便利,但是也会因为类型转换带来一些烦恼,所以必须要掌握类型转换的原理。

JS 的强制类型转换

JS 的强制类型转换规则主要就是一下三种:

  • Number 运算符转换
  • String 运算符转换
  • Boolean 运算符转换

1. Number 运算符转换规则:

  • null 转换为 0
  • undefined 转换为 NaN
  • true 转换为 1, false 转换为 0
  • 字符串转换时遵循 数字常量规则,转换失败返回 NaN
    • 字符串包含数值字符,包括数值前面带加、减号的情况,则转换为一个十进制数值:Number('11')返回11,Number('+11')返回 11, Number('-11')返回 -11, Number('011') 返回 11 (注意忽略了前面的 零);
    • 字符串包含有效的浮点数值,则会转换为相应的 浮点数值(同样,忽略前面的 零);
    • 如果字符串包含有效的十六进制格式,如:'0xa', 则会转换为与该十六进制对应的十进制数值;
    • 如果字符串包含有效的八进制格式,如:'0o11', 则会转换为与该八进制对应的十进制数值;
    • 如果是空字符串,则转换为 0;
    • 如果字符串包含除上述情况之外的其他字符,则转换为 NaN, 注意忽略空格,如:Number(' 22 ') 返回 22。
  • 对象类型转换时要先转换为 原始值,调用 ToPimitive 转换,请看下文的 ToPimitive

2. String 运算符转换:

  • null 转换为 'null'
  • undefined 转换为 'undefined'
  • true 转换为 'true', false 转换为 'false'
  • 数字转换时遵循通用规则,极大极小的数字使用指数形式
  • 对象类型转换时同样要先转换为 原始值,调用 ToPimitive 转换,请看下文的 ToPimitive
String(null)                       // 'null'
String(undefined)                 // 'undefined'
String(true)                      // 'true'
String(1)                         // '1'
String(-1)                        // '-1'
String(0)                         // '0'
String(-0)                       // '0'
String(Math.pow(1000,10))    // '1e+30'
String(Infinity)             // 'Infinity'
String(-Infinity)            // '-Infinity'
String({})                   //'[object Object]'
String([1,[2,3]])            //'1,2,3'
String(['koala',1])          // 'loala,1'

3. Boolean 运算符转换:

除了一下几种转换为 false, 其他的全部为 true

  • null

  • undefined

  • '' (空字符串)

  • 0 | +0 | -0 | NaN

    这些值以外的其他值,包括 空对象,空数组,转换结果都是 true,甚至连 false 对应的布尔对象 new Boolean(false) 也是 true

Boolean(undefined)      // false
Boolean(null)          // false
Boolean(0)            // false
Boolean(NaN)          // false
Boolean('')           // false

Boolean({})           // true
Boolean([])           // true
Boolean(new Boolean(false))       // true

ToPimitive(转换为原始值)

ToPimitive 只转换 对象类型(引用类型)的数据,因为 基本类型的数据不需要进行转换。ToPimitive 转换时接收两个参数,第一个参数是要转换的对象,第二个参数是要将改对象转换为 哪种 基本数据类型,第二个参数可以不给,会根据具体对象使用对应的默认值(Date对象转换时默认转换成 string 类型,其他的对象默认 转换成 number 类型)。

/**
* @obj 需要转换的对象
* @type 期望转换为的原始数据类型,可选
*/
ToPrimitive(obj,type)

type 不同值的说明:

  • type 为 string:
  1. 先调用 obj 的 toString 方法,如果为原始值,则return ,否则进行第二步;
  2. 调用 obj 的 valueOf 方法,如果为原始值,则 return, 否则抛出 Type Error 异常;
  • type 为 number:
  1. 先调用 obj 的 valueOf 方法, 如果为原始值,则return ,否则进行第二步;
  2. 调用 obj 的 toString 方法,如果为原始值,则 return, 否则抛出 Type Error 异常;
  • type 参数为空:
  1. 若对象为 Date, 则 type 被设置为 string;
  2. 其他对象,type 被设置为 number

toString

Object.prototype.toString()

toString() 方法返回一个表示对象的字符串。每个对象都有 toString() 方法,单对象被表示为文本值或当以期望字符串的方式引用对象时,该方法会自动调用。

valueOf

Object.prototype.valueOf()

valueOf() 放回指定对象的原始值。不同内置对象的valueOf实现:

  • String => 返回字符串值
  • Number => 返回数字值
  • Date => 返回一个数字,即时间戳,字符串中内容是依赖于具体实现的
  • Boolean => 返回Boolean的this值(true / false)
  • Object => 返回this
var str = new String('123');
console.log(str.valueOf());                //123

var num = new Number(123);
console.log(num.valueOf());               //123

var date = new Date();
console.log(date.valueOf());             //1526990889729

var bool = new Boolean('123');
console.log(bool.valueOf());             //true

var obj = new Object({valueOf:()=>{
    return 1
}})
console.log(obj.valueOf());              //1

JS 转换规则不同场景的应用

自动转换为字符串

  • 没有对象的前提下: 主要发生在字符串的 加法运算 时,字符串和 非字符串相加,后者转为 字符串
'2' + 1                         // '21'
'2' + true                     // "2true"
'2' + false                    // "2false"
'2' + undefined               // "2undefined"
'2' + null                    // "2null"
  • 当有对象且与对象+时候
//toString的对象
var obj2 = {
    toString:function(){
        return 'a'
    }
}
console.log('2'+obj2)
console.log(2 + obj2)
//输出结果都是 2a

//常规对象
var obj1 = {
   a:1,
   b:2
}
console.log('2' + obj1)
console.log(2 + obj1)
//输出结果都是 2[object Object]

//几种特殊对象
'2' + {}                  // "2[object Object]"
'2' + []                  // "2"
'2' + function (){}                 // "2function (){}"
'2' + ['koala',1]                    // 2koala,1

注意: '2'+obj2 和 '2'+obj1 中,由于 obj1, obj2 转换时没有指定类型,所以 type 值被指定为了默认值 number,然后按照上面的规则转换

自动转换为Number类型

  • 有加法运算,但无 string 类型时,优先转换为 number 类型
true + 0                   // 1
true + true               // 2
true + false              //1
  • 除了加法运算,其他的运算符都会自动转换成 number 类型
'5' - '2'                         // 3
'5' * '2'                        // 10
true - 1                         // 0
false - 1                        // -1
'1' - 1                          // 0
'5' * []                         // 0
false / '5'                      // 0
'abc' - 1                        // NaN
null + 1                         // 1
undefined + 1                    // NaN

//一元运算符(注意点)
+'abc'                      // NaN
-'abc'                      // NaN
+true                       // 1
-false                      // 0

注意 一元运算符 是将 值转换成 number 类型的数值

  • 抽象相等 == 使用 抽象相等 (双等号)时也是优先 转换为 number 类型
  1. 如果x,y均为number,直接比较值是否相等
1 == 2                      //false
  1. 如果存在对象,ToPrimitive() type为number进行转换,再进行后面比较
var obj1 = {
    valueOf:function(){
        return '1'
    }
}
1 == obj1                        //true
//obj1转为原始值,调用obj1.valueOf()
//返回原始值'1'
//'1'toNumber得到 1 然后比较 1 == 1
[] == ![]                        //true
//[]作为对象ToPrimitive得到 ''
//![]作为boolean转换得到0
//'' == 0
//转换为 0==0              //true
  1. 存在boolean,按照ToNumber将boolean转换为1或者0,再进行后面比较
//boolean 先转成number,按照上面的规则得到1
//3 == 1 false
//0 == 0 true
3 == true                     // false
'0' == false                  //true
  1. 如果x为string,y为number,x转成number进行比较
//'0' toNumber()得到 0
//0 == 0 true
'0' == 0                     //true

转换为 布尔 类型

  • 布尔比较时
  • if(), while() 等判断 或者 三元运算符 都要转换成 布尔值
if ( !undefined
  && !null
  && !0
  && !NaN
  && !''
) {
  console.log('true');
}                            // true

//下面两种情况也会转成布尔类型
expression ? true : false
!! expression

JS中的数据类型判断

知道了 JS 中的数据类型及类型转换的规则了,那么如何来判断数据类型呢?

通常有三种方式:typeof, instanceof, Object.prototype.toString()

  • typeof typeof 操作符可以判断一个值属于那种基本数据类型,它的返回值一定是一个字符串,值通常是这几项:sting, number, boolean, null, undefined, symbol, object, function,
typeof 'seymoe'         // 'string'
typeof true             // 'boolean'
typeof 10               // 'number'
typeof Symbol()         // 'symbol'
typeof null              // 'object' 无法判定是否为 null
typeof undefined         // 'undefined'

typeof {}               // 'object'
typeof []               // 'object'
typeof(() => {})        // 'function'

上面的代码的输出结果可以看出:

  1. null 的判断是有误的,这是一个历史遗留问题,知道这个结果就可以。
  2. typeof 操作符对于 对象类型及其子类型的判断,处理函数都会得到 'object' 的结果,所以要判断一个对象类型的值是 数组[] 或者对象{} 时, 不能得到想要的答案
  • instanceof instanceof 操作符 也可以 判断对象的类型,其原理是 检测 构造函数的 prototype 是否在 被检测的对象的原型链上。
[] instanceof Array                 // true
({}) instanceof Object             // true
(()=>{}) instanceof Function       // true

但是 instanceof 在判断一个值 是不是一个 对象(Object)时,可能有些问题:

let arr = []
let obj = {}
arr instanceof Array    // true
arr instanceof Object   // true
obj instanceof Object   // true

上面的代码中 Array 的实例 arr 的原型链上也有 Object,arr instanceof Object 也是 true,这是因为 Array 是 Object 的一个子类型。

  • Object.prototype.toString() Object.prototype.toString() 可以说是 判断 JS 数据类型的中级解决方法了,用法请看:
Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {})        // '[object Function]'
Object.prototype.toString.call('seymoe')        // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(new Date())      // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(new Set())       // '[object Set]'
Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
Object.prototype.toString.call(new Map())       // '[object Map]'
Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'

可以看出,任何类型的值都能返回正确的数据类型。有几点需要理解:

  1. 该方法本质就是依托Object.prototype.toString() 方法得到对象内部属性 [[Class]]
  2. 传入原始类型却能够判定出结果是因为对值进行了包装
  3. null 和 undefined 能够输出结果是内部实现有做处理

其他

parseInt & parseFloat

在字符串转 number 类型时,除了 Number() 函数外,还有 parseInt() 、parseFloat() 函数,下面具体说一下异同点:

parseInt()函数

parseInt() 函数更专注于字符串是否包含数值模式,parseInt() 函数也会忽略空格,从第一个非空格字符开始转换,如果第一个字符不是数值字符,加号、减号,则会返回 NaN。注意空字符串也返回 NaN(这里跟Number() 函数不同)。如果第一个字符是数值字符、加减号,则会向后依次检测每个字符,知道碰到非数值字符,或字符串末尾。如:parseInt('123abc') 返回 123。

如果字符串以 “0x” 开头,则会被解释为 16进制整数,并将其转换为 十进制 数值。现在已 “0o” 开头的字符串都被转为 0 ,之前被解释为了 八进制。

let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt("0o23")  // 0

此外,parseInt() 函数还可以传入第二个参数,用于指定底数(进制数),也就是用什么进制解析当前字符串。如解析一个16进制的数值字符串: let num = parseInt('0xaf', 16); // 175 事实上,如果传了第二个表示进制的参数 16, 那么 'ox' 是可以省略的: let num = ParseInt('af, 16); // 175

通过第二个参数,可以极大扩展目标字符串的解析进制数:

let num1 = parseInt("10",2);  // 2,按二进制解析
let num2 = parseInt("10",8);  // 8,按八进制解析
let num3 = parseInt("10",10);  // 10,按十进制解析
let num4 = parseInt("10",16);  // 16,按十六进制解析

因为不传第二个参数,表示让parseInt()函数自己决定如何解析,多数情况下,默认按十进制解析,为了避免出错,建议始终传入第二个参数。

parseFloat()函数

parseFloat()函数的工作方式跟parseInt()函数类似,不同的是

  • 第一次出现的小数点是有效的(注意只有第一个小数点有效,后面的无效从而不在解析),如: parseFloat('123.4.5'); // 123.4;
  • parseFloat() 函数只解析十进制值,不用传第二个参数;
  • 如果字符串表示整数:即没有小数点或者小数点后面只有一个 0 ,则返回整数
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); //31250000

NaN 相关

NaN 是一个全局对象属性,同时它是一个特殊的 number 类型值

typeof NaN
"number"

那么什么时候返回 NaN 呢

  • 非数字字符串解析成数字;
  • 算数运算符 与 不是数字的 或者 不能转成数字的 值一起使用;
  • 无穷大除以无穷大;
  • 给任意负数做开方运算;
Infinity / Infinity;   // 无穷大除以无穷大
Math.sqrt(-1);         // 给任意负数做开方运算
'a' - 1;               // 算数运算符与不是数字或无法转换为数字的操作数一起使用
'a' * 1;
'a' / 1;
parseInt('a');         // 字符串解析成数字
parseFloat('a');

Number('a');   //NaN
'abc' - 1   // NaN
undefined + 1 // NaN
//一元运算符(注意点)
+'abc' // NaN
-'abc' // NaN

关于toString和String

  • toString
  1. toSting() 方法可以将数据转成 字符串, 但是 null 和 undefined 不可以转换。
null.toString()
// Uncaught TypeError: Cannot read property 'toString' of null

undefined.toString()
// Uncaught TypeError: Cannot read property 'toString' of null
  1. toString() 可以传递参数 -- 数字,代表进制,表示要转换成多少进制的值对应的字符串

二进制:.toString(2);

八进制:.toString(8);

十进制:.toString(10);

十六进制:.toString(16);

  • String 如果值有toString()方法,则调用该方法(不传参数)并返回结果。

String 可以将 null 和 undefined 转换成字符串 "null" 和 "undefined",但是不能转换进制

String(null)
// "null"
String(undefined)
// "undefined"