本章內(nèi)容
- 理解對象屬性
- 理解并創(chuàng)建對象
- 理解繼承
一焕蹄、理解對象
對象的定義為,無序?qū)傩缘募戏埽鋵傩钥梢园局的逶唷ο蠡蚝瘮?shù)鸦泳。
可以先創(chuàng)建一個object實(shí)例,再為它添加屬性永品,也可以利用對象字面量的方法創(chuàng)建對象做鹰。
var person = new Object();
person.name = "Nicholas"; //給對象添加屬性
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
alert(this.name);
};
//使用對象字面量添加屬性
var person = {
name: "Nicholas",
age:29,
job:"Software Engineer",
sayName: function(){
alert(this.name);
}
}
1.1 屬性類型
對象屬性的特性有兩種:數(shù)據(jù)屬性和訪問器屬性。他們是在Javascript內(nèi)部使用的鼎姐,因此必須使用Object.defineProperty方法來修改屬性默認(rèn)的特性钾麸。
數(shù)據(jù)屬性,默認(rèn)都為true
- Configurable 一旦定義為false炕桨,就不能在定義為true了
- Enumerable
- Writable
- Value
Object.defineProperty()可以創(chuàng)建一個新的屬性饭尝,但多數(shù)情況下沒有必要利用Object.defineProperty()提供的這些高級功能。
var person = {};
//使用Object.defineProperty創(chuàng)建一個新的屬性献宫,如果不指定钥平,其數(shù)據(jù)屬性的默認(rèn)值都為false
Object.defineProperty(person,"name",{
writable: false,
value: "Nicholas"
})
alert(person.name);
person.name = "Gerg";
alert(person.name);
訪問器屬性,只包含一對getter()和setter()函數(shù)姊途。
- Get 讀取屬性時調(diào)用涉瘾,默認(rèn)值為undefined
- Set 寫入屬性時調(diào)用,默認(rèn)值為undefined
var book = {
_year:2014,
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);
可以使用Object.defineProperties定義多個屬性捷兰,使用Object.getOwnPropertyDescriptor取得屬性的描述立叛。
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
var descriptor = Object.getOwnPropertyDescriptor(book,"year");
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //function
二、創(chuàng)建對象
上節(jié)講過創(chuàng)建對象的兩種方式贡茅,一種是使用構(gòu)造函數(shù)秘蛇,一種是使用對象字面量。但這樣做會產(chǎn)生大量的重復(fù)代碼友扰。
2.1 工廠模式
這種模式抽象了創(chuàng)建具體對象的過程彤叉,用函數(shù)來封裝以實(shí)現(xiàn)特定接口的細(xì)節(jié)庶柿。
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 person = createPerson("Nicholas", 29, "Software Engineer");
2.2 構(gòu)造函數(shù)模式
ECMAScript可以使用構(gòu)造函數(shù)來創(chuàng)建對象村怪,像Object和Array這樣的原生構(gòu)造函數(shù),在運(yùn)行時會自動出現(xiàn)在執(zhí)行環(huán)境中浮庐。此外也可以創(chuàng)建自定義的構(gòu)造函數(shù)甚负,從而定義自定義對象類型的屬性和方法。將上面的例子重寫如下
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person = new Person("Nicholas", 29, "Software Engineer");
我們可以注意到工廠模式與構(gòu)造函數(shù)模式的區(qū)別如下:
- 沒有顯示的創(chuàng)建對象审残;
- 直接將屬性和方法賦給了this對象;
- 沒有return語句;
按照慣例梭域,構(gòu)造函數(shù)始終應(yīng)該以一個大寫字母開頭,創(chuàng)建的實(shí)例person會有一個constructor (構(gòu)造函數(shù))屬性搅轿,該屬性指向Person病涨。
可以使用instanceof操作符檢測對象類型。
alert(person instanceof Object); //true
alert(person instanceof Person);//true
這里要注意兩個問題璧坟,一是構(gòu)造函數(shù)也是函數(shù)既穆,可以當(dāng)做普通函數(shù)調(diào)用赎懦。若直接調(diào)用,屬性和方法會被加到window對象上幻工。二是以構(gòu)造函數(shù)模式定義的方法會在每個實(shí)例上都重新創(chuàng)建一遍。然而創(chuàng)建兩個完成同樣任務(wù)的Function實(shí)例是不必要的。
2.3 原型模式
我們創(chuàng)建的每個函數(shù)都有一個prototype屬性下面,這個屬性是一個指針畏妖,指向一個對象。使用原型對象的好處是可以讓所有對象實(shí)例共享它所包含屬性和方法踢代。換句話說盲憎,不必在構(gòu)造函數(shù)中定義對象實(shí)例的信息,而是可以將這些信息直接添加到原型對象中奸鬓。
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
1.理解原型對象
無論什么時候只要創(chuàng)建一個,函數(shù)焙畔,就會為該函數(shù)創(chuàng)建一個prototype屬性,這個屬性指向函數(shù)的原型對象串远。原型對象默認(rèn)只會取得constructor屬性宏多,其他方法是由Object繼承來的。當(dāng)用構(gòu)造函數(shù)創(chuàng)建一個新實(shí)例時澡罚,這個實(shí)例的內(nèi)部將包含一個指針[[Prototype]]伸但,指向構(gòu)造函數(shù)的原型對象。一般無法訪問[[Prototype]],但有兩個方法可以確定原型操作方法留搔。
//判斷Person的原型對象是不是person1的原型對象
alert(Person.prototype.isPrototypeOf(person1)); //true
//返回對象的原型
alert(Object.getPrototypeOf(person1) === Person.prototype) //true
雖然可以通過對象實(shí)例訪問保存在原型中的值更胖,但不能重寫屬性。如果在實(shí)例中添加一個新的屬性隔显,而該屬性與原型中的一個屬性同名却妨,那么原型中的屬性會被屏蔽±撸可以通過hasOwnProperty方法彪标,判斷什么時候訪問實(shí)例屬性,什么時候訪問原型屬性掷豺。
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();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1);//true 關(guān)于in操作符捞烟,只要對象能夠訪問該屬性
//不論屬性是在實(shí)例中還是在原型中
alert(hasPrototypetypeProperty(person1,"name")); //true
person1.name = "Greg";
alert(person1.name); //"Greg"----來自實(shí)例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1);//true
alert(hasPrototypetypeProperty(person1,"name")); // false
//若要取得對象上所有可枚舉的實(shí)例屬性,Object.keys()方法,接收一個
//對象為參數(shù)当船,返回一個包含所有可枚舉屬性的字符串?dāng)?shù)組题画。
var keys = Object.keys(Person.prototype);
alert(keys);//"name, age, job, sayName"
var p1 = Object.keys(person1);
alert(p1 );//"name"
//若要取得所有實(shí)例屬性,無論它是否可枚Object.getOwnPropertyNames()方法
var keys = Object.getOwnPropertyNames((Person.prototype);
alert(keys); //"constructor, name, age, job, sayName"
3.更簡單的原型語法
我們可以將Person.prototype設(shè)置為一個以對象字面量的方法創(chuàng)建的新對象。相當(dāng)于完全重寫了prototype對象德频。此時默認(rèn)的constructor屬性會指向Object苍息。如有需要可將其設(shè)回適當(dāng)?shù)闹怠?/p>
function Person(){}
Person.prototype = {
constructor: Person, //重新設(shè)置constructor屬性,此時可枚舉
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
alert(this.name);
}
}
4.原型的動態(tài)性
由于在原型中查找值的過程是一次搜索,因此我們對原型的修改能夠反映在所有實(shí)例上---即使先創(chuàng)建實(shí)例后修改原型也是如此竞思。
可以隨時隨地為原型添加屬性方法桌粉,但不能使用字面量創(chuàng)建的對象重寫原型。重寫原型將切斷現(xiàn)有原型與原來已經(jīng)存在的對象實(shí)例之間的聯(lián)系衙四,他們引用的仍然是之前的原型铃肯。
5.原生對象的原型
所有的原生引用類型(Object, Array, String)都在其構(gòu)造函數(shù)的原型上添加了方法。
alert(typeof Array.prototype.sort);//function
alert(typeof String.prototype.substring);//function
通過原生對象的原型传蹈,不僅可以取得其所有方法的引用押逼,也可以定義新方法,但不推薦在原生對象的原型上添加新方法惦界。
6.原型對象的問題
原型對象省略了為構(gòu)造函數(shù)初始化傳遞參數(shù)這一環(huán)節(jié)挑格,并且所有的實(shí)例能夠共享屬性和方法。但它也存在問題沾歪,這一問題正是由于其共享的本性導(dǎo)致的漂彤。對于引用類型的屬性值尤為突出。
function Person(){}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends:["Shelby", "Court"],
sayName: function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby, Court, Van"
alert(person2.friends); //"Shelby, Court, Van"
2.4 組合使用構(gòu)造函數(shù)模式和原型模式
對于上面6中提到的問題很容易解決灾搏,那就是組合使用構(gòu)造函數(shù)模式和原型模式挫望,這也是創(chuàng)建自定義類型最常用的方式。每個實(shí)例都保存自己實(shí)例屬性的副本狂窑,同時又共享方法的引用媳板。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "doctor");
person1.push("van");
alert(person1.friends); //"Shelby, Court, Van"
alert(person2.friends); //"Shelby, Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName ); //true
2.5 動態(tài)原型模式
2.4中的構(gòu)造函數(shù)與原型是分開寫的∪可以在構(gòu)造函數(shù)中初始化原型蛉幸,初始化原型的代碼只會在初次構(gòu)造函數(shù)的時候調(diào)用。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
2.6 寄生構(gòu)造函數(shù)模式
基本思想是創(chuàng)建一個函數(shù)丛晦,該函數(shù)的作用僅僅是創(chuàng)建封裝對象的代碼奕纫,然后再返回新創(chuàng)建的對象。這個模式跟工廠模式是一模一樣的烫沙,可以在特殊的情況下用來為對象創(chuàng)造構(gòu)造函數(shù)匹层。缺點(diǎn)是不能依賴instanceof操作符來確定對象類型。
function SpecialArray(){
//創(chuàng)建數(shù)組
var values = new Array(); //構(gòu)造函數(shù)斧吐,不用this指針又固,重寫構(gòu)造函數(shù)返回時的值仲器。
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
//返回數(shù)組
return values;
}
var color = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
2.7 穩(wěn)妥構(gòu)造函數(shù)模式
穩(wěn)妥對象最適合在一些安全的環(huán)境中(這些環(huán)境禁止使用this和new)煤率,或者防止數(shù)據(jù)被其他應(yīng)用程序改動時使用。穩(wěn)妥構(gòu)造函數(shù)遵循與及生構(gòu)造函數(shù)類似的模式乏冀。但有兩點(diǎn)不同:一是新創(chuàng)建對象的實(shí)例方法不使用this蝶糯。二是不使用new操作符調(diào)用構(gòu)造函數(shù)。
在這種模式下創(chuàng)建的對象辆沦,除了使用成員方法外昼捍,不允許使用別的方法訪問其數(shù)據(jù)成員
三识虚、繼承
繼承是OO語言中的一個最為人津津樂道的概念。ECMAScript只支持實(shí)現(xiàn)繼承妒茬,而其實(shí)現(xiàn)繼承主要是依賴原型鏈來實(shí)現(xiàn)的
3.1原型鏈
基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法担锤。每個實(shí)例中都包含著一個指向原型對象的指針,原型對象中又包含著一個指向構(gòu)造函數(shù)的指針乍钻,如果將原型對象的指針指向另一個類型的原型對象肛循,那么該原型對象將可以繼承其所有的原型屬性和方法。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
//繼承了Super
Super.prototype = new SuperType();//將原型對象等于另一個類型的實(shí)例
SubType.prototype.gerSubValue = function(){
return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue());//true
SubType的原型會保留有SuperType類型的實(shí)例屬性银择,其prototype指針會指向SuperType的原型多糠,也就是SuperType的原型屬性可以通過原型鏈找到。
1. 別忘記默認(rèn)的原型
所有的引用類型都默認(rèn)繼承了Object浩考,這個繼承也是通過原型鏈實(shí)現(xiàn)的夹孔。函數(shù)的默認(rèn)原型也是Object實(shí)例。例如上述SubType繼承了SuperType,而SuperType繼承了Object析孽。當(dāng)調(diào)用instance.toString()是會調(diào)用保存在Object.prototype中的那個方法搭伤。
2.確定原型和實(shí)例的關(guān)系
可以通過兩種操作符來確定實(shí)例和原型的關(guān)系,一是instanceof操作符袜瞬,二是isPrototypeOf()方法闷畸。
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
alert(SuperType.prototype.isPrototypeOf(instance));//true
alert(SubType.prototype.isPrototypeOf(instance));//true
3.謹(jǐn)慎的定義方法
子類型有時候需要覆蓋超類型中的某個方法,或者添加超類型中不存在的方法吞滞。但不管怎樣給原型添加方法的代碼一定要放在替換原型的語句之后佑菩。
另外,在通過原型鏈實(shí)現(xiàn)繼承時裁赠,不能使用對象字面量創(chuàng)建原型方法殿漠,因?yàn)檫@樣會重寫原型鏈。
4.原型鏈的問題
原型鏈最主要的問題來自包含引用類型的原型佩捞。因?yàn)闃?gòu)造函數(shù)中定義的引用屬性會被繼承為子類型的原型屬性绞幌,這時候就出現(xiàn)數(shù)組被所有實(shí)例共享的問題。
第二個問題是:在創(chuàng)建子類型的實(shí)例時一忱,不能向超類型傳遞參數(shù)莲蜘。或者說沒辦法在不影響所有對象實(shí)例的情況下帘营,給超類型傳遞參數(shù)票渠。
3.2 借用構(gòu)造函數(shù)
為解決上述原型鏈中引用類型值帶來的問題,開發(fā)人員開始使用一種叫做借用構(gòu)造函數(shù)的技術(shù)芬迄。這種技術(shù)的思想相當(dāng)簡單问顷,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型的構(gòu)造函數(shù)。別忘了,函數(shù)只不過是在特定環(huán)境下執(zhí)行代碼的對象杜窄。
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){
//繼承了SuperType
SuperType.call(this); //通過call方法借調(diào)了超類型的構(gòu)造函數(shù)肠骆。
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"
相對于原型鏈而言,借用構(gòu)造函數(shù)有一個很大的優(yōu)勢塞耕,即可以在子類型中向超類型的構(gòu)造函數(shù)傳遞參數(shù)蚀腿。
但僅僅是借用構(gòu)造函數(shù),會有一個構(gòu)造函數(shù)模式無法避免的問題------方法都在構(gòu)造函數(shù)中了扫外,方法復(fù)用就無從談起了唯咬。在超類型中定義的方法,對子類型而言是不可見的畏浆〉ㄒ龋考慮這個問題,借用構(gòu)造函數(shù)技術(shù)很少單獨(dú)使用刻获。
3.3 組合繼承
組合繼承指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一起蜀涨,從而發(fā)揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實(shí)現(xiàn)對原型屬性和方法的繼承蝎毡,使用借用構(gòu)造函數(shù)實(shí)現(xiàn)對實(shí)例屬性的繼承厚柳。這樣,既通過在原型上定義方法實(shí)現(xiàn)了函數(shù)的復(fù)用沐兵,又能保證每個函數(shù)有自己的實(shí)例屬性别垮。這是JavaScript中最常用的繼承模式。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age ){
//繼承屬性
Super.call(this.name);
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
alert(instance1.colors); //”red, blue,green,black”
instance1.sayName(); //”Nicholas”
instance1.sayAge(); //29
var instance2 = new SubType("Greg",27);
alert(instance2.colors); //”red,blue,green”
instance2.sayName(); //”Greg”
instance2.sayAge(); //27
3.4 原型式繼承
原型式繼承要求必須有一個對象作為另一個對象的基礎(chǔ)扎谎。如果有一個對象的話碳想,可以把它傳遞給object函數(shù),然后再根據(jù)具體需求對得到的對象加以修改毁靶。ECMAScript5通過新增Object.create()方法規(guī)范了原型式繼承胧奔。
var person = {
name:"Nicholas",
friends:["Shelby", "Court", "Van"]
}
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
anotherPerson.name = "Linda";
anotherPerson.friends.push("Barbie");
alert(person.friends); //” Shelby, Court, Van, Rob, Barbie”
3.5寄生式繼承
寄生式繼承是與原型式繼承緊密相關(guān)的一種思路,與及生構(gòu)造函數(shù)模式和工廠模式類似预吆,即創(chuàng)建一個僅用于封裝繼承過程的函數(shù)龙填,該函數(shù)以某種方式來增強(qiáng)對象,最后再像真的是它做了所有工作一樣返回對象拐叉。
function createAnother(original){
var clone = object(original); //通過調(diào)用函數(shù)創(chuàng)建一個新對象
clone.sayHi = function(){ //以某種方式來增強(qiáng)這個對象
alert("hi");
}
return clone; //返回這個對象
}
var person = {
name: "Nicholas",
friends:["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //”hi”
3.6寄生組合式繼承
前面說過岩遗,組合繼承是Javascript中最常用的繼承模式,但它每次都會調(diào)用兩次超類型的構(gòu)造函數(shù)凤瘦,一次是在創(chuàng)建子類型原型的時候宿礁,另一次是在子類型構(gòu)造函數(shù)內(nèi)部。子類型會包含超類型的全部實(shí)例屬性廷粒,但又不得不在子類型的構(gòu)造函數(shù)中重寫這些屬性窘拯。解決辦法就是寄生組合式繼承。
function inheriPrototype(subType, superType){
var prototype = Object(superType.prototype); //創(chuàng)建對象
prototype.constructor = subType; //增強(qiáng)對象
subType.prototype = prototype; //指定對象
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
//替換了SubType.prototype = new SuperType();少調(diào)用一次超類型的構(gòu)造函數(shù)
SubType.prototype.sayAge = function(){
alert(this.age);
};
上述例子的高效率體現(xiàn)在它只調(diào)用了一次SuperType構(gòu)造函數(shù)坝茎,并且避免了在Subtype.prototype上面創(chuàng)建不必要的多余的屬性涤姊,與此同時,原型鏈還能保持不變嗤放。因此開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式思喊。