“一篇文章教会你掌握JavaScript深拷贝和浅拷贝!”

在计算机编程中,"拷贝"是将一个数据复制到另一个位置或变量的过程。拷贝通常包括将数据从源位置复制到目标位置,并在目标位置创建一个新的、与源数据相同的副本。

在编程中,我们经常需要"拷贝"数据,以便对其进行处理、存储或传输。例如,在JavaScript中,我们可能需要将一个对象的值复制到另一个对象中,或者将一个数组复制到另一个数组中。拷贝数据可以帮助我们避免直接修改原始数据,从而保持数据的完整性和正确性。

浅拷贝(Shallow Copy)

浅拷贝指的是只复制对象或数组本身,而不复制它们内部引用的其他对象或数组。也就是说,浅拷贝会创建一个新的对象或数组,并将原始对象或数组中的元素复制到新的对象或数组中,但是这些元素仍然是原始对象或数组中元素的引用。

简单来说就是通过方法把某个对象完整拷贝后,原对象的修改会影响新的对象。

在JavaScript中,我们可以使用一些方法实现浅拷贝,比如 - 常见的浅拷贝方法:

  1. Object.create(obj)
  2. Object.assign({ } , obj)
  3. [ ].concat(arr)
  4. 数组解构
  5. arr.toReversed().reverse()

方法一:Object.create(obj)

Object.create() 是 JavaScript 中的一个方法,它用于创建一个新对象,并将一个已有的对象作为新对象的原型。

ini 复制代码
let obj1 = {
 name: '小明'
}
let obj2 = Object.create(obj1)
obj1.name = '小红'

console.log(obj2.name); // 输出 '小红'

在上面的代码中,我们先定义了一个名为 obj1 的对象,然后使用 Object.create() 方法创建了一个新对象 obj2,并将 obj1 作为其原型。由于 obj2 继承了 obj1 的属性,因此我们可以通过 obj2.name 修改 obj1.name 的值。

方法二:Object.assign({} , obj)

Object.assign({} ,obj) 方法会将所有可枚举的自有属性从一个或多个源对象复制到目标对象,并返回目标对象。该方法执行的是浅拷贝,因此如果源对象的属性值是基本类型数据,那么它们会被复制到目标对象中,如果是引用类型数据,则仅复制它们的引用。

ini 复制代码
    let obj = {
    name: '小明',
    like: {
        n: 'coding'
    }
}
let obj2 = Object.assign({}, obj );
obj.name = '小红' ;
obj.like.n = 'running' ;
console.log(obj2); 

运行上述代码后,obj2的输出结果为:

css 复制代码
    { name: '小明',
    like: { n: 'running' } }

为什么输出结果name不会改变,like里面n会改变呢?

这是因为在使用 Object.assign() 复制对象时,只有源对象的第一层属性被复制,而内部的对象引用则是浅拷贝。所以,当修改 obj.like.n 的值时,obj2.like.n 同样会发生变化。但是 obj.name 的修改不会影响到 obj2.name,因为它们是两个不同的值。

我们也可以用内存的角度来解释为什么这样:

yaml 复制代码
a = 1003
1003 => {
    name: '小明',
    like: 1101
}
1101 => {
    n: 'coding'
}
b = 1004
1004 => {
        name: '小明',
        like:  1101
}

补充

Object.assign({}, obj)Object.create(obj) 有以下区别:

  1. 创建方式:Object.assign() 是一个静态方法,用于将一个或多个源对象的属性复制到目标对象中。它创建了一个新的目标对象,并将源对象的属性复制到目标对象中。而 Object.create() 是一个原型方法,用于创建一个新对象,以指定的原型对象作为其原型。
  2. 对象关系:Object.assign() 创建的新对象是一个独立的对象,与源对象没有原型链关系。它只是将源对象的属性复制到新对象中。而 Object.create() 创建的新对象会继承原型对象的属性和方法,形成了原型链关系。
  3. 属性复制:Object.assign({}, obj) 将源对象的可枚举属性复制到目标对象中。如果目标对象和源对象具有相同名称的属性,则会覆盖目标对象的属性值。而 Object.create(obj) 创建的新对象继承了原型对象的所有属性和方法,包括可枚举和不可枚举属性。
  4. 原型链:Object.assign() 不会创建原型链,只是简单地复制属性。而 Object.create() 创建的新对象会与原型对象建立原型链关系,通过原型链继承原型对象的属性和方法。

总而言之,Object.assign({}, obj) 用于复制对象的属性到一个新的独立对象中,没有原型链关系。而 Object.create(obj) 用于创建一个新对象,并将原型对象作为其原型,建立了原型链关系。

方法三:[ ].concat(arr)

[ ].concat() 方法会创建一个新数组,并将原始数组中的元素复制到新数组中。由于该方法执行的是浅拷贝,因此如果数组的元素是基本类型数据,那么它们会被复制到新数组中,如果是引用类型数据,则仅复制它们的引用。

