[day 04] 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]

作業 - FooConstructorFoo改成同樣形式

其實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。當然拉,以上很多內容幾乎都可以魔改,但現在是否有更多概念了呢?

小節

透過classextends語法糖,可以使用很像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

參考資料

#js #javascript #EMCAScript





寫了這麼久的JS,你還在物件之前的時代嗎?只有資料、函式可以用,破破的抽象化,不會難以維護?儘管JS起初並不以物件導向設計,但透過原形鏈設計,其仍然可以具有好維護的物件導向特色。本系列從最基礎的this,深入ES6之後的class。

留言討論