前端戰五渣學JavaScript——面向對象、原型和原型鏈

日漫中的血統

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

沒有對象,那就new一個

今天要從對象這個話題開始聊起,不是拉仇恨,若是沒有,那就new一個對象好了前端

let girl = new Object();
複製代碼

這麼簡單,如今咱們就擁有了一個對象girl,也許你會說,你這個對象太普通了,我想要一個我喜歡的對象,我要DIY,能夠啊。java

// 咱們能夠經過字面量的形勢建立新的對象
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中的構造函數可用來建立特定類型的對象性能

如今咱們來裝修一下咱們的女神工廠,讓它能夠來構造咱們的女神ui

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,這是慣例,記住就好,是借鑑的其餘面向對象的語言。主要也是爲了區別其餘函數,由於構造函數自己也是函數,只不過能夠用來建立對象而已this

是怎麼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

結尾

不知道這篇加入了女神和海賊王例子的講解對象和原型鏈你們能不能看明白,能夠給我反饋,若是很難懂,我會再從新寫一篇偏嚴肅的博客來說解這部分的知識。若是還有沒涉及的只是,也能夠評論告訴我。


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

相關文章
相關標籤/搜索