深入理解JavaScript引用類型的深拷貝、淺拷貝和按值傳參

對C++熟悉的同學肯定很熟悉:值和引用。比如按值傳參和按引用傳參,按值返回和按引用返回。那在javascript裏邊,變量複製以及傳參時,又會是神馬情況呢?不同語言之間,有些基本概念的區別還是需要細細品味的。

首先明確JavaScript(ECMAScript)中的基本概念:
變量包括兩種:基本類型和引用類型。
基本類型:Undefined,Null,Boolean,Number,String
引用類型:Object,Array,Data,RegExp,Function

在 JavaScript 中, 引用類型是一種數據結構,用於將數據和功能組織在一起。它也常被稱爲類。(實際上和C++的中的類不同,叫做類並不妥當,只是說和類相似)。引用類型的值(即對象)是引用類型的一個實例。

注意:
1、引用類型的值是保存在內存中的對象。與C++不同, JavaScript 沒有指針,不允許直接訪問內存中的位置,也就是說不能直接操作對象的內存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。爲此,引用類型的值是按引用訪問的。
2、在很多語言中,字符串以對象的形式來表示,因此被認爲是引用類型的。JavaScript 放棄了這一傳統。String是基本類型。

1、基本類型的變量複製/訪問

如果從一個變量向另一個變量複製基本類型的值,會在變量對象上創建一個新值,然後把該值複製到爲新變量分配的位置上,新值是和原值是完全獨立的一個副本,兩者無任何關係

var num1 = 5;
var num2 = num1;

在此, num1 中保存的值是 5。當使用 num1 的值來初始化 num2 時, num2 中也保存了值 5。但 num2中的 5 與 num1 中的 5 是完全獨立的,該值只是 num1 中 5 的一個副本。此後,這兩個變量可以參與任
何操作而不會相互影響。

2、引用類型的變量複製/訪問

當從一個變量向另一個變量複製引用類型的值時,同樣也會將存儲在變量對象中的值複製一份放到爲新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一個對象。複製操作結束後,兩個變量實際上將引用同一個對象。因此,改變其中一個變量,就會影響另一個變量。也就是我們說的淺拷貝。

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name);     //結果爲"Nicholas"

首先,變量 obj1 保存了一個對象的新實例。然後,這個值被複制到了 obj2 中;換句話說, obj1
和 obj2 ,相當於指針,都指向內存中同一個對象。這樣,當爲 obj1 添加 name 屬性後,可以通過 obj2 來訪問這個屬性,因爲這兩個變量引用的都是同一個對象。
在這裏插入圖片描述

3、基本類型和引用類型傳參:均爲按值傳參

ECMAScript 中所有函數的參數都是按值傳遞的。

也就是說,把函數外部的值複製給函數內部的參數,就和把值從一個變量複製到另一個變量一樣。基本類型值的傳遞如同基本類型變量的複製一樣,而引用類型值的傳遞,則如同引用類型變量的複製一樣。有不少開發人員在這一點上可能會感到困惑,因爲訪問變量有按值和按引用兩種方式,而參數只能按值傳遞。
而且熟悉C++的同學會糊塗,爲什麼引用類型在傳參時是按值傳參呢?
我的理解,這裏的按值傳參的含義是,在向參數傳遞基本類型的值時,被傳遞的值會被複制給一個局部變量。在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量。這個複製的過程就像上邊所講過的一樣,產生的局部變量實際上是相當於指向原對象內存的指針。所以即使引用類型是按值傳遞,但是實際上這個局部變量的變化會反映在函數的外部。

function setName(obj) {
    obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name);     //函數外部的實參person屬性也變爲了"Nicholas"

以上代碼中創建一個對象,並將其保存在了變量 person 中。然後,這個變量被傳遞到 setName()
函數中之後就被複制給了 obj。在這個函數內部, obj 和 person 引用的是同一個對象。換句話說,即
使這個變量是按值傳遞的, obj 也會按引用來訪問同一個對象。於是,當在函數內部爲 obj 添加 name
屬性後,函數外部的 person 也將有所反映;因爲 person 指向的對象在堆內存中只有一個,而且是全
局對象。

有很多開發人員錯誤地認爲:在局部作用域中修改的對象會在全局作用域中反映出來,這不就說明
參數是按引用傳遞的嘛,不是跟C++的按引用傳參一樣嗎?爲了證明引用對象是按值傳遞的,我們再看一看下面這個經過修改的例子:

function setName(obj) {
    obj.name = "Nicholas";    //見3
    obj = new Object();       //見4
    obj.name = "Greg";        //見5
}                             //見6

var person = new Object();    //見1
setName(person);              //見2
alert(person.name);     //並沒有成爲"Greg",而是仍舊爲"Nicholas"

如果 person 是按引用傳遞的,那麼obj的name變爲"Greg"時,person應該也會變成新new出來的對象並且name爲"Greg"。但是 person.name 時實際上仍然是"Nicholas"。這說明即使在函數內部修改了參數的值,但原始的引用仍然保持未變。實際上,當在函數內部重寫 obj 時,這個變量引用的就是新new出來的局部對象。而這個局部對象會在函數執行完畢後立即被銷燬。

詳細過程分析如下:
1、person引用到內存中new出來的一個對象。
2、經過引用類型的按值傳參之後,生成了副本obj(實際上是指針),可以想象它是函數中的一個局部變量,跟person一樣引用到了內存中的同一個對象。
3、修改obj對象屬性之後,內存中這個對象屬性變了,person和obj引用的都是這個對象,屬性當然也會跟着變。
4、新new出來一個對象之後賦給obj,導致obj從此以後不引用原來內存的對象,而是引用到了新的對象。
5、obj與person分別指向了不同的兩個對象,那麼修改obj當然並不會影響person了。
6、函數執行完畢後,局部變量obj自動銷燬。

講到這裏,應該能明白這裏說的引用類型的按值傳遞是怎麼回事了吧。回想一下C++的按值傳遞和按引用傳遞,(按值傳參是產生了一個完全獨立的副本。按引用傳參實際上相當於實參的一個別名。)應該能明白區別了吧。

4、如何實現深拷貝?

深拷貝在於引用類型的時候,淺拷貝只複製地址值,實際上還是指向同一堆內存中的數據,深拷貝則是重新創建了一個相同的數據,二者指向的堆內存的地址是不同的。這個時候修改賦值前的變量數據不會影響賦值後的變量。

js深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。對於循環引用的問題還有一些數據類型的拷貝,如Map, Set, RegExp, Date, ArrayBuffer 和其他內置類型。處理起來並非像想象的那麼簡單。
爲了節省時間,這裏僅僅給出一種最簡單的實現方法,可以用於通常情況下使用。

使用如下兩個方法:
JSON對象parse方法可以將JSON字符串反序列化成JS對象。
JSON對象stringify方法可以將JS對象序列化成JSON字符串。

var a = {age:18, name: "Tom", info: {address: "wuhan", interest: "playCards"}};
var b = JSON.parse(JSON.stringify(a));
a.info.address = "shenzhen";
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章