JavaScript面向對象—繼承的實現
- 2022 年 3 月 13 日
- 筆記
- javascript, js相關, 前端
JavaScript面向對象—繼承的實現
前言
面向對象的三大特性:封裝、繼承和多態。上一篇我們簡單的了解了封裝的過程,也就是把對象的屬性和方法封裝到一個函數中,這一篇講一下JavaScript中繼承的實現,繼承是面向對象中非常重要的特性,它可以幫助我們提高程式碼的復用性。繼承主要的思想就是將重複的程式碼邏輯抽取到分類中,子類只需要通過繼承分類,就可以使用分類中的方法,但是在實現JavaScript繼承之前,需要先了解一個重要的知識點「原型鏈」。
1.JavaScript中的原型鏈
在上一篇JavaScript面向對象—對象的創建和操作中已經簡單的了解過了JavaScript中對象的原型和函數的原型,當我們從一個對象上獲取屬性時,如果在當前對象自身沒有找到該屬性的話,就會去它原型上面獲取,如果原型中也沒有找到就會去它原型的原型上找,沿著這麼一條線進行查找,那麼這條線就是我們所說的原型鏈了。
示例程式碼:
const obj = {
name: 'curry',
age: 30
}
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = { height: 1.83 }
console.log(obj.height) // 1.83
對應的記憶體中的查找過程:
當通過原型鏈查找某個屬性時,一直找不到的話會一直查找下去么?肯定是不會的,JavaScript的原型鏈也是有盡頭的,這個盡頭就是Object的原型。
2.Object的原型
事實上,不管是對象還是函數,它們原型鏈的盡頭都是Object的原型,也稱之為頂層原型,我們可以列印看看這個頂層原型長什麼樣。
(1)列印Object的原型
console.log(Object.prototype)
- 在node環境中:
- 在瀏覽器中:
(2)Object原型的特殊之處
-
如果我們再次列印
Object.prototype
的原型,這個原型屬性已經指向了null;console.log(Object.prototype.__proto__) // null
-
並且在
Object.prototype
上有很多默認的屬性和方法,像toString、hasOwnProperty
等;
(3)上一篇中講到當使用new操作符調用構造函數時,其對象的[[prototype]]
會指向該構造函數的原型prototype
,其實Object
也是一個構造函數,因為我們可以使用new操作符來調用它,創建一個空對象。
-
示例程式碼:
const obj = new Object() obj.name = 'curry' obj.age = 30 console.log(obj.__proto__ === Object.prototype) // true console.log(obj.__proto__) // [Object: null prototype] {} console.log(obj.__proto__.__proto__) // null
-
記憶體表現:
(4)總結
- 從Object的原型可以得出一個結論「原型鏈最頂層的原型對象就是Object的原型對象」,這也就是為什麼所有的對象都可以調用
toString
方法了; - 從繼承的角度來講就是「Object是所有類的父類」;
3.JavaScript繼承的實現方案
3.1.方案一:通過原型鏈實現繼承
如果需要實現繼承,那麼就可以利用原型鏈來實現了。
- 定義一個父類
Person
和子類Student
; - 父類中存放公共的屬性和方法供子類使用;
- 核心:將父類的實例化對象賦值給子類的原型;
// 定義Person父類公共的屬性
function Person() {
this.name = 'curry'
this.age = 30
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
console.log('I am ' + this.name)
}
// 定義Student子類特有的屬性
function Student() {
this.sno = 101111
}
// 實現繼承的核心:將父類的實例化對象賦值給子類的原型
Student.prototype = new Person()
// 定義Student子類特有的方法
Student.prototype.studying = function() {
console.log(this.name + ' studying')
}
// 實例化Student
const stu = new Student()
console.log(stu.name) // curry
console.log(stu.age) // 30
console.log(stu.sno) // 101111
stu.say() // I am curry
stu.studying() // curry studying
記憶體表現:
缺點:
- 從記憶體表現圖中就可以看出,當列印stu對象時,name和age屬性是看不到的,因為不會列印原型上的東西;
- 當父類中的屬性為引用類型時,子類的多個實例對象會共用這個引用類型,如果進行修改,會相互影響;
- 在使用該方案實現繼承時,屬性都是寫死的,不支援動態傳入參數來訂製化屬性值;
3.2.方案二:借用構造函數實現繼承
針對方案一的缺點,可以借用構造函數來進行優化。
- 在子類中通過call調用父類,這樣在實例化子類時,每個實例就可以創建自己單獨屬性了;
// 定義Person父類公共的屬性
function Person(name, age) {
this.name = name
this.age = age
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
console.log('I am ' + this.name)
}
// 定義Student子類特有的屬性
function Student(name, age, sno) {
// 通過call調用Person父類,創建自己的name和age屬性
Person.call(this, name, age)
this.sno = sno
}
// 實現繼承的核心:將父類的實例化對象賦值給子類的原型
Student.prototype = new Person()
// 定義Student子類特有的方法
Student.prototype.studying = function() {
console.log(this.name + ' studying')
}
// 實例化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Person { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Person { name: 'kobe', age: 24, sno: 101112 }
記憶體表現:
缺點:
- 在實現繼承的過程中,Person構造函數被調用了兩次,一次在
new Person()
,一次在Person.call()
; - 在Person的實例化對象上,也就是stu1和stu2的原型上,多出來了沒有使用的屬性name和age;
3.3.方案三:寄生組合式繼承
通過上面兩種方案,我們想實現繼承的目的是重複利用另外一個對象的屬性和方法,如果想解決方案二中的缺點,那麼就要減少Person的調用次數,避免去執行
new Person()
,而解決的辦法就是可以新增一個對象,讓該對象的原型指向Person的原型即可。
(1)對象的原型式繼承
將對象的原型指向構造函數的原型的過程就叫做對象的原型式繼承,主要可以通過以下三種方式實現:
-
封裝一個函數,將傳入的對象賦值給構造函數的原型,最後將構造函數的實例化對象返回;
function createObj(o) { // 定義一個Fn構造函數 function Fn() {} // 將傳入的對象賦值給Fn的原型 Fn.prototype = o // 返回Fn的實例化對象 return new Fn() } const protoObj = { name: 'curry', age: 30 } const obj = createObj(protoObj) // 得到的obj對象的原型已經指向了protoObj console.log(obj.name) // curry console.log(obj.age) // 30 console.log(obj.__proto__ === protoObj) // true
-
改變上面方法中的函數體實現,使用
Object.setPrototypeOf()
方法來實現,該方法設置一個指定的對象的原型到另一個對象或null;function createObj(o) { // 定義一個空對象 const newObj = {} // 將傳入的對象賦值給該空對象的原型 Object.setPrototypeOf(newObj, o) // 返回該空對象 return newObj }
-
直接使用
Object.create()
方法,該方法可以創建一個新對象,使用現有的對象來提供新創建的對象的__proto__
;const protoObj = { name: 'curry', age: 30 } const obj = Object.create(protoObj) console.log(obj.name) // curry console.log(obj.age) // 30 console.log(obj.__proto__ === protoObj) // true
(2)寄生組合式繼承的實現
寄生式繼承就是將對象的原型式繼承和工廠模式進行結合,即封裝一個函數來實現繼承的過程。而這樣結合起來實現的繼承,又可以稱之為寄生組合式繼承。下面就看看具體的實現過程吧。
- 創建一個原型指向
Person
父類的對象,將其賦值給Student
子類的原型; - 在上面的實現方案中,
Student
子類的實例對象的類型都是Person
,可以通過重新定義constructor
來優化;
// 定義Person父類公共的屬性
function Person(name, age) {
this.name = name
this.age = age
}
// 定義Person父類的公共方法
Person.prototype.say = function() {
console.log('I am ' + this.name)
}
// 定義Student子類特有的屬性
function Student(name, age, sno) {
// 通過call調用Person父類,創建自己的name和age屬性
Person.call(this, name, age)
this.sno = sno
}
// 調用Object.create方法生成一個原型指向Person原型的對象,並將這個對象賦值給Student的原型
Student.prototype = Object.create(Person.prototype)
// 定義Student原型上constructor的值為Student
Object.defineProperty(Student.prototype, 'constructor', {
configurable: true,
enumerable: false,
writable: true,
value: Student
})
// 定義Student子類特有的方法
Student.prototype.studying = function() {
console.log(this.name + ' studying')
}
// 實例化Student
const stu1 = new Student('curry', 30, 101111)
const stu2 = new Student('kobe', 24, 101112)
console.log(stu1) // Student { name: 'curry', age: 30, sno: 101111 }
console.log(stu2) // Student { name: 'kobe', age: 24, sno: 101112 }
記憶體表現:
總結:
-
多個地方用到了繼承,可以將上面的核心程式碼賦值在一個函數裡面,如果不想用
Object.create()
,也可以使用上面封裝的createObj
函數;function createObj(o) { function Fn() {} Fn.prototype = o return new Fn() } /** * @param {function} SubClass * @param {function} SuperClass */ function inherit(SubClass, SuperClass) { SubClass.prototype = createObj(SuperClass.prototype) Object.defineProperty(SubClass.prototype, 'constructor', { configurable: true, enumerable: false, writable: true, value: SubClass }) }
-
寄生組合式實現繼承的原理其實就是創建一個空對象用於存放子類原型上的方法,並且這個對象的原型指向父類的原型,在ES6中推出的class的實現原理就在這;