Appearance
深浅拷贝
这个问题牵扯到 JS 的数据类型:基础类型和引用类型。
浅拷贝
对于基础类型 浅拷贝其实没有影响,但是对于引用类型,因为浅拷贝只是对数据一层的拷贝,所以引用类型的值是还是相互影响的
js
var a = { a: 1, b: { b1: 2, b2: 3 } }
function shallowClone(source) {
var target = {}
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i]
}
}
return target
}
var c = shallowClone(a)
c.a = 2
c.b.b1 = 4
console.log(a.a)
console.log(a.b.b1)
结果如下图
发现对于 a.b.b1 的值修改了 c 的值 a 的值也改变了
深拷贝
对于深拷贝就是解决上面所说的,如果对象的属性的值也是一个对象,那么也需要将此属性的对象的值进行拷贝,而不是只是将对象的地址赋值过来。
对于深拷贝一般存在两种方式
利用 JSON 对象中的 parse 和 stringify
利用递归来实现每一层都重新创建对象并赋值
JSON.stringify/parse 的方法
先看看这两个方法吧:
The JSON.stringify() method converts a JavaScript value to a JSON string.
JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。
The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.
JSON.parse 是将一个 JSON 字符串转成一个 JavaScript 值或对象。
很好理解吧,就是 JavaScript 值和 JSON 字符串的相互转换。
它能实现深拷贝呢?我们来试试。
js
const originArray = [1, 2, 3, 4, 5]
const cloneArray = JSON.parse(JSON.stringify(originArray))
console.log(cloneArray === originArray) // false
const originObj = { a: "a", b: "b", c: [1, 2, 3], d: { dd: "dd" } }
const cloneObj = JSON.parse(JSON.stringify(originObj))
console.log(cloneObj === originObj) // false
cloneObj.a = "aa"
cloneObj.c = [1, 1, 1]
cloneObj.d.dd = "doubled"
console.log(cloneObj) // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj) // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:
js
const originObj = {
name: "axuebin",
sayHello: function () {
console.log("Hello World")
},
}
console.log(originObj) // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj))
console.log(cloneObj) // {name: "axuebin"}
发现在 cloneObj 中,有属性丢失了。。。那是为什么呢?
在 MDN 上找到了原因:
If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).
undefined、function、symbol 会在转换过程中被忽略。。。
明白了吧,就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝。
- 如果对象存在 BigInt 类型的值、循环引用就会报错
- Date 引用类型 : 拷贝的时候会变成字符串。
- 属性丢失: 属性值为 undefined 类型、function 类型、Symbol 类型
- 属性值为空对象: 属性值为 RegExp 类型、Set 类型、Map 类型
- 不能拷贝 不能枚举的属性、原型上的属性
递归的方法
递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,简单粗暴上代码:
js
function deepClone(source) {
const targetObj = source.constructor === Array ? [] : {} // 判断复制的目标是数组还是对象
for (let keys in source) {
// 遍历目标
if (source.hasOwnProperty(keys)) {
if (source[keys] && typeof source[keys] === "object") {
// 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {}
targetObj[keys] = deepClone(source[keys])
} else {
// 如果不是,就直接赋值
targetObj[keys] = source[keys]
}
}
}
return targetObj
}
我们来试试:
js
const originObj = { a: "a", b: "b", c: [1, 2, 3], d: { dd: "dd" } }
const cloneObj = deepClone(originObj)
console.log(cloneObj === originObj) // false
cloneObj.a = "aa"
cloneObj.c = [1, 1, 1]
cloneObj.d.dd = "doubled"
console.log(cloneObj) // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj) // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
可以。那再试试带有函数的:
js
const originObj = {
name: "axuebin",
sayHello: function () {
console.log("Hello World")
},
}
console.log(originObj) // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj)
console.log(cloneObj) // {name: "axuebin", sayHello: ƒ}
也可以。搞定。
是不是以为这样就完了?? 当然不是。
其实对于深拷贝我们主要考虑下面几个问题
栈溢出
循环引用问题
一些特殊的属性 如值为函数、undefined、symbol、Null 的属性
下面我们就分别使用以上三种情况的值去进行两种深拷贝方法的测试
js
var obj = {
a: {},
}
let c = obj.a
for (var i = 0; i < 200000; i++) {
c[1] = {}
c = c[1]
}
// 我们生成一个 200002层的对象 obj = { a : { 1 : { 1 : { 1 : { 1 : {} } }}}}
deepClone(obj) // Maximum call stack size exceeded
JSON.parse(JSON.stringify(obj)) // Maximum call stack size exceeded
说明两种方式都会出现栈溢出问题
例子 2
js
var obj = {
a: {},
}
obj.a = obj
// 我们生成一个 200002层的对象 obj = { a : { 1 : { 1 : { 1 : { 1 : {} } }}}}
deepClone(obj) // Maximum call stack size exceeded
JSON.parse(JSON.stringify(obj)) // Converting circular structure to JSON
特别是对于 Vue 其 this 上就保留有当前实例对象
那么我们如何解决循环引用的问题
例子 3
js
var obj = {
a: {},
b: function () {},
c: undefined,
d: null,
e: Symbol("fn"),
}
// 我们生成一个 200002层的对象 obj = { a : { 1 : { 1 : { 1 : { 1 : {} } }}}}
console.log(deepClone(obj).f) // Maximum call stack size exceeded
console.log(JSON.parse(JSON.stringify(obj)).f) // Converting circular structure to JSON
我们发现通过JSON.parse(JSON.stringify(obj))
其 b、c、e 都没有复制过来
所以我们通过上面可以看出JSON.parse(JSON.stringify(obj))
的不足之处:
也会引起栈溢出
如果有循环引用就会报
// Converting circular structure to JSON
对于 undefined, a function, or a symbol 是不可以拷贝的
而对于自定义深拷贝的方法 我们也需要解决的问题