堆和棧的區別
堆和棧都是內存中劃分出來的用于存儲的區域。
深拷貝與淺拷貝的區別就是其在內存中存儲的類型不同。
棧(stack)為自動分配的內存空間,它由系統自動釋放;而堆(heap)則是動態分配的內存,大小不定也不會自動釋放。
ECMAScript的數據類型
基本數據類型 (number
string
boolean
undefined
null
)。
上面5個是ECMAScript中的5種基本數據類型。
基本數據類型存放在棧中
存放在棧內存中的簡單數據段,數據大小確定,內存空間大小可以分配,是直接按值存放的,所以可以直接訪問。
基本數據類型數據值不可改變
javascript中的原始值(undefined、null、布爾值、數字和字符串)與對象(包括數組和函數)有著根本區別。原始值是不可更改的:任何方法都無法更改(或“突變”)一個原始值。對數字和布爾值來說顯然如此 —— 改變數字的值本身就說不通,而對字符串來說就不那么明顯了,因為字符串看起來像由字符組成的數組,我們期望可以通過指定索引來假改字符串中的字符。實際上,javascript 是禁止這樣做的。字符串中所有的方法看上去返回了一個修改后的字符串,實際上返回的是一個新的字符串值。
例如:
// 基本類型的值不可改變
var str='html'
str[0]='c'
console.log(str) // html
基本類型的比較是值的比較
基本類型的比較是值的比較,只要它們相等就認為它們是相等的。
// 基本類型的比較是值的比較
var a='html'
var b='html'
console.log(a===b) // true
但是注意,比較的時候請使用 === 三個等號,表示嚴格等于。
比如:
// js會默認做類型轉換
var a=1
var b=true
console.log(a==b) // true
引用類型
引用類型(object
)是存放在堆內存中的,變量實際上是一個存放在棧內存的指針,這個指針指向堆內存中的地址。 每個空間大小不一樣,要根據情況進行特定的分配,例如。
var person1 = {name:'apple'};
var person2 = {name:'xiaomi'};
var person3 = {name:'huawei'};
引用類型值可變
// 引用類型值可變
var arr=[1,2,3,4,5]
arr[0]=10
console.log(arr) // [10, 2, 3, 4, 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={}
var b=a
a.name='jozo'
console.log(a.name) // jozo
console.log(b.name) // jozo
淺拷貝
上面的=只是引用,不是淺拷貝。
賦值(=)與淺拷貝的區別
下面是一個例子:
// 賦值(=)與淺拷貝的區別
var obj1={
name: 'xiaomi',
age: 13,
language:['English','Chinese','Germany']
}
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="huawei"
obj3.age=18
obj2.language='Germany'
obj3.language='Greek'
console.log(obj1)
/*
{
age: 13
language: (3) ["Greek", "French", "Germany"]
name: "huawei"
__proto__: Object
}
*/
console.log(obj2)
/*
{
age: 13
language: Array(3)
0: "Greek"
1: "French"
2: "Germany"
length: 3
__proto__: Array(0)
name: "huawei"
}
*/
console.log(obj3)
/*
{
age: 18
language: Array(3)
0: "Greek"
1: "French"
2: "Germany"
length: 3
__proto__: Array(0)
name: "xiaomi"
}
*/
先定義個一個原始的對象 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
方法就可以。
而在 extend 的內部,是拷貝的過程。