所有的面向对象的语言中,都存在着对象引用、复制等等问题,对于初学者来说可能难以理解。今天我来总结一下JavaScript中对象复制。
首先我们要知道JavaScript中的数据分为基本类型(单类型) 和 引用类型。
除了Object对象,其余都是基本类型。数组、时间对象以及我们自定义的对象等等,都是继承自Object的,所以说都是引用类型。
在JavaScript中,基本类型有以下几种:
undefined未定义Boolean布尔值Number数值String字符串BigInt长整型SymbolES6中新增的符号类型null空
我们可以使用typeof看一看各种常用类型:
console.log(typeof undefined);
console.log(typeof null);
console.log(typeof true);
console.log(typeof 1);
console.log(typeof 'abc');
console.log(typeof Symbol('sym'));
// 函数
function a() {}
let b = () => {}
console.log(typeof a);
console.log(typeof b);
// 对象
let obj = {};
console.log(typeof obj);
1,基本类型
在js中对对象进行赋值时,基本类型会被直接复制,例如下:
let a = 1;
let b = a;
b = 2;
console.log(a);
console.log(b);
效果:
可见a,b都是number基本类型,直接赋值就把a复制给了b,改变b并不改变a。
同样可以看这个:
function ch(num) {
num = num + 10;
}
let a = 1;
console.log(a);
ch(a);
console.log(a);
结果:
我们试图通过函数改变a的值,但是没有成功。因为把a作为形参传入后,形参会作为传入实参的一个副本,相当于先做了mun = a的操作,再num自己加10,函数结束,num被销毁,num是a的复制品,num变了也不影响a。
可见,基本数据类型赋值操作时,在内存中可以简单表示如下:
2,引用类型
下面建立一个对象,对其进行操作试试看:
// 构造一个角色对象
let miyako = {
name: '宫子',
age: '14',
race: 'ghost',
skills: [{
name: '把你变成布丁',
description: '对敌人造成伤害并回复自身生命值',
injury: 2000
}, {
name: '我~好~恨~啊~',
description: '变身为幽灵,在一段时间内进入无敌状态',
injury: 0
}, {
name: '点心时间到了',
description: '中幅回复自身生命值',
injury: 0
}, {
name: '透明妖怪来咯~',
description: '战斗开始时,中幅提升自身物理防御力',
injury: 0
}],
say: () => {
console.log('布丁布丁布丁布丁布丁~');
}
}
// 新建一个对象,用原对象赋值
let miyakoHelloween = miyako;
// 修改新的对象
miyakoHelloween.name = '宫子(万圣节)';
miyakoHelloween.skills = [{
name: '不给布丁就捣蛋的说',
description: '对最远处的一名敌人造成物理伤害(大),并使其陷入诅咒状态',
injury: 10000
}, {
name: '狼女孩来了的说',
description: '小幅降低最远处一名敌人的技能值,并使其眩晕',
injury: 0
}, {
name: '幽灵木马的说',
description: '中幅降低最远处一名敌人的物理攻击力和魔法攻击力',
injury: 0
}, {
name: '嗷呜嗷呜的说',
description: '战斗开始时,中幅提升自身的物理攻击力',
injury: 0
}];
miyakoHelloween.say = () => {
console.log('不给布丁就捣蛋!');
}
// 分别输出原对象、新对象,并分别调用两者的方法
console.log(miyako);
console.log(miyakoHelloween);
miyako.say();
miyakoHelloween.say();
这里我建立了个自定义对象miyako,然后新建变量miyakoHelloween并使用原对象给其赋值,改变新建变量,原变量会变吗?结果如下:
可见虽然只改变了新对象,但是原对象也被改了,和我们上面的试验结果不一样了,这是为什么呢?
因为我们自定义的对象属于Object类型,属于引用类型,直接赋值给别的对象时,只是发生了引用,相当于 miyako和miyakoHelloween两个名字指向了内存中同一空间。如果改变其中一个,另一个也会发生变化,简而言之,这两个名字实质上指向了同一个数据。
对于引用类型数据,在内存中可以简单表示如下:
这在js中属于浅复制,即我们把已有对象赋值给新对象时并没有给新对象在内存中开辟一个新空间,而是发生引用。
同样把这个对象作为形参传入一个函数并在函数中修改它,你会发现它也发生了改变。
3,再看==和===
在JavaScript中我们都使用==或者===判断两个值是否相等。并且我们也知道,前者判断相等时会作数据类型的隐式转换,但是后者不会。
但是当我们把判断相等运算符用在引用类型数据上是什么情况呢?这时就是判断两者指向的地址是否相同了。并且在对引用类型的对象进行比较时,==和===的作用是一样的。
let a = 1;
let b = 2;
let c = 1;
let s = '1';
let o = {
name: 'oo'
}
let oo = o;
let ooo = {
name: 'oo'
}
console.log(a == b);
console.log(a == c);
console.log(a == s);
console.log(a === s);
console.log(o == oo);
console.log(o === oo);
console.log(o == ooo);
console.log(o === ooo);
结果:
可见总结起来如下:
- 对于基本类型:
==用于判断两者是否值相等,判断时会先进行隐式转换===用于判断两者是否全等,判断时直接比较不会进行隐式转换
- 对于引用类型:
==和===作用相同,用于判断两者指向的内存空间是否是一样的
4,实现深复制
那么我们想要复制一个对象,即实现深复制,怎么做呢?
可以写一个函数,新建一个对象,并通过遍历、递归方式对原对象的属性进行获取,把每个属性依次给新对象复制。如果属性是基本类型数据例如数值字符串等等,我们就可以直接赋值给新对象属性了,因为基本类型直接赋值就是相当于真正的复制了,如果是引用类型那就进行递归复制操作:
/**
* 对一个对象进行深复制
* @param {*} originObject 原对象
* @returns 复制的对象
*/
function copyObject(originObject, map = new WeakMap()) {
// 如果传入对象是基本类型,则直接返回
// null是一种特殊情况,它被判定为Object类型,但是实质上它是个基本类型
// 并且,我们不对函数(Function)对象进行复制,因为深复制函数是没有意义的,并且递归复制函数对象只会得到一个空的函数
if (originObject === null || typeof originObject != 'object') {
return originObject;
}
// 否则,说明是引用类型
// 我们使用map记录已经克隆过的变量,先检测map中是否记录了原对象,如果是说明这个传入对象已被复制过,直接返回
if (map.has(originObject)) {
return map.get(originObject);
}
// 获取原对象的构造函数,并用这个构造函数创建新的对象
let destObject = new originObject.constructor();
// 记录到map里面,表示这个对象已被复制,并传入递归调用,防止循环引用导致内存溢出
map.set(originObject, destObject);
// 开始遍历对象的属性并执行递归复制
for (let key in originObject) {
destObject[key] = copyObject(originObject[key], map);
}
return destObject;
}
来看一下这个代码,主要是使用了递归的思想。除此之外,我们还使用了一个WeakMap对象,用于记录已经克隆过的属性。否则,有的对象存在循环引用(对象其中有属性引用了自己本身),就会发生无限递归导致内存溢出。
WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
然后我们对上面已经构造的对象进行试验:
// 新建一个对象,复制原对象
let miyakoHelloween = copyObject(miyako);
// 修改新的对象
miyakoHelloween.name = '宫子(万圣节)';
miyakoHelloween.skills = [{
name: '不给布丁就捣蛋的说',
description: '对最远处的一名敌人造成物理伤害(大),并使其陷入诅咒状态',
injury: 10000
}, {
name: '狼女孩来了的说',
description: '小幅降低最远处一名敌人的技能值,并使其眩晕',
injury: 0
}, {
name: '幽灵木马的说',
description: '中幅降低最远处一名敌人的物理攻击力和魔法攻击力',
injury: 0
}, {
name: '嗷呜嗷呜的说',
description: '战斗开始时,中幅提升自身的物理攻击力',
injury: 0
}];
miyakoHelloween.say = () => {
console.log('不给布丁就捣蛋!');
}
// 分别输出原对象、新对象,并分别调用两者的方法
console.log(miyako);
console.log(miyakoHelloween);
miyako.say();
miyakoHelloween.say();
结果:
可见通过该方法实现了深复制,无论是对象本身还是对象里面的对象都实现了深复制。
其实在Java、C#等等面向对象的编程语言中,对象的复制、引用都是和上述js中的情况是一样的。