Javascript 物件與繼承

Posted by Benjamin Lu on 2017-08-22

物件與繼承

宣告一個物件

ES5中的物件

宣告一個最簡單的物件

1
var a = {}

javascript中函式也是一種物件,稱一級函式(First class functions)
意思是任何你對其他類型(Objects, String, Boolean, Numbers)做的事
你也可以對Function做
對於支持函式可如數值一樣指定給變數的語言
我們稱函式在這個語言中是一等(First-class)函式或一級函式

更詳細的說明可見良葛格的筆記

1
2
3
4
// method是一個函式,為一個Function的實例
var method = function () {
console.log(this)
}

ES5宣告一個類別

1
2
3
4
5
6
7
8
9
var Car = function (name) {
this.getBrand = function () {
return name
}
}
var car1 = new Car('Rolls-Royce')
var car2 = new Car('Benz')
console.log(car1.getBrand())
console.log(car2.getBrand())

ES6中的物件

ES6語法提供了更直覺的物件宣告關鍵字(class),如下
注意:目前ES6尚未針對private寫法訂出標準,只有proposals
這邊先使用_的做法識別
但這種寫法並無法阻擋存取,實際上並沒有private property的效果
可以透過closure觀念或是其他tricky的做法
在現階段做到private property/private method的做法
但有違ES6乾淨語法的目標,不多作介紹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
constructor (name) {
this.name = name
}
getBrand () {
return this.name
}
}
let car1 = new Car('Rolls-Royce')
let car2 = new Car('Benz')
console.log(car1.getBrand())
console.log(car2.getBrand())

物件導向

物件導向強調繼承封裝多型,其實javascript是一個有物件導向性質的語言,不過因為在ES5中要寫出這樣的特性,語法上非常不直覺,因此有了Javascript是世界上最被誤解的語言一文

但關於繼承這件事,javascript的繼承屬於Prototypal Inheritance,與一般靜態語言(Java, C++)的Class Inheritance有著本質上的不同,ES6的關鍵字class也只是Prototypal Inheritance的語法糖,實際上背後的實作仍然是Prototypal Inheritance

更詳細的差異請參閱
What’s the Difference Between Class & Prototypal Inheritance?

因此有別於Class Inheritance類型的物件導向語言
這種差異影響著多型(多重繼承或介面實作來達到)和封裝(常透過interface了解實作的封裝)
在javascript中呈現的形式,或是本質上無法實現,此處不會講太過複雜的實作,若想深入了解多重繼承,下方有補充資料

故在講解javascript物件導向時

  • 繼承: 只會說明單一繼承,多重繼承比較像混合(mixins)

  • 多型: Prototypal Inheritance的語言實作,與繼承極為相似,且ES5/ES6並沒有支援interface等相關關鍵字,所以不多做說明,如果有寫Angular.js或TypeScript可能會知道Typescript interface,事實上Typescript interface只是個compile-time語法,不會被翻譯成ES5的任何實作

  • 封裝: 沒有interface語法,資料封裝其實就會跟操作封裝很像,同為js物件的做法

關於多重繼承的模擬,通常採mixins方式實作,可以參考這篇

此處個別針對ES5/ES6在物件上的語法特別說明其差異,希望可以幫助讀者打好基底,不要迷茫在js設計上的問題,進而造成js不支援某種特性的誤解

Static Variable / Static Method

談到物件和實體,也要提一下靜態的成員變數與方法

注意static的變數和方法不會被繼承

ES5 Static Variable

1
2
3
4
5
6
7
8
9
10
11
12
13
var Car = function (name) {
this.name = name
}
// 毫無反應,只是個靜態變數
Car.staticObject = new Object()
var car1 = new Car('Benz')
var car2 = new Car('BMW')
console.log(car1.name) // Benz
console.log(car2.name) // BMW
console.log(Car.staticObject) // Object
console.log(car1.staticObject) // undefined,要小心,跟Java不同

ES6 Static Variable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Car {
static staticObject = new Object()
constructor (name) {
this.name = name
}
}
var car1 = new Car('Benz')
var car2 = new Car('BMW')
console.log(car1.name) // Benz
console.log(car2.name) // BMW
console.log(Car.staticObject) // Object
console.log(car1.staticObject) // undefined,要小心,跟Java不同

