满满的干货:深拷贝和浅拷贝的多种实现方法以及手搓版
浅拷贝(6种方法)
什么是浅拷贝?
浅拷贝是指创建一个新的对象或数组,这个新对象或数组中的顶层元素与原对象或数组中的顶层元素相同,即它们指向相同的内存地址。
换句话说,浅拷贝仅复制一级结构,对于原始数据类型(如数字、字符串、布尔值)来说,拷贝的是值本身;而对于引用类型(如对象、数组)而言,拷贝的是这些引用类型的指针或地址,而不是它们所指向的数据的完整拷贝。
因此,如果原对象中的引用类型数据发生改变,那么浅拷贝得到的新对象中的相应数据也会受到影响,因为它们实际上指向同一个内存空间。
浅拷贝通常只针对引用类型。
浅拷贝的方法
Object.create(object)
方法介绍:
Object.create()
是用于创建新对象的一个方法,该方法接受两个可选参数,并基于传入的第一个参数(原型对象)来创建一个新对象,新对象的原型(__proto__
)将指向这个原型对象。第二个参数可选,用于为新创建的对象添加属性和特性(属性描述符)。
语法:
Object.create(proto, [propertiesObject])
- proto: 必需。这个参数是一个对象或者
null
。新创建的对象将继承该对象的属性和方法。如果为null
,则新对象将没有任何继承属性,即它的原型链顶端为null
。 - propertiesObject(可选): 一个可枚举的属性描述符对象或者多个属性描述符对象的数组。这些属性和特性将直接定义在新创建的对象上,而非其原型上。
实现浅拷贝:
Object.create(a)
方法用于创建一个新对象b,对象b的原型([[Prototype]])被链接到对象a上。b可以隐式得使用a的属性,实现了拷贝。
let a = {
name: '张三',
age: 18,
sex: '男'
}
let b = Object.create(a)
a.age=17
console.log(a.age,b.age)
对象b的age值随对象a的age值的变化而变化,所以实现的拷贝是浅拷贝。
Object.assign({},object)
方法介绍:
Object.assign()
是用于合并对象属性的一个方法。它可以把源对象(source objects)的所有可枚举的自有属性,复制到目标对象(target object)中。
语法:
Object.assign(target, ...sources)
- target: 必需。这是接收源对象属性的目标对象。如果该参数不是对象,则会先被转换为一个对象。这意味着,如果target参数是null或undefined,那么它会被转换为空对象{},这可能会导致意料之外的行为。
- ...sources: 可选。这是零个或多个源对象,它们的可枚举属性会被复制到目标对象中。后续的源对象的属性会覆盖前面源对象的同名属性。
实现浅拷贝:
当执行let obj1 = Object.assign({}, obj)
时,一个空对象和obj对象进行合并形成了一个新的对象,实现了拷贝。
let obj = {
name: '李四',
age: 19,
sex: '女',
like: {
a:'run'
}
}
let obj1 = Object.assign({}, obj)
obj.like.a = 'sleep'
console.log(obj, obj1)
obj.like.a
的值会随着obj1.like.a
的值的变化而变化,所以实现的拷贝是浅拷贝。
[].concat(array)
方法介绍:
数组的concat()
方法是一个用于合并两个或多个数组,返回一个新数组而不改变原有数组的方法。
语法:
let newArray = array1.concat(array2, array3, ..., valueN);
array1
:必需,原数组。array2, array3, ..., valueN
:可选,可以是更多的数组或单独的值,这些都将被合并到新数组中。
实现浅拷贝:
通过执行[].concat(array)
可以将一个数组和空数组合并形成一个新数组,实现拷贝。
let arr = [1, 2, 3, { a: 1 }]
let newArr = [].concat(arr)
arr[3].a = 2
console.log(newArr, arr);
newArr[3].a
的值随着arr[3].a
的值变化而变化,所以实现的拷贝是浅拷贝。
let newArr = [...arr](数组解构)
方法介绍:
数组解构赋值是一种将数组中的元素直接分配给不同变量的表达式
eg:
let a = [1,2,3,4]
console.log(...a)
let b = [...a]//1 2 3 4
console.log(a,b)//[1,2,3,4] [1,2,3,4]
实现浅拷贝:
通过数组的解构将数组的元素提取出并赋值给一个新的数组,可以实现拷贝。
let arr = [1, 2, 3, { a: 1 }]
let newArr = [...arr]
arr[3].a = 2
console.log(newArr, arr);
newArr[3].a
的值随着arr[3].a
的值变化而变化,所以实现的拷贝是浅拷贝。
arr.slice(0)
方法介绍:
slice()
方法用于从数组中提取一部分元素,返回一个新数组,原数组保持不变。
语法:
let newArray = originalArray.slice(begin, end);
begin
:起始索引(包含该位置的元素)end
:结束索引(不包含该位置的元素)
可以左闭右开的口诀记忆。
如果省略结束索引,则提取从起始索引到数组末尾的所有元素。如果起始索引是负数,则从数组末尾开始计数。如果只有一个负数参数,它将被当作结束索引,从数组末尾开始提取。
实现浅拷贝:
通过执行let newArr = arr.slice(0)
可以实现newArr对arr的拷贝。
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, { a: 1 }];
let newArr = arr.slice(0);
arr[9].a = 2
console.log(arr, newArr);
newArr[9].a
的值随着 arr[9].a
的值变化而变化,所以实现的拷贝是浅拷贝。
arr.toReversed().reverse()
方法介绍:
toReversed()
方法是数组的一个新方法,它是在 ECMAScript 2022 (ES12) 版本中引入的。此方法会返回一个新数组,其中的元素顺序与原数组相反,而原数组本身不会被修改。
语法:
let newArray = originalArray.toReversed();
eg:
let numbers = [1, 2, 3, 4, 5];
let reversedNumbers = numbers.toReversed();
console.log(reversedNumbers); // 输出: [5, 4, 3, 2, 1]
console.log(numbers); // 输出: [1, 2, 3, 4, 5],原数组未改变
reverse()
方法用于颠倒数组中元素的顺序。与toReversed()
不同,reverse()
会直接修改原数组,而不是返回一个新的数组。
实现浅拷贝:
通过toReversed()
方法获得一个反转的新数组,再对新数组使用reverse()
方法再颠倒回去,实现对原数组的拷贝。
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, { a: 1 }];
let newArr = arr.toReversed().reverse();
arr[9].a = 2;
console.log(newArr, arr);
newArr[9].a
的值随着 arr[9].a
的值变化而变化,所以实现的拷贝是浅拷贝。
浅拷贝的实现原理(手搓版)
let obj = {
nickname: '小明'
}
let obj1 = Object.create(obj)
obj1.name = '张三'
obj1.age = 18
obj1.like = {
a: 1,
b: 2,
c: 3
}
创建一个obj对象成为obj1对象的原型。为什么要怎么做呢?等一下你就知道了。
手搓一个浅拷贝的方法。
通过for in
遍历目标对象的每个属性,将属性值一一复制到新创建的空对象 res
中,从而实现了对目标对象的浅拷贝。
function shallowCopy(target) {
let res = {}
for (let key in target) {
res[key] = target[key]
}
return res
}
调用该方法实现浅拷贝。
let obj2 = shallowCopy(obj1)
console.log(obj1,obj2,obj2.nickname)
obj2比obj1多了一个nickname
属性,不能算是浅拷贝。因为for in
会遍历隐式属性,所以通过调用shallowCopy
函数obj2会将obj1的隐式属性转变成自己的显式属性。
**改进:**让for in
不遍历到隐式属性,通过hasOwnProperty
方法判断对象的属性是否是继承来的。
function shallowCopy(target) {
let res = {}
for (let key in target) {
if (target.hasOwnProperty(key)) {
res[key] = target[key]
}
}
return res
}
obj2中并没有nickname
属性,返回的是undefined。说明成功手搓了一个浅拷贝方法。
所以我们可以总结出浅拷贝的实现原理。
浅拷贝实现原理:
- 借助
for in
遍历原对象,将原对象的属性值增加在新对象中。 - 因
for in
会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)
来判断要拷贝的属性是否为对象本身的属性。
深拷贝(2种方法)
什么是深拷贝?
深拷贝通常只针对引用类型。
深拷贝是指创建一个新对象或数组,其内容是原对象或数组的完整拷贝,包括所有层级的元素。与浅拷贝不同,深拷贝不仅复制外层对象或数组本身,还会递归地复制其内部的所有对象和数组,确保源对象和拷贝对象在内存中是完全独立的两个实例。即使修改拷贝对象中的嵌套对象或数组,也不会影响到原始数据。
深拷贝的方法
JSON.parse(JSON.stringify(obj))
方法介绍:
- 第一步(序列化):当JSON.stringify(obj)被执行时,它会尝试将obj对象转换成一个JSON字符串。在这个过程中,对象中的函数、Symbol、undefined值会被忽略,循环引用也会导致错误或被处理(具体行为取决于环境,某些环境下可能抛出错误或被静默处理)。
- 第二步(反序列化):随后,JSON.parse()接收到上一步产生的字符串,将其解析回JavaScript的对象结构。由于是从字符串重新构建的对象,所以与原对象在内存中是完全独立的,实现了深拷贝的效果。
实现深拷贝:
let obj = {
name: '张三',
age: 18,
sex: '男',
like: {
a: 1
}
}
let obj1 = JSON.parse(JSON.stringify(obj))
obj.like.a = 2
console.log(obj1, obj)
obj1.like.a
的值不会随着obj.like.a
的值变化而变化,实现了深拷贝。但是该方法存在一些缺点。
缺点:
- 不能识别BigInt类型。
- 不能拷贝 undefined、symbol、函数类型的值。
- 不能处理循环引用。
structuredClone()
方法介绍:
structuredClone
用于实现深拷贝,它能够创建一个给定值的完全独立的副本,包括所有嵌套的对象和数组,以及一些特定的内置类型。
语法:
const clone = structuredClone(originalValue);
originalValue
可以是一个对象、基本数据类型值、或者某些特殊类型值(如 Date
、RegExp
等),但不包括函数、Error对象或DOM节点等无法序列化的值。
实现深拷贝:
const user = {
name: {
first: '张三',
last: '李四'
},
age: 18,
}
const newUser = structuredClone(user)
user.name.first = 'a'
console.log(newUser, user)
newUser.name.first
的值没有随着user.name.first
的值变化而变化,所以实现了深拷贝。
深拷贝的实现原理(手搓简易版)
深拷贝的实现原理和浅拷贝的实现原理非常相似。
function deepCopy(obj) {
let newUser = {}
for (let key in user) {
if (obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) {
newUser[key] = deep(obj[key])
} else
newUser[key] = obj[key]
}
}
return newUser
}
相比之下,深拷贝多了一步递归操作,递归地复制其内部的所有对象和数组实现完全拷贝。
深拷贝的实现原理:
-
借助for in遍历原对象,将原对象的属性值增加在新对象中
-
因为for in会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是否为对象本身的属性
-
如果遍历到的属性值是原始值类型时,直接在新对象中赋值,如果是引用类型,递归创建新的子对象。
转载自:https://juejin.cn/post/7371365854011867147