淺拷貝
前面已經(jīng)提到疙描,在定義一個對象或數(shù)組時,變量存放的往往只是一個地址业舍。當(dāng)我們使用對象拷貝時,如果屬性是對象或數(shù)組時升酣,這時候我們傳遞的也只是一個地址舷暮。因此子對象在訪問該屬性時,會根據(jù)地址回溯到父對象指向的堆內(nèi)存中噩茄,即父子對象發(fā)生了關(guān)聯(lián)下面,兩者的屬性值會指向同一內(nèi)存空間。
/**
* 淺拷貝copy绩聘,只深拷貝了第一級的數(shù)值
*/
var a = { key: "111" }
function copy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
return c;
}
a.key1 = 1
a.key2 = ["xiao", "xiao"] //引用類型是淺拷貝沥割,結(jié)果相同
var b = copy(a)
b.key = "22" //數(shù)值部分是深拷貝耗啦,結(jié)果不同
b.key3 = "333";
b.key2.push("xiao")
console.log(a)
console.log(b)
輸出結(jié)果:
{ key: '111', key1: 1, key2: [ 'xiao', 'xiao', 'xiao' ] }
{ key: '22',
key1: 1,
key2: [ 'xiao', 'xiao', 'xiao' ],
key3: '333' }
上述語句輸入結(jié)果的原因是:
key1的值屬于基本類型,所以拷貝的時候傳遞的就是該數(shù)據(jù)段机杜;
但是key2的值是堆內(nèi)存中的對象帜讲,所以key2在拷貝的時候傳遞的是指向key2對象的地址,無論復(fù)制多少個key2椒拗,其值始終是指向父對象的key2對象的內(nèi)存空間似将。
用下圖來表示內(nèi)存中語句的賦值過程如下:
常見的淺拷貝的方法:(這里的淺拷貝指的是“只深拷貝第一級屬性”)
/**
* ES6實現(xiàn)只深拷貝第一級的方法
*/
//1: Object.assign()
var a = { name: "暖風(fēng)" }
var b = Object.assign({}, a);
b.age = 18;
console.log(a.age);//undefined
console.log("-------------")
//2: 數(shù)組
var a = [1, 2, 3];
var b = a.slice();
b.push(4);
console.log(a)//[ 1, 2, 3 ]
console.log(b)//[ 1, 2, 3, 4 ]
var a = [{ "k": [1, 2] }]
var b = a.slice();
b[0].k = [1, 2]
console.log(a)//[ { k: [ 1, 2 ] } ]
console.log(b)//[ { k: [ 1, 2 ] } ]
console.log("-------------")
//3: concat
var a = [1, 2, 3];
var b = a.concat();
b.push(4);
console.log(a)//[ 1, 2, 3 ]
console.log(b)//[ 1, 2, 3, 4 ]
var a = [{ "k": [1, 2] }]
var b = a.slice();
b[0].k = [1, 2]
console.log(a)//[ { k: [ 1, 2 ] } ]
console.log(b)//[ { k: [ 1, 2 ] } ]
console.log("-------------")
//4 :...
var a = [1, 2, 3];
var b = [...a];
b.push(4)
console.log(a)//[ 1, 2, 3 ]
console.log(b)//[ 1, 2, 3, 4 ]
var a = [{ "k": [1, 2] }];
var b = [...a];
console.log(a)//[ { k: [ 1, 2 ] } ]
console.log(b)//[ { k: [ 1, 2 ] } ]
深拷貝
或許以上并不是我們在實際編碼中想要的結(jié)果,我們不希望父子對象之間產(chǎn)生關(guān)聯(lián)陡叠,那么這時候可以用到深拷貝玩郊。既然屬性值類型是數(shù)組和或象時只會傳址肢执,那么我們就用遞歸來解決這個問題枉阵,把父對象中所有屬于對象的屬性類型都遍歷賦給子對象即可。測試代碼如下:
/**
* 深拷貝實現(xiàn)方法
*/
var a = { key1: "11111" }
function _copy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === "object") {
c[i] = (p[i].constructor === Array) ? [] : {}
_copy(p[i], c[i]);
} else {
c[i] = p[i]
}
}
return c;
}
a.key2 = ["小", "小"]
var b = {}
b = _copy(a, b);
b.key1 = "22"
b.key2.push("大");
console.log(a)
console.log(b)
輸出結(jié)果:
{ key1: '11111', key2: [ '小', '小' ] }
{ key1: '22', key2: [ '小', '小', '大' ] }
好了预茄,我們看下圖內(nèi)存中的賦值過程:
來個最后的總結(jié) : 基本數(shù)據(jù)類型和引用數(shù)據(jù)類型區(qū)別
1兴溜、聲明變量時內(nèi)存分配不同
- 原始類型:在棧中,因為占據(jù)空間是固定的耻陕,可以將他們存在較小的內(nèi)存中-棧中拙徽,這樣便于迅速查詢變量的值
- 引用類型:存在堆中,棧中存儲的變量诗宣,只是用來查找堆中的引用地址膘怕。
這是因為:引用值的大小會改變,所以不能把它放在棧中召庞,否則會降低變量查尋的速度岛心。相反,放在變量的椑鹤疲空間中的值是該對象存儲在堆中的地址忘古。地址的大小是固定的,所以把它存儲在棧中對變量性能無任何負(fù)面影響
2诅诱、不同的內(nèi)存分配帶來不同的訪問機(jī)制
在javascript中是不允許直接訪問保存在堆內(nèi)存中的對象的髓堪,所以在訪問一個對象時,首先得到的是這個對象在堆內(nèi)存中的地址娘荡,然后再按照這個地址去獲得這個對象中的值干旁,這就是傳說中的按引用訪問。而原始類型的值則是可以直接訪問到的炮沐。
3争群、復(fù)制變量時的不同
- 原始值:在將一個保存著原始值的變量復(fù)制給另一個變量時,會將原始值的副本賦值給新變量央拖,此后這兩個變量是完全獨(dú)立的祭阀,他們只是擁有相同的value而已鹉戚。
- 引用值:在將一個保存著對象內(nèi)存地址的變量復(fù)制給另一個變量時,會把這個內(nèi)存地址賦值給新變量专控,
也就是說這兩個變量都指向了堆內(nèi)存中的同一個對象抹凳,他們中任何一個作出的改變都會反映在另一個身上。
(這里要理解的一點(diǎn)就是伦腐,復(fù)制對象時并不會在堆內(nèi)存中新生成一個一模一樣的對象赢底,只是多了一個保存指向這個對象指針的變量罷了)。多了一個指針
4柏蘑、參數(shù)傳遞的不同(把實參復(fù)制給形參的過程)
首先我們應(yīng)該明確一點(diǎn):ECMAScript中所有函數(shù)的參數(shù)都是按值來傳遞的幸冻。
但是為什么涉及到原始類型與引用類型的值時仍然有區(qū)別呢?還不就是因為內(nèi)存分配時的差別咳焚。
- 原始值:只是把變量里的值傳遞給參數(shù)洽损,之后參數(shù)和這個變量互不影響。
- 引用值:對象變量它里面的值是這個對象在堆內(nèi)存中的內(nèi)存地址革半,這一點(diǎn)你要時刻銘記在心碑定!
因此它傳遞的值也就是這個內(nèi)存地址,這也就是為什么函數(shù)內(nèi)部對這個參數(shù)的修改會體現(xiàn)在外部的原因了又官,因為它們都指向同一個對象延刘。