輕鬆理解建構函式和原型物件

_Dreams發表於2019-08-18

前言

曾經看過很多關於原型的視訊和文章的你,是否還是對原型雲裡霧裡,一頭霧水呢,今天讓我們一起揭開這層神祕的面紗吧~~~go go go!

利用建構函式建立物件

在ES6之前,物件不是基於類建立的,而是用一種稱為建構函式的特殊函式來定義物件和它們的特徵

建立物件可以通過以下三種方式

1.物件字面量

2.new Object()

3.自定義建構函式

這裡我們著重來看下怎麼利用建構函式建立物件, 我們把物件的公共屬性放在建構函式中

function Star(name,age) {
    this.name = name;
    this.age = age;
    this.sing = function() {
        console.log('我在唱歌');
    }
}
var star1 = new Star('歌星1','27');
var star2 = new Star('歌星2','23');

star1.sing();
star2.sing();
複製程式碼

這樣我們就生成了兩個獨立的物件

建構函式的定義

建構函式是一種特殊的函式,主要用來初始化物件,他總是與new一起使用,我們可以把物件中的一些公共屬性和方法抽取出來,然後封裝到這個函式裡。

在JS中,使用建構函式時需要注意以下兩點:

1.建構函式用於建立某一類物件,其首字母要大寫

2.建構函式要和new一起使用才有意義

new的執行過程

1.建立一個新的空物件

2.讓this指向這個新的物件

3.執行建構函式裡面的程式碼,給這個新物件新增屬性和方法

4.返回這個新物件(所以建構函式裡面不需要return)

例項成員

在js的建構函式中,有很多例項和很多方法。

所謂例項成員就是建構函式內部通過this新增的成員

舉個例子

function Star(name,age) {
    this.name = name;
    this.age = age;
    this.sing = function() {
        console.log('我在唱歌');
    }
}
var star1 = new Star('歌星1','27')
複製程式碼

在上面的例子中,name,age,sing就是例項成員

例項成員只能通過例項化的物件來訪問

例如: console.log(star1.age)

靜態成員

所謂靜態成員就是在建構函式本身上新增的成員

繼續沿用上面的程式碼

Star.sex = '男'

那麼這個sex就是靜態成員

如果想要訪問那麼就可以 console.log(Star.sex)

建構函式的問題

浪費記憶體

繼續想像我們之前的程式碼。

輕鬆理解建構函式和原型物件

這裡我們建立出了劉德華張學友物件。

sing這個函式我們明明可以只建立一個,因為他們都是歌手,但現在我們每個創造出來的物件裡都有sing,這就很明顯的造成了記憶體浪費問題,如果我們有一百個物件,那麼想想都覺得恐怖。

我們希望所有的物件使用同一個函式,這樣就比較節省記憶體,那麼我們要怎麼做?

原型物件---prototype

每個建構函式都有一個prototype屬性,指向另一個函式,注意這個prototype就是一個物件,這個物件的所有屬性和方法都會被這個建構函式所擁有。

我們列印下建構函式,看下建構函式中有沒有prototype這個屬性

輕鬆理解建構函式和原型物件

至此,我們可以把那些不變的方法,直接定義在prototype物件上,這樣所有的物件的例項就可以共享這些方法。

所以現在我們就可以把sing方法放到我們的原型物件上

Star.prototype.sing = function(){
    console.log('我會唱歌')
}
複製程式碼

那麼現在我們來思考下

1.原型是什麼?

原型其實就是一個物件

2.原型的作用是什麼?

共享屬性和方法

物件原型-- proto

物件都會有一個屬性__proto__指向建構函式的prototype原型物件,之所以我們物件可以使用建構函式prototype原型物件的屬性和方法,就是因為物件有__proto__原型的存在

那下面我們看看物件上有沒有__proto__這個屬性吧

輕鬆理解建構函式和原型物件

我們來思考下這個例子

