JavaScriptのクラスについて学ぼう【4.Setter/Getter編】

JavaScript

はじめに

JavaScriptにはSetterとGetterという機能があります。これらはオブジェクトやインスタンスのプロパティへのアクセスと操作を柔軟に行うための仕組みです。SetterとGetterはプロパティをカプセル化する際にも利用され、クラスやオブジェクトの実装において保守性を高める効果が期待されます。

この記事では、JavaScriptにおいてのSetter/Getterについて解説します。なお、この記事ではクラス構文でSetterとGetterについて学んでいきますが、これらはオブジェクトリテラルでオブジェクトを定義する場合でも使うことができます。

Setter/Getterの理解を深めることで、コードの読解力の向上や、拡張性・再利用性を意識したコーディングを行うための助けになるかと思います。

この記事はクラス構文についての基本的な知識があることが前提となっているので、これらについて学んでいない方は以下の記事を参照いただくことをオススメします。

この記事の対象者

  • クラス構文の基本を学んだ方
  • set/getの意味を理解したい方
  • 保守性を意識してコーディングしたい方

この記事で学べること

  • Setter/Getterの基本
  • Setter/Getterの具体的な役割
  • Setterによるエラーハンドリング
  • Getterの使いどころ
  • JavaScriptでのprivateの実装【おまけ】

Setter/Getterとは

SetterとGetterは、オブジェクトやクラスで生成されたインスタンスからプロパティにアクセスする際に、特定の挙動を定義するためのメソッドです。これらの機能を利用することで、プロパティの読み取りや変更時に特定の処理を実行でき、コードの柔軟性や保守性を向上させることができます。これらの主な役割は以下です。

  • Setter:プロパティへの値の代入
  • Getter:プロパティの値の取得

SetterとGetterはメソッドの一種ですが、一般的なプロパティへのアクセス方法と同じ記述を行うことで呼び出され、一見するとメソッドが実行されているように見えないという特徴があります。

class Car {
  // Setter
  set speed(value) { … }

  // Getter
  get speed() { return … }
}

const myCar = new Car();

// 以下はset speed()が呼び出されます
myCar.speed = 100;

// 以下はget speed()が呼び出されます
const myCarSpeed = myCar.speed;

上記のように、通常のプロパティの値の取得・変更するプロセスと変わらない記述でSetterとGetterは呼び出されるため、SetterとGetter内で特定の処理を行うことにより、インスタンスを利用する側のコードが簡潔になります。

たとえSetterとGetterを自分で使用する機会がなくても、これらの概念を理解しておくことで大規模な開発においてのコードの理解力が向上するかと思います。

Setter/Getterとカプセル化

SetterとGetterが使用されるケースとして、プロパティのカプセル化が挙げられます。カプセル化とは、クラスの外部からプロパティへ直接アクセスすることを制限することを意味します。カプセル化を行うことにより、予期せぬかたちでプロパティの値が書き換えられることを防ぐことができ、保守性の向上が期待されます。

ここでは、プロパティのカプセル化を例に挙げ、SetterとGetterの基本的な使い方について解説します。まずは前提として、カプセル化の対象となるプロパティを定義します。

class Car {
  constructor(speed) {
    // 以下のプロパティをカプセル化します
    this._speed = speed;
  }
}

_speedというプロパティをカプセル化し、SetterとGetterを使用してこの_speedプロパティへのアクセスを関節的に行っていきます。

ちなみに、プロパティ名の先頭のアンダースコア(_)はprivateであることを示す慣習的なものですが、privateであることを言語レベルで強制する効果はありません。privateなプロパティやメソッドは基本的に直接外部からアクセスされることを前提としませんが、アンダースコアによる表現はprivateであることが明示的であるとは言えないので、誤った使われ方をされる可能性があります。

Getterの基本

まずは、privateなプロパティの値を取得するためにGetterの基本について解説します。

Getterはプロパティの値を取得する際に呼び出され、通常のプロパティアクセスとは異なる処理を挿入できます。Getterはgetキーワードを使用します。Getterを追加してみましょう。

class Car {
  constructor(speed) {
    this._speed = speed;
  }

