天天看點

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()) // 毛球      

繼續閱讀