面试官:请你说下深、浅拷贝并且手写深、浅拷贝,我:你咋知道我只会这个?

简介: 面试官:请你说下深、浅拷贝并且手写深、浅拷贝,我:你咋知道我只会这个?

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库  web前端面试题库 VS java后端面试题库大全

一、引言

当我们需要在 JavaScript 中处理对象和数组时,经常需要使用对象和数组的复制功能。JS中有着两种复制方式:深拷贝和浅拷贝。两种方式的复制效果不同,适用场景也不同。

二、什么是浅拷贝和深拷贝?

1.浅拷贝

浅拷贝就是对对象或数组的第一层进行复制,如果这个属性是基本类型数据则直接复制,如果是引用类型数据则只是浅复制一份引用(内存地址),这个引用指向的是原有的引用类型数据。这就意味着,如果复制得到的数据被修改,原有的引用类型数据也会受到影响

2.深拷贝

在 JavaScript 中,深拷贝是指将一个对象或数组完全复制一份,生成一份新的,不管有多少层嵌套关系都要完全独立出来。也就是说,深拷贝实现的是真正意义上的复制而不是一种引用。如果复制后的对象或数组被修改,原来的对象或数组也不会受到影响

三、浅拷贝和深拷贝的区别

使用浅拷贝方式得到的新对象和原对象共享引用类型的数据,因此如果修改新对象中的引用类型数据,原对象也会受到影响,而深拷贝会完全复制一个对象,新对象与原对象间没有任何关系,因此任何修改新对象中的引用类型数据,都不会影响原对象。因此,在处理嵌套数据结构的情况下,深拷贝比浅拷贝更为可靠。

四、实现浅拷贝和深拷贝的方法

1.浅拷贝

1-1.slice()

Array.prototype.slice()方法可以将数组中的一部分元素复制到一个新的数组中,这个方法是浅拷贝,因为它只复制对象的引用而不是对象本身。可以看到,修改了复制对象arr2的值后,原有对象arr1的值也被改了。

let arr1 = [1, 2, { a: 3, b: {c: 4}}];
let arr2 = arr1.slice();
console.log(arr1); //[ 1, 2, { a: 3, b: { c: 4 } } ] 
console.log(arr2); //[ 1, 2, { a: 3, b: { c: 4 } } ] 
arr2[2].b.c = 666;
let arr3 = arr1.slice();
console.log(arr1); //[ 1, 2, { a: 3, b: { c: 666 } } ]
console.log(arr3); //[ 1, 2, { a: 3, b: { c: 666 } } ]

1-2.concat()

Array.prototype.concat()方法可以将数组中的一部分元素复制到一个新的数组中,也可以将多个数组合并成一个新数组。这个方法是浅拷贝,因为它只复制对象的引用而不是对象本身。可以看到,修改了复制对象arr2的值后,原有对象arr1的值也被改了。

let arr1 = [1, 2, { a: 3, b: {c: 4}}];
let arr2 = [].concat(arr1);
// let arr2 = arr1.concat();
console.log(arr1); //[ 1, 2, { a: 3, b: 4 } ]
console.log(arr2); //[ 1, 2, { a: 3, b: 4 } ]
arr2[2].b.c = 666;
let arr3 = arr1.concat();
console.log(arr1); //[ 1, 2, { a: 3, b: { c: 666 } } ]
console.log(arr3); //[ 1, 2, { a: 3, b: { c: 666 } } ]

1-3.Object.assign()

Object.assign()方法可以将多个对象的属性进行浅复制,浅复制只是复制对象的引用,而不是对象本身。可以看到,修改了复制对象obj2的值后,原有对象obj1的值也被改了。

let obj1 = { a: 1, b: {c: 2}};
let obj2 = Object.assign({}, obj1);
console.log(obj1);   //{ a: 1, b: { c: 2 } }
console.log(obj2);   //{ a: 1, b: { c: 2 } }
console.log(obj2.b); //{ c: 2 }
obj2.b.c = 666;
console.log(obj1);   //{ a: 1, b: { c: 666 } }
console.log(obj2);   //{ a: 1, b: { c: 666 } }
console.log(obj2.b); //{ c: 666 }