ini 复制代码
let arr = [1,2,3,{n:10}]
let newArr = [].concat(arr)
arr.push(4)
arr[3].n = 100
console.log(newArr);

首先创建了一个名为 arr 的数组,其中包含数字和一个包含属性 n 的对象。然后,使用 arr.push(4) 将数字 4 添加到 arr 数组的末尾。

接下来,通过使用 .concat(arr) 方法,将 arr 数组的内容复制到一个新的数组 newArr 中。这样做是为了创建一个与原始数组相同的副本,以便后续进行比较。

然后,通过修改 arr[3].n 的值为 100,也就是修改了 arr 数组中索引为 3 的对象的属性 n 的值。

最后,打印 newArr 的结果是 [1, 2, 3, { n: 100 }]。这是因为 newArr 是通过浅拷贝 arr 创建的,所以它们共享相同的对象引用。当修改 arr[3].n 的值时,newArr 中相应的元素也会被修改。

方法四:数组解构

ini 复制代码
    let arr = [1,2,3,{n:10}]
    arr.push(4)
    let newArr = [...arr]
    arr[3].n = 100
    console.log(newArr);

这段代码和上面方法三作用效果差不多,第四行代码 let newArr = [...arr] 是使用展开运算符 [...arr] 对数组 arr 进行浅拷贝,并将拷贝的结果赋值给新的数组 newArr

展开运算符 [...arr] 可以将数组 arr 展开为多个元素,然后将这些元素放入一个新的数组中。这样就实现了对原始数组的浅拷贝,创建了一个新的数组,并复制了原始数组中的所有元素。

在这段代码中,通过 [...arr] 将数组 arr 中的所有元素展开,并将它们放入一个新的数组中,赋值给变量 newArr。因此,newArr 中包含了与 arr 相同的元素,实现了对 arr 数组的浅拷贝。

这种方式相比于使用 concat() 方法进行浅拷贝,使用展开运算符 [...arr] 语法更加简洁和直观。

知识补充

for...in 语句 和 hasOwnProperty( )

"for...in" 是一种常见的循环结构,在编程中用于遍历可迭代对象(例如列表、字符串、字典等)中的元素。

hasOwnProperty 是 JavaScript 中的一个对象方法,用于判断一个对象是否包含指定名称的属性(不会检查原型链)。这个方法通常用于避免访问对象原型链上的属性时出现意外情况。

vbnet 复制代码
    let obj = {
    name: '涛哥',
    age: 18,
    like: {
        n: 'reading'
    }
}
let o = Object.create(obj)
o.sex = 'boy'
console.log(o);
for (let key in o) {  // key是属性名
    if (o.hasOwnProperty(key)) {
        console.log(key );
    }
}

这段代码是 JavaScript 代码,主要功能是创建一个对象 obj,然后用它作为原型创建了一个新的对象 o,并输出 o 的属性,以及使用 for...in 循环遍历 o 的属性名并输出其自身的属性名。

for (let key in o) {...}:使用 for...in 循环遍历对象 o属性名 ,并输出属性名。其中,if (o.hasOwnProperty(key)) 判断该属性是否为 o 对象本身的属性,防止继承来的属性也被遍历到。

输出结果为:

css 复制代码
{sex: 'boy' }
sex

浅拷贝的应用

观察下面代码:

ini 复制代码
    let obj = {
    name: '涛哥',
    age: 18,
    like: {
        n: 'reading'
    }
};

function shallowCopy(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;
}
let newObj = shallowCopy(obj);
obj.like.n = 'running';
console.log(newObj);

下面是每行代码的解释:

javascript 复制代码
function shallowCopy(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;
}
  • 这行代码定义了一个名为 shallowCopy 的函数,接收一个参数 obj
  • 函数首先检查 obj 的类型是否不是对象或者为 null。如果是,则立即返回,不执行任何拷贝操作。
  • 接着,根据输入对象的类型创建一个新的变量 objCopy。如果 obj 是数组,则将 objCopy 初始化为空数组。否则,如果 obj 是对象,则将 objCopy 初始化为空对象。
  • 然后,函数使用 for...in 循环遍历输入对象的键。
  • 对于每个键,使用 hasOwnProperty 方法判断该键是否直接存在于对象上(而不是从原型继承而来)。
  • 如果键存在,则将输入对象 obj 中对应键的值赋值给新对象 objCopy 中的相同键。
  • 完成所有键的遍历后,函数返回新的对象 objCopy
ini 复制代码
let newObj = shallowCopy(obj);
  • 这行代码调用 shallowCopy 函数,将 obj 对象作为参数传递进去。
  • 函数的返回值被赋值给一个新的变量 newObj
ini 复制代码
obj.like.n = 'running';
  • 这行代码修改了 obj 对象内部嵌套对象 liken 属性。将其值从 'reading' 修改为 'running'
