都2022年了你不会还没搞懂JS赋值拷贝、浅拷贝、深拷贝吧

730 阅读7分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

简介

面试中能把JS赋值拷贝、浅拷贝、深拷贝问题说明白的寥寥无几。今天笔者再来温习一遍,希望能对你有所帮助。

image.png

变量

要理解JS中深浅拷贝和浅拷贝,先要熟悉变量类型,JS中变量分为基本数据类型(值类型)和引用数据类型(复杂数据类型)。基本数据类型的值是直接存在栈内存的,而引用数据类型的栈内存保存的是内存地址,值保存在堆内存中。

基本数据类型有 NumberStringBooleanNullUndefinedSymbolBigInt

引用数据类型主要有ObjectArrayDateErrorFunctionRegExp

引用数据类型的存储如下图所示

赋值拷贝

赋值拷贝就是我们常用的=赋值。赋值拷贝分为基本数据类型赋值拷贝和引用数据类型赋值拷贝。

基本数据类型的赋值拷贝相互之间是不会有影响。

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))好用也可以实现深拷贝,但是需要注意几个点

  1. undefined任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。
  2. 函数undefinedSymbol 被单独转换时,会返回 undefined
  3. 所有以 symbol 为属性键的属性都会被完全忽略掉。
  4. Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
  5. 错误对象会被转成空对象。
  6. 正则会被转成空对象。
  7. NaNInfinity 格式的数值及 null 都会被当做 null
  8. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  9. 当尝试去转换 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]

函数undefinedSymbol 被单独转换时,会返回 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":{}}

NaNInfinity 格式的数值及 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赋值拷贝、浅拷贝、深拷贝,笔者已经讲完啦,小伙伴们是否弄懂了呢?最后感谢大家的耐心观看。

系列文章

都2022年了你不会还没搞懂JS数据类型吧

都2022年了你不会还没搞懂JS原型和继承吧

都2022年了你不会还没搞懂JS赋值拷贝、浅拷贝、深拷贝吧

都2022年了你不会还没搞懂对象数组的遍历吧

都2022年了你不会还没搞懂this吧

都2022年了你不会还没搞懂JS Object API吧

都2022年了你不会还没搞懂js垃圾回收和内存泄露吧

都2022年你不会还没搞懂js执行上下文和事件循环机制吧

都2022年了你不会还没搞懂js中的事件吧

都2020年了你不会还没搞懂js异步编程吧

后记

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!