JavaScript 賦值,淺復制和深復制的區別
前言:
淺復制和深復制可以說是面試中很常見的一道題瞭,本文就來聊一聊JavaScript中的淺復制和深復制。
一、變量賦值
不知道會不會有人會和我一樣,會覺得淺復制就是通過=
操作符將一個變量賦值給另外一個變量。但實際上,淺復制和變量賦值之間是存在區別的。所以,我們先來瞭解一下變量賦值。
1.1 原始值和引用值
原始值(primitive value):最簡單的數據,即:Undefined、Null、Boolean、Number、String、BigInt、Symbol這其中類型的值。保存原始值的變量是按值訪問的,我們操作的就是存儲在變量中的實際值。
引用值(reference value):有多個值構成的對象,即 Object 類型。引用值是保存在內存中的對象,但是 JavaScript 不允許直接訪問內存位置,所以不能直接操作對象所在的內存空間。在操作對象時,實際操作的是該對象的引用(reference) 而非實際的對象本身。保存引用值得變量實際上存儲得是對象得引用,是按引用訪問得。
1.2 賦值
首先說明這裡說的賦值,不是直接把引用值(例如:{})或者原始值(例如: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 高級程序設計(第四版)》
接下來要說的淺復制和深復制就是針對引用值而言的。
二、淺復制(Shallow Copy)
我們先來看一篇博客中對於淺復制的定義:
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.
對此,個人的理解淺復制就是復制該對象的的每個屬性,如果該屬性值是原始值,則復制該原始值,如果屬性值是一個對象,那麼就復制該對象的引用。
即:淺復制將復制頂層屬性,但嵌套對象在原始(源)和拷貝(目標)之間共享
2.1 原生 JavaScript 中的淺復制
Object.assign()
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 }
2.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; }
三、深復制(Deep Copy)
首先來看深復制的定義:
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.
與淺復制不同時,當源對象屬性的值為對象時,賦值的是該對象,而不是對象的引用。所以深復制中,源對象和拷貝對象之間不存在任何共享的內容。
2.1 原生 JavaScript 中的深復制
JSON.parse(JSON.stringify(object))
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 } }
2.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 中的實現。
小結
- 賦值操作符是把一個對象的引用賦值給一個變量,所以變量中存儲的是對象的引用
- 淺復制是復制源對象的每個屬性,但如果屬性值是對象,那麼復制的是這個對象的引用。所以源對象和拷貝對象之間共享嵌套對象。
- 深復制與淺復制不同的地方在於,如果屬性值為對象,那麼會復制該對象。源對象和拷貝對象之間不存在共享的內容。
到此這篇關於JavaScript 賦值,淺復制和深復制的區別的文章就介紹到這瞭,更多相關JS 賦值內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- JavaScript深拷貝的幾種實現方法實例
- js中如何復制一個數組(淺復制、深復制)
- 帶你搞懂js的深拷貝
- JS對象復制(深拷貝和淺拷貝)
- 詳解JSON.parse和JSON.stringify用法