面向?qū)ο蟆?gòu)造函數(shù)、原型、原型鏈與繼承

一、對象的定義
在ECMAScript-262中,對象被定義為“無序?qū)傩缘募希鋵傩钥梢园局担瑢ο蠡蛘吆瘮?shù)”。
也就是說,在JavaScript中,對象無非就是由一些列無序的key-value對組成。其中value可以是基本值,對象或者函數(shù)。
// 這里的person就是一個對象
var person = {
name: 'Tom',
age: 18,
getName: function() {},
parent: {}
}
創(chuàng)建對象
我們可以通過new的方式創(chuàng)建一個對象。
var obj = new Object();
也可以通過對象字面量的形式創(chuàng)建一個簡單的對象。
var obj = {};
當(dāng)我們想要給我們創(chuàng)建的簡單對象添加方法時,可以這樣表示。
// 可以這樣
var person = {};
person.name = "TOM";
person.getName = function() {
return this.name;
}
// 也可以這樣
var person = {
name: "TOM",
getName: function() {
return this.name;
}
}
訪問對象的屬性和方法
假如我們有一個簡單的對象如下:
var person = {
name: 'TOM',
age: '20',
getName: function() {
return this.name
}
}
當(dāng)我們想要訪問他的name屬性時,可以用如下兩種方式訪問。
person.name
// 或者
person['name']
如果我們想要訪問的屬性名是一個變量時,常常會使用第二種方式。
二、工廠模式
使用上面的方式創(chuàng)建對象很簡單,但是在很多時候并不能滿足我們的需求。就以person對象為例。假如我們在實際開發(fā)中,不僅僅需要一個名字叫做TOM的person對象,同時還需要另外一個名為Jake的person對象,雖然他們有很多相似之處,但是我們不得不重復(fù)寫兩次。
var perTom = {
name: 'TOM',
age: 20,
getName: function() {
return this.name
}
};
var perJake = {
name: 'Jake',
age: 22,
getName: function() {
return this.name
}
}
很顯然這并不是合理的方式,當(dāng)相似對象太多時,大家都會崩潰掉。
我們可以使用工廠模式的方式解決這個問題。顧名思義,工廠模式就是我們提供一個模子,然后通過這個模子復(fù)制出我們需要的對象。我們需要多少個,就復(fù)制多少個。
var createPerson = function(name, age) {
// 聲明一個中間對象,該對象就是工廠模式的模子
var o = new Object();
// 依次添加我們需要的屬性與方法
o.name = name;
o.age = age;
o.getName = function() {
return this.name;
}
return o;
}
// 創(chuàng)建兩個實例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);
很顯然,工廠模式幫助我們解決了重復(fù)代碼上的麻煩,讓我們可以寫很少的代碼,就能夠創(chuàng)建很多個person對象。但是這里還有兩個麻煩,需要我們注意。
第一個麻煩就是這樣處理,我們沒有辦法識別對象實例的類型。使用instanceof可以識別對象的類型,如下例子:
var obj = {};
var foo = function() {}
console.log(obj instanceof Object); // true
console.log(foo instanceof Function); // true
因此在工廠模式的基礎(chǔ)上,我們需要使用構(gòu)造函數(shù)的方式來解決這個麻煩。
三、構(gòu)造函數(shù)
在JavaScript中,new關(guān)鍵字可以讓一個函數(shù)變得與眾不同。
function demo() {
console.log(this);
}
demo(); // window
new demo(); // demo
為了能夠直觀的感受他們不同,建議大家動手實踐觀察一下。很顯然,使用new之后,函數(shù)內(nèi)部發(fā)生了一些變化,讓this指向改變。那么new關(guān)鍵字到底做了什么事情呢。
// 先一本正經(jīng)的創(chuàng)建一個構(gòu)造函數(shù),其實該函數(shù)與普通函數(shù)并無區(qū)別
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
// 將構(gòu)造函數(shù)以參數(shù)形式傳入
function New(func) {
// 聲明一個中間對象,該對象為最終返回的實例
var res = {};
if (func.prototype !== null) {
// 將實例的原型指向構(gòu)造函數(shù)的原型
res.proto = func.prototype;
}
// ret為構(gòu)造函數(shù)執(zhí)行的結(jié)果,這里通過apply,將構(gòu)造函數(shù)內(nèi)部的this指向修改為指向res,即為實例對象
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 當(dāng)我們在構(gòu)造函數(shù)中明確指定了返回對象時,那么new的執(zhí)行結(jié)果就是該返回對象
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果沒有明確指定返回對象,則默認(rèn)返回res,這個res就是實例對象
return res;
}
// 通過new聲明創(chuàng)建實例,這里的p1,實際接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());
// 當(dāng)然,這里也可以判斷出實例的類型了
console.log(p1 instanceof Person); // true
所以,為了能夠判斷實例與對象的關(guān)系,我們就使用構(gòu)造函數(shù)來搞定。
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
var p1 = new Person('Ness', 20);
console.log(p1.getName()); // Ness
console.log(p1 instanceof Person); // true
關(guān)于構(gòu)造函數(shù),就先記住下面這幾個結(jié)論吧。
?與普通函數(shù)相比,構(gòu)造函數(shù)并沒有任何特別的地方,首字母大寫只是我們約定的小規(guī)定,用于區(qū)分普通函數(shù);
?new關(guān)鍵字讓構(gòu)造函數(shù)具有了與普通函數(shù)不同的許多特點,而new的過程中,執(zhí)行了如下過程:
1.聲明一個中間對象;
2.將該中間對象的原型指向構(gòu)造函數(shù)的原型;
3.將構(gòu)造函數(shù)的this,指向該中間對象;
4.返回該中間對象,即返回實例對象。
四、原型
雖然構(gòu)造函數(shù)解決了判斷實例類型的問題,但是,說到底,還是一個對象的復(fù)制過程。跟工廠模式頗有相似之處。也就是說,當(dāng)我們聲明了100個person對象,那么就有100個getName方法被重新生成。
這里的每一個getName方法實現(xiàn)的功能其實是一模一樣的,但是由于分別屬于不同的實例,就不得不一直不停的為getName分配空間。這就是工廠模式存在的第二個麻煩。
顯然這是不合理的。我們期望的是,既然都是實現(xiàn)同一個功能,那么能不能就讓每一個實例對象都訪問同一個方法?
當(dāng)然能,這就是原型對象要幫我們解決的問題了。
我們創(chuàng)建的每一個函數(shù),都可以有一個prototype屬性,該屬性指向一個對象。這個對象,就是我們這里說的原型。
當(dāng)我們在創(chuàng)建對象時,可以根據(jù)自己的需求,選擇性的將一些屬性和方法通過prototype屬性,掛載在原型對象上。而每一個new出來的實例,都有一個proto屬性,該屬性指向構(gòu)造函數(shù)的原型對象,通過這個屬性,讓實例對象也能夠訪問原型對象上的方法。因此,當(dāng)所有的實例都能夠通過proto訪問到原型對象時,原型對象的方法與屬性就變成了共有方法與屬性。
我們通過一個簡單的例子與圖示,來了解構(gòu)造函數(shù),實例與原型三者之間的關(guān)系。
// 聲明構(gòu)造函數(shù)
function Person(name, age) {
this.name = name;
this.age = age;
}
// 通過prototye屬性,將方法掛載到原型對象上
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true

