前言
曾經看過很多關於原型的視訊和文章的你,是否還是對原型雲裡霧裡,一頭霧水呢,今天讓我們一起揭開這層神祕的面紗吧~~~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;
複製程式碼
到此,我們一個組合繼承就寫完了,而且我們也明白了為什麼這麼寫,就這樣我們以後應該就能很清楚的明白他們之間的關係了。
總結
希望大家能在專案中多多使用,牢記於心!
如果大佬在文中發現了錯誤之處,請指正!
碼字不易,希望大家能舉起你的小手點個贊?