「JavaScript深入」深拷贝与浅拷贝,如何手写实现?
JavaScript深入 — 深拷贝与浅拷贝
- 浅拷贝
- 实现
- 手写浅拷贝
- 深拷贝
- JSON.parse(JSON.stringify(obj))
- 边界
- 手写深拷贝
- 浅拷贝:浅拷贝得到的对象会受原对象的影响(同样可以影响原对象)
- 深拷贝:深拷贝得到的对象不会受到原对象影响
浅拷贝
实现
Object.assign
Array.prototype.slice()
、Array.prototype.concat()
- 使用拓展运算符实现的赋值
手写浅拷贝
// 实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let key in obj) {
if(obj.hasOwnProperty(key)){
newObj[key] = obj[key];
}
}
return newObj;
}
深拷贝
JSON.parse(JSON.stringify(obj))
JSON.stringify()
将对象序列化成json
对象(转换成JSON字符串)JSON.parse()
将json
对象反序列化成js
对象(解析成对象)
// 使用 JSON.parse(JSON.stringify(obj)) 实现深拷贝
const obj = { name: '高富帅', age: 20 }
const newObj = JSON.parse(JSON.stringify(obj))
const arr = [1, '1', null, true, undefined]
const newArr = JSON.parse(JSON.stringify(arr))
arr[1] = 'a'
obj.name = 'song'
console.log(newObj); // { name: '高富帅', age: 20 }
console.log(newArr); // [1, '1', null, true, undefined]
这个方法其实已经能适用于大部分的应用场景,毕竟很少有情况需要去拷贝一个函数什么的
🤔 但这里面的确有一些问题,是 JSON.stringify()
这个方法本身的实现逻辑带来的,该方法有下面特点:
- 布尔值、数值、字符串对应的包装对象,在序列化过程会自动转换成其原始值。
undefined
、任意函数
、Symbol
值,在序列化过程有两种不同的情况。若出现在非数组对象的属性值中,会被忽略;若出现在数组中,会转换成null
。任意函数
、undefined
被单独转换时,会返回undefined
。- 所有以
Symbol
为属性键的属性都会被完全忽略,即便在该方法第二个参数replacer
中指定了该属性。 Date
日期调用了其内置的toJSON()
方法转换成字符串,因此会被当成字符串处理。NaN
和Infinity
的数值及null
都会当做null
。- 这些对象
Map
、Set
、WeakMap
、WeakSet
仅会序列化可枚举的属性。 - 被转换值如果含有
toJSON()
方法,该方法定义什么值将被序列化。 - 对包含
循环引用
的对象进行序列化,会抛出错误。
🔗 实现深拷贝总结—— JSON.parse(JSON.stringify(obj))
边界
上面已经说明了 JSON.stringify()
还有很多情况不能处理,虽然我们不能一下记住这些情况,但至少说明了如果要自己实现一个深拷贝的方法,还有很多边界问题要处理
-
常见的边界Case
主要有循环引用,包装对象、函数、原型链、不可枚举属性、
Map/WeakMap
、Set/WeakSet
、RegExp
、Symbol
、Date
、ArrayBuffer
、原生DOM/BOM
对象等 -
第三方库
目前而言最完善的深拷贝方法是 Lodash 库的_.cloneDeep(value)
方法🔗 Lodash中文文档 —— _.cloneDeep(value)
手写深拷贝
手写一个简易的深拷贝:
const deepCopy = (source) => {
//判断是否为arr
const isArray = (arr) =>
Object.prototype.toString.call(arr) === "[object Array]";
//判断是否为object和function
const isObject = (obj) =>
obj !== null && (typeof obj === "object" || typeof obj === "function");
//递归拷贝
const copy = (input) => {
//如果是函数或者不是对象,直接返回(函数和非对象不需要深拷贝)
if (typeof input === "function" || !isObject(input)) return input;
const output = isArray(input) ? [] : {};
for (let key in input) {
//确保只复制对象自身的属性,而不是原型链上的属性
if (input.hasOwnProperty(key)) {
output[key] = copy(input[key]);
}
}
return output;
};
return copy(source);
};
验证深拷贝结果:
const school2 = deepCopy(school);
school2.name = "buaa2";
console.log(school.name); // buaa
console.log(school2.name); // buaa2
const arr1 = [1, 2];
const arr2 = deepCopy(arr1);
arr2[0] = 3;
console.log(arr1[0]); // 1
console.log(arr2[0]); // 3
对于很多边界情况,以上简易版本还有许多地方需要处理,需要具体情况具体分析,但主要思想就是通过这样递归的方法,实现引用类型的深拷贝