js原型鏈繼承的傻瓜式詳解


本文爭取用最簡單的語言來講解原型鏈繼承的OOP原理

 

0.如果對原型繼承還沒有大致了解,完全一頭霧水,請先閱讀

《JavaScript高級程序設計》第六章最后部分的寄生組合式繼承

或者_廖雪峰js教程里面面向對象部分的原型承部分https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/0014344997013405abfb7f0e1904a04ba6898a384b1e925000

 

1.類,構造函數和實例

如果對函數使用 new,那么會默認返回一個對象,這個對象是該函數的實例。

var x = new Object();

此時x是一個Object的實例

 

假如我們有兩種構造函數

//情況A
function Person(name,age){
    this.name = name;
    this.age = age;

    this.sayHi = function (){
        alert("Hi,I'm "  + this.name);
    }
}

var wang = new Person('老王',30);
var li = new Person('老李',22);

wang.sayHi == li.sayHi //false


//情況B

function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayHi = function (){
    alert("Hi,I'm "  + this.name);
}

var wang = new Person('老王',30);
var li = new Person('老李',22);

wang.sayHi == li.sayHi //true

wang,li是兩個Person類的實例,Person是他們的構造函數。

第一種情況A,方法被重復定義,各實例的方法實際上是被分別定義了一遍。第二種情況B,實現了類的方法的復用。

顯然,第一種方法里面,sayHi函數是各實例自有的屬性,第二種方法,sayHi被定義在了Person的prototype上面。

 

要點1:當實例對象本身不存在某個屬性時,js會查找該實例的【構造函數】的【prototype】屬性

 

2.如何繼承

問題來了,如果定義一個子類,比如Student,如何繼承Person類。

按照廖雪峰老師和書上比較推薦的方法

function Student(name, age, grade){
    Person.call(this, name, age);
    this.grade = grade;
}


//--------
function F(){};
F.prototype = Person.prototype;
Student.prototype = new F();
Student.prototype.constructor = Student;
//--------

Student.prototype.sayHello =function(){
        alert("Hello, I'm "+ this.name +", I'm in grade " + this.grade + ", nice to meet you.");
    };

var wang_son = new Student('老王兒子',8,'二年級');
wang_son.sayHi();//Hi,I'm 老王兒子
wang_son.sayHello();//Hello, I'm 老王兒子, I'm in grade 二年級, nice to meet you.
wang_son.sayHi == wang.sayHi; //結果是true

結果來看確實比較良好的實現了繼承,可是中間那四行是什么玩意,那個F又是什么?好像除了中間這四行,其它的都挺容易懂。簡單的四行代碼,把人繞的雲里霧里的。

道格拉斯·克羅克福德在 2006年寫了一篇文章,題為 Prototypal Inheritance in JavaScript (JavaScript中的原型式繼承)。在這篇文章中,他介紹了這種實現繼承的方法

如果不是很熟悉js的細節,這個方法看起來多少有點耍雜技。乍一看很繞,理解之后會發現它的精妙之處。

 

先來看一些假設:如果不這么搞行不行?

不就是繼承父類的方法嗎,直接Student.prototype = Person.prototype不就得了,

結果是 wang_son.sayHi()返回結果正確,但是父類wang有了兒子用的sayHello()了,這明顯不對。

如果wang_son的sayHi需要重寫成alert("叔叔好"),那么老王跟你打招呼也會叫叔叔好,這下徹底亂了。

 

不就是不重疊不覆蓋嗎,我把父類的prototype里面有的東西淺復制一份,把那四行代碼替換成

Object.getOwnPropertyNames(Person.prototype).forEach(function(key){
    Student.prototype[key]=Person.prototype[key]
});

看似沒什么問題,改子類代碼不會影響父類,但是在后續代碼中,如果修改或者添加父類的方法,子類是不會隨着變化的。二者的狀態只是在復制的一瞬間進行了同步。

 

那。。。試試把他倆連起來呢? Student.prototype.prototype = Person.prototype  這不就是標准的繼承了嗎

想得美!!

 

要點2:只有函數才有prototype屬性。普通實例對象沒有!!!