圖示
通過圖示我們可以看出,構(gòu)造函數(shù)的prototype與所有實例對象的proto都指向原型對象。而原型對象的constructor指向構(gòu)造函數(shù)。
除此之外,還可以從圖中看出,實例對象實際上對前面我們所說的中間對象的復(fù)制,而中間對象中的屬性與方法都在構(gòu)造函數(shù)中添加。于是根據(jù)構(gòu)造函數(shù)與原型的特性,我們就可以將在構(gòu)造函數(shù)中,通過this聲明的屬性與方法稱為私有變量與方法,它們被當(dāng)前被某一個實例對象所獨有。而通過原型聲明的屬性與方法,我們可以稱之為共有屬性與方法,它們可以被所有的實例對象訪問。
當(dāng)我們訪問實例對象中的屬性或者方法時,會優(yōu)先訪問實例對象自身的屬性和方法。
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
console.log('this is constructor.');
}
}
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('tim', 10);
p1.getName(); // this is constructor.
在這個例子中,我們同時在原型與構(gòu)造函數(shù)中都聲明了一個getName函數(shù),運行代碼的結(jié)果表示原型中的訪問并沒有被訪問。
我們還可以通過in來判斷,一個對象是否擁有某一個屬性/方法,無論是該屬性/方法存在與實例對象還是原型對象。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('tim', 10);
console.log('name' in p1); // true
in的這種特性最常用的場景之一,就是判斷當(dāng)前頁面是否在移動端打開。
isMobile = 'ontouchstart' in document;
// 很多人喜歡用瀏覽器UA的方式來判斷,但并不是很好的方式
更簡單的原型寫法
根據(jù)前面例子的寫法,如果我們要在原型上添加更多的方法,可以這樣寫:
function Person() {}
Person.prototype.getName = function() {}
Person.prototype.getAge = function() {}
Person.prototype.sayHello = function() {}
... ...
除此之外,我還可以使用更為簡單的寫法。
function Person() {}
Person.prototype = {
constructor: Person,
getName: function() {},
getAge: function() {},
sayHello: function() {}
}
這種字面量的寫法看上去簡單很多,但是有一個需要特別注意的地方。Person.prototype = {}實際上是重新創(chuàng)建了一個{}對象并賦值給Person.prototype,這里的{}并不是最初的那個原型對象。因此它里面并不包含constructor屬性。為了保證正確性,我們必須在新創(chuàng)建的{}對象中顯示的設(shè)置constructor的指向。即上面的constructor: Person。
五、原型鏈
原型對象其實也是普通的對象。幾乎所有的對象都可能是原型對象,也可能是實例對象,而且還可以同時是原型對象與實例對象。這樣的一個對象,正是構(gòu)成原型鏈的一個節(jié)點。因此理解了原型,那么原型鏈并不是一個多么復(fù)雜的概念。
我們知道所有的函數(shù)都有一個叫做toString的方法。那么這個方法到底是在哪里的呢?
先隨意聲明一個函數(shù):
function foo() {}
那么我們可以用如下的圖來表示這個函數(shù)的原型鏈。

