【进阶4-1期】详细解析赋值、浅拷贝和深拷贝的区别(转)
这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://github.com/yygmind/blog/issues/25
一、赋值(Copy)
赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分
-
基本数据类型:赋值,赋值之后两个变量互不影响
-
引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响
对基本类型进行赋值操作,两个变量互不影响。
let a = 1 let b = a console.log(b) a = 3 console.log(a) console.log(b)
对引用类型进行赋址操作,两个变量指向同一个对象,改变变量 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
。浅拷贝之后基本类型数据 field2
和 filed1
是不同属性,互不影响。但引用类型 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 相应的位置也发生了变化。
-
展开语法
Spread
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()
是一样的。
展开语法 Spread本质就是浅拷贝
Array.prototype.slice()
slice()
方法返回一个新的数组对象,这一对象是一个由 begin
和 end
(不包括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、不能解决循环引用的对象
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"}
上面代码说明了不能正常处理 undefined
、symbol
和函数这三种情况。
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
除了上面介绍的深拷贝方法,常用的还有`jQuery.extend()
和 lodash.cloneDeep()
四、总结
现在解释为什么深拷贝不能正常处理 undefined
、symbol
和函数这三种情况?
参考地址:https://www.imooc.com/article/70653
JSON.parse(JSON.stringify(obj))
我们一般用来深拷贝,其过程说白了 就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象;序列化的作用是存储(对象本身存储的只是一个地址映射,如果断电,对象将不复存在,因此需将对象的内容转换成字符串的形式再保存在磁盘上 )和传输(例如 如果请求的Content-Type
是 application/x-www-form-urlencoded
,则前端这边需要使用qs.stringify(data)
来序列化参数再传给后端,否则后端接受不到; ps: Content-Type
为 application/json;charset=UTF-8
或者 multipart/form-data
则可以不需要 );我们在使用 JSON.parse(JSON.stringify(xxx))
时应该注意一下几点:
1、如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
var test = { name: 'a', date: [new Date(1536627600000), new Date(1540047600000)], }; let b; b = JSON.parse(JSON.stringify(test)) console.log(b)
2、如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
const test = { name: 'a', date: new RegExp('\\w+'), }; // debugger const copyed = JSON.parse(JSON.stringify(test)); test.name = 'test' console.error('ddd', test, copyed)
3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
const test = { name: 'a', age: undefined, date: function hehe() { console.log('fff') }, }; // debugger const copyed = JSON.parse(JSON.stringify(test)); test.name = 'test' console.error('ddd', test, copyed)
4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
const test = { name: 'a', age: undefined, date: NaN }; // debugger const copyed = JSON.parse(JSON.stringify(test)); test.name = 'test' console.error('ddd', test, copyed)
5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的,
则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
function Person(name) { this.name = name; console.log(name) } const liai = new Person('liai'); const test = { name: 'a', date: liai, }; // debugger const copyed = JSON.parse(JSON.stringify(test)); test.name = 'test' console.error('ddd', test, copyed)
6、如果对象中存在循环引用的情况也无法正确实现深拷贝;
如果拷贝的对象不涉及上面讲的情况,可以使用JSON.parse(JSON.stringify(obj))实现深拷贝,