前言
在JavaScript中,浅拷贝和深拷贝是两种常用的对象复制方式,它们的区别主要体现在对嵌套对象的处理上。今天我们就来聊聊浅拷贝,了解浅拷贝的实现原理,并自己手搓一个浅拷贝,同时浅拷贝在前端面试中也是一个容易被问到的考点,我们一起来看看吧
我们知道对象是引用数据类型,我们先来看一道题:
js
let obj = {
name: '菌菌'
}
let lw = obj
obj.name = '来颗奇趣蛋'
console.log(lw.name);
如果不太了解引用数据类型的小伙伴们,这里可能就会觉得输出菌菌了。让我们来看看输出结果:
我们可以看到,这里输出了来颗奇趣蛋。这是为什么呢,我们修改obj.name,为什么lw.name也被修改了呢?
- 引用类型的值是存在堆当中,但是会将引用地址存在栈中
这里我们画张图来理解一下:(图画的不是很好,请见谅)
实际上,引用数据的值存放在堆区,也就是图中黄色框框所在的位置,堆区中存在着一段连续的地址,而这些地址又对应引用数据的值,如图中的1005,而栈区中变量存储的是堆区中的地址,就如图中obj = 1005,所以通过这些地址值,我们就可以访问堆区中的值,这里也可以叫指针。
为什么改变obj.name会影响到lw.name呢?从图中观察到,obj和lw存储着相同的指向堆区的地址。当let obj = {...}时,obj会将变量的值的地址存储进去,这里为obj = 1005,而真正的变量值存储在堆区。当赋值语句lw = obj时,lw被赋值为obj相同的地址,lw = 1005.
而当执行到obj.name = '来颗奇趣蛋'时,先在栈区寻找变量obj,发现obj存储的是一段地址,然后会顺着obj存储的地址来到堆区,将堆区中name的值改为'来颗奇趣蛋'.所以当输出lw.name时,发现lw的值为1005,为地址,顺着地址找到了堆区中name的值,但此时name的值已经被修改为'来颗奇趣蛋',所以输出'来颗奇趣蛋',引用数据类型共享变量。
浅拷贝
JavaScript 中的浅拷贝是创建一个新对象,将原始对象的属性值复制到新对象中。当我们改变新(原)对象中属性的值时,原(新)对象中的属性值也会改变
,这就是浅拷贝的特性。在我们上面那道例题中,let lw = obj
,直接将原对象赋值给新对象,这同样也是浅拷贝的一种方式,所以当我们改变对象中的值时,另一个对象也会改变。
当然直接将对象赋给另一个对象,这是最简单的方式,大家也都知道,我们下面来聊聊别的浅拷贝方法:
Object.create(x)
Object.create
方法用于创建一个新对象,新对象的原型是指定的对象或 null
。虽然 Object.create
通常用于对象的继承,但也可以实现一种浅拷贝的效果,因为新创建的对象与原始对象之间存在原型链关系。
js
const sourceObject = {
a: 1,
b: {
c: 2
}
};
// 使用 Object.create 实现浅拷贝
const shallowCopy = Object.create(sourceObject);
console.log(shallowCopy.a); // 1
console.log(shallowCopy.b.c); // 2
// 修改原始对象的嵌套属性
sourceObject.b.c = 99;
console.log(shallowCopy.b.c); // 99,因为浅拷贝中嵌套对象的引用未复制
在这个示例中,Object.create(sourceObject)
创建了一个新对象 shallowCopy
,并将 sourceObject
设为 shallowCopy
的原型。这样,shallowCopy
通过原型链访问了 sourceObject
的属性。
需要注意的是,Object.create
只复制对象的第一层属性,而不会递归复制嵌套对象。也就是说,如果我们修改sourceObject.a = 66
, shallowCopy中的属性a的值是不会改变的,但如果原始对象包含引用类型,也就是此题中的b对象,当我们改变b对象中属性的值,还是会影响新对象,所以还是浅拷贝。
Object.assign({}, x)
Object.assign({}, x)
方法用于创建一个浅拷贝,将一个或多个源对象的可枚举属性复制到目标对象。这是一种常见的浅拷贝方法,适用于对象的第一层属性,但不会递归地复制嵌套对象。
js
const sourceObject = {
a: 1,
b: {
c: 2
}
};
// 使用 Object.assign 实现浅拷贝
const shallowCopy = Object.assign({}, sourceObject);
console.log(shallowCopy.a); // 1
console.log(shallowCopy.b.c); // 2
// 修改原始对象的嵌套属性
sourceObject.b.c = 99;
console.log(shallowCopy.b.c); // 99,因为浅拷贝中嵌套对象的引用未复制
在这个示例中,Object.assign({}, sourceObject)
创建了一个新对象 shallowCopy
,并将 sourceObject
的属性复制到了 shallowCopy
。这样,shallowCopy
中的属性与 sourceObject
中的属性具有相同的值。
此方法同样与上方法一样,只复制第一层属性,当我们改变b对象中属性的值,还是会影响新对象,所以还是浅拷贝。
concat
在 JavaScript 中,concat
方法用于连接两个或多个数组,并返回一个新数组。对于数组中的基本类型元素,concat
方法实现了浅拷贝。
示例:
js
let arr = [1, 2, 3, {a: 10}]
let newArr = [].concat(arr)
arr[3].a = 100
console.log(newArr); // [1, 2, 3, {a: 100}]
concat
方法对于数组中的对象是浅拷贝的。在这个例子中,arr
中的第四个元素是一个对象 {a: 10}
,通过 concat
方法创建的新数组 newArr
仍然引用相同的对象。
所以,当你修改 arr
中的对象时,这个变化也会反映在 newArr
中,因为它们实际上共享同一个对象引用。
这是因为 concat
方法只复制数组的一层,而不是递归复制数组中的每个对象。
slice
slice
方法是复制数组的一部分,它从给定的开始索引复制到结束索引,并返回一个新数组。
js
let arr = [1, 2, 3, {a: 10}]
let newArr = arr.slice()
arr[3].a = 100
console.log(newArr); // [1, 2, 3, {a: 100}]
arr
使用 slice
方法创建的新数组 newArr
,但仍然共享相同的对象引用。因此,当你修改 arr
中的对象时,这个变化也会反映在 newArr
中。只复制对象的第一层属性,而不会递归复制嵌套对象。当我们改变对象中的属性值时,还是会影响到新的对象。
浅拷贝实现源码
js
function shalldowCopy(obj) {
if (typeof obj !== 'object' || obj == null) return
let objCopy = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
objCopy[key] = obj[key]
}
}
return objCopy
}
-
类型检查:
if (typeof obj !== 'object' || obj == null) return obj;
- 如果传入的
obj
不是对象或者为null
,则直接返回原对象,因为不需要拷贝。
- 如果传入的
-
创建新对象:
let objCopy = obj instanceof Array ? [] : {};
- 根据原对象的类型创建一个新的对象或数组。这里使用
instanceof
检查obj
是否是数组。
- 根据原对象的类型创建一个新的对象或数组。这里使用
-
遍历属性:
for (let key in obj) { ... }
- 使用
for...in
循环遍历原对象的属性。
- 使用
-
判断属性所有权:
if (obj.hasOwnProperty(key)) { ... }
- 使用
hasOwnProperty
方法判断属性是否为对象自身的属性,而不是原型链上的属性。
- 使用
-
复制属性:
objCopy[key] = obj[key];
- 将原对象的属性复制到新对象中。
-
返回新对象:
return objCopy;
- 返回新的对象,完成浅拷贝。
总结
浅拷贝
- 常见的浅拷贝方法:
- Object.create(x)
- Object.assign({}, x)
- concat
- slice
我们需要注意的是,这些方法只复制对象的第一层属性,而不会递归复制嵌套对象。也就是说,如果我们修改嵌套对象的值,也就是说原始对象包含引用类型的值,还是会影响新对象,所以还是浅拷贝。