我所理解的JavaScript的深浅拷贝
这并不是我第一次接触这个话题,但是这是我第一次深入地对javaScript的深浅拷贝机制进行学习
数据类型
我们都知道javaScript拥有很多数据类型我们将他们分为两种:
- 基本数据类型
- 引用数据类型
基本数据类型包括: String, Number, Boolean, undefined, null, symbol(Es6专属)
引用数据类型包括: Object(对象)、Array(数组)、Function(函数)
对于基本数据类型来说,变量直接按值存放在栈内存的简单数据段中,可以直接进行访问,因此并不存在深浅拷贝的区别,
引用类型存放在堆内存中,变量保存的是一个指针,当我们需要访问引用类型的值的时候,首先从栈中获得该对象的地址指针,然后从堆中取得需要的数据
浅拷贝
什么是浅拷贝?
答: 浅拷贝就是只复制了引用,而没有复制真正的值,浅拷贝会使两者指向同一块内存空间
使用=
进行浅拷贝是我们做常用的操作
const originArray = [1,2,3,4];
const originObj = {name: 'a', age: 30, favorite: {obj: 'sing'}};
const cloneArray = originArray;
const cloneObj = originObj;
console.log(cloneArray); // [1,2,3,4]
console.log(cloneObj); // {name: 'a', age: 30, favorite: {obj: 'sing'}}
cloneArray.push(6);
console.log(originArray); // [1,2,3,4,6]
console.log(cloneArray); // [1,2,3,4,6]
cloneObj.name = 'b';
console.log(originObj); // {name: 'b', age: 30, favorite: {obj: 'sing'}}
console.log(cloneObj); // {name: 'b', age: 30, favorite: {obj: 'sing'}}
console.log(originObj === cloneObj) //true
以上代码使用了=
进行了浅拷贝操作,clone***
和origin***
指向的是同一块内存空间,因此一个引用的值发生改变时,代表着两个对象的值全都被修改了
深拷贝
不同于浅拷贝,深拷贝是对目标的完全拷贝,深拷贝并不只是复制了引用,而是将值也一并复制,使用了新的内存空间,成为新的数据,与旧数据互不影响
常用的深拷贝方法:
- 利用
JSON
对象的parse
和stringify
- 利用递归实现对每一层次的重新创建和赋值
JSON.stringify()和JSON.parse()方法
他们都是做什么的呢?
The
JSON.parse()
method parses a JSON string, constructing the JavaScript value or object described by the string. An optional reviver function can be provided to perform a transformation on the resulting object before it is returned.
JSON.parse()
是将一个JSON
字符串转换为Javascript
值或对象
The
JSON.stringify()
method converts a JavaScript object or value to a JSON string, optionally replacing values if a replacer function is specified or optionally including only the specified properties if a replacer array is specified.
JSON.stringify()
是将一个JavaScript
值转换为JSON
字符串
多说无益直接代码分析:
let originArr = [1,2,3,4];
let cloneArr = JSON.parse(JSON.stringify(originArr));
cloneArr.push(6);
console.log(originArr); //[ 1, 2, 3, 4 ]
console.log(cloneArr); //[ 1, 2, 3, 4, 6 ]
可以看出进行深拷贝之后,原数组的值并没有因为新的数组值改变而改变,十分方便
但是这种方法也是有一定缺点的:
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"}
此时发现sayHello
并没有被加入到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).
使用上述方法时function
、undefined
、symbol
会被自动忽略,因此当我们的原对象中含有函数时我们不能使用这种方法进行深拷贝
递归方法实现深拷贝
function deepClone(oldObj) {
const newObj = oldObj.constructor === Array ? [] : {};//判断目标为数组还是对象并
for(let key in oldObj) {
if(oldObj.hasOwnProperty(key)) {
if(oldObj[key] && typeof oldObj[key] === 'object') {
newObj[key] = deepClone(oldObj[key])
} else {
newObj[key] = oldObj[key]
}
}
}
return newObj
}
用以下方法验证一下
let oldObj = {
name: 'a',
b: {
age: 20,
favorite: 'coding'
},
sayHello() {
console.log('Hello')
}
};
let newObj = deepClone(oldObj);
newObj.b.age = 19;
console.log(oldObj); //{ name: 'a', b: { age: 20, favorite: 'coding' }, sayHello: [Function: sayHello] }
console.log(newObj); //{ name: 'a', b: { age: 19, favorite: 'coding' }, sayHello: [Function: sayHello] }
经过验证这个函数完美实现了深拷贝
Object.assign()
先来看一下MDN对于这个函数的解释
The
Object.assign()
method copies all enumerable own properties from one or more source objects to a target object. It returns the target object.
Object.assgin()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = Object.assgin({}, x);
console.log(y.b.f === x.b.f) //true
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f) //true
console.log(x); //{ a: 1, b: { f: { g: 1 } }, c: [ 1, 2, 3 ] }
console.log(y); //{ a: 1, b: { f: { g: 1 } }, c: [ 1, 2, 3 ] }
x.a = 2;
x.b.f.g = 2;
console.log(x); //{ a: 2, b: { f: { g: 2 } }, c: [ 1, 2, 3 ] }
console.log(y) //{ a: 1, b: { f: { g: 2 } }, c: [ 1, 2, 3 ] }
由上述代码可知Object.assgin
仅仅只是对源对象的第一层进行了深拷贝,而对于内层以及更深层次的对象却只是进行了浅拷贝
concat
The
concat()
method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
开起来是对源数组进行了一次深拷贝,具体状况我们可以试一下:
const originArr = [1,2,3,4];
const cloneArr = originArr.concat();
console.log(originArr === cloneArr); //false
originArr.push(6);
console.log(originArr); // [1,2,3,4,6]
console.log(cloneArr) // [1,2,3,4]
originArr.push([1,5,4]);
const cloneArr2 = originArr.concat();
console.log(originArr); //[ 1, 2, 3, 4, 6, [ 1, 5, 4 ] ]
console.log(cloneArr2) // [ 1, 2, 3, 4, 6, [ 1, 5, 4 ] ]
originArr[5].push(6)
console.log(originArr); //[ 1, 2, 3, 4, 6, [ 1, 5, 4, 6 ] ]
console.log(cloneArr2) // [ 1, 2, 3, 4, 6, [ 1, 5, 4, 6 ] ]
由此可见concat
也仅仅只是对源数组的第一层进行了深拷贝
slice
The
slice()
method returns a shallow copy of a portion of an array into a new array object selected frombegin
toend
(end
not included) wherebegin
andend
represent the index of items in that array. The original array will not be modified.
在MDN的定义中已经明确写了shallow copy,但是通过阅读**axuebin**博客文章我发现slice
方法的确不是完全的浅拷贝
const originArr = [1, 2, 3, 4, [5, 6, 7, 8]];
const cloneArr = originArr.slice();
originArr.push(9);
console.log(originArr); //[ 1, 2, 3, 4, [ 5, 6, 7, 8 ], 9 ]
console.log(cloneArr); //[ 1, 2, 3, 4, [ 5, 6, 7, 8 ] ]
通过实验证明,Array.prototype.slice()
的确并非全部浅拷贝,而是对源数组的第一层进行了深拷贝!
问题解析
为什么会出现只对第一层进行了深拷贝的情况呢?
答:因为在数组或对象中,对内层或跟深层的数组或对象都是存储了一个指针引用,而这些函数在底层实现时都是直接将第一层的数据和引用拷贝下来,因此就形成了只有首层是深拷贝的情况。
总结
- 对于
=
赋值运算符来说,仅仅是进行了浅拷贝,只是拷贝了对象或数组的引用 JSON.stringify()
实现了深拷贝,但是会自动省略源对象中的undefined
、function
、symbol
- 其他js中的函数大多数都只是实现了首层的深拷贝和内层的浅拷贝
- 递归方法是实现完全深拷贝最简单有效的方法