看完這篇,真正理解JS物件導向程式設計思想

Monster1010發表於2020-03-05

物件導向介紹

什麼是物件

Everything is object (萬物皆物件)

物件到底是什麼,我們可以從兩次層次來理解。

(1) 物件是單個事物的抽象。

一本書、一輛汽車、一個人都可以是物件,一個資料庫、一張網頁、一個與遠端伺服器的連線也可以是物件。當實物被抽象成物件,實物之間的關係就變成了物件之間的關係,從而就可以模擬現實情況,針對物件進行程式設計。

(2) 物件是一個容器,封裝了屬性(property)和方法(method)。

屬性是物件的狀態,方法是物件的行為(完成某種任務)。比如,我們可以把動物抽象為animal物件,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。

在實際開發中,物件是一個抽象的概念,可以將其簡單理解為:資料集或功能集

ECMAScript-262 把物件定義為:無序屬性的集合,其屬性可以包含基本值、物件或者函式。 嚴格來講,這就相當於說物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都對映到一個值。

提示:每個物件都是基於一個引用型別建立的,這些型別可以是系統內建的原生型別,也可以是開發人員自定義的型別。

什麼是物件導向

物件導向不是新的東西,它只是過程式程式碼的一種高度封裝,目的在於提高程式碼的開發效率和可維 護性。

物件導向程式設計 —— Object Oriented Programming,簡稱 OOP ,是一種程式設計開發思想。 它將真實世界各種複雜的關係,抽象為一個個物件,然後由物件之間的分工與合作,完成對真實世界的模擬。

在物件導向程式開發思想中,每一個物件都是功能中心,具有明確分工,可以完成接受資訊、處理資料、發出資訊等任務。 因此,物件導向程式設計具有靈活、程式碼可複用、高度模組化等特點,容易維護和開發,比起由一系列函式或指令組成的傳統的程式式程式設計(procedural programming),更適合多人合作的大型軟體專案。

物件導向與程式導向:

  • 程式導向就是親力親為,事無鉅細,面面俱到,步步緊跟,有條不紊
  • 物件導向就是找一個物件,指揮得結果
  • 物件導向將執行者轉變成指揮者
  • 物件導向不是程式導向的替代,而是程式導向的封裝

物件導向的特性:

  • 封裝性
  • 繼承性
  • [多型性]抽象

答主從知乎找了關於面對物件的答案,供大家深入理解:

程式中物件導向的基本體現

在 JavaScript 中,所有資料型別都可以視為物件,當然也可以自定義物件。 自定義的物件資料型別就是物件導向中的類( Class )的概念。

我們以一個例子來說明程式導向和麵向物件在程式流程上的不同之處。

假設我們要處理學生的成績表,為了表示一個學生的成績,程式導向的程式可以用一個物件表示:

var std1 = { name: 'Michael', score: 98 }
var std2 = { name: 'Bob', score: 81 }
複製程式碼

而處理學生成績可以通過函式實現,比如列印學生的成績:

function printScore (student) {
  console.log('姓名:' + student.name + '  ' + '成績:' + student.score)
}
複製程式碼

如果採用物件導向的程式設計思想,我們首選思考的不是程式的執行流程, 而是 Student 這種資料型別應該被視為一個物件,這個物件擁有 namescore 這兩個屬性(Property)。 如果要列印一個學生的成績,首先必須建立出這個學生對應的物件,然後,給物件發一個 printScore 訊息,讓物件自己把自己的資料列印出來。

抽象資料行為模板(Class):

function Student(name, score) {
  this.name = name;
  this.score = score;
  this.printScore = function() {
    console.log('姓名:' + this.name + '  ' + '成績:' + this.score);
  }
}
複製程式碼

根據模板建立具體例項物件(Instance):

var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)
複製程式碼

例項物件具有自己的具體行為(給物件發訊息):

std1.printScore() // => 姓名:Michael  成績:98
std2.printScore() // => 姓名:Bob  成績 81
複製程式碼

物件導向的設計思想是從自然界中來的,因為在自然界中,類(Class)和例項(Instance)的概念是很自然的。 Class 是一種抽象概念,比如我們定義的 Class——Student ,是指學生這個概念, 而例項(Instance)則是一個個具體的 Student ,比如, Michael 和 Bob 是兩個具體的 Student 。

