由於javascript沒有類的概念,因此無法通過接口繼承,只能通過實現繼承。實現繼承是繼承實際的方法,javascript中主要是依靠原型鏈要實現。
原型鏈繼承
原型鏈繼承是基本的繼承模式,其本質是重寫原型對象,使其為新對象的實例。代碼實現如下:
function Person(){ this.name = "default"; var temp = "temp"; } Person.prototype.age=0; Person.prototype.getName = function(){ return this.name; } Person.prototype.getAge = function(){ return this.age; } console.log(Person.prototype.age);//0 console.log(Person.age);//undefined console.log(Person.prototype.name);//undefined console.log(Person.name);//Person, if other property, should be undefined function Student(){ this.type = "student"; } //inheritance Student.prototype = new Person(); console.log(Student.prototype.constructor);//Person(){} console.log(Student.prototype.name);//default Student.prototype.constructor = Student; var student1 = new Student(); console.log(student1.getName());//default console.log(student1.name);//default console.log(student1.getAge());//0 console.log(student1.age);//0 console.log(student1.__proto__.age);//0 console.log(student1.temp);//undefined console.log(student1 instanceof Object);//true console.log(student1 instanceof Person);//true console.log(student1 instanceof Student);//true console.log(Student instanceof Person);//false
以上代碼主要注意兩個問題:
1.函數局部變量,內部屬性及原型屬性的區別。var temp定義了一個局部變量,this.name定義了一個內部屬性,prototype.age則定義了一個原型屬性。
對於局部變量,無法在函數以外的地方調用,包括實例。
之前說過,函數本身的prototype屬性僅僅用於函數實例的屬性繼承,而函數本身不會使用這個關聯的prototype,在prototype中設置的屬性將直接作用於所有實例。(比如Person的實例Student.prototype和student1,注意Student並不是Person的實例)
而對於函數內部屬性,函數實例將直接擁有對應的內部屬性(初始值),而無法通過函數本身使用內部屬性。這一點其實跟prototype屬性有所區別。
2.利用重寫原型對象實現繼承的時候,Student.prototype = new Person(), Student.prototype將指向了另一個對象Person.prototype,因此此時Student.prototype.constructor將指向Person函數。通過Student.prototype.constructor = Student 可以將其constructor重新指向Student。
通過原型鏈可以更好的理解上面的代碼:
原型鏈繼承的缺點
關於原型鏈繼承的問題,其實就是跟通過原型方式創建對象的問題一樣,就是原型中包含引用類型所帶來的共享問題。
還有就是創建實例的時候,無法向構造器中傳遞參數。
構造函數繼承
另一種經典的繼承便是通過構造函數實現繼承,即通過apply()和call()方法在子類構造函數內部調用父類構造函數。具體實現如下:
function Person(name){ this.name = name; this.friends = new Array(); } Person.prototype.age = 0; function Student(name){ Person.call(this, name); } var student1 = new Student("Huge"); student1.friends.push("Alan"); console.log(student1.name);//Huge console.log(student1.age);//undefined console.log(student1.friends);//["Alan"] var student2 = new Student("Heri"); student2.friends.push("Amly"); console.log(student2.name);//Heri console.log(student2.friends);//["Amly"] console.log(student1 instanceof Person);//false console.log(student1 instanceof Student);//true
通過構造函數繼承的問題除了構造函數模式本身存在的缺點之外(重復實例化方法),也無法類型識別,因此在父類原型中定義的方法和屬性無法在子類中調用。
組合繼承
由於通過原型鏈繼承和構造函數繼承都有其優缺點,因此將這兩種繼承方式組合起來,使用原型鏈繼承實現原型中方法和屬性的繼承,通過構造函數繼承實現參數傳遞和引用類型繼承,是javascript中最常用的繼承模式。代碼實現如下:
function Person(name, age){ this.name = name; this.age = age; this.friends = new Array(); } Person.prototype.getName = function(){ return this.name; } function Student(name, age){ this.type = "student"; Person.call(this, name, age); } Student.prototype = new Person(); Student.prototype.constructor = Student; var student1 = new Student("Huge", 15); student1.friends.push("Alan"); console.log(student1.name);//Huge console.log(student1.age);//15 console.log(student1.friends);//["Alan"] console.log(student1.getName());//Huge console.log(student1 instanceof Person);//true console.log(student1 instanceof Student);//true var student2 = new Student("Heri", 16); student2.friends.push("Amly"); console.log(student2.name);//Heri console.log(student2.age);//16 console.log(student2.friends);//["Amly"] console.log(Student.prototype.name);//undefined console.log(Student.prototype.friends);//[]
從代碼可以看出,組合繼承會調用兩次父類的構造函數:創建子類原型的時候和在子類構造函數內部調用。實際上,第一次創建子類原型的時候,子類已經包含了父類對象的全部實例屬性,因此當通過調用子類構造函數創建實例的時候,將會重寫這些屬性。即同時存在兩組屬性,一組在實例上,一組在子類原型中,如上代碼中Student.prototype.friends返回的空數組。這就是調用兩次父類構造函數的結果。
其他繼承方式
Crockford曾經提出了prototypal inheritance以及與之結合的parasitic inheritance。通過原型創建基於原有對象的新對象,並為新對象增加功能。
//prototypal inhertance function createObject(obj){ function F(){} F.prototype = obj; return new F(); } //parasitic inheritance function enhanceObject(obj){ var enhanceObj = createObject(obj); enhanceObj.getName = function(){ return this.name; } return enhanceObj; } var person = { name : "Alan" }; var person1 = enhanceObject(person); console.log(person1.getName());//Alan
更進一步,為了避免組合繼承模式兩次調用父類構造函數的問題,可以利用parasitic inheritance來繼承父類的原型,再將其指定給子類的原型。如下代碼:
//prototypal inhertance function createObject(obj){ function F(){} F.prototype = obj; return new F(); } //parasitic inheritance function inheritPrototype(superObj, subObj){ var obj = createObject(superObj.prototype); obj.constructor = subObj; subObj.prototype = obj; } function Person(name, age){ this.name = name; this.age = age; this.friends = new Array(); } Person.prototype.getName = function(){ return this.name; } function Student(name, age){ this.type = "student"; Person.call(this, name, age); } inheritPrototype(Person, Student); var student1 = new Student("Huge", 15); student1.friends.push("Alan"); console.log(student1.name);//Huge console.log(student1.age);//15 console.log(student1.friends);//["Alan"] console.log(student1.getName());//Huge console.log(student1 instanceof Person);//true console.log(student1 instanceof Student);//true var student2 = new Student("Heri", 16); student2.friends.push("Amly"); console.log(student2.name);//Heri console.log(student2.age);//16 console.log(student2.friends);//["Amly"]\ console.log(Student.prototype.name);//undefined console.log(Student.prototype.friends);//undefined
可以看出,子類只調用了父類一次構造函數,避免在子類原型中創建不必要的屬性。同時,原型鏈也保持不便,可以說是實現類型繼承的最有效方式。