使用類

JavaScript 是一種基於原型的語言——物件的行為由其自身的屬性和其原型的屬性來指定。然而,隨著的加入,物件層次結構的建立以及屬性及其值的繼承更符合其他面嚮物件語言(例如 Java)。在本節中,我們將演示如何從類建立物件。

在許多其他語言中,_類_或建構函式與_物件_或例項是明確區分的。在 JavaScript 中,類主要是在現有原型繼承機制之上的一種抽象——所有模式都可以轉換為基於原型的繼承。類本身也是普通的 JavaScript 值,並且有它們自己的原型鏈。事實上,大多數普通的 JavaScript 函式都可以用作建構函式——你使用 `new` 運算子和建構函式來建立新物件。

在本教程中,我們將使用抽象良好的類模型,並討論類提供了哪些語義。如果你想深入瞭解底層原型系統,可以閱讀繼承與原型鏈指南。

本章假設你對 JavaScript 已有一些瞭解,並且使用過普通物件。

類概覽

如果你有一些 JavaScript 實踐經驗,或者一直跟著指南學習,你可能已經使用過類,即使你沒有建立過一個。例如,這對你來說可能很熟悉

js
const bigDay = new Date(2019, 6, 19);
console.log(bigDay.toLocaleDateString());
if (bigDay.getTime() < Date.now()) {
  console.log("Once upon a time...");
}

在第一行中,我們建立了`Date`類的一個例項,並將其命名為 `bigDay`。在第二行中,我們在 `bigDay` 例項上呼叫了一個方法`toLocaleDateString()`,它返回一個字串。然後,我們比較了兩個數字:一個由`getTime()`方法返回,另一個直接從 `Date` 類_本身_呼叫,例如`Date.now()`

Date 是 JavaScript 的內建類。從這個例子中,我們可以得到一些關於類作用的基本概念

  • 類透過`new`運算子建立物件。
  • 每個物件都帶有一些由類新增的屬性(資料或方法)。
  • 類本身儲存一些屬性(資料或方法),這些屬性通常用於與例項進行互動。

這些對應於類的三個關鍵特性

  • 建構函式;
  • 例項方法和例項欄位;
  • 靜態方法和靜態欄位。

宣告類

類通常透過_類宣告_建立。

js
class MyClass {
  // class body...
}

在類體中,有多種特性可用。

js
class MyClass {
  // Constructor
  constructor() {
    // Constructor body
  }
  // Instance field
  myField = "foo";
  // Instance method
  myMethod() {
    // myMethod body
  }
  // Static field
  static myStaticField = "bar";
  // Static method
  static myStaticMethod() {
    // myStaticMethod body
  }
  // Static block
  static {
    // Static initialization code
  }
  // Fields, methods, static fields, and static methods all have
  // "private" forms
  #myPrivateField = "bar";
}

如果你來自 ES6 之前的世界,你可能更熟悉使用函式作為建構函式。上述模式大致可以轉換為以下使用函式建構函式的形式

js
function MyClass() {
  this.myField = "foo";
  // Constructor body
}
MyClass.myStaticField = "bar";
MyClass.myStaticMethod = function () {
  // myStaticMethod body
};
MyClass.prototype.myMethod = function () {
  // myMethod body
};

(function () {
  // Static initialization code
})();

注意:私有欄位和方法是類中的新特性,在函式建構函式中沒有簡單的等價物。

構建類

宣告類後,你可以使用`new`運算子建立它的例項。

js
const myInstance = new MyClass();
console.log(myInstance.myField); // 'foo'
myInstance.myMethod();

典型的函式建構函式既可以透過 `new` 構造,也可以不帶 `new` 呼叫。然而,嘗試不帶 `new` “呼叫”類將導致錯誤。

js
const myInstance = MyClass(); // TypeError: Class constructor MyClass cannot be invoked without 'new'

類宣告提升

與函式宣告不同,類宣告不會被提升(或者,在某些解釋中,會被提升但受限於暫時性死區),這意味著你不能在類宣告之前使用它。

js
new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {}

此行為類似於使用`let``const`宣告的變數。

類表示式

與函式類似,類宣告也有其表示式形式。

