ES5 實作物件導向

以前學的做法。

簡述

我們先來看結果會長什麼樣,等等再來解釋:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {
this._name = name
}
Person.prototype.getName = function () {
console.log(this._name)
}
Person.prototype.setName = function (newName) {
this._name = newName
}

const person1 = new Person('PeaNu')
const person2 = new Person('PPB')

person1.getName() // PeaNu
person2.getName() // PPB

順便幫你複習一下 ES6 的寫法,做個對照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
constructor(name) {
this._name = name
}
getName() {
console.log(this._name)
}
setName(newName) {
this._name = newName
}
}

const person1 = new Person('PeaNu')
const person2 = new Person('PPB')

person1.getName() // PeaNu
person2.getName() // PPB

關於 constructor

可以發現 ES5 寫起來沒有那麼直覺,在沒有 class 時只能把 function 當作 constructor 來用,而且還蹦出一堆 prototype 的東西。

另外為了避免跟普通的 function 搞混,一般會像 class 一樣用「大寫開頭」來區分。再來是很重要的一點,就是 constructor 一定要搭配 new 來使用,不然會沒有作用。

所以說只要你看到 new xxx() 的話一定代表是 call 某個 constructor,而不是 function。

prototype 幹嘛用的?

以前我對變數的概念不熟,所以一直不懂 prototype 的實際意義是什麼。但現在熟了以後就很清楚了。

你先想想看,如果不用 prototype 的話會怎樣?

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
this._name = name
this.getName = function () {
console.log(this._name)
}
this.setName = function (newName) {
this._name = newName
}
}

const person1 = new Person('PeaNu')
const person2 = new Person('PPB')

看起來好像沒什麼差?但如果執行這段的話會發現:

1
console.log(person1.getName === person2.getName) // false

結果是 false,代表這兩個 getName 是兩個不同的 function。不太懂的話再舉個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getName1() {
console.log('hi')
}
function getName2() {
console.log('hi')
}

const getNameByReference = getName1

getName1() // hi
getName2() // hi
getNameByReference() // hi

console.log(getName1 === getName2) // false
console.log(getNameByReference === getName1) // true

這三個 function 都可以正常執行,可是差在哪裡?差在 getName1getName2 是不同的 function,而 getNameByReference 是透過 refer 的方式參考到 getName1,所以 getNameByReference === getName1 才會是true

回到 prototype 的例子也一樣,如果沒有用 prototype 來做設定的話,每當 new 一個 instance 的時候就會重新宣告一個新的 function,所以比對的結果會是 false,因為它們是不一樣的。

問題很明顯,這樣子很浪費資源,明明每個 function 要做的事情一樣,為什麼不讓它們共用就好?還要幫每一個 instance 都重新宣告一次。

所以 prototype 就誕生了,讓每個 instance 共用同一個 function,就是它的初衷。

至於別人常說 class 是語法糖的原因是因為寫起來比較簡單和直覺,你可以滑上去對比 ES5 和 ES6,就能看到這兩個差別:

  1. 直接在 class 裡用 constructor,而不是透過 function declaration
  2. 不需要透過 prototype 來綁定,也能達到一樣的效果

所以現在要實作物件導向都會透過 class,比較少在用 prototype,但還是要理解它們背後的涵義。

繼承

先來一段範例,複習一下在 ES6 裡面我們是怎麼用 class 來做繼承的。

附註:Admin 會繼承 User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class User {
constructor(name, email) {
this.name = name
this.email = email
}
login() {
console.log(`${this.name} has logged in.`)
}
}

class Admin extends User {
constructor(permission, ...props) {
super(...props)
this.permission = permission
this.users = []
}
addUser(user) {
this.users = this.users.map((user) => ({ ...user })).concat(user)
}
deleteUser(user) {
this.users = this.users.filter((u) => u.name !== user.name)
}
}

const user1 = new User('peanu', 'peanu@peanu.dev')
const user2 = new User('ppb', 'ppb@peanu.dev')
const admin = new Admin('root', 'admin', 'admin@peanu.dev')

admin.login() // admin has logged in
admin.addUser(user1)
admin.addUser(user2)
console.log('init users', admin.users) // [user1, user2]
admin.deleteUser(admin.users[0]) // delete user1
console.log('deleted users', admin.users) // user2

簡單來說,在建立 Admin 時我們會多做兩件事:

  1. extends 表示我們想要繼承的那個 class(User
  2. 為了建立新的 property 給 Admin,我們會在 constructor 中使用 super 建立原本 User 中應有的 property。(如果沒有這個需求的話其實可以省略這個步驟)

最後用 Admin 建立的出來的 Instance 就會繼承 User 身上的 property 及 method(nameemaillogin)。

如果變成 ES5 的形式的話會改成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function User(name, email) {
this.name = name
this.email = email
}

User.prototype.login = function () {
console.log(`${this.name} has logged in`)
}

function Admin(role, ...args) {
// 把 this (物件)丟給 User constructor 建立 property
User.apply(this, args)
// 建立 Admin 自己的 property
this.role = role
this.users = []
}

// 繼承 User.prototype 中的所有 method
Admin.prototype = Object.create(User.prototype)

// 在 Admin.prototype 建立自己的 method
Admin.prototype.addUser = function (user) {
this.users = this.users.map((user) => ({ ...user })).concat(user)
}
Admin.prototype.deleteUser = function (user) {
this.users = this.users.filter((u) => u.name !== user.name)
}

const user1 = new User('peanu', 'peanu@peanu.dev')
const user2 = new User('ppb', 'ppb@peanu.dev')
const admin = new Admin('root', 'admin', 'admin@peanu.dev')

admin.login() // admin has logged in
admin.addUser(user1)
admin.addUser(user2)
console.log('added users', admin.users) // [user1, user2]
admin.deleteUser(admin.users[0])
console.log('deleted users', admin.users) // [user2]

拆開語法糖的包裝後:

  • 建立 property 的方式會從 super(...args) 變成 User.apply(this, args)
  • 建立 method 的方式會從 extends User 變成 Admin.prototype = Object.create(User.prototype)

其實背後在做的事情都一樣,只是 ES6 把它包裝成更好看一點而已。

理解原型鍊的運作 ES6 實作物件導向
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×