大家好,我是小寒。
最近有朋友参加了比亚迪的前端面试,又被问到了一道经典的前端面试题:深拷贝和浅拷贝有什么区别? 本期一起来盘一下这道题。
一、深拷贝和浅拷贝
浅拷贝的特点:
- 只拷贝第一层
- 对于基本数据类型,拷贝值
- 对于引用数据类型,拷贝引用
深拷贝的特点:
- 多层递归拷贝
- 对于基本数据类型,拷贝值
- 对于引用数据类型,创建一个新的引用对象,并循环拷贝到新对象中
举个例子:
const obj = {
a: 1,
b: [1, 2, 3],
c: {
d: 4,
e: 5
}
}
const newObj = deepClone(obj);
console.log(newObj);
/**
打印结果:
{
a: 1,
b: [1, 2, 3],
c: {
d: 4,
e: 5
}
}
*/
对于上面的例子来说,newObj
的打印结果都是相同的,不同的是,对于浅拷贝来说:
- obj.b === newObj.b
- obj.c === newObj.c
而对于深拷贝来说,它们前后都是不相等的。
二、浅拷贝的实现方式
2.1 展开运算符
可以用ECMAScript 2018 规范新增特性,也就是ES6
提供的语法三个点(…)来实现深拷贝,具体用法如下:
const newObj = { ...obj };
2.2 Object.assign
Object.assign
是Object
类自带的一个静态方法,可以将一个或多个对象中的可枚举
(此属性的enumerable为true)自有属性
(对象自身的,从原型上继承的不算)合并到目标对象中。
const newObj = Object.assign({}, obj1, obj2, ...);
这里要注意一下,展开运算符和Object.assign
虽然可以实现浅拷贝,但仍有细微的区别,比如在遇到setter
和getter
时,两者的表现不一样。来举个例子:
首先是展开运算符:
const obj1 = {
get a() {
console.log('getter')
return 1;
},
set a(val) {
console.log('setter')
}
}
const obj2 = {
a: 2,
}
const newObj = { ...obj1, ...obj2 };
console.log(newObj);
console.log(newObj.a)
console.log('------------------------');
const newObj1 = { ...obj2, ...obj1 };
console.log(newObj1);
console.log(newObj1.a)
/**
* 打印结果:
getter
{ a: 2 }
2
------------------------
getter
{ a: 1 }
1
*/
从打印结果可以看出,展开运算符在拷贝时有如下特点:
- 合并时不会执行setter;
- 合并后同名getter和属性会同时存在,但取值时会按照合并的先后顺序,会先取后合并的值。
然后是Object.assign
:
const obj1 = {
get a() {
console.log('getter')
return 1;
},
set a(val) {
console.log('setter')
}
}
const obj2 = {
a: 2,
}
const newObj = Object.assign(obj1, obj2);
console.log(newObj);
console.log(newObj.a)
console.log('------------------------');
const newObj1 = Object.assign(obj2, obj1);
console.log(newObj1);
console.log(newObj1.a);
/**
* 打印结果:
setter
{ a: [Getter/Setter] }
getter
1
------------------------
getter
{ a: 1 }
1
*/
从打印结果可以看出,``Object.assign`在拷贝时有如下特点:
同名属性
和同名getter
、setter
合并时,会执行setter
,而同名getter
、setter
和同名属性
合并时却不会执行setter。- 无论同名属性和同名
getter
和setter
的合并先后顺序如何,最终访问只会访问到getter
里面的值,只是从控制台里看的效果不一样而已。
2.3 for…in + Object.prototype.hasOwnProperty
直接用for..in
循环,配合hasOwnProperty
判断是否是自身的属性来进行拷贝。
function shallowClone(obj) {
const newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
const obj = {
a: 1,
b: [1, 2, 3],
c: {
d: 4,
e: 5
}
};
const newObj = shallowClone(obj);
console.log(newObj)
或者用Object.keys
先拿到自身的属性的数组,然后forEach
循环拷贝也可,方式有很多种,大家可以自行扩展。
三、深拷贝的实现方式
3.1 JSON.parse + JSON.stringify
const newObj = JSON.parse(JSON.stringify(obj));
这种方式实现深拷贝十分简单,我在开发中也经常使用,不过这种方式实现深拷贝有以下缺点:
- 无法处理循环引用,遇到会报错。
- 不支持
function
和undefined
,拷贝后会丢失。 - 无法复制某些对象类型, 遇到比如Date、RegExp、Map、Set、Error、Symbol 等 JavaScript 内置对象及其属性时,会被转换为基本的字符串或数组形式,导致前后的数据类型不一样,不符合预期。
尽管如此,它仍然在实际开发中广泛使用,比如拷贝表单数据进行前后对比等场景。
3.2 使用第三方库
比如使用loddash
:
const _ = require('lodash');
const newObj = _.cloneDeep(obj);
console.log(newObj);
3.3 手动实现深拷贝
const isObj = (target) => typeof target === 'object' && target !== null
function deepClone(obj, hash = new WeakMap()) {
if (!isObj(obj)) return obj;
if (hash.has(obj)) return has.get(obj);
const target = new obj.constructor();
hash.set(obj, target);
Object.keys(obj).forEach((key) => {
target[key] = deepClone(obj[key], hash);
})
return target;
}
const newObj = deepClone(obj);
console.log(newObj);
这里用WeakMap
处理循环引用的问题,通过new obj.constructor
直接构造对象、数组
等对象的实例,然后forEach
复制属性,简单实现一个深拷贝。
四、小结
本文主要分享了一道前端高频面试题,也就是深拷贝和浅拷贝的区别,希望小伙伴们读完后对浅拷贝和深拷贝有更深入的理解。
这里也分享下比亚迪的全部前端面试题:
- 自我介绍;
- 实习中的项目业务介绍;
- 你现在用RN开发吗?那有没有用RN做过一些手机端的适配;
- react和vue你觉得哪个好用?为什么?
- vue的生命周期;
- 双向绑定与响应式原理;
- Vue的单向数据流;
- Vue的组件之间的通信;
- 你刚说到pinia通信,那你知道pinia的原理吗?和Vuex有什么区别?
- 怎么使用pinia?
- Vue-router的所有钩子函数介绍一下;
- vue的单页面和多页面的区别?
- 防抖和节流在实习项目中用过吗?
- vue的computed和watch的区别?
- 用过Vue的脚手架吗?
- vite的原理知道吗?
- react生命周期;
- js中要做异步操作该怎么办?
- 要清除定时器该怎么操作?
- 深拷贝和浅拷贝有什么区别?
大家觉得这个面试题难度如何呢?
标签:面试题,const,log,console,newObj,obj,拷贝,比亚迪 From: https://blog.csdn.net/m0_67275869/article/details/144520768