本文主要講一下 js 的基本數據類型以及一些堆和棧的知識和什麼是深拷貝、什麼是淺拷貝、深拷貝與淺拷貝的區別,以及怎麼進行深拷貝和怎麼進行淺拷貝。javascript
本文思惟導圖以下:
html
本文首發於個人我的博客:cherryblog.site/java
其實深拷貝和淺拷貝的主要區別就是其在內存中的存儲類型不一樣。數組
堆和棧都是內存中劃分出來用來存儲的區域。ecmascript
棧(stack)爲自動分配的內存空間,它由系統自動釋放;而堆(heap)則是動態分配的內存,大小不定也不會自動釋放。函數
在將深拷貝和淺拷貝以前,咱們先來從新回顧一下 ECMAScript 中的數據類型。主要分爲ui
undefined,boolean,number,string,null
)基本數據類型主要是:undefined,boolean,number,string,null
。spa
存放在棧內存中的簡單數據段,數據大小肯定,內存空間大小能夠分配,是直接按值存放的,因此能夠直接訪問。設計
javascript中的原始值(undefined、null、布爾值、數字和字符串)與對象(包括數組和函數)有着根本區別。原始值是不可更改的:任何方法都沒法更改(或「突變」)一個原始值。對數字和布爾值來講顯然如此 —— 改變數字的值自己就說不通,而對字符串來講就不那麼明顯了,由於字符串看起來像由字符組成的數組,咱們指望能夠經過指定索引來假改字符串中的字符。實際上,javascript 是禁止這樣作的。字符串中全部的方法看上去返回了一個修改後的字符串,實際上返回的是一個新的字符串值。3d
基本數據類型的值是不可變的,動態修改了基本數據類型的值,它的原始值也是不會改變的,例如:
var str = "abc";
console.log(str[1]="f"); // f
console.log(str); // abc複製代碼
這一點其實開始我是比較迷惑的,老是感受 js 是一個靈活的語言,任何值應該都是可變的,真是圖樣圖森破,咱們一般狀況下都是對一個變量從新賦值,而不是改變基本數據類型的值。就如上述引用所說的那樣,在 js 中沒有方法是能夠改變布爾值和數字的。卻是有不少操做字符串的方法,可是這些方法都是返回一個新的字符串,並無改變其原有的數據。
因此,記住這一點:基本數據類型值不可變。
基本類型的比較是值的比較,只要它們的值相等就認爲他們是相等的,例如:
var a = 1;
var b = 1;
console.log(a === b);//true複製代碼
比較的時候最好使用嚴格等,由於 ==
是會進行類型轉換的,好比:
var a = 1;
var b = true;
console.log(a == b);//true複製代碼
引用類型(object
)是存放在堆內存中的,變量其實是一個存放在棧內存的指針,這個指針指向堆內存中的地址。每一個空間大小不同,要根據狀況開進行特定的分配,例如。
var person1 = {name:'jozo'};
var person2 = {name:'xiaom'};
var person3 = {name:'xiaoq'};複製代碼
引用類型是能夠直接改變其值的,例如:
var a = [1,2,3];
a[1] = 5;
console.log(a[1]); // 5複製代碼
因此每次咱們對 js 中的引用類型進行操做的時候,都是操做其對象的引用(保存在棧內存中的指針),因此比較兩個引用類型,是看其的引用是否指向同一個對象。例如:
var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false複製代碼
雖然變量 a 和變量 b 都是表示一個內容爲 1,2,3 的數組,可是其在內存中的位置不同,也就是說變量 a 和變量 b 指向的不是同一個對象,因此他們是不相等的。
瞭解了基本數據類型與引用類型的區別以後,咱們就應該能明白傳值與傳址的區別了。
在咱們進行賦值操做的時候,基本數據類型的賦值(=)是在內存中新開闢一段棧內存,而後再把再將值賦值到新的棧中。例如:
var a = 10;
var b = a;
a ++ ;
console.log(a); // 11
console.log(b); // 10複製代碼
因此說,基本類型的賦值的兩個變量是兩個獨立相互不影響的變量。
可是引用類型的賦值是傳址。只是改變指針的指向,例如,也就是說引用類型的賦值是對象保存在棧中的地址的賦值,這樣的話兩個變量就指向同一個對象,所以二者之間操做互相有影響。例如:
var a = {}; // a保存了一個空對象的實例
var b = a; // a和b都指向了這個空對象
a.name = 'jozo';
console.log(a.name); // 'jozo'
console.log(b.name); // 'jozo'
b.age = 22;
console.log(b.age);// 22
console.log(a.age);// 22
console.log(a == b);// true複製代碼
在深刻了解以前,我認爲上面的賦值就是淺拷貝,哇哈哈,真的是圖樣圖森破。上面那個應該只能算是「引用」,並不算是真正的淺拷貝。
一下部分參照知乎中的提問: javascript中的深拷貝和淺拷貝
那麼賦值和淺拷貝有什麼區別呢,咱們看下面這個例子:
var obj1 = {
'name' : 'zhangsan',
'age' : '18',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
var obj3 = shallowCopy(obj1);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
obj2.language[1] = ["二","三"];
obj3.language[2] = ["四","五"];
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
// 'language' : [1,["二","三"],["四","五"]],
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
// 'language' : [1,["二","三"],["四","五"]],
//};複製代碼
先定義個一個原始的對象 obj1
,而後使用賦值獲得第二個對象 obj2
,而後經過淺拷貝,將 obj1
裏面的屬性都賦值到 obj3
中。也就是說:
obj1
:原始數據obj2
:賦值操做獲得obj3
:淺拷貝獲得而後咱們改變 obj2
的 name
屬性和 obj3
的 name
屬性,能夠看到,改變賦值獲得的對象 obj2
同時也會改變原始值 obj1
,而改變淺拷貝獲得的的 obj3
則不會改變原始對象 obj1
。這就能夠說明賦值獲得的對象 obj2
只是將指針改變,其引用的仍然是同一個對象,而淺拷貝獲得的的 obj3
則是從新建立了新對象。
然而,咱們接下來來看一下改變引用類型會是什麼狀況呢,我又改變了賦值獲得的對象 obj2
和淺拷貝獲得的 obj3
中的 language
屬性的第二個值和第三個值(language
是一個數組,也就是引用類型)。結果見輸出,能夠看出來,不管是修改賦值獲得的對象 obj2
和淺拷貝獲得的 obj3
都會改變原始數據。
這是由於淺拷貝只複製一層對象的屬性,並不包括對象裏面的爲引用類型的數據。因此就會出現改變淺拷貝獲得的 obj3
中的引用類型時,會使原始數據獲得改變。
深拷貝:將 B 對象拷貝到 A 對象中,包括 B 裏面的子對象,
淺拷貝:將 B 對象拷貝到 A 對象中,但不包括 B 裏面的子對象
-- | 和原數據是否指向同一對象 | 第一層數據爲基本數據類型 | 原數據中包含子對象 |
---|---|---|---|
賦值 | 是 | 改變會使原數據一同改變 | 改變會使原數據一同改變 |
淺拷貝 | 否 | 改變不會使原數據一同改變 | 改變會使原數據一同改變 |
深拷貝 | 否 | 改變不會使原數據一同改變 | 改變不會使原數據一同改變 |
看了這麼半天,你也應該清楚什麼是深拷貝了吧,若是還不清楚,我就剖腹自盡(ಥ_ಥ)
深拷貝是對對象以及對象的全部子對象進行拷貝。
那麼問題來了,怎麼進行深拷貝呢?
思路就是遞歸調用剛剛的淺拷貝,把全部屬於對象的屬性類型都遍歷賦給另外一個對象便可。咱們直接來看一下 Zepto 中深拷貝的代碼:
// 內部方法:用戶合併一個或多個對象到第一個對象
// 參數:
// target 目標對象 對象都合併到target裏
// source 合併對象
// deep 是否執行深度合併
function extend(target, source, deep) {
for (key in source)
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
// source[key] 是對象,而 target[key] 不是對象, 則 target[key] = {} 初始化一下,不然遞歸會出錯的
if (isPlainObject(source[key]) && !isPlainObject(target[key]))
target[key] = {}
// source[key] 是數組,而 target[key] 不是數組,則 target[key] = [] 初始化一下,不然遞歸會出錯的
if (isArray(source[key]) && !isArray(target[key]))
target[key] = []
// 執行遞歸
extend(target[key], source[key], deep)
}
// 不知足以上條件,說明 source[key] 是通常的值類型,直接賦值給 target 就是了
else if (source[key] !== undefined) target[key] = source[key]
}
// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target){
var deep, args = slice.call(arguments, 1);
//第一個參數爲boolean值時,表示是否深度合併
if (typeof target == 'boolean') {
deep = target;
//target取第二個參數
target = args.shift()
}
// 遍歷後面的參數,都合併到target上
args.forEach(function(arg){ extend(target, arg, deep) })
return target
}複製代碼
在 Zepto 中的 $.extend
方法判斷的第一個參數傳入的是一個布爾值,判斷是否進行深拷貝。
在 $.extend
方法內部,只有一個形參 target,這個設計你真的很巧妙。
由於形參只有一個,因此 target 就是傳入的第一個參數的值,並在函數內部設置一個變量 args 來接收去除第一個參數的其他參數,若是該值是一個布爾類型的值的話,說明要啓用深拷貝,就將 deep 設置爲 true,並將 target 賦值爲 args 的第一個值(也就是真正的 target)。若是該值不是一個布爾類型的話,那麼傳入的第一個值仍爲 target 不須要進行處理,只須要遍歷使用 extend 方法就能夠。
這裏有點繞,可是真的設計的很精妙,建議本身打斷點試一下,會有意外收穫(玩轉 js 的大神請忽略)。
而在 extend 的內部,是拷貝的過程。
參考文章: