繼承是javascript中實現代碼復用的一種方式,也能綁定對象或者函數之間的關系
為什么要繼承
比如以下代碼,Person、Student和Teacher構造函數,可以發現他們有一些特征
- Person和Student都有姓名、年齡的屬性和吃的方法,但Student還有學號、分數的屬性和學習的方法
- Person和Teacher都有姓名、年齡的屬性和吃的方法,但Teacher還有教學的方法
function Person(name, age) {
this.name = name
this.age = age
this.eating = function () {
console.log('eating')
}
}
function Student(name, age, sno, score) {
this.name = name
this.age = age
this.sno = sno
this.score = score
this.eating = function () {
console.log('eating')
}
this.studing = function () {
console.log('studing')
}
}
function Teacher(name, age) {
this.name = name
this.age = age
this.eating = function () {
console.log('eating')
}
this.teaching = function () {
console.log('teaching')
}
}
可以發現在定義函數的時候有很多的重復代碼,而且Person和Student、Teacher是包含關系,繼承就是通過這種包含關系為突破口來減少的重復代碼
父類原型賦值給子類
這種方式是直接把子類和父類原型指向同一個對象
Student.prototype = Person.prototype
這樣會將所有添加到子元素原型上的屬性和方法都添加到父元素身上,再增加一個子元素,也會擁有其它子元素的所有方法,這樣實現繼承的方式並不推薦
原型式繼承
通過函數的prototype屬性就可以實現繼承關系,原型式繼承就是將Student.prototype賦值為Person構造函數實例對象
圖解如下
function Person() {
this.name = 'alice'
}
Person.prototype.eating = function () {
console.log('eating')
}
function Student(score) {
this.score = score
}
var person = new Person()
Student.prototype = person
var student = new Student(88)
console.log(student)
console.log(student.name)
console.log(student.__proto__)
student.eating()
以上代碼的執行結果為
這樣Student的實例對象可以訪問Person的屬性和方法,但是存在一些問題
- 通過打印,無法獲取Student繼承的Person屬性和方法
- 不能自定義Person中屬性的值
借用構造函數繼承
基於以上問題,在原型式繼承的基礎上來借用構造函數來解決,子函數內通過call/apply來調用父函數
function Person(name) {
this.name = name
}
Person.prototype.eating = function () {
console.log('eating')
}
function Student(name, score) {
Person.call(this, name)
this.score = score
}
var person = new Person()
Student.prototype = person
var student = new Student('kiki', 88)
console.log(student)
console.log(student.name)
console.log(student.__proto__)
student.eating()
執行結果如下
通過這樣的方式,可以解決原型式繼承存在的兩個問題,一是可以從Student的實例對象上找到父元素Person的屬性和方法,二是可以自定義Person屬性的值。
但這樣一種定義方式也存在問題
- Person構造函數至少被執行了兩次,一次是創建Person的實例對象,一次是Student構造函數中通過call調用
- 創建的person對象也保存了一份Person的數據,但這份數據是不必要的
寄生式繼承
寄生式繼承結合了原型式繼承和工廠函數,創建一個實現繼承的函數,在函數內部操作對象並返回
var user = {
flying: function(){
console.log('flying')
}
}
function createObj(name, age){
var obj = Object.create(user)
obj.name = name
obj.age = age
obj.eating = function(){
console.log('eating')
}
return obj
}
var kiki = createObj('kiki', 18)
console.log(kiki)
console.log(kiki.__proto__)
執行結果如下
通過寄生式繼承,解決了借用構造函數重復調用父類函數和保存不必要數據的問題,但它又產生了新的問題
- 無法知道對象的類型,比如Person或者Student
- 每個對象都保存了一份eating的方法,其實是沒有必要的
寄生組合式繼承
通過一個對象來鏈接父子函數之間的繼承關系
圖示如下
首先定義一個原型繼承函數,提供三種定義的方式,可以根據項目兼容性選擇
var obj = {
name: 'alice'
}
// 方式一:setPrototypeOf
function createObject1(o) {
var obj = {}
Object.setPrototypeOf(obj, o)
return obj
}
// 方式二:構造函數
function createObject2(o) {
function Fn() { }
Fn.prototype = o
var obj = new Fn()
return obj
}
var obj1 = createObject1(obj)
var obj2 = createObject2(obj)
// 方式三:Object.create
var obj3 = Object.create(obj)
console.log(Object.getOwnPropertyDescriptors(obj1.__proto__))
console.log(Object.getOwnPropertyDescriptors(obj2.__proto__))
console.log(Object.getOwnPropertyDescriptors(obj3.__proto__))
執行結果為
然后再將原型式繼承函數添加到構造函數的繼承關系中
function createObj(o) {
function Fn() { }
Fn.prototype = o
return new Fn()
}
// 封裝繼承的函數
function inheritPrototype(subType, superType){
subType.prototype = createObj(superType.prototype)
Object.defineProperty(subType.prototype, 'constructor', {
value: subType,
configurable: true
})
}
function Person(name, age) {
this.name = name
this.age = age
this.eating = function () {
console.log('eating')
}
}
function Student(name, age, score) {
Person.call(this, name, age)
this.score = score
}
inheritPrototype(Student, Person)
Student.prototype.running = function () {
console.log('running')
}
var student = new Student('alice', 18, 100)
console.log(student)
student.running()
執行結果如下
寄生組合式繼承就能比較好的解決以上繼承方式存在的問題
原型的繼承關系
實例對象、構造函數、原型對象之間存在繼承的關系
var obj = {}
function Foo() { }
var foo = new Foo()
console.log(obj.__proto__ === Object.prototype)
console.log(foo.__proto__ === Foo.prototype)
console.log(Foo.__proto__ === Function.prototype)
console.log(Function.__proto__ === Function.prototype)
console.log(Object.__proto__ === Function.prototype)
console.log(Foo.prototype.__proto__ === Object.prototype)
console.log(Function.prototype.__proto__ === Object.prototype)
console.log(Object.prototype.__proto__)
執行結果如下
繼承關系如下
-
實例對象
- 定義obj字面量相當於創建Function Object的實例,所以obj的隱式原型等於Object的顯式原型
- foo對象是構造函數Foo的實例,所以foo的隱式等於Foo的顯式原型
-
函數對象
- 函數Foo也是對象,是構造函數Function的實例,所以函數Foo的隱式原型等於Function的顯式原型
- 函數Function也是對象,是構造函數Function的實例,所以函數Function的隱式原型等於Function的顯式原型,即Function的隱式原型與顯式原型相等
- 函數Object也是對象,是構造函數Function的實例,所以函數Object的隱式原型等於Function的顯式原型
-
原型對象
- Foo的顯式原型也是對象,是構造函數Object的實例,所以Foo的顯式原型對象的隱式原型等於Object的顯式原型
- Function的顯式原型也是對象,是構造函數Object的實例,所以Function的顯式原型對象的隱式原型等於Object的顯式原型
- Object的顯式原型也是對象,它的隱式原型指向null
圖示如下
上述原型關系更為詳細的圖解如下
以上就是js實現繼承的五種方法及原型的繼承關系,關於js高級,還有很多需要開發者掌握的地方,可以看看我寫的其他博文,持續更新中~