JavaScript ES6 class 關鍵字

在 ES6 中,引入了 Class (類別) 這個新的概念 (如果寫過 C++ 或 Java 等傳統語言應該非常熟悉),透過 class 這新的關鍵字,可以定義類別。

另外還引入了一些其他的新語法,來讓你更簡單直觀的用 JavaScript 寫 OOP (物件導向) 程式,但大多數語法只是語法糖 (syntactical sugar),並不是重新設計一套物件繼承模型 (object-oriented inheritance model),只是讓你更方便操作 JavaScript 既有的原型繼承模型 (prototype-based inheritance)

在 ES6 你可以用 class 語法定義一個類別:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

上面我們定義了一個 Animal 類別:

  • 其中 constructor 方法用來定義類別的建構子 (constructor)
  • 方法 (method) 的定義也有新語法,我們在 Animal 類別中定義了 speak() 方法,你可以看到新語法省略了 function 關鍵字和冒號 :

我們說過新語法只是語法糖,底層還是 prototype-based 的關係:

let a = new Animal('Elephant');

// true
a.constructor === Animal.prototype.constructor;

// true
a.speak === Animal.prototype.speak;

// true
Animal.prototype.constructor === Animal;

// "function"
typeof Animal;

extends

extends 關鍵字用作類別繼承:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');

// 顯示 Mitzie barks.
d.speak();

super

如果子類別 (sub-class) 有定義自己的 constructor,必須在 constructor 方法中顯示地調用 super(),來調用父類別的 constructor,否則會出現錯誤 - ReferenceError: this is not defined

而且在 sub-class constructor 中,必須先執行完 super() 後,才能引用 this 關鍵字,否則也會出現錯誤 - ReferenceError: this is not defined

這是因為在 ES6 中,是先建立父類別 (parent class) 的物件實例 this (所以必須先執行 super()),然後再用子類別的 constructor 修改 this。

class Car {
  constructor() {
    console.log('Creating a new car');
  }
}

class Porsche extends Car {
  constructor() {
    super();
    console.log('Creating Porsche');
  }
}

let c = new Porsche();

// 依序顯示
// Creating a new car
// Creating Porsche

super 關鍵字有兩種用法:

  1. 當作函數 super(),只能在子類別的 constructor 中使用,在其他地方用會報錯 - SyntaxError: 'super' keyword unexpected here
  2. 當作物件 super,在一般方法中使用,用來引用父類別的方法和屬性
class Cat {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

let bigCat = new Lion('Hoo');

bigCat.speak();
// 依序顯示
// Hoo makes a noise.
// Hoo roars.

另外,透過 super 調用父類別的方法時,super 會綁定子類別的 this (而不是父類別的 this):

class A {
  constructor() {
    this.x = 1;
  }

  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }

  foo() {
    super.print();
  }
}

let b = new B();

// 顯示 2 而不是 1
b.foo();

static

static 關鍵字用來定義靜態方法 (static method)。

class StaticMethodCall {
  static staticMethod() {
    return 'Static method has been called';
  }

  static anotherStaticMethod() {
    // 你可以用 this 來調用其他的 static method
    return this.staticMethod() + ' from another static method';
  }
}

// 顯示 Static method has been called
StaticMethodCall.staticMethod();

// 顯示 Static method has been called from another static method
StaticMethodCall.anotherStaticMethod();

父類別上的靜態方法也可以透過 super 來調用:

class Triple {
  static triple(n) {
    if (n === undefined) {
      n = 1;
    }
    return n * 3;
  }
}

class BiggerTriple extends Triple {
  static triple(n) {
    return super.triple(n) * super.triple(n);
  }
}

// 3
console.log(Triple.triple());

// 18
console.log(Triple.triple(6));

var tp = new Triple();

// 81
console.log(BiggerTriple.triple(3));

// 報錯
// TypeError: tp.triple is not a function
console.log(tp.triple());

私有屬性與方法 (Private Fields & Methods)

在傳統的 JavaScript Class 中,所有的屬性都是公開的 (Public),外部可以直接存取。ES2022 (ES13) 正式引入了私有欄位,只要在變數名稱前加上 # 符號,該屬性或方法就只能在類別內部被存取。

class Counter {
  // 私有屬性
  #count = 0;

  increment() {
    this.#count++;
  }

  getCount() {
    return this.#count;
  }

  // 私有方法
  #logSecret() {
    console.log('這是私有秘密');
  }
}

const c = new Counter();
c.increment();
console.log(c.getCount()); // 1

// 報錯,外部無法存取私有屬性
// SyntaxError: Private field '#count' must be declared in an enclosing class
// console.log(c.#count);

靜態初始化區塊 (Static Initialization Blocks)

當你需要執行複雜的靜態屬性初始化時,可以使用 static { ... } 區塊。這在需要處理異常狀況或執行多行初始化邏輯時非常有用。

class Database {
  static connection;

  static {
    try {
      // 執行複雜的初始化邏輯
      this.connection = 'Initialized';
    } catch (e) {
      console.error('初始化失敗');
    }
  }
}