  // Getter
  get speed() {
    console.log('Getter called');
    return this._speed;
  }
}

Getterのポイントとして、Getterは「プロパティの値の取得」という性質上、プロパティのカプセル化においては基本的に、対象となるprivateなプロパティの値を返します。

get speed() {
  …
  // privateなプロパティを返します
  return this._speed;
}

Getterはメソッドの一種として考えることができますが、通常のメソッドの呼び出しとは異なる方法で呼び出します。以下のようには呼び出せません。

// Getterは通常の方法では呼び出せません
myCar.speed(200);
// Uncaught TypeError: myCar.speed is not a function

Getterのメソッド名はプロパティ名として作用します。通常のプロパティへのアクセスと同じ方法でメソッド名を記述することでGetterが呼び出されます。

const myCar = new Car(100);

// speedへのアクセス時にGetterが実行されます
console.log(myCar.speed);
/*
結果:
Getter called
100
*/

このように、Getterを追加することで関節的に_speedプロパティへアクセスすることができるようになります。続いて、Setterについて見ていきましょう。

Setterの基本

Setterはプロパティの値を変更する際に呼び出され、新しい値に対するバリデーションや変換処理などを組み込むことができます。Setterはsetキーワードを使用します。_speedプロパティに値をセットするためのSetterを定義してみましょう。

class Car {
  constructor(speed) {
    this._speed = speed;
  }

  get speed() {
    console.log('Getter called');
    return this._speed;
  }

  // Setterを追加
  set speed(value) {
    console.log('Setter called');
    this._speed = value;
  }
}

上記の例では、speed()という名前のSetterメソッドを定義しています。プロパティのカプセル化においては、GetterとSetterのメソッド名を同じにすることにより、メソッドであることを意識することなくprivateなプロパティへアクセスすることができるようになります。

Setterの動作について見ていきましょう。SetterもGetterと同様、通常のメソッドと同じ方法では呼び出すことができません。

const myCar = new Car(100);

myCar.speed(200);
// Uncaught TypeError: myCar.speed is not a function

また、Setterのメソッド名もGetterと同様にプロパティ名として作用します。通常のプロパティへの値の代入と同じ方法でメソッド名を記述することで、Setterが呼び出されます。

const myCar = new Car(100);

// speedへの値の代入時にSetterが実行されます
myCar.speed = 120;

console.log(myCar.speed);
/*
結果:
Setter called
Getter called
120
*/

この時、speedに代入した値がSetterの引数(value)に渡ります。Setterのポイントとして、Setterは「プロパティへの値の代入」という性質上、プロパティのカプセル化においては基本的に、対象となるprivateプロパティへ値を代入することになります。

// valueに120が渡されます
set speed(value) {
  console.log('Setter called');
  // privateなプロパティへ代入します
  this._speed = value;
}

このように、Setterを追加することで関節的に_speedプロパティへ値を代入することができるようになります。

この章のまとめ

プロパティのカプセル化においてのGetter/Setter のポイントを簡潔にまとめておきます。ポイントは以下です。

  • privateプロパティの定義
  • GetterとSetterのメソッド名を一致させる
  • Getterはprivateプロパティを返す
  • Setterはprivateプロパティに値を代入する
class Car {
  constructor(speed) {

    // プロパティ名に「 _ 」を付ける
    this._speed = speed;
  }

  // Setterと一致
  get speed() {

    // _speedをreturnする
    return this._speed;
  }

  // Getterと一致
  set speed(value) {

    // _speedに値を代入する
    this._speed = value;
  }
}

また、privateを表すアンダースコアは慣習的に広く利用されていますが、分かりにくいと感じる場合は「privateSpeed」のように記述しても良いと思います。

Getter/Setterの具体的な使い方

前述のとおり、GetterとSetterを利用することで、プロパティへアクセスする方法と同じ記述でメソッドを呼び出すことができます。しかし、初学者の方の中には「プロパティの取得・変更をわざわざメソッドにする必要はあるの?」と疑問に思われる方もいるかと思います。