js
const MyClass = class {
  // Class body...
};

類表示式也可以有名稱。表示式的名稱只在類的作用域內可見。

js
const MyClass = class MyClassLongerName {
  // Class body. Here MyClass and MyClassLongerName point to the same class.
};
new MyClassLongerName(); // ReferenceError: MyClassLongerName is not defined

建構函式

類最重要的作用可能是充當物件的“工廠”。例如,當我們使用 `Date` 建構函式時,我們期望它提供一個新物件,該物件表示我們傳入的日期資料——然後我們可以使用例項公開的其他方法來操作它。在類中,例項的建立由建構函式完成。

例如,我們將建立一個名為 `Color` 的類,它代表一種特定的顏色。使用者透過傳入一個RGB三元組來建立顏色。

js
class Color {
  constructor(r, g, b) {
    // Assign the RGB values as a property of `this`.
    this.values = [r, g, b];
  }
}

開啟瀏覽器的開發者工具,將上面的程式碼貼上到控制檯中,然後建立一個例項

js
const red = new Color(255, 0, 0);
console.log(red);

你應該會看到類似這樣的輸出

Object { values: (3) […] }
  values: Array(3) [ 255, 0, 0 ]

你已成功建立了一個 `Color` 例項,並且該例項有一個 `values` 屬性,它是一個包含你傳入的 RGB 值的陣列。這與以下內容大致相同

js
function createColor(r, g, b) {
  return {
    values: [r, g, b],
  };
}

建構函式的語法與普通函式完全相同——這意味著你可以使用其他語法,例如剩餘引數

js
class Color {
  constructor(...values) {
    this.values = values;
  }
}

const red = new Color(255, 0, 0);
// Creates an instance with the same shape as above.

每次呼叫 `new` 都會建立一個不同的例項。

js
const red = new Color(255, 0, 0);
const anotherRed = new Color(255, 0, 0);
console.log(red === anotherRed); // false

在類建構函式中,`this` 的值指向新建立的例項。你可以向其賦值屬性,或讀取現有屬性(特別是方法——我們接下來會介紹)。

`this` 值將作為 `new` 的結果自動返回。建議不要從建構函式返回任何值——因為如果你返回一個非原始值,它將成為 `new` 表示式的值,並且 `this` 的值會被丟棄。(你可以閱讀其描述中關於 `new` 作用的更多資訊。)

js
class MyClass {
  constructor() {
    this.myField = "foo";
    return {};
  }
}

console.log(new MyClass().myField); // undefined

例項方法

如果一個類只有一個建構函式,它與一個只建立普通物件的 `createX` 工廠函式並沒有太大區別。然而,類的強大之處在於它們可以作為“模板”,自動為例項分配方法。

例如,對於 `Date` 例項,你可以使用一系列方法從單個日期值中獲取不同的資訊,例如年份月份星期幾等。你還可以透過 `setX` 對應方法(如`setFullYear`)來設定這些值。

對於我們自己的 `Color` 類,我們可以新增一個名為 `getRed` 的方法,它返回顏色的紅色值。

js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255

如果沒有方法,你可能會嘗試在建構函式中定義函式

js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
    this.getRed = function () {
      return this.values[0];
    };
  }
}

這也行得通。然而,一個問題是,每次建立 `Color` 例項時,都會建立一個新的函式,即使它們都做相同的事情!

js
console.log(new Color().getRed === new Color().getRed); // false

相反,如果你使用方法,它將在所有例項之間共享。一個函式可以在所有例項之間共享,但在不同例項呼叫時,其行為仍然可能不同,因為 `this` 的值是不同的。如果你好奇這個方法儲存在_哪裡_——它定義在所有例項的原型上,即 `Color.prototype`,這在繼承和原型鏈中詳細解釋。

類似地,我們可以建立一個名為 `setRed` 的新方法,它設定顏色的紅色值。

js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(0);
console.log(red.getRed()); // 0; of course, it should be called "black" at this stage!

私有欄位

你可能會想:當我們可以直接訪問例項上的 `values` 陣列時,為什麼要費力使用 `getRed` 和 `setRed` 方法呢?

js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
}

const red = new Color(255, 0, 0);
red.values[0] = 0;
console.log(red.values[0]); // 0