所以,物件導向的設計思想是:

  • 抽象出 Class(建構函式)
  • 根據 Class(建構函式) 建立 Instance
  • 指揮 Instance 得結果

物件導向的抽象程度又比函式要高,因為一個 Class 既包含資料,又包含運算元據的方法。

建立物件

簡單方式

我們可以直接通過 new Object() 建立:

var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayName = function () {
  console.log(this.name)
}
複製程式碼

每次建立通過 new Object() 比較麻煩,所以可以通過它的簡寫形式物件字面量來建立:

var person = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}
複製程式碼

對於上面的寫法固然沒有問題,但是假如我們要生成兩個 person 例項物件呢?

var person1 = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

var person2 = {
  name: 'Mike',
  age: 16,
  sayName: function () {
    console.log(this.name)
  }
}
複製程式碼

通過上面的程式碼我們不難看出,這樣寫的程式碼太過冗餘,重複性太高。

簡單方式的改進:工廠函式

我們可以寫一個函式,解決程式碼重複問題:

function createPerson (name, age) {
  return {
    name: name,
    age: age,
    sayName: function () {
      console.log(this.name)
    }
  }
}
複製程式碼

然後生成例項物件:

var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)
複製程式碼

這樣封裝確實爽多了,通過工廠模式我們解決了建立多個相似物件程式碼冗餘的問題, 但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。

建構函式

我們引入了建構函式來解決以上問題,建構函式也是建立物件最通用的方法。

更優雅的工廠函式:建構函式

一種更優雅的工廠函式就是下面這樣,建構函式:

function Person (name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}

var p1 = new Person('Jack', 18)
p1.sayName() // => Jack

var p2 = new Person('Mike', 23)
p2.sayName() // => Mike
複製程式碼

建構函式和工廠函式的區別

在上面的示例中,Person() 函式取代了 createPerson() 函式,但是實現效果是一樣的。 這是為什麼呢?

我們注意到,Person() 中的程式碼與 createPerson() 有以下幾點不同之處:

  • 沒有顯示的建立物件
  • 直接將屬性和方法賦給了 this 物件
  • 沒有 return 語句
  • 函式名使用的是大寫的 Person

而要建立 Person 例項,則必須使用 new 操作符。 以這種方式呼叫建構函式會經歷以下 4 個步驟:

  1. 建立一個新物件
  2. 將建構函式的作用域賦給新物件(因此 this 就指向了這個新物件)
  3. 執行建構函式中的程式碼
  4. 返回新物件

下面是具體的虛擬碼:

function Person (name, age) {
  // 當使用 new 操作符呼叫 Person() 的時候,實際上這裡會先建立一個物件
  // var instance = {}
  // 然後讓內部的 this 指向 instance 物件
  // this = instance
  // 接下來所有針對 this 的操作實際上操作的就是 instance

  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }

  // 在函式的結尾處會將 this 返回,也就是 instance
  // return this
}
複製程式碼

建構函式和例項物件的關係

使用建構函式的好處不僅僅在於程式碼的簡潔性,更重要的是我們可以識別物件的具體型別了。 在每一個例項物件中同時有一個 constructor 屬性,該屬性指向建立該例項的建構函式:

console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
複製程式碼

物件的 constructor 屬性最初是用來標識物件型別的, 但是,如果要檢測物件的型別,還是使用 instanceof 操作符更可靠一些:

console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true
複製程式碼

總結:

  • 建構函式是根據具體的事物抽象出來的抽象模板
  • 例項物件是根據抽象的建構函式模板得到的具體例項物件
  • 每一個例項物件都具有一個 constructor 屬性,指向建立該例項的建構函式

建構函式的問題

使用建構函式帶來的最大的好處就是建立物件更方便了,但是其本身也存在一個浪費記憶體的問題:

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = function () {
    console.log('hello ' + this.name)
  }
}

var p1 = new Person('Tom', 18)
var p2 = new Person('Jack', 16)
複製程式碼

在該示例中,從表面上好像沒什麼問題,但是實際上這樣做,有一個很大的弊端。 那就是對於每一個例項物件,typesayHello 都是一模一樣的內容, 每一次生成一個例項,都必須為重複的內容,多佔用一些記憶體,如果例項物件很多,會造成極大的記憶體浪費。

console.log(p1.sayHello === p2.sayHello) // => false
複製程式碼

對於這種問題我們可以把需要共享的函式定義到建構函式外部:

