深入了解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 和 nullundefined 这五种类型统称为「原始类型」(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转换为NaNtrue转换为 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转换为undefinedtrue转换为'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:
undefinednull-00或+0NaN''(空字符串)
假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是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返回