function Star(name,age) {
    this.name = name;
    this.age = age;
}
Star.prototype.sing = function(){
    console.log('我會唱歌')
}
var star1 = new Star('歌星1','27')
star1.sing()
複製程式碼

雖然star1身上沒有sing這個方法,但是這個star1物件裡有一個__proto__他指向的就是建構函式的原型物件(prototype),所以我們就可以獲取到這個方法。

我們來看下 star1.__proto__指向 Star.prototype嗎?

我們會發現兩個恆等於true,說明是這樣指向的。

那麼這裡我們就會發現方法的查詢規則如下:

首先先看歌星1這個物件身上是否有sing這個方法,如果有就執行這個物件的sing,如果沒有sing這個方法,因為有__proto__的存在,那麼就去建構函式原型物件(prototype)身上去查詢sing這個方法

下面我們看一張圖,應該會理解的更深刻一些:

輕鬆理解建構函式和原型物件

這裡我們要說的是 __proto__物件原型和原型物件prototype是等價的

__proto__物件原型的意義就在於為物件的查詢機制提供了一條路線,但是它是一個非標準屬性,因此在實際開發中,不可以使用這個屬性,它只是內部指向原型物件prototype

我們通常把prototype稱為原型物件,__proto__稱為物件原型,__proto__指向的就是建構函式中的原型物件。

constructor建構函式

物件原型__proto__和建構函式(prototype)原型物件裡面都有一個屬性constructor屬性,constructor我們稱為建構函式,因為它指回建構函式本身

我們這邊列印下star.__proto__和Star.prototype

列印結果如下圖

輕鬆理解建構函式和原型物件

的確如我們所說,它們都有constructor

constructor的作用

只要用於記錄該物件引用於哪個建構函式,它可以讓原型物件重新指向原來的建構函式。

很多情況下,我們需要手動的利用constructor這個屬性指回原來的建構函式

我們來列印下Star.prototype.constructor和star1.proto.constructor

結果如下圖:

輕鬆理解建構函式和原型物件

那麼上面我們說很多情況下,需要手動校準constructor,那麼下面我們來舉個例子

輕鬆理解建構函式和原型物件

這邊我們採用這種寫法,我們再列印下 Star.prototype.constructor和star1.proto.constructor 我們會發現建構函式發生了改變:

輕鬆理解建構函式和原型物件

那麼這是為什麼呢?

其實我們可以理解為上面的寫法是用了一個新的物件,把原來的prototype給覆蓋掉了,那麼覆蓋完之後,我們的Star.prototype裡就沒有constructor了。 那怎麼解決呢,其實很簡單,我們只需要把上面的程式碼改成這樣就可以了:

輕鬆理解建構函式和原型物件
我們再來列印就會發現已經好了,又指回我們原來的建構函式了

輕鬆理解建構函式和原型物件

建構函式,物件例項,原型物件三者之間的關係

輕鬆理解建構函式和原型物件

原型鏈

只要是物件就有__proto__原型,指向原型物件,那麼理論上我們的star物件就會有__proto__

我們輸出下Star.prototpye

輕鬆理解建構函式和原型物件

我們會發現這個原型物件裡也有一個原型__proto__, 那麼我們再來看看這個__proto__指向的是誰呢?

輕鬆理解建構函式和原型物件
我們發現它指向的是Object,我們來驗證下:

輕鬆理解建構函式和原型物件

看看這個是否相等,如果相等說明我們這個Star的原型物件的__proto__確實指向的是Object的原型物件(prototype),我們會發現這句輸出結果為true

那麼再回到上面,我們這個Object的原型物件是誰創造出來的呢,毫無疑問,肯定是Object的建構函式建立出來的,那麼按道理在這個Object原型物件上肯定有一個constructor指回Object建構函式。

問題來了,Object的原型物件他也是一個物件,那他肯定也有一個__proto__存在,我們的Object原型物件的__proto__到底會指向誰呢?

輕鬆理解建構函式和原型物件

我們會發現輸出結果是null

我們得出結論:Object.prototype原型物件裡面的__proto__原型,指向為null

