js数据类型

152 阅读13分钟

数据类型

ECMAScript有7种基本数据类型1种复杂数据类型(Object类型)

七种基本数据类型(原始值):UndefiendNullBooleanNumberStringSymbol(ES6新增)、bigInt

复杂数据类型:基本的对象、函数(Function)、数组(Array)和内置对象(Date等)。

类型判断

判断js数据类型通常有以下几种方法:

  1. typeof

(1) typeof 对于基本数据类型,除了 null 都可以显示正确的类型。

typeof 10 // 'number'
typeof '10' // 'string'
typeof undefined // 'undefined'
typeof a // a 没有声明,但是还会显示 undefined
typeof false // 'boolean'
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'

对于 null 来说,虽然它是基本数据类型,但是会显示 object,这是一个存在很久了的 Bug。

typeof null // 'null'

PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

(2) typeof 对于对象,除了函数都会显示 object

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

typeof 一般用来检测基本数据类型。

  1. instanceof
let bool = true;
let num = 1;
let str = 'abc';
let und= undefined;
let nul = null;
let arr = [1,2,3,4];
let obj = {};
let fun = function(){console.log('hello')};
let s1 = Symbol();

console.log(bool instanceof Boolean);// false
console.log(num instanceof Number);// false
console.log(str instanceof String);// false
console.log(und instanceof Object);// false
console.log(nul instanceof Object);// false
console.log(arr instanceof Array);// true
console.log(obj instanceof Object);// true
console.log(fun instanceof Function);// true
console.log(s1 instanceof Symbol);// false

从结果可以看出instanceof 一般用来检测复杂数据类型,如array、object、function,同时对于是使用new声明的类型,它还可以检测出多层继承关系。

js的继承都是采用原型链来继承的。比如objA instanceof A ,其实就是看objA的原型链上是否有A的原型,而A的原型上保留A的constructor属性。

  1. constructor
let bool = true;
let num = 1;
let str = 'abc';
let und= undefined;
let nul = null;
let arr = [1,2,3,4];
let obj = {};
let fun = function(){console.log('hello')};
let s1 = Symbol();

console.log(bool.constructor === Boolean);// true
console.log(num.constructor === Number);// true
console.log(str.constructor === String);// true
console.log(arr.constructor === Array);// true
console.log(obj.constructor === Object);// true
console.log(fun.constructor === Function);// true
console.log(s1.constructor === Symbol);//true

null、undefined没有construstor方法,因此constructor不能判断undefined和null。

但是这种方法是不安全的,因为contructor的指向是可以被改变。

  1. Object.property.toString.call()
console.log(Object.prototype.toString.call(bool));//[object Boolean]
console.log(Object.prototype.toString.call(num));//[object Number]
console.log(Object.prototype.toString.call(str));//[object String]
console.log(Object.prototype.toString.call(und));//[object Undefined]
console.log(Object.prototype.toString.call(nul));//[object Null]
console.log(Object.prototype.toString.call(arr));//[object Array]
console.log(Object.prototype.toString.call(obj));//[object Object]
console.log(Object.prototype.toString.call(fun));//[object Function]
console.log(Object.prototype.toString.call(s1)); //[object Symbol]

该方法可以相对较全的判断js的数据类型,至于在项目中使用哪个判断,还是要看使用场景。

类型转换

ToBoolean

转换规则 -- (同Boolean())

数据类型转换为true转换为false
Booleanturefalse
String非空字符串""(空字符串)
Number非0数值(包括无穷值)0、NaN
Object任意对象null
UndefinedN/A(不存在)undefined

从表中可以看出,在条件判断时,除了undefined, null, false, NAN, '', +0, -0, 其他所有值都转为true, 包括所有对象。

ToNumber

有3个函数可以将非数值转换为数值:Numner()parseInt()parseFloat()Number()是转型函数,可以用于任何数据类型, 后两个函数主要用于将字符串转换为数值。

