时间:2022-06-23 11:38:48 | 栏目:JavaScript代码 | 点击:次
前言:
浅复制和深复制可以说是面试中很常见的一道题了,本文就来聊一聊JavaScript中的浅复制和深复制。
不知道会不会有人会和我一样,会觉得浅复制就是通过=
操作符将一个变量赋值给另外一个变量。但实际上,浅复制和变量赋值之间是存在区别的。所以,我们先来了解一下变量赋值。
原始值(primitive value):最简单的数据,即:Undefined、Null、Boolean、Number、String、BigInt、Symbol这其中类型的值。保存原始值的变量是按值访问的,我们操作的就是存储在变量中的实际值。
引用值(reference value):有多个值构成的对象,即 Object 类型。引用值是保存在内存中的对象,但是 JavaScript 不允许直接访问内存位置,所以不能直接操作对象所在的内存空间。在操作对象时,实际操作的是该对象的引用(reference) 而非实际的对象本身。保存引用值得变量实际上存储得是对象得引用,是按引用访问得。
首先说明这里说的赋值,不是直接把引用值(例如:{})或者原始值(例如:false、1、"str"等)直接赋值给一个变量。而是通过变量把一个值赋值给另一个变量。
原始值赋值:保存原始值的变量是按值访问的,所以通过变量把一个原始值赋值给另一个变量时,原始值会被复制到新变量的位置。
let num1 = 5; let num2 = num1; console.log(num1, num2); // 5 5 num2 = 4; console.log(num1, num2); // 5 4
可以看出 num2
通过 num1
被赋值为5,保存的是同一个原始值。而且两个变量相互独立,互不干扰。
具体赋值过程如下:
引用值赋值:保存引用值的变量是按引用访问的,通过变量把一个引用赋值给另一个变量时,存储在变量中的值也会被复制到新变量的位置。但是,这里复制的实际上是一个指向存储在堆内存中对象的指针。赋值后,两个变量实际上指向同一个对象。所以两个变量通过引用对对象的操作会互相影响。
let obj1 = {}; let obj2 = obj1; console.log(obj1); // {} console.log(obj2); // {} obj1.name = 'haha'; console.log(obj1); // { name: 'haha' } console.log(obj2); // { name: 'haha' } obj1.age = 24; console.log(obj1); // { name: 'haha', age: 24 } console.log(obj2); // { name: 'haha', age: 24 }
如上代码,通过 obj1
将指向对象的引用赋值给 obj2
后, obj1
和 obj2
保存了指向同一对象的引用,所以操作的是同一对象。
具体可见下图:
注:如上两图来自《JavaScript 高级程序设计(第四版)》
接下来要说的浅复制和深复制就是针对引用值而言的。
我们先来看一篇博客中对于浅复制的定义:
An object is said to be shallow copied when the source top-level properties are copied without any reference and there exist a source property whose value is an object and is copied as a reference. If the source value is a reference to an object, it only copies that reference value to the target object.
对此,个人的理解浅复制就是复制该对象的的每个属性,如果该属性值是原始值,则复制该原始值,如果属性值是一个对象,那么就复制该对象的引用。
即:浅复制将复制顶层属性,但嵌套对象在原始(源)和拷贝(目标)之间共享
Object.assign()
方法将所有可枚举(Object.propertyIsEnumerable()
返回 true)和自有(Object.hasOwnProperty()
返回 true)属性从一个或多个源对象复制(浅复制) 到目标对象,返回修改后的对象。
如下代码可以看出,浅复制和变量赋值不同,修改对象的属性值互不影响。
const source = { a: 1, b: 2 }; const objCopied = Object.assign({}, source); console.log(objCopied); // { a: 1, b: 2 } source.a = 3; console.log(source); // { a: 3, b: 2 } console.log(objCopied); // { a: 1, b: 2 } objCopied.a = 4; console.log(source); // { a: 3, b: 2 } console.log(objCopied); // { a: 4, b: 2 }
对象内的嵌套对象在源对象和拷贝对象之间还是共享的,如上代码,修改对象内对象的属性时会相互影响。
const source = { a : {b : 1} }; const objCopied = Object.assign({}, source); console.log(objCopied); // { a: { b: 1 } } source.a.b = 3; console.log(source); // { a: { b: 3 } } console.log(objCopied); // { a: { b: 3 } }
但是注意如下代码中,source.a = {};
修改的是源对象中属性的值,这个并不共享。
const source = { a : {b : 1} }; const objCopied = Object.assign({}, source); console.log(objCopied); // { a: { b: 1 } } source.a = {}; console.log(source); // { a: {} } console.log(objCopied); // { a: { b: 1 } }
展开运算符(...):展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。
const source = { a : {b : 1}, c: 2 }; const objCopied = {...source} console.log(objCopied); // { a: { b: 1 }, c: 2 } source.c = 3; console.log(source); // { a: { b: 1 }, c: 3 } console.log(objCopied); // { a: { b: 1 }, c: 2 } source.a.b = 3; console.log(source); // { a: { b: 3 }, c: 3 } console.log(objCopied); // { a: { b: 3 }, c: 2 }
function shallowClone(source) { // 如果是原始值,直接返回 if (typeof source !== 'object') { return source; } // 拷贝后的对象 const copied = Array.isArray(source) ? [] : {}; // 遍历对象的key for(let key in source) { // 如果key是对象的自有属性 if(source.hasOwnProperty(key)) { // 复制属性 copied[key] = source[key] } } // 返回拷贝后的对象 return copied; }
首先来看深复制的定义:
A deep copy will duplicate every object it encounters. The copy and the original object will not share anything, so it will be a copy of the original.
与浅复制不同时,当源对象属性的值为对象时,赋值的是该对象,而不是对象的引用。所以深复制中,源对象和拷贝对象之间不存在任何共享的内容。
JavaScript 中最常见的深复制的方法就是JSON.parse(JSON.stringify(object))
如下代码所示,深复制中源对象和拷贝对象不共享任何内容,即使是嵌套对象。
let obj = { a: 1, b: { c: 2, }, } let newObj = JSON.parse(JSON.stringify(obj)); obj.b.c = 20; console.log(obj); // { a: 1, b: { c: 20 } } console.log(newObj); // { a: 1, b: { c: 2 } }
function deepClone(source) { // 如果是原始值,直接返回 if (typeof source !== 'object') { return source; } // 拷贝后的对象 const copied = Array.isArray(source) ? [] : {}; // 遍历对象的key for(let key in source) { // 如果key是对象的自有属性 if(source.hasOwnProperty(key)) { // 深复制 copied[key] = deepClone(source[key]); } } return copied; }
有关浅复制和深复制的手动实现,这里只是简单实现了一下。其中还有很多细节未实现,具体的实现大家可以参见 lodash 中的实现。