前端戰五渣學JavaScript——物件導向、原型和原型鏈

前端戰五渣發表於2019-03-26

日漫中的血統

小悟空為什麼魔擋殺魔,因為悟空有著超級賽亞人的血統,龍珠正傳中第一個變身超級賽亞人的賽亞人。鳴人一個吊車尾,為什麼能當上火影,因為他老爹就是四代火影。黑崎一護為什麼那麼厲害,因為他爹就曾是護庭十三隊的隊長。路飛獨自出海為什麼現在是擁有草帽大船團的人,也是因為他爹,他爺爺都是了不的的人物,並且是傳說中的D之一族。江戶川柯南(工藤新一)推理為什麼那麼厲害,他老爹就是知名的推理小說作家。
日漫中的浦飯有助、小當家、犬夜叉,甚至魔卡少女櫻和美少女戰士,每一部都是赤裸裸的血統論,也是因為血統,他們有了父輩們優良的基因,可以所向披靡。

沒有物件,那就new一個

今天要從物件這個話題開始聊起,不是拉仇恨,如果沒有,那就new一個物件好了

let girl = new Object();
複製程式碼

這麼簡單,現在我們就擁有了一個物件girl,也許你會說,你這個物件太普通了,我想要一個我喜歡的物件,我要DIY,可以啊。

// 我們可以通過字面量的形勢建立新的物件
let girlGod = {
    height: '165cm',
    weight: '50kg',
    hairLength: 'long',
    hairColor: 'black',
    leg: 'long',
    faceValue: 10
}
複製程式碼

現在就得到了一個我們心目中想要的女神形象。
但是,現在的女神是你自己心中的女神,並不一定是所有人的女神,大家對女神的標準是很多的,那我們就需要一個能批量產出女神的地方。

女神工廠

工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體物件的過程。考慮到在ECMAScript中無法建立類,開發人員就發明了一種函式,用函式來封裝特定介面建立物件的細節。

現在我們就來建造我們自己的女神工廠吧

function girlGodFactory(height, weight, hairLength, hairColor, leg, faceValue) {
    let o = new Object();
    o.height = height;
    o.weight = weight;
    o.hairLength = hairLength;
    o.hairColor = hairColor;
    o.leg = leg;
    o.faceValue = faceValue;
    o.sayGoodMan = function (name = '') {
        console.log(`${name}你是個好人,但我${this.faceValue}分顏值,你配嗎?`)
    }
    return o
}
let girlGod1 = girlGodFactory('165cm', '45kg', 'short', 'yellow', 'long', 8);
let girlGod2 = girlGodFactory('160cm', '47kg', 'long', 'brown', 'long', 9);
複製程式碼

這樣我們就生產出了兩個風格迥異的女神(放舔狗)
但我們現在沒有用到new,我想要個新物件,新的,好,那我們來新一個物件

讓我們來構造女神

ECMAScript中的建構函式可用來建立特定型別的物件

現在我們來裝修一下我們的女神工廠,讓它可以來構造我們的女神

function CreateGirlGod(height, weight, hairLength, hairColor, leg, faceValue) {
  this.height = height;
  this.weight = weight;
  this.hairLength = hairLength;
  this.hairColor = hairColor;
  this.leg = leg;
  this.faceValue = faceValue;
  this.sayGoodMan = function (name = '') {
    console.log(`${name}你是個好人,但我${this.faceValue}分顏值,你配嗎?`)
  }
}

let girlGod1 = new CreateGirlGod('165cm', '45kg', 'short', 'yellow', 'long', 8);
let girlGod2 = new CreateGirlGod('160cm', '47kg', 'long', 'brown', 'long', 9);
複製程式碼

這就是一個可以構造女神的建構函式,注意,函式名的首字母必須大寫CreateGirlGod,這是慣例,記住就好,是借鑑的其他物件導向的語言。主要也是為了區別其他函式,因為建構函式本身也是函式,只不過可以用來建立物件而已

是怎麼new出一個新物件的

好,現在讓我們來了解一下面試題經常問的問題“你能說一下new一個物件的時候,new操作符做了些什麼嗎”,下面我就來說一下真確答案:

  1. 建立一個新物件;
  2. 講建構函式的作用域賦給新物件(因此this就指向了這個新物件);
  3. 執行建構函式中的程式碼(為這個新物件新增屬性);
  4. 返回新物件

上面的正確答案要記住了,對自己寫程式碼,面試,都有很重要的作用

原型模式

現在有了新的需求,現實生活中,我們可能對女神的要求千奇百怪,但是會有相同的部分,比如顏值都在8分,這時候,我們就需要一個可以共享屬性的方法來滿足這個需求。下面說到的prototype(原型)屬性就可以實現

function CommonGirlGod(height, weight, hairLength, hairColor) {
  this.height = height;
  this.weight = weight;
  this.hairLength = hairLength;
  this.hairColor = hairColor;
  this.sayGoodMan = function (name = '') {
    console.log(`${name}你是個好人,但我${this.faceValue}分顏值,你配嗎?`)
  }
}