function sayHello = function () {
  console.log('hello ' + this.name)
}

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = sayHello
}

var p1 = new Person('Top', 18)
var p2 = new Person('Jack', 16)

console.log(p1.sayHello === p2.sayHello) // => true
複製程式碼

因此我們引入了 原型

原型

更好的解決方案: prototype

JavaScript 規定,每一個建構函式都有一個 prototype 屬性,指向另一個物件。 這個物件的所有屬性和方法,都會被建構函式的所擁有。

這也就意味著,我們可以把所有物件例項需要共享的屬性和方法直接定義在 prototype 物件上。

function Person (name, age) {
  this.name = name
  this.age = age
}

console.log(Person.prototype)

Person.prototype.type = 'human'

Person.prototype.sayName = function () {
  console.log(this.name)
}

var p1 = new Person(...)
var p2 = new Person(...)

console.log(p1.sayName === p2.sayName) // => true
複製程式碼

這時所有例項的 type 屬性和 sayName() 方法, 其實都是同一個記憶體地址,指向 prototype 物件,因此就提高了執行效率。

建構函式、例項、原型三者之間的關係

任何函式都具有一個 prototype 屬性,該屬性是一個物件。

function F () {}
console.log(F.prototype) // => object

F.prototype.sayHi = function () {
  console.log('hi!')
}
複製程式碼

建構函式的 prototype 物件預設都有一個 constructor 屬性,指向 prototype 物件所在函式。

console.log(F.prototype.constructor === F) // => true
複製程式碼

通過建構函式得到的例項物件內部會包含一個指向建構函式的 prototype 物件的指標 __proto__

var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
複製程式碼

`__proto__` 是非標準屬性。

例項物件可以直接訪問原型物件成員。

instance.sayHi() // => hi!
複製程式碼

總結:

  • 任何函式都具有一個 prototype 屬性,該屬性是一個物件
  • 建構函式的 prototype 物件預設都有一個 constructor 屬性,指向 prototype 物件所在函式
  • 通過建構函式得到的例項物件內部會包含一個指向建構函式的 prototype 物件的指標 __proto__
  • 所有例項都直接或間接繼承了原型物件的成員

屬性成員的搜尋原則:原型鏈

瞭解了 建構函式-例項-原型物件 三者之間的關係後,接下來我們來解釋一下為什麼例項物件可以訪問原型物件中的成員。

每當程式碼讀取某個物件的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性

  • 搜尋首先從物件例項本身開始
  • 如果在例項中找到了具有給定名字的屬性,則返回該屬性的值
  • 如果沒有找到,則繼續搜尋指標指向的原型物件,在原型物件中查詢具有給定名字的屬性
  • 如果在原型物件中找到了這個屬性,則返回該屬性的值

也就是說,在我們呼叫 person1.sayName() 的時候,會先後執行兩次搜尋:

  • 首先,解析器會問:“例項 person1 有 sayName 屬性嗎?”答:“沒有。
  • ”然後,它繼續搜尋,再問:“ person1 的原型有 sayName 屬性嗎?”答:“有。
  • ”於是,它就讀取那個儲存在原型物件中的函式。
  • 當我們呼叫 person2.sayName() 時,將會重現相同的搜尋過程,得到相同的結果。

而這正是多個物件例項共享原型所儲存的屬性和方法的基本原理。 原型鏈如圖:

看完這篇,真正理解JS物件導向程式設計思想

更簡單的原型語法

我們注意到,前面例子中每新增一個屬性和方法就要敲一遍 Person.prototype 。 為減少不必要的輸入,更常見的做法是用一個包含所有屬性和方法的物件字面量來重寫整個原型物件:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
  }
}
複製程式碼

在該示例中,我們將 Person.prototype 重置到了一個新的物件。 這樣做的好處就是為 Person.prototype 新增成員簡單了,但是也會帶來一個問題,那就是原型物件丟失了 constructor 成員。

所以,我們為了保持 constructor 的指向正確,建議的寫法是:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  constructor: Person, // => 手動將 constructor 指向正確的建構函式
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
  }
}
複製程式碼

原生物件的原型

所有函式都有 prototype 屬性物件。

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype
  • ...

原型物件使用建議

  • 私有成員(一般就是非函式成員)放到建構函式中
  • 共享成員(一般就是函式)放到原型物件中
  • 如果重置了 prototype 記得修正 constructor 的指向

相關文章