完全搞懂JavaScript中的繼承

上一片文章,給你們分享了對象的建立和函數的建立及優缺點。還不知道的小夥伴能夠去面向對象,搞定對象這節先了解一下,回過頭再來看這篇文章。javascript

概念

什麼是原型鏈以及書中介紹了好多種繼承方法,優缺點是什麼!java

什麼是原型鏈

當談到繼承時,JavaScript 只有一種結構:對象。每一個實例對象( object )都有一個私有屬性(稱之爲 proto )指向它的構造函數的原型對象(prototype )。該原型對象也有一個本身的原型對象( proto ) ,層層向上直到一個對象的原型對象爲null。根據定義,null沒有原型,並做爲這個原型鏈中的最後一個環節。---來着MDNapp

這裏是官方給出的解釋,咱們用個例子,來具體的去看看這個原型鏈。在舉例以前,咱們先來了解一下,原型和實例的關係。函數

每一個構造函數(constructor)都有一個原型對象(prototype),原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。post

上面的這段還若是你還沒理解,我仍是建議你去看面向對象,搞定對象,這裏已經給你詳細的解釋了。這裏就很少說了。
那咱們想一下,若是讓一個函數的實例對象,指向函數的原型對象,會發生什麼?ui

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屬性,都經歷了什麼呢?this

  1. 先在instance中尋找,沒找到
  2. 接着去instance.proto(Son.prototype)中尋找,也是Father的實例中尋找。又沒找到。
  3. 去Father.prototype中尋找,找到了,返回結果。假如還沒找到。
  4. 去object.prototype中尋找,假如還沒找到。
  5. 返回undefined.

instance --> new Father() --> Father.prototype --> object.prototype --> undefined 這就是你日思夜想不得解的原型鏈啊!是否是很好理解了。
實例與原型鏈接起來的鏈條,叫作原型鏈(我本身定義的)。 spa

原型鏈繼承問題

上面的例子,就是原型鏈繼承的標準例子。可是原型鏈繼承,是有問題的。高級程序設計上說,他有兩個問題:prototype

  • 當原型鏈中包含引用類型值的原型時,該引用類型值會被全部實例共享;
  • 在建立子類型(例如建立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傳了參。完美解決傳參問題。

借用構造函數的問題

  1. 全部的方法都得定義在構造函數上,沒法實現複用。
  2. 超類型中的方法,對於子類型是不可見的。

如何解決這兩個問題呢?引出咱們下一個繼承方法---組合繼承。

組合繼承

組合繼承也叫作僞經典繼承,指的是將原型鏈和借用構造函數的技術組合在一塊兒,從而發揮兩者之長的繼承模式

話很少說,上代碼!

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

這個函數,接收兩個參數,一個子類型,一個超類型。在函數內部,

  1. 建立超類型的一個副本。
  2. 爲副本添加constructor屬性,你補重寫prototype屬性形成的constructor屬性丟失問題。
  3. 將副本賦值給子類型的原型。
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);
}
複製代碼

這個例子,只調用了一次超類型構造函數,避免了在子類型上建立沒必要要的屬性和方法。是最理想的繼承方式。 可是,我可能不會用。。。你會用嗎?

相關文章
相關標籤/搜索