数据类型
ECMAScript有7种基本数据类型和1种复杂数据类型(Object类型)。
七种基本数据类型(原始值):Undefiend
、Null
、Boolean
、Number
、String
和Symbol
(ES6新增)、bigInt
。
复杂数据类型:基本的对象、函数(Function)、数组(Array)和内置对象(Date等)。
类型判断
判断js数据类型通常有以下几种方法:
- 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
一般用来检测基本数据类型。
- 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属性。
- 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
的指向是可以被改变。
- 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 |
---|---|---|
Boolean | ture | false |
String | 非空字符串 | ""(空字符串) |
Number | 非0数值(包括无穷值) | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A(不存在) | undefined |
从表中可以看出,在条件判断时,除了undefined
, null
, false
, NAN
, ''
, +0
, -0
, 其他所有值都转为true
, 包括所有对象。
ToNumber
有3个函数可以将非数值转换为数值:Numner()
、parseInt()
和parseFloat()
。Number()
是转型函数,可以用于任何数据类型, 后两个函数主要用于将字符串转换为数值。
转换规则 -- (同Number() 和一元➕号)
Boolean值
,true
转换为1,false
转换为0Number值
,直接返回null
, 返回0undefined
,返回NaNString值
,会忽略前导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。Symbol值
,报错(Uncaught TypeError: Cannot convert a Symbol value to a number
)- 对象,调用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()转换规则
- 忽略前导空格
- 从第一个非空格字符串开始转换。如果第一个字符不是数值字符、加号或减号,返回NaN。(这就意味着空字符串也返回NaN)
- 如果第一个字符是数值字符、加号或者减号,则继续依次检测每个字符,直到字符串末尾或者碰到非数值字符
- 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()转换规则
- 忽略前导0,十六进制数值始终返回0
- 只解析十进制值,不能指定进制数
- 从位置0开始检测每个字符,解析到字符串末尾或者解析到一个无效的浮点数值字符为止。(第一次出现的小数点有效,第二次出现的小数点无效,此时字符串的剩余字符都会被忽略,因此,"22.34.5"将转换成22.34)
ToString
有三种方式将一个值转换为字符串,toString()方法、String()函数以及给一个值加上空字符串,比如5 + "",返回"5"。
toString()
- 该方法只适用于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(){}"
- 在对数值调用这个方法时,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()
- 如果值有toString()方法,则调用盖方法(不传参数)并返回结果
- 如果值是null,返回"null"
- 如果值为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
为可选参数,只接受Number
或String
。
转换规则
- 如果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
求原值,则PreferredType
是String
,其他均为Number
。 PreferredType
是String
,则先调用toString()
,结果不是原始值的话再调用valueOf()
,还不是原始值的话则抛出错误;PreferredType
是Number
,则先调用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 类型相同时和类型不相同时:
==
两边类型相同时,没有类型转换,等同于===
比较规则,值得注意的时NaN不与任何值相等,包括它自己,即NaN !== NaN
==
两边类型不相同时,有以下规则:
(1) x, y为null
、undefined
两者中一个,返回true
(2) x, y为Number
和String
类型时,则转换为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方法,结果返回""。 此时变成"" == 0。
5. 根据第(2)条转换规则,"" => ToNumber("") = 0, 最后变成0==0, 故返回true
经典面试题
下面代码中的 a 在什么情况下会打印1?
const 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
关系运算符比较时
- 如果比较运算符两边都是数字类型,则直接比较大小
- 如果非数值进行比较时,则会将其转换为数字然后再比较
- 如果符号两侧的值都是字符串时,不会将其转换成数字进行比较,而是分别比较字符串中字符的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…