arduino 复制代码
console.log(newObj);
  • 最后,这行代码将 newObj 对象打印到控制台。

  • 由于 shallowCopy 进行的是浅拷贝,修改原始对象 obj 也会影响到拷贝对象 newObj

  • 因此,控制台输出将显示 newObj 中的 like 对象的 n 属性已经更新为 'running'

    输出结果为:

css 复制代码
    {  name: '涛哥',
     age: 18,
     like: {
     n: 'running' 
     } 
    }

深拷贝(Deep Copy)

深拷贝(Deep Copy)是创建一个新的对象,该对象与原始对象完全独立,在内存中占据不同的位置。深拷贝会复制原始对象的所有属性和嵌套对象,并且对其中一个对象的修改不会影响到另一个对象。

方法一:

javascript 复制代码
    let obj = {
    name: '李总',
    age: 18,
    a: {
        n: 1
    },
    b: undefined, 
    c: null,
    d: function() {},
    e: Symbol('hello'),
    f: {
        n: 100
    }
}
function deepCopy(obj) {
    let objCopy = {} // 创建一个空对象,用于存放拷贝后的对象
    for (let key in obj) { // 遍历对象的所有属性
    if (obj.hasOwnProperty(key)) { // 使用hasOwnProperty方法确保只拷贝对象自身的属性
     if (obj[key] instanceof Object) { // 判断属性值是否为引用类型
       objCopy[key] = deepCopy(obj[key]); 
            } else {
                objCopy[key] = obj[key] // 如果是原始类型,则直接赋值
            }
        }
    }
    return objCopy
}
let obj2 = deepCopy(obj);
console.log(obj2);

下面是对代码的一些解释:

vbnet 复制代码
function deepCopy(obj) {
   let objCopy = {}
   for (let key in obj) {
       if (obj.hasOwnProperty(key)) {
           if (obj[key] instanceof Object) {
               objCopy[key] = deepCopy(obj[key]);
           } else {
               objCopy[key] = obj[key]
           }
       }
   }
   return objCopy
}
  • 这里定义了一个名为 deepCopy 的函数,用于进行深拷贝操作。该函数接受一个对象作为参数。
  • 函数内部首先创建了一个空对象 objCopy,用于存放拷贝后的对象。
  • 然后使用 for...in 循环遍历原始对象 obj 的所有属性。
  • 对于每个属性,使用 hasOwnProperty 方法确保只拷贝对象自身的属性。
  • 判断属性值是否为引用类型,如果是引用类型,则递归调用 deepCopy 函数进行深拷贝。
  • 如果是原始类型,则直接赋值给 objCopy
  • 最后,返回拷贝后的对象 objCopy
ini 复制代码
let obj2 = deepCopy(obj);
console.log(obj2);
  • 这里调用了 deepCopy 函数,将 obj 对象作为参数传递进去,并将返回值赋给新的变量 obj2
  • 最后,将 obj2 对象打印到控制台。

这样,整个代码就是对对象进行深拷贝,并将拷贝后的对象打印到控制台。

方法二:

JSON.parse(JSON.stringify(obj)) 是另一种深度拷贝对象的方法,其原理是将对象转换为 JSON 字符串,再将 JSON 字符串解析成新的对象,从而得到对象的一个副本。

还是以上面例子:

javascript 复制代码
    let obj = {
    name: '李总',
    age: 18,
    a: {
        n: 1
    },
    b: undefined, 
    c: null,
    d: function() {},
    e: Symbol('hello'),
    f: {
        n: 100
    }
}
console.log(obj);
console.log(JSON.stringify(obj));// 把对象变成字符串
console.log(JSON.parse(str)); // 把字符串变成对象

输出结果为:

在控制台中,字符串是没有颜色的

所以为了达到深拷贝效果我们可以:

javascript 复制代码
     let obj = {
    name: '李总',
    age: 18,
    a: {
        n: 1
    },
    b: undefined, 
    c: null,
    d: function() {},
    e: Symbol('hello'),
    f: {
        n: 100
    }
}
let obj2 = JSON.parse(JSON.stringify(obj)) //达到深拷贝效果
obj.age = 20
obj.a.n = 2
console.log(obj2);

输出为: 该方法的优点是简单易懂,适用于大多数情况下的对象拷贝,但也有以下几个缺点:

  1. 该方法无法拷贝函数和 RegExp 等特殊对象。
  2. 该方法会忽略 undefined 和 Symbol 属性。
  3. 如果对象中存在循环引用,该方法会报错。
  4. 对于 Date 类型的属性,该方法会将其序列化成一个 ISO 格式的字符串,然后再解析出来,而不是直接拷贝 Date 对象。

今天的内容就是这些啦~欢迎大家指正、补充

相关推荐
一颗花生米。18 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐0122 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199523 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&1 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈1 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光6 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   6 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发