2021 年 12 月 16 日

類別基本語法

在物件導向程式設計中,類別是可擴充的程式碼範本,用於建立物件,提供狀態 (成員變數) 的初始值和行為 (成員函式或方法) 的實作。

維基百科

在實務上,我們經常需要建立很多種類似物件,例如使用者、商品或其他任何東西。

正如我們從章節 建構函式、運算子「new」 所知,new function 可以協助我們達成此目的。

但在現代的 JavaScript 中,有一個更進階的「類別」建構,它引入了許多對物件導向程式設計有用的新功能。

「類別」語法

基本語法為

class MyClass {
  // class methods
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

然後使用 new MyClass() 來建立一個包含所有列出方法的新物件。

constructor() 方法會由 new 自動呼叫,因此我們可以在這裡初始化物件。

例如

class User {

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

  sayHi() {
    alert(this.name);
  }

}

// Usage:
let user = new User("John");
user.sayHi();

當呼叫 new User("John")

  1. 會建立一個新的物件。
  2. constructor 會執行指定的參數,並將其指定給 this.name

…然後我們就可以呼叫物件方法,例如 user.sayHi()

類別方法之間不能有逗號

初學開發人員常犯的錯誤是,在類別方法之間加上逗號,這會導致語法錯誤。

此處的表示法不能與物件文字混淆。在類別中,不需要逗號。

什麼是類別?

那麼,class 究竟是什麼?這並不是一個全新的語言層級實體,就像人們可能認為的那樣。

讓我們揭開任何魔法,看看類別的本質。這有助於理解許多複雜的方面。

在 JavaScript 中,類別是一種函式。

請看這裡

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// proof: User is a function
alert(typeof User); // function

class User {...} 結構實際上執行的動作是

  1. 建立一個名為 User 的函式,成為類別宣告的結果。函式程式碼取自 constructor 方法(如果我們沒有撰寫此方法,則假設為空)。
  2. 將類別方法(例如 sayHi)儲存在 User.prototype 中。

建立 new User 物件後,當我們呼叫其方法時,它會從原型中取得,正如 F.prototype 章節中所述。因此,物件可以存取類別方法。

我們可以說明 class User 宣告的結果如下

以下是內省它的程式碼

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// class is a function
alert(typeof User); // function

// ...or, more precisely, the constructor method
alert(User === User.prototype.constructor); // true

// The methods are in User.prototype, e.g:
alert(User.prototype.sayHi); // the code of the sayHi method

// there are exactly two methods in the prototype
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

不只是語法糖

人們有時會說 class 是「語法糖」(一種旨在讓事物更易於閱讀的語法,但不會引入任何新事物),因為我們實際上可以在完全不使用 class 關鍵字的情況下宣告相同的事物

// rewriting class User in pure functions

// 1. Create constructor function
function User(name) {
  this.name = name;
}
// a function prototype has "constructor" property by default,
// so we don't need to create it

// 2. Add the method to prototype
User.prototype.sayHi = function() {
  alert(this.name);
};

// Usage:
let user = new User("John");
user.sayHi();

此定義的結果大致相同。因此,確實有理由將 class 視為語法糖,用於定義建構函式及其原型方法。

儘管如此,還是有重要的差異。

  1. 首先,由 class 建立的函式會標記一個特殊的內部屬性 [[IsClassConstructor]]: true。因此,它與手動建立並不完全相同。

    語言在各種地方檢查該屬性。例如,與一般函數不同,它必須使用 new 呼叫

    class User {
      constructor() {}
    }
    
    alert(typeof User); // function
    User(); // Error: Class constructor User cannot be invoked without 'new'

    此外,在大部分 JavaScript 引擎中,類別建構函式的字串表示會以「class…」開頭

    class User {
      constructor() {}
    }
    
    alert(User); // class User { ... }

    還有其他差異,我們很快就會看到

  2. 類別方法不可列舉。類別定義會將 "prototype" 中所有方法的 enumerable 旗標設為 false

    這很好,因為如果我們對物件執行 for..in,通常不想要它的類別方法

  3. 類別總是 use strict。類別建構中的所有程式碼都會自動進入嚴格模式

此外,class 語法帶來了許多其他功能,我們稍後會探討

類別表達式

就像函數一樣,類別可以在另一個表達式中定義,傳遞、傳回、指定等

以下是類別表達式的範例

let User = class {
  sayHi() {
    alert("Hello");
  }
};

類似於命名函數表達式,類別表達式可以有名稱

如果類別表達式有名稱,則只會在類別內部可見

// "Named Class Expression"
// (no such term in the spec, but that's similar to Named Function Expression)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass name is visible only inside the class
  }
};

