我们知道js包含基本数据类型和引用类型,基本数据类型和引用类型最大的区别就是变量保存的值是数值还是地址,怎么理解我说的这句话呢? 截屏2020-05-11 下午5.43.32
我们发现基本数据类型在栈中保存的值是对应的数值,而引用保存的是地址,指向堆中的地址。
当我们在做赋值操作的时候,js只会在栈的层面进行一次拷贝。
let a = "wu";
let b = a; // 这个过程是这样的,首先在栈中开辟空间,然后将a的值保存到b所对应的空间中
let obj = {test: 123};
let cloneObj = obj; // 这里跟上面是一样的,只是在栈的层面上进行操作,只不过对象的具体值是保存在堆中的,所以这个赋值的过程其实就是简单的将obj保存的地址拷贝一份保存到cloneObj中。
// 接下来当我们改变obj的时候,这个过程发生了什么
obj.test = 1000;
// 此时如果查看cloneObj的值的时候会发现,其所对应的test属性也发生改变,这是为啥呢。
// obj通过.test的方式改变的时候堆中的内容,而obj与cloneObj在栈中保存的都是一样的地址,所以当obj改变的时候,cloneObj也同样发生了改变。
可以想象成酒店的门牌号一样或者合租的宿舍。
我们理解了js中的赋值,在使用中就可以避开很多坑。
深拷贝是指赋值操作以后,赋值中的一个变量的改变不会影响另外一个变量。
至于为什么需要深拷贝,因为有这样的需求。同样面试官也会经常问到。
使用递归的方式实现,首先想到的是遍历所有的属性,然后进行一对一的赋值。
function cloneObj(obj) {
let clone = {...obj};
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
if (typeof obj[keys[i]] === 'object') {
clone[keys[i]] = cloneObj(obj[keys[i]]);
}
}
return clone;
}
表面上看没有什么问题,但是忽略了不可枚举属性。
Object.defineProperties(obj, {
test: {
value: "wujingyue",
enumerable: true
},
});
如果我是通过这种方式设置的变量,按上面的方式是不能全部获得对象属性的。其实这样就已经满足大部分需求了。因为不可枚举的属性,在JSON的转换中同样不会转换。
// 只需要稍微变化一下就可以了
let clone = {};
let keys = Object.getOwnPropertyNames(obj);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
if (typeof obj[key] === 'object') {
clone[key] = cloneObj(obj[key]);
} else {
clone[key] = obj[key];
}
}
return clone;
如果需要将原有的属性是什么属性就变成什么属性,添加对应的判断进行额外处理即可。我这个地方的判断类型,使用type是不够准确的,但大部分需求是可行的。
不知道看到这里的小伙伴们有没有觉得我的写法不好。我写完以后也发现一个很重要的问题,也就是我返回的是一个函数的局部变量,我们知道返回局部变量的话,会存在内存泄漏的危险,因为我们在使用完成以后可能会忘记销毁。其实在js中一般情况下不会遇到这种情况,根据我的经验,一般需要使用克隆的需求都是在以后的使用过程中不断使用这个变量,即便作为临时变量也会在函数中进行处理,这样的话这个变量只要不保存到全局变量中,那么在函数执行结束后都会自动销毁。只要记住在不使用的时候将其置为null为最佳方案。
补充:如果你的对象里的key值是一个Symbol类型的值,那么基本通过 getOwnPropertyNames 也是获取不到的,需要使用 getOwnPropertySymbols 来获取。