拷贝的问题主要是针对引用类型
浅拷贝和深拷贝区别
对于这个问题,首先让我们先简单回顾一下 JavaScript 的基本知识
1、JavaScript 包含两种不同数据类型的值:基本类型(原始值)
和 引用类型
基本类型有以下几种,具体如下:
string、number、boolean、null、undefined、symbol、bigInt引用类型具体有:
Object(Object、Array、Function…)
在将一个值赋给变量时,解析器必须确定这个值是基本类型还是引用类型
- 基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值
- 引用类型的值是保存在内存中的对象,栈内存存储的是变量的标识符以及对象在堆内存中的存储地址。JavaScript 不允许直接访问内存中的位置,即不能直接操作对象的内存空间。因此在操作对象时实际上是对操作对象的引用而不是实际的对象。当需要访问引用类型(如对象、数组等)的值时,首先从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需的数据
2、JavaScript 的变量存储方式 — 栈(stack)
和 堆(heap)
栈
:自动分配内存空间,系统自动释放,里面存放的是基本类型的值和引用类型的地址指针堆
:动态分配内存,大小不定,也不会自动释放,里面存放引用类型的值
3、JavaScript 值传递与址传递
基本类型与引用类型最大的区别实际就是传值与传址的区别
- 值传递:基本类型采用的是值传递
let a = 1;
let b = a;
b++;
console.log(a, b) // 1, 2
复制代码
- 址传递:引用类型则是地址传递,将存放在栈内存中的地址赋值给接收的变量
let a = ['a', 'b', 'c'];
let b = a;
b.push('d');
console.log(a) // ['a', 'b', 'c', 'd']
console.log(b) // ['a', 'b', 'c', 'd']
复制代码
分析:
- a 是数组是引用类型,赋值给 b 就是将 a 的地址赋值给 b,因此 a 和 b 指向同一个地址(该地址都指向了堆内存中引用类型的实际的值)
- 当 b 改变了这个值的同时,因为 a 的地址也指向了这个值,故 a 的值也跟着变化,就好比 a 租了一间房,将房间的地址给了 b,b 通过地址找到了房间,那么 b 对房间做的任何改变对 a 来说肯定同样是可见的
那么如何解决上面出现的问题,这里就引出了浅拷贝或者深拷贝了。JS 的基本类型不存在浅拷贝还是深拷贝的问题,主要是针对引用类型
浅拷贝
:拷贝的级别浅。浅拷贝是指复制对象时只对第一层键值对进行复制,若对象内还有对象则只能复制嵌套对象的地址指针
- 缺点:当有一个属性是引用值(数组或对象)时,按照这种克隆方式,只是把这个引用值的指向赋给了新的目标对象,即一旦改变了源对象或目标对象的引用值属性,另一个也会跟着改变
深拷贝
:拷贝级别更深。深拷贝是指复制对象时是完全拷贝,即使嵌套了对象,拷贝后两者也相互不影响,修改一个对象的属性不会影响另一个。原理其实是递归把那些值是对象的属性再次进入对象内部进行复制
浅拷贝
slice
、concat
若是数组,数组元素均为基本数据类型,可利用数组的一些方法如 slice
、concat
返回一个新数组的特性来实现拷贝(此时相当于深拷贝)
若数组的元素是引用类型(Object,Array),slice
和 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"]
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"]
let arr = [{a:1}, 'two', 'three'];
let newArr = arr.concat();
newArr[0].a = 2;
console.log(arr) // [{a: 2},"two","three"]
console.log(newArr) // [{a: 2},"two","three"]
复制代码
Object assign()
该方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。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"}
let arr = {
a: 'one',
b: 'two',
c: {a: 1}
};
let newArr = Object.assign({}, arr)
newArr.c.a = 3;
console.log(arr); // {a: "one", b: "two", c: {a: 3}}
console.log(newArr); // {a: "one", b: "two", c: {a: 3}}
复制代码
浅拷贝封装
原理:遍历对象,然后把属性和属性值放在一个新对象并返回
function clone(obj) {
// 只拷贝对象
if (typeof src !== 'object') return;
// 根据 obj 的类型判断是新建一个数组还是对象
let newObj = Obejct.prototype.toString.call(obj) == '[object Array]' ? [] : {};
for(let prop in newObj) {
if(newObj.hasOwnProperty(prop)) {
newObj[prop] = obj[src];
}
}
return newObj;
}
复制代码
深拷贝
JSON.parse(JSON.stringify(arr))
:不仅适用于数组还适用于对象,但该方法有局限性
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
ES6 扩展运算符[...]
:不仅适用于数组还适用于对象,只有原始值可以深拷贝,当含有引用值时进行浅拷贝
深拷贝封装
原理:在拷贝时判断一下属性值的类型,若是对象则递归调用深拷贝函数,深拷贝是完全拷贝了原对象的内容并寄存在新的内存空间,指向新的内存地址
function deepClone(src, target) {
var target = target || {};
for (let prop in src) {
if (src.hasOwnProperty(prop)) {
if(src[prop] !== 'null' && typeof(src[prop]) === 'object') {
target[prop] = Object.prototype.toString.call(src[prop]) == '[object Array]' ? [] : {};
deepClone(src[prop], target[prop]);
} else {
target[prop] = src[prop];
}
}
}
return target;
}
复制代码
库实现
上面的方式可以满足基本的场景的需求,若有更复杂的需求可自己实现。一些框架和库的也有对应的解决方案,如:jQuery.extend()
、lodash
应用场景
浅拷贝
对于一层结构的 Array
和 Object
想要拷贝一个副本时使用
vue
的 mixin
是浅拷贝的一种复杂型式
深拷贝
复制深层次的 object
数据结构,如想对某个数组或对象的值进行修改,但又要保留原数组或对象的值不被修改,此时就可以用深拷贝来创建一个新的数组或对象
参考资料
javascript中的深拷贝和浅拷贝?
JavaScript 如何完整实现深度Clone对象?
ithub lodash源码
MDN 结构化克隆算法
jQuery v3.2.1 源码