CommonGirlGod.prototype.leg = 'long';
CommonGirlGod.prototype.faveValue = 8;

let girlGod1 = new CommonGirlGod('165cm', '45kg', 'short', 'yellow');
let girlGod2 = new CommonGirlGod('160cm', '47kg', 'long', 'brown');

console.log(girlGod1.leg); // long
console.log(girlGod2.leg); // long
console.log(girlGod1.faveValue); // 8
console.log(girlGod2.faveValue); // 8
複製程式碼

可以看到我們CommonGirlGod建構函式有一些定製的女神要求,然後我們又在下面在CommonGirlGod建構函式的prototype屬性上寫了兩個共有屬性,這兩個共有屬性在new出來的兩個女神中都包含。

[[Prototype]]__proto__prototype

我們來看一張《javascript高階程式設計》中的一張圖

原型、原型鏈
上圖中的Person是一個建構函式,person1person2是兩個經過new Person以後例項化的兩個。好,我們可以從圖中看出Person建構函式有一個prototype的屬性指向原型物件Person Prototype,裡面包含一些共有的屬性和方法,而經過Person建構函式例項的person1person2也包含一個屬性[[Prototype]][[Prototype]]也指向建構函式的原型物件Person Prototype,所以person1person2也共有裡面的屬性和方法。而建構函式的原型物件Person Prototype有一個屬性constructor又指回Person建構函式,這個我們後面再講,先說[[Prototype]]
我們都知道person1person2中有屬性[[Prototype]]包含共享的方法,但是在javascript這個指令碼語言中沒有標準的方式訪問[[Prototype]],但是一些高階瀏覽器在每個物件上都支援一個屬性__proto__;而在其他實現中,這個屬性對指令碼是完全不可見的,雖然我們可以呼叫裡面的方法。
一句話,建構函式上的是prototype屬性,包含各種需要共享的屬性和方法。通過建構函式例項化的物件有[[Prototype]]屬性,跟建構函式上的prototype一樣,但不是副本,是指標,指向同一個引用型別物件,但是我們不能通過obj.[[Prototype]]訪問到這個物件,只能通過高階瀏覽器提供的屬性__proto__訪問到
以上就是我目前對這三個名詞的理解。

原型物件Person Prototype中的constructor屬性

經過上面的探究我們可以初步認為[[Prototype]]__proto__prototype都是指向同一個物件。
這個物件擁有一個屬性constructor,屬性constructor預設指回prototype所在函式(我理解一般都是指回之前的建構函式)。他有什麼作用呢,看下面的例子:

function CommonGirlGod(height, weight, hairLength, hairColor, leg, faceValue) {
  this.height = height;
  this.weight = weight;
  this.hairLength = hairLength;
  this.hairColor = hairColor;
  this.leg = leg;
  this.faceValue = faceValue;
  this.sayGoodMan = function (name = '') {
    console.log(`${name}你是個好人,但我${this.faceValue}分顏值,你配嗎?`)
  }
}

let girlGod1 = new CommonGirlGod('165cm', '45kg', 'short', 'yellow', 'long', 8);

CommonGirlGod.prototype.sayHeight = function () {
  console.log(this.height)
};

girlGod1.sayHeight(); // 165cm
複製程式碼

上面程式碼做了什麼呢?通過new一個新的girlGod1出來,然後我們又通過在prototype上增加一個方法,讓女神可以報出自己的身高,我們可以執行看看,上面這樣寫是可以順利讓自己的女神報出身高的,但是,我們如果不是CommonGirlGod.prototype.sayHeight上賦值,而是通過字面量的方法新增呢,像下面這樣

function CommonGirlGod(height, weight, hairLength, hairColor, leg, faceValue) {
  this.height = height;
  this.weight = weight;
  this.hairLength = hairLength;
  this.hairColor = hairColor;
  this.leg = leg;
  this.faceValue = faceValue;
  this.sayGoodMan = function (name = '') {
    console.log(`${name}你是個好人,但我${this.faceValue}分顏值,你配嗎?`)
  }
}

let girlGod1 = new CommonGirlGod('165cm', '45kg', 'short', 'yellow', 'long', 8);

- CommonGirlGod.prototype.sayHeight = function () {
-   console.log(this.height)
- };

+ CommonGirlGod.prototype = {
+   sayHeight: function () {
+     console.log(this.height)
+   }
+ };

girlGod1.sayHeight(); // 165cm
複製程式碼

如果我們像上面這樣寫,會丟擲錯誤,這是為什麼呢?

因為第二種方法我們相當於重寫CommonGirlGod.prototype物件,prototype中包含的constructor屬性不復存在了,打斷了prototype物件和建構函式的聯絡了,所以會報錯。

