VueBloghyhero6

JS 深拷贝 与 浅拷贝

2019-08-14 / 2019-08-14 / 184次浏览

这俩天 实在忙,瞎忙,各种问题,然后一个星期没有更新。

今天 得空 赶紧更新下。

拷贝场景,大批量对数据进行操作处理,并且不会污染原有数据。或者说原有数据还有其他位置进行使用。一位博主用redux 碰到了这个问题。

偷懒引用下 他的问题。
redux的机制要求在reducer中必须返回一个新的对象,而不能对原来的对象做改动,事实上,当时我当然不会主动犯这个错误,但很多时候,一不小心可能就会修改了原来的对象,例如:var newObj = obj; newObj.xxx = xxx 实际上,这个时候newObj和obj两个引用指向的是同一个对象,我修改了newObj,实际上也就等同于修改了obj,这,就是我和深浅拷贝的第一次相遇。

reducer 要求必须是纯净的。

回到最初,关于深浅 拷贝我们带着问题来看。
第一,为什么会有浅度拷贝的概念,以及深拷贝的概念。
第二,什么样的场景会使用深拷贝,然后我们探究深浅拷贝的各种算法。

浅显的来说, 就要提到 堆和栈的区别,其实深拷贝 和 浅拷贝的主要区别就是其在内存中的存储类型不同。堆和栈都是内存中划分出来存储的区域。

栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap) 则是动态分配的内存,大小不定也不会自动释放

基本数据类型(undifend,bloolean,number,string,null)

基本数据类型存放在栈中

存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的。所以可以直接访问。

基本数据类型值不变

JavaScript中的原始值(undefiend、null、布尔值、数字和字符串) 与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或'突变')一个原始值。对数字和布尔值来说显然如此 ,改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符串组成的数组,我们期望可以通过对通过指定的索引来假改字符串中的字符。实际上,JavaScript是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。

基本类型的值是不可突变的

基本数据类型的值是不可变的,动态修改了基本数据类型的值,他的原始值也是不会改变的,例如:

var str = "abc" console.log( str[1] = "f" ); //f console.log( str ); // abc

这一点其实开始我也晕,基本类型不是可以任意给定赋值的嘛,JS 不是一个灵活的语言嘛。

我们通常情况是对一个变量重新赋值,而不是改变基本数据类型的值。就如上述引用说的那样,在js 中没有方法是改变布尔值和数字的。倒是有很多操作字符串的方法,但是这些方法都是返回一个新的字符串,并没有改变原有的数据。

所以,记住这一点:基本数据类型值不可变。

讲深浅拷贝就涉及到值的类型,讲值的类型就涉及到堆栈,这个木有办法,图样图森破。

在重复 一遍遍 ,基本类型值不可变,基本类型值不可变。

基本类型的比较是值的比较

只要值相等就认为他们是相等的,例如:

var a = 1; var b = 1; console.log( a === b ); // true

比较的时候最好使用严格等,因为 == 是会进行类型转换的,比如:

var a = 1; var b = true; console.log( a == b ); // true

然后下面说引用类型

引用类型( object ) 是存放在堆内存中的,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小都不一样,要根据情况进行特定的分配。

原谅我直接截图。

例如:

var a = [1, 2, 3];
a[1] = 5;
console.log(a[1]); // 5

在引用类型中值的比较是引用比较

直接上代码

var a = [1, 2, 3]
var b = [1, 2, 3]
console.log( a === b ); // false

虽然 变量a 和 变量 b 都是表示一个内容为 1,2,3 的数组,但是其在内存中的位置不一样,也就是变量a 和 变量b 指向的不是同一个对象,所以说他们是不相等的。

基本了解 基本数据类型 与 引用数字类型的区别之后。我们就应该能明白传值 和 传址 的 区别了。

在我们进行赋值操作的时候,基本数据类型的赋值 (=)是在内存中新开辟一段栈内存,然后再将值赋值到新的栈中。例如:

var a = 10;
var b = a;
a++;
console.log(a); // 11
console.log(b); // 10

如图

所以说,基本类型的赋值的俩个变量是独立相互不影响的变量。

但是 引用类型的赋值是传址。只是改变指针的指向,例如,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话俩个变量就指向同一个对象,因此两者之间操作互相有影响。例如:
上代码:

var a = {}; // a 保存 了一个空对象的实例
var b = a: // a 和 b 都指向 了这个空对象
a.name = 'jozo';
console.log( a.name ); // 'jozo'
console.log( b.name ); // 'jozo'
b.age = 22;
console.log(b.age); //22
console.log(a.age); //22
console.log(a == b); // true

那么现在说明一个问题,赋值就是浅拷贝嘛,其实不对,由于指向的是同一个堆的区域,上面只能算引用,并不算,真正的浅拷贝。
直接上代码。

