JavaScript 数据类型

基本类型:字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol、以及ES10新增的BigInt(任意精度整数)七类。

引用数据类型:对象(Object)、数组(Array)、函数(Function)。

注:SymbolES6 引入了一种新的原始数据类型,表示独一无二的值。

数据类型

数据存放

JavaScript的所有变量(包括函数)在整个处理过程中都是存放在内存中,所以要对一个变量进行处理。首先得为变量分配内存。

JavaScript内存分配和其他语言一样,是根据变量的数据类型来分配内存的,而JavaScript变量的数据类型由所赋的值的类型所决定的。在JavaScript中,基本数据类型变量分配在栈内存中,其中存放了变量的值,对其是按值访问的;而引用数据类型的变量则同时会分配栈内存和堆内存,其中栈内存存放的是地址。堆内存存放的是引用的值,栈内存存放的地址指向堆内存存放的值。对该变量的访问是按引用来访问的,即首先读取到栈内存存放的地址,然后按地址找到堆内存读取其中的值。如下图:

基本数据类型

img

引用数据类型

img

JavaScript之所以按变量的不同数据类型来分配内存,主要原因是栈内存比堆内存小,而且栈内存的大小是固定的,而堆内存大小可以动态变化。基本数据类型的值的大小固定,对象类型的值大小不固定,所以将它们分别存放在栈内存和堆内存是合理的。

判断变量的数据类型

typeof

typeof是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括number,string,boolean,undefined,object,function,symbol等。

1
2
3
4
5
6
7
8
9
10
11
12
typeof ""; //string
typeof 0; //number
typeof false; //boolean
typeof undefined; //undefined
typeof
function () {}; //function
typeof {}; //object
typeof Symbol(); //symbol
typeof null; //object
typeof []; //object
typeof new Date(); //object
typeof new RegExp(); //object

instanceof

instanceof用来判断A是否为B的实例,表达式为:A instanceof B,如果AB的实例,则返回true,否则返回falseinstanceof检测的是原型,内部机制是通过判断对象的原型链中是否有类型的原型。

1
2
3
4
5
{} instanceof Object;  //true
[] instanceof Array; //true
[] instanceof Object; //true
"123" instanceof String; //false
new String(123) instanceof String; //true

constructor

当一个函数F被定义时,JS引擎会为F添加prototype原型,然后在prototype上添加一个constructor属性,并让其指向F的引用,F利用原型对象的constructor属性引用了自身,当F作为构造函数创建对象时,原型上的constructor属性被遗传到了新创建的对象上,从原型链角度讲,构造函数F就是新对象的类型。这样做的意义是,让对象诞生以后,就具有可追溯的数据类型。

image-20211202152811772

Object.prototype.toString()

toString()Object的原型方法,调用该方法,默认返回当前对象的[[Class]]。这是一个内部属性,其格式为[object Xxx],其中Xxx就是对象的类型。 对于Object对象,直接调用toString()就能返回[object Object],而对于其他对象,则需要通过callapply来调用才能返回正确的类型信息。

image-20211202153634529

装一个准确判断数据类型的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 获取变量准确类型
* @param obj
* @returns {string|"undefined"|"boolean"|"number"|"string"|"function"|"symbol"|"bigint"}
*/
export function getRealType(obj) {
const type = typeof obj
// 如果不是object类型的数据,直接用typeof就能判断出来
if (type !== 'object') {
return type
}
// 如果是object类型数据,准确判断类型必须使用Object.prototype.toString.call(obj)的方式才能判断
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
}

引用数据类型的深拷贝

上面说了,引用数据类型的变量会分配栈内存和堆内存,其中栈内存存放的是地址,当我们把引用数据类型变量a 赋值给变量b,例如:

let a = [0, 1, 2, 3, 4], b = a,当变量b发生改变后,变量a也相应发生了改变。

img

当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。

img

而当我们b[0]=1时进行数组修改时,由于ab指向的是同一个地址,所以自然a也受了影响,这就是所谓的浅拷贝了。

浅拷贝:只是拷贝的栈区的引用地址

根据浅拷贝的方法不同,会有不同的效果。

一、最弱的浅拷贝为直接赋值

1
let b = a;

是直接将整个a数组的地址赋给b,故b的任意值(为什么说任意值呢?因为后面会介绍到,有些浅拷贝,可以使部分值看上去有深拷贝的效果)改变,都会影响到a

二、下面四种浅拷贝方法就厉害一些了,它们在有些情况(原数组里的数据不包含引用类型)下也能达到深拷贝效果(没错只是披着狼皮的小绵羊啦,本质还是浅拷贝)

一个数组变化,另一个数组不受影响。

原数组里的数据不包含引用类型

1
let a = [0,1,2,3,4]  // 原数组

1.使用拓展运算符

1
2
3
4
let b = [...a];
//等价于
let b=[0,1,2,3,4];
//这也就是它不同于b=a的地方

2.使用assign()

1
let b = Object.assign([], a)

3.使用concat()

1
let b = [].concat(a);

因为 concat() 返回的是一个副本,所以这个时候改变 b 就不会导致 a 改变了。

4.使用slice()

1
let arr2 = arr1.slice(0);

深拷贝:就是不管里面多少层,都遍历,克隆一个与旧不相关的,修改新的不影响旧的。

原数组里的数据包含引用类型

1
let a = [1 , 2 , 3 , {"key" : "value"} , {"key1" : "value1"}];  //原数组

使用上述方法均不能实现全部深拷贝,非引用类型的值不会受影响,嵌套的一层引用类型的值会受影响。

解决方法:

方法一:递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deepClone(obj) {
// 对常见的“非”值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj
if (typeof obj !== 'object' && typeof obj !== 'function') {
// 原始类型直接返回
return obj
}
const o = Array.isArray(obj) ? [] : {}
for (const i in obj) {
if (Object.prototype.hasOwnProperty.call(obj, i)) {
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
}
}
return o
}

let b = cloneObj(a);

方法二:通过JSON解析解决

1
let b = JSON.parse(JSON.stringify(a));

注意:这种方法拷贝后的数组会丢失原数组中定义的方法和数组原型中定义的方法。