曾經看過不少關於原型的視頻和文章的你,是否仍是對原型雲裏霧裏,一頭霧水呢,今天讓咱們一塊兒揭開這層神祕的面紗吧~~~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一塊兒使用纔有意義
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對象上,這樣全部的對象的實例就能夠共享這些方法。
因此如今咱們就能夠把sing方法放到咱們的原型對象上
Star.prototype.sing = function(){
console.log('我會唱歌')
}
複製代碼
那麼如今咱們來思考下
1.原型是什麼?
原型其實就是一個對象
2.原型的做用是什麼?
共享屬性和方法
對象都會有一個屬性__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__指向的就是構造函數中的原型對象。
對象原型__proto__和構造函數(prototype)原型對象裏面都有一個屬性constructor屬性,constructor咱們稱爲構造函數,由於它指回構造函數自己
咱們這邊打印下star.__proto__和Star.prototype
打印結果以下圖
的確如咱們所說,它們都有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。
1.當訪問一個對象的屬性(包括方法)時,首先查找這個對象自身有沒有該屬性。
2.若是沒有就查找他的原型(也就是__proto__指向的prototype原型對象)
3.若是尚未就查找原型對象的原型
4.依次類推一直找到Object爲止(null)
咱們來看看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繼承的語法糖,因此咱們得經過構造函數+原型對象模擬實現繼承,這種方式被稱爲組合繼承
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;
複製代碼
到此,咱們一個組合繼承就寫完了,並且咱們也明白了爲何這麼寫,就這樣咱們之後應該就能很清楚的明白他們之間的關係了。
但願你們能在項目中多多使用,牢記於心!
若是大佬在文中發現了錯誤之處,請指正!
碼字不易,但願你們能舉起你的小手點個贊👍