手写面试问题 2:浅抄与深抄,如何手写浅抄和深抄?
转载请注明原文链接。原文链接
手写面试题系列是我为了准备当下和以后的面试而编写的文章系列,当然对于前端小伙伴也有帮助。我建议读完之后,自己动手敲代码或者手写一遍才能更好地掌握。
一、什么是浅拷贝?什么是深拷贝?
浅拷贝:浅拷贝是对象的按位拷贝。该对象具有原始对象中值的精确副本。如果对象的任何字段是对其他对象的引用,则仅复制引用地址,即仅复制内存地址。
如果更通俗一点儿说就是,浅拷贝的对象(或数组),如果对象中的属性存在引用类型,浅拷贝也只会复制属性的引用地址。
深拷贝:深拷贝副本 所有字段,并复制这些字段指向的动态分配的内存。当一个对象与其引用的对象一起被复制时,就会发生深度复制。
通俗地讲,深拷贝与浅拷贝正好相反,它会复制对象中属性的值而不是引用地址。
由于浅拷贝复制对象时,对于属性中的引用类型复制的是引用地址,那么修改拷贝后的副本或者被拷贝的对象都会同时影响到两个对象。但是深拷贝是要求,被拷贝的对象和副本完全没有关联。
举例说明:
const tom = {
name: '汤姆',
type: '猫',
skill: {
cook: ['烤老鼠'] ,
friends: ['杰瑞'],
}
};
// 浅拷贝
const tom1 = Object.assign({}, tom);
tom1.name = '汤姆一号';
tom.skill.cook.push('老鼠三明治');
tom1.skill.friends.push('斯派克');
console.log(tom.skill.cook, tom1.skill.cook); // ['烤老鼠', '老鼠三明治']
console.log(tom.skill.friends, tom1.skill.friends);// ['杰瑞', '斯派克']
根据输出结果可以看到浅拷贝
后,对于非引用类型的属性的修改是互不干扰的。比如,拷贝后的tom1
改名为“汤姆一号”,不会影响到原对象。
但是对于属性skill
,试图修改里面的属性,同时也会影响到被拷贝后的对象。
上面的例子中"汤姆一号"交到了朋友“斯派克”,“汤姆”学会了新厨艺“老鼠三明治”,结果这些技能同时影响到了原对象和拷贝对象的属性。
因为浅拷贝拷贝引用类型的属性时拷贝的是属性的引用地址,而不是值。
而深拷贝
则要求拷贝引用类型的值,这样修改拷贝后的对象不会影响到原对象。
二、手写浅拷贝
基于上面对于浅拷贝的概念的理解,我先介绍一下Javascript本身就支持的几种实现浅拷贝的方式。
1. Object.assign()实现对象的浅拷贝
**Object.assign() **方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
语法
Object.assign(target, ...sources)
实现浅拷贝的写法就是Object.assign({}, obj)
,就是把被拷贝的对象分配到一个属性为空的对象上,因为Object.assign()
只会把obj的属性(引用类型的属性)的引用分配给目标对象,也就是浅拷贝。
示例
const obj = {a:1, b: {c: 2}};
const objClone = Object.assign({}, obj);
// 修改属性b的值,会影响到源对象和被拷贝的对象
objClone.b.c = 3;
console.log(obj.b, objClone.b); // {c: 3}
2. Array.prototype.concat()、Array.prototype.slice()实现数组的浅拷贝
对于数组而言,可以通过concat()
和slice()
实现浅拷贝。
concat()
方法可以用于拼接数组,slice()
方法可以用于截取数组,方法执行后都会返回一个新的数组。
浅拷贝
用法示例:
const arr = [1, {a: 2}];
const arrClone1 = [].concat(arr); // 或者arr.concat([]);
const arrClone2 = arr.slice();
arr[1].a = 3;
console.log(arr[1].a, arrClone1[1].a, arrClone2[1].a); // 输出 3 3 3
3. 手写浅拷贝
实际工作中,我们应该就使用上面的方式进行浅拷贝即可。对于数组使用concat()
或者slice()
,对于对象使用Object.assign()
。当然我们也可以自己实现一个通用的浅拷贝,但实际意义不大。
根据上面的例子,我们就可以实现一个简单的浅拷贝,如下:
function shallowClone(obj) {
if(typeof obj !== 'object' {
return obj;
}
if(obj instanceof Object) {
return Object.assign({}, obj);
}
if(obj instanceof Array) {
return [].concat(obj);
}
}
还可以通过for in
遍历对象去实现
function shallowClone(obj) {
if(typeof obj !=== 'object' {
return obj;
}
let cloneObj;
if(obj instanceof Object) {
cloneObj = {};
}
if(obj instanceof Array) {
cloneObj = []
}
for(let prop in obj) {
if(obj.hasOwnProperrty(prop)) {
cloneObj[prop] = obj[prop];
}
}
return cloneObj;
}
4. 展开运算符实现浅拷贝
展开运算符...obj
也可以实现浅拷贝,具体用法是:
对于数组:
[...arr]
示例:
const arr = [1, {a: 2}];
const cloneArr = [...arr];
arr.a = 3;
console.log(arr.a, cloneArr.a); // 输出: 3 3
对于对象:
示例:
const obj = {
a: 1,
b: {
c: 2
}
};
const cloneObj = {...obj};
obj.b.c = 3;
console.log(obj.b.c, cloneObj.b.c); // 输出:3 3
根据上面浅拷贝的例子,相信同学们也可以举一反三写出展开运算符的浅拷贝
function shallowClone(obj) {
if(typeof obj !== 'object' {
return obj;
}
if(obj instanceof Object) {
return {...obj};
}
if(obj instanceof Array) {
return [...obj];
}
}
三、手写深拷贝
在手写深拷贝之前,我们可以先探索一下现成可用的深拷贝的方法。
1. JSON.parse(JSOn.stringify(obj))
JSON.parse(JSOn.stringify(obj))
是通过JSON.stringify()
把对象序列化成字符串,然后再把字符串转换成新对象。新的对象和原对象没有关联。因为它是根据字符串转换创建。
- JSON.stringify():将对象转成 JSON 字符串。
- JSON.parse():将字符串解析成对象。
但是这个方法存在一定的局限性,具体包括(不用记忆):
- 不能存放函数或者 Undefined,否则会丢失函数或者 Undefined;
- 不要存放时间对象,否则会变成字符串形式;
- 不能存放 RegExp、Error 对象,否则会变成空对象;
- 不能存放 NaN、Infinity、-Infinity,否则会变成 null;
- 等等,具体来说就是 JavaScript 和 JSON 存在差异,两者不兼容的就会出问题。
2. lodash.cloneDeep()
lodash
是一个景点的JavaScript函数库。里面封装的方法都很好用,其中就包括深拷贝的函数cloneDeep()
。
用法也很简单,在引入lodash后:使用_.cloneDeep(obj)。
3. 手写深拷贝
在列出深拷贝的代码之前,我先简单分析一下深拷贝的思路。
- 深拷贝对象时,需要拷贝对象中的所有属性到新的对象中。对于
值类型
性不用特殊处理,对于函数(typeof obj === 'function')
也不用特殊处理,因为函数没有子集,只需要处理typeof obj === ‘object’
的引用类型。 - 对于
typeof obj === ‘object’
的引用类型可以分为对象和数组,需要分别处理。 - 深拷贝的对象的引用类型的属性中仍可能包含引用类型,可能层层引用,可以看出这里可以用递归算法去解决;
下面是深拷贝的实现
/**
* 深拷贝
* @params obj 要拷贝的对象
*/
function deepClone(obj = {}) {
/** obj是null, 或者不是对象和数组,直接返回;对象和数组需要进一步拷贝对象的属性、数组的元素,通过递归实现 */
if (typeof obj !== "object" || obj == null) {
return obj;
}
// 初始化返回结果
let result;
if (obj instanceof Array) {
result = [];
} else {
result = {};
}
for (const key in obj) {
// 保证key不是从原型取得的属性
if (Object.hasOwnProperty.call(obj, key)) {
// 递归调用
result[key] = deepClone(obj[key]);
}
}
return result;
}