深入了解JS 数据类型

admin2026-03-02 18:47:38620

深入了解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 类型等。依据「存储方式」不同,数据类型大致可以分成两类:

「基础类型」存储在「栈内存」,被引用或拷贝时,会创建一个完全相等的变量。「引用类型」存储在「堆内存」,在「栈内存」存储的是地址,多个引用指向同一个内存地址。可以通过以下栗子加深理解:

代码语言:javascript复制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 类型转换ToPrimitivestring 、number 、boolean 和 null undefined 这五种类型统称为「原始类型」(Primitive),表示不能再细分下去的基本类型。

ToPrimitive对原始类型不发生转换处理,只「针对引用类型(object)的」,其目的是将引用类型(object)转换为非对象类型,也就是原始类型。

代码语言:javascript复制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()。例如:

代码语言:javascript复制[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()

toStringtoString()方法返回一个表示该对象的字符串。

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

「【注】」toString()和valueOf() 在特定的场合下会自行调用。

valueOfObject.prototype.valueOf()方法返回指定对象的原始值。

JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。

不同内置对象的valueOf实现:

String => 返回字符串值Number => 返回数字值Date => 返回一个数字,即时间值Boolean => 返回Boolean的this值Object => 返回this下面来看几个栗子:

代码语言:javascript复制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

NumberNumber运算符转换规则:

null 转换为 0undefined 转换为 NaNtrue 转换为 1,false 转换为 0字符串转换时遵循数字常量规则,转换失败返回NaN**【注】**对象这里要先转换为原始值,调用ToPrimitive转换,type指定为number了,继续回到ToPrimitive进行转换。

接下来看几个栗子:

代码语言:javascript复制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

StringString 运算符转换规则

null 转换为 'null'undefined 转换为 undefinedtrue 转换为 'true',false 转换为 'false'数字转换遵循通用规则,极大极小的数字使用指数形式**【注】**对象这里要先转换为原始值,调用ToPrimitive转换,type就指定为string了,继续回到ToPrimitive进行转换。

接下来看几个栗子:

代码语言:javascript复制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

BooleanToBoolean 运算符转换规则

除了下述 6 个值转换结果为 false,其他全部为true:

undefinednull-00或+0NaN''(空字符串)假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

接下来看几个栗子:

代码语言:javascript复制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转换为基本类型,然后按照基本类型的规则处理

代码语言:javascript复制// {}.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() {}"

加法过程中,遇到字符串,则会被处理为「字符串拼接」

上面的对象最后也都转成了字符串,遵循本条规则。接着来几个纯字符串的例子:

代码语言:javascript复制1 + "1" === "11"

1 + 1 === 2

1 + 1 + "1" === "21"

"1" + 1 === "11"

"1" + "1" === "11"

1 + "1" + 1 === "111"

对象字面量{}在最前面则不代表对象

不是对象是什么?我们看看下面这个栗子:

代码语言:javascript复制// [].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(「除了加法运算符,其他运算符都会把运算自动转成数值。」)代码语言:javascript复制1 + true === 2

1 + false === 1

1 + null === 1

1 + undefined // NaN

减法操作时,一律需要把类型转换为Number,进行数学运算

代码语言:javascript复制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

代码语言:javascript复制+ 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)等判断时或者 「三元运算符」只能够包含布尔值代码语言:javascript复制// 条件部分的每个值都相当于false,使用否定运算符后,就变成了true

if ( !undefined && !null && !0 && !NaN && !'' ) {

console.log('true');

} // true

//下面两种情况也会转成布尔类型

expression ? true : false

!! expression

宽松相等 ==相等于、全等都需要对类型进行判断,当类型不一致时,宽松相等会触发隐式转换。下面介绍规则:

对象与对象类型一致,不做转换

代码语言:javascript复制{} != {}

[] != {}

[] != []

对象与基本类型,对象先执行ToPrimitive转换为基本类型

代码语言:javascript复制// 小心代码块

"[object Object]" == {}

[] == ""

[1] == "1"

[1,2] == "1,2"

数字与字符串类型对比时,字符串总是转换成数字

代码语言:javascript复制"2" == 2

[] == 0

[1] == 1

// [1,2].toString() => "1,2" => Number(...) => NaN

[1,2] != 1

布尔值先转换成数字,再按数字规则操作

代码语言:javascript复制// [] => "" => 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 是一个特例。

代码语言:javascript复制null == null

undefined == undefined

null == undefined

null != 0

null != false

undefined != 0

undefined != false

Symbol('x') != Symbol('x')

对比 < >对比不像相等,可以严格相等(===)防止类型转换,对比一定会存在隐式类型转换。

对象总是先执行ToPrimitive为基本类型

代码语言:javascript复制[] < [] // false

[] <= {} // true

{} < {} // false

{} <= {} // true

任何一边出现「非字符串」的值,则一律转换成「数字」做对比

代码语言:javascript复制// ["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 数据类型判断typeoftypeof操作符可以区分「基本类型」,「函数」和「对象」。

判断结果: 'string'、'number'、'boolean'、'undefined'、'function'、'symbol'、'bigInt'、'object'

代码语言:javascript复制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)

代码语言:javascript复制.... 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。

代码语言:javascript复制 #define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))

「如何识别null」

null变量存储的也是个特殊值JSVAL_NULL,并且恰巧取值是空指针机器码(0),正好低位bit的值跟对象的type tag是一样的,这也导致著名的bug:

代码语言:javascript复制typeof null // object

有很多方法可以判断一个变量是一个非null的对象,例如:

代码语言:javascript复制// 利用Object函数的装箱功能

function isObject(obj) {

return Object(obj) === obj;

}

isObject({}) // true

isObject(null) // false

instanceof语法:A instanceof B , 即判断A是否为B类型的实例,也可以理解为B的prototype是否在A的原型链上

代码语言:javascript复制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进行隐式类型转换:

代码语言:javascript复制"" instanceof String; // false,基本类型不会转成对象

new String('') instanceof String; // true

对于没有原型的对象或则基本类型直接返回false:

代码语言:javascript复制1 instanceof Object // false

Object.create(null) instanceof Object // false

B必须是个对象。并且大部分情况要求是个构造函数(即要具有prototype属性)

代码语言:javascript复制// 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 (() => {})

「原理:」

代码语言:javascript复制// 自定义 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]" 的字符串

代码语言:javascript复制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]最后,我们可以封装一个通用的类型检测方法:

代码语言:javascript复制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返回