【js重学系列】赋值-浅拷贝-深拷贝
赋值(Copy)
概念
赋值是将某一数值或对象赋给某个变量的过程,分为:
基本数据类型:赋值,赋值之后两个变量互不影响
引用数据类型:赋址,两个变量具有相同的引用,指向同一个对象,相互之间有影响当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
赋值其实是将原始对象存储在栈中的指向堆内存的地址赋值给新对象,而不是堆中的数据。所以新对象的任何数据类型的属性值发生改变,都会影响到原始数据。
基本数据类型
// 对基本数据类型进行赋值操作,两个变量互不影响。
// saucxs
let a = "saucxs";
let b = a;
console.log(b); // saucxs
a = "change";
console.log(a); // change
console.log(b); // saucxs
引用数据类型
// 对引用类型进行赋**址**操作,两个变量指向同一个对象,改变变量 a 之后会影响变量 b,哪怕改变的只是对象 a 中的基本类型数据。
// saucxs
let a = {
name: "saucxs",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = a;
console.log(b);
// {
// name: "saucxs",
// 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"}
// }
let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;
console.log(arr);//[100, 2, 3]
这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变
浅拷贝(Shallow Copy)
概念
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址
浅拷贝使用场景
通常在开发中并不希望改变变量 a 之后会影响到变量 b,这时就需要用到浅拷贝和深拷贝。
浅拷贝方案
Object.assign()
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。有些文章说Object.assign()
是深拷贝,其实这是不正确的。
// saucxs
let a = {
name: "saucxs",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = Object.assign({}, a);
console.log(b);
// {
// name: "saucxs",
// 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: "saucxs",
// book: {title: "You Don't Know JS", price: "55"}
// }
// 上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 `book` 时,对象 b 相应的位置也发生了变化。
展开语法 Spread
// saucxs
let a = {
name: "saucxs",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = {...a};
console.log(b);
// {
// name: "saucxs",
// 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: "saucxs",
// book: {title: "You Don't Know JS", price: "55"}
// }
Array.prototype.slice方法 仅仅适用于数组
slice不会改变原数组,
slice()
方法返回一个新的数组对象,这一对象是一个由begin
和end
(不包括end
)决定的原数组的浅拷贝
// saucxs
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
等,在工作中面对复杂数组结构要额外注意。
let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;
console.log(arr);//[1, 2, 3]
当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!
Array.prototype.concat()-------仅仅适用于数组
手写浅拷贝
function shallowClone(obj) {
let isObject = (arg) => typeof arg === "object" && arg !== null
if (!isObject(obj)) return obj;
const cloneObj = Array.isArray(obj) ? [] : {};
for (let v in obj) {
if (obj.hasOwnProperty(v)) cloneObj[v] = obj[v];
}
return cloneObj;
}
深拷贝(Deep Copy)
概念
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
使用深拷贝的场景
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);//[ 1, 2, { val: 1000 } ]
不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。
这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。
深拷贝方案
JSON.parse(JSON.stringify(object))
// saucxs
let a = {
name: "saucxs",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "saucxs",
// 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: "saucxs",
// book: {title: "You Don't Know JS", price: "45"}
// }
完全改变变量 a 之后对 b 没有任何影响,这就是深拷贝的魔力。
我们看下对数组深拷贝效果如何。
// saucxs
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)不能处理正则
其中(1)(2)(3) undefined
、symbol
和函数这三种情况,会直接忽略。
// saucxs
let obj = {
name: 'saucxs',
a: undefined,
b: Symbol('saucxs'),
c: function() {}
}
console.log(obj);
// {
// name: "saucxs",
// a: undefined,
// b: Symbol(saucxs),
// c: ƒ ()
// }
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "saucxs"}
其中(4)循环引用会报错
// saucxs
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
其中(5)* new Date
情况下,转换结果不正确
// saucxs
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"
解决方法转成字符串或者时间戳就好了。
// saucxs
let date = (new Date()).valueOf();
// 1545620645915
JSON.stringify(date);
// "1545620673267"
JSON.parse(JSON.stringify(date));
// 1545620658688
其中(6)正则情况下
// saucxs
let obj = {
name: "saucxs",
a: /'123'/
}
console.log(obj);
// {name: "saucxs", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "saucxs", a: {}}
PS:为什么会存在这些问题可以学习一下 JSON。
用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。这是因为 JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。
除了上面介绍的深拷贝方法,
jQuery.extend()
lodash.cloneDeep()
函数库lodash
手写深拷贝
function deepClone(obj) {
// 1.先判断是否为对象类型
let isObject = (arg) => typeof arg === "object" && arg !== null
// 2.如果不是,则返回本身
if (!isObject(obj)) return obj;
// 3.定义一个变量,判断是数组还是对象
let cloneObj = Array.isArray(obj) ? [] : {};
// 4.循环每一个元素,如果满足是自身属性,则每一个值再判断是否为对象类型,如果是则继续递归调用,否则返回本身
for (let key in obj) {
if (obj.hasOwnProperty(key)) cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];
}
// 5.返回创建的变量值
return cloneObj;
}
总结
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
浅拷贝:
创建一个新对象,这个新对象对原始对象的值有着一份精确的拷贝。
如果原始对象的值是基本类型数据,拷贝的就是基本类型的值,改变原始数据的值,新对象值不会发生改变。
如果原始对象的值是引用类型,那么就是拷贝的这个值的内存地址,如果该值发生改变,就会影响到原始对象。
深拷贝:创建一个新对象,将原始对象从内存中完整的拷贝出来,从堆内存中开辟出一个新的区域存放新对象,且修改新对象,不会影响原始对象