值類型與引用類型
談淺拷貝與深拷貝之前,我們需要先理清一個概念,即值類型與引用類型。
什么是值類型與引用類型?這要先從JS中的基本類型說起。
首先我們知道,JS中有六種基本類型,number, string, boolean, null, undefined,object
。這幾個類型就統(tǒng)共被分為兩類,值類型與引用類型。
number,string,boolean,undefined
就是值類型;object
就是引用類型。object
里面涵蓋的就多了,我們常用的數(shù)組呀,函數(shù)呀,還有什么Date對象,Math對象,這些都算在object
里面的。
這里面null
比較特殊,ECMA標準中將它定義為值類型,當你使用在你的編譯器里執(zhí)行typeof(null)
時,它的返回值是object
。我個人偏好于將它理解為一個指向空對象的指針,便于理解。
(在stackoverflow上搜索的時候看到這么一個回答
If
null
is a primitive, why doestypeof(null)
return"object"
?
Because the spec says so.
深以為然哈哈哈哈
)
等等,可能有人要問了,你說null
是一個空對象指針,那什么是指針呢?
不著急,讓我們從計算機如何存儲一個數(shù)據(jù)說起。
值類型與引用類型的存儲
計算機存儲值類型和引用類型的方法是不同的。這里我們需要提到兩種分配內(nèi)存的數(shù)據(jù)結構,堆和棧。
什么是堆和棧呢?這講起來就復雜了,我們只需要知道,棧和堆都是一種內(nèi)存的分配方式,棧是后進先出的,堆是先進先出的(這個聽起來有點像隊列,但實際上它的存儲更像是鏈表)。
棧里面的數(shù)據(jù)占據(jù)空間的大小是固定的(例如JS里的數(shù)字就固定為64bit的浮點數(shù)),空間也是相對較小的,JS里面會把值類型放到棧里面去存儲,而存儲的就是這個值本身。
而堆里面的數(shù)據(jù)占據(jù)空間的大小是不固定的,空間相對較大,JS會把引用類型的值放到堆里面去存儲,而把這個引用類型的地址存放到棧里面去(這個保存地址的變量就是指針)。
為什么要這樣做呢?
你想呀,我們學的很多知識,什么算法呀,什么數(shù)據(jù)結構呀,都有一個中心思想,節(jié)約是美德。而計算機里最寶貴的是什么?內(nèi)存和CPU呀。
想想我們平常會用到的引用類型,數(shù)組元素可以幾百上千,對象里面定義幾十個成員,函數(shù)里面變量表達式幾十行。跟值類型比起來,引用類型的大小不定,而且通常還蠻大的。這么些個大家伙,計算可要好好想想怎么存儲它們。
于是計算機拿了一個指針指向引用類型,當你想要用到那些引用值時,計算機就會去找指向它們地址的指針,然后再去找到它們的值。
于是,回歸正題,當我們想要拷貝一個變量的值得時候,它的存儲類型就決定了我們拷貝一個值的方式。
這里偷一張《JavaScript高級程序設計》里面的圖,很清晰了表示了兩者的區(qū)別。
值類型的拷貝
JS里面,經(jīng)常有這么一個需求,讓你去實現(xiàn)一個函數(shù),可以復制當前傳入?yún)?shù)的值,而傳入的參數(shù)有數(shù)字、布爾值、字符串,當然,還有對象。
透過上面的圖,我們可以很輕松地就完成一個值類型的拷貝。
上面我們說了,值類型是存儲在棧里面的,直接存儲的就是這個變量的值。那么要拷貝值類型,很直接的將這個變量賦值給另一個新的變量就行了。
引用類型的淺拷貝與深拷貝
淺拷貝
引用類型與值類型就不同了。
引用類型的淺拷貝,我個人認為就是上圖所示,直接拷貝的對象的引用,放到代碼里面就長這樣。
var obj = {
"a":"1",
"b":"2",
"c":{
"c1":3,
"c2":4
}
}
var newObj = obj;
newObj[a] = 3;
console.log(obj[a]);//3
很容易理解,拷貝了原對象的引用,那么這個新變量的值實際上保存的就是原對象的地址,當新對象對對象中的值進行賦值的時候,同時也改變了原對象的值。
也有人把只拷貝對象中的一層屬性的拷貝稱為淺拷貝。什么意思呢?像上面的那個對象的a和b屬性就只有一層屬性,而c屬性復雜一些,它代表了一個對象。
但是我決定把這個放在深拷貝里討論。
深拷貝
深拷貝是一個復雜的命題。何為深拷貝?即復制一個與原對象一模一樣的對象,包括里面的每個屬性,不論是嵌套了幾層的,是日期還是數(shù)組還是對象。并且兩者的地址不同,是兩個獨立的對象。與淺拷貝不同,不論你如何修改新對象的值,都不會對舊的對象造成任何的影響。
遍歷屬性拷貝
最簡單也是最容易想到的一個辦法,即創(chuàng)建一個新的空對象,把原對象的值遍歷一遍,然后賦給新對象。
var obj = {
"a": 1,
"object": {
"b": [2, 3, 4],
"c": 3
}
}
function cloneObject(obj) {
var copy = {};
for (var prop in obj) {
if (obj.hasOwnProperty(prop))
copy[prop] = obj[prop];
}
return copy;
}
var newObj = cloneObject(obj);
console.log(newObj);//與obj看起來似乎是相同的
然而事實真是這樣嗎?讓我們改變一下newObj中object屬性中的值,然后打印出來原對象object屬性的值。
newObj["object"].c = 4;
console.log(obj["object"].c);//變成了4
這是為什么呢?
這是因為當我們遍歷到例如(原對象中的)對象或者數(shù)組這樣引用類型時,進行的卻是淺拷貝。
于是問題來了,這種拷貝方式如果要進行真真正正的深拷貝必然是不行的,對于對象中的引用類型,我們還要做一次深拷貝。如何做呢?遞歸。
遞歸拷貝
這是我在做百度前端學院的2015春季題的時候?qū)懙纳羁截惔a,只考慮了對象中出現(xiàn)數(shù)組、對象、日期的情況。(這里我也記錄了一下做春季題的思路和代碼,有興趣可以看看我的另一篇博文:點我)
function getVarType(data) {
//確定當前變量的對象
if (data === undefined) {
return 'Undefined';
}
if (data === null) {
return 'Null';
}
return Object.prototype.toString.call(data).slice(8, -1).toLowerCase();
};
function cloneObject(data) {
var objectType = getVarType(data);
//the object for cloning is native object
if (objectType == "null" || objectType == "undefined") {
return data;
}
if (objectType == "string" || objectType == "number" || objectType == "boolean") {
var copy = data;
return copy;
} else if (objectType == "date") {
var copy = new Date();
copy.setTime(data.getTime());
return copy;
} else if (objectType == "array") {
var copy = [];
for (var i = 0; i < data.length; i++) {
copy[i] = cloneObject(data[i]);
}
return copy;
} else if (objectType == "object") {
var copy = {};
for (var attr in data) {
if (data.hasOwnProperty(attr)) {
copy[attr] = cloneObject(data[attr]);
}
}
return copy;
}
}
前面一大堆完成了對值類型和數(shù)組字符串日期的拷貝。最后一個if語句中,完成了對對象的深拷貝。
這里用到遞歸,相當于再對對象的屬性值做一次深拷貝,如果是值類型,直接賦值就好,如果是引用類型,再按分類進行分別的拷貝。
讓我們用在這個函數(shù)再進行一次上面的檢測。
var obj = {
"a": 1,
"object": {
"b": [2, 3, 4],
"c": 3
}
}
var newObj = cloneObject(obj);
console.log(newObj);
newObj["object"].c = 4;
console.log(obj["object"].c);//與新對象不同,這里輸出的值為3
于是,我們完成了對對象的深拷貝。
但是等等。
是不是還有點東西沒考慮?
想想如果對象屬性的值有函數(shù)呢?讓我們來試試這個例子。
var obj = {
"a": 1,
"b": {
"c": 2,
},
"c": function hello() {
console.log("hello,world");
}
}
console.log(cloneObject(obj));
/*
打印結果如下:
{
a: 1,
b: {
c: 2,
},
c: undefined
}
*/
我們這個函數(shù)有點小小的遺憾,它不能拷貝函數(shù)。
但是仔細想想,我們需要拷貝函數(shù)嗎?
函數(shù)是做什么用的?我們需要它去實現(xiàn)一個功能的,拷貝一個一模一樣的函數(shù),它實現(xiàn)的功能不也一模一樣嗎?拷貝一個函數(shù)真的有必要嗎?(并不是偷懶哈哈哈哈哈)
自己寫完了,讓我們也來看看用點其他方法去實現(xiàn)的深拷貝。
jQuery實現(xiàn)深拷貝
jQuery要實現(xiàn)深拷貝,要用到extend
這個方法,這是干嘛的呢?讓我們看看文檔:
Merge the contents of two or more objects together into the first object.
[jQuery.extend( deep ], target, object1 [, objectN ] )
deep
Type: Boolean
If true, the merge becomes recursive (aka. deep copy). Passing
false
for this argument is not supported.target
Type: Object
The object to extend. It will receive the new properties.
object1
Type: Object
An object containing additional properties to merge in.
objectN
Type: Object
Additional objects containing properties to merge in.
jQuery怎么做深拷貝?簡單粗暴一行代碼var newObj = $.extend(true,{},obj);
至于具體的,等博主有力氣了再來分析分析源碼(躺)。
JSON實現(xiàn)深拷貝
JSON怎么做深拷貝?最開始我挺莫名其妙的,然后看了代碼才豁然開朗。
也是簡單粗暴的一句代碼newObj = JSON.parse( JSON.stringify(obj) );
巧用了JSON的parse和stringify,但是它也沒辦法實現(xiàn)函數(shù)的拷貝。
其他
還有一些工具庫,例如lodash,underscore等等,這些對深拷貝的實現(xiàn),就……等以后再分析分析啦。