不同数据类型的存储:栈与堆
- 基本数据类型:Number、String 、Boolean、Null、Undefined、Symbol 和 Bigint。值存在栈里。
- 引用数据类型:Object。包括Object、Function、Array、Data、RegExp 类。值存在堆里,栈里存的是指针,指向存储堆里的对象的地址。
注意:const 定义的常量,是引用数据类型时,栈内存的是指针指向的地址,由指针指向堆内存中,里面有真正的数据,不可改变的是指针,真正的数据是可以更改的。因此,const的常量不能再修改指针,但是可以更改数值。
const obj = {}
const obj1 = {"name":"lucy"}
obj1["name"] = "tom" //成功
obj1 = obj2 //报错
这就是一次浅拷贝,对 p2 属性的修改会导致 p1 属性也被修改:
const p1 = {name: 'Tom'};
const p2 = p1;
p2.name = 'lucy';
console.log(p1.name); // "lucy"
栈
栈 是一种 先进后出 的数据结构,当声明一个基本数据时,它就会被存储到栈内存中。
const a = 1;
const b = "1";
复制:对应内存的数据复制一份到新开辟的内存中:
const c = b;
在JS的内存世界里:
栈内存的地址分配是连续的,b、c两个变量占用了不同的存储空间,所以他们之间也并没有什么联系。
特点:存取速度快,但不灵活,同时由于结构简单,在变量使用完成后就可以将其释放,内存回收容易实现。
堆
引用类型(对象)的引用地址存在栈中,数据存在堆里,但是堆内存存储变量时没有什么规律可言。
const p1 = {"name":"tom"};
const p2 = p1;
复制后,p2和p1都是指向同一个地址,这就说明对 p1 进行修改时就会影响到 p2 的值,这时还是浅拷贝。
简言之,深拷贝就是,把对象 a 拷贝成 b 后,a 与 b 之间不再有交集,对 对象 b 的修改不会修改到 a。
实现深拷贝
方法一:用 JSON
const b = JSON.parse(JSON.stringify(a))
JSON.stringify将对象变成字符串,JSON.parse再将字符串变成对象。
该方法有缺陷:
- 不支持Date、正则、undefined、函数等数据
- 不支持引用(环状结构)
方法二:递归
const deepClone = (a, cache) => {
if(!cache){
cache = new Map() // 缓存
}
if(a instanceof Object) { // 对象
if(cache.get(a)) {
return cache.get(a)
}
let result = undefined
if(a instanceof Function) {
if(a.prototype) { // 普通函数
result = function(){ return a.apply(this, arguments) }
}else { // 箭头函数
result = (...args) => { return a.call(undefined, ...args) }
}
} else if(a instanceof Array) { // 数组
result = []
} else if(a instanceof Date) { // 日期
result = new Date(a - 0)
} else if(a instanceof RegExp) { // 正则
result = new RegExp(a.source, a.flags)
} else {
result = {}
}
cache.set(a, result)
for(let key in a) {
if(a.hasOwnProperty(key)){
result[key] = deepClone(a[key], cache)
}
}
return result
} else { //基本类型
return a
}
}
验证一下
const a = {
number:1,
bool:false,
str: 'hi',
empty1: undefined,
empty2: null,
array: [ {name: 'frank', age: 18}, {name: 'jacky', age: 19} ],
date: new Date(2000,0,1,20,30,0),
regex: /\.(j|t)sx/i,
obj: { name:'frank', age: 18},
f1: (a, b) => a + b,
f2: function(a, b) { return a + b }
}
a.self = a
const b = deepClone(a)
b.self === b // true
b.self = 'hi'
a.self !== 'hi' // true
b.self === a.self // false
按照传入的数据类型,来返回不同的内容:
- 引用:1.首先,构造要返回的本体 result,初始值 undefined。2.然后再按照对象的不同类来确定返回的类型
- 函数:普通/箭头,返回
function(){ return a.apply(this, arguments)/(...args) => { return a.call(undefined, ...args) } - 数组:返回
[] - 日期:返回
new Date(a - 0) - 正则:返回
new RegExp(a.source, a.flags) - 其他:返回
{}3.遍历将对象 a 的所有属性都递归地拷贝到 b 里面
- 函数:普通/箭头,返回
- 基本:直接返回 a
注意
- 判断 a 是否是对象/函数/数组/日期/正则,用
a instanceof Object/Function/Array/Date/RegExp,判断函数是普通/箭头函数,用a.prototype a.self = a使 a 中出现了环,此时递归地遍历 a 的属性去创建 b 是有问题的,需要用 Map 去记录是否访问过,如果访问过就直接返回访问过的这个属性值,(map 的key可以是对象,Object的key只能是String/Symbol)- 不拷贝原型上的属性:有些属性是继承得到的,不应该对它深拷贝,用
a.hasOwnProperty(key)去筛选
总结
深拷贝的主要思想就是:递归地判断数据是引用/基本类型,遍历要进行深拷贝的对象的属性,不拷贝原型上的属性。