輕鬆理解構造函數和原型對象

前言

曾經看過不少關於原型的視頻和文章的你,是否仍是對原型雲裏霧裏,一頭霧水呢,今天讓咱們一塊兒揭開這層神祕的面紗吧~~~go go go!javascript

利用構造函數建立對象

在ES6以前,對象不是基於類建立的,而是用一種稱爲構造函數的特殊函數來定義對象和它們的特徵java

建立對象能夠經過如下三種方式

1.對象字面量es6

2.new Object()函數

3.自定義構造函數ui

這裏咱們着重來看下怎麼利用構造函數建立對象, 咱們把對象的公共屬性放在構造函數中this

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();
複製代碼

這樣咱們就生成了兩個獨立的對象spa

構造函數的定義

構造函數是一種特殊的函數,主要用來初始化對象,他老是與new一塊兒使用,咱們能夠把對象中的一些公共屬性和方法抽取出來,而後封裝到這個函數裏。prototype

在JS中,使用構造函數時須要注意如下兩點:3d

1.構造函數用於建立某一類對象,其首字母要大寫code

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;
複製代碼

到此,咱們一個組合繼承就寫完了,並且咱們也明白了爲何這麼寫,就這樣咱們之後應該就能很清楚的明白他們之間的關係了。

總結

但願你們能在項目中多多使用,牢記於心!

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

碼字不易,但願你們能舉起你的小手點個贊👍

相關文章
相關標籤/搜索