在JavaScript中,對象的建立能夠脫離類型(class free),經過字面量的方式能夠很方便的建立出自定義對象。函數
另外,JavaScript中擁有原型這個強大的概念,當對象進行屬性查找的時候,若是對象自己內找不到對應的屬性,就會去搜索原型鏈。因此,結合原型和原型鏈的這個特性,JavaScript就能夠用來實現對象之間的繼承了。this
下面就介紹一下JavaScript中的一些經常使用的繼承方式。spa
因爲原型鏈搜索的這個特性,在JavaScript中能夠很方便的經過原型鏈來實現對象之間的繼承。prototype
下面看一個例子:code
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(staffId){ this.staffId = staffId; } Teacher.prototype = new Person(); var will = new Teacher(1000); will.name = "Will"; will.age = 28; will.getInfo(); // Will is 28 years old! console.log(will instanceof Object); // true console.log(will instanceof Person); // true console.log(will instanceof Teacher); // true console.log(Object.prototype.isPrototypeOf(will)); // true console.log(Person.prototype.isPrototypeOf(will)); // true console.log(Teacher.prototype.isPrototypeOf(will)); // true
在這個例子中,有兩個構造函數"Person"和"Teacher",經過"Teacher.prototype = new Person()"語句建立了一個"Person"對象,而且設置爲"Teacher"的原型。對象
經過這種方式,就實現了"Teacher"繼承"Person","will"這個對象能夠成功的調用"getInfo"這個屬於"Person"的方法。blog
在這個例子中,還演示了經過"instanceof"操做符和"isPrototypeOf()"方法來查看對象和原型之間的關係。繼承
對於原型鏈繼承,下面看看其中的一些細節問題。ip
對於全部的JavaScript原型對象,都有一個"constructor"屬性,該屬性對應用來建立對象的構造函數。ci
對於"constructor"這個屬性,最大的做用就是能夠幫咱們標明一個對象的"類型"。
在JavaScript中,當經過"typeof"查看Array對象的時候,返回的結果是"object"。這個咱們的預期結果,因此若是要判對一個對象究竟是不是Array類型,就能夠結合"constructor"屬性獲得想要的結果。
function isArray(myArray) { return myArray.constructor.toString().indexOf("Array") > -1; } var arr = [] console.log(typeof arr); // object console.log(isArray(arr)); // true
如今回到前面的例子,查看一下對象"will"的原型和構造函數:
從這個結果能夠看到,"will"的原型是"Person {name: undefined, age: undefined}"(經過new Person()構造出來的對象),"will"的構造函數是"function Person"。
等等,"will"不是經過"Teacher"建立出來的對象麼?爲何構造函數對於的是"function Person",而不是"function Teacher"?
下面,根據前面的例子繪製一張對象關係圖,從而分析一下繼承關係以及"constructor"屬性:
圖中給出了各類對象之間的關係,有幾點須要注意的是:
爲了解決上面的問題,讓子類對象的"constructor"屬性對應正確的構造函數,咱們能夠重設子類原型對象的"constructor"屬性。
通常來講,能夠簡單的使用下面代碼來重設"constructor"屬性:
Teacher.prototype.constructor = Teacher;
可是經過這種方式重設"constructor"屬性會致使它的[[Enumerable]]特性被設置爲 true。默認狀況下,原生的"constructor"屬性是不可枚舉的。
所以若是使用兼容 ECMAScript 5 的 JavaScript 引擎,就可使用"Object.defineProperty()":
Object.defineProperty(Teacher.prototype, "constructor", { enumerable: false, value: Teacher });
經過下面的結果能夠看到:
經過這個設置,對象"will" 的"constructor"屬性就指向了正確的"function Teacher"。
這時的對象關係圖就變成了以下,跟前面的關係圖比較,惟一的區別就是"Teacher.prototype"對象多了一個"constructor"屬性,而且這個屬性指向"function Teacher":
原型對象是能夠修改的,因此,當建立了繼承關係以後,咱們能夠經過更新子類的原型對象給子類添加特有的方法。
例如經過下面的方式就給子類添加了一個特有的"getId"方法。
Teacher.prototype.getId = function(){ console.log(this.name + "'s staff Id is " + this.staffId); } will.getId(); // Will's staff Id is 1000
可是,必定要區分原型的修改和原型的重寫。若是對原型進行了重寫,就會產生徹底不一樣的效果。
下面看看若是對"Teacher"的原型重寫會產生什麼效果,爲了分清跟前面代碼的順序,這裏貼出了完整的代碼:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(staffId){ this.staffId = staffId; } Teacher.prototype = new Person(); Object.defineProperty(Teacher.prototype, "constructor", { enumerable: false, value: Teacher }); var will = new Teacher(1000); will.name = "Will"; will.age = 28;
// 更新原型 Teacher.prototype.getId = function(){ console.log(this.name + "'s staff Id is " + this.staffId); } will.getId(); // Will's staff Id is 1000
// 重寫原型 Teacher.prototype = { getStaffId: function(){ console.log(this.name + "'s staff Id is " + this.staffId); } } will.getInfo(); // Will is 28 years old! will.getId(); // Will's staff Id is 1000 console.log(will.__proto__); // Person {name: undefined, age: undefined} console.log(will.__proto__.constructor); // function Teacher var wilber = new Teacher(1001); wilber.name = "Wilber"; wilber.age = 28; // wilber.getInfo(); // Uncaught TypeError: wilber.getInfo is not a function(…) wilber.getStaffId(); // Wilber's staff Id is 1001 console.log(wilber.__proto__); // Object {} console.log(wilber.__proto__.constructor); // function Object() { [native code] }
通過重寫原型以後狀況更加複雜了,下面就來看看重寫原型以後的對象關係圖:
從關係圖能夠看到:
在經過原型鏈方式實現的繼承中,父類和子類的構造函數相對獨立,若是子類構造函數能夠調用父類的構造函數,而且進行相關的初始化,那就比較好了。
這時就想到了JavaScript中的call方法,經過這個方法能夠動態的設置this的指向,這樣就能夠在子類的構造函數中調用父類的構造函數了。
這樣就有了組合繼承這種方式:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(name, age, staffId){ Person.call(this, name, age); // 經過call方法來調用父類的構造函數進行初始化 this.staffId = staffId; } Teacher.prototype = new Person(); Object.defineProperty(Teacher.prototype, "constructor", { enumerable: false, value: Teacher }); var will = new Teacher("Will", 28, 1000); will.getInfo(); console.log(will.__proto__); // Person {name: undefined, age: undefined} console.log(will.__proto__.constructor); // function Teacher
在這個例子中,在子類構造函數"Teacher"中,直接經過"Person.call(this, name, age);"的方式調用了父類的構造函數,進而設置了"name"和"age"屬性(但這裏依舊是覆蓋了父類的"name"和"age"屬性)。
組合式繼承是比較經常使用的一種繼承方法,其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數複用,又保證每一個實例都有它本身的屬性。
雖然組合繼承是 JavaScript 比較經常使用的繼承模式,不過經過前面組合繼承的代碼能夠看到,它也有一些小問題。
首先,子類會調用兩次父類的構造函數:
子類型最終會包含超類型對象的所有實例屬性,但咱們不得不在調用子類型構造函數時重寫這些屬性,從下圖能夠看到"will"對象中有兩份"name"和"age"屬性。
後面,咱們會看到如何經過"寄生組合式繼承"來解決組合繼承的這個問題。
在前面兩種方式中,都須要用到對象以及建立對象的構造函數(類型)來實現繼承。
可是在JavaScript中,建立對象徹底不須要定義一個構造函數(類型),經過字面量的方式就能夠建立一個自定義的對象。
爲了實現對象之間的直接繼承,就有了原型式繼承。
這種繼承方式方法並無使用嚴格意義上的構造函數,而是直接藉助原型基於已有的對象建立新對象,同時還沒必要建立自定義類型(構造函數)。爲了達到這個目的,咱們能夠藉助下面這個函數:
function object(o){ function F(){} F.prototype = o; return new F(); }
在 "object()"函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例。
下面看看使用"object()"函數實現的對象之間的繼承:
var utilsLibA = { add: function(){ console.log("add method from utilsLibA"); }, sub: function(){ console.log("sub method from utilsLibA"); } } var utilsLibB = object(utilsLibA); utilsLibB.add = function(){ console.log("add method from utilsLibB"); } utilsLibB.div = function(){ console.log("div method from utilsLibB"); } utilsLibB.add(); // add method from utilsLibB utilsLibB.sub(); // sub method from utilsLibA utilsLibB.div(); // div method from utilsLibB
經過原型式繼承,基於"utilsLibA"建立了一個"utilsLibB"對象,而且能夠正常工做,下面看看對象之間的關係:
經過"object()"函數的幫助,將"utilsLibB"的原型賦值爲"utilsLibA",對於這個原型式繼承的例子,對象關係圖以下,"utilsLibB"的"add"方法覆蓋了"utilsLibA"的"add"方法:
ECMAScript 5 經過新增 "Object.create()"方法規範化了原型式繼承。這個方法接收兩個參數:
在傳入一個參數的狀況下,"Object.create()"與 上面的"object"函數行爲相同。關於更多"Object.create()"的內容,請參考MDN。
繼續上面的例子,此次使用"Object.create()"來建立對象"utilsLibC":
utilsLibC = Object.create(utilsLibA, { sub: { value: function(){ console.log("sub method from utilsLibC"); } }, mult: { value: function(){ console.log("mult method from utilsLibC"); } }, }) utilsLibC.add(); // add method from utilsLibA utilsLibC.sub(); // sub method from utilsLibC utilsLibC.mult(); // mult method from utilsLibC console.log(utilsLibC.__proto__); // Object {add: (), sub: (), __proto__: Object} console.log(utilsLibC.__proto__.constructor); // function Object() { [native code] }
寄生式繼承是與原型式繼承緊密相關的一種思路,寄生式繼承的思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。
如下代碼示範了寄生式繼承模式,其實就是封裝"object()"函數的調用,以及對新的對象進行自定義的一些操做:
function create(o){ var f= object(o); // 經過原型式繼承建立一個新對象 f.run = function () { // 以某種方式來加強這個對象 return this.arr; }; return f; // 返回對象 }
所謂寄生組合式繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。
其背後的基本思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所須要的無非就是父類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承父類型的原型,而後再將結果指定給子類型的原型。
注意在寄生組合式繼承中使用的「inheritPrototype()」函數。
function object(o) { function F() {} F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); // 建立對象 prototype.constructor = subType; // 加強對象,設置constructor屬性 subType.prototype = prototype; // 指定對象 } function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(name, age, staffId){ Person.call(this, name, age) this.staffId = staffId; } inheritPrototype(Teacher, Person); Teacher.prototype.getId = function(){ console.log(this.name + "'s staff Id is " + this.staffId); } var will = new Teacher("Will", 28, 1000); will.getInfo(); // Will is 28 years old! will.getId(); // Will's staff Id is 1000 var wilber = new Teacher("Wilber", 29, 1001); wilber.getInfo(); // Wilber is 29 years old! wilber.getId(); // Wilber's staff Id is 1001
代碼中有一處地方須要注意,給子類添加"getId"方法的代碼("Teacher.prototype.getId")必定要放在"inheritPrototype()"函數調用以後,由於在「inheritPrototype()」函數中會重寫「Teacher」的原型。
下面繼續查看一下對象"will"的原型和"constructor"屬性。
這個示例中的" inheritPrototype()"函數實現了寄生組合式繼承的最簡單形式。這個函數接收兩個參數:子類型構造函數和父類型構造函數。
在函數內部,第一步是建立超類型原型的一個副本。第二步是爲建立的副本添加 "constructor" 屬性,從而彌補因重寫原型而失去的默認的 "constructor" 屬性。最後一步,將新建立的對象(即副本)賦值給子類型的原型。這樣,咱們就能夠用調用 "inheritPrototype()"函數的語句,去替換前面例子中爲子類型原型賦值的語句了("Teacher.prototype = new Person();")。
對於這個寄生組合式繼承的例子,對象關係圖以下:
本文介紹了JavaScirpt中的 幾種經常使用繼承方式,咱們能夠經過構造函數實現繼承,也能夠直接基於現有的對象來實現繼承。
不管哪一種繼承的實現,本質上都是經過JavaScript中的原型特性,結合原型鏈的搜索實現繼承。
與其說"JavaScript是一種面向對象的語言",更恰當的能夠說"JavaScript是一種基於對象的語言"。
經過了這些介紹,相信你必定對JavaScript的繼承有了一個比較清楚的認識了。