1. 程式人生 > >對象(一)--對象的繼承

對象(一)--對象的繼承

pre name 常識 通過 一次 mon 系統 單獨使用 homework

聊一聊JavaScript中的對象的繼承

吐槽

時間過得是真的快,感覺才更新博客怎麽就快一個禮拜了...這兩天看了點pythonflask框架了解了下,最後還是打算去系統地學習下node,又看了MongoDB,之後覺得Linux挺有意思的又去找資料學習(選發行版本,裝虛擬機...), 感覺把時間都折騰在這兒了,有點不務正業(想了想還是日後抽時間學習吧,現在還是把前端知識鞏固好,畢竟目前是個連實習都找不到的渣渣...
不過node還是要學的,計劃之後手擼一個個人博客練練手...

前言

在前一篇對象的創建中,文中提到過原型模式,其實呢在JavaScript中對象的繼承就是以原型模式作為基礎的。
每個構造函數都有一個prototype

屬性指向的它的原型對象,而構造函數創建的實例對象可以順勢訪問到原型對象中的屬性

原型繼承

原型對象,它也可以是另外一個構造函數的實例對象,因此它就可以訪問更高層次的原型對象的屬性,這樣原型對象與原型對象以某種形式相連就構成了一條原型鏈, 而繼承其實就是依賴於原型鏈。
打個比方PersonStudent兩個類,根據常識Student肯定是Person的一個子集,即Person具有的屬性和方法Student按道理都應該有,那麽我們在已經有Person的基礎上不應該再在Student中編寫重復的代碼,而是用Student去繼承Person來實現代碼的復用

function Person(name="name", age="20") {
  this.name = name 
  this.age = age
  this.hobbies = [‘readBooks‘, ‘swimming‘]
}
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`)
}

function Student(homework) {
  this.homework = homework
}
<!-- 將Student的原型對象去引用Person的實例化對象 -->
Student.prototype = new Person()

Student.prototype.doHomework = function() {
  console.log(`i am doing ${this.homework} homework`)
}

let stu1 = new Student(‘math‘)
console.log(!!(stu1.name && stu1.age && stu1.sayHello)) // true

我們可以發現是在Student的實例化對象可以順著原型鏈(綠色)找到這幾個屬性,nameage分布在Student.prototype中, sayHello分布在Person.prototype中,參照如下圖
技術分享圖片

另外,構造函數的原型對象都繼承自Object.prototype, 這就是為什麽所有對象都具有toString 等方法

原型繼承通過將原型對象賦值為一個父類的實例對象來實現繼承,實質就是原型鏈的搭建

需要註意的地方是在將原型對象重新賦值後,它的constructor 需要手動指正

另外大家有沒有註意到在上面這個例子中name, age 等屬性都包含在同一個對象中,也就是說所有的子類對象在訪問這些屬性的時候實際上都是同一個值,如若它們是引用類型,任何實例對象對其的修改都會在其他對象訪問時得到反映

let stu1 = new Student()
console.log(stu1.hobbies) // [‘readBooks‘, ‘swimming‘]
stu1.hobbies.push(‘running‘)
let stu2 = new Student()
console.log(stu2.hobbies) // [‘readBooks‘, ‘swimming‘, ‘runnning‘]

並且在創建子類型的實例時不能對父類型的構造函數傳參,這也是一個問題

借用構造函數組合繼承

為了解決以上兩個問題:

  1. 所有子類型實例訪問父類型屬性時其實都是在訪問原型屬性
  2. 在創建子類型的實例時不能對父類型的構造函數進行傳參
    借用構造函數閃亮登場了
function Student(name, age, homework) {
  // 使用call()(or apply())方法綁定this
  Person.call(this, name, age)
  this.homework = homework
}

// 相當於下面
function Student(name, age, homework) {
  this.name = name
  this.age = age
  this.homework = homework
}

<!-- 將Student的原型對象去引用Person的實例化對象 -->
Student.prototype = new Person()

這樣將屬性重新定義在子類的實例上,方法還是放置在原型鏈上,很好地解決了以上兩個問題, 自身的nameage遮蔽了原型鏈上的nameage

還要一個問題就是子類型已經可以創建父類屬性了,原型對象還是會再次創建這些屬性,在這裏Student.prototype對象中nameage依舊會被創建,因為調用了一個Person的構造函數, 但是nameage在原型對象中已經是多余的了

直接繼承prototype

之前將父級構造函數的實例賦值給子構造函數的原型對象Student.prototype = new Person(), 會造成屬性在原型對象中的冗余
這次我們選擇直接繼承父構造函數的原型對象

Student.prototype = Person.prototype

避免了不必要的屬性定義在原型對象中,並且少了一次構造函數的調用節省了內存
但是又有一個問題暴露出來了,就是此時子構造函數的原型對象和父構造函數的原型對象同時指向一個對象, 其中任何一個添加修改屬性都會影響對方
比如上面,當我們想要指正Student.prototypeconstructor時,Person.prototypeconstructor也發生了改變

Student.prototype.constructor = Student
console.log(Perosn.prototype.constructor) // Student

寄生組合式繼承

利用空對象作為中介

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}
function Student(name, age, homework) {
  Person.call(this, name, age)
  this.homework = homework
}

Student.prototype = object(Person.prototype)

技術分享圖片

既避免的父級構造函數的多余調用,又將父子構造函數的原型對象區分開來,且空對象的占有的內存很少,因此寄生組合式繼承棒棒的!

小結

JavaScript主要通過原型鏈實現繼承,原型鏈的構建是通過將一個類型的實例賦值給另一個構造函數的原型實現。這樣,子類型就能夠訪問父類型的所有屬性和方法。原型鏈的問題是對象實例共享所有繼承的屬性和方法,因此不適宜單獨使用。解決這個問題的技術是借用構造函數,即在子類型構造函數的內部調用超類型的構造函數。這樣就可以做到每個實例都有自己的屬性。使用最多的繼承模式是組合繼承,即使用原型鏈繼承共享的屬性和方法,而通過借用構造函數繼承實例屬性

但是,寄生組合式繼承是實現基於類型繼承的最有效方式

對象(一)--對象的繼承