本文爭取用最簡單的語言來講解原型鏈繼承的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; }
沒了