在面向物件程式設計中有一種哲學叫做“封裝”。這意味著你不應該訪問物件的底層實現,而是使用良好抽象的方法與其互動。例如,如果我們突然決定將顏色表示為HSL而不是

js
class Color {
  constructor(r, g, b) {
    // values is now an HSL array!
    this.values = rgbToHSL([r, g, b]);
  }
  getRed() {
    return this.values[0];
  }
  setRed(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.values[0]); // 0; It's not 255 anymore, because the H value for pure red is 0

使用者關於 `values` 意味著 RGB 值的假設突然崩潰,這可能會導致他們的邏輯出錯。因此,如果你是一個類的實現者,你會希望向使用者隱藏例項的內部資料結構,既能保持 API 簡潔,又能防止使用者程式碼在你進行一些“無害重構”時崩潰。在類中,這透過_私有欄位_來實現。

私有欄位是帶有 `#`(雜湊符號)字首的識別符號。雜湊是欄位名稱的組成部分,這意味著私有欄位永遠不會與公共欄位或方法名稱衝突。為了在類的任何地方引用私有欄位,你必須在類體中_宣告_它(你不能即時建立私有元素)。除此之外,私有欄位與普通屬性幾乎等同。

js
class Color {
  // Declare: every Color instance has a private field called #values.
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
console.log(red.getRed()); // 255

在類外部訪問私有欄位是一個早期的語法錯誤。語言可以防止這種情況發生,因為 `#privateField` 是一種特殊語法,因此它可以在評估程式碼之前進行一些靜態分析並找到所有私有欄位的使用。

js
console.log(red.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class

注意:在 Chrome 控制檯中執行的程式碼可以訪問類外部的私有元素。這是 DevTools 專屬的 JavaScript 語法限制放寬。

JavaScript 中的私有欄位是_硬私有_:如果類不實現暴露這些私有欄位的方法,則絕對沒有機制可以從類外部檢索它們。這意味著你可以安全地對類的私有欄位進行任何重構,只要暴露方法的行為保持不變。

我們將 `values` 欄位設為私有後,可以在 `getRed` 和 `setRed` 方法中新增更多邏輯,而不是讓它們成為簡單的直通方法。例如,我們可以在 `setRed` 中新增一個檢查,看它是否是一個有效的 R 值

js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  getRed() {
    return this.#values[0];
  }
  setRed(value) {
    if (value < 0 || value > 255) {
      throw new RangeError("Invalid R value");
    }
    this.#values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.setRed(1000); // RangeError: Invalid R value

如果我們讓 `values` 屬性暴露在外,我們的使用者可以很容易地透過直接賦值給 `values[0]` 來規避該檢查,並建立無效顏色。但是,透過一個良好封裝的 API,我們可以使程式碼更加健壯,並防止下游的邏輯錯誤。

一個類的方法可以讀取其他例項的私有欄位,只要它們屬於同一個類。

js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    // #values doesn't necessarily need to be accessed from this:
    // you can access private fields of other instances belonging
    // to the same class.
    return this.#values[0] - anotherColor.#values[0];
  }
}

const red = new Color(255, 0, 0);
const crimson = new Color(220, 20, 60);
red.redDifference(crimson); // 35

但是,如果 `anotherColor` 不是 `Color` 例項,那麼 `#values` 將不存在。(即使另一個類有一個同名的 `#values` 私有欄位,它也不是指同一件事,並且無法在此處訪問。)訪問不存在的私有元素會丟擲錯誤,而不是像普通屬性那樣返回 `undefined`。如果你不知道一個私有欄位是否存在於一個物件上,並且希望在不使用 `try`/`catch` 處理錯誤的情況下訪問它,你可以使用`in`運算子。

js
class Color {
  #values;
  constructor(r, g, b) {
    this.#values = [r, g, b];
  }
  redDifference(anotherColor) {
    if (!(#values in anotherColor)) {
      throw new TypeError("Color instance expected");
    }
    return this.#values[0] - anotherColor.#values[0];
  }
}

注意:請記住,`#` 是一個特殊的識別符號語法,你不能將欄位名像字串一樣使用。`"#values" in anotherColor` 將會查詢一個字面上名為 `"#values"` 的屬性,而不是一個私有欄位。

使用私有元素有一些限制:同一個名稱不能在單個類中宣告兩次,並且不能刪除它們。兩者都會導致早期的語法錯誤。

js
class BadIdeas {
  #firstName;
  #firstName; // syntax error occurs here
  #lastName;
  constructor() {
    delete this.#lastName; // also a syntax error
  }
}

方法、getter 和 setter 也可以是私有的。當你的類需要內部執行一些複雜操作,但其他程式碼部分不應該被允許呼叫時,它們會很有用。

例如,想象一下建立HTML 自定義元素,當被點選/輕觸/以其他方式啟用時,它們應該做一些相當複雜的事情。此外,當元素被點選時發生的那些相當複雜的事情應該僅限於這個類,因為 JavaScript 的其他部分將永遠不會(或不應該)訪問它。

js
class Counter extends HTMLElement {
  #xValue = 0;
  constructor() {
    super();
    this.onclick = this.#clicked.bind(this);
  }
  get #x() {
    return this.#xValue;
  }
  set #x(value) {
    this.#xValue = value;
    window.requestAnimationFrame(this.#render.bind(this));
  }
  #clicked() {
    this.#x++;
  }
  #render() {
    this.textContent = this.#x.toString();
  }
  connectedCallback() {
    this.#render();
  }
}

