上一片文章,給大家分享了物件的建立和函式的建立及優缺點。還不知道的小夥伴可以去物件導向,搞定物件這節先了解一下,回過頭再來看這篇文章。
概念
什麼是原型鏈以及書中介紹了好多種繼承方法,優缺點是什麼!
什麼是原型鏈
當談到繼承時,JavaScript 只有一種結構:物件。每個例項物件( object )都有一個私有屬性(稱之為 proto )指向它的建構函式的原型物件(prototype )。該原型物件也有一個自己的原型物件( proto ) ,層層向上直到一個物件的原型物件為null。根據定義,null沒有原型,並作為這個原型鏈中的最後一個環節。---來著MDN
這裡是官方給出的解釋,我們用個例子,來具體的去看看這個原型鏈。在舉例之前,我們先來了解一下,原型和例項的關係。
每個建構函式(constructor)都有一個原型物件(prototype),原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。
上面的這段還如果你還沒理解,我還是建議你去看物件導向,搞定物件,這裡已經給你詳細的解釋了。這裡就不多說了。
那我們想一下,如果讓一個函式的例項物件,指向函式的原型物件,會發生什麼?
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,導致Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true
複製程式碼
instance例項通過原型鏈找到了Father原型中的getFatherValue方法. 為了找到getFatherValue屬性,都經歷了什麼呢?
- 先在instance中尋找,沒找到
- 接著去instance.proto(Son.prototype)中尋找,也是Father的例項中尋找。又沒找到。
- 去Father.prototype中尋找,找到了,返回結果。假如還沒找到。
- 去object.prototype中尋找,假如還沒找到。
- 返回undefined.
instance --> new Father() --> Father.prototype --> object.prototype --> undefined
這就是你日思夜想不得解的原型鏈啊!是不是很好理解了。
例項與原型連線起來的鏈條,叫做原型鏈(我自己定義的)。
原型鏈繼承問題
上面的例子,就是原型鏈繼承的標準例子。但是原型鏈繼承,是有問題的。高階程式設計上說,他有兩個問題:
- 當原型鏈中包含引用型別值的原型時,該引用型別值會被所有例項共享;
- 在建立子型別(例如建立Son的例項)時,不能向超型別(例如Father)的建構函式中傳遞引數.
是有這兩個問題,但我說也不一定,當你原型鏈不需要飲用型別,建立的子型別不需要傳參!那就不存在問題啊!哈哈哈
別鬧,畢竟這樣的需求很少,甚至根本不可能有,還是老實兒解決上面的問題吧。
借用建構函式
原理:在子型別建構函式中呼叫超型別建構函式。
原型鏈繼承的問題
先來看看原型鏈函式的問題:
function Father (){
this.colors = ['red','green','blue'];
}
function Son (){
}
Son.prototype = new Father();
let instance1 = new Son();
instance1.colors.push('white');
console.log(instance1.colors);
let instance2 = new Son();
console.log(instance2.colors);
複製程式碼
執行之後,就會發現,返回的結果是一樣的,也就是說,所有的例項都會共享colors這個屬性。這並不是不我們想要的結果。
原型鏈繼承的問題的解決辦法
在子型別建構函式中呼叫超型別建構函式。
function Father(name){
this.colors = ['red','green','blue'];
}
function Son(name){
Father.call(this,name);
}
let instance1 = new Son();
instance1.colors.push('white');
console.log(instance1.colors); // ['red','green','blue','white'];
let instance2 = new Son();
console.log(instance2.colors); // ['red','green','blue'];
複製程式碼
請記住,函式只是在特定環境中執行的程式碼的物件,因此可以通過call()或apply()方法也可以在新建立的物件上執行建構函式。
這段話很好理解:誰幹(調)的,誰負責。
結合上面的程式碼,instance1呼叫的colors屬性,那就你instance1物件負責,我instance2沒做任何事,我不負責。
再來看傳參問題
function Father(name){
this.colors = ['red','green','blue'];
this.name = name; //新增code
}
function Son(name){
Father.call(this,name); //將name,傳遞給Father。
}
let instance1 = new Son('hanson'); //建立例項物件時,傳入引數
instance1.colors.push('white');
console.log(instance1.colors); // ['red','green','blue','white'];
console.log(instance1.name); // 'hanson'
let instance2 = new Son();
console.log(instance2.colors); // ['red','green','blue'];
複製程式碼
很好理解嘛,通過建構函式Son,我們給Father傳了參。完美解決傳參問題。
借用建構函式的問題
- 所有的方法都得定義在建構函式上,無法實現複用。
- 超型別中的方法,對於子型別是不可見的。
如何解決這兩個問題呢?引出我們下一個繼承方法---組合繼承。
組合繼承
組合繼承也叫做偽經典繼承,指的是將原型鏈和借用建構函式的技術組合在一起,從而發揮二者之長的繼承模式
話不多說,上程式碼!
function Father(name){
this.colors = ['red','green','blue'];
this.name = name;
}
Father.prototype.sayName = function(){
console.log(this.name)
}
function Son(name,age){
Father.call(this,name); //將name,傳遞給Father。
this.age = age;
}
Son.prototype = new Father();
Son.prototype.sayAge = function(){
console.log(this.age);
}
let instance1 = new Son('hanson',18);//建立例項物件時,傳入引數
Son.prototype.constructor = Son;
instance1.colors.push('white');
console.log(instance1.colors); // ['red','green','blue','white'];
console.log(instance1.sayAge); // 18
let instance2 = new Son('grey',20);
console.log(instance2.colors); // ['red','green','blue'];
console.log(instance2.sayName()); // 'grey'
console.log(instance2.sayAge()); //18
複製程式碼
總結:分別擁有自己的屬性,還享有公共的方法,真好。
不靠譜的原型式繼承和寄生式繼承
這兩個繼承方式,都是一個叫克羅克德的人提出的,我們也不知道,我們也不敢問,估計式為了後面的組合式寄生繼承做鋪墊?
//原型式繼承
var person = {
name:'hanson',
friends:['sha','feng','qiang']
}
var another = Object(person); //複製一份
another.name = 'bo';
another.friends.push('lei');
console.log(another.friends); //['sha','feng','qiang','lei']
console.log(another.name); //'bo'
console.log(person.friends); //['sha','feng','qiang','lei']
複製程式碼
相比上面的方法,這個要簡便的多,沒用到建構函式,原型鏈。但問題也十分明顯,汙染引用屬性。
//寄生式繼承---也是克羅克德提出來的
function creat(obj) {
var clone = Object(obj);
clone.sayName = function(){
console.log('hanson')
}
return clone;
}
var person = {
age:18,
friends:['qiang','sha','feng']
}
var another = creat(person);
console.log(another.sayName());
console.log(person.sayName());
複製程式碼
其實道理沒變,clone了一份物件,但是,同樣,peron物件也被玷汙了!不信,你列印一下,person也有了sayName()方法。犧牲太大,反正我不用~
最後一個,寄生組合式繼承
在講解之前呢,我們先來看看,組合繼承的缺點。
function Father(name){
this.name = name;
this,friends = ['qiang','sha','feng'] ;
}
Father.prototype.constructor = function(){
console.log(this.name);
}
function Son(name,age){
Father.call(this); //第二次呼叫
}
Son.prototype = new Father(); //第一次呼叫
Son.prototype.constructor = Son;
Son.prototype.sayName = function(){
console.log(this.name);
}
複製程式碼
我們第一次呼叫超型別建構函式(Father),無非是想指定子型別的原型,讓他們直接建立聯絡而已。 給他個副本,又如何!
function inheritPrototype(Son,Father){
var prototype = Object(Father.prototype);
prototype.constructor = Son;
Son.prototype = prototype;
}
複製程式碼
這個函式,接收兩個引數,一個子型別,一個超型別。在函式內部,
- 建立超型別的一個副本。
- 為副本新增constructor屬性,你補重寫prototype屬性造成的constructor屬性丟失問題。
- 將副本賦值給子型別的原型。
function Father(name){
this.name = name;
this,friends = ['qiang','sha','feng'] ;
}
Father.prototype.constructor = function(){
console.log(this.name);
}
function Son(name,age){
Father.call(this); //第二次呼叫
}
inheritPrototype(Son,Father);
Son.prototype.sayName = function(){
console.log(this.name);
}
複製程式碼
這個例子,只呼叫了一次超型別建構函式,避免了在子型別上建立不必要的屬性和方法。是最理想的繼承方式。 但是,我可能不會用。。。你會用嗎?