浅拷贝的原理
对于浅拷贝的定义,我们可以大致这么理解:
- 对于基本数据类型,浅拷贝就是将基本类型的值赋给新对象。
- 对于引用数据类型,浅拷贝就是将其内存中的引用赋值给新对象。
浅拷贝的API
在JavaScript中,有提供许多关于浅拷贝的API:
1.“=” 赋值
最基础的赋值方法,只是将对象的引用赋值。
2. Object.assign
Object.assgin是ES6的新函数。Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
但是需要注意的是:
- 它不会拷贝对象的继承属性
- 它不会拷贝对象的不可枚举属性
- 它可以拷贝Symbol类型的属性
- 它可以处理一层的深度拷贝。
let target = {};
let source = { a: { b: 2 }, c: 2, sym: Symbol(1) };
Object.defineProperty(source, 'innumerable' ,{
value: '不可枚举属性',
enumerable: false
});
Object.assign(target, source);
console.log(target); // { a: { b: 10 }, c: 2, sym: Symbol(1) }
source.a.b = 10;
source.c = 10;
console.log(source); // { a: { b: 10 }, c: 10, sym: Symbol(1), innumerable: '不可枚举属性' }
console.log(target); // { a: { b: 10 }, c: 2, sym: Symbol(1) }
3. 扩展运算符
我们也可以使用JS中的扩展运算符,在构造对象的同时完成浅拷贝功能。
使用扩展运算符同样也存在Object.assign一样的缺陷。
let source = { a: { b: 2 }, c: 2, sym: Symbol(1) };
Object.defineProperty(source, 'innumerable' ,{
value: '不可枚举属性',
enumerable: false
});
let target = {...source};
console.log(target); // { a: { b: 10 }, c: 2, sym: Symbol(1) }
source.a.b = 10;
source.c = 10;
console.log(source); // { a: { b: 10 }, c: 10, sym: Symbol(1), innumerable: '不可枚举属性' }
console.log(target); // { a: { b: 10 }, c: 2, sym: Symbol(1) }
4. 对于数组来说,我们可以使用slice和concat实现拷贝的功能
- slice
slice通过控制两个参数来决定原数组截取的开始和结束位置,是不会影响和改变原数组的。
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); // [ 1, 2, { val: 1000 } ]
从上面的例子看出,slice也只能是处理一层的深度拷贝。如果存在对象的嵌套,它也无能为力。
- concat
concat表示将多个数组连接起来,从而实现浅拷贝的功能。但注意的是,当修改原数组中的元素属性时,会影响拷贝之后的数组。
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
浅拷贝的手动实现
对于浅拷贝方法的实现,大致有以下两点思路:
- 对于基础数据类型来说,就是一个最基本的拷贝
- 对于引用数据类型来说,需要开辟一个新的存储,并且拷贝一层对象属性。
const shallowCopy = target => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = target[key];
}
}
return cloneTarget;
} else {
return target;
}
};
深拷贝的原理
深拷贝应该满足以下几点:
- 将一个对象从内存中完整地拷贝出来,并从堆内存中开辟一个全新的空间存放。
- 两个对象之间相互独立不受影响,完全实现了内存上的分离。
深拷贝的实现
1. 使用JSON.stringify序列化
用JSON.stringify把对象转成字符串,再用JSON.parse将字符串转成新的对象。
var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1); // { body: { a: 10 } } <-- 沒被改到
console.log(obj2); // { body: { a: 20 } }
console.log(obj1 === obj2); // false
console.log(obj1.body === obj2.body); // false
但是使用这个方法有以下几点不足:
- 拷贝的对象中的值如果有函数、undefined、symbol这几种类型时,经过JSON.stringify序列化之后的字符串中会消失。
- 拷贝的对象中的值为Date时,会变成字符串。
- 无法拷贝不可枚举的属性。
- 无法拷贝对象的原型链。
- 拷贝的对象中的值为RegExp时,会变成空对象。
- 拷贝的对象中的值为NaN、Infinity或者-Infinity时,会变成null
- 无法拷贝对象的循环应用,即对象成环(obj[key] = obj);
function Obj() {
this.func = function() { alert(1); };
this.obj = { a: 1 };
this.arr = [1, 2, 3];
this.reg = /123/;
this.und = undefined;
this.date = new Date(0);
this.NaN = NaN;
this.Infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable: false,
value: 'innumerable'
});
console.log('obj1', obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2', obj2);
打印出来的结果如下:
2. 递归方式实现深拷贝
const deepClone = (origin) => {
let target = null;
if (typeof origin === "object" && origin !== null) {
target = Array.isArray(origin) ? [] : {};
for (let key in origin) {
target[key] = deepClone(origin[key]);
}
} else {
target = origin;
}
return target;
}
再次拿上面的例子做实验:
function Obj() {
this.func = function() { alert(1); };
this.obj = { a: 1 };
this.arr = [1, 2, 3];
this.reg = /123/;
this.und = undefined;
this.date = new Date(0);
this.NaN = NaN;
this.Infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable: false,
value: 'innumerable'
});
console.log('obj1', obj1);
let obj2 = deepClone(obj1);
console.log('obj2', obj2);
根据结果,我们可以得出以下几点:
- 这个深拷贝方法不可拷贝不可枚举的属性
- 这个方法对于Date、RegExp不能够正确的拷贝
- 无法拷贝对象的循环应用,即对象成环(obj[key] = obj);
3.改进版深度拷贝
要更好的实现一个深拷贝的方法,那么必须要了解以下四点:
- 对于不可枚举的属性,我们采用Reflect.ownKeys方法。
- 当参数为Date、RegExp类型时,则直接生成一个新的实例返回。
- 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对应的特性。顺便结合Object.create创建对象,并继承传入原对象的原型链。
- 利用WeakMap类型作为哈希表,作为检测循环引用。如果存在循环,则引用直接返回WeakMap存储的值。
const isComplexDataType = obj => (typeof obj === "object" || typeof obj === "function") && obj !== null;
const deepClone = function(origin, hash = new WeakMap()) {
if (origin.constructor === Date) return new Date(origin);
if (origin.constructor === RegExp) return new RegExp(origin);
// 如果循环引用就用WeakMap解决
if (hash.has(origin)) return hash.get(origin);
// 遍历传入参数所有键的特性
let allDesc = Object.getOwnPropertyDescriptors(origin);
// 继承原型链
let cloneObj = Object.create(Object.getPrototypeOf(origin), allDesc);
hash.set(origin, cloneObj);
for (let key of Reflect.ownKeys(origin)) {
cloneObj[key] = (isComplexDataType(origin[key]) && typeof origin[key] !== "function" ? deepClone(origin[key], hash) : origin[key];
}
return cloneObj;
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!