customElements.define("num-counter", Counter);

在這種情況下,幾乎所有欄位和方法都是該類的私有成員。因此,它向其餘程式碼呈現的介面本質上就像一個內建的 HTML 元素。程式的任何其他部分都無法影響 `Counter` 的任何內部結構。

訪問器欄位

`color.getRed()` 和 `color.setRed()` 允許我們讀取和寫入顏色的紅色值。如果你來自 Java 等語言,你會對這種模式非常熟悉。然而,在 JavaScript 中,僅僅為了訪問一個屬性而使用方法仍然有些不符合人體工程學。_訪問器欄位_允許我們像操作“實際屬性”一樣操作某些東西。

js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
  set red(value) {
    this.values[0] = value;
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 0

看起來這個物件有一個名為 `red` 的屬性——但實際上,例項上不存在這樣的屬性!只有兩個方法,但它們字首為 `get` 和 `set`,這使得它們可以像屬性一樣被操作。

如果一個欄位只有 getter 而沒有 setter,它將有效地是隻讀的。

js
class Color {
  constructor(r, g, b) {
    this.values = [r, g, b];
  }
  get red() {
    return this.values[0];
  }
}

const red = new Color(255, 0, 0);
red.red = 0;
console.log(red.red); // 255

嚴格模式下,`red.red = 0` 這一行會丟擲一個型別錯誤:“Cannot set property red of #<Color> which has only a getter”。在非嚴格模式下,賦值會被靜默忽略。

公共欄位

私有欄位也有它們的公共對應物,它們允許每個例項擁有一個屬性。欄位通常設計為獨立於建構函式的引數。

js
class MyClass {
  luckyNumber = Math.random();
}
console.log(new MyClass().luckyNumber); // 0.5
console.log(new MyClass().luckyNumber); // 0.3

公共欄位幾乎等同於將屬性分配給 `this`。例如,上面的示例也可以轉換為

js
class MyClass {
  constructor() {
    this.luckyNumber = Math.random();
  }
}

靜態屬性

透過 `Date` 示例,我們還遇到了`Date.now()`方法,它返回當前日期。這個方法不屬於任何日期例項——它屬於類本身。然而,它被放在 `Date` 類上,而不是作為全域性 `DateNow()` 函式公開,因為它主要在處理日期例項時有用。

注意:將實用方法加上它們所處理的字首被稱為“名稱空間”,並被認為是一種良好實踐。例如,除了較舊的、無字首的`parseInt()`方法之外,JavaScript 後來還添加了帶字首的`Number.parseInt()`方法,以表明它是用於處理數字的。

_靜態屬性_是一組定義在類本身而不是類的單個例項上的類特性。這些特性包括

  • 靜態方法
  • 靜態欄位
  • 靜態 getter 和 setter

一切也都有私有對應物。例如,對於我們的 `Color` 類,我們可以建立一個靜態方法來檢查給定的三元組是否是有效的 RGB 值

js
class Color {
  static isValid(r, g, b) {
    return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255;
  }
}

Color.isValid(255, 0, 0); // true
Color.isValid(1000, 0, 0); // false

靜態屬性與它們的例項對應物非常相似,除了

  • 它們都以 `static` 為字首,並且
  • 它們不能從例項訪問。
js
console.log(new Color(0, 0, 0).isValid); // undefined

還有一個特殊的構造叫做_靜態初始化塊_,它是一段在類首次載入時執行的程式碼塊。

js
class MyClass {
  static {
    MyClass.myStaticProperty = "foo";
  }
}

console.log(MyClass.myStaticProperty); // 'foo'

靜態初始化塊幾乎等同於在類聲明後立即執行一些程式碼。唯一的區別是它們可以訪問靜態私有元素。

extends 和繼承

類帶來的一個關鍵特性(除了透過私有欄位實現符合人體工程學的封裝)是_繼承_,這意味著一個物件可以“借用”另一個物件的大部分行為,同時用自己的邏輯覆蓋或增強某些部分。

例如,假設我們的 `Color` 類現在需要支援透明度。我們可能會嘗試新增一個新欄位來指示其透明度

js
class Color {
  #values;
  constructor(r, g, b, a = 1) {
    this.#values = [r, g, b, a];
  }
  get alpha() {
    return this.#values[3];
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha value must be between 0 and 1");
    }
    this.#values[3] = value;
  }
}

