数据存储
在我们了解了js的数据类型之后,接下来我们便需要重点考虑数据的存储问题了,因为其在各大框架中都是很重要的。
基本数据类型因为占用内存较小因而存储在栈内存中,同时当基本类型的变量互相赋值时,不会出现值共享问题。
而引用数据类型因为存储在堆内存中,通过一个存储在栈中的引用(即内存地址)指向其在堆内存中的空间,因此互相赋值的时候便会出现引用值共享问题,那么如何解决这个问题呢?其实只要搞懂了原理不就可以解决这个问题了吗。
浅拷贝
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;
console.log(arr);//[100, 2, 3]
这是直接赋值的情况,不涉及任何拷贝,当改变newArr的时候,由于指向的是同一个引用,因此arr值也会跟着改变(也就是上面提到的值共享问题)
现在便来说说大家所常说的浅拷贝:
let arr = [1, 2, 3];
let newArr = arr.slice();//原理就是将原数组的值拷贝一份放入一个新的数组中并返回
newArr[0] = 100;
console.log(arr);//[1, 2, 3]
此时便可以发现,当修改newArr的时候,arr的值并不改变,因为这里的newArr是浅拷贝arr之后的结果,newArr和arr现在引用的已经不是同一块内存空间了,而这就是大家所常说的浅拷贝。
接下来,我们来研究一下js中到底有多少种方式去实现浅拷贝?到这里其实你应该已经搞清了原理,那方法不就挺多的吗。
1,使用ES6的展开运算符。
2,使用slice方法。
3,使用concat方法
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
4,我们也可以手动实现浅拷贝
function shallowClone(target) {
if (typeof target === "object" && target !== null) {
//第一步:根据是普通对象还是数组对象创建一个空的对象
let cloneTarget = Array.isArray(target) ? [] : {};
//第二步:利用循环将所有元素拷贝一份
for (let i = 0; i < target.length; i++) {
if (target.hasOwnProperty(i)) {
cloneTarget.push(target[i]);
}
}
return cloneTarget;
} else {
return target;
}
}
//我们来测试一下
let arr = [1, 2, 3];
let newArr = shallowClone(arr);
console.log(arr, newArr);
newArr[0] = 10;
console.log(arr, newArr);
5,其实也可以使用 Object.assign 方法(react中的setState原理其实就是利用的这个方法)
let obj = {
a: '1'
}
let newObj = Object.assign({}, obj, { a: '2' });
console.log(obj, newObj);
所以通过上面的几种方法,我们可以发现它们其实使用的都是同一种原理:即通过遍历或者拷贝等方式拿到原数组的所有元素并放入一个新的数组中,最后返回(这不就相当于重新开辟了一块新的内存空间吗)
深拷贝
注意:这里只解决对象嵌套以及循环引用的问题。
学习深拷贝之前,我们先来看下面一段代码:
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]
在这里你会发现为什么改变了newArr下标为2的元素的val值,arr下标为2的元素的val值也跟着变了,这是为什么呢?这就是浅拷贝的限制所在了:它只能拷贝一层对象,如果有对象的嵌套,那么浅拷贝将无能为力,但幸运的是,深拷贝就是为了解决这个问题而生的,它能解决无限级的对象嵌套问题,实现彻底的拷贝。
JSON.parse(JSON.stringify());
估计这个api能覆盖大多数的应用场景,比如:上面对象嵌套的问题,没错,谈到深拷贝,我第一个想到的也是它,但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的,比如:无法解决循环引用的问题,举个例子:
const a = {val:2};
a.target = a;//循环引用
那么此时拷贝a便会出现系统栈溢出,因为出现了无限递归调用的情况,因此这个api先pass掉,我们重新写一个可以解决循环引用问题的深拷贝:首先创建一个Map记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它就行了。
function deepClone(target, map = new WeakMap()) {
const isObject = (target) => {
(typeof target === "object" || typeof target === "function") &&
target !== null;
};
if (isObject(target)) {
map.set(target, true);
let cloneTarget = Array.isArray(target) ? [] : {};
for (let i = 0; i < target.length; i++) {
if (target.hasOwnProperty(i)) {
cloneTarget.push(deepClone(target[i], map));
}
}
return cloneTarget;
} else {
return target;
}
}
//我们来测试一下是否可以解决循环引用的问题
let obj = {
name: "kobe",
};
obj.a = obj;
let newObj = deepClone(obj);
console.log(newObj);
好像是没有问题了, 拷贝也完成了,但还是有一个潜在的坑,就是map 上的 key 和 map 构成了强引用关系,这是相当危险的,被弱引用的对象可以在任何时候被回收 ,而对于强引用来说,只要这个强引用还在,那么对象在程序结束之前便不会被释放回收。怎么解决这个问题呢?很简单,让 map 的 key 和 map 构成弱引用即可,ES6给我们提供了这样的数据结构,它的名字叫weakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的。