变量
要理解JS中深浅拷贝和浅拷贝,先要熟悉变量类型,JS中变量分为基本数据类型
(值类型)和引用数据类型
(复杂数据类型)。基本数据类型的值是直接存在栈内存的,而引用数据类型的栈内存保存的是内存地址,值保存在堆内存中。
基本数据类型有 Number
、String
、 Boolean
、Null
、 Undefined
、 Symbol
、 BigInt
。
引用数据类型主要有Object
、 Array
、Date
、Error
、 Function
、 RegExp
。
引用数据类型的存储如下图所示
赋值拷贝
赋值拷贝就是我们常用的
=
赋值。赋值拷贝分为基本数据类型赋值拷贝和引用数据类型赋值拷贝。
基本数据类型的赋值拷贝相互之间是不会有影响。
let name = "randy";
let name2 = name; // 将 name 赋值给 name2
name = "demi"; // 修改 name 的值为 'demi'
console.log(name); // demi
console.log(name2); // randy
引用数据类型的赋值拷贝是地址引用
,即两个变量指向堆内存中的同一个地址,所以相互之间就会有影响。
const user = { name: "randy" };
const user2 = user;
user.name = "demi";
console.log(user.name); // demi
console.log(user2.name); // demi
那我不想引用数据类型之间的拷贝相互之间影响呢?就需要用到我们的浅拷贝深拷贝知识啦。浅拷贝、深拷贝只针对引用数据来讲,基本数据类型没有浅拷贝深拷贝一说。
浅拷贝
浅拷贝只拷贝原对象的第一层属性。也就是说如果属性是基本数据类型,拷贝的就是基本类型的值。如果属性是引用数据类型,拷贝的是引用类型的内存地址。
手动实现浅拷贝
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝,不处理原型上的属性
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
}
}
return newObject;
}
常用浅拷贝方法
在JS中常见的浅拷贝方法有对象的Object.assign()
、扩展运算符{...obj}
和数组的Array.concat()
、Array.slice()
、Array.from()
、扩展运算符[...arr]
和lodash库的clone方法。
下面我用例子说明
const user = {
name: "randy",
address: { province: "湖南", city: "汨罗" },
};
const user2 = Object.assign({}, user);
const user3 = { ...user };
user.name = "demi";
user.address.province = "上海";
console.log("user:", user); // {name: "demi", address: {province: '上海', city: '汨罗'}}
console.log("user2:", user2); // {name: "randy", address: {province: '上海', city: '汨罗'}}
console.log("user3:", user3); // {name: "randy", address: {province: '上海', city: '汨罗'}}
上面的例子,基本数据类型name
修改不会互相影响,但是address
引用数据类型修改会互相影响。
const arr = ["randy", { province: "湖南", city: "汨罗" }];
const arr2 = arr.concat([]);
const arr3 = arr.slice();
const arr4 = Array.from(arr);
const arr5 = [...arr];
arr[0] = "demi";
arr[1].province = "上海";
console.log("arr:", arr); // {name: "demi", address: {province: '上海', city: '汨罗'}}
console.log("arr2:", arr2); // {name: "randy", address: {province: '上海', city: '汨罗'}}
console.log("arr3:", arr3); // {name: "randy", address: {province: '上海', city: '汨罗'}}
console.log("arr4:", arr4); // {name: "randy", address: {province: '上海', city: '汨罗'}}
console.log("arr5:", arr5); // {name: "randy", address: {province: '上海', city: '汨罗'}}
上面的例子,基本数据类型name
修改不会互相影响,但是address
引用数据类型修改会互相影响。
深拷贝
要解决浅拷贝的问题就要用到我们的深拷贝啦!
深拷贝是从内存中完整的拷贝一份出来,在堆内存中开一个新的内存空间,与原对象完全独立。修改新对象不会影响原对象。
手动实现深拷贝
function deepCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝,不处理原型上的属性
for (let key in object) {
if (object.hasOwnProperty(key)) {
// 如果还是对象,则递归处理
newObject[key] =
typeof object[key] === "object"
? deepCopy(object[key])
: object[key];
}
}
return newObject;
}
常用深拷贝方法
在JS中深拷贝除了自己手动实现外还可以使用JSON.parse(JSON.stringfy(obj))
或者lodash库的deepClone方法。
下面我用例子说明
// 对象
const user = {
name: "randy",
address: { province: "湖南", city: "汨罗" },
};
const user2 = JSON.parse(JSON.stringify(user));
user.name = "demi";
user.address.province = "上海";
console.log("user:", user); // {name: "demi", address: {province: '上海', city: '汨罗'}}
console.log("user2:", user2); // {name: "randy", address: {province: '湖南', city: '汨罗'}}
上面的例子,不管基本数据类型还是引用数据类型,修改相互之间不会有影响
// 数组
const arr = ["randy", { province: "湖南", city: "汨罗" }];
const arr2 = JSON.parse(JSON.stringify(arr));
arr[0] = "demi";
arr[1].province = "上海";
console.log("arr", arr); // {name: "demi", address: {province: '上海', city: '汨罗'}}
console.log("arr2", arr2); // {name: "randy", address: {province: '湖南', city: '汨罗'}}
上面的例子,不管基本数据类型还是引用数据类型,修改相互之间不会有影响
扩展
JSON.parse(JSON.stringfy(obj))真的完美无瑕吗?
虽然JSON.parse(JSON.stringfy(obj))
好用也可以实现深拷贝,但是需要注意几个点
undefined
、任意的函数
以及symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时),或者被转换成null
(出现在数组中时)。函数
、undefined
、Symbol
被单独转换时,会返回undefined
。- 所有以
symbol
为属性键的属性都会被完全忽略掉。 Date
日期调用了toJSON()
将其转换为了string
字符串(同Date.toISOString()
),因此会被当做字符串处理。- 错误对象会被转成空对象。
- 正则会被转成空对象。
NaN
和Infinity
格式的数值及null
都会被当做null
。- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 当尝试去转换
BigInt
类型的值会抛出TypeError ("BigInt value can't be serialized in JSON"
)。
undefined
、任意的函数
以及 symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时),或者被转换成 null
(出现在数组中时)。
const obj4 = {
a: undefined,
b: function say() {},
c: Symbol(123),
};
const str4 = JSON.stringify(obj4);
console.log(str4); // {}
const obj5 = [undefined, function say() {}, Symbol(123)];
const str5 = JSON.stringify(obj5);
console.log(str5); // [null,null,null]
函数
、undefined
、Symbol
被单独转换时,会返回 undefined
。
console.log(
JSON.stringify(Symbol(123)),
JSON.stringify(undefined),
JSON.stringify(function say() {})
); // undefined undefined undefined
所有以 symbol
为属性键的属性都会被完全忽略掉。
const s1 = Symbol();
const obj6 = { a: 1, b: 2, [s1]: 3 };
console.log(JSON.stringify(obj6)); // {"a":1,"b":2}
Date
日期调用了 toJSON()
将其转换为了 string
字符串(同Date.toISOString()
),因此会被当做字符串处理。
const obj7 = { a: 1, b: 2, c: new Date() };
console.log(JSON.stringify(obj7)); // {"a":1,"b":2,"c":"2022-02-17T06:22:43.145Z"}
错误对象会被转成空对象。
//5、
const obj8 = { a: 1, b: 2, c: new Error("error") };
console.log("错误会被转成空对象: ", JSON.stringify(obj8)); // {"a":1,"b":2,"c":{}}
正则会被转成空对象。
const obj9 = { a: 1, b: 2, c: new RegExp("\\d", "i") };
console.log("正则会被转成空对象: ", JSON.stringify(obj9)); // {"a":1,"b":2,"c":{}}
NaN
和 Infinity
格式的数值及 null
都会被当做 null
。
const obj10 = { a: 1, b: 2, c: NaN, d: Infinity, e: null };
console.log(JSON.stringify(obj10)); // {"a":1,"b":2,"c":null,"d":null,"e":null}
对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
// const obj11 = {};
// const obj12 = { a: obj11 };
// obj11.a = obj12;
// console.log(JSON.stringify(obj12));
当尝试去转换 BigInt
类型的值会抛出TypeError ("BigInt value can't be serialized in JSON")
。
// const obj11 = { a: 1, b: 2, c: BigInt("12222222222222222222222") };
// console.log("BigInt 类型的值会抛出TypeError: ", JSON.stringify(obj11));
好啦,关于JS赋值拷贝、浅拷贝、深拷贝,笔者已经讲完啦,小伙伴们是否弄懂了呢?最后感谢大家的耐心观看。
系列文章
后记
本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!