確かに、GetterやSetterを利用しなくても求める実装を行うことは可能ですが、GetterとSetterを利用するメリットも少なからずあり、GetterとSetterを利用する目的はクラスやオブジェクトの役割などによってさまざまです。ここでは、クラス構文を学んだJavaScript初学者の方にオススメのSetterの使い方として、「インスタンスのプロパティを安全に保つ方法」について解説します。

プロパティを安全に保つとはすなわち、適切にエラーハンドリングを行うということです。JavaScriptにおいてのインスタンス(オブジェクト)のプロパティは変数のようなものであり、何かしらの対策を行わなければ意図しないデータに上書きされる可能性があります。これは、たとえconstキーワードを利用してオブジェクトやインスタンスを宣言している場合でも同じです。

Setterを利用することで、プロパティの値が期待どおりであることを保証させることができます。プロパティを安全に保つ方法について学び、GetterとSetterの役割について理解を深めていきましょう。

Setterでエラーハンドリングする

通常、プロパティの安全性が損なわれるケースとは値が意図しないデータに変更された場合です。なので、プロパティを安全に保つためにはSetter内でエラーハンドリングを行うことが効果的です。

ここまでのサンプルコードのCarクラスにおける_speedプロパティの値について考えてみます。前提として、_speedプロパティは数値型である必要があるとしましょう。つまり、_speedプロパティに数値型以外のデータを代入しようとした場合はエラーを発生させる必要があります。簡単な例として、以下のようになります。

class Car {
  …
  set speed(value) {
    console.log('Setter called');

    // 以下を追加
    if (typeof value !== 'number') {
      const errMsg = 'The speed property must be of type number.';
      throw new Error(errMsg);
    }

    this._speed = value;
  }
}

Errorオブジェクトをthrowすることでエラーが発生した場合に強制的に処理を中断することができます。上記の場合、Setterが呼び出されたタイミングでエラーが発生し処理を中断します。

const myCar = new Car(100);
myCar.speed = '100';

// 以下は実行されません
console.log(myCar.speed);

/*
結果:
Uncaught Error: The speed property must be of type number. at …
*/

条件分岐を追加することで、より厳密に求める値であることを保証できます。数値型で予期せぬ結果を招く原因となる「Infinity」と「NaN」の対策を追加してみます(InfinityやNaNを判定するためのisFinite()という便利な関数があります。興味のある方は調べてみると良いでしょう)。

set speed(value) {
  …
  // Infinity対策
  if (value === Infinity) {
    const errMsg = 'value is infinity.';
    throw new Error(errMsg);
  }

  // NaN対策
  if (isNaN(value)) {
    const errMsg = 'value is NaN.';
    throw new Error(errMsg);
  }

  this._speed = value;
}

perseInt()を使用することで強制的に数値型に変換することもできます。

set speed(value) {
  …
  if (typeof !== 'number') {
    // 変換したことを警告しておくと親切です
    console.warn(
      `convert it to a numeric type.\n${typeof value} => number`
    );
    // 数値型に変換します
    value = parseInt(value);
  }
  …
}

これによりspeedプロパティの値は常に数値型であることが基本的に保証されますが、完全ではありあません。Carクラスのインスタンスを生成する際はSetterが呼び出されないため、この時にspeedに別の値が設定される可能性があります。

const myCar = new Car('100');
console.log(typeof myCar.speed); // string

この問題の解決は簡単で、Setterをconstructor()内で呼び出すことにより解決できます。

constructor(speed) {
  // 以下に修正
  this.speed = speed;
}

これでspeedが有効な数値型であることが完全に保証されることになります。最後に、エラーハンドリングの部分を関数化しましょう。全体のコードがすっきりして可読性・再利用性が向上する効果があります。

// 数値型に変換する関数
function validateNumber(value) {
  if (typeof value !== 'number') {
    console.warn(
      `convert it to a numeric type.\n${typeof value} => number`
    );
    num = parseInt(value);
  }
  if (value === Infinity) {
    const errMsg = 'value is infinity.';
    throw new Error(errMsg);
  }
  if (isNaN(value)) {
    const errMsg = 'value is NaN.';
    throw new Error(errMsg);
  }

  return value;
}

class Car {
  constructor(speed) {
    this.speed = speed;
  }
  get speed() {
    console.log('Getter called');
    return this._speed;
  }

