浅拷贝(shallow copy)与深拷贝(deep copy)
深浅拷贝都是对于引用数据类型而言的,对于基本数据类型的拷贝,并没有深浅拷贝的区别。
浅拷贝
- 仅复制最外一层,对于内层是相同的引用。
- 对于基本数据类型的属性,拷贝值。源对象和拷贝对象开辟不同的内存空间,不共享。
- 对于引用数据类型的属性,拷贝内存地址。源对象和拷贝对象指向同一块内存空间,共享。
深拷贝
- 在堆内存中开辟新的空间存储数据对象,源对象和拷贝对象存储地址不同,不共享。
赋值 与 浅拷贝
赋值
- 基本数据类型:在栈内存空间创建一块数据对象,被变量引用,不同变量引用的内存不同。
- 引用数据类型:赋值时变量引用的是存储在栈内存中的一个地址,这个地址指向存储在堆空间的引用数据类型对象。即对变量的赋值是赋给地址,堆空间的数据共享。
区别
针对引用数据类型的赋值与浅拷贝存在区别:
-
赋值时,这个引用数据类型变量的内存数据是共享的,它的基本数据类型属性改变了,源对象和拷贝对象都会改变。
-
浅拷贝时,对于基本数据类型的属性,拷贝值,不共享不会一起变化。引用数据类型属性,拷贝地址,内存数据共享,会联动变化。
-
举例:
const source = { A: 'A', referProp: ['A','B','C'] } // 赋值 const obj1 = source; obj1.A = 'newA'; obj1.referProp[0] = 'newA' console.log(source.A) // newA console.log(source.referProp) // ['newA','B','C'],都改变了 // 浅拷贝 const obj2 = {...source} obj2.A = 'newA'; obj2.referProp[0] = 'newA' console.log(source.A) // A --不改变 console.log(source.referProp) // ['newA','B','C'] --改变
浅拷贝实现
在 JavaScript 中,所有标准的内置对象复制操作(展开语法、Array.prototype.concat()、Array.prototype.slice()、Array.from()、Object.assign() 和 Object.create())创建的都是浅拷贝。
扩展运算符
const source = {
A: 'A',
referProp: ['A','B','C']
}
// 浅拷贝
const result = {...source}
Object.assign
const source1 = {
A: 'A',
referProp: ['A','B','C']
}
const source2 = { B: 'B' }
// 浅拷贝
const result = Object.assign({}, source1, source2)
// {A: 'A', referProp: ['A','B','C'], B: 'B'}
原生数组方法
const arrSource1 = ['A' ,['a','b','c'], 'C']
const arrSource2 = ['a' ,['1','2','3'], 'c']
const result = arrSource1.concat(arrSource2);
const arrSource = ['A' ,['a','b','c'], 'C']
const result = arrSource1.slice();
const arrSource = ['A' ,['a','b','c'], 'C']
const result = Array.from(arrSource);
手写
function shallowCopy (target) {
// 基本类型直接返回
if (!target || typeof target !== "object") return target;
// 判断对象or数组
let result = Array.isArray(params) ? [] : {};
// 遍历拷贝属性
for (let key in target) {
if (target.hasOwnProperty(key)) { // 只拷贝target自有的属性
result[key] = target[key];
}
}
return result;
}
深拷贝实现
函数库lodash
const _ = require('lodash');
const source = {
A: 'A',
referProp: ['A','B','C']
};
const result = _.cloneDeep(source);
JSON.stringify
使用 JSON.stringify() 将可以被序列化的对象转换为 JSON 字符串,然后使用 JSON.parse() 将该字符串转换回(全新的)JavaScript 对象。
const source = {
A: 'A',
referProp: ['A','B','C']
};
const result = JSON.parse(JSON.stringify(source));
存在问题
- 拷贝的对象中如果有 function、undefined、symbol,当使用过
JSON.stringify()进行处理之后,都会消失。 - 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝
Date引用类型会变成字符串; - 拷贝
RegExp引用类型会变成空对象; - 对象中含有
NaN、Infinity以及-Infinity,JSON序列化的结果会变成null; - 无法拷贝对象的循环引用,即对象成环 (
obj[key] = obj)。
手写
只拷贝基本数据类型、数组Array、对象{}
参考:前端面试 第三篇 js之路 深拷贝与浅拷贝 - 掘金 (juejin.cn)
// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/[object (\w+)]/, "$1").toLowerCase();
// 实现深拷贝(仅仅为Object/Array)
const clone = (target, hash = new WeakMap) => {
let result;
let type = checkedType(target);
if (type === 'object') result = {};
else if (type === 'array') result = [];
else return target;
let copyObj = new target.constructor();
if (hash.has(target)) return hash.get(target);
hash.set(target, copyObj)
for (let key in target) {
if (checkedType(target[key]) === 'object' || checkedType(target[key]) === 'array') {
result[key] = clone(target[key], hash);
} else {
result[key] = target[key];
}
}
return result;
}
兼容处理多种类型,基本满足使用
// 分别要处理
// 1. 基本类型;
// 2. 引用类型:数组、对象、map、set(可循环)
// 3. 不可循环类型:基本类型的包装类型(Boolean、Number、String、Symbol) 和 部分引用类型(Function、RegExp、Date)
// 可循环类型
const traverseTypes = ['array', 'object', 'map', 'set', 'argument'];
// 检测数据类型的功能函数
const checkedType = (target) => Object.prototype.toString.call(target).replace(/[object (\w+)]/, "$1").toLowerCase();
// 拷贝RegExp的方法
const cloneRegExp = (source) => {
const reFlags = /\w*$/;
const result = new source.constructor(source.source, reFlags.exec(source));
result.lastIndex = source.lastIndex;
return result;
}
// 拷贝不可遍历的对象类型
const cloneOtherType = (obj, type) => {
switch (type) {
case 'boolean':
case 'number':
case 'string':
case 'date':
return new obj.constructor(obj.valueOf());
case 'symbol':
return Object(obj.valueOf());
case 'regexp':
return cloneRegExp(obj);
case 'function': // function不做处理
return obj;
}
}
// 实现深拷贝
const deepClone = (target, map = new WeakMap) => {
const type = checkedType(target);
// 1. 处理基本数据类型,直接返回
if (target instanceof Object === false) return target
let result;
// 2.
if (traverseTypes.includes(type)) {
// 如果是可遍历类型,直接创建空对象
result = new obj.constructor();
} else {
// 若不是,则走特殊处理
return cloneOtherType(target, type);
}
// 3. 解决循环引用问题
result = new target.constructor();
if (map.has(target)) return map.get(target);
map.set(target, result)
/* ----------------处理Map、Set的深拷贝---------------- */
// 4. 处理Map类型
if (type === 'map') {
target.forEach((value, key) => {
result.set(key, deepClone(value, map))
})
return result
}
// 5. 处理Set类型
if (type === 'set') {
target.forEach(value => {
result.add(deepClone(value, map))
})
return result
}
/* ----------------处理数组和对象{}类型---------------- */
// 6. 处理数组和对象{}
for (let key in target) {
if (target.hasOwnProperty(key)) {
result[key] = deepClone(target[key], map);
}
}
return result;
}
const obj = {
// 基本类型
str: 'test',
num1: 123,
boolean: true,
sym: Symbol('独一无二key'),
// 引用类型(以下8种数据对象均需进行真正意义上的深拷贝)
obj_object: { name: 'squirrel' },
arr: [123, [1, 23, 2], '456'],
func: (name, age) => console.log(`姓名:${name},年龄:${age}岁`),
map: new Map([['t', 100], ['s', 200]]),
set: new Set([1, 2, 3]),
date: new Date(),
reg: new RegExp(/test/g),
// 包装类
num2: new Number(123),
}
obj.loop = obj;
const objCopy = deepClone(obj);
obj.set.add(123)
obj.arr[0] = 999
obj.loop.arr[0] = 888
obj.func = [] // 改变源对象,不会影响深拷贝的copy对象
console.log(obj, 'target')
console.log(objCopy, 'result')
console.log(obj.loop == obj) // true
console.log(objCopy.loop == objCopy) // true