原型鏈


圖片1.png

其中foo是Function對象的實例。而Function的原型對象同時又是Object的實例。這樣就構(gòu)成了一條原型鏈。原型鏈的訪問,其實跟作用域鏈有很大的相似之處,他們都是一次單向的查找過程。因此實例對象能夠通過原型鏈,訪問到處于原型鏈上對象的所有屬性與方法。這也是foo最終能夠訪問到處于Object原型對象上的toString方法的原因。
基于原型鏈的特性,我們可以很輕松的實現(xiàn)繼承。
六、繼承
我們常常結(jié)合構(gòu)造函數(shù)與原型來創(chuàng)建一個對象。因為構(gòu)造函數(shù)與原型的不同特性,分別解決了我們不同的困擾。因此當(dāng)我們想要實現(xiàn)繼承時,就必須得根據(jù)構(gòu)造函數(shù)與原型的不同而采取不同的策略。
我們聲明一個Person對象,該對象將作為父級,而子級cPerson將要繼承Person的所有屬性與方法。
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.getName = function() {
return this.name;
}
首先我們來看構(gòu)造函數(shù)的繼承。在上面我們已經(jīng)理解了構(gòu)造函數(shù)的本質(zhì),它其實是在new內(nèi)部實現(xiàn)的一個復(fù)制過程。而我們在繼承時想要的,就是想父級構(gòu)造函數(shù)中的操作在子級的構(gòu)造函數(shù)中重現(xiàn)一遍即可。我們可以通過call方法來達(dá)到目的。
// 構(gòu)造函數(shù)的繼承
function cPerson(name, age, job) {
Person.call(this, name, age);
this.job = job;
}
而原型的繼承,則只需要將子級的原型對象設(shè)置為父級的一個實例,加入到原型鏈中即可。
// 繼承原型
cPerson.prototype = new Person(name, age);
// 添加更多方法
cPerson.prototype.getLive = function() {}

原型鏈
以上方式即為組合繼承,也稱偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一起,其背后的思路是使用原型鏈實現(xiàn)對原型屬性的繼承,通過借用構(gòu)造函數(shù)來實現(xiàn)實例屬性的繼承。這是js中最常用的繼承模式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,462評論 2 378