物件與繼承
宣告一個物件
ES5中的物件
宣告一個最簡單的物件
javascript中函式
也是一種物件,稱一級函式
(First class functions)
意思是任何你對其他類型(Objects, String, Boolean, Numbers)做的事
你也可以對Function做
對於支持函式可如數值一樣指定給變數的語言
我們稱函式在這個語言中是一等(First-class)函式或一級函式
更詳細的說明可見良葛格的筆記
1 2 3 4
| 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) console.log(car2.name) console.log(Car.staticObject) console.log(car1.staticObject)
|
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) console.log(car2.name) console.log(Car.staticObject) console.log(car1.staticObject)
|
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()) console.log(car1.staticFunction())
|
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()) console.log(car1.staticFunction())
|
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()) c.setPrivate(6) console.log(c.getPrivate())
|
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) c.prop = 6 console.log(c.prop)
|
建構子 (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 () { var _wheels var initialWheels = function () { _wheels = 4 } initialWheels() this.brand = 'default' this.numberOfWheels = function () { return _wheels } } var Benz = function () { Car.apply(this, arguments) this.brand = 'Benz' this.getBrand = function () { return this.brand } } var BMW = function () { Car.apply(this, arguments) this.getBrand = function () { return this.brand } } Benz.prototype = Object.create(Car.prototype) Benz.prototype.constructor = Benz BMW.prototype = Object.create(Car.prototype) BMW.prototype.constructor = BMW var benz = new Benz() var bmw = new BMW() console.log(benz.getBrand()) console.log(benz.numberOfWheels()) console.log(bmw.getBrand()) console.log(bmw.numberOfWheels()) console.log(benz instanceof Car) console.log(bmw instanceof Car) console.log(benz.__proto__ === Benz.prototype) console.log(bmw.__proto__ === BMW.prototype) console.log(Benz.prototype.__proto__ === Car.prototype) console.log(BMW.prototype.__proto__ === Car.prototype)
|
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 () { this.wheels = 4 this.brand = 'default' } Car.prototype.numberOfWheels = function () { return this.wheels } var Benz = function () { Car.apply(this, arguments) this.brand = 'Benz' this.getBrand = function () { return this.brand } } var BMW = function () { Car.apply(this, arguments) this.getBrand = function () { return this.brand } } Benz.prototype = Object.create(Car.prototype) Benz.prototype.constructor = Benz BMW.prototype = Object.create(Car.prototype) BMW.prototype.constructor = BMW var benz = new Benz() var bmw = new BMW() console.log(benz.getBrand()) console.log(benz.numberOfWheels()) console.log(bmw.getBrand()) console.log(bmw.numberOfWheels()) console.log(benz instanceof Car) console.log(bmw instanceof Car) console.log(benz.__proto__ === Benz.prototype) console.log(bmw.__proto__ === BMW.prototype) console.log(Benz.prototype.__proto__ === Car.prototype) console.log(BMW.prototype.__proto__ === Car.prototype)
|
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' } numberOfWheels () { return this._wheels } } class Benz extends Car { constructor () { 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()) console.log(benz.numberOfWheels()) console.log(bmw.getBrand()) console.log(bmw.numberOfWheels()) console.log(benz instanceof Car) console.log(bmw instanceof Car) console.log(benz.__proto__ === Benz.prototype) console.log(bmw.__proto__ === BMW.prototype) console.log(Benz.prototype.__proto__ === Car.prototype) console.log(BMW.prototype.__proto__ === Car.prototype)
|
細講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() let b = new B() let o = new Object() console.log(b.__proto__ === B.prototype) console.log(a.__proto__ === A.prototype) console.log(o.__proto__ === Object.prototype) console.log(B.prototype.__proto__ === A.prototype) console.log(Function.prototype.__proto__ === Object.prototype) console.log(A.prototype.__proto__ === Object.prototype) console.log(Object.prototype.__proto__ === null) console.log(B.prototype.constructor === B) console.log(A.prototype.constructor === A) console.log(Function.prototype.constructor === Function) console.log(Object.prototype.constructor === Object) console.log(B.__proto__ === A) console.log(A.__proto__ === Function.prototype) console.log(Function.__proto__ === Function.prototype) console.log(Object.__proto__ === Function.prototype)
|
補充: 關於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
請參考其他資料