浅拷贝与深拷贝
浅拷贝与深拷贝的定义
对于引用类型的数据(数组或对象),在拷贝数据之后,对于新的数据所做的改变不会影响到之前的数据,该拷贝则被称为深拷贝,否则是浅拷贝。对于深拷贝,会重新创建一个数组或对象,与之前的地址不一样。以下是对于不同情况的分析。
1 基本数据类型的赋值
let a = 5;
let b = a;
以上是基本数据类型的赋值,不属于浅拷贝与深拷贝的讨论范畴
2 引用数据类型直接赋值
let arr = [1,2,3];
let newArr = arr;
newArr.push(4);
console.log(arr,newArr);
/*运行结果
[1,2,3,4]
[1,2,3,4]
*/
可以看到对新数组的操作也影响到了旧数组,这属于浅拷贝。
3 引用数据类型解构赋值(扩展运算符...
)
3.1 一维数组和只包括基本数据类型的对象
let arr = [1,2,3];
let newArr = [...arr];
newArr.push(4);
console.log(arr,newArr);
/*运行结果
[1,2,3]
[1,2,3,4]
*/
可以看到修改新数组没有影响到旧数组,那么这是深拷贝吗。先持保留意见。
3.2 多维数组与较复杂的对象
let arr = [1,2,3,[4,5]];
let newArr = [...arr];
newArr[3].push(6);
console.log(arr,newArr);
/*运行结果
[1,2,3,[4,5,6]]
[1,2,3,[4,5,6]]
*/
可以看到同样是解构赋值,这次旧数组也产生了变化,这是浅拷贝。所以对于解构赋值,在一维数组中可以勉强看做是深拷贝,但不能说解构赋值就是深拷贝。
如何实现深拷贝
深拷贝不能影响之前的数据,而基本数据类型的赋值不会影响之前的数据。那么我们应该把数组或对象全部拆成一个一个的基本数据类型的数据,再按照原本的结构赋值给新的数组或对象。
1 JSON.parse(JSON.stringify(obj))
通过JSON方法进行深拷贝,利用JSON.stringify(obj)
可以将js对象序列化(转化为JSON字符串),然后再用JSON.parse()
将JSON字符串反序列化为js对象。
但是使用这个方法需要注意以下问题。
- 如果原对象中含有时间
Date
对象,拷贝后的对象中时间对象会变成字符串String
,而不再是时间对象
let a = {
name:'a',
date:[new Date(1536627600000),new Date(1540047600000)]
}
let b = JSON.parse(JSON.stringify(a))
console.log(a,b);
/*运行结果
{
name: 'a',
date: [ 2018-09-11T01:00:00.000Z, 2018-10-20T15:00:00.000Z ]
}
{
name: 'a',
date: [ '2018-09-11T01:00:00.000Z', '2018-10-20T15:00:00.000Z' ]
}
*/
- 如果原对象中含有
RegExp
、Error
对象,则结果会是空对象
const a = {
name:'a',
reg:new RegExp('\\w+')
}
const b = JSON.parse(JSON.stringify(a))
a.name = 'c'
console.error('d',a,b)
/*运行结果
d { name: 'c', reg: /\w+/ }
{ name: 'a', reg: {} }
*/
- 如果原对象含有函数
function
,undefined
,则结果会丢失函数function
和undefined
const a = {
name:'a',
func:function fun(){
console.log('fun')
}
}
const b = JSON.parse(JSON.stringify(a))
console.error('d',a,b)
/*运行结果
d { name: 'a', func: [Function: fun] }
{ name: 'a' }
*/
- 如果原对象含有
NaN
、Infinity
和-Infinity
,结果会变成null
- 如果原对象中有构造函数
constructor
生成的对象,那么结果会丢失对象的构造函数constructor
function Person(name){
this.name = name
console.log(name)
}
const Tom = new Person('Tom')
const a = {
name:'a',
person:Tom
}
const b = JSON.parse(JSON.stringify(a))
a.name = 'c'
console.error('d',a,b)
/*运行结果
Tom
d { name: 'c', person: Person { name: 'Tom' } }
{ name: 'a', person: { name: 'Tom' } }
*/
- 如果原对象含有循环引用,拷贝无法正确完成
2 Object.assign(target,source)
Object.assign()
可以实现对对象属性值的拷贝。它用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。参数target
代表目标对象,source
代表原对象,原对象可以是多个对象。
利用Object.assign()
进行深拷贝时,由于Object.assign()
拷贝的是属性值,假如源对象的属性值是一个对象的引用,那么它也只指向那个引用的地址。这样拷贝过来的对象会影响之前的对象,属于是浅拷贝。这个方法和之前提到的扩展运算符...
有相通之处,以下两段代码等价
let target = Object.assign(target,source)
let target = {...source}
当然,利用他们进行深拷贝都会遇到上面提到的问题。
3 jQuery的extend(deep,target,object)方法
jQuery
中有extend()
方法,可以实现将一个或多个对象的内容合并到目标对象。第一个参数deep
是可选参数,它是一个boolean
类型的值,它默认为false
(但函数不接受这个参数为false
),如果是true
,且多个对象的某个同名属性也都是对象,则该"属性对象"的属性也将进行合并,也就是进行深拷贝,否则进行浅拷贝。target
是一个对象,如果有这个参数,那么extend()会对这个对象进行修改,将其他对象的属性赋到这个对象上,而不影响其他的对象,因为如果没有这个参数,那么后面的对象参数会被当做target
参数。object
参数是N个对象,代表需要被合并的对象,它们的属性与值都会被赋到target
对象上,如果后面的对象和前面的有同名属性,那么后面的属性值会覆盖前面的。
有以下两个对象a,b
let a = {
name:'a',
age:'9',
score:{chinese:90, math:100, english:60}
}
let b = {
name:'b',
job:'student',
score:{chinese:80, english:90}
}
deep
值为默认false
,进行浅拷贝
var c = $.extend({},a,b)
console.log(a,b,c)
/*运行结果
{
name:'a',
age:'9',
score:{chinese:90, math:100, english:60}
}
{
name: 'a',
job: "student"
score: {chinese: 80, english: 90}
}
{
name: 'b',
age: "9"
job: "student"
score: {chinese: 80, english: 90}
}
*/
deep
值为true
,进行深拷贝
var c = $.extend(true,{},a,b)
console.log(a,b,c)
/*运行结果
{
name: 'a',
age: "9"
score: {chinese: 90, math: 100, english: 60}
}
{
name: 'b',
job: "student"
score: {chinese: 80, english: 90}
}
{
name: 'b',
age: "9"
job: "student"
score: {chinese: 80, math:100, english: 90}
}
*/
- 去掉
target
参数{}
,a对象被当做了target
,a对象受到影响
var c = $.extend(true,a,b)
console.log(a,b,c)
/*运行结果
{
name: 'b',
age: "9"
job: "student"
score: {chinese: 80, math:100, english: 90}
}
{
name: 'b',
job: "student"
score: {chinese: 80, english: 90}
}
{
name: 'b',
age: "9"
job: "student"
score: {chinese: 80, math:100, english: 90}
}
*/
4 递归遍历对象的所有属性(标准的深拷贝)
定义一个函数deepClone()
,他有一个参数source
,返回一个数组或对象target
。首先判断一下接收到的参数是一个数组还是对象,这关系到我们要返回一个数组还是对象。我们可以用constructor
来达成这一目的,使用三目运算符?:
,给要返回的值target
赋值。const target = source.constructor === Array ? [] : {}
。紧接着,我们使用for in
循环来遍历source
,用变量keys
来参与循环,并判断当前遍历的对象是否有这个属性hasOwnProperty()
,有值才做处理,没有直接跳过。这里的source[keys]
的数据类型有两种不同的取值,一是基本数据类型,二是引用数据类型,而引用数据类型又分为对象'object'
、数组'array'
。对于基本数据类型,可以直接赋值给target
,对于引用数据类型,需要进行递归,将source[keys]
继续传给下一个deepClone()
处理,因为递归的deepClone()
中会判断是对象还是数组,所以这里不需要提前判断。
以下是完整代码:
function deepClone(source){
const target = source.constructor === Array ? [] : {}
for (const keys in source) {
if (source.hasOwnProperty(keys)) {
if(source[keys]&&typeof source[keys] === 'object'){
target[keys] = deepClone(source[keys])
}else{
target[keys] = source[keys]
}
}
}
return target
}
带入数据进行测试
let a = {
a:123,
b:'123',
c:[1,2,3],
d:{
aa:123,
bb:function cc(){
console.log('dd');
}
}
}
let b = deepClone(a)
console.log(a,b);
b.d.bb()
/*运行结果
{
a: 123,
b: '123',
c: [ 1, 2, 3 ],
d: { aa: 123, bb: [Function: cc] }
}
{
a: 123,
b: '123',
c: [ 1, 2, 3 ],
d: { aa: 123, bb: [Function: cc] }
}
dd
*/