ES5 Static Method

1
2
3
4
5
6
7
8
9
10
11
12
var Car = function (name) {
this._name = name
}
// 毫無反應,只是個靜態方法
Car.staticFunction = function () {
return 'static'
}
var car1 = new Car('Benz')
var car2 = new Car('BMW')
console.log(Car.staticFunction()) // static
console.log(car1.staticFunction()) // TypeError, car1.staticFunction is not a function

ES6 Static Method

1
2
3
4
5
6
7
8
9
10
class Car {
static staticFunction () {
return 'static'
}
}
var car1 = new Car('Benz')
var car2 = new Car('BMW')
console.log(Car.staticFunction()) // static
console.log(car1.staticFunction()) // car1.staticFunction is not a function

Getter / Setter

ES5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Car = function () {
var _privateProperty = 5
this.getPrivate = function () {
return _privateProperty
}
this.setPrivate = function (value) {
_privateProperty = value
}
}
var c = new Car()
console.log(c.getPrivate()) // 5
c.setPrivate(6)
console.log(c.getPrivate()) // 6

ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Car {
constructor () {
this._privateProperty = 5
}
get prop () {
return this._privateProperty
}
set prop (value) {
this._privateProperty = value
}
}
var c = new Car()
console.log(c.prop) // 5
c.prop = 6
console.log(c.prop) // 6

建構子 (Constructor) 與繼承

ES5

支援存取private property的繼承寫法

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var Car = function () {
// constructor
// private property
var _wheels
// private method
var initialWheels = function () {
_wheels = 4
}
// call private method
initialWheels()
// pulic property
this.brand = 'default'
// public method
this.numberOfWheels = function () {
return _wheels
}
}
var Benz = function () {
Car.apply(this, arguments) // 相當於呼叫super(args)
this.brand = 'Benz'
this.getBrand = function () {
return this.brand
}
}
var BMW = function () {
Car.apply(this, arguments) // 相當於呼叫super(args)
this.getBrand = function () {
return this.brand
}
}
// 建立父類別實體 設定繼承關係
Benz.prototype = Object.create(Car.prototype) // Benz.prototype.__proto__ = Car.prototype
Benz.prototype.constructor = Benz
BMW.prototype = Object.create(Car.prototype) // BMW.prototype.__proto__ = Car.prototype
BMW.prototype.constructor = BMW
var benz = new Benz() // benz.__proto__ = Benz.prototype
var bmw = new BMW() // bmw.__proto__ = BMW.prototype
console.log(benz.getBrand()) // Benz
// benz.numberOfWheels() 有此方法
// 每次呼叫Car.apply(this, arguments)時複製到子類別的物件上
// 初始化物件時較慢,在run time想要動態改變numberOfWheels()的實作時
// 無法影響已經創建的子類別或父類別instances
console.log(benz.numberOfWheels()) // 4
console.log(bmw.getBrand()) // default
console.log(bmw.numberOfWheels()) // 4
console.log(benz instanceof Car) // true
console.log(bmw instanceof Car) // true
console.log(benz.__proto__ === Benz.prototype) // true
console.log(bmw.__proto__ === BMW.prototype) // true
console.log(Benz.prototype.__proto__ === Car.prototype) // true
console.log(BMW.prototype.__proto__ === Car.prototype) // true

ES5另一種繼承關係的寫法,在初始化物件比較有效率的做法
優點是可以動態更改父類別prototype chain上的方法實作
並且所有相關的父類別的instance與子類別的instance因為共用這個Car.prototype上的方法
所以就會一起被修改

