浅谈 JS 里的浅拷贝和深拷贝
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 函数里栈内存示意图

什么是浅拷贝和深拷贝
正因为引用类型存储在堆内存里,栈内存中存储指向堆内存的引用的设定,造成了复制它们的方式不同,由此有浅拷贝和深拷贝区分。
浅拷贝只复制指向每个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
浅拷贝示意图
深拷贝创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响原对象。
深拷贝示意图
手动实现浅拷贝和深拷贝
本篇博客主要讨论 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]
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
Objet.assign( )
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象, 它将返回目标对象。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值, 来看个例子:
const book = {
title: "JavaScript高级程序设计",
};
const anotherBook = Object.assign({}, book);
anotherBook.title = "JavaScript权威指南";
console.log(book); // {title: "JavaScript权威指南"}
console.log(anotherBook); //{title: "JavaScript权威指南"}
- 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]
深拷贝
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 值的属性,
- 手写递归复制方法。用
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;
};
通过对需要拷贝的对象属性进行递归遍历,如果遇到对象的属性值为对象类型时,继续递归遍历,属性值为基本类型是,就把属性和属性值赋给新对象。
其他内置对象的拷贝
Set
、Map
、WeakSet
和WeakMap
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}
使用内置对象的构造函数可以拷贝出新的内置对象实例
- 箭头函数与普通函数
箭头函数没有 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
方法用来深拷贝数据
lodash.clonedeep
方法 minified 大小仅仅 8.9kb,gzip 后只有 3.3kb,推荐在项目中直接使用 lodash.clonedeep
参考链接
转载自:https://juejin.cn/post/7360980866794930186