最後我們總結出一張圖:

輕鬆理解建構函式和原型物件

通過上圖我們發現ldh是一個物件,物件裡有一個__proto__指向了Star原型物件,Star也是一個物件,那麼它裡面也有__proto__,他指向Object原型物件,那麼它裡面也有__proto__,他指向null,那麼我們發現這張圖裡有很多__proto__將物件之間連線了起來,成為了一個鏈條,我們把這個鏈條稱為原型鏈

原型鏈

有了原型鏈,後面我們在訪問物件成員時給我們提供了一條鏈路,我們會先到ldh例項看看有沒有這個屬性,如果沒有,那麼就到Star原型物件上去看,如果還沒有我們再往上一層到Object原型物件去看,如果還沒有那麼就找不到了,就會返回undefined

所以我們總結:原型鏈就好比是一條線路一樣,讓我們去查詢時按照這個路一層一層的往上找就可以了。

我們再來回顧下上面我們曾經說過的概念:

只要是物件它裡面就有__proto__,這個__proto__指向的就是原型物件prototype。

javascript的成員查詢機制

1.當訪問一個物件的屬性(包括方法)時,首先查詢這個物件自身有沒有該屬性。

2.如果沒有就查詢他的原型(也就是__proto__指向的prototype原型物件)

3.如果還沒有就查詢原型物件的原型

4.依次類推一直找到Object為止(null)

原型物件的this指向

我們來看看this指向問題

function Star(name,age) {
    this.name = name;
    this.age = age;
}
Star.prototype.sing = function() {
    console.log(this);
}
var singer = new Star('張三',18)
複製程式碼

1.建構函式中裡面這個this指向的是物件例項 在這個例子中指向的就是singer這個物件。

2.原型物件函式裡面的this指向的還是singer這個物件

繼承

我們知道在es6之前並沒有給我們提供extends繼承的語法糖,所以我們得通過建構函式+原型物件模擬實現繼承,這種方式被稱為組合繼承

call方法的作用

1.它可以呼叫某個函式,並且可以修改函式執行時this的指向。

輕鬆理解建構函式和原型物件

繼承父類屬性

核心原理:通過call()把父類的this指向子類的this,這樣就可以實現子類繼承父類的屬性了。 我們來看一個例子:

輕鬆理解建構函式和原型物件

在父建構函式的this指向父建構函式的物件例項。

在子建構函式的this指向子建構函式的物件例項。

那現在問題是我的子建構函式怎麼才能把父建構函式裡的uname和age這兩個屬性拿過來使用呢?

其實很簡單,我們只需要在子建構函式中呼叫父建構函式就可以了,所以我們把這種方式稱為借用建構函式繼承

所以我們可以這麼來寫:

輕鬆理解建構函式和原型物件

Father.call(this,uname,age);
複製程式碼

主要是這句話,這個是什麼意思呢?

就是說子類建構函式中通過call將父類建構函式的this指向了自身,以達到繼承屬性的目的。

我們現在需要做的就是看看這個子物件例項裡有沒有uname,age,如果有那說明繼承成功了。

輕鬆理解建構函式和原型物件

我們發現的確是有了這兩個屬性。

繼承父類的方法

之前我們也說過,共有的屬性我們寫到建構函式裡,那麼共有的方法呢?

我們是不是寫到原型物件上就可以了?

我們們舉個例子:

不管是父親還是孩子,他們都可以去掙錢,所以我們們可以在父親的prototype上加上money方法.

function Father(name,age) {
    this.name = name;
    this.age = age;
}
Father.prototype.money = function(){
    console.log(1000+'元')
}
function Son(name,age,score){
    Father.call(this,uname,age);
    this.score = score;
}
var son = new Son('劉德華',18,100);
console.log(son)
複製程式碼

我們現在想讓son去繼承父親掙錢的方法,該怎麼做?

我們可以把父親的原型物件賦值給孩子的原型物件,這樣應該就不會有問題

