JavaScript浅拷贝与深拷贝
一、数据类型
数据分为基本数据类型(string, number, boolean, null, undefined,symbol等)和对象数据类型。
1、基本数据类型的特点:直接存储在栈中的数据
2、引用数据类型的特点:存储的是该对象在栈中引用,真实数据存放在堆内存里
二、浅拷贝与深拷贝
深拷贝和浅拷贝是只针对对象和数组这样的引用数据类型的。
以下是深拷贝和浅拷贝的区别:
区别:浅拷贝只复制指向某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
三、赋值和浅拷贝的区别
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
先来看两个例子.
- 对象赋值
// 对象赋值
let obj1 = {
name: 'zhangsan',
age: '18',
language': [1, [2, 3], [4, 5]],
};
let obj2 = obj1;
obj2.name = "lisi";//修改obj2,obj1也随之被修改
obj2.language[1] = ["二", "三"];
console.log('obj1', obj1);
console.log('obj2', obj2);
运行结果:
- 浅拷贝
// 浅拷贝
function shallowCopy(src) {
let dst = {};
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
运行结果:
浅拷贝只是拷贝了子对象的引用,所以执行obj2.a.a =“wade”;时,两个子对象的属性都发生了改变,想要实现子对象的拷贝需要使用深拷贝(后面会讲)
- 结论:
上面例子中,我们可以很清晰看到赋值和浅拷贝对原始数据不同的影响,具体请看下表:
四、浅拷贝的实现方式
1. Object.assign()
Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()
进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
let obj1 ={ a:{a:"kobe", b:39},b:"kobe"};
let obj2 = Object.assign({}, obj1);
obj2.a.a ="wade";
obj2.b="wade";
console.log(obj1,obj2);
注意:当object只有一层的时候,浅拷贝就等同于深拷贝
2. 展开运算符(推荐)
展开运算符是ES6里面的写法,写起来非常简洁
let obj1 = {
username: 'kobe'
};
let obj2 = {...obj1};
obj2.username = 'wade';
console.log(obj1,obj2);
运行结果:
五、深拷贝的实现方式(重点)
1. JSON.parse(JSON.stringify())
1.1概念与基本用法
先介绍两个概念:
- 序列化:对象=>JSON字符串,通过JSON.stringify()实现
- 反序列化:JSON字符串=>对象,通过JSON.parse()实现
利用 JSON.stringify 将js对象序列化(对象=>JSON字符串),再使用JSON.parse来反序列化(JSON字符串=>对象)js对象
基本用法:
let arr1 =[1,3,{username:' kobe'}];
let arr2 =JSON.parse(JSON.stringify(arr1));
arr1[2].username ='duncan';
console.log(arr1, arr2);
运行结果:
可以看到,对子对象的修改并没有改变原来的对象,这就是深拷贝
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象, 新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
1.2局限性
不过这种方式只适用于一般数据的拷贝(对象、数组),存在局限性,以下场景会出问题
- 如果数组(对象)里面有时间对象,经过JSON.parse(JSON.stringify())后,时间将只是字符串的形式,而不是时间对象;
let arr1 =[1,3,{username:' kobe',birth:new Date()}];
let arr2 =JSON.parse(JSON.stringify(arr1));
console.log(arr1, arr2);
- 如果数组(对象)里有RegExp、Error对象,则经过序列化反序列化的结果将只得到空对象;
let arr1 =[ new RegExp('\\w+'), new Error('error message')];
let arr2 =JSON.parse(JSON.stringify(arr1));
console.log(arr1, arr2);
运行结果:
- 如果数组(对象)里有function或者undefined,则经过序列化反序列化的结果将中没有function和undefined(即null)
let arr1 =[1,3,{username:' kobe'},function(){},undefined];
let arr2 =JSON.parse(JSON.stringify(arr1));
console.log(arr1, arr2);
运行结果:
- 如果数组(对象)里有对象是由构造函数生成的,则经过序列化反序列化之后会没有对象的 constructor;
function Person(name) {
this.name = name;
}
let arr1 =[new Person('kobe') ];
let arr2 =JSON.parse(JSON.stringify(arr1));
console.log(arr1, arr2);
运行结果:
下面两种情况,实际项目不怎么会碰到,了解一下即可
- 如果数组(对象)里有NaN、Infinity和-Infinity,则经过序列化反序列化的结果为null
let arr1 =[ NaN,Infinity,-Infinity];
let arr2 =JSON.parse(JSON.stringify(arr1));
console.log(arr1, arr2);
运行结果:
- 如果对象中存在循环引用的情况,则序列化反序列化直接报错(无法深拷贝)
let obj1 ={name:'kobe'};
obj1.obj1 = obj1;
let obj2 =JSON.parse(JSON.stringify(obj1));
console.log(obj1, obj2);
运行结果:
1.3总结:
- 如果拷贝的对象不涉及上面的情况,可以使用 JSON.parse(JSON.stringify(obj)) 实现深拷贝
2. 手写递归方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
// 定义检测数据类型的功能函数
function checkedType(target) {
return Object.prototype.toString.call(target).slice(8, -1)
}//这个函数下文会有解释
function depCopy(target) {// 实现深拷贝
let result,
targetType = checkedType(target) ;// 判断拷贝的值
if (targetType === 'Object') {
result = {}
} else if (targetType === 'Array') {
result = []
} else { // 如果不是对象或数组就直接返回
return target
}
// 遍历目标数据
for (let i in target) {
let value = target[i]
if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
result[i] = depCopy(value)
} else {
// 获取到value值是基本的数据类型或者是函数。
result[i] = value;
}
}
return result
}
解释一下这个类型判断函数
Object.prototype.toString()该方法返回描述某个对象数据类型的字符串如:
var toString = Object.prototype.toString;
toString.call([1,2,3]);//[object Array]
toString.call({name:kobe});//[object Object]
Object.prototype.toString.call(obj).slice(8,-1)返回的就是这个obj对象的类型
slice(startIndex,endIndex),从0开始索引,其中8代表从第8位(包含)开始截取,-1代表截取到倒数第一位(不含),如[object Object],正好会截取到[object Object]中的Object。
3. lodash工具库(推荐)
在实际项目开发中,推荐使用lodash工具库,节省时间,当然也可以用jquery,underscore等其它的.
导入
1.官网下载:https://www.lodashjs.com
2.NPM
npm i -g npm
npm i -S lodash
使用_.cloneDeep
函数 用来做深拷贝。
import _ from 'lodash'
let _ = require('lodash');
let obj1 = {
a: 1,
b: {
f: {
g: 1
}
},
c: [1, 2, 3]
};
let obj2 = _.cloneDeep(obj1); //直接使用就行
console.log(obj1.b.f === obj2.b.f); // false,深拷贝成功