new User().sayHi(); // works, shows MyClass definition

alert(MyClass); // error, MyClass name isn't visible outside of the class

我們甚至可以這樣動態地「依需求」建立類別

function makeClass(phrase) {
  // declare a class and return it
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

// Create a new class
let User = makeClass("Hello");

new User().sayHi(); // Hello

Getter/setter

就像文字物件一樣,類別可以包含 getter/setter、運算屬性等

以下是使用 get/set 實作的 user.name 範例

class User {

  constructor(name) {
    // invokes the setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // Name is too short.

技術上來說,此類別宣告會在 User.prototype 中建立 getter 和 setter

運算名稱 […]

以下是使用方括號 [...] 的運算方法名稱範例

class User {

  ['say' + 'Hi']() {
    alert("Hello");
  }

}

new User().sayHi();

這些功能很容易記住,因為它們類似於文字物件

類別欄位

舊瀏覽器可能需要多重載入

類別欄位是語言最近新增的功能

以前,我們的類別只有方法

「類別欄位」是一種語法,允許新增任何屬性

例如,我們將 name 屬性新增到 class User

class User {
  name = "John";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

因此,我們只要在宣告中寫入 " = ",就可以了

類別欄位的重大差異在於,它們設定在個別物件上,而不是 User.prototype

class User {
  name = "John";
}

let user = new User();
alert(user.name); // John
alert(User.prototype.name); // undefined

我們也可以使用更複雜的表達式和函數呼叫來指定值

class User {
  name = prompt("Name, please?", "John");
}

let user = new User();
alert(user.name); // John

使用類別欄位建立繫結方法

函數繫結 章節中所示,JavaScript 中的函數有一個動態 this。它取決於呼叫的內容

因此,如果一個物件方法被傳遞並在另一個內容中呼叫,this 將不再是其物件的參考。

例如,此程式碼將顯示 undefined

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined

這個問題稱為「失去 this」。

有兩種方法可以修正它,如 函式繫結 章節中所討論的

  1. 傳遞一個包裝函式,例如 setTimeout(() => button.click(), 1000)
  2. 將方法繫結到物件,例如在建構函式中。

類別欄位提供了另一種相當簡潔的語法

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello

類別欄位 click = () => {...} 是根據每個物件建立的,每個 Button 物件都有個別的函式,其中 this 參照該物件。我們可以在任何地方傳遞 button.click,而 this 的值將永遠是正確的。

這在瀏覽器環境中特別有用,適用於事件監聽器。

摘要

基本的類別語法如下所示

class MyClass {
  prop = value; // property

  constructor(...) { // constructor
    // ...
  }

  method(...) {} // method

  get something(...) {} // getter method
  set something(...) {} // setter method

  [Symbol.iterator]() {} // method with computed name (symbol here)
  // ...
}

MyClass 在技術上是一個函式(我們提供為 constructor 的函式),而方法、getter 和 setter 則寫入 MyClass.prototype

在後面的章節中,我們將進一步了解類別,包括繼承和其他功能。

任務

重要性:5

Clock 類別(請參閱沙盒)是用函式風格編寫的。請使用「類別」語法重寫它。

附註:時鐘在主控台中滴答作響,請開啟它查看。

開啟一個沙盒進行任務。

class Clock {
  constructor({ template }) {
    this.template = template;
  }

  render() {
    let date = new Date();

    let hours = date.getHours();
    if (hours < 10) hours = '0' + hours;

    let mins = date.getMinutes();
    if (mins < 10) mins = '0' + mins;

    let secs = date.getSeconds();
    if (secs < 10) secs = '0' + secs;

    let output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs);

    console.log(output);
  }

  stop() {
    clearInterval(this.timer);
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), 1000);
  }
}


let clock = new Clock({template: 'h:m:s'});
clock.start();

在沙盒中開啟解答。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請 提交 GitHub 問題 或發起 pull 要求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,對於多行程式碼,請將它們包覆在 <pre> 標籤中,對於超過 10 行的程式碼,請使用沙盒(plnkrjsbincodepen…)