目录
一文弄懂Javascript 深拷贝与浅拷贝
1 Javascript数据存储规则
Javascript
中存在两大数据类型:
-
基本类型
-
引用类型
基本类型主要为以下6种,数据保存在在栈内存中:
- Number
- String
- Boolean
- Undefined
- null
- symbol
栈内存图解:
let a = 10;
let b = a;
b = 20;
// 给b赋值不影响a的值
console.log(a); // 10;
引用类型主要为以下三种,数据保存在堆内存中:
- Object
- Array
- Function
引用数据类型的变量是一个指向堆内存中实际对象的引用,变量以及指向堆内存的指针存在在栈内存中;数据存在堆内存中。
堆内存图解:
var obj1 = {
name: 'jone'
};
// 将obj1的内存地址赋给obj2
var obj2 = obj1;
// 修改堆内存对象属性值
obj2.name = "sam";
console.log(obj1.name); // sam;对obj2的属性修改影响了obj1的属性;
2 浅拷贝
浅拷贝,指的是创建新的栈内存数据,这个数据有着原始数据属性值的一份精确拷贝;
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址;
即浅拷贝是拷贝一层,对于基本类型来讲,第一层栈内存即可;
在Javascript 中,绝对浅拷贝的有:
- 赋值运算符"="
上面的案例即为浅拷贝案例,只拷贝了第一层对象的栈内存村粗的变量以及引用地址;
var obj1 = {
name: 'jone'
};
// 将obj1的内存地址赋给obj2
var obj2 = obj1;
// 修改堆内存对象属性值
obj2.name = "sam";
console.log(obj1.name); // sam;对obj2的属性修改影响了obj1的属性;
3 部分深拷贝
部分深拷贝的意思就是不止拷贝一层,对数据拷贝两层及以上;但没有覆盖所有存储层级,称之为部分深拷贝;
以下运算可以实现一级引用类型的深拷贝,当有多级引用类型时,二级属性之后的就是浅拷贝,
- Object.assign
- Array.prototype.slice()
- Array.prototype.concat()
- 拓展运算
...
符实现的复制
3.1 Object.assign
Object.assign(target,source)
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target);
var obj = {
age: 18,
names: {
name1: 'jone',
name2: 'sam'
}
}
var newObj = Object.assign({}, Obj);
// 修改对象的第一层属性
newObj.age =20;
// 修改对象的第二层属性
newObj.names.name1="change"
// 最后的结果是
console.log(obj);
/*
修改后的obj
{
"age": 18,
"names": {
"name1": "change",
"name2": "sam"
}
*/
我们可以看到,修改新对象的第一层属性和第二层属性,源对象的第一层属性没有发生变化,第二层属性却发生了变化,原因是Object.assign
只对原始栈内存和第一层属性重新赋予了存储空间,也就是说只拷贝了两层;所以才会出现如上效果,图解如下:
修改前:
修改后:
以下三个运算符的部分深拷贝原理与此相同;
3.2 slice()
slice()
方法用于提取目标数组的一部分,返回一个新数组,原数组不变。它的第一个参数为起始位置(从0开始,会包括在返回的新数组之中),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员。
const Arr = ["One", "Two", "Three"]
const newArr = Arr.slice(0)
newArr[1] = "love";
console.log(Arr) // ["One", "Two", "Three"]
console.log(newArr) // ["One", "love", "Three"]
3.3 concat()
concat
方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。
const Arr = ["One", "Two", "Three"]
const newArr = Arr.concat()
newArr[1] = "love";
console.log(Arr) // ["One", "Two", "Three"]
console.log(newArr) // ["One", "love", "Three"]
3.4 拓展运算符
const Arr = ["One", "Two", "Three"]
const newArr = [...Arr]
newArr[1] = "love";
console.log(Arr) // ["One", "Two", "Three"]
console.log(newArr) // ["One", "love", "Three"]
4 完全深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
- _.cloneDeep()
- 结构化拷贝
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
深拷贝和浅拷贝区别如图所示:
下面我们看一下这五种实现深拷贝的代码;
4.1_.cloneDeep()
借助lodash
包的_.cloneDeep()
方法实现深拷贝
const _ = require('lodash');
const obj1 = {
a: 1,
b: {
f: {
g: 1
}
},
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false,两者不相等说明指向的地址不同
4.2 结构化拷贝
结构化克隆算法是由HTML5规范定义的用于复制复杂|avaScript对象的算法。通过来自Workers的postMessage()或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。
function structuralClone(obj){
return new Promise(resolve =>{
const {port1,port2}= new MessageChannel();
port2.onmessage=ev =>resolve(ev.data);
port1.postMessage(obj);
});
}
const obj1 = {
a: 1,
b: {
f: {
g: 1
}
},
c: [1, 2, 3]
};
const obj2 = await structuralClone(obj1);
console.log(obj1.b.f === obj2.b.f);// false,两者不相等说明指向的地址不同
此方法一般情况下能够解决大部分问题,且性能较好。但不支持Function数据类型。
4.3 json.stringify()
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
这种方式存在弊端,会忽略undefined、symbol 和函数
4.4 循环递归
通过遍历对象属性进行循环浅拷贝。只是在遇到一个 object 属性时,需要再次调用拷贝函数。
function deepClone(obj){
if(typeof obj !== "object") return;
let newObj = obj instanceof Array ? [] : {};
for(let key in obj){
if(obj.hasOwnProperty(key)){
newObj[key] = typeof obj[key] === "object" ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
let obj = {a: 11, b: function(){}, c: {d: 22}};
deepClone(obj); // {a: 11, b: f(), c: {d: 22}};
4.5 jQuery.extend()
与lodash库相同,jQuery也提供了深拷贝方法,使用方法如下:
const $ = require('jquery');
const obj1 = {
a: 1,
b: {
f: {
g: 1
}
},
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false,两者不相等说明指向的地址不同
5 总结
深拷贝和浅拷贝的区别就在于拷贝的层级,在日常使用中,我们可以按需去是使用拷贝方法:
- 基本类型使用普通赋值运算符;
- 引用类型:
-
一维数据结构的深拷贝方法建议使用:
Object.assign()
; -
二维数据结构及以上的深拷贝方法建议使用:
JSON.parse(JSON.stringify())
; -
特别复杂的数据结构的深拷贝方法建议使用:三方API;