然而,這意味著每個例項——即使絕大多數不透明的例項(alpha 值為 1 的例項)——都將不得不擁有額外的 alpha 值,這不是很優雅。此外,如果功能不斷增加,我們的 `Color` 類將變得非常臃腫且難以維護。

相反,在面向物件程式設計中,我們會建立一個_派生類_。派生類可以訪問父類的所有公共屬性。在 JavaScript 中,派生類透過`extends`子句宣告,該子句指示它所繼承的類。

js
class ColorWithAlpha extends Color {
  #alpha;
  constructor(r, g, b, a) {
    super(r, g, b);
    this.#alpha = a;
  }
  get alpha() {
    return this.#alpha;
  }
  set alpha(value) {
    if (value < 0 || value > 1) {
      throw new RangeError("Alpha value must be between 0 and 1");
    }
    this.#alpha = value;
  }
}

有幾件事立刻引起了注意。首先是在建構函式中,我們呼叫了 `super(r, g, b)`。在訪問 `this` 之前呼叫`super()`是語言要求。`super()` 呼叫父類的建構函式來初始化 `this`——這裡大致等同於 `this = new Color(r, g, b)`。你可以在 `super()` 之前編寫程式碼,但在 `super()` 之前不能訪問 `this`——語言會阻止你訪問未初始化的 `this`。

父類完成修改 `this` 後,派生類可以執行自己的邏輯。這裡我們添加了一個名為 `#alpha` 的私有欄位,並提供了一對 getter/setter 與它們進行互動。

派生類繼承其父類的所有方法。例如,考慮我們在訪問器欄位部分新增到 `Color` 中的 `get red()` 訪問器——即使我們沒有在 `ColorWithAlpha` 中宣告它,我們仍然可以訪問 `red`,因為此行為由父類指定

js
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color.red); // 255

派生類還可以覆蓋父類的方法。例如,所有類都隱式繼承`Object`類,該類定義了一些基本方法,如`toString()`。然而,基本的 `toString()` 方法臭名昭著地無用,因為它在大多數情況下會列印 `[object Object]`

js
console.log(red.toString()); // [object Object]

相反,我們的類可以覆蓋它來列印顏色的 RGB 值

js
class Color {
  #values;
  // …
  toString() {
    return this.#values.join(", ");
  }
}

console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'

在派生類中,你可以使用 `super` 訪問父類的方法。這使你能夠構建增強方法並避免程式碼重複。

js
class ColorWithAlpha extends Color {
  #alpha;
  // …
  toString() {
    // Call the parent class's toString() and build on the return value
    return `${super.toString()}, ${this.#alpha}`;
  }
}