  set speed(value) {
    console.log('Setter called');
    this._speed = validateNumber(value);
  }
}

このように、Setterを利用することでプロパティの値に対して柔軟にエラーハンドリングを行うことができます。エラーハンドリングは一般的にtry/catch構文が使用されますが、エラーハンドリングを学ぶための取っ掛かりとして、上記の方法なら簡単に実装できるかと思います。

Getterの役割について考える

安全性を担保するための処理はSetterで行う方が自然ですが、Getterでも同じ方法でエラーハンドリングを行うことはできます。

class Car {
  constructor(speed) {
    this.speed = speed;
    this.logging = true;
  }

  get speed() {
    console.log('Getter called');

    // 以下に変更
    return validateNumber(this._speed);
  }

  set speed(value) {
    console.log('Setter called');
    this._speed = value;
  }
}

しかし、上記のようにGetterでデータの安全性を担保するための処理を実装することは望ましくありません。理由として、speedプロパティにアクセスする度にエラーハンドリングが実行されるのでパフォーマンスの面で問題があるためです。本来、データの安全性はプロパティに値をセットするタイミングのみで十分です。

また、Getterの役割は「値の取得」であり、Getterにそれ以外の役割を持たせることはコードを予測しにくくさせます。

では、どのようにGetterを利用することができるでしょうか?ただprivateなプロパティを返すだけでは、Getterの機能を効果的に活用できているとは言えません。

Getterの良いところは、privateなプロパティを直接変更せずに値を加工することができる点です。Getterでは必ず何らかの値をreturnすることになりますが、これはprivateなプロパティそのものである必要はなく、複雑な処理を行ってその結果を返すこともできます。

get maxSpeed() {
  return Math.floor(this._speed * 1.2);
}

maxSpeedというGetter内ではthis._speedを加工してreturnしています。他の例も見てみましょう。

get speedMessage() {
  return `This car can travel at ${this._speed} km/h.`;
}

このように、Getterを使用することでprivateなプロパティそのものに変更を加えることなく値を利用することができます。

おまけ:_speedをprivateにする

GetterとSetterについては以上ですが、カプセル化の補足としてJavaScriptでのprivate機能について簡単に解説します。

ここまでのコードは_speedプロパティはアンダースコアによってprivateであることを示していました。かつてはJavaScriptにprivateなメソッドやプロパティを定義する機能がありませんでしたが、ECMAScript2022よりprivateを実装する機能が追加されました。

プロパティ名の先頭に「#」を記述することで、_speedプロパティへのクラス外からのアクセスを防ぐことができます。

function validateNumber(value) { … }

class Car {
 // 以下を追加
  #_speed;

  constructor(speed) {
    this.speed = speed;
  }
  get speed() {
    console.log('Getter called');
    return validateNumber(this.#_speed);
  }

  set speed(value) {
    console.log('Setter called');
    this.#_speed = validateNumber(value);
  }
}

const myCar = new Car(100);

// 以下はエラーになります
myCar.#_speed = 120;
// Uncaught SyntaxError: Private field '#_speed' must be declared in an enclosing class at …

簡単に使えるのでSetter/Getterと一緒に覚えておくと良いと思います。

最後に

この記事では、JavaScriptにおいてのSetter/Getterについて解説しました。Setter/Getterはクラス構文でしか定義できないものではなく、オブジェクトリテラルでも使うことができます。小規模な開発でクラスを定義するほどでもないような場合でも、可読性を意識したエラーハンドリングについて学ぶ機会を得ることができるかと思います。

昨今のフロントエンド開発では、JavaScriptを静的型付け言語に拡張した「TypeScript」というプログラミング言語を使用することが主流であり、この記事でご紹介したような単純なエラーハンドリングを行う必要性はTypeScriptではあまりないかもしれません。しかし、TypeScriptを扱うためにはJavaScriptの基本を理解していることが望ましく、Setter/Getterの利用そのものはTypeScriptでも使うことができます。

積極的にこれらの機能を利用してメリット・デメリットを理解しておくことは、JavaScriptによるウェブ開発のみならず、オブジェクト指向プログラミングにおいてのスキル向上に貢献します。

タイトルとURLをコピーしました