前言
今天聊一聊js中的深浅拷贝,我们先来看一份代码:
js
let obj = {
age:18
}
let obj2 = obj
obj.age = 20
console.log(obj2.age);
思考一下,这份代码的执行结果?
分析:
- 在调用栈中,创建全局上下文执行对象,有变量环境和词法环境
- let声明的变量存放于词法环境中。
- obj,obj2首先值为undefined,然后赋值,发现是一个对象,引用类型数据存放于堆中,由地址指向这个堆里的值,然后把obj的地址赋予obj2
- 因此他们的地址一样,更改了里面的内容,另一份打印出来也跟着变了。
拷贝
通常只针对引用类型,原始类型直接赋值,永远都是深拷贝,没有什么好讨论的。
浅拷贝
基于原对象,拷贝得到一个新对象,原对象中的内容修改会影响新对象
常见的浅拷贝方法:
Object.create(x)
js
let a = {
name: '毛毛'
}
let b = Object.create(a)
a.name = '123'
console.log(b.name);
原来的a改变了,b也跟着改变,这种拷贝叫做浅拷贝。
Object.assign({},a)
js
let a = {
name: '毛毛'
}
let b = {
age: 18
}
// 数组中合并可以用concat,对象怎么合并?
let c = Object.assign(a, b)
console.log(c, a);
// 发现原来的a也变了(将b拼接进了a)
在这份代码中,我们发现assign方法可以将b拼接给a,我们接着往下看。
js
let a = {
name: '毛毛'
}
let c = Object.assign({}, a)
a.name = 'aaa'
console.log(c, a);
我们拷贝了一份a,更改a中的name属性,那么c中的name会改变吗?
我们发现拷贝的这一份并没有跟着改变,难道这是深拷贝?接着往下看
js
let a = {
name: '毛毛',
like: {
n: 'running'
}
}
let c = Object.assign({}, a)
// a.name = '123'
a.like.n = 'swimming'
console.log(c, a);
我们发现两份都改变了,回过头看我们的前言,事实上,这还是由于引用类型的数据存放于堆中,这种拷贝还是不够深,因此属于浅拷贝的范畴。
[].concat(arr)
js
let arr = [1, 2, 3, { a: 10 }]
let newArr = [].concat(arr)
arr[3].a = 100
console.log(newArr, arr);
原数组和新数组都跟着改变了,这还是属于浅拷贝。
数组解构
js
let arr = [1, 2, 3, { a: 10 }]
let newArr = [...arr]
arr[3].a = 100
console.log(newArr, arr);
原数组和新数组都跟着改变了,这还是属于浅拷贝。
slice(0)
js
let arr = [1, 2, 3, { a: 10 }]
let newArr = arr.slice(0)
arr[3].a = 100
console.log(newArr, arr);
slice()类似python中的切片,左闭右开,传两个参数,切下标哪里到哪里,第一位0第二位不写默认从下标0到最后。
arr.toReversed().reverse()
js
let arr = [1, 2, 3, { a: 10 }]
let newArr = arr.toReversed().reverse()
arr[3].a = 100
console.log(newArr, arr);
这里由于我node的版本老了,就不给展示结果了,但是它依然是浅拷贝。
手写浅拷贝
至此我们都在讲一些js内置方法做浅拷贝,那么如果面试官问你能不能自己手写一个浅拷贝呢?
js
let obj = {
name:'张三',
like:{
a:'food'
}
}
// 手写一个浅拷贝,希望传一个对象进来,能够拷贝一个
function shallowCopy(obj){
let newObj = {};
return newObj;
}
中间的代码应该如何填充?做到,丢一个obj进去,拷贝一份(newObj),最终返回newObj。
js
let obj = {
name: '张三',
like: {
a: 'food'
}
}
// 手写一个浅拷贝,希望传一个对象进来,能够拷贝一个
function shallowCopy(obj) {
let newObj = {};
for (let key in obj) {
newObj[key] = obj[key];
}
return newObj;
}
let obj2 = shallowCopy(obj);
obj.like.a = 'drink';
console.log(obj2);
但是这种方法会把原对象中隐式属性也给遍历到,在实际开发中浅拷贝一般不需要将原型上具有的属性一块拷贝。因此我们需要优化一下代码,规避这个问题
实现原理
- 借助for in 遍历原对象,将原对象的属性增加在新对象中
- 因为for in 会遍历到原对象中隐式具有的属性,通常需要使用
obj.hasOwnProperty(key)
来判断要拷贝的属性是不是对象显式具有的
js
let obj = {
name: '张三',
like: {
a: 'food'
}
}
// 手写一个浅拷贝,希望传一个对象进来,能够拷贝一个
function shallowCopy(obj) {
let newObj = {};
for (let key in obj) {
if(obj.hasOwnProperty(key)){
newObj[key] = obj[key];
}
}
return newObj;
}
let obj2 = shallowCopy(obj);
obj.like.a = 'drink';
console.log(obj2);
归根结底:引用类型复制的是引用地址,因此原来的东西变更了,你也得变更,引用地址都是一样的,但是内容改变了。
深拷贝
基于原对象拷贝,拷贝得到一个新对象,原对象中的内容修改不会影响新对象。
深拷贝手段
JSON.parse(JSON.stringify(obj))
先将对象序列化为 JSON 字符串,再将其解析回对象,会得到一个与原始对象结构相同但在内存中独立的新对象。
js
let obj = {
name: '张三',
age: 18,
like: {
a: 'coding'
},
a: true,
b: undefined,
c: null,
d: Symbol('123'),
// e: 123n,
f: function () { }
}
let obj2 = JSON.parse(JSON.stringify(obj))
obj.like.n = 'running'
console.log(obj2);
缺陷:
- 不能识别BigInt类型
- 不能拷贝undefined、symbol、function类型的值
- 不能处理循环引用
structuredClone()
js
const user = {
name: {
firstName: '牛',
lastName: '蜗'
},
age: 19
}
const newUser = structuredClone(user)
user.name.firstName = 'kk'
console.log(newUser);
手写深拷贝
面试官经典一问:如果js不提供这些方法,你能不能手搓一个方法,实现深拷贝对象?
思考:我们还能像刚刚手写浅拷贝一样那么做吗?
手写浅拷贝,我们会把一个对象的引用地址给复制过去,但是既然是深拷贝,那我们就不能用原来的地址了,不然不就成浅拷贝了?我们需要开辟一块新的地址。
js
const user = {
name: {
firstName: '牛',
lastName: '蜗'
},
age: 19
}
function deep(obj) {
let newObj = {}
for (let key in obj) {
// 只拷贝显式具有的属性
if (obj.hasOwnProperty(key)) {
// 判断属性值是否是对象
if (obj[key] instanceof Object) {
newObj[key] = deep(obj[key])
} else {
newObj[key] = obj[key]
}
}
}
return newObj;
}
const newUser = deep(user)
user.name.firstName = 'kk'
console.log(newUser);
在这里,我们通过自定义的 deep
函数实现了对复杂对象的深度拷贝。当我们修改原始对象中的属性值时,新拷贝出来的对象 newUser
并不会受到影响。
- 借助for in 遍历原对象,将原对象的属性增加在新对象中
- 因为for in 会遍历到原对象中隐式具有的属性,通常需要使用
obj.hasOwnProperty(key)
来判断要拷贝的属性是不是对象显式具有的 - 如果遍历到的属性是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象(开辟新的内存空间)。
小结
小小总结一下这篇文章,介绍了深浅拷贝的内置方法,以及手写深浅拷贝,理解一下深浅拷贝底层到底为什么一个是浅一个是深。
通过对具体代码示例的分析,我们了解到可以通过自定义函数来处理对象的拷贝,对于属性值为对象的情况进行递归操作,从而确保了数据的独立性。