function Father(name,age) {
    this.name = name;
    this.age = age;
}
Father.prototype.money = function(){
    console.log(1000+'元')
}
function Son(name,age,score){
    Father.call(this,uname,age);
    this.score = score;
}
Son.prototype = Father.prototype;
var son = new Son('劉德華',18,100);
console.log(son)
複製程式碼

我們來輸出下兒子看下列印結果:

輕鬆理解建構函式和原型物件

可以看到的確繼承成功了,很開心是不是?

其實想象很美好,現實很骨感,總會有奇奇怪怪的問題出現,我們將程式碼再進行新增:

我們在孩子上加一個考試的方法:

function Father(name,age) {
    this.name = name;
    this.age = age;
}
Father.prototype.money = function(){
    console.log(1000+'元')
}
function Son(name,age,score){
    Father.call(this,uname,age);
    this.score = score;
}
Son.prototype = Father.prototype;
//這個是子類專有方法,父類不應該具備這個方法
Son.prototype.exam = function(){
    console.log('孩子要考試');
}
var son = new Son('劉德華',18,100);
console.log(son);
console.log(Father.prototype);
複製程式碼

我們再來看下son,看是否新增成功:

輕鬆理解建構函式和原型物件
我們看到子類的確具有了exam方法。 我們再來列印下父親的原型看看是怎麼樣的?

輕鬆理解建構函式和原型物件

可以看到父類上也多了一個exam方法,這顯然不是我們想看到的結果,那導致這個問題的原因是什麼呢?

輕鬆理解建構函式和原型物件

可以看到我們的父建構函式裡有一個原型物件, 子建構函式也有一個原型物件,都是自身的。

這句程式碼我們重點看下:

Son.prototype = Father.prototype;
複製程式碼

這句程式碼實際做了這麼一件事:

輕鬆理解建構函式和原型物件

把我們的子類的原型物件指向的父類的原型物件,就相當於把父類原型物件的地址給了孩子,那麼此時如果我們修改了子類的原型物件,就相當於同時修改了父類的原型物件,因為是引用關係,那麼這也就是為什麼會導致這個問題的原因。

所以如何解決呢?

我們可以這樣寫:

Son.prototype = new Father();
複製程式碼

new Father做了什麼事情呢,相當於例項化了一個父建構函式的物件,如圖所示:

輕鬆理解建構函式和原型物件

我們想想新建立的這個物件和我們Father的原型物件不是一個記憶體地址,因為物件都會新開闢一個記憶體空間,所以他們兩個不是同一個物件。

我們把例項化好的father賦值給了Son.prototype, 相當於這樣:

輕鬆理解建構函式和原型物件

father例項物件能訪問到Father的prototype嗎? 根據前面的知識點可以得到:肯定可以:

father的例項物件可以通過__proto__訪問Father的原型物件

輕鬆理解建構函式和原型物件

那在Father的原型物件裡有一個方法:money,

那father這個例項物件就可以使用money這個方法了,那這個Son的原型物件指向了father這個例項物件,所以我們這個Son也可以使用Father裡的這個money了,如圖所示:

輕鬆理解建構函式和原型物件

所以我們列印下Son,目前就繼承了money這個方法:

輕鬆理解建構函式和原型物件

我給孩子的原型物件加的考試方法會不會影響父親呢?

不會,因為現在每個物件都是獨立的,不會相互引用,所以是沒有這個問題存在的

還有最後一個問題,現在我們列印下孩子的constructor,會發現居然是Father這個建構函式

前面我們也說了, 如果利用物件的形式修改了原型物件,別忘了利用constructor指回原來的建構函式

只需要一句程式碼:

Son.prototype.constructor = Son;
複製程式碼

到此,我們一個組合繼承就寫完了,而且我們也明白了為什麼這麼寫,就這樣我們以後應該就能很清楚的明白他們之間的關係了。

總結

希望大家能在專案中多多使用,牢記於心!

如果大佬在文中發現了錯誤之處,請指正!

碼字不易,希望大家能舉起你的小手點個贊?

相關文章