缺點是這種寫法不支援prototype底下的方法操作private property

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var Car = function () {
// constructor
// public property
this.wheels = 4
// pulic property
this.brand = 'default'
}
// 這種寫法不支援存取private property
// 但可以在run time修改此Car.prototype.numberOfWheels()的實作
// 就可以透過prototype chain更改所有相關的instance功能 因為instance共用prototype chain上的方法
Car.prototype.numberOfWheels = function () {
return this.wheels
}
var Benz = function () {
Car.apply(this, arguments) // 複製繼承父類別public property
this.brand = 'Benz'
this.getBrand = function () {
return this.brand
}
}
var BMW = function () {
Car.apply(this, arguments) // 複製繼承父類別public property
this.getBrand = function () {
return this.brand
}
}
// 建立父類別實體 設定繼承關係
Benz.prototype = Object.create(Car.prototype) // Benz.prototype.__proto__ = Car.prototype
Benz.prototype.constructor = Benz
BMW.prototype = Object.create(Car.prototype) // BMW.prototype.__proto__ = Car.prototype
BMW.prototype.constructor = BMW
var benz = new Benz() // benz.__proto__ = Benz.prototype
var bmw = new BMW() // bmw.__proto__ = BMW.prototype
console.log(benz.getBrand()) // Benz
// benz.numberOfWheels() 找不到此方法,往prototype chain找 直到頂層Object為止 ->
// benz.__proto__.numberOfWheels() = Benz.prototype.numberOfWheels() 也找不到
// benz.__proto__.proto__.numberOfWheels() = Benz.prototype.__protoy__.numberOfWheels() == Car.prototype.numberOfWheels() // 找到了
console.log(benz.numberOfWheels()) // 4
console.log(bmw.getBrand()) // default
console.log(bmw.numberOfWheels()) // 4
console.log(benz instanceof Car) // true
console.log(bmw instanceof Car) // true
console.log(benz.__proto__ === Benz.prototype) // true
console.log(bmw.__proto__ === BMW.prototype) // true
console.log(Benz.prototype.__proto__ === Car.prototype) // true
console.log(BMW.prototype.__proto__ === Car.prototype) // true

ES6
Classes只是定義物件prototype的一個語法糖 (syntactic sugar)

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
39
40
41
42
43
44
45
46
47
48
class Car {
constructor () {
this._wheels = 4
this.brand = 'default'
}
// public method
numberOfWheels () {
return this._wheels
}
}
class Benz extends Car {
constructor () {
// 建立父類別實體 設定繼承關係 Benz.prototype.__proto__ == Car.prototype
super()
// 此處使用this前要先呼叫super()
this.brand = 'Benz'
}
getBrand () {
return this.brand
}
}
class BMW extends Car {
getBrand () {
return this.brand
}
}
let benz = new Benz()
let bmw = new BMW()
console.log(benz.getBrand()) // Benz
console.log(benz.numberOfWheels()) // 4
console.log(bmw.getBrand()) // default
console.log(bmw.numberOfWheels()) // 4
console.log(benz instanceof Car) // true
console.log(bmw instanceof Car) // true
console.log(benz.__proto__ === Benz.prototype) // true
console.log(bmw.__proto__ === BMW.prototype) // true
console.log(Benz.prototype.__proto__ === Car.prototype) // true
console.log(BMW.prototype.__proto__ === Car.prototype) // true

細講Prototype Chain

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
class A {
constructor() {
}
}
class B extends A {
constructor() {
super()
}
}
let a = new A() // A
let b = new B() // B
let o = new Object() // Object
console.log(b.__proto__ === B.prototype) // true
console.log(a.__proto__ === A.prototype) // true
console.log(o.__proto__ === Object.prototype) // true
console.log(B.prototype.__proto__ === A.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true
console.log(A.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
console.log(B.prototype.constructor === B) // true
console.log(A.prototype.constructor === A) // true
console.log(Function.prototype.constructor === Function) // true
console.log(Object.prototype.constructor === Object) // true
console.log(B.__proto__ === A) // true
console.log(A.__proto__ === Function.prototype) // true
console.log(Function.__proto__ === Function.prototype) // true
console.log(Object.__proto__ === Function.prototype) // true

補充: 關於instanceof的實作

1
2
3
4
5
6
7
8
function instanceOf(object, constructor) {
while (object != null) {
if (object == constructor.prototype)
return true
object = object.__proto__
}
return false
}

小節: 繼承不是萬靈丹,只是一種reuse程式碼的手段
錯誤的繼承反而會導致一場災難,所以design pattern有一個心法為

多用合成,少用繼承

有時候composition就能更有語意的解決程式碼的重用需求,就不需要用到繼承

補充: 物件的複製 Shallow Copy與Deep Copy

請參考其他資料