console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'

當你使用 `extends` 時,靜態方法也會相互繼承,因此你也可以覆蓋或增強它們。

js
class ColorWithAlpha extends Color {
  // …
  static isValid(r, g, b, a) {
    // Call the parent class's isValid() and build on the return value
    return super.isValid(r, g, b) && a >= 0 && a <= 1;
  }
}

console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false

派生類無權訪問父類的私有欄位——這是 JavaScript 私有欄位“硬私有”的另一個關鍵方面。私有欄位的作用域僅限於類體本身,不授予_任何_外部程式碼訪問許可權。

js
class ColorWithAlpha extends Color {
  log() {
    console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class
  }
}

一個類只能繼承一個類。這可以避免多重繼承中的問題,例如菱形問題。然而,由於 JavaScript 的動態性質,仍然可以透過類組合和混入來實現多重繼承的效果。

派生類的例項也是基類的例項

js
const color = new ColorWithAlpha(255, 0, 0, 0.5);
console.log(color instanceof Color); // true
console.log(color instanceof ColorWithAlpha); // true

為什麼使用類?

到目前為止,本指南一直是實用的:我們專注於_如何_使用類,但有一個問題尚未解答:_為什麼_要使用類?答案是:這取決於情況。

類引入了一種_正規化_,或者一種組織程式碼的方式。類是面向物件程式設計的基礎,它建立在繼承多型(尤其是_子型別多型_)等概念之上。然而,許多人從哲學上反對某些 OOP 實踐,因此不使用類。

例如,`Date` 物件臭名昭著的一個原因就是它們是_可變的_。

js
function incrementDay(date) {
  return date.setDate(date.getDate() + 1);
}
const date = new Date(); // 2019-06-19
const newDay = incrementDay(date);
console.log(newDay); // 2019-06-20
// The old date is modified as well!?
console.log(date); // 2019-06-20

可變性和內部狀態是面向物件程式設計的重要方面,但通常會使程式碼難以推理——因為任何看似無害的操作都可能產生意外的副作用並改變程式其他部分的行為。

為了重用程式碼,我們通常會擴充套件類,這會建立龐大的繼承模式層次結構。

A typical OOP inheritance tree, with five classes and three levels

然而,當一個類只能繼承另一個類時,通常很難清晰地描述繼承。通常,我們希望具有多個類的行為。在 Java 中,這是透過介面完成的;在 JavaScript 中,可以透過混入來完成。但歸根結底,它仍然不是很方便。

從好的方面來看,類是一種在更高層次上組織程式碼的強大方式。例如,如果沒有 `Color` 類,我們可能需要建立十幾個實用函式

js
function isRed(color) {
  return color.red === 255;
}
function isValidColor(color) {
  return (
    color.red >= 0 &&
    color.red <= 255 &&
    color.green >= 0 &&
    color.green <= 255 &&
    color.blue >= 0 &&
    color.blue <= 255
  );
}
// …

但是有了類,我們可以將它們都集中在 `Color` 名稱空間下,這提高了可讀性。此外,私有欄位的引入使我們能夠向下遊使用者隱藏某些資料,從而建立清晰的 API。

一般來說,當你想建立儲存自己的內部資料並公開大量行為的物件時,應該考慮使用類。以內建的 JavaScript 類為例

  • `Map``Set` 類儲存元素集合,並允許你使用 `get()`、`set()`、`has()` 等方法透過鍵訪問它們。
  • `Date` 類將日期儲存為 Unix 時間戳(一個數字),並允許你格式化、更新和讀取單個日期元件。
  • `Error` 類儲存有關特定異常的資訊,包括錯誤訊息、堆疊跟蹤、原因等。它是少數具有豐富繼承結構的類之一:有多個內建類,如`TypeError``ReferenceError`擴充套件了 `Error`。在錯誤的情況下,這種繼承允許細化錯誤的語義:每個錯誤類代表一種特定型別的錯誤,可以使用`instanceof`輕鬆檢查。

JavaScript 提供了以規範的面向物件方式組織程式碼的機制,但如何使用以及是否使用完全取決於程式設計師的判斷。