var obj1 = {
'name': 'zhangsan',
age': '18',
'language': [1,[2,3],[4,5]]
}
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for ( var prop in src ) {
if ( src.hasOwnProperty ( prop ) ) {
dst[ prop ] = src [ prop ];
}
}
return dst;
}
obj2.name = 'list';
obj2.age = '20';
obj2.language[1] = [ '二','三' ]
obj3.language[2] = [ '四','五' ]
console.log(obj1);
//obj1 = {// 'name' : 'lisi',// 'age' : '18',// 'language' : [1,["二","三"],["四","五"]],//};console.log(obj2);
//obj2 = {// 'name' : 'lisi',// 'age' : '18',// 'language' : [1,["二","三"],["四","五"]],//};console.log(obj3);
/obj3 = {// 'name' : 'zhangsan',// 'age' : '20',// 'language' : [1,["二","三"],["四","五"]],//};

这段代码可以试着放在浏览器中跑一下。
说明一下:
obj1:原始数据
obj2: 赋值操作数据
obj3:浅拷贝得到

简单来说,obj3 由于重新创建了新的对象那么,有 新的对应的栈 和 堆。

obj2 是赋值得到 obj1 所以必定指向同一个堆地址是一样。

有个问题就是浅拷贝的obj3 依旧改变对象的属性。

浅拷贝 只能拷贝一层的对象属性, 并不包括对象里面的为引用类型的数据。所以obj3 依旧改变了原始数据。

用个图吧。

网络上的博文修修改改 你看我的我看你的。-。=

看个美女延缓下心情

好的浅拷贝说完了我们说深拷贝,

先明确一波区分吧,

1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

需求也是很明白了,我就想堆栈指针全是新的没有旧的,全新的。

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

只对第一层级遍历

上代码

var array = [1, 2, 3, 4];
function copy (array) {
let newArray = []
for(let item of array) {
newArray.push(item);
}
return newArray;
}
var copyArray = copy(array);
copyArray[0] = 100;
console.log(array); // [1, 2, 3, 4]
console.log(copyArray); // [100, 2, 3, 4]

直接遍历就是浅拷贝

第二个数字的 slice 方法

var array = [1, 2, 3, 4];
var copyArray = array.slice();
copyArray[0] = 100;
console.log(array); // [1, 2, 3, 4]
console.log(copyArray); // [100, 2, 3, 4]

slice() 方法返回一个从已有的数组中截取一部分元素片段组成的新数组(不改变原来的数组!)

同样的
用法:array.slice(start,end) start表示是起始元素的下标, end表示的是终止元素的下标

当slice()不带任何参数的时候,默认返回一个长度和原数组相同的新数组

数组自带的slice 浅拷贝的方法

concat()

var array = [1, 2, 3, 4];
var copyArray = array.concat();
copyArray[0] = 100;
console.log(array); // [1, 2, 3, 4]
console.log(copyArray); // [100, 2, 3, 4]

concat() 方法用于连接两个或多个数组。( 该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。)

用法:array.concat(array1,array2,......,arrayN)

因为我们上面调用concat的时候没有带上参数,所以var copyArray = array.concat();实际上相当于var copyArray = array.concat([]);

也即把返回数组和一个空数组合并后返回

第一级数组元素是对象或者数组等引用类型变量的数组,上面的三种方式都将失效

这个👆 呢也实验过了,不作二遍解释了。

2.ES6的Object.assign

var obj = {
name: '彭湖湾',
job: '学生'
}
var copyObj = Object.assign({}, obj);
copyObj.name = '我才不叫彭湖湾呢! 哼 (。・ω´・)'; console.log(obj); // {name: "彭湖湾", job: "学生"} console.log(copyObj); // {name: "我才不叫彭湖湾呢! 哼 (。・ω´・)", job: "学生"}

Object.assign:用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),并返回合并后的target

用法: Object.assign(target, source1, source2); 所以 copyObj = Object.assign({}, obj); 这段代码将会把obj中的一级属性都拷贝到 {}中,然后将其返回赋给copyObj

扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中

但是遗憾的上述三种方法,对于多层嵌套对象依旧失效。

var obj = {
name: {
firstName: '彭',
lastName: '湖湾'
},
job: '学生'
}
var copyObj = Object.assign({}, obj)
copyObj.name.lastName = '湖水的小浅湾';
console.log(obj.name.lastName); // 湖水的小浅湾
console.log(copyObj.name.lastName); // 湖水的小浅湾

以上名字又被改变了。

但是有没有拷贝所有层级的方案
下面这一招可谓是“一招鲜,吃遍天”

1.JSON.parse(JSON.stringify(XXXX))

var array = [
{ number: 1 },
{ number: 2 },
{ number: 3 }
];
var copyArray = JSON.parse(JSON.stringify(array))
copyArray[0].number = 100;
console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

2.手动写递归

var array = [
{ number: 1 },
{ number: 2 },
{ number: 3 }
];
function copy (obj) {
var newobj = obj.constructor === Array ? [] : {};
if(typeof obj !== 'object'){
return;
}
for(var i in obj){
newobj[i] = typeof obj[i] === 'object' ?
copy(obj[i]) : obj[i];
}
return newobj
}
var copyArray = copy(array)
copyArray[0].number = 100;
console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

遗憾的是上文的所有的示例都忽略一些特殊的情况:对对象/数组中的Function,正则表达式等特殊类型的拷贝。

存在大量深拷贝需求的代码——immutable提供的解决方案

性能判断

深拷贝实际上是很消耗性能的。
(我们可能只是希望改变新数组里的其中一个元素的时候不影响原数组,但却被迫要把整个原数组都拷贝一遍,这不是一种浪费吗?)

当需要大量深度拷贝性能成为瓶颈的时候。

immutable的作用:

通过immutable引入的一套API,实现:

1.在改变新的数组(对象)的时候,不改变原数组(对象)
2.在大量深拷贝操作中显著地减少性能消耗

直接上代码吧。

const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50

至此大概深浅拷贝的用法原理深层次实现应该大致了解。

呼,真的长啊

最后放上引用

掘金小姐姐的,应该是饿了吗的
https://juejin.im/post/59ac1c4ef265da248e75892b

csdn 的博客
https://blog.csdn.net/qq_39207948/article/details/81067482

上述代码自己的理解加实验可以加深记忆哦。

对了 知乎上面的对象指针写的很好,看了那个图就不晕了,我就不贴图了。好长啊。
https://www.zhihu.com/question/23031215

js