一驶鹉、可變對象
把對象a賦值給對象b绩蜻,更改對象b的屬性值,被引用的對象a也隨之改變室埋,這就是可變對象办绝。
var a = {name: '張三', age: 18},
b = a;
b.name = '李四';
a.name; // "李四"
為啥會這樣呢?其他字符串和數(shù)值怎么不會變姚淆?要搞懂這個問題孕蝉,我們必須了解下其內(nèi)部的存儲邏輯。
JavaScript有7種基本數(shù)據(jù)類型:number腌逢、string降淮、boolean、null搏讶、undefined佳鳖、object
以及 ES6 新增的symbol
。除了 object
為引用類型媒惕,其他都是基本類型腋颠。基本類型將值存在棧中吓笙,而 object
存在棧中的是地址值而不是本身的內(nèi)容值淑玫,該地址值指向堆里的一塊區(qū)域,該區(qū)域存的才是本身的內(nèi)容值面睛。
var value = 1,
obj = {name: '張三', age: 18},
value2 = value,
obj2 = obj;
可以看到對于基本類型 number
絮蒿,其存值是棧里單獨(dú)存一塊,即使賦值也是copy一份再單獨(dú)賦值叁鉴,所以兩者改變互不影響土涝。但對于引用類型的 object
,雖然也是在棧里單獨(dú)存一塊并copy值賦值幌墓,但是其存的是地址值但壮,由于copy的地址值相同冀泻,所以最后兩個obj指向的內(nèi)容區(qū)域也相同,所以修改會同步蜡饵。當(dāng)修改 obj.name
改的是堆內(nèi)存里面的 name
弹渔,改完后, obj2.name
拿著同樣的地址值去找堆內(nèi)存對應(yīng)區(qū)域溯祸,此時 obj2
看到的是已經(jīng)改后的 name
肢专,于是返回改后的 name
。
var obj = {name: '張三', age: 18},
obj2 = {name: '張三', age: 18};
obj2.name = '李四';
obj.name; // “張三"
上面這個例子焦辅,改了 obj2.name
博杖,obj.name
卻不受影響,這又是為啥呢筷登?我們來看看內(nèi)存圖剃根。
從內(nèi)存圖可以看到,當(dāng)創(chuàng)建一個新的引用類型變量時前方,會生成一個新的地址值狈醉,由于obj
和 obj2
地址不同指向的區(qū)域不同,所以修改之后互不影響镣丑。
array舔糖、function
等也都屬于 object
,也都是引用類型莺匠,所以修改數(shù)組的時候金吗,原理和上面的一樣。所以為了使兩個引用類型互不影響趣竣,一般我們不直接將舊的 obj
賦值給新的 obj
摇庙,而是生成新的引用類型變量,然后將原先對象的屬性值一個個復(fù)制填充到新的引用類型變量里遥缕,這就是所謂的深拷貝卫袒。
二、不可變對象
竟然對象可能會被其他擁有同樣引用地址的對象同步改變单匣,那么除了上述說的深拷貝一個新對象之外夕凝,有沒有辦法讓原對象也不可改寫?有的户秤,JavaScript 確實(shí)提供了一些原生的方法來將可變對象變成不可變對象码秉,通過控制對象屬性的增刪改來實(shí)現(xiàn),由弱到強(qiáng)依次分為三種:Object.preventExtensions
(不可擴(kuò)展)鸡号、Object.seal
(密封)转砖、Object.freeze
(凍結(jié))。
1. 不可擴(kuò)展
Object.preventExtensions()
可以使一個對象不可再添加新的屬性鲸伴,參數(shù)為目標(biāo)對象府蔗,返回修改后的對象晋控。
var obj = Object.preventExtensions({});
// 直接定義新的屬性會報錯
Object.defineProperty(obj, 'content', {
value: 'hello'
}); // TypeError: Cannot define property:p, object is not extensible.
// 非嚴(yán)格模式下通過點(diǎn)符號添加不會報錯,但會靜默失敗姓赤,原對象仍然沒有 content 屬性
obj.content = 'hello';
obj.content; // undefined
對應(yīng)的赡译,Object.isExtensible()
可以判斷一個對象是否可擴(kuò)展,即是否可以添加新的屬性模捂。參數(shù)是目標(biāo)對象捶朵,返回布爾值蜘矢, true
代表可擴(kuò)展狂男,false
不可擴(kuò)展。
var obj = new Object();
Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false
2. 密封
Object.seal()
可以使一個對象無法添加新屬性的同時品腹,也無法刪除舊屬性岖食。參數(shù)是目標(biāo)對象,返回修改后的對象舞吭。
其本質(zhì)是通過修改屬性的 configurable
為 false
來實(shí)現(xiàn)的泡垃。在屬性描述對象里講到,configurable
為 false
時羡鸥,其他配置不可改變蔑穴,writable
只能 true
變 false
,且屬性無法被刪除惧浴。而由于只要 writable
或 configurable
其中之一為 true
存和,則 value
可改,所以密封之后的對象還是可以改屬性值的衷旅。
var obj = {content: 'hello'};
Object.getOwnPropertyDescriptor(obj, 'content');
// Object {
// value: "hello",
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.seal(obj);
Object.getOwnPropertyDescriptor(obj, 'content'); // seal 后 configurable 變?yōu)?false
// Object {
// value: "hello",
// writable: true,
// enumerable: true,
// configurable: false
// }
對應(yīng)的捐腿,Object.isSealed()
可以檢測一個對象是否密封,即是否可以增刪屬性柿顶。參數(shù)是目標(biāo)對象茄袖,返回布爾值,true
代表被密封不可增刪屬性嘁锯,false
代表沒被密封可增刪屬性宪祥。
var obj = new Object();
Object.isExtensible(obj); // true
Object.isSealed(obj); // false
Object.seal(obj);
Object.isExtensible(obj); // false,注意 seal 后對象的 isExtensible() 也隨之改變
Object.isSealed(obj); // true
3. 凍結(jié)
Object.freeze()
可以使對象一個對象不能再添加新屬性家乘,也不可以刪除舊屬性蝗羊,且不能修改屬性的值。參數(shù)是目標(biāo)對象烤低,返回修改后的對象肘交。
var obj = Object.freeze({name: 'example'});
// 直接定義新的屬性會報錯
Object.defineProperty(obj, 'content', {
value: 'hello'
}); // TypeError: Cannot define property:p, object is not extensible.
// 非嚴(yán)格模式下通過點(diǎn)符號添加不會報錯,但會靜默失敗扑馁,原對象仍然沒有 content 屬性
obj.content = 'hello';
obj.content; // undefined
delete obj.name; // 刪除失敗涯呻,返回 false
obj.name = 'hello';
obj.name; // 仍然是 "example"
對應(yīng)的凉驻,Object.isFrozen()
可以檢測一個對象是否凍結(jié),即是否可以增刪改复罐。參數(shù)是目標(biāo)對象涝登,返回布爾值,true
表示已經(jīng)凍結(jié)不可再增刪改效诅,false
反之胀滚。
var obj = new Object();
Object.isExtensible(obj); // true
Object.isSealed(obj); // false
Object.isFrozen(obj); // false
Object.freeze(obj);
Object.isExtensible(obj); // false,注意 freeze 后對象的 isExtensible() 也隨之改變
Object.isSealed(obj); // true乱投,注意 freeze 后對象的 isSealed() 也隨之改變
Object.isFrozen(obj); // true
4. 注意要點(diǎn)
無論是不可擴(kuò)展咽笼,密封,還是凍結(jié)戚炫,都是淺層控制的剑刑,即只控制對象本身屬性的增刪改。如果對象屬性是一個引用類型双肤,比如數(shù)組 subArr
或?qū)ο?subObj
等施掏,雖然subArr、subObj
的不可被刪改茅糜,但subArr七芭、subObj
的屬性仍然可增刪改。
var obj = Object.freeze({
content: {name: 'example'}
});
obj.content = new Object();
obj.content; // {name: "example"}蔑赘,content 本身不可改
obj.content.name = 'test';
obj.content; // {name: "test"}狸驳,但 content 的屬性仍可改,因?yàn)閮鼋Y(jié)的是 obj 而不是 obj.content
由于每個對象都有一個屬性 __proto__
米死,該屬性的值是該對象的原型對象锌历,也是引用類型,由于凍結(jié)是淺層的所以原型對象并不會被連著凍結(jié)峦筒,仍然可以通過給對象的原型對象加屬性達(dá)到給當(dāng)前對象新增屬性的效果究西。所以如果想進(jìn)一步凍結(jié)還需要把原型對象也凍結(jié)上。
var obj = Object.freeze({});
obj.content = 'hello';
obj.content; // undefined物喷,增加失敗
var proto = Object.getPrototypeOf(obj);
proto.content = 'hello';
obj.content; // "hello"卤材,增加成功
Object.freeze(proto);
proto.name = 'example';
obj.name; // undefined,凍結(jié)原型之后增加失敗