1-4.Object.create()

Object.create()方法可以将一个对象作为原型,创建一个新的对象。新的对象是浅拷贝原型对象的属性,也就是只复制对象的引用而不是对象本身。可以看到,修改了复制对象obj2的值后,原有对象obj1的值也被改了。

let obj1 = { a: 1, b: {c: 2}};
let obj2 = Object.create(obj1);
console.log(obj1);   //{ a: 1, b: { c: 2 } }
console.log(obj2);   //{}
console.log(obj2.a); //1
console.log(obj2.b); //{ c: 2 }
obj2.b.c = 666;
console.log(obj1);   //{ a: 1, b: { c: 666 } }
console.log(obj2);   //{}
console.log(obj2.a); //1
console.log(obj2.b); //{ c: 666 }

Object.create()方法创建一个新对象,新对象的原型是指定的对象。新对象继承了参数对象的属性,但是并没有属性和方法,所以看起来是个空对象,所以严格来说可能不是浅拷贝,这个方法仅供参考

1-5.扩展运算符(...)

扩展运算符可以将一个对象展开成多个单独的属性,相当于浅拷贝,也是复制对象引用而不是对象本身。可以看到,修改了复制对象obj2的值后,原有对象obj1的值也被改了。

let obj1 = { a: 1, b: {c: 2}};
let obj2 = { ...obj1 };
console.log(obj1);   //{ a: 1, b: { c: 2 } }
console.log(obj2);   //{ a: 1, b: { c: 2 } }
console.log(obj2.b); //{ c: 2 }
obj2.b.c = 666;
console.log(obj1);   //{ a: 1, b: { c: 666 } }
console.log(obj2);   //{ a: 1, b: { c: 666 } }
console.log(obj2.b); //{ c: 666 }

2.深拷贝

2-1.JSON.parse(JSON.stringify())

这种方式的实现是先将对象转换成JSON字符串,再将JSON字符串转换回对象,这样就可以完全复制对象或数组,同时所有数据都是基本类型数据,不存在引用类型数据的互相影响的问题。

let obj = {
    fruit: '水果',
    type: {
      one: {
        name: '哈密瓜',
        price: 10
      },
      two: {
        name: '西瓜',
        price: 20,
        date: new Date(),
        regexp: /^B/,
        birth: undefined
      },
      three: {
        name: Symbol("荔枝"),
        price: 30
      }
    }
};
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2);
// {
//     fruit: '水果',
//     type: {
//       one: { name: '哈密瓜', price: 10 },
//       two: {
//         name: '西瓜',
//         price: 20,
//         date: '2023-06-14T02:50:07.911Z',
//         regexp: {}
//       },
//       three: { price: 30 }
//     }
//  }
obj2.type.one.name = "蓝莓"; //修改拷贝对象,看原对象会不会改变
console.log(obj);
// {
//     fruit: '水果',
//     type: {
//       one: { name: '哈密瓜', price: 10 },
//       two: {
//         name: '西瓜',
//         price: 20,
//         date: 2023-06-14T02:52:16.530Z,
//         regexp: /^B/,
//         birth: undefined
//       },
//       three: { name: Symbol(荔枝), price: 30 }
//     }
//  }

但是需要注意的是,这种方式有缺陷

  1. 无法复制函数RegExp正则表达式等特殊对象
  2. 无法处理循环引用的情况
  3. 无法复制undefinedsymbol类型的属性
  4. 对象中的Date类型会被转换成字符串
  5. 对象中含有NaNInfinity会变成null

五、手写实现深拷贝和浅拷贝

1.浅拷贝

