構造函數(shù)媒抠,原型對象,實例化對象的關系:
- 每個函數(shù)(包括構造函數(shù))都有一個原型對象(prototype)
- 原型對象都包含一個指向構造函數(shù)的指針(constructor)
- 原型對象上的屬性和方法咏花,都可以被構造函數(shù)的實例化對象所繼承(所有實例化對象共享一個原型對象)
- 實例化對象又都包含一個指向構造函數(shù)的原型對象的內(nèi)部指針(__proto__)趴生,(p1.__proto__ === Person.prototype)(p1.__proto__.constructor === Person.prototype.constructor)(p1.__proto__.constructor === Person)
- 實例化對象的 constructor 屬性指向構造函數(shù)(p1.constructor === Person)
prototype
是函數(shù)才有的屬性,切記昏翰,切記
__proto__
是每個對象(包括函數(shù))都有的屬性
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("a1", 12, "程序");
var person2 = createPerson("b1", 18, "銷售");
console.log(person1.name) // a1
console.log(person2.age) // 18
2.構造函數(shù)模式
構造函數(shù)苍匆,是用來創(chuàng)建對象的函數(shù),本質(zhì)上也是函數(shù)
任何函數(shù)棚菊,只要通過 new 操作符來調(diào)用浸踩,那它就可以作為構造函數(shù) ;
任何函數(shù)统求,如果不通過 new 操作符來調(diào)用检碗,那它跟普通函數(shù)也沒有什么兩樣。
// 創(chuàng)建函數(shù)
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
// 當作構造函數(shù)使用
var person = new Person('Nicholas', 29, 'Software Engineer'); // this --> person
person.sayName(); // 'Nicholas'
這里的 Person 就稱為構造函數(shù)码邻,person 稱為 Person 函數(shù)對象的一個實例(復制品)
// 當作普通函數(shù)調(diào)用
Person('Greg', 27, 'Doctor'); // this --> window
window.sayName(); // 'Greg'
“構造函數(shù)模式” 與 “工廠模式” 對比
* 1折剃、沒有顯式地創(chuàng)建對象
* 2、直接將屬性和方法賦給了 this 對象
* 3像屋、沒有 return 語句
* 4怕犁、構造函數(shù)都應該以 一個大寫字母開頭 function Person(){...},而非構造函數(shù)則應該以一個小寫字母開頭 function person(){...}
* 5、使用 new 創(chuàng)建對象
* 6因苹、能夠識別對象(這正是構造函數(shù)模式勝于工廠模式的地方)
構造函數(shù)模式的問題
使用構造函數(shù)的主要問題就是每個方法都要在每個實例上重新創(chuàng)建一遍
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1 和 person2 都有一個名為 sayName() 的方法苟耻,但是這兩個方法不是同一個 Function 的實例
console.log(person1.sayName == person2.sayName) // false
然而,創(chuàng)建兩個完成同樣任務的 Function 實例的確沒有必要扶檐,大可像下面這樣凶杖,通過把函數(shù)定義轉(zhuǎn)移到構造函數(shù)外部來解決這個問題
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName (){
alert(this.name);
}
console.log(person1.sayName == person2.sayName) // true
在這個例子中,我們把 sayName() 函數(shù)的定義轉(zhuǎn)移到了構造函數(shù)外部款筑。而在構造函數(shù)內(nèi)部智蝠,我們將 sayName 屬性設置成等于全局的 sayName 函數(shù)。這樣一來奈梳,由于 sayName 包含的是一個指向函數(shù)的指針杈湾,因此 person1 和 person2 對象就共享了在全局作用域中定義的同一個 sayName() 函數(shù)。這樣做確實解決了兩個函數(shù)做同一件事的問題攘须,可是新問題又來了:在全局作用域中定義的函數(shù)實際上只能被某個對象調(diào)用漆撞,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法于宙,那么就要定義很多個全局函數(shù)浮驳,于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。好在這些問題可以通過使用原型模式來解決捞魁。
3.原型模式
在 JS 中至会,無論什么時候,只要你創(chuàng)建了一個新函數(shù)谱俭,就會根據(jù)一組特定的規(guī)定為該函數(shù)創(chuàng)建一個 prototype 的屬性奉件,這個屬性指向函數(shù)的原型對象。而在默認情況下,所有的原型對象都會自動獲得一個 constructor (構造函數(shù))屬性,這個屬性包含一個指向 prototype 屬性所在函數(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"
var person2 = new Person();
person2.sayName(); // "Nicholas"
alert(person1.sayName == person2.sayName); // true
在此,我們將 sayName() 方法和所有屬性直接添加到了 Person 的 prototype 屬性中征候,構造函數(shù)變成了空函數(shù)杭攻。與構造函數(shù)模式不同的是,新對象的這些屬性和方法是由所有實例共享的疤坝。換句話說兆解,person1 和 person2 訪問的都是同一組屬性和同一個 sayName() 函數(shù)
雖然可以通過對象實例訪問保存在原型中的屬性和方法,但卻不能通過對象實例重寫原型中的屬性和方法(可以通過對象的 __proto__ 重寫)
如果我們在實例中添加了一個屬性和方法跑揉,而該屬性和方法與實例原型中的同名锅睛,那我們就在實例中創(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 = "Grag";
person1.sayName = function(){
alert(this.age);
};
alert(person1.name); // "Grag"---來自實例
person1.sayName(); // "29"---來自實例
alert(person2.name); // "Nicholas"--來自原型
person2.sayName(); // "Nicholas"---來自原型
通過使用 delete 操作符可以完全刪除實例屬性
var person1 = new Person();
var person2 = new Person();
person1.name = "Grag";
alert(person1.name); // "Grag"---來自實例
alert(person2.name); // "Nicholas"--來自原型
delete person1.name;
alert(person1.name); // "Nicholas"--來自原型
通過使用 hasOwnProperty() 方法,可以檢測一個屬性是存在實例中现拒,還是存在于原型中
alert(person1.hasOwnProperty("name")); // false
alert(hasPrototypeProperty("person", "name")); // true
person1.name = "Grag";
alert(person1.name); // "Grag"---來自實例
alert(person1.hasOwnProperty("name")); // true
alert(hasPrototypeProperty("person", "name")); // false
alert(person2.name); // "Nicholas"--來自原型
alert(person2.hasOwnProperty("name")); // false
delete person1.name;
alert(person1.name) // "Nicholas"--來自原型
alert(person1.hasOwnProperty("name")); // false
更簡單的原型寫法
用一個包含所有屬性和方法的對象字面量來重寫整個原型對象
function Person(){}
Person.prototype = {
// 由于字面量寫法辣垒,等于以一個字面量形式創(chuàng)建的新對象,本質(zhì)上完全重寫了默認的 prototype 對象印蔬,
// 導致 constructor (構造函數(shù)的指針)不再指向 Person 了勋桶,
// 因此 constructor 屬性也就變成了新對象的 constructor 屬性(指向 Object 構造函數(shù)),所以我們需要特意將它設置適當?shù)闹? constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
}
var person1 = new Person();
alert(person1.name) // "Nicholas"
原型對象的問題
首先它省略了為構造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié)侥猬,結(jié)果所有實例在默認情況下都將取得相同的屬性值例驹,但還不是原型的最大問題,其實最大的問題還是原型中所有屬性是被很多實例共享的退唠,這種共享對于函數(shù)非常合適鹃锈。對于那些包含基本值的屬性倒也說得過去,畢竟通過在實例上添加一個同名屬性瞧预,可以隱藏原型中的對應屬性屎债。然而,對于包含引用類型值的屬性來說松蒜,就有問題了---全部共享一個屬性(無論怎樣修改扔茅,其他實例的值都是一樣的)
4.混合模式(組合使用構造函數(shù)模式和原型模式)
構造函數(shù)模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性秸苗,結(jié)果,每個實例都會有自己的一份實例屬性的副本运褪,但同時又共享著對方方法的引用惊楼,這種混成模式還支持向構造函數(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("Grag", 27, "Doctor");
person1.friends.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
這種構造函數(shù)與原型混成的模式,是目前在 ECMAScript 中使用最廣泛秸讹,認同度最高的一種創(chuàng)建自定義類型的方法檀咙。可以說璃诀,這是用來定義引用類型的一種默認模式
5.動態(tài)原型模式
有其他 OO 語言經(jīng)驗的開發(fā)人員在看到獨立的構造函數(shù)和原型時弧可,很可能會感到非常困惑。動態(tài)原型模式正是致力于解決這個問題的一個方案劣欢,它把所有信息都封裝在了構造函數(shù)中棕诵,而通過在構造函數(shù)中初始化原型(僅在必要的情況下),又保持了同時使用構造函數(shù)和原型的優(yōu)點凿将。換句話說校套,可以通過檢香某個應該存在的方法是否有效,來決定是否需要初始化原型
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 person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();
這里只在 sayName() 方法不存在的情況下牧抵,才會將它添加到原型中笛匙。這段代碼只會在初次調(diào)用構造函數(shù)時才會執(zhí)行侨把。此后,原型已經(jīng)完成初始化妹孙,不需要在做什么修改了秋柄。
使用動態(tài)原型模式時,不能使用對象字面量重寫原型蠢正。
6.寄生構造函數(shù)模式
類似于工廠模式與構造函數(shù)模式結(jié)合體
function Person(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 = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
構造函數(shù)在不返回值的情況下华匾,默認會返回新對象的實例。而通過在構造函數(shù)的末尾添加一個 return 語句机隙,可以重寫調(diào)用構造函數(shù)時返回的值
7.穩(wěn)妥構造函數(shù)模式
所謂穩(wěn)妥對象蜘拉,指的是沒有公共屬性,而且其方法也不引用 this 對象有鹿,不使用 new 操作符調(diào)用構造函數(shù)旭旭,穩(wěn)妥對象最適合在一些安全的環(huán)境中(這些環(huán)境中會禁止使用 this 和 new),或者在防治數(shù)據(jù)被其他應用程序改動時使用
function Person(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 = Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
這種模式創(chuàng)建的對象中葱跋,除了使用 sayName() 方法之外持寄,沒有其他辦法訪問 name 的值