JavaScript深拷贝和浅拷贝
1、认识JavaScript基本数据类型和引用类型
想要深入理解JavaScript深浅拷贝,我们必须先了解两个JavaScript数据类型-------基本数据类型和引用类型。
(1)基本数据类型
1、 基本数据类型的值在内存中占据固定大小的空间,并被保存在栈内存中。
2、当基本类型a的值赋值给另一个变量b时,会在内存中重新开辟一个空间并复制基本类型a的值给b。
3、基本数据类型的值不能添加属性。
javascript基本数据类型:Undefined、Null、Boolean、Number、String、Symbol
var x = 1;
var y = x;
console.log(y);//1
x = 3;
console.log(y);//1
console.log(x);//3
(2)引用类型
1、复杂的数据类型(比如对象)即使引用类型,名字在栈内存中,值是对象,保存在堆内存中。
2、引用类型的变量,包含是一个指向该对象的指针,而不是对象本身。
3、一个变量向另一个变量赋值引用类型的值时,实际上是复制了它的指针地址,因此两个变量最终都指向同一个对象。
var obj = {
name:'大虫'
}
var obj2 = obj;
var num = 123;
从上图可以看出,我们把obj赋值给obj2后,赋值时只是赋值了obj指针的地址,他们指向同一个引用。
2、数组的浅拷贝
我们可以利用数组slice、concat方法中返回一个新数组的特性来实现拷贝。
var arrOne = [1,2,3,'one','two','three',undefined,null];
var arrTwo = arrOne.concat();
arrTwo[4] = 'six';
console.log(arrOne);//[1, 2, 3, "one", "two", "three", undefined, null]
console.log(arrTwo);//[1, 2, 3, "one", "six", "three", undefined, null]
var arrThree = arrOne.slice();
arrThree[4] = 'ten';
console.log(arrThree);//[1, 2, 3, "one", "ten", "three", undefined, null]
上面这种写法,看似没什么问题。但是如果数组里面嵌套了数组或者对象,拷贝就不灵敏了。
var arr = [1,2,{name:'大虫'},[3,4]];
var newArr = arr.concat();
arr[3][2] = 5;
console.log(arr);// [1,2,{name:'大虫'},[3,4,5]]
console.log(newArr);// [1,2,{name:'大虫'},[3,4,5]]
从这里我们可以看出,新旧数组都发生了变化,所以concat方法,克隆的并不彻底。
1、如果数组元素都是基本类型,就会拷贝数组里面的每一个元素,拷贝后与原数组互不影响。
2、如果数组元素里面含有引用类型(对象或数组),这样只会拷贝数组内引用类型的引用,所以我们无论是修改新数组还是旧数组,两者都会变化。
浅拷贝:我们把复制引用的拷贝方法称为新拷贝。
深拷贝:完全拷贝一个对象,即便里面嵌有引用类型,两者也互相分离,修改一个属性不会影响到另一个属性,我们叫做深拷贝。
3、数组的深拷贝
var arr1 = [1,2,{name:'大虫'},[3,4]];
var arr2 = JSON.parse(JSON.stringify(arr1));
arr1[2].age = 23;
console.log(arr1);//[1,2,{name:'大虫',age:23},[3,4]]
console.log(arr2);//[1,2,{name:'大虫'},[3,4]]
注意:该方式不能拷贝函数
var arr = [function(){console.log('fun1')}, {b: function(){console.log('fun2')}}]
var new_arr = JSON.parse(JSON.stringify(arr));
console.log(new_arr);//[null, {}]
4、浅拷贝的实现
function shallowCopy(obj){
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
5、深拷贝的实现
function deepCopyObj(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
注意:因为深拷贝使用了递归,性能方面不如浅拷贝,在开发中还需要根据实际情况进行选择。
6、自己写一个extend(摘自冴羽博客,仅供学习使用)
function extend() {
// 默认不进行深拷贝
var deep = false;
var name, options, src, copy;
var length = arguments.length;
// 记录要复制的对象的下标
var i = 1;
// 第一个参数不传布尔值的情况下,target默认是第一个参数
var target = arguments[0] || {};
// 如果第一个参数是布尔值,第二个参数是才是target
if (typeof target == 'boolean') {
deep = target;
target = arguments[i] || {};
i++;
}
// 如果target不是对象,我们是无法进行复制的,所以设为{}
if (typeof target !== 'object') {
target = {}
}
// 循环遍历要复制的对象们
for (; i < length; i++) {
// 获取当前对象
options = arguments[i];
// 要求不能为空 避免extend(a,,b)这种情况
if (options != null) {
for (name in options) {
// 目标属性值
src = target[name];
// 要复制的对象的属性值
copy = options[name];
if (deep && copy && typeof copy == 'object') {
// 递归调用
target[name] = extend(deep, src, copy);
}
else if (copy !== undefined){
target[name] = copy;
}
}
}
}
return target;
};
7、最终版extend
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
function isPlainObject(obj) {
var proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
proto = Object.getPrototypeOf(obj);
if (!proto) {
return true;
}
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}
function extend() {
// 默认不进行深拷贝
var deep = false;
var name, options, src, copy, clone, copyIsArray;
var length = arguments.length;
// 记录要复制的对象的下标
var i = 1;
// 第一个参数不传布尔值的情况下,target 默认是第一个参数
var target = arguments[0] || {};
// 如果第一个参数是布尔值,第二个参数是 target
if (typeof target == 'boolean') {
deep = target;
target = arguments[i] || {};
i++;
}
// 如果target不是对象,我们是无法进行复制的,所以设为 {}
if (typeof target !== "object" && !isFunction(target)) {
target = {};
}
// 循环遍历要复制的对象们
for (; i < length; i++) {
// 获取当前对象
options = arguments[i];
// 要求不能为空 避免 extend(a,,b) 这种情况
if (options != null) {
for (name in options) {
// 目标属性值
src = target[name];
// 要复制的对象的属性值
copy = options[name];
// 解决循环引用
if (target === copy) {
continue;
}
// 要递归的对象必须是 plainObject 或者数组
if (deep && copy && (isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {
// 要复制的对象属性值类型需要与目标属性值相同
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
target[name] = extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
};
8、思考题
//题目一
var a = extend(true, [4, 5, 6, 7, 8, 9], [1, 2, 3]);
console.log(a) // ???
//题目二
var obj1 = {
value: {
3: 1
}
}
var obj2 = {
value: [5, 6, 7],
}
var b = extend(true, obj1, obj2) // ???
var c = extend(true, obj2, obj1) // ???