在 ES6 以前,還沒有 class 可以來創建一個類別,類別可以想像成定義一個 object 的設計稿,我們可以透過 class 來描述一個 object 長什麼樣子,通常這個 object 我們稱為 instance,例如你創建了一個手機的設計稿,設計稿裡面有描述手機該叫什麼名字,有什麼功能可以使用,而當我們去規範這個一支手機叫 'iPhone',並且有打電話這功能,那這支手機我們稱為 instance。
即使在 ES6 以前沒有 class 可以使用,我們仍然 function 的方式去實現物件導向的概念,或許之前我們已經常使用類似的概念的,可能我們沒有發現而已。
以下是簡單的例子:
function Phone(name){
var myName = name
return {
getName: function(){
return myName
},
calling: function() {
console.log(myName + ' => call someone...')
}
}
}
var iPhone = Phone('iPhone')
iPhone.calling()
var s3 = Phone('S3')
s3.calling()
我們創建了兩支手機,iPhone 和 S3,並且他們都有打calling的 method 可以使用。所以理論上 iPhone.calling === s3.calling 應該要是 true 才對,但其實會回傳 false,由此可知當我們創建了一萬支手機,我們就會創建了一萬次 calling 這 method,這不符合邏輯,因為所有手機應該都要共用 calling。
所以我們改寫了一下程式:
function Phone(name){
return this.name = name
}
Phone.prototype.getName = function(){
return this.name
}
Phone.prototype.calling = function(){
console.log(this.name + ' => call someone...')
}
var iPhone = new Phone('iPhone')
iPhone.calling()
var s3 = new Phone('S3')
s3.calling()
注意到我們使用了關鍵字 new
來新增一個 instance,還有利用 prototype
來創建共用的 method,並且現在 iPhone.calling === s3.calling
就是 true 了。
其實每當我們新增一個實例的時候 JS 會幫我們建立 __proto__
,而這個 __proto__
就是 Phone 的 prototype
。
iPhone.__proto__ === Phone.prototype //true
當我們輸入 iPhone.calling()
的時候會發生一些事:
- iPhone 本身有沒有 calling
- iPhone.proto (Phone.prototype) 有沒有 calling
- iPhone.proto.proto (Object.prototye) 有沒有 calling
是一層一層往上找的概念,這就是 prototype chain
原型鍊,那麼如果真的連 Object.prototype 也找不到呢,就會找到最頂層,回傳 null,那麼 iPhone.calling() 就會回傳錯誤。
由此可知,其實我們可以在 Object 上加上 prototype ,這樣 iPhone 一樣也可以使用 calling。
其實我們很常使用一些 method,像是 Array 的 push, join, .....這些,就是利用 prototype 的特性,當我們建立一個 array 的時候這個 array 的 proto 會與 JS中 Array.prototype 連接起來,所以我們才可以使用 Array.prototype 的 method。(正確來說是我們新增的 array 會繼承了 Array.prototype 的所有屬性和方法)
let arr = []
console.log(arr.__proto__ === Array.prototype) // true
console.log(arr.__proto__) // 會列出所有 Array 可以使用的 method
那麼 __proto__
是怎麼來的呢,其實是因為 new
這個關鍵字在背後幫我們做了幾件事情,你可以把它想成這是以下這樣:
function Phone(name){
return this.name = name
}
Phone.prototype.getName = function(){
return this.name
}
Phone.prototype.calling = function(){
console.log(this.name + ' => call someone...')
}
// new 的工作在這裡
function newPhone(name) {
var obj = {}
Phone.call(obj, name)
obj.__proto__ = Phone.prototype
return obj
}
// 這樣就可以不用 new 了
var oppo = newPhone('oppo')
oppo.calling()
new 的主要工作就是生成一個 Object,然後重點在將這個 Object 的 proto與 Phone 的 prototype 接起來,然後再回傳這個 Object,我們就可以使用 calling 了。
以上都是介紹 ES5 實現類別與實例的方式,那到了 ES6 之後,我們就有了 class
來取代這個 function ,但其實背後做的事情是差不多的。
class Phone{
constructor(name) {
this.name = name
}
getName(name) {
return this.name
}
calling() {
console.log(this.name + ' => call someone')
}
}
var iPhone = new Phone('iPhone')
iPhone.calling()
注意這個 constructor
,是使用了 class 自動幫我們創建的,裡面會放著我們對該實例的描述。
最後,在物件導向中有個很重要的觀念就是繼承,extends
。
假如我們現在新增一支手機,一樣有 calling 功能,也跟剛剛新增的手機一樣有名字,但這支手機可以照相其他手機不行,我們就可以使用繼承的概念實現,因為只是多了一個其他手機沒有的功能而已,剩下的都共用。
class Phone{
constructor(name) {
this.name = name
}
getName(name) {
return this.name
}
calling() {
console.log(this.name + ' => call someone')
}
}
class PhoneM extends Phone {
constructor(name) {
super(name)
}
takePhoto(){
console.log(this.name + ' => taking photo')
}
}
var iPhoneM = new PhoneM('iPhoneM')
iPhoneM.calling()
其實生活上很多例子都跟物件導向有相關,例如有個類別是 user,那就可以創建很多個 user 出來,每個 user 有不一樣的 id 但是都有著一樣的 method 可以使用,例如登入、新增文章....之類的,那我們可以使用繼承的觀念去新增一個 user 是 admin , 因為 admin 其實很多 method 和屬性都跟 user 共用,可能只是多加一個可以刪除文章的功能,當然生活上其實很多東西可以想成是類別與實例來看!這樣或許就不會那麼難理解了。
在以上我們大概知道了物件導向是什麼東西,也用了 JS 去簡單的示範怎麼呈現類別與實例,接下來我將這些片段整理一下,首先我們要知道設計物件導向的類別的時候幾個大觀念:
- 抽象化 Abstraction:簡單的說,就是在抽象化概念中我們不需要去談細節,就像你在使用 setTimeout 或 監聽事件的時候,我們並不需要去知道運作的細節,我只要知道怎麼去使用,所以在設計類別的時候我們要站在使用者的角度,我只要讓使用者知道怎麼使用就好。
- 封裝 Encapsulation:我們應該要明確定的定義哪些屬性是可以對外開放,哪些是不希望可以從外部被操控。
- 繼承 Inheritance:就像剛剛舉的 user 與 admin 的例子,當我們建立 user 和 admin 類別時,實際上 admin 也是其中一個 user,所以 admin 應該要繼承 user 可以使用的屬性,並且 admin 有著自己獨有的屬性,總之,user 是 admin 的 parent 。
- 多型 Polymrophism:可以想像成多型就是指一個類別可以再延伸出多個子類別,而此這些子類別有許多跟父類別共用的屬性,跟父類別有點相似但其實有一些不一樣。(其實這部分我還沒有到非常懂啦,先解說到這邊)
好了,現在對 JS 的物件導向有了基本認識之後,我們可以來認識 this
這個大魔王了