都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异步编程吧

后记

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

相关文章
|
28天前
|
JavaScript 前端开发 开发者
混淆赋值运算符(=)和相等比较运算符(==, ===)(js的问题)
混淆赋值运算符(=)和相等比较运算符(==, ===)(js的问题)
|
28天前
|
JSON JavaScript 数据格式
深拷贝和浅拷贝(js的问题)
深拷贝和浅拷贝(js的问题)
12 0
|
12天前
|
JavaScript
js浅拷贝与深拷贝的区别?
js浅拷贝与深拷贝的区别?
|
13天前
|
JavaScript 前端开发 安全
js中浅拷贝和深拷贝的区别
js中浅拷贝和深拷贝的区别
|
19天前
|
JavaScript 前端开发
js中浅拷贝和深拷贝的区别
js中浅拷贝和深拷贝的区别
24 1
|
2月前
|
JavaScript 前端开发
js开发:请解释什么是ES6的解构赋值(destructuring assignment),并给出一个示例。
ES6的解构赋值简化了JavaScript中从数组和对象提取数据的过程。例如,`[a, b, c] = [1, 2, 3]`将数组元素赋值给变量,`{name, age} = {name: '张三', age: 18}`则将对象属性赋值给对应变量,提高了代码的可读性和效率。
|
2月前
|
JavaScript 前端开发
javascript中的解构赋值
javascript中的解构赋值
|
2月前
|
JavaScript 前端开发 API
javascript中的浅拷贝和深拷贝
javascript中的浅拷贝和深拷贝
|
JavaScript 前端开发
JavaScript专题之深浅拷贝
JavaScript 专题系列第六篇,讲解深浅拷贝的技巧和以及实现深浅拷贝的思路
85 0
JavaScript专题之深浅拷贝
|
2天前
|
存储 移动开发 JavaScript
学习javascript,前端知识精讲,助力你轻松掌握
学习javascript,前端知识精讲,助力你轻松掌握