转换规则 -- (同Number() 和一元➕号)

  1. Boolean值true转换为1, false转换为0
  2. Number值,直接返回
  3. null, 返回0
  4. undefined,返回NaN
  5. String值会忽略前导0,此外有以下规则:
    5.1. 如果字符串包含数值字符,包括树脂字符前面带加减号的情况,则转换为一个十进制数值。因此,Number("1")返回1,Number('123')返回123,Number("011")返回11。
    5.2. 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值。
    5.3. 如果字符串包含有效的十六进制如"0xf",则会转换为与该十六进制值对应的十进制整数值。
    5.4. 如果是空字符串,则返回0
    5.5. 如果不满足以上规则,则返回NaN ,如Number("123ab"), 返回NaN。
  6. Symbol值,报错(Uncaught TypeError: Cannot convert a Symbol value to a number
  7. 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。

⚠️一元加操作符与Number()函数遵循相同的转换规则

Number(null)  // 0
Number(undefined)  //NaN
Number(true)  //1
Number(false)  //0
Number("11")  //11
Number("1.1e+21") //1.1e+21
Number("abc")  //NaN
Number([])   // 0 先调用valueOf方法结果还是返回[], 然后调用toString()方法返回空字符串"", 最后Number("") => 0
Number([0])  // 0
Number([1])  // 1
Number(["abc"]) // NaN 先调用valueOf方法结果还是返回{}, 然后调用toString()方法返回空字符串"[object Object]", 最后Number("[object Object]") => NaN
Number({})  // NaN

parseInt()转换规则

  1. 忽略前导空格
  2. 从第一个非空格字符串开始转换。如果第一个字符不是数值字符、加号或减号,返回NaN。(这就意味着空字符串也返回NaN)
  3. 如果第一个字符是数值字符、加号或者减号,则继续依次检测每个字符,直到字符串末尾或者碰到非数值字符
  4. parseInt能识别不同的整数格式(十进制、八进制、十六进制),不同数值格式很容易混淆,因此parseInt接收第二个参数,用于指定进制数。
let num1 = parseInt("1234blue");  // 1234
let num2 = parseInt("");          // NaN
let num3 = parseInt("0xA");       // 10,解释为十六进制整数
let num4 = parseInt(22.5);        // 22
let num5 = parseInt("70");        // 70,解释为十进制值
let num6 = parseInt("0xf");       // 15,解释为十六进制整数
let num1 = parseInt("10", 2);   // 2,按二进制解析
let num2 = parseInt("10", 8);   // 8,按八进制解析
let num3 = parseInt("10", 10);  // 10,按十进制解析
let num4 = parseInt("10", 16);  // 16,按十六进制解析

parseFloat()转换规则

  1. 忽略前导0,十六进制数值始终返回0
  2. 只解析十进制值,不能指定进制数
  3. 从位置0开始检测每个字符,解析到字符串末尾或者解析到一个无效的浮点数值字符为止。(第一次出现的小数点有效,第二次出现的小数点无效,此时字符串的剩余字符都会被忽略,因此,"22.34.5"将转换成22.34)

ToString

有三种方式将一个值转换为字符串,toString()方法、String()函数以及给一个值加上空字符串,比如5 + "",返回"5"。

toString()

  1. 该方法只适用于Number、Boolean、对象和String,null和undefined没有toString()方法。

(1) 对于普通对象来说,除非自定定义,否则toString()返回Object.prototype.toString()的值,其他对象有自己的toString()方法则调用自己的该方法。

const a = {} 
console.log(a.toString()) // "[object Object]"

const b = { name: 'minminzhi' } 
console.log(b.toString()) // "[object Object]"

(2) 数组的默认toString()方法进行了重新定义,将所有单元字符字符串化以后再用','连接起来。

const a = [1,2,3]
console.log(a.toString()) //"1,2,3"

(3) 函数:function a(){} 转为字符串是"function a(){}"

  1. 在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,返回数值的十进制字符串表示。
let num = 10;
console.log(num.toString());     // "10"
console.log(num.toString(2));    // "1010"
console.log(num.toString(8));    // "12"
console.log(num.toString(10));   // "10"
console.log(num.toString(16));   // "a"

String()

  1. 如果值有toString()方法,则调用盖方法(不传参数)并返回结果
  2. 如果值是null,返回"null"
  3. 如果值为undefined, 返回"undefined"
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;

console.log(String(value1));  // "10"
console.log(String(value2));  // "true"
console.log(String(value3));  // "null"
console.log(String(value4));  // "undefined"

ToPrimitive

js引擎内部的抽象操作ToPrimitive有着这样的签名:

ToPrimitive(input [, PreferredType])

ToPrimitive(input [, PreferredType])是用来把input转换成原始值。其中 input是要转换的值,PreferredType为可选参数,只接受NumberString

转换规则

  • 如果input是原始值,不转换直接返回
  • 如果input是对象,则按照下面步骤进行转换
ToPrimitive(input [, PreferredType])

1.如果没有传入PreferredType参数,则让hint的值为'default'
2.否则,如果PreferredType值为String,则让hint的值为'string'
3.否则,如果PreferredType值为Number,则让hint的值为'number'
4.如果input对象有@@toPrimitive方法,则让exoticToPrim的值为这个方法,否则让exoticToPrim的值为undefined
5.如果exoticToPrim的值不为undefined,则
	a.让result的值为调用exoticToPrim后得到的值
	b.如果result是原值,则返回
	c.抛出TypeError错误
6.否则,如果hint的值为'default',则把hint的值重新赋为'number'
7.返回 OrdinaryToPrimitive(input,hint)

OrdinaryToPrimitive(input,hint)

1.如果hint的值为'string',则
	a.调用input对象的toString()方法,如果值是原值则返回
	b.否则,调用input对象的valueOf()方法,如果值是原值则返回
	c.否则,抛出TypeError错误
2.如果hint的值为'number',则
	a.调用input对象的valueOf()方法,如果值是原值则返回
	b.否则,调用input对象的toString()方法,如果值是原值则返回
	c.否则,抛出TypeError错误

当没有给ToPrimitive方法传类型时,通常的表现就像是传递了Number类型。但是在ES6中,用户是可以自定义@@toPrimitive方法从而进行重写这个行为。在本标准中, Symbol对象和Date对象已经默认定义了@@toPrimitive方法Date对象不传类型时,表现就像是传递了String类型。

总结一下:

  • 在没有改写或自定义@@toPrimitive方法的条件下,如果是Date求原值,则PreferredTypeString,其他均为Number
  • PreferredTypeString,则先调用toString(),结果不是原始值的话再调用valueOf(),还不是原始值的话则抛出错误;PreferredTypeNumber,则先调用valueOf()再调用toString()
  • 如果有Symbol.toPrimitive属性的话,会优先调用,它的优先级最高,同样只能return原始类型的值,否则会报错。
const obj = {
  toString () {
    console.log('toString')
    return {}
  },
  valueOf () {
    console.log('valueOf')
    return {}
  },
  [Symbol.toPrimitive] () {
    console.log('primitive')
    return 'primi'
  }
}

console.log(1 + obj) // 1primi
const obj = {
  toString () {
    console.log('toString')
    return {}
  },
  valueOf () {
    console.log('valueOf')
    return {}
  },
  [Symbol.toPrimitive] () {
    console.log('primitive')
    return {}
  }
}

console.log(1 + obj) // 报错 TypeError: Cannot convert object to primitive value

提到的@@toPrimitive方法是Well-Known Symbols中一个,可以理解为是一个方法名,提供给引擎去调用。

Date默认定义的方法是Date.prototype[Symbol.toPrimitive] Symbol默认定义的方法是Symbol.prototype[Symbol.toPrimitive]

用户可以重写上面的两种方法或者给其他对象新定义求原值的方法,用如下方式:

Array.prototype[Symbol.toPrimitive] = function(hint){
    switch(hint){
    	case 'number' :
    		return 123;
    	case 'string' :
    		return 'hello world!';
    	case 'default' : 
    		return 'default';
    	default :
    		throw new Error();
    } 
}

隐式转换

加号

加法运算符
先来看一下标准里是如何定义**加法运算符“+”**的:加法运算符是用来连接字符串或数字相加的。

再来看一下标准里是如何定义加法过程的:

AdditiveExpression : AdditiveExpression + MultiplicativeExpression

在加法的过程中,首先把加号左右两边进行了求原始值ToPrimitive()操作,然后如果两个原值只要有一个是String类型,就把两个原值都进行转化字符串ToString()操作,进行字符串拼接;否则把两个原值都进行转化数字ToNumber()操作,进行数字相加。

一共涉及了三个方法:ToPrimitive(),ToString(),ToNumber()。

一元运算符“+”
先来看一下标准里是如何定义一元运算符“+”的:一元运算符“+”是用来把目标转化成数字类型的。

再来看一下标准里是如何定义一元运算“+”过程的:

UnaryExpression : + UnaryExpression

在一元“+”运算过程中,把目标直接转化成数字类型。 一共涉及了一个方法:ToNumber()。

开始做题

题目1

[] + []  //返回 ""

过程解析: 

首先,进行ToPrimitive,两个都是Array对象,不是Date对象,所以以Number为转换标准,所以先调用valueOf(),结果还是[],不是原始值,所以继续调用toString(),结果是""原始值,将""回。

第二个[ ]过程是相同的,返回""。

加号两边结果都是String类型,所以进行字符串拼接,结果是""

题目2

[] + {} // 返回"[object Object]"

过程解析:

进行ToPrimitive,依然是以Number为转换标准。

[]的结果是""。

{}先调用valueOf(),结果是{},不是原始值,所以继续调用toString(),结果是"[object Object]",是原始值,将"[objectObject]"返回。

加号两边结果都是String类型,所以进行字符串拼接,结果是"[object Object]"

题目3

{} + []  // 返回 0

这道题按照上一题的步骤,讲道理的话,结果应该还是"[object Object]",但结果却如人意料——显示的答案是0!

这是什么原因呢?  

原来{} + []被解析成了{};+[],前面是一个空代码块被略过,剩下+[]就成了一元运算。[]的原值是"", 将""转化成Number结果是0

题目4

++[[]][+[]]+[+[]]  // 返回"10"

过程解析:

1. 先拆分成A:++[[]][+[]]和B:[+[]]
2. B式比较简单:+[]=> 0,  故[0]
3. A式转换成++[[]][0]也就是++[]
4. A式结果为1
5. 1 + [0], [0]的结果是"0"
故返回"10"

== 运算符的隐式转换

== 类型转换主要分两种情况,x,y 类型相同时和类型不相同时:

  1. ==两边类型相同时,没有类型转换,等同于===比较规则,值得注意的时NaN不与任何值相等,包括它自己,即NaN !== NaN
  2. ==两边类型不相同时,有以下规则:
    (1) x, y为nullundefined两者中一个,返回true
    (2) x, y为NumberString类型时,则转换为Number类型进行比较
    (3) 有Boolean类型时,Boolean转换为Number类型比较
    (4) 一个Object类型,一个String类型或者Number类型,先将Objetc类型进行原始值转换后(ToPrimitive),然后再按照上面的流程进行原始值比较
    (5) x, y都是对象时,比较的是地址

开始做题

题目1

var a = {
    valueOf: function() {
        return 1;
    };
    toString: function() {
        return '123';
    }
}

console.log(true == a) // true

过程解析:
1. 首先,x, y类型不同, x为Boolean类型,则进行ToNumber转换为1
2. y为Object类型,先对y进行toPrimitive转换,ToPrimitive(a, ?),没有指定转换类型,默认number类型。而后,ToPrimitive(a,Number)首先调用valueOf方法,返回1,得到原始类型1
3. 最后1==1, 返回true

题目2

[] == !{} // true

过程解析:
1. !运算符优先级高于==, 故先进行!运算
2. !{}运算结果为false, 结果变成[] == false比较
3. 等式右边y = ToNumber(false) = 0,结果变成 [] == 0
4. 等式左边 x = ToPrimitive([]), 首先调用valueOf方法,结果返回[], 不是原始值,接着调用toString方法,结果返回""。 此时变成"" == 05. 根据第(2)条转换规则,"" => ToNumber("") = 0, 最后变成0==0, 故返回true

经典面试题

下面代码中的 a 在什么情况下会打印1const a = ?
if(a==1 && a==2 && a==3) {
  console.log(1)
}

cosnt a = {
    value: 0,
    valueOf() {
        return ++ this.value
    }
}
或者
const a = {
    i: 1,
    toString() {
        return a.i++
    }
}

以const a = {
    i: 1,
    toString() {
        return a.i++
    }
}为例

原因:
1. 当执行a==1&&a==2&&a==3时,会从左到右一步一步解析,首先a==1, 会对a进行ToPrimitive转换
2. ToPrimitive(a, Number),会先调用valueOf方法,返回a本身,而非原始类型,故会调用toString方法
3. 因为toString被重写,所以会调用重写的toString方法,故返回1,注意这里是i++, 而不是++i,它会先返i,再将i+1。所以ToPrimitive(a, Number) = 1。也就是1==1, 此时i = 1+1 =2
4. 执行万a==1返回true,会执行a==2,同理,会调用ToPrimitive(a, Number),同上先调用valueOf方法,再调用toString方法,由于第一步i=2, 此时ToPrimitive(a, Number) = 2,也就是2 == 2, 此时i=2+1
5. 同上可以推导a==3也返回true。故最终结果a==1&&a==2&a==3返回true

关系运算符比较时

  1. 如果比较运算符两边都是数字类型,则直接比较大小
  2. 如果非数值进行比较时,则会将其转换为数字然后再比较
  3. 如果符号两侧的值都是字符串时,不会将其转换成数字进行比较,而是分别比较字符串中字符的Unicode编码
 3 > 4  // false
"2" > 10  // false
"2" > "10"  // true

参考:
www.jianshu.com/p/7cb41d109…
www.jianshu.com/p/ddc7f189d…
sinaad.github.io/xfe/2016/04…