隨手扎
7天搞懂JS進階議題(day04)-Class & Constructor: 吃語法糖別噎到
本系列文章討論JS 物件導向設計相關的特性。 不含CSS,不含HTML!
建議先有些JS基礎再繼續閱讀。
你也可以看看從零開始遲來的Web開發筆記
雖然是「7天寫作松」挑戰,但同樣可以視為系列後續文章
No CSS! No HTML! No Browser!
Just need programming language
現在你應該已經有發車前的基礎準備了。繫緊安全帶,撈思跡要踩油門加速了!
關於class
這個關鍵字,JS將其作為保留字好一段時間,直至ES6標準的制定,再經過瀏覽器漫長的實做,至今才有class
的語法糖可以使用。
ECMAScript 6 中引入了類別 (class) 作為 JavaScript 現有原型程式(prototype-based)繼承的語法糖。類別語法並不是要引入新的物件導向繼承模型到 JavaScript 中,而是提供一個更簡潔的語法來建立物件和處理繼承。
昨天,已經有了prototype-based的概念,這將有助於你更安全的吃這個語法糖而不噎到。偌你還不清楚JS裡的Prototype,建議你坐時光機回昨天看看。
在第二天說明過new Operator建立物件的方法。當時給的例子如下:
function FooConstructor(name){
this.name = name;
this.hello = function(){
console.log(`Hello, ${this.name}`);
}
}
var obj2 = new FooConstructor("Foo");
obj2.hello() // Hello, Foo
今天要改寫成class
語法糖的形式,其實這幾乎是等價的,因為「類別實際上是一種特別的函數」1。
類別實際上是一種特別的函數(functions),就跟你可以定義函數敘述和函數宣告一樣,類別的語法有兩個元件:類別敘述(class expressions)和類別宣告(class declarations)。
class _Foo{
constructor(name){
this.name = name;
}
hello(){
console.log(`Hello, ${this.name}`);
}
}
var obj0 = new _Foo("_Foo");
obj0.hello() // Hello, _Foo
你可以使用 類別敘述(class expressions) 或 類別宣告(class declarations) 的方式定義類別,上面範例就是類別宣告。同函式有 函式敘述(function expressions) 2,也可使用類別敘述:
var Foo = class {
constructor(name){
this.name = name;
}
hello(){
console.log(`Hello, ${this.name}`);
}
}
var obj0 = new Foo("Foo");
obj0.hello() // Hello, Foo
// class _Foo{} // error! 重複宣告_Foo
這差別在於類別宣告會加以保護,無法再次宣告(很像let
)。不過由於類別敘述更好用於測試示例(因為可以複寫),故本文之後都會使用類別敘述,但實務上,我會更推薦你使用類別宣告。
現在,或許你明白了為什麼第二天會使用FooConstructor
的命名方式,這恰好可以和class Foo{constructor(){}}
相對應。此外其實還有更多細節。
Prototype的consructor
之前還看過這樣的例子:
var obj3 = {};
obj3.constructor = FooConstructor;
obj3.constructor("Kitty");
obj3.hello(); // => Hello, Kitty
var obj4 = {};
obj4.__proto__ = FooConstructor.prototype;
obj4.constructor("K-on"); // 完了,暴露宅屬性
obj4.hello(); // => Hello, K-on
Object.is(obj3.constructor, obj4.constructor);
obj4.constructor
其實也就是FooConstructor.prototype.constructor
,其實也是FooConstructor
Object.is(obj4.constructor, FooConstructor.prototype.constructor); // => true
Object.is(obj4.constructor, FooConstructor); // => true
回到Foo
,看看是不是一樣。
var obj0 = new Foo("Foo");
Object.is(obj0.constructor, Foo.prototype.constructor); // => true
Object.is(obj0.constructor, Foo); // => true
Beingo!
關於類別方法
Foo
建立的物件和FooConstructor
建立的物件,對於方法的處理還有一些不同。Foo
物件的方法是在obj.prototype
上,另一個是在實例上。
var obj2 = new FooConstructor("Foo");
var obj0 = new Foo("Foo");
Object.getOwnPropertyNames(obj2); // => [ 'name', 'hello' ]
Object.getOwnPropertyNames(obj0); // => [ 'name']
obj2.__proto__.hello // => undefined
obj0.__proto__.hello // => [Function: hello]
作業 - FooConstructor
和Foo
改成同樣形式
其實FooConstructor
可以改成跟Foo
又更相似的行為,你能做到嗎?
絕對不是因為我懶!!
繼承
Bar = class extends Foo{
bye(){
console.log(`Bye Bye, ${this.name}`);
}
}
obj2 = new Bar("Disney");
obj2.hello(); // => Hello, Disney
obj2.bye(); // => Bye Bye, Disney
透過extends
可以讓Bar
繼承Foo
。既然是語法糖,我們來檢查看看建立的物件是否符合Prototype Chain:
console.log(obj2.__proto__); // => Bar {}
console.log(obj2.__proto__.__proto__); // => Foo {}
Object.is(obj2.__proto__.__proto__, Foo.prototype); // => true
obj2 instanceof Foo // => true
看上去都沒問題呢!不過你知道函數Bar
本身也多做了一些事情嗎?
Object.is(Bar.__proto__, Foo); // => true
Bar instanceof Function; //ture
Object.is(Bar.__proto__.__proto__, Function.prototype); // => true
對…Bar
的原形鏈上不是直接到Function
。當然拉,以上很多內容幾乎都可以魔改,但現在是否有更多概念了呢?
小節
透過class
和extends
語法糖,可以使用很像Java
的語法建立、繼承物件。在重新看一次Bar
範例:
Bar = class extends Foo{
constructor(name){
super(name);
this.last_name = "Bar";
}
bye(){
console.log(`Bye Bye, ${this.name} ${this.last_name}`);
}
}
obj2 = new Bar("Disney");
obj2.hello(); // => Hello, Disney
obj2.bye(); // => Bye Bye, Disney Bar
你都會了嗎?
我偷偷加了
super()
。不過相信已經懂Prototype chain的你能夠自己理解發生什麼事情
類別靜態方法(class static method): 寫在最後
最後在補充個我認為比較少用到的語法糖static
。
物件方法的尋找,除了物件本身外,就是在原形鏈上尋找。但有時,會需要直接從類別呼叫,需要類別方法,這時static
關鍵字就可以幫助到。
var Foo = class {
constructor(name){
this.name = name;
}
hello(){
console.log(`Hello, ${this.name}`);
}
static helloWorld(){
console.log("Hello, World");
}
}
Foo.helloWorld(); // => Hello, World
使用static
建立的類別靜態方法,之所以可以直接在類別呼叫,是因為其建立在的不是在prototye
上,而是在類別本身上。
Foo.prototype.name = "Default";
Foo.prototype.hello(); // => Hello, Default
Foo.helloWorld(); // => Hello, World
注意到兩者呼叫方式的不同了嗎?這邊關於預設的屬性"Defalut"
是怎麼運作的就不多做解釋了。來看看helloWorld
,他會在Foo
類別的屬性裡面:
Object.getOwnPropertyNames(Foo);
// [ 'length', 'prototype', 'helloWorld', 'name' ]
目前我想不到怎樣用比較好,以下例子可能不好。
你可能會希望像Ruby那樣建立物件,而不是使用new Operator。
※ 我以前有這種需求的時候,都是用工廠模式,然後函式名稱加個new
的前綴😂。
s = String.new "Hello, World"
在了解後,你可以這樣做:
var Foo = class {
constructor(name){
this.name = name;
}
hello(){
console.log(`Hello, ${this.name}`);
}
static new(){
return new Foo(...arguments);
}
}
obj0 = Foo.new("Dell");
obj0.hello();// => Hello, Dell