问题描述
问题描述:有一个可接受用户edit编辑的表单。若用户输入错误,需要cancel取消原输入的值。但bug在于,用户点击cancel之后,却在表单保存了新输入错误的值,并没有在点击cancel之后还原原来的值。
出错代码
cancel(){
// change status after 'cancel' button clicked
// do something
// 遍历赋值 (关键点!!)
// 还原表单原本的值
for(let i = 0; i < this.data.length; i++){
this.data[i] = this.dataFromDataBase[i];
// 即点击了cancel按钮后,应当把数据库中的data重新赋值还给表单中的data。从而没有保存错误的值,还原原本的数据。
}
// do something
// this.service.emit();
// this.service.closeBanner()
}
当时发现这个bug时,我大致觉得是赋值这里出了问题。但由于并不是太熟悉这一part,所以我想先log出来data和dataFromDataBase具体是什么,好进一步“诊断”。
console.log(this.data[i]);
console.log(this.dataFormDataBase[i]);
this.data[i] = this.dataFromDataBase[i];
console.log(this.data[i]);
结果分析
我们只关注数组中的一个代表
举个例子,譬如未改动之前原表单的data和数据库的都是typescript。
第一次改动:
- 我在typescript后面加了个1,即表单的data变成typescript1
- 第一次点击cancel
- 输出结果如下:
console.log(this.data[i]); //typescript1;
console.log(this.dataFormDataBase[i]) //typescript
this.data[i] = this.dataFromDataBase[i];
console.log(this.data[i]); //typescript
- 说明第一次改动后cancel,数据库里的数据dataFromDataBase能够把原本的数据typescript还原给表单中的data,从而起到还原的作用
第二次改动:
- 我还是在原本表单输入框,在typescript后面加个1
- 第二次cancel
- 输出结果
console.log(this.data[i]); //typescript1;
console.log(this.dataFormDataBase[i]) //typescript1
this.data[i] = this.dataFromDataBase[i];
console.log(this.data[i]); //typescript1
??数据库里面的data也变成了typescript1???
为什么第一次dataFromDataBase没有问题,第二次就出现问题了
从此以后,不管怎么cancel,表单输入的值都会保存错误输入的值。
OMG
到这里,这么多疑惑,我们就得去了解一下深拷贝和浅拷贝的内涵了!
等了解了深拷贝和浅拷贝的区别原理,那就知道为什么会出现这样的bug。
再关注一下这一句关于赋值的代码,它在后面讲解是重点对象。
this.data[i] = this.dataFromDataBase[i];
深拷贝与浅拷贝
赋值操作
在了解深拷贝和浅拷贝之前,得先了解什么是拷贝copy。
其实在日常敲码的过程中,我们是经常在拷贝copy的。
这个拷贝,就是赋值操作。
赋值: 简单来说就是把某一数值或对象交给另外一个变量。
在JavaScript中,变量只是一个用于保存值的占位符。
1.如果我们赋值赋的只是基本数据类型,那么变量只是保存了这个值。
2. 如果我们赋值的是引用数据类型,那么这个变量实际上保存的是该引用数据的地址。赋值,实际上是赋址。
举个例子再理解一下赋值与“赋址”的区别:
一、基本数据类型:赋值后,两个变量互不影响
let a = 'coffee';
let b = a; //赋值
console.log(a); //coffee;
console.log(b); //coffee; 赋值成功
a = 'milk';
console.log(a); //milk
console.log(b); //coffee; 赋值后,变量a和变量b并没有关系
二、引用数据类型:赋值后,两个变量保存同一个地址,指向同一个对象,互相有影响。
let a = {
name:'coffee',
ice:'less'
price:30
}
let b = a;
console.log(a);//{ name:'coffee',ice:'less',price:30};
console.log(b);//{ name:'coffee',ice:'less',price:30};
a.name = 'milk';
a.ice = 'none';
a.price = 8;
console.log(a);//{ name:'milk',ice:'none',price:8};
console.log(b);//{ name:'milk',ice:'none',price:8};
// 改变a,b也被改变了
// 因为实际上把a赋值给b,b就保存了a指向对象的地址,即a和b保存的是同一个对象的地址,他们都指向同一个对象,都引用了同一个对象。
破案:
到这里,其实我们已经能知道为什么会有上述的bug了:data和dataFromDataBase都是引用数据类型。在第一次cancel时,由于dataFromDataBase赋值给data,那么,data和dataFromDataBase则都引用了同一个对象;在第二次cancel时,当data发生变化,dataFromDataBase也就发生了变化!因为他们都指向同一个对象了。
那怎么办,我们就单纯地只想赋值呀。并不想这二者纠缠不清,表单的数据变了就影响到db的数据。
这时候就需要,深拷贝!进一步了解**什么是浅拷贝,什么是深拷贝!**最后的最后,知道怎么实现深拷贝,就可完结撒花了!
深拷贝的三种实现
深拷贝有什么用,开门见山:深拷贝之后,两个变量所引用的对象是两个毫无关系的对象,各过各的,互不影响。
深拷贝会拷贝所有的属性,相当于浅拷贝来说时间长开销大。
噢~ ,那深拷贝正合我意,我就是想data和dataFromDataBase两个变量互不影响,我在赋值的时候实现深拷贝就好!
那怎么实现深拷贝?
总结一下实现深拷贝三种方法,具体如何使用可以上文档查看:
- JSON.stringify,JSON.parse : let b = JSON.parse(JSON.stringify(a));
- JQ的extend方法:let b = $.extend(true,[],a);
- lodash的cloneDeep方法:let b = cloneDeep(a);
深拷贝解决bug
这三种方法用哪一种都可以,我发现项目里这一part也有使用lodash的cloneDeep,所以,我也使用cloneDeep()
cancel(){
// change status after 'cancel' button clicked
// do something
// 遍历赋值 (关键点!!)
// 还原表单原本的值
for(let i = 0; i < this.data.length; i++){
this.data[i] = cloneDeep(this.dataFromDataBase[i]);
// 使用cloneDeep(),实现深拷贝。
}
// do something
// this.service.emit();
// this.service.closeBanner()
}
浅拷贝
一个对象如果进行浅拷贝,所拷贝的属性若是基本数据类型,拷贝之后二者的属性不会互相影响;但若所拷贝的属性是引用数据类型,则两个变量中该属性都是指向同一个引用对象。
也就是说,变量a引用一个对象,这个对象中有基本数据类型和引用数据类型。若浅拷贝给变量b,变量a中的基本数据类型发生变化,不会影响到b,但若是引用数据类型变化,b的也会被影响。
具体实现也有三种方式
- Object.assign() :let b = Object.assign({},a);
- 展开语法 : let b = {…a};
- array.prototype.slice(): let b = a.slice(0);
总结
一、普通赋值拷贝:
- 若是基本数据类型,互不影响
- 若是引用数据类型,对象中的值会互相影响
二、浅拷贝:
- 对象中的基本数据类型互不影响
- 对象中的引用数据类型互相影响
三、深拷贝
- 对象中的基本数据类型互不影响
- 对象中的引用数据类型互不影响
- 总之两个变量所引用的对象,毫无关系互不影响
从普通的赋值拷贝,到浅拷贝,最后的深拷贝,层层递进,拷贝的程度越来越深。浅拷贝之后的两个对象a和b,他们的基本数据类型是互不影响了,但他们的引用数据类型还是有关系的。深拷贝之后的两个变量,则脱离了关系,互不影响了。
实现总结:
一、浅拷贝实现:
- Object.assign()
- 展开语法
- array.prototype.slice()
二、深拷贝实现:
- JSON.parse(),JSON.stringify()
- JQ的extend()
- lodash的cloneDeep()