2021年12月16日

クラスの基本構文

オブジェクト指向プログラミングでは、クラスは、オブジェクトを作成するための拡張可能なプログラムコードテンプレートであり、状態(メンバ変数)の初期値と動作(メンバ関数またはメソッド)の実装を提供します。

Wikipedia

実際には、ユーザー、商品など、同じ種類のオブジェクトを多数作成する必要があることがよくあります。

すでにコンストラクタ、演算子 "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

ゲッター/セッター

リテラルオブジェクトと同様に、クラスにはゲッター/セッター、計算されたプロパティなどを組み込むことができます。

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 にゲッターとセッターを作成することで機能します。

計算された名前 [...]

ブラケット [...] を使用して計算されたメソッド名を使用した例を次に示します。

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 を失う」と呼ばれます。

この問題を修正するには、関数バインディングの章で説明されているように、2つのアプローチがあります。

  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 として提供する関数)、メソッド、ゲッター、セッターは MyClass.prototype に書き込まれます。

次の章では、継承やその他の機能を含め、クラスについてさらに詳しく学びます。

タスク

重要度: 5

Clock クラス(サンドボックスを参照)は、関数スタイルで記述されています。「クラス」構文で書き換えてください。

P.S. 時計はコンソールで刻々と時を刻んでいます。開いて確認してください。

タスク用のサンドボックスを開きます。

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 issue を送信するか、プルリクエストを送信してください。
  • 記事の内容で理解できないことがある場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code> タグを使用し、数行の場合は、<pre> タグで囲み、10 行を超える場合は、サンドボックスを使用します(plnkrjsbincodepen…)