繼承(血統論、血繼限界)

物件導向的三個基本特徵是:封裝、繼承、多型。下面我們要講得就是繼承
舉例,預設大家看過《海賊王》吧,卡普、龍和路飛,祖孫三代,他們之間存在著強大的血統遺傳,龍生龍鳳生鳳,老鼠的孩子會打洞。這就是繼承

D之一族

回顧一下建構函式、原型和例項的關係:每個建構函式都有一個原型物件prototype,原型物件都包含一個指向建構函式的指標constructor,而例項都包含一個指向原型物件的內部指標[[Prototype]](瀏覽器實現__proto__)。

// D之一族建構函式
function FamilyD() {
  this.middleName = 'D';
  this.sayWhoAmI = function () {
    console.log(`${this.familyName} · ${this.middleName} · ${this.name} `);
  }
}
// 卡普建構函式
function Garp() {
  this.familyName = 'Monkey';
  this.name = 'Garp';
}
// 卡普繼承D之一族
Garp.prototype = new FamilyD();
// 龍建構函式
function Dragon() {
  this.name = 'Dragon';
}
// 龍繼承卡普
Dragon.prototype = new Garp();
// 路飛建構函式
function Luffy() {
  this.name = 'Luffy';
}
// 路飛繼承龍
Luffy.prototype = new Dragon();

// 例項卡普
let garp = new Garp();
// 例項龍
let dragon = new Dragon();
// 例項路飛
let luffy = new Luffy();

garp.sayWhoAmI(); // Monkey · D · Garp
dragon.sayWhoAmI(); // Monkey · D · Dragon
luffy.sayWhoAmI(); // Monkey · D · Luffy 
複製程式碼

上面的程式碼寫的就是祖孫三代,卡普、龍和路飛的關係。
最開始宣告一個FamilyDD之一族的建構函式,寫上他們的中間名都是‘D’,還有一個說出自己是誰的方法sayWhoAmI()。緊接著宣告卡普的建構函式Garp,並定義了家族姓氏和卡普自己的名字,然後建構函式卡普Garp再關聯上D之一族的Garp.prototype = new FamilyD();,這就算卡普繼承了D之一族。然後同理宣告建構函式Dragon和建構函式Luffy,並定義各自的名字,最後例項化三個人,然後各自執行sayWhoAmI()函式,輸出各自的全名。
三個人的中間名都是繼承的D之一族,龍和路飛的姓氏都是繼承的卡普中的familyName

家譜

上面宣告瞭三個人,那我們如何體現原型鏈和各自的關係呢,這就要用到之前說到的[[Prototype]]__proto__了,利用__proto__去訪問[[Prototype]]。好,我們用他們最小的來做示範,就是孫子輩的路飛。

// 在上面程式碼基礎上,我們新增以下程式碼
console.log(`路飛的名字 ${luffy.name}`); // 路飛的名字 Luffy
console.log(`路飛爸爸的名字 ${luffy.__proto__.name}`); // 路飛爸爸的名字 Dragon
console.log(`路飛爺爺的名字 ${luffy.__proto__.__proto__.name}`); // 路飛爺爺的名字 Garp
複製程式碼

不知道這樣打出來大家能不能看明白其中的關係,根據__proto__一級一級的向上查詢,找到了父輩和爺爺輩的屬性

繼承
不知道這樣有沒有更直觀一些。
所有例項,物件的最上面都是Object的例項,而Object__proto__指向null,而null就沒有__proto__了,就到頭了

基於上面的程式碼如果我在路飛的建構函式中沒有定義name屬性,那console.log(luffy.name)就會輸出‘Dragon’,因為原型鏈的特徵,例項會現在自身查詢有沒有對應的屬性或者方法,如果沒有,就去__proto__上面去找,如果還沒有,就繼續在__proto__.__proto__去找,一直到為null,停止搜尋。這種情況稱為"屬性遮蔽 (property shadowing)。

弊端

就像如果路飛出問題了,你可能會找他的爸爸,他爸爸管不了,就要去找爸爸的爸爸,這樣其不是很費勁。程式碼也是一樣,如果在當前例項沒有的屬性和方法,js就會遍歷原型鏈上所有的屬性和方法,這樣很耗費效能。所以請注意程式碼中原型鏈的長度,並在必要時將其分解,以避免可能的效能問題

不要試圖修改或擴充套件原生物件的原型

大佬可忽略,對於一般水平的開發,儘量不要去修改類似Object.prototype,你可能會覆蓋原型屬性或方法,也可能對繼承自Object的例項發生不可預知的bug

結尾

不知道這篇加入了女神和海賊王例子的講解物件和原型鏈大家能不能看明白,可以給我反饋,如果很難懂,我會再重新寫一篇偏嚴肅的部落格來講解這部分的知識。如果還有沒涉及的只是,也可以評論告訴我。


我是前端戰五渣,一個前端界的小學生。

相關文章