likes
comments
collection
share

浅谈 JS 里的浅拷贝和深拷贝

作者站长头像
站长
· 阅读数 36

JS 里的栈和堆内存

在 JavaScript 引擎中,对变量的存储有两种方式,栈内存堆内存

JavaScript 中的变量分为基本类型(基本数据类型)和引用类型(复杂数据类型)。一共有八种,分别是:

  • Undefined
  • Null
  • Boolean
  • String
  • Number
  • Symbol
  • BigInt (新增)
  • Object (引用数据类型)

前七种是基本数据类型,最后一种是引用数据类型

基本类型保存在栈内存中,而引用类型保存在堆内存中。但在 JS 中并没有严格区分栈堆内存。在 JS 中, 每当一个函数调用时,它会创建一个函数作用域,同时也会建立自己的栈内存,用来存放该作用域里声明的所有变量类型(基本类型存储具体的值,引用类型存储指向堆内存的引用)。而在全局作用域中,全局的所有变量保存在全局作用域的栈内存里。

用浏览器环境下列子说明下。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>栈内存和堆内存</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <script>
      function foo() {
        const color = "red";
        const subjects = [];
      }

      foo();
    </script>
  </body>
</html>
foo 函数里栈内存示意图 浅谈 JS 里的浅拷贝和深拷贝

什么是浅拷贝和深拷贝

正因为引用类型存储在堆内存里,栈内存中存储指向堆内存的引用的设定,造成了复制它们的方式不同,由此有浅拷贝和深拷贝区分。

浅拷贝只复制指向每个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

浅拷贝示意图 浅谈 JS 里的浅拷贝和深拷贝

深拷贝创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响原对象。

深拷贝示意图 浅谈 JS 里的浅拷贝和深拷贝

手动实现浅拷贝和深拷贝

本篇博客主要讨论 Object 类型和 Array 类型的浅拷贝和深拷贝。主要是下面这些形式

const arr = [] || [[], "a"];
const obj = {} || { o: {}, a: [] };

浅拷贝

1. Array.prototype.slice([begin[,end]) 方法返回一个新的数组对象,这一对象是由 begin 和 end 决定的原数组的浅拷贝,来看个例子:

const numbers = [[1], 2];
const newNumbers = numbers.slice();

newNumbers[0][0] = 3;

console.log(numbers); // [[3], 2]
console.log(newNumbers); // [[3], 2]
  1. Array.prototype.concat( ) 方法用来合并两个或多个数组,并返回一个新的数组,当源数组中的元素是个对象的引用, concat 在合并时拷贝的就是这个对象的引用,来看个例子:
const arr1 = [{ count: 2 }, 1, 2];
const arr2 = [{ count: 3 }, 3, 4];

const arr3 = arr1.concat(arr2);
arr3[0].count = 6;

console.log(arr1[0].count); // 6
  1. Objet.assign( ) 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象, 它将返回目标对象。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值, 来看个例子:
const book = {
  title: "JavaScript高级程序设计",
};

const anotherBook = Object.assign({}, book);
anotherBook.title = "JavaScript权威指南";

console.log(book); // {title: "JavaScript权威指南"}
console.log(anotherBook); //{title: "JavaScript权威指南"}
  1. ES6 新增的扩展运算符(...), 可以对数组或对象进行浅拷贝。
// 拷贝对象
const book = {
  title: "JavaScript高级程序设计",
};

const anotherBook = { ...book };
anotherBook.title = "JavaScript权威指南";

console.log(book); // {title: "JavaScript权威指南"}
console.log(anotherBook); //{title: "JavaScript权威指南"}

// 拷贝数组
const numbers = [1, 2];
const newNumbers = { ...numbers };

numbers.push(3);

console.log(numbers); // [1, 2]
console.log(newNumbers); // [1, 2, 3]

深拷贝

  1. JSON 正反序列化。JSON.stringify()JSON.parse() 的混合配对使用。来看个例子
const book = {
  title: "JavaScript高级程序设计",
};

const anotherBook = JSON.parse(JSON.stringify(book));
anotherBook.title = "JavaScript权威指南";

console.log(book); // {title: "JavaScript权威指南"}
console.log(anotherBook); //{title: "JavaScript权威指南"}

上述例子可以看出,使用 JSON.stringify() 和 JSON.parse() 确实可以实现深拷贝,在新对象中修改对象的引用时,并不会影响源对象里面的值。但 JSON.stringify() 方法本身会过滤掉值为 undefined、任意的函数以及 Symbol 值的属性,

  1. 手写递归复制方法。用 for...in 实现的 deepCopy 递归复制方法。
const deepCopy = function (obj) {
  if (obj == null || typeof obj !== "object") {
    return;
  }

  const result = Array.isArray(obj) ? [] : {};

  for (const key in obj) {
    if ({}.hasOwnProperty.call(obj, key)) {
      if (obj[key] && typeof obj[key] === "object") {
        result[key] = deepCopy(obj[key]);
      } else {
        result[key] = obj[key];
      }
    }
  }

  return result;
};

通过对需要拷贝的对象属性进行递归遍历,如果遇到对象的属性值为对象类型时,继续递归遍历,属性值为基本类型是,就把属性和属性值赋给新对象。

其他内置对象的拷贝

  1. SetMapWeakSetWeakMap
const s1 = new Set([1, 2]);
const s2 = new Set(s1);

s1.add(3);

console.log(s1); // Set(3) {1, 2, 3}
console.log(s2); // Set(2) {1, 2}

使用内置对象的构造函数可以拷贝出新的内置对象实例

  1. 箭头函数与普通函数

箭头函数没有 prototype (原型),可以由此区分箭头函数与普通函数。

const foo = () => {};
const boo = function () {};
console.log(foo.prototype); // undefined
console.log(boo.prototype); //{constructor: ƒ}

可以通过 Function 构造函数拷贝普通函数。看看下面的例子。

const foo = function () {};
const boo = new Function(foo);
boo.isType = "ordinary function";
console.log(foo.isType); // undefined
console.log(boo.isType); // 'ordinary function'

而对于箭头函数的拷贝,我们可以用 eval 方法实现。来看个例子。

const foo = () => {};
const boo = eval(foo.toString());

boo.isType = "arrow function";

console.log(foo.isType); // undefined
console.log(boo.isType); // "arrow function"

应用场景

日常开发中,JS 对象的拷贝主要用在 数据保存数据比对数据同步等需求上。

lodash.cloneDeep

社区提供了 Lodash 工具库用来处理各种数据。cloneDeep 方法用来深拷贝数据

浅谈 JS 里的浅拷贝和深拷贝

lodash.clonedeep 方法 minified 大小仅仅 8.9kb,gzip 后只有 3.3kb,推荐在项目中直接使用 lodash.clonedeep

参考链接

转载自:https://juejin.cn/post/7360980866794930186
评论
请登录