深入了解JS 数据类型
由于JavaScript
是弱类型语言,而且JavaScript
声明变量的时候并没有预先确定的类型,变量的类型就是其值的类型,也就是说「变量当前的类型由其值所决定」,夸张点说上一秒是String
,下一秒可能就是个Number
类型了,这个过程可能就进行了某些操作发生了强制类型转换。虽然弱类型的这种「不需要预先确定类型」的特性给我们带来了便利,同时也会给我们带来困扰,为了能充分利用该特性就必须掌握类型转换的原理。本文我们将深入了解JavaScript
的类型机制。
JS 类型分类
JS
内置数据类型有 8 种类型,分别是:undefined
、Null
、Boolean
、Number
、String
、BigInt
、Symbol
、Object
。
其中又可分为「基础类型」和「引用类型」。
- 「基础类型」:
undefined
、Null
、Boolean
、Number
、String
、BigInt
、Symbol
- 「引用类型」:统称为
Object
类型。细分的话,有:Object
类型、Array
类型、Date
类型、RegExp
类型、Function
类型等。
依据「存储方式」不同,数据类型大致可以分成两类:
- 「基础类型」存储在「栈内存」,被引用或拷贝时,会创建一个完全相等的变量。
- 「引用类型」存储在「堆内存」,在「栈内存」存储的是地址,多个引用指向同一个内存地址。
可以通过以下栗子加深理解:
const obj1 = { name: 'obj1', id: '123' } const obj2 = obj1; console.log(obj1.name); // obj1 obj2.name = 'obj2'; console.log(obj1.name); // obj2 console.log(obj2.name); // obj2
当obj2
的name
被修改后,obj1
的name
也随之改变,这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。
JS 类型转换
ToPrimitive
string
、number
、boolean
和 null
undefined
这五种类型统称为「原始类型」(Primitive
),表示不能再细分下去的基本类型。
ToPrimitive
对原始类型不发生转换处理,只「针对引用类型(object)的」,其目的是将引用类型(object
)转换为非对象类型,也就是原始类型。
toPrimitive(obj: any, preferedType?: 'string' |'number')
ToPrimitive
运算符「接受一个值,和一个可选的期望类型作参数」。ToPrimitive
运算符将值转换为非对象类型,如果对象有能力被转换为不止一种原语类型,可以使用可选的 「期望类型」 来暗示那个类型。
它内部方法,将任意值转换成原始值,转换规则如下:
preferedType
为string
:
- 先调用
obj
的toString
方法,如果为原始值,则return
,否则进行第2步 - 调用
obj
的valueOf
方法,如果为原始值,则return
,否则进行第3步 - 抛出
TypeError
异常
preferedType
为number
:
- 先调用
obj
的valueOf
方法,如果为原始值,则return
,否则进行第2步 - 调用
obj
的toString
方法,如果为原始值,则return
,否则第3步 - 抛出
TypeError
异常
preferedType
参数为空
- 该对象为
Date
,则type被设置为String
- 否则,type被设置为
Number
接着,我们看下各个对象的转换实现:
「对象」 | 「valueOf()」 | toString() | 「默认 preferedType」 |
Object | 原值 | "[object Object]" | Number |
Function | 原值 | "function func() {...}" or "() => {...}" | Number |
Array | 原值 | "a, b, c,..." | Number |
Date | 数字 | 例如:"Thu Nov 11 2021 19:49:37 GMT+0800 (中国标准时间)" | String |
- 数组的
toString()
可以等效为join(",")
,遇到null
,undefined
都被忽略,遇到symbol
直接报错,遇到无法ToPrimitive
的对象也报错。 - 使用
模板字符串
或者使用String()
包装时,preferedType=string
,即优先调用.toString()
。
例如:
[1, null, undefined, 2].toString() // '1,,,2' // Uncaught TypeError: Cannot convert a Symbol value to a string [1, Symbol('x')].toString() // Uncaught TypeError: Cannot convert object to primitive value [1, Object.create(null)].toString()
toString
toString()
方法返回一个表示该对象的字符串。
每个对象都有一个 toString()
方法,当对象被表示为「文本值」时或者当以期望「字符串」的方式引用对象时,该方法被自动调用。
「【注】」toString()
和valueOf()
在特定的场合下会自行调用。
valueOf
Object.prototype.valueOf()
方法返回指定对象的原始值。
JavaScript
调用 valueOf()
方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf
方法一般都会被 JavaScript
自动调用。
不同内置对象的valueOf
实现:
- String => 返回字符串值
- Number => 返回数字值
- Date => 返回一个数字,即时间值
- Boolean => 返回Boolean的this值
- Object => 返回this
下面来看几个栗子:
const Str = new String('123'); console.log(Str.valueOf());//123 const Num = new Number(123); console.log(Num.valueOf());//123 const Date = new Date(); console.log(Date.valueOf()); //1637131242574 const Bool = new Boolean('123'); console.log(Bool.valueOf());//true var Obj = new Object({valueOf:()=>{ return 1 }}) console.log(Obj.valueOf());//1
Number
Number
运算符转换规则:
null
转换为 0undefined
转换为NaN
true
转换为 1,false
转换为 0- 字符串转换时遵循数字常量规则,转换失败返回
NaN
**【注】**对象这里要先转换为原始值,调用ToPrimitive
转换,type指定为number
了,继续回到ToPrimitive
进行转换。
接下来看几个栗子:
Number("0") // 0 Number("") // 0 Number(" ") // 0 Number("\n") // 0 Number("\t") // 0 Number(null) // 0 Number(false) // 0 Number(true) // 1 Number(undefined); // NaN Number("x"); // NaN Number({}); // NaN
String
String
运算符转换规则
null
转换为'null'
undefined
转换为undefined
true
转换为'true'
,false
转换为'false'
- 数字转换遵循通用规则,极大极小的数字使用指数形式
**【注】**对象这里要先转换为原始值,调用ToPrimitive
转换,type
就指定为string
了,继续回到ToPrimitive
进行转换。
接下来看几个栗子:
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]) //koala,1
Boolean
ToBoolean
运算符转换规则
除了下述 6 个值转换结果为 false
,其他全部为true
:
undefined
null
-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
什么时候转 string
字符串的自动转换,主要发生在字符串的「加法运算」时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。
遇到对象先执行ToPrimitive
转换为基本类型,然后按照基本类型的规则处理
// {}.toString() === "[object Object]" 1 + {} === "1[object Object]" // [2, 3].toString() === "2,3" 1 + [2, 3] === "12,3" [1] + [2, 3] === "1,2,3" function test() {} // test.toString() === "function test() {}" 10 + test === "10function test() {}"
加法过程中,遇到字符串,则会被处理为「字符串拼接」
上面的对象最后也都转成了字符串,遵循本条规则。接着来几个纯字符串的例子:
1 + "1" === "11" 1 + 1 === 2 1 + 1 + "1" === "21" "1" + 1 === "11" "1" + "1" === "11" 1 + "1" + 1 === "111"
对象字面量{}
在最前面则不代表对象
不是对象是什么?我们看看下面这个栗子:
// [].toString() === ""; // {}.toString() === "[object Object]"; [] + {} === "[object Object]"; // { // empty block } + [] => [].toString() => "" => Number("") => 0 {} + [] === 0; { a: 2 } + [] === 0;
先说 [] + {}
。一个数组加一个对象。加法会进行隐式类型转换,规则是调用其 valueOf()
或 toString()
以取得一个非对象的值(primitive value
)。如果两个值中的任何一个是字符串,则进行字符串串接,否则进行数字加法。[]
和 {}
的 valueOf()
都返回对象自身,所以都会调用 toString()
,最后的结果是字符串串接。[].toString()
返回空字符串,({}).toString()
返回"[object Object]"
。最后的结果就是"[object Object]"
。
然后说 {} + []
。看上去应该和上面一样。但是 {}
除了表示一个对象之外,也可以表示一个空的 block
。在 [] + {}
中,[]
被解析为数组,因此后续的+
被解析为加法运算符,而 {}
就解析为对象。但在{} + []
中,{}
被解析为空的 block
,随后的 +
被解析为正号运算符。即实际上成了:{ // empty block } + []
即对一个空数组执行正号运算,实际上就是把数组转型为数字。首先调用 [].valueOf()
。返回数组自身,不是primitive value
,因此继续调用[].toString()
,返回空字符串。空字符串转型为数字,返回0
,即最后的结果。
「【注】」{}+[]
如果被parse
成statement
的话,{}
会被parse
成空的block
,但是在需要被parse
成expression
的话,就会被parse
成空的Object
。所以{}+[]
和console.log({}+[])
的输出结果还不一样,因为参数列表只接受expression
。
什么时候转 Number
- 加法操作时,遇到非字符串的基本类型,都会转
Number
(「除了加法运算符,其他运算符都会把运算自动转成数值。」)
1 + true === 2 1 + false === 1 1 + null === 1 1 + undefined // NaN
减法操作时,一律需要把类型转换为Number
,进行数学运算
3 - 1 === 2 3 - '1' === 2 '3' - 1 === 2 '3' - '1' - '2' === 0 // [].toString() => "" => Number(...) => 0 3 - [] === 3 // {}.toString() => "[object Object]" => Number(...) => NaN 3 - {} // NaN
+x 和 一元运算 + x 是等效的(以及- x),都会强制转换成Number
+ 0 === 0 - 0 === -0 1 + + "1" === 2 1 + + + + ["1"] === 2 // 负负得正 1 + - + - [1] === 2 // 负负得正 1 - + - + 1 === 2 1 - + - + - 1 === 0 1 + + [""] === 1 // ["1", "2"].toString() => "1,2" => Number(...) => NaN 1 + + ["1", "2"] // NaN // 多出来的 + 是一元操作符,操作数是后面那个 undefined,Number(undefined) => NaN ("ba" + + undefined + "a").toLowerCase() === "banana"
- 在宽松的
==
的比较中,Number
优先于String
,下面以x == y
为例:
- 如果
x
,y
均为number
,直接比较 - 如果存在对象,
ToPrimitive()
type为number
进行转换,再进行后面比较 - 存在
boolean
,按照ToNumber
将boolean
转换为1或者0,再进行后面比较 - 如果
x
为string
,y
为number
,x
转成number
进行比较
什么时候转 Boolean
- 布尔比较时
if(obj)
,while(obj)
等判断时或者 「三元运算符」只能够包含布尔值
// 条件部分的每个值都相当于false,使用否定运算符后,就变成了true if ( !undefined && !null && !0 && !NaN && !'' ) { console.log('true'); } // true //下面两种情况也会转成布尔类型 expression ? true : false !! expression
宽松相等 ==
相等于、全等都需要对类型进行判断,当类型不一致时,宽松相等会触发隐式转换。下面介绍规则:
对象与对象类型一致,不做转换
{} != {} [] != {} [] != []
对象与基本类型,对象先执行ToPrimitive
转换为基本类型
// 小心代码块 "[object Object]" == {} [] == "" [1] == "1" [1,2] == "1,2"
数字与字符串类型对比时,字符串总是转换成数字
"2" == 2 [] == 0 [1] == 1 // [1,2].toString() => "1,2" => Number(...) => NaN [1,2] != 1
布尔值先转换成数字,再按数字规则操作
// [] => "" => Number(...) => 0 // false => 0 [] == false // [1] => "1" => 1 // true => 1 [1] == true // [1,2] => "1,2" => NaN // true => 1 [1,2] != true "0" == false "" == false
null
、undefined
、symbol
null
、undefined
与任何非自身的值对比结果都是false
,但是null == undefined
是一个特例。
null == null undefined == undefined null == undefined null != 0 null != false undefined != 0 undefined != false Symbol('x') != Symbol('x')
对比 < >
对比不像相等,可以严格相等(===
)防止类型转换,对比一定会存在隐式类型转换。
对象总是先执行ToPrimitive
为基本类型
[] < [] // false [] <= {} // true {} < {} // false {} <= {} // true
任何一边出现「非字符串」的值,则一律转换成「数字」做对比
// ["06"] => "06" => 6 ["06"] < 2 // false ["06"] < "2" // true ["06"] > 2 // true 5 > null // true -1 < null // true 0 <= null // true 0 <= false // true 0 < false // false // undefined => Number(...) => NaN 5 > undefined // false
JS 数据类型判断
typeof
typeof
操作符可以区分「基本类型」,「函数」和「对象」。
判断结果: 'string'
、'number'
、'boolean'
、'undefined'
、'function'
、'symbol'
、'bigInt'
、'object'
console.log(typeof null) // object console.log(typeof undefined) // undefined console.log(typeof 1) // number console.log(typeof 1.2) // number console.log(typeof "hello") // string console.log(typeof true) // boolean console.log(typeof Symbol()) // symbol console.log(typeof (() => {})) // function console.log(typeof {}) // object console.log(typeof []) // object console.log(typeof /abc/) // object console.log(typeof new Date()) // object
缺点:
typeof
有个明显的bug就是typeof null
为object
;typeof
无法区分各种内置的对象,如Array
,Date
等。
接下来讲简单介绍一下原理:
JS
是动态类型的变量,每个变量在存储时除了存储变量值外,还需要存储变量的类型。JS
里使用32位(bit)存储变量信息。低位的1~3个bit
存储变量类型信息,叫做类型标签(type tag
)
.... XXXX X000 // object .... XXXX XXX1 // int .... XXXX X010 // double .... XXXX X100 // string .... XXXX X110 // boolean
- 只有
int
类型的type tag
使用1个bit,并且取值为1,其他都是3个bit, 并且低位为0。这样可以通过type tag
低位取值判断是否为int
数据; - 为了区分
int
,还剩下2个bit,相当于使用2个bit区分这四个类型:object
,double
,string
,boolean
; - 但是
null
,undefined
和Function
并没有分配type tag
。
「如何识别Function
」
函数并没有单独的type tag
,因为函数也是对象。typeof
内部判断如果一个对象实现了[[call]]
内部方法则认为是函数。
「如何识别undefined
」
undefined
变量存储的是个特殊值JSVAL_VOID
(0-2^30),typeof
内部判断如果一个变量存储的是这个特殊值,则认为是undefined
。
#define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
「如何识别null
」
null
变量存储的也是个特殊值JSVAL_NULL
,并且恰巧取值是空指针机器码(0),正好低位bit的值跟对象的type tag
是一样的,这也导致著名的bug:
typeof null // object
有很多方法可以判断一个变量是一个非null
的对象,例如:
// 利用Object函数的装箱功能 function isObject(obj) { return Object(obj) === obj; } isObject({}) // true isObject(null) // false
instanceof
语法:A instanceof B
, 即判断A
是否为B
类型的实例,也可以理解为B
的prototype
是否在A
的原型链上
Object.create({}) instanceof Object // true Object.create(null) instanceof Object // false Function instanceof Object // true Function instanceof Function // true Object instanceof Object // true [] instanceof Array // true {a: 1} instanceof Object // true new Date() instanceof Date // true // 对于基本类型,使用字面量声明的方式可以正确判断类型 new String('dafdsf') instanceof String // true 'xiaan' instanceof String // false, 原型链不存在
作为类型判断的一种方式,instanceof
操作符不会对变量object
进行隐式类型转换:
"" instanceof String; // false,基本类型不会转成对象 new String('') instanceof String; // true
对于没有原型的对象或则基本类型直接返回false
:
1 instanceof Object // false Object.create(null) instanceof Object // false
B
必须是个对象。并且大部分情况要求是个构造函数(即要具有prototype
属性)
// TypeError: Right-hand side of 'instanceof' is not an object 1 instanceof 1 // TypeError: Right-hand side of 'instanceof' is not callable 1 instanceof ({}) // TypeError: Function has non-object prototype 'undefined' in instanceof check ({}) instanceof (() => {})
「原理:」
// 自定义 instanceof function myInstanceof(obj, objType) { // 首先用typeof来判断基础数据类型,如果是,直接返回false if(typeof obj !== 'object' || obj === null) return false; // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象 let proto = Object.getPrototypeOf(obj); while(true) { //循环往下寻找,直到找到相同的原型对象 if(proto === null) return false; if(proto === objType.prototype) return true;//找到相同原型对象,返回true proto = Object.getPrototypeof(proto); } } // 验证一下自己实现的myInstanceof是否OK console.log(myInstanceof(new Array('2','3'), Array)); // true console.log(myInstanceof(123, Number)); // false console.log(myInstanceof(new Number(123), Number)); //true
Object.prototype.toString
对于 Object.prototype.toString()
方法,会返回一个形如 "[object XXX]"
的字符串
Object.prototype.toString.call(null) //"[object Null]" Object.prototype.toString.call(undefined) //"[object Undefined]" Object.prototype.toString.call(1) // "[object Number]" Object.prototype.toString.call('Miss U') // “[object String]" Object.prototype.toString.call(true) // "[object Boolean]" Object.prototype.toString({}) // "[object Object]" Object.prototype.toString.call({}) // "[object Object]" Object.prototype.toString.call(function(){}) // ”[object Function]" Object.prototype.toString.call([]) //"[object Array]" Object.prototype.toString.call(/123/g) //"[object RegExp]" Object.prototype.toString.call(new Date()) //"[object Date]" Object.prototype.toString.call(document) //[object HTMLDocument]" Object.prototype.toString.call(window) //"[object Window]"
- 如果实参是个基本类型,会自动转成对应的引用类型;
Object.prototype.toString
不能区分基本类型的,只是用于区分各种对象;null
和undefined
不存在对应的引用类型,内部特殊处理了;
「原理:」
每个对象都有个内部属性[[Class]]
,内置对象的[[Class]]
的值都是不同的("Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"),并且目前[[Class]]
属性值只能通过Object.prototype.toString
访问。而Object.prototype.toString
内部先访问对象的Symbol.toStringTag
属性值拼接返回值的。
Object.prototype.toString
的内部逻辑:
- 如果实参是
undefined
, 则返回"[object Undefined]"
; - 如果实参是
null
, 则返回"[object Null]"
; - 把实参转成对象
- 获取对象的
Symbol.toStringTag
属性值subType
- 如果
subType
是个字符串,则返回[object subType]
- 否则获取对象的
[[Class]]
属性值type
,并返回[object type]
最后,我们可以封装一个通用的类型检测方法:
function getPrototype(obj){ let type = typeof obj; if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回 console.log(obj,':',res) return type; } // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果 const res = Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1') console.log(obj,'=',res); return res; } getPrototype([]) // "Array" typeof []是object,因此toString返回 getPrototype('abc') // "string" typeof 直接返回 getPrototype(window) // "Window" toString返回 getPrototype(null) // "Null"首字母大写,typeof null是object,需toString来判断 getPrototype(undefined) // "undefined" typeof 直接返回 getPrototype() // "undefined" typeof 直接返回 getPrototype(function(){}) // "function" typeof能判断,因此首字母小写 getPrototype(/123/g) //"RegExp" toString返回