前言
JS 的数据类型主要分为两类:基础数据类型和引用数据类型。
基础类型的数据直接存放在栈内存中;引用类型的数据存放堆内存中,栈内存中存放该数据的内存地址(指针)。
let a = 'first';
let arr1 = [1, 2, '3'];
let obj = { a: 'second', b: 16, c: a, d: arr1};
a = 'first1';
console.log(obj) // { a: 'second', b: 16, c: 'first', d: [1, 2, "3"]}
arr1.push('44'); // 修改数组 arr1 的值
// arr1 的值改变后,obj中的属性 d 的值也发生了变化
console.log(obj) // { a: 'second', b: 16, c: 'first', d: [1, 2, "3", "44"]}
// 直接修改 obj.d
obj.d.push(55)
// 导致 arr1 的值也发生了变化
console.log(arr1); // [1, 2, "3", "44", 55]
改变引用类型数据的值时候,凡是引用该值的变量,也会发生改变。
看了上面的例子,就会发现,假如我们的变量指向同一个内存地址,修改它的值,就会影响到所有引用它的变量。
为了解决上面的问题,我们就需要对值做浅拷贝(shallow clone
)或者深拷贝(deep clone
)。
什么是浅拷贝和深拷贝
浅拷贝和深拷贝主要是针对Object
(包含函数、Regex
、Date
)和Array
的。
浅拷贝只拷贝指向某个对象的内存地址(指针),而不是复制对象本身,新旧对象共享一个内存地址,修改任意一个都会影响到另外一个。 浅拷贝会创建一个新的对象,对原始对象的属性进行逐一的拷贝。如果原始对象的属性是基础类型的话,拷贝就是属性的值;如果原始对象的属性是引用类型的话,拷贝的是就是内存地址,因此修改后会相互影响。
深拷贝会额外创造一个一模一样的对象,新旧对象不共享同一个内存地址,修改不会相互影响。
浅拷贝的实现
浅拷贝只能实现一层的拷贝,无法进行深层次的拷贝
对象浅拷贝
1. 展开运算符
let obj = {
a: '',
b: null,
c: undefined,
d: ['1', 2, 3, { a: 12 }],
};
let shallowCloneObj = {...obj};
console.log(shallowCloneObj)
2. Object.assign()
let obj = {
a: '',
b: null,
c: undefined,
d: ['1', 2, 3, { a: 12 }],
};
let shallowCloneObj = Object.assign(obj);
console.log(shallowCloneObj)
3. for..in 遍历
function shallowClone(obj) {
if (!isObject(obj)) return obj;
const cloneObj = Array.isArray(obj) ? [] : {};
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
cloneObj[prop] = obj[prop];
}
}
return cloneObj;
};
4. 依次赋值
let obj = {
a: '',
d: ['1', 2, 3, { a: 12 }],
};
let shallowCloneObj = {};
shallowCloneObj.a = obj.a
shallowCloneObj.d = obj.d
数组浅拷贝
Array.prototype 中的一些能够返回一个新数组的方法,都可以看做是浅拷贝
1. Array.prototype.slice()
let arr = [1, 2, 3, 4];
console.log(arr.slice(0))
2. Array.prototype.concat()
let arr = [1, 2, 3, 4];
console.log(arr.concat([]))
3. Array.prototype.map()
let arr = [1, 2, 3, 4];
console.log(arr.map((ele) => ele))
3. Array.prototype.filter()
let arr = [1, 2, 3, 4];
console.log(arr.filter(() => true))
4. 展开运算符
let arr = [1, 2, 3, 4];
console.log([...arr])
5. 依次赋值
值比较简单的时候可以使用
let arr = [1, 2];
let shallowCloneArr = [arr[0], arr[1]]
手写实现浅拷贝
// 判断是否是对象
const isObject = (obj) => typeof obj === 'object' && obj !== null;
const shallowClone = (obj) => {
if (!isObject(obj)) return obj;
const cloneObj = Array.isArray(obj) ? [] : {};
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
cloneObj[prop] = obj[prop];
}
}
return cloneObj;
}
深拷贝的实现
JSON.stringfy()
JSON.stringfy()
是前端很常用的深拷贝方式,把对象序列化成为 JSON
的字符串,并将对象里面的内容转换成字符串,然后用 JSON.parse()
将 JSON
字符串生成一个新的对象。
let obj = {
a: '',
b: null,
c: undefined, // undefined
d: ['1', 2, 3, { a: 12 }],
e: {m: 5, n: '66'},
m: new Date(), // Date
n: NaN,
f: function fn(){ console.log('11') }, // 函数
r: new RegExp('/d'), // RegExp
s: Symbol(66), // Symbol
[Symbol('test')]: 1,
}
// 定义不可枚举的属性
Object.defineProperty(obj, 'innumerable', { enumerable: false, value: 'innumerable'});
console.log(JSON.parse(JSON.stringify(obj)));
/** 结果如下
a: ""
b: null
d: (4) ["1", 2, 3, {…}]
e: {m: 5, n: "66"}
m: "2021-09-09T11:42:05.771Z",
n: null
r: {}
**/
但是有个问题需要注意:
- 拷贝的对象的值中如果有 函数、
undefined
、Symbol
这几种类型,经过JSON.stringify
序列化之后的字符串中这个键值对会消失 - 如果是
RegExp
对象,会变成{}
- 如果是
Date
会转为字符串 NaN
会转为null
- 不可枚举的属性(
enumerable: false
),无法拷贝
lodash.cloneDeep()
源码:
手写递归实现
function cloneDeep(target) {
// 判断是否为对象
if (!typeof targe === 'object') {
return target;
}
// 定义一个空的对象
let newObj = Array.isArray(target) ? [] : {};
for(let key in target) {
newObj[key] = typeof target[key] === 'object' ? cloneDeep(target[key]) : target[key]
}
return newObj;
}
思考
赋值和浅拷贝的不同之处
- 如果是基础类型数据的话,可以说赋值和浅拷贝一样的,
- 如果是引用类型数据的话,赋值赋的其实是该对象的在栈中的地址,而不是堆中的数据,两个数据相互影响,任何一个发生变化,另外一个也会变化;浅拷贝会创建一个新的对象,对原始对象的属性进行逐一的拷贝。如果原始对象的属性是基础类型的话,拷贝就是属性的值;如果原始对象的属性是引用类型的话
结语
如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。
文章如有错误之处,希望在评论区指正🙏🙏。