最全JS拷贝问题解析
前言
作为初入前端的同学,可能会经常碰到JS对象的拷贝问题。你复制了对象A,并命名为B,当你兴致勃勃地修改了B对象后却惊讶的发现了A对象也被改变了。
没关系!读完这边文章,你将学到
- JS数据类型,以及如何存储
- JS浅拷贝和深拷贝区别以及如何实现
1. JS数据类型
在JS中,数据类型分两种:基本数据类型、引用数据类型;
基本数据类型
基本类型主要是: Undefined、Boolean、String、Number、Null、Symbol(ECMAScript 6 新定义); 基本类型存储在栈内存中,数据大小确定,内存空间大小可以分配,按值存放,所以可直接访问
引用数据类型
也就是对象类型Object type,比如:Object 、Array 、Function 、Data等。 javascript的引用数据类型是「保存在堆内存中的对象」。
「与其他语言的不同是,你不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。」

所以,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址 所以,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "我有名字了";
console.log(obj1.name); // 我有名字了
说明这两个引用数据类型指向了同一个堆内存对象。obj1赋值给onj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了obj2,
但是实际上他们共同指向了同一个堆内存对象。实际上改变的是堆内存对象。 下面我们来演示这个引用数据类型赋值过程:

综上所述:「本类型与引用类型最大的区别实际就是传值与传址的区别」
JS浅拷贝和深拷贝区别以及如何实现
浅拷贝
浅拷贝 (shallow copy) 一般指第一层 values (Array 的每个元素, Object 第一层 keys 对应的 values 等) 的拷贝, 也就是说这些 value 如果是一个 Object, 则它将会被复制到新的变量中. 可参考代码如下:
// 数组的浅拷贝
let arr1 = [1, 2, 3]
let arr2 = []
for (let i in arr1) {
arr2[i] = arr1[i]
}
arr2.push(4)
console.log(arr1) // [1, 2, 3]
console.log(arr2) // [1, 2, 3, 4]
// 对象的浅拷贝
let obj1 = {
a: '1',
b: '2',
c: '3'
}
let obj2 = {}
for (let i in obj1) {
obj2[i] = obj1[i]
}
obj2.d = '4'
console.log(obj1) // {a: "1", b: "2", c: "3"}
console.log(obj2) // {a: "1", b: "2", c: "3", d: "4"}
// 浅拷贝函数封装
function shallowCopy(obj1, obj2) {
for(var key in obj1) {
obj2[key] = obj1[key]
}
}
但上面代码只能实现一层的拷贝,无法进行深层次的拷贝,封装函数再次通过对象数组嵌套测试如下: 结果证明,如果对象内还有对象,则只能复制嵌套对象的地址,无法进行深层次的拷贝,当改变obj2嵌套对象c的值后,obj1嵌套对象c的值也跟着变了这个时候我们可以使用深拷贝来完成,所谓深拷贝,就是能够实现真正意义上的数组和对象的拷贝,我们通过递归调用浅拷贝的方式实现。 。
深拷贝
深拷贝不会拷贝引用类型的引用,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突
数组和对象的浅拷贝
数组
「1. Array.concat()」
let arr = ['one', 'two', 'three'];
let newArr = arr.concat();
newArr.push('four')
console.log(arr) // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]
「2.Array.slice()」
let arr = ['one', 'two', 'three'];
let newArr = arr.slice();
newArr.push('four')
console.log(arr) // ["one", "two", "three"]
console.log(newArr) // ["one", "two", "three", "four"]
对象
「1.Object.assign」
let arr = {
a: 'one',
b: 'two',
c: 'three'
};
let newArr = Object.assign({}, arr)
newArr.d = 'four'
console.log(arr); // {a: "one", b: "two", c: "three"}
console.log(newArr); // {a: "one", b: "two", c: "three", d: "four"}
「2. 对象浅拷贝方法封装」
「原理就是遍历对象复制」
let shallowCopy = function (obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
let newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
数组和对象的深拷贝
1.JSON.parse(JSON.stringify())
这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
但是要注意这种方法的坑: 「JSON.stringify()有一些局限,比如对于RegExp类型和Function类型则无法完全满足,而且不支持有循环引用的对象。」

「2.深拷贝的一个通用方法」
实现思路:拷贝的时候判断属性值的类型,如果是对象,继续递归调用深拷贝函数 原理是:「递归遍历对象拷贝每一层」
// 定义一个深拷贝函数 接收目标target参数
function deepClone(target) {
// 定义一个变量
let result;
// 如果当前需要深拷贝的是一个对象的话
if (typeof target === 'object') {
// 如果是一个数组的话
if (Array.isArray(target)) {
result = []; // 将result赋值为一个数组,并且执行遍历
for (let i in target) {
// 递归克隆数组中的每一项
result.push(deepClone(target[i]))
}
// 判断如果当前的值是null的话;直接赋值为null
} else if(target===null) {
result = null;
// 判断如果当前的值是一个RegExp对象的话,直接赋值
} else if(target.constructor===RegExp){
result = target;
}else {
// 否则是普通对象,直接for in循环,递归赋值对象的所有值
result = {};
for (let i in target) {
result[i] = deepClone(target[i]);
}
}
// 如果不是对象的话,就是基本数据类型,那么直接赋值
} else {
result = target;
}
// 返回最终结果
return result;
}
可以看一下效果
let obj1 = {
a: {
c: /a/,
d: undefined,
b: null
},
b: function () {
console.log(this.a)
},
c: [
{
a: 'c',
b: /b/,
c: undefined
},
'a',
3
]
}
let obj2 = deepClone(obj1);
console.log(obj2);

感谢您的查阅,代码冗余或者有错误的地方望不吝赐教;菜鸟一枚,请多关照
本文使用 mdnice 排版