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

简介: 面试中能把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异步编程吧

后记

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

相关文章
|
1月前
|
JavaScript 前端开发
JavaScript中的深拷贝与浅拷贝
JavaScript中的深拷贝与浅拷贝
46 4
|
2月前
|
JavaScript 前端开发
js 变量作用域与解构赋值| 22
js 变量作用域与解构赋值| 22
|
2月前
|
JSON JavaScript 数据格式
手写JS实现深拷贝函数
本文介绍了如何实现一个深拷贝函数`deepClone`,该函数可以处理对象和数组的深拷贝,确保拷贝后的对象与原始对象在内存中互不干扰。通过递归处理对象的键值对和数组的元素,实现了深度复制,同时保留了函数类型的值和基础类型的值。
22 3
|
3月前
|
缓存 JavaScript 安全
2022年最新最详细的安装Node.js以及cnpm(详细图解过程、绝对成功)
这篇文章提供了2022年最新最详细的Node.js和cnpm安装教程,包括步骤图解、全局配置路径、cnpm安装命令、nrm的安装与使用,以及如何管理npm源和测试速度。
2022年最新最详细的安装Node.js以及cnpm(详细图解过程、绝对成功)
|
3月前
|
存储 JavaScript 前端开发
2022年前端js面试题
2022年前端js面试题
39 0
|
3月前
|
JavaScript 前端开发
JavaScript中的深拷贝与浅拷贝
JavaScript中的深拷贝与浅拷贝
40 2
|
3月前
|
JavaScript 前端开发
JavaScript中的深拷贝和浅拷贝的实现讲解
在JavaScript中,浅拷贝与深拷贝用于复制对象。浅拷贝仅复制基本类型属性,对于引用类型仅复制引用,导致双方共享同一数据,一方修改会影响另一方。深拷贝则完全复制所有层级的数据,包括引用类型,确保双方独立。浅拷贝可通过简单属性赋值实现,而深拷贝需递归复制各层属性以避免共享数据。
64 1
|
3月前
|
JavaScript 前端开发
js中浅拷贝和深拷贝的区别
js中浅拷贝和深拷贝的区别
29 0
|
3月前
|
JavaScript 前端开发
js中浅拷贝,深拷贝的实现
js中浅拷贝,深拷贝的实现
34 0
|
6月前
|
JSON 前端开发 JavaScript
【面试题】JavaScript 深拷贝和浅拷贝 高级
【面试题】JavaScript 深拷贝和浅拷贝 高级