Javascript語言的繼承機制,它沒有"子類"和"父類"的概念,也沒有"類"(class)和"實例"(instance)的區分,全靠一種很奇特的"原型鏈"(prototype chain)模式,來實現繼承。html
這部分知識也是JavaScript裏的核心重點之一,同時也是一個難點。我把學習筆記整理了一下,方便你們學習,同時本身也加深印象。這部分代碼的細節不少,須要反覆推敲。那咱們就開始吧。chrome
原型鏈例子(要點寫在註釋裏,能夠把代碼複製到瀏覽器裏測試,下同)瀏覽器
function foo(){} //經過function foo(){}定義一個函數對象 foo.prototype.z = 3; //函數默認帶個prototype對象屬性 (typeof foo.prototype;//"object") var obj =new foo(); //咱們經過new foo()構造器的方式構造了一個新的對象 obj.y = 2; //經過賦值添加兩個屬性給obj obj.x = 1; //經過這種方式構造對象,對象的原型會指向構造函數的prototype屬性,也就是foo.prototype obj.x; // 1 //當訪問obj.x時,發現obj上有x屬性,因此返回1 obj.y; // 2 //當訪問obj.y時,發現obj上有y屬性,因此返回2
obj.z; // 3 //當訪問obj.z時,發現obj上沒有z屬性,那怎麼辦呢?它不會中止查找,它會查找它的原型,也就是foo.prototype,這時找到z了,因此返回3
//咱們用字面量建立的對象或者函數的默認prototype對象,實際上它也是有原型的,它的原型指向Object.prototype,而後Object.prototype也是有原型的,它的原型指向null。
//那這裏的Object.prototype有什麼做用呢?
typeof obj.toString; // ‘function'
//咱們發現typeof obj.toString是一個函數,可是無論在對象上仍是對象的原型上都沒有toString方法,由於在它原型鏈的末端null以前都有個Object.prototype方法,
//而toString正是Object.prototype上面的方法。這也解釋了爲何JS基本上全部對象都有toString方法
'z' in obj; // true //obj.z是從foo.prototype繼承而來的,因此'z' in obj返回了true
obj.hasOwnProperty('z'); // false //可是obj.hasOwnProperty('z')返回了false,表示z不是obj直接對象上的,而是對象的原型鏈上面的屬性。(hsaOwnProperty也是Object.prototype上的方法)
剛纔咱們訪問x,y和z,分別經過原型鏈去查找,咱們能夠知道:當咱們訪問對象的某屬性時,而該對象上沒有相應屬性時,那麼它會經過原型鏈向上查找,一直找到null尚未話,就會返回undefined。函數
function Foo(){ this.y = 2; } Foo.prototype.x = 1; var obj3 = new Foo(); //①當使用new去調用的時候,函數會做爲構造器去調用②this會指向一個對象(這裏是obj3),而這個對象的原型會指向構造器的prototype屬性(這裏是Foo.prototype)
obj3.y; //2
obj3.x; //1 //能夠看到y是對象上的,x是原型鏈上的原型(也就是Foo.prototype上)
咱們再來看看Foo.prototype是什麼樣的結構,當咱們用函數聲明去建立一個空函數的時候,那麼這個函數就有個prototype屬性,而且它默認有兩個屬性,constructor和__proto__,學習
constructor屬性會指向它自己Foo,__proto__是在chrome中暴露的(不是一個標準屬性,知道就行),那麼Foo.prototype的原型會指向Object.prototype。所以Object.prototype上測試
的一些方法toString,valueOf纔會被每一個通常的對象所使用。this
function Foo(){} typeof Foo.prototype; // "object"
Foo.prototype.x = 1; var obj3 = new Foo();
總結一下:咱們這裏有個Foo函數,這個函數有個prototype的對象屬性,它的做用就是當使用new Foo()去構造實例的時候,這個構造器的prototype屬性會用做new出來的這些對象的原型。spa
因此咱們要搞清楚,prototype和原型是兩回事,prototype是函數對象上的預設屬性,原型一般是構造器上的prototype屬性。prototype
function Person(name, age) { this.name = name; //直接調用的話,this指向全局對象(this知識點整理) this.age = age; //使用new調用Peoson的話,this會指向原型爲Person.prototype的空對象,經過this.name給空對象賦值,最後this做爲return值 } Person.prototype.hi = function() { //經過Person.prototype.hi建立全部Person實例共享的方法,(能夠參考上節的左圖:對象的原型會指向構造器的prototype屬性,因此想讓obj1,obj2,obj3共享一些方法的話,只需在原型對象上一次性地添加屬性和方法就能夠了); console.log('Hi, my name is ' + this.name + ',I am ' + this.age + ' years old now.')//這裏的this是全局對象 }; Person.prototype.LEGS_NUM = 2; //再設置一些對Person類的全部實例共享的數據 Person.prototype.ARMS_NUM = 2; Person.prototype.walk = function() { console.log(this.name + ' is walking...'); }; function Student(name, age, className) { //每一個學生都屬於人 Person.call(this, name, age); //在Student這個子類裏面先調用一下父類 this.className = className; }
//下一步就是咱們怎麼去把Student的實例繼承Person.prototype的一些方法
Student.prototype = Object.create(Person.prototype); //Object.create():建立一個空對象,而且這個對象的原型指向它的參數 //這樣子咱們能夠在訪問Student.prototype的時候能夠向上查找到Person.prototype,又能夠在不影響Person的狀況下,建立本身的方法 Student.prototype.constructor = Student; //保持一致性,不設置的話constructor會指向Person Student.prototype.hi = function() { //經過Student.prototype.hi這樣子的賦值能夠覆蓋咱們基類Person.prototype.hi console.log('Hi, my name is ' + this.name + ',I am ' + this.age + ' years old now, and from ' + this.className + '.'); } Student.prototype.learn = function(subject) { //同時,咱們又有本身的learn方法 console.log(this.name + 'is learning ' + subject + ' at' + this.className + '.'); }; //test var yun = new Student('Yunyun', 22, 'Class 3,Grade 2'); yun.hi(); //Hi,my name is Yunyun,I'm 22 years old now,and from Class 3, Grade 2. console.log(yun.ARMS_NUM); // 2 //咱們自己對象是沒有的,對象的原型也就是Student.prototype也沒有,可是咱們用了繼承,繼續向上查找,找到了Person.prototype.ARMS_NUM,因此返回2 yun.walk(); //Yunyun is walking... yun.learn('math'); //Yunyun is learning math at Class 3,Grade 2.
結合圖咱們來倒過來分析一下上面代碼:咱們先經過new Student建立了一個Student的實例yun,yun的原型指向構造器的prototype屬性(這裏就是Student.prototype), Student.prototype上有hi方法和learn方法,Student.prototype是經過Object.create(Person.prototype)構造的,因此這裏的Student.prototype是空對象,而且這個對象的原型指向Person.prototype,接着咱們在Person.prototype上也設置了LEGS_NUM,ARMS_NUM屬性以及hi,walk方法。而後咱們直接定義了一個Person函數,Person.prototype就是一個預置的對象,它自己也會有它的原型,它的原型就是Object.prototype,也正是由於這樣,咱們隨便一個對象纔會有hasOwnProperty,valueOf,toString這樣些公共的函數,這些函數都是從Object.prototype上來的。這樣子就實現了基於原型鏈的繼承。 那咱們調用hi,walk,learn方法的時候發生了什麼呢?好比咱們調用hi方法的時候,咱們首先看這個對象yun上有沒有hi方法,可是在這個實例中沒有因此會向上查找,查找到yun的原型也就是Student.protoype上有這hi方法,因此最終調用的是Student.prototype.hi,調用其餘方法也是相似的。code
咱們知道JavaScript中的prototype原型不像Java中的class,Java中的class一旦寫好就很難動態的去改變了,可是JavaScript中的原型實際上也是普通的對象,那就意味着在程序運行的階段,咱們也能夠動態的給prototype添加或刪除些屬性。
在上述代碼的基礎上,咱們已經有yun這個實例了,咱們接着來進行實驗:
Student.prototype.x = 101; //經過Student.prototype.x把yun的原型動態地添加一個屬性x yun.x; //101 //那咱們發現全部的實例都會受到影響 //接着咱們作個有趣的實驗 Student.prototype = {y:2}; //咱們直接修改構造器的prototype屬性,把它賦值爲一個新的對象 yun.y; //undefined yun.x; //101 //因此咱們得出:當咱們修改Student.prototype值的時候,並不能修改已經實例化的對象
var Tom = new Student('Tom',3,'Class LOL KengB');
Tom.x; //undefined //但當咱們建立一個新的實例時,這一次x就不見了,
Tom.y; //2 //而且y是新的值
因此說當動態修改prototype的時候,是會影響全部已建立或新建立的實例的,可是修改整個prototype賦值爲新的對象的話,對已建立的實例是不會影響的,可是會影響後續的實例。
實現繼承有多種方式,下面咱們仍是以Person和Student來分析
function Person() { } function Student() { } Student.prototype = Person.prototype; // 咱們可不可用這種方式呢?這種方法是錯誤的:由於子類Student有本身的一些方法
//,若是經過這樣子賦值,改變Student的同時也改變了Person。
Student.prototype = new Person(); //這種方式是能夠實現的,可是調用構造函數有時候也是有問題的,好比要傳進Person一個name和age
//,這裏的Student是個類,還沒實例化,這時候有些奇怪了,傳什麼都不是。
Student.prototype = Object.create(Person.prototype); //相對來講這中方式是比較理想的,這裏咱們建立了一個空的對象//,而且對象的原型指向Person.prototype,這樣咱們既保證了繼承了Person.prototype上的方法,而且Student.prototype又有本身空的對象。//可是Object.create是ES5之後纔有的