聲明:此文集下的文章都是看了Javascript
高級程序設(shè)計所做的筆記进倍。此章對應(yīng)書中的第六章。
一购对、理解對象
在JS
早期開發(fā)中經(jīng)常使用以下兩種方式創(chuàng)建對象:
var person = new Object();
person.name = "Tom";
person.age = 23;
person.job = "Software Engineer";
person.sayName = function(){
console.log(this.name);
}
var person = {
name : "Tom",
age : 29,
job : "Software Engineer",
sayName : function(){
console.log(this.name);
}
};
說明:這里創(chuàng)建了一個對象猾昆,此對象有三個屬性和一個方法。在第二種方式中骡苞,屬性名可以是name垂蜗、'name'
或者"name"
,但是如果屬性是保留字或者以數(shù)字開頭則必須加引號解幽。
1.1 屬性類型
在ECMAScript
中有兩種屬性:數(shù)據(jù)屬性和訪問器屬性贴见。
1、數(shù)據(jù)屬性
-
[[Configurable]]
:表示能否通過delete
刪除屬性從而重新定義屬性躲株,能否修改屬性的特性片部,或者能否把屬性修改為訪問器屬性,對于直接在對象上定義的屬性(如上)霜定,默認為true
档悠,表示可以廊鸥。 -
[[Enumerable]]
:表示能否通過for-in
循環(huán)返回每個屬性,對于直接在對象上定義的屬性辖所,默認為true
惰说。 -
[[Writable]]
:表示能否修改屬性的值,對于直接在對象上定義的屬性奴烙,默認為true
助被。 -
[[Value]]
:包含這個屬性的數(shù)據(jù)值。讀取屬性值的時候切诀,從這個位置讀揩环;寫入屬性值的時候,把新值保存在這個位置幅虑。默認為undefined
丰滑。
如果要修改屬性默認的特性,必須使用ECMAScript5
中的Object.defineProperty()
方法倒庵。接收三個參數(shù):屬性所在對象褒墨、屬性的名字和一個描述符對象,其中描述符對象必須是:configurable擎宝、enumberable郁妈、writable、value
绍申,比如:
var person = {};
Object.defineProperty(person, 'name', {
writable : false,
value : "Tom"
});
console.log(person.name);
person.name = "Jerry";
console.log(person.name);
說明:這里我們將writable
屬性設(shè)置為了false
噩咪,標明此屬性的值不能被修改了,可以看到使用此方法就能精確控制每個屬性了极阅。這里沒有設(shè)置的屬性則取false
(這和之前定義時不同)胃碾。當然在后面的代碼中我們還可以將此屬性改為true
,但是如果屬性configurable
被設(shè)置為了false
則表示不能使用delete
將某個屬性刪除掉了筋搏,同時也表示此屬性變?yōu)椴豢膳渲昧似桶伲慈绻渲昧?code>writable,則在后面的代碼中不能改變其值了奔脐。同時configurable
本身也不能改變俄周。如:
var person = {};
Object.defineProperty(person, 'name', {
configurable : false,
value : "Tom"
});
//拋出錯誤
Object.defineProperty(person, 'name', {
configurable : true,
value : "Tome"
});
2、訪問器屬性(存取器屬性)
訪問器屬性不包括數(shù)據(jù)值髓迎,它們是一對兒getter
和setter
函數(shù)(不過峦朗,這兩個函數(shù)都不是必須的),有以下四個屬性:
-
[[Configurable]]
:表示能否通過delete
刪除屬性從而重新定義屬性竖般,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性茶鹃,對于直接在對象上定義的屬性涣雕,默認為true
艰亮,表示可以。 -
[[Enumerable]]
:表示能否通過for-in
循環(huán)返回每個屬性挣郭,對于直接在對象上定義的屬性迄埃,默認為true
。 -
[[Get]]
:在讀取屬性時調(diào)用的函數(shù)兑障。默認值為undefined
侄非。 -
[[Set]]
:在寫入屬性時調(diào)用的函數(shù)。默認值為undefined
流译。
訪問器屬性不能直接定義逞怨,必須使用Object.defineProperty()
定義。比如:
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
book._year = 3000;
alert(book._year);
說明:以上代碼中我們給對象book
定義了兩個屬性_year福澡、edition
叠赦,其中_year
前面的下劃線是一種標記,表示只能通過對象方法訪問的屬性革砸,當然這里我們還是可以直接訪問的除秀。這里還定義了一個訪問器屬性year
,包含getter算利、setter
方法册踩,修改year
的值會導致_year
值改變,而edition
變?yōu)?code>2效拭,起始訪問器屬性就像一種標志暂吉,可以在方法中做一些操作來達到某個目的,比如這里在setter
方法中我們就讓edition
的值隨著year
的值改變而改變允耿。
定義getter
與setter
方法還可以這樣:
var person = {
name: "Tom",
get age() {
return 12;
}
}
console.log(person.age);
而此時age
也是對象的一個屬性借笙。
在這兩個方法之前一般使用的是兩個非標準的方法:__defineGetter__()、__defineSetter__()
较锡,使用這兩個方法實現(xiàn)之前的代碼:
var book = {
_year: 2004,
edition: 1
};
//legacy accessor support
book.__defineGetter__("year", function(){
return this._year;
});
book.__defineSetter__("year", function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
});
book.year = 2005;
alert(book.edition); //2
1.2 定義多個屬性
使用Object.defineProperty()
方法定義屬性太麻煩业稼,我們可以使用Object.defineProperties()
一次性定義多個屬性,如下:
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
book.year = 2005;
book._year = 100;//無效蚂蕴,因為這樣定義的屬性是不能修改的
console.log(book._year);
console.log(book.edition);//2
1.3 讀取屬性的特性
使用ECMAScript 5
的Object.getOwnPropertyDescriptor()
方法低散,可以取得給定屬性的描述符。接收兩個參數(shù):屬性所在對象和要讀取其描述符的屬性名稱骡楼。返回值是一個對象熔号,如果是訪問器屬性,則這個對象的屬性有configurable鸟整、enumberable引镊、get、set
;如果是數(shù)據(jù)屬性弟头,則這個對象的屬性有configurable吩抓、enumberable、writable赴恨、value
疹娶。如:
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"
二、創(chuàng)建對象
雖然使用Object
構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個對象伦连,但這些方式使用同一個接口創(chuàng)建很多對象雨饺,會產(chǎn)生大量的重復(fù)代碼。下面看幾種其他的方式惑淳。
2.1 工廠模式
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
說明:這里使用函數(shù)來封裝以特定接口創(chuàng)建對象的細節(jié)额港,這中方式雖然解決了相似對象的問題,但是卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)汛聚。
2.2 構(gòu)造函數(shù)模式
可以使用構(gòu)造函數(shù)將前面例子重寫:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
說明:這里使用Person()
函數(shù)取代了createPerson()
函數(shù)锹安,有幾點不同的地方:
- 沒有顯式地創(chuàng)建對象
- 直接將屬性和方法賦給
this
對象 - 沒有
return
語句
這里要創(chuàng)建Person
新實例,必須使用new
操作符倚舀,上述代碼創(chuàng)建了兩個Person
實例叹哭,都有一個相同的construtor
(構(gòu)造函數(shù))屬性:
console.log(person1.construtor == Person);//true
console.log(person2.construtor == Person);//true
對象的construtor
屬性最初是來標識對象類型的,但是痕貌,在檢測對象類型時风罩,還是使用instanceof
操作符更可靠一些。
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
這里所有對象均繼承自Object
舵稠。
2.2.1 將構(gòu)造函數(shù)當作函數(shù)
任何函數(shù)超升,只要通過new
操作符來調(diào)用,那它就可以作為構(gòu)造函數(shù)哺徊,而任何函數(shù)室琢,如果不通過new
操作符來調(diào)用,那它跟普通的函數(shù)也一樣落追。如前面定義的Person
構(gòu)造函數(shù):
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
Person("Greg", 27, "Doctor"); //adds to window
window.sayName(); //"Greg"
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
說明:當在全局作用域中調(diào)用一個函數(shù)時盈滴,this對象總是指向Global
對象(在瀏覽器中就是window
對象)。也可以使用call()
或apply()
調(diào)用轿钠。
2.2.2 構(gòu)造函數(shù)的問題
構(gòu)造函數(shù)模式的主要問題就是每個方法都要在每個實例上重新創(chuàng)建一遍巢钓。因為函數(shù)就是對象,因此沒定義一個函數(shù)疗垛,也就是實例化了一個對象症汹,于是構(gòu)造函數(shù)可以定義為這樣:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");
}
說明:不同實例上的同名函數(shù)sayName
不是相等的。這里我們可以將函數(shù)定義在構(gòu)造函數(shù)之外:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Tom", 29, "Software Engineer");
var person2 = new Person("Jerry", 28, "Doctor");
說明:此時兩個實例中的同名函數(shù)sayName
就是同一個了贷腕,但是新問題又來了:在全局作用域中定義的函數(shù)實際上只能被某個對象調(diào)用背镇,這讓全局作用域有點名不副實咬展,而且,如果要定義很多方法瞒斩,那就得定義多個全局函數(shù)挚赊,那自定義的引用類型就沒有封裝性了。這需要使用原型模式解決济瓢。
2.2.3 原型模式
我們創(chuàng)建了每個函數(shù)都有一個prototype
(原型)屬性,這個屬性是一個指針妹卿,指向一個對象旺矾,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法夺克。如下:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
說明:我們將sayName()
方法和所有屬性都直接添加到了Person
的prototype
屬性中箕宙,構(gòu)造函數(shù)變成了空函數(shù),當然還是可以通過構(gòu)造函數(shù)來創(chuàng)建新對象铺纽,而且新對象還會具有相同的屬性和方法柬帕。
1、理解原型對象
說明:無論什么時候狡门,只要創(chuàng)建一個新函數(shù)陷寝,就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個
prototype
屬性,這個屬性指向函數(shù)的原型對象其馏,如圖中的構(gòu)造函數(shù)Person
凤跑、實例person1、person2
其中的prototype
屬性都指向原型對象叛复。創(chuàng)建了自定義的構(gòu)造函數(shù)之后仔引,其原型對象默認只會取得construtor
屬性,至于其他方法褐奥,都是從Object
繼承或我們自己添加的咖耘。一般使用屬性__proto__
訪問原型對象。雖然在所有實現(xiàn)中都無法訪問到
[[Prototype]]
撬码,但是可以通過isPrototypeOf()
方法來確定對象之間是否存在這種關(guān)系儿倒,本質(zhì)上講,如果[[Prototype]]
指向調(diào)用isPrototypeOf()
方法的對象(Person.prototype
)耍群,那么這個方法就返回true
:
alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true
在ECMAScript 5
中增加了一個新方法义桂,叫Object.getPrototypeOf()
,在所有支持的實現(xiàn)中蹈垢,這個方法返回[[Prototype]]
的值慷吊,如:
alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//"Tom"
說明:每當代碼讀取某個對象的某個屬性時,都會執(zhí)行搜索曹抬,目標是具有給定名字的屬性溉瓶。搜索首先從對象實例本身開始。如果在實例中找了具有給定名字的屬性,則返回該屬性的值堰酿;如果沒有疾宏,則繼續(xù)搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性触创,如果找到則返回相關(guān)的值坎藐。
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值哼绑。如果我們在實例中添加了一個屬性岩馍,而該屬性與實例原型中的一個屬性同名,那我們就在實例中創(chuàng)建該屬性抖韩,該屬性將會屏蔽原型中的那個屬性蛀恩。看如下例子:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"來自實例
alert(person2.name); //"Nicholas"來自原型
說明:在實例中增加一個和原型對象中同名屬性茂浮,只會屏蔽原型中的屬性双谆,并不會覆蓋。當在搜素時席揽,如果在實例中搜索到了相關(guān)屬性顽馋,則不會繼續(xù)向原型中搜索了。當然使用delete
方法可以完全刪除實例中的某個屬性幌羞,從而放我們能夠重新訪問原型中的屬性趣避。
delete person1.name;
alert(person1.name); //"Nicholas"來自原型
使用hasOwnProperty()
方法可以檢測一個屬性是存在于實例中,還是存在于原型中新翎,這個方法是從Object
繼承過來的程帕。只在給定屬性存在于對象實例中,才會返回true
地啰。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name"));//false
person1.name = "Greg";
alert(person1.name); //"Greg"來自實例
alert(person1.hasOwnProperty("name"));//true
alert(person2.name); //"Nicholas"來自原型
alert(person2.hasOwnProperty("name"));//false
delete person1.name;
alert(person1.name); //"Nicholas"來自原型
alert(person1.hasOwnProperty("name"));//false