likes
comments
collection
share

满满的干货:深拷贝和浅拷贝的多种实现方法以及手搓版

作者站长头像
站长
· 阅读数 69

浅拷贝(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。说明成功手搓了一个浅拷贝方法。

所以我们可以总结出浅拷贝的实现原理。

浅拷贝实现原理:

  1. 借助for in遍历原对象,将原对象的属性值增加在新对象中。
  2. for in会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是否为对象本身的属性。

深拷贝(2种方法)

什么是深拷贝?

深拷贝通常只针对引用类型

深拷贝是指创建一个新对象或数组,其内容是原对象或数组的完整拷贝,包括所有层级的元素。与浅拷贝不同,深拷贝不仅复制外层对象或数组本身,还会递归地复制其内部的所有对象和数组,确保源对象和拷贝对象在内存中是完全独立的两个实例。即使修改拷贝对象中的嵌套对象或数组,也不会影响到原始数据。

深拷贝的方法

JSON.parse(JSON.stringify(obj))

方法介绍:

  1. 第一步(序列化):当JSON.stringify(obj)被执行时,它会尝试将obj对象转换成一个JSON字符串。在这个过程中,对象中的函数、Symbol、undefined值会被忽略,循环引用也会导致错误或被处理(具体行为取决于环境,某些环境下可能抛出错误或被静默处理)。
  2. 第二步(反序列化):随后,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的值变化而变化,实现了深拷贝。但是该方法存在一些缺点。

缺点:

  1. 不能识别BigInt类型。
  2. 不能拷贝 undefined、symbol、函数类型的值。
  3. 不能处理循环引用。

structuredClone()

方法介绍:

structuredClone 用于实现深拷贝,它能够创建一个给定值的完全独立的副本,包括所有嵌套的对象和数组,以及一些特定的内置类型。

语法:

const clone = structuredClone(originalValue);

originalValue可以是一个对象、基本数据类型值、或者某些特殊类型值(如 DateRegExp等),但不包括函数、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
}

相比之下,深拷贝多了一步递归操作,递归地复制其内部的所有对象和数组实现完全拷贝。

深拷贝的实现原理:

  1. 借助for in遍历原对象,将原对象的属性值增加在新对象中

  2. 因为for in会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是否为对象本身的属性

  3. 如果遍历到的属性值是原始值类型时,直接在新对象中赋值,如果是引用类型,递归创建新的子对象。

转载自:https://juejin.cn/post/7371365854011867147
评论
请登录