重學JS: 多態封裝繼承

前言

JS是一種基於對象的語言,在JS中幾乎所有的東西都可以看成是一個對象,但是JS中的對象模型和大多數面嚮對象語言的對象模型不太一樣,因此理解JS中面向對象思想十分重要,接下來本篇文章將從多態、封裝、繼承三個基本特徵來理解JS的面向對象思想

多態

含義

同一操作作用於不同的對象上面,可以產生不同的解釋和不同的執行結果,也就是說,給不同的對象發送同一個消息時,這些對象會根據這個消息分別給出不同的反饋。
舉個例子:假設家裏養了一隻貓和一隻狗,兩隻寵物都要吃飯,但是吃的東西不太一樣,根據主人的吃飯命令,貓要吃魚,狗要吃肉,這就包含了多態的思想在裏面,用JS代碼來看就是:

let petEat = function (pet) {
  pet.eat()
} 
let Dog = function () {}
Dog.prototype.eat = function () {
  console.log('吃肉')
}
let Cat = function () {}
Cat.prototype.eat = function () {
  console.log('吃魚')
}

petEat(new Dog())
petEat(new Cat())

上面這段代碼展示的就是對象的多態性,由於JS是一門動態類型語言,變量類型在運行時是可變的,因此一個JS對象既可以是Dog類型的對象也可以是Cat類型的對象,JS對象多態性是與生俱來的,而在靜態類型語言中,編譯時會進行類型匹配檢查,如果想要一個對象既表示Dog類型又表示Cat類型在編譯的時候就會報錯,當然也會有解決辦法,一般會通過繼承來實現向上轉型,這裏感興趣的可以去對比一下靜態語言的對象多態性。

作用

多態的作用是通過把過程化的條件分支語句轉化爲對象的多態性,從而消除這些條件分支語句,舉個例子:還是上面寵物吃飯的問題,如果在沒有使用對象的多態性之前代碼可能是這樣是的:

let petEat = function (pet) {
  if (pet instanceof Dog) {
    console.log('吃肉')
  } else if (pet instanceof Cat) {
    console.log('吃魚')
  }
}
let Dog = function () {}
let Cat = function () {}
petEat(new Dog())
petEat(new Cat())

通過條件語句來判斷寵物的類型決定吃什麼,當家裏再養金魚,就需要再加一個條件分支,隨着新增的寵物越來越多,條件語句的分支就會越來越多,按照上面多態的寫法,就只需要新增對象和方法就行,解決了條件分支語句的問題

封裝

封裝的目的是將信息隱藏,一般來說封裝包括封裝數據、封裝實現,接下來就逐一來看:

封裝數據

由於JS的變量定義沒有private、protected、public等關鍵字來提供權限訪問,因此只能依賴作用域來實現封裝特性,來看例子

var package = (function () {
  var inner = 'test'
  return {
    getInner: function () {
      return inner
    }
  }
})()
console.log(package.getInner()) // test
console.log(package.inner) // undefined

封裝實現

封裝實現即隱藏實現細節、設計細節,封裝使得對象內部的變化對其他對象而言是不可見的,對象對它自己的行爲負責,其他對象或者用戶都不關心它的內部實現,封裝使得對象之間的耦合變鬆散,對象之間只通過暴露的API接口來通信。
封裝實現最常見的就是jQuery、Zepto、Lodash這類JS封裝庫中,用戶在使用的時候並不關心其內部實現,只要它們提供了正確的功能即可

繼承

繼承指的是可以讓某個類型的對象獲得另一個類型的對象的屬性和方法,JS中實現繼承的方式有多種,接下來就看看JS實現繼承的方式

構造函數綁定

這種實現繼承的方式很簡單,就是使用call或者apply方法將父對象的構造函數綁定在子對象上,舉個例子:

function Pet (name) {
  this.type = '寵物'
  this.getName = function () {
    console.log(name)
  }
}
function Cat (name) {
  Pet.call(this, name)
  this.name = name
}
let cat = new Cat('毛球')
console.log(cat.type) // 寵物
cat.getName() // 毛球

通過調用父構造函數的call方法實現了繼承,但是這種實現有一個問題,父類的方法是定義在構造函數內部的,對子類是不可見的

原型繼承

原型繼承的本質就是找到一個對象作爲原型並克隆它。這句話怎麼理解,舉個例子:

function Pet (name) {
  this.name = name
}
Pet.prototype.getName = function () {
  return this.name
}
let p = new Pet('毛球')
console.log(p.name) // 毛球
console.log(p.getName()) // 毛球
console.log(Object.getPrototypeOf(p) === Pet.prototype) // true

上面這段代碼中p對象實際上就是通過Pet.prototype的克隆和一些額外操作得來的,有了上面的代碼基礎,接下來來看一個簡單的原型繼承代碼:

let pet = {name: '毛球'}
let Cat = function () {}
Cat.prototype = pet
let c = new Cat()
console.log(c.name) // 毛球

來分析一下這段引擎做了哪幾件事:

  • 首先遍歷c中的所有屬性,但是沒有找到name屬性
  • 查找name屬性的請求被委託給對象c的構造器原型即Cat.prototype,Cat.prototype是指向pet的
  • 在pet對象中找到name屬性,並返回它的值

上面的代碼實現原型繼承看起來有點繞,實際上在es5提供了Obejct.create()方法來實現原型繼承,舉個例子:

function Pet (name) {
  this.name = name
}
Pet.prototype.getName = function () {
  return this.name
}
let c = Object.create(new Pet('毛球'))
console.log(c.name) // 毛球
console.log(c.getName()) // 毛球

組合繼承

組合繼承即使用原型鏈實現對原型屬性和方法的繼承,通過構造函數實現對實例屬性的繼承,舉個例子:

function Pet (name) {
  this.name = name
}
Pet.prototype.getName = function () {
  return this.name
}
function Cat (name) {
  Pet.call(this, name)
}
Cat.prototype = new Pet()
let c = new Cat('毛球')
console.log(c.name) // 毛球
console.log(c.getName()) // 毛球

總結

本篇文章主要介紹了JS面向對象編程思想的多態封裝繼承的特性,這裏只做了淺析,想要深挖還需要更深入的學習,希望看完本篇文章對理解JS面向對象編程思想有所幫助
如果有錯誤或不嚴謹的地方,歡迎批評指正,如果喜歡,歡迎點贊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章