yygmind / blog

我是木易杨,公众号「高级前端进阶」作者,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!
https://muyiy.cn/blog/
10.53k stars 1.11k forks source link

【进阶4-1期】详细解析赋值、浅拷贝和深拷贝的区别 #25

Open yygmind opened 5 years ago

yygmind commented 5 years ago

一、赋值(Copy)

赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分

对基本类型进行赋值操作,两个变量互不影响。

// 木易杨
let a = "muyiy";
let b = a;
console.log(b);
// muyiy

a = "change";
console.log(a);
// change
console.log(b);
// muyiy

对引用类型进行赋操作,两个变量指向同一个对象,改变变量 a 之后会影响变量 b,哪怕改变的只是对象 a 中的基本类型数据。

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = a;
console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//  name: "change",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//  name: "change",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

通常在开发中并不希望改变变量 a 之后会影响到变量 b,这时就需要用到浅拷贝和深拷贝。

二、浅拷贝(Shallow Copy)

1、什么是浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

上图中,SourceObject 是原对象,其中包含基本类型属性 field1 和引用类型属性 refObj。浅拷贝之后基本类型数据 field2filed1 是不同属性,互不影响。但引用类型 refObj 仍然是同一个,改变之后会对另一个对象产生影响。

简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址

2、浅拷贝使用场景

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

有些文章说Object.assign() 是深拷贝,其实这是不正确的。

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = Object.assign({}, a);
console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//  name: "change",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 book 时,对象 b 相应的位置也发生了变化。

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = {...a};
console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//  name: "change",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

通过代码可以看出实际效果和 Object.assign() 是一样的。

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

// 木易杨
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [4, 3]]

可以看出,改变 a[1] 之后 b[0] 的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝,相应的还有concat等,在工作中面对复杂数组结构要额外注意。

三、深拷贝(Deep Copy)

1、什么是深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

2、深拷贝使用场景

JSON.parse(JSON.stringify(object))

// 木易杨
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
//  name: "change",
//  book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
//  name: "muyiy",
//  book: {title: "You Don't Know JS", price: "45"}
// } 

完全改变变量 a 之后对 b 没有任何影响,这就是深拷贝的魔力。

我们看下对数组深拷贝效果如何。

// 木易杨
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [2, 3]]

对数组深拷贝之后,改变原数组不会影响到拷贝之后的数组。

但是该方法有以下几个问题。

1、会忽略 undefined

2、会忽略 symbol

3、不能序列化函数

4、不能解决循环引用的对象

5、不能正确处理new Date()

6、不能处理正则

// 木易杨
let obj = {
    name: 'muyiy',
    a: undefined,
    b: Symbol('muyiy'),
    c: function() {}
}
console.log(obj);
// {
//  name: "muyiy", 
//  a: undefined, 
//  b: Symbol(muyiy), 
//  c: ƒ ()
// }

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}
// 木易杨
let obj = {
    a: 1,
    b: {
        c: 2,
        d: 3
    }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON
// 木易杨
new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

解决方法转成字符串或者时间戳就好了。

// 木易杨
let date = (new Date()).valueOf();
// 1545620645915

JSON.stringify(date);
// "1545620673267"

JSON.parse(JSON.stringify(date));
// 1545620658688

let b = JSON.parse(JSON.stringify(obj)); console.log(b); // {name: "muyiy", a: {}}



PS:为什么会存在这些问题可以学习一下 JSON。

除了上面介绍的深拷贝方法,常用的还有`jQuery.extend()` 和 `lodash.cloneDeep()`,后面文章会详细介绍源码实现,敬请期待!

### 四、总结

|   --   | 和原数据是否指向同一对象 |   第一层数据为基本数据类型   |      原数据中包含子对象      |
| :----: | :----------------------: | :--------------------------: | :--------------------------: |
|  赋值  |            是            |    改变会使原数据一同改变    |    改变会使原数据一同改变    |
| 浅拷贝 |            否            | 改变**不**会使原数据一同改变 |    改变会使原数据一同改变    |
| 深拷贝 |            否            | 改变**不**会使原数据一同改变 | 改变**不**会使原数据一同改变 |

### 参考

> [js 深拷贝 vs 浅拷贝](https://juejin.im/post/59ac1c4ef265da248e75892b)
>
> [Java 深拷贝和浅拷贝](https://my.oschina.net/jackieyeah/blog/206391)
>
> [MDN 之 Object.assign()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
>
> [MDN 之展开语法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax)
>
> [MDN 之 Array.prototype.slice()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)

### 进阶系列目录

* 【进阶1期】 调用堆栈
* 【进阶2期】 作用域闭包
* 【进阶3期】 this全面解析
* 【进阶4期】 深浅拷贝原理
* 【进阶5期】 原型Prototype
* 【进阶6期】 高阶函数
* 【进阶7期】 事件机制
* 【进阶8期】 Event Loop原理
* 【进阶9期】 Promise原理
* 【进阶10期】Async/Await原理
* 【进阶11期】防抖/节流原理
* 【进阶12期】模块化详解
* 【进阶13期】ES6重难点
* 【进阶14期】计算机网络概述
* 【进阶15期】浏览器渲染原理
* 【进阶16期】webpack配置
* 【进阶17期】webpack原理
* 【进阶18期】前端监控
* 【进阶19期】跨域和安全
* 【进阶20期】性能优化
* 【进阶21期】VirtualDom原理
* 【进阶22期】Diff算法
* 【进阶23期】MVVM双向绑定
* 【进阶24期】Vuex原理
* 【进阶25期】Redux原理
* 【进阶26期】路由原理
* 【进阶27期】VueRouter源码解析
* 【进阶28期】ReactRouter源码解析

### 交流

进阶系列文章汇总:[https://github.com/yygmind/blog](https://github.com/yygmind/blog),内有优质前端资料,觉得不错点个star。

我是木易杨,网易高级前端工程师,跟着我**每周重点攻克一个前端面试重难点**。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

![](https://github.com/yygmind/blog/raw/master/images/weixin_re.png)
qinghua5453 commented 5 years ago

数组的concat 和slice是浅拷贝吗。但我之前在某一些博文看见这两个方式是深拷贝,专门针对数组的。而且我记得我也用过。

ABCyuan commented 5 years ago

数组的concat 和slice是浅拷贝吗。但我之前在某一些博文看见这两个方式是深拷贝,专门针对数组的。而且我记得我也用过。

是浅拷贝,因为你使用的数组里面是基本数据类型,所以不会存在覆盖,你使用引用类型的就可以看出来

jecyu commented 3 years ago

如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

这里「其中一个对象改变了这个地址」,应该是改变了这个地址指向的值吧?