從要點1中得知,訪問實例對象沒有的屬性,會向上追溯,去查該對象的【構造函數的prototype】。

那問題來了,prototype里面也沒有怎么辦?查prototype的【構造函數的prototype】啊。畢竟prototype也是個實例對象啊。

還是上面那些代碼,使用typeof Person.prototype 查看類型,返回的是"Object" 也就是說Person.prototype本身是一個Object的實例。

Person.valueOf();
//顯示結果
//ƒ Person(name,age){
//    this.name = name;
//    this.age = age;
//}
Person.valueOf == Object.prototype.valueOf  //返回true

你看Person一路向上查,順利的調用到了Object的prototype的valueOf方法

那么問題就好辦了,如果要Student類要繼承Person,或者說,  Student.prototype里面查不到,能繼續沿着這個鏈條往上查 Person.prototype,最簡單的辦法是什么?

Student.prototype 是 Person 構造出來的就好了。 就這么簡單。。。。。。。

你看看最開始那段代碼,有個叫老李的是吧,就是li,他是用Person構造出來的,就先讓他當老王兒子的干爹吧( ̄︶ ̄)↗

把中間那四行代碼替換一下

function Person(name,age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayHi = function (){
    alert("Hi,I'm "  + this.name);
}

var wang = new Person('老王',30);
var li = new Person('老李',22);


function Student(name, age, grade){
    Person.call(this, name, age);
    this.grade = grade;
}


//--------下面四行注釋掉
//function F(){};
//F.prototype = Person.prototype;
//Student.prototype = new F();
//Student.prototype.constructor = Student;

Student.prototype = li;
//--------

Student.prototype.sayHello =function(){
        alert("Hello, I'm "+ this.name +", I'm in grade " + this.grade + ", nice to meet you.");
    };

var wang_son = new Student('老王兒子',8,'二年級');
wang_son.sayHi();//Hi,I'm 老王兒子
wang_son.sayHello();//Hello, I'm 老王兒子, I'm in grade 二年級, nice to meet you.
wang_son.sayHi == wang.sayHi; //結果是true

然而最神奇的是這么干竟然成了,這叫啥?干爹繼承法?

有點反直覺吧。控制台里面直接打li看輸出,干爹老李背負了兒子輩的sayHello方法。

Person {name: "老李", age: 22, sayHello: ƒ}
age: 22
name: "老李"
sayHello: ƒ ()
__proto__: Object

其實沒必要非得老李,現場臨時生成一個干爹就成了,把老李那句改成。

Student.prototype = new Person();

一樣的效果。

 

問題來了,繼承問題好像完美解決了,一句話能搞定,干嘛寫四句。原因是這么干還不完美。

主要問題

1.Student.prototype.constructor 應該指向該類的構造函數,既Student。如果只找個干爹,不做處理,那么會沿着原型鏈向上查找,一直找到有這個屬性的prototype為止。

上面的代碼構造出來的實例,wang_son.constructor返回的結果是Person。對應這個問題,一句話修正掉,這里不難理解。

Student.prototype.constructor = Student;

2.wang_son構造函數里面調用了父類Person的構造函數來初始化自己的屬性name,age,而此時li里面也有老李的name,age。雖然老李是老王兒子構造函數的prototype,但是老王兒子自己有name,age屬性了,wang_son是訪問不到老李的屬性的。同理,如果是new Person()構造一個實例充當prototype,里面也有name和age,只不過值是undefined,也是死活訪問不到的。可見,我們對這個干爹的屬性是毫不關心的。

這些屬性鐵定會被子類實例自己的屬性覆蓋掉,留着沒用,那干脆就不要了,同時也省內存開銷。

只要這個干爹能引導我們找到Person的prototype就行,其它無所謂。於是有了下面的代碼

function F(){};
F.prototype = Person.prototype;
Student.prototype = new F();

把構造函數弄成空的,prototype設成父類的,這樣一來生成的中間實例里面干干凈凈,並沒有多余的屬性。而且繼承了父類的原型鏈。

 

看吧,很簡單吧。

把這四句單獨打包成函數,往后就可以隨便繼承了

function inherits(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

沒了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM