前言
拷贝,在编程中用处广泛,而在JS语言中,拷贝分为两种情况:浅拷贝与深拷贝,本文就介绍了JS拷贝的相关知识。
一、浅拷贝与深拷贝有什么区别?
想要知道这两者之间的区别,首先要先明白为什么会出现这两种拷贝情况。在JS中,数据类型被分为七种基本数据类型和一种引用数据类型,分别如下。
基本数据类型
- 字符串(String)
- 数字(Number)
- 布尔(Boolean)
- 空(Null)
- 未定义(Undefined)
- Symbol
- 大数(BigInt)
引用数据类型
- 对象(Object)
- 数组(Array)
- 函数(Function)
- 正则(RegExp)
- 日期(Date)
基本数据类型存放在栈中,而引用数据类型存放在堆中,基本数据类型在调用时使用的就是它本身的数据,也因此对于基本数据类型的拷贝,并不区分什么深浅拷贝,而引用数据类型则不同,比如这个例子:
const obj = {a:1, b:2}
这行代码运行时,首先会在栈中定义一个属性obj,之后会在堆中开辟一个内存空间用于存放对象{a:1, b:2},而这个新开辟的空间会有一个自己的内存地址,假如说这里叫0x001,之后JS会把这个堆中开辟的内存地址赋值给属性obj,因此,堆于引用数据类型,在拷贝时会出现一些特殊情况,比如:
const obj1 = {a:1}
const obj2 = obj1
obj1.a = 2
console.log(obj1) // {a:2}
console.log(obj2) // {a:2}
console.log(obj1===obj2) // true
这个例子中,直接将obj1赋值给了obj2,其实在内存空间中,是相当于把obj1的内存地址直接赋值给了obj2,因此这两个属性obj1和obj2其实指向的都是同一个对象,所以,在修改obj1中的属性时,obj2中的属性也会受到影响。这种拷贝我们就称之为浅拷贝。
除了直接对引用数据类型赋值外,以下这些情况也同样属于浅拷贝
1.使用扩展运算符(…)
const obj = { a: 1, b: 2 };
const shallowCopy = { ...obj };
2.使用Object.assign()
const obj = { a: 1, b: 2 };
const shallowCopy = Object.assign({}, obj);
3.使用Array的slice()方法(仅适用于数组)
const arr = [1, 2, 3];
const shallowCopy = arr.slice();
4.手动遍历对象属性进行拷贝
function shallowCopy(original) {
let copy = {};
for (let key in original) {
if (original.hasOwnProperty(key)) {
copy[key] = original[key];
}
}
return copy;
}
const obj = { a: 1, b: 2 };
const shallowCopy = shallowCopy(obj);
这几种常见情况都是浅拷贝,也因此当输出 obj === shallowCopy时会发现都是true。
二、深拷贝
了解了什么是浅拷贝,深拷贝也就很容易就明白了,既然浅拷贝其实拷贝的仅仅只是堆内存中的地址,那与之对应的,在堆中重新开辟一块新的内存空间,而空间中存储的值与被拷贝的值完全相同就是深拷贝了,目前深拷贝最常用的方法有两种,分别如下
1.通过JSON进行拷贝
const deepClone = function deepClone(obj){
return JSON.parse(JSON.stringify(obj))
}
const obj1 = { a: 1, b: 2 };
const obj2 = deepClone(obj1);
console.log(obj2) // { a: 1, b: 2 }
console.log(obj1===obj2) // false
这种方法深拷贝的方法很方便,但也有其局限性
1.会忽略不可序列化的值,例如函数、undefined、循环引用等。
2.不能复制对象的不可枚举属性,比如原型链。
3.如果对象中包含日期、正则表达式或者其他特殊值,可能会导致转换失败。
2.自己手写一个深拷贝方法
const deepClone = function deepClone(value) {
const cache = new WeakMap(); // 用WeakMap可以不阻碍垃圾回收机制运行
function _deepClone(value){
if(value === null || typeof value !=='object'){
return value
}
if(cache.has(value)){
return cache.get(value)
}
const result = Array.isArray(value)?[]:{}
cache.set(value, result)
for (const key in value) {
if(value.hasOwnProperty(key)){
result[key] = _deepClone(value[key])
}
}
return result
}
return _deepClone(value)
}
const obj1 = { a: 1, b: 2 };
const obj2 = deepClone(obj1);
console.log(obj2) // { a: 1, b: 2 }
console.log(obj1===obj2) // false
思路就是:首先判断传入的拷贝是否是基本数据类型,如果是,直接返回,之后判断传入的是对象还是数组,决定返回的是什么类型,之后判断当前需要拷贝的值是否还是引用数据类型,如果是则递归调用_deepClone,否则进行赋值。这里考虑到了循环引用的问题,因此创建了一个cache用于保存当前的拷贝结果,如果发现存在引用,则直接返回缓存中的值。
3.通过MessageChannel实现
const {port1, port2} = new MessageChannel()
const obj1={a: 1, b:2}
port1.postMessage(obj1)
port2.onmessage=(res)=>{
const obj2 = res.data
console.log(obj2) // { a: 1, b: 2 }
console.log(obj1===obj2) // false
}
通过MessageChannel也可以实现深拷贝,但通常我们不会使用这种方法,仅仅作为了解即可,想要深入了解详情的小伙伴儿也可以下去查一下MessageChannel的功能和用法,这里就不多赘述了。
总结:
以上就是JS深浅拷贝问题的内容了,本文仅仅简单介绍了浅拷贝与深拷贝的区别和一些使用方法,想深入了解的小伙伴儿也可以进一步查找相关资料后深入学习。