寫在前面
因為本人想回顧一遍JavaScript基于原型繼承的相關(guān)知識桐智,所以開始翻查過去的筆記和相關(guān)博客佑女,想來想去還是先從創(chuàng)建對象這一塊入手奥秆,這個地方講的比較清楚的應該首推《JavaScript高級編程指南》。下面的內(nèi)容80%出自這本書的第五章傅是,如果大家不想翻書的話搅吁,姑且可以先看看我的這篇抄書筆記~
后文附贈了這本圣經(jīng)的高清第三版下載地址,希望能節(jié)省各位查詢下載的時間落午。下面開始介紹創(chuàng)建對象的幾種常見模式(寄生構(gòu)造函數(shù)模式和穩(wěn)妥構(gòu)造函數(shù)模式等不常見的模式暫且忽略谎懦,等后面關(guān)于原型鏈的文章繼續(xù)填坑~)
工廠模式
考慮到ECMAScript無法創(chuàng)建類,開發(fā)人員就發(fā)明了一種函數(shù)溃斋,用函數(shù)來封裝以特定接口創(chuàng)建對象的細節(jié)界拦,如下例子所示:
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name)
}
return o;
}
var person1 = createPerson('feng', 25, 'enginner');
var person2 = createPerson('yun', 52, 'doctor');
可以無數(shù)次調(diào)用createPerson()
, 每次都會返回一個包含三個屬性一個方法的對象梗劫。工廠模式雖然解決了創(chuàng)建多個相似對象的問題享甸,但是無法解決對象識別的問題(類型都是Object
,我們希望能返回類型為function
并且具有特征name
的對象),于是就有了下面的構(gòu)造函數(shù)模式。
構(gòu)造函數(shù)模式
構(gòu)造函數(shù)可以用來創(chuàng)建特定類型的對象
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name)
}
}
var person1 = new Person('feng', 25, 'enginner');
var person2 = new Person('yun', 52, 'doctor');
在本方法中我們使用到了new
操作符來達到目的梳侨。以這種方式調(diào)用構(gòu)造函數(shù)實際上會經(jīng)歷一下四個步驟
- 創(chuàng)建一個新對象(讓空對象的'_proto'屬性指向Person.prototype,詳見下文)
- 將構(gòu)造函數(shù)的作用域賦值給新對象(因此this指向了這個新對象)
- 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個新對象添加屬性)
- 返回新對象
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實例標志為一個特定的類型蛉威;而這正是構(gòu)造函數(shù)模式勝過工廠模式的地方。
console.log(person1.constructor === Person);//true
console.log(person2.constructor === Person);//true
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
何為構(gòu)造函數(shù)
構(gòu)造函數(shù)與其他函數(shù)唯一的區(qū)別走哺,就在于調(diào)用他們的方式不同蚯嫌。不過,構(gòu)造函數(shù)畢竟也是函數(shù)丙躏,不存在定義構(gòu)造函數(shù)的特殊語法择示。任何函數(shù),只要通過new
操作符來調(diào)用晒旅,那他就可以當做構(gòu)造函數(shù)栅盲;不通過new
來調(diào)用,則就是普通函數(shù)废恋。上面構(gòu)造器模式中的Person()
函數(shù)可以通過下面任何一種方式來調(diào)用:
var person = new Person('feng', 25, 'enginner');
console.log(person.sayName());//feng
Person('duang', 101, 'god');//函數(shù)直接調(diào)用谈秫,this綁定到window
window.sayName();//duang
構(gòu)造函數(shù)的問題
每個方法都要在每個實例上重新創(chuàng)建一遍扒寄。在前面的例子中,person1
和person2
都有一個名為sayName()
的方法拟烫,但那兩個方法不是同一個Function
的實例(ECMAScript中旗们,函數(shù)就是對象,因此每定義一個函數(shù)构灸,也就實例化了一個對象)上渴。從邏輯上講,此時的構(gòu)造函數(shù)也可以這樣定義:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)")
}
因此不同實例上的同名函數(shù)實不相等的
console.log(person1.sayName == person2.sayName);//false
這樣做浪費內(nèi)存喜颁,我們希望得到一種能把共享的屬性和方法放到同一個容器的模式稠氮,于是就有了下文的原型模式。
原型模式
原型模式初步
我們創(chuàng)建的每個函數(shù)在生成的一瞬間半开,都有一個prototype
(原型)屬性隔披,這個屬性是一個指針,指向一個對象寂拆,而這個對象的用途是包含可由特定類型的所有實例共享的實例和方法奢米,這個對象一般稱為這個函數(shù)的 原型對象(prototype
)。所有原型對象都會自動獲得一個constructor
(構(gòu)造函數(shù))屬性纠永,這個屬性包含一個指回構(gòu)造函數(shù)鬓长。
使用原型對象的好處就是可以讓所有的實例對象共享原型所包含的屬性和方法。
function Person(){}
Person.prototype.name = 'Nicholas';
Person.prototype.age = '29';
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
person1.sayName();//feng
var person2 = new Person();
person.sayName();//feng
console.log(person1.sayName == person2.sayName);//true
與構(gòu)造函數(shù)模式不同的是尝江,新對象的屬性和方法是有所有實例所共享的涉波。
理解原型
默認情況下,具體的關(guān)系如下所示
注:
(2)中炭序,如果手動分配了原型指針(重寫了原型對象啤覆,比如將原型對象重新聲明為一個字面量對象),則需要手動為原型添加constructor
屬性惭聂,重新建立構(gòu)造函數(shù)和原型對象之間的關(guān)系
(5)中窗声,Chrome 中可以通過_proto_
訪問到該屬性,而在IE中是不可見的辜纲。
上面那段代碼參考上圖的關(guān)系如下:
從上圖可見笨觅,構(gòu)造函數(shù)和實例對象沒有直接關(guān)系!當解釋器讀取某個對象的某個屬性的時候侨歉,都會按照上圖中的實現(xiàn)執(zhí)行一遍搜索屋摇,目標是具有給定名字的屬性。搜索首先從對象實例開始幽邓,如果在實例中找到該屬性則返回,如果沒有則繼續(xù)搜索指針指向的原型對象(prototype
)火脉,如果還是沒有找到則繼續(xù)遞歸prototype
的prototype
對象牵舵,直到找到為止柒啤,如果遞歸到object
(原型鏈最頂端)仍然沒有則返回錯誤。
可以通過對象事例訪問保存在原型中的值畸颅,但卻不能通過對象事例重寫原型中的值担巩。如果在實例中定義和原型中同名的屬性或函數(shù),則會事例中的屬性會覆蓋原型中的同名屬性没炒。
重寫原型
上面例子中涛癌,每添加一個屬性和方法都要敲一遍Person.prototype
。為了減少不必要的輸入送火,更常見的寫法是用一個包含所有屬性和方法的字面量對象來從斜原型對象拳话,代碼如下:
function Person(){}
Person.prototype = {
name: 'Nicholas',
age: '29',
job: 'Software Engineer',
sayName: function(){
console.log(this.name);
}
}
但是這么寫有一個例外:constructor
屬性不再指向Person
了。因為這樣寫种吸,本質(zhì)上重寫了prototype
對象弃衍,因此constructor
屬性也就變成了新的對象的constructor
(指向Object構(gòu)造函數(shù)),不再指向Person函數(shù)坚俗。此時盡管instanceOf操作符還能返回正確結(jié)果镜盯,但是通過constructor
已經(jīng)無法確定對象的類型了。
var friend = new Person();
console.log(friend instanceof Object);//true
console.log(friend.instanceof Person);//true
console.log(friend.constructor == Person);//false
console.log(friend.constructor == Object);//true
所以如果constructor
屬性真的很重要的話猖败,可以向下面的方式特意將它設(shè)置為適當?shù)闹担?/p>
function Person(){}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: '29',
job: 'Software Engineer',
sayName: function(){
console.log(this.name);
}
}
in && hasOwnProperty
如何確認屬性是保存在原型中還是保存在實例對象中呢速缆?組合使用in
和hasOwnProperty
in
操作符,如果value in object
返回true
則表示這個value
屬性要么能在實例中訪問恩闻,要么保存在原型中激涤,反正能通過原型鏈訪問到-
hasOwnProperty
只在屬性存在于實例中時才返回true
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty && (name in object)
}
上面這段代碼返回true則表示,屬性在原型中而不在實例對象中
原型模式的問題
首先判呕,他省略了為構(gòu)造函數(shù)傳遞參數(shù)這個環(huán)節(jié)倦踢,結(jié)果所有屬性默認情況下都獲得了相同的屬性值。還有最重要的問題是由其共享的本質(zhì)導致的侠草。
原型中所有屬性被很多實力共享辱挥,這種共享對函數(shù)很適合,對那些事基本值的屬性也還說得過去,然而咪橙,對那些包含引用值得屬性來說韭山,問題就比較大了:
function Person(){}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: '29',
job: 'Software Engineer',
friends:['aa', 'bb', 'cc'],
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('dd');
console.log(person1.friends);//aa,bb,cc,dd
console.log(person2.friends);//aa,bb,cc,dd
console.log(person1.friends === person2.friends);//true
可見,一個事例修改原型中的某個引用類型屬性园爷,其他事例都會受到影響。正是這個原因?qū)е铝撕苌儆腥藛为毷褂迷湍J绞胶常谑蔷陀辛讼挛牡幕旌夏J健?/p>
組合使用原型模式和構(gòu)造函數(shù)模式
組合使用原型模式和構(gòu)造函數(shù)模式是最常見童社,應用最廣泛的創(chuàng)建自定義類型的方式:構(gòu)造函數(shù)模式用來定義實力屬性,原型模式用來定義方法和共享的屬性著隆。結(jié)果扰楼,每個實例都會有自己的一份實例屬性的副本呀癣,同時共享著對方法的引用。另外這種混合模式弦赖,還支持想構(gòu)造函數(shù)傳遞參數(shù)项栏。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends =['aa', 'bb'];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("Nicholas","29","Software Engineer");
var person2 = new Person("Grep","27","Doctoer");
person1.friends.push('cc');
console.log(person1.friends);//aa,bb,cc
console.log(person2.friends);//aa,bb
console.log(person1.friends === person2.friends);//false
console.log(person1.sayName === person2.sayName);//true