// 简陋版浅拷贝
function shallowCopy(obj) {
  // 判断是否是对象或者数组
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  // 判断当前属性是数组还是对象
  let newObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 复制属性值
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

2.深拷贝

2-1 方法一

// 简陋版深拷贝
function deepCopy(obj) {
  // 如果obj是null,则直接返回
  if(obj === null){
    return null;
  }
  // 如果obj不是对象或数组,则直接返回
  if(typeof obj !== 'object'){
    return obj;
  }
  if (obj instanceof RegExp) return new RegExp(obj);// 处理正则表达式
  if (obj instanceof Date) return new Date(obj);    // 处理日期对象
  // 判断obj是数组还是对象
  let newObj = Array.isArray(obj) ? [] : {};
  // 遍历对象或数组的所有属性或元素
  // 判断是否是显示具有的属性,而不是从原型上继承得到的属性
  for(let key in obj){
    if(Object.prototype.hasOwnProperty.call(obj, key)) {
      // Reflect.ownKeys(obj)
      // 如果属性或元素还是对象或数组,则递归调用深拷贝函数
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
    }
  }
  return newObj;
}

2-2 方法二

MessageChannel接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性发送数据。

function deepCopy(obj) {
  return new Promise((resolve, reject) => {
    // 创建一个新的 MessageChannel 对象,并获取两个端口 port1 和 port2
    const { port1, port2 } = new MessageChannel(); 
    // 将要拷贝的对象通过 port1 发送出去
    port1.postMessage(obj); 
    // 监听 port2 收到的消息
    port2.onmessage = (msg) => { 
    // 当 port2 收到消息时,将消息的数据作为 Promise 的结果进行 resolve
      resolve(msg.data); 
    }
  })
}
deepCopy(obj).then(res => {
  console.log(res);
})

六、最后的话

深拷贝和浅拷贝不存在什么优劣、高级低级之分,在不同的需求场景使用合适的方法即可。

能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎大佬指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝大家生活愉快!

前后端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库  web前端面试题库 VS java后端面试题库大全

相关文章
|
7月前
|
编译器 C++ Python
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
【C/C++ 泡沫精选面试题02】深拷贝和浅拷贝之间的区别?
121 1
|
7月前
|
JSON 前端开发 JavaScript
【面试题】JavaScript 深拷贝和浅拷贝 高级
【面试题】JavaScript 深拷贝和浅拷贝 高级
|
7月前
|
JSON JavaScript 前端开发
【面试题】马上金九银十了,简历该准备起来了,面试题你准备好了吗 ?浅谈 JS 浅拷贝和深拷贝
【面试题】马上金九银十了,简历该准备起来了,面试题你准备好了吗 ?浅谈 JS 浅拷贝和深拷贝
【面试题精讲】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
【面试题精讲】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
|
4月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
7月前
|
存储 JavaScript 前端开发
【JavaScript】面试手撕浅拷贝
引入 浅拷贝和深拷贝应该是面试时非常常见的问题了,为了能将这两者说清楚,于是打算用两篇文章分别解释下深浅拷贝。 PS: 我第一次听到拷贝这个词,有种莫名的熟悉感,感觉跟某个英文很相似,后来发现确实Copy的音译,感觉这翻译还是蛮有意思的
77 6
面试官:深拷贝与浅拷贝有啥区别?
面试官:深拷贝与浅拷贝有啥区别?
|
7月前
|
存储 Java Apache
【面试问题】深拷贝和浅拷贝的区别?
【1月更文挑战第27天】【面试问题】深拷贝和浅拷贝的区别?
|
Java
【java面试题】- java深拷贝和浅拷贝区别?什么是引用拷贝?
java深拷贝和浅拷贝区别?什么是引用拷贝?
92 0
|
存储 JSON JavaScript
面试常问:深拷贝和浅拷贝
在了解深拷贝和浅拷贝之前我们先简单了解一下堆和栈的概念。 堆的概念:在javaScript中,堆是用来存储地址的。比如引用数据类型的地址就是存储在堆中。 栈的概念:栈在javascript中用于存储值的。比如基本数据类型都是直接存在栈内存中的。
下一篇
DataWorks