JavaScript | ES6+ 文法:クラス構文 – getter / setter

JavaScript JavaScript
スポンサーリンク

getter / setter とは何か(まずイメージから)

getter / setter は、
「プロパティに見える“入り口・出口”の裏に、処理を仕込むための仕組み」 です。

普通のプロパティはこうです。

user.age = 20;
console.log(user.age);
JavaScript

getter / setter を使うと、見た目は同じ書き方をしながら、
実際には「関数」が呼ばれます。

class User {
  #age = 0;

  get age() {            // 読み取り時に呼ばれる
    console.log("getter が呼ばれた");
    return this.#age;
  }

  set age(value) {       // 書き込み時に呼ばれる
    console.log("setter が呼ばれた:", value);
    if (value < 0) {
      throw new Error("年齢は0以上にしてください");
    }
    this.#age = value;
  }
}

const u = new User();
u.age = 20;              // setter が呼ばれる
console.log(u.age);      // getter が呼ばれる → 20
JavaScript

ここが重要です。
u.age と書いていても、実際には
読むときは get age() が、書くときは set age(...) が呼ばれています。
「プロパティのように見える関数」 とイメージすると理解しやすいです。

クラスでの getter / setter の基本構文

getter の書き方(読み取り専用の“プロパティ”)

get プロパティ名() { ... } で定義します。

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {                    // 面積を“プロパティのように”取得できる
    return this.width * this.height;
  }
}

const r = new Rectangle(10, 5);
console.log(r.area);              // 50(r.area() ではないことに注目)
JavaScript

area はメソッドではなく、
r.area と書いた瞬間に get area() が呼ばれ、その戻り値が返ってきます。

ここが重要です。
getter を使うと、「計算が必要な値」を「普通のプロパティのような顔」で提供できます。
読み取り専用にしたいときも便利です(setter を定義しなければ書き込めない)。

setter の書き方(代入時のチェックや変換)

set プロパティ名(value) { ... } で定義します。

class User {
  #name = "";

  get name() {
    return this.#name;
  }

  set name(value) {
    if (value.length === 0) {
      throw new Error("名前は空にできません");
    }
    this.#name = value;
  }
}

const u = new User();
u.name = "Alice";       // setter が呼ばれてチェックされる
console.log(u.name);    // getter 経由で表示 → Alice

// u.name = "";         // エラー:名前は空にできません
JavaScript

代入側は普通に u.name = "Alice" と書くだけなのに、
裏ではバリデーション(チェック)や変換処理を走らせられます。

getter と setter をペアで定義する

同じ名前で getset の両方を定義すると、「読み書き可能なプロパティ」を作れます。

class Counter {
  #value = 0;

  get value() {
    return this.#value;
  }

  set value(v) {
    if (typeof v !== "number") {
      throw new Error("数値を指定してください");
    }
    this.#value = v;
  }
}

const c = new Counter();
c.value = 10;           // set value(10)
console.log(c.value);   // get value() → 10
JavaScript

getter / setter をなぜ使うのか(重要な設計の話)

「直接触らせたくないけど、値は見せたい・変えさせたい」

プライベートフィールド(#)と組み合わせると、
中身は隠したまま、安全な窓口だけを用意できます。

class BankAccount {
  #balance = 0;

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error("金額がおかしい");
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount <= 0) throw new Error("金額がおかしい");
    if (this.#balance < amount) throw new Error("残高不足");
    this.#balance -= amount;
  }
}

const acc = new BankAccount();
acc.deposit(1000);

console.log(acc.balance); // getter 経由で残高を見る
// acc.balance = 9999;    // setter がないので書き換え不可(TypeError になる環境もある)
JavaScript

ここが重要です。
内部の生データ(#balance)を直接いじらせず、
「見せ方」「変更のルール」を getter / メソッドに閉じ込める

これがカプセル化(中身の保護)の具体的な形です。

「内部の表現」と「外から見える形」を分離できる

例えば、温度を内部ではケルビンで持ち、外からは摂氏で扱いたい場合:

class Temperature {
  #kelvin = 0;

  constructor(celsius) {
    this.celsius = celsius; // setter を経由
  }

  get celsius() {
    return this.#kelvin - 273.15;
  }

  set celsius(value) {
    this.#kelvin = value + 273.15;
  }

  get kelvin() {
    return this.#kelvin;
  }
}

const t = new Temperature(25);    // 25℃
// 内部的には #kelvin に変換されて保存される

console.log(t.celsius);           // 25
console.log(t.kelvin);            // 298.15 くらい
JavaScript

使う側は「摂氏で扱いたい」のに、
中では「ケルビン」、と内部表現を自由に変えられます。
後から内部構造を変えても、celsius プロパティのインターフェースは維持できます。

getter / setter と普通のメソッドの違い

見た目が「プロパティアクセス」か「関数呼び出し」か

普通のメソッドならこうです。

user.getAge();
user.setAge(20);
JavaScript

getter / setter だとこう書けます。

user.age;      // getter
user.age = 20; // setter
JavaScript

どちらでも機能的には可能ですが、
「値っぽいもの」はプロパティ(+getter/setter)として扱うほうが自然です。

// よくない例
user.getName();
user.setName("Bob");

// こちらの方が自然
user.name;
user.name = "Bob";
JavaScript

ここが重要です。
「物(状態)」を表現したいなら getter / setter、「動き(動作)」ならメソッド
という切り分けを意識すると、API の読みやすさがかなり変わります。

内部実装を後から変えられる

最初は単純な公開プロパティにしておき、
後から getter / setter に変えても、呼び出し側のコードは変えずに済みます。

// 最初:単純なプロパティ
class User {
  constructor(name) {
    this.name = name;
  }
}

// いずれ:チェックを入れたい → getter / setter に変える
class User {
  #name = "";

  constructor(name) {
    this.name = name; // setter 経由
  }

  get name() {
    return this.#name;
  }

  set name(value) {
    if (!value) throw new Error("名前が空です");
    this.#name = value;
  }
}
JavaScript

使う側はずっと user.name / user.name = ... と書くだけでよく、
裏側の実装を差し替えても影響が少なく済みます。

クラス継承と getter / setter

親クラスの getter / setter をそのまま使う

継承すると、getter / setter も普通のメソッドと同じように引き継がれます。

class Person {
  #name = "";

  get name() {
    return this.#name;
  }

  set name(value) {
    this.#name = value;
  }
}

class Employee extends Person {
  // name の getter / setter をそのまま使える
}

const e = new Employee();
e.name = "Alice";
console.log(e.name); // Alice
JavaScript

オーバーライドして挙動を変える

子クラスで、同じ名前の getter / setter を定義し直すこともできます。

class Person {
  #name = "";

  get name() {
    return this.#name;
  }

  set name(value) {
    this.#name = value;
  }
}

class Employee extends Person {
  get name() {
    return "[社員]" + super.name;
  }

  set name(value) {
    super.name = value.trim(); // 前後の空白を削ってからセット
  }
}

const e = new Employee();
e.name = "  Bob  ";
console.log(e.name); // [社員]Bob
JavaScript

super.name は、親クラスの getter / setter を通じたアクセスになり、
「親のルール+子の追加仕様」をきれいに組み合わせられます。

例題で getter / setter の感覚を固める

例1:フルネームを計算する getter / setter

class User {
  constructor(first, last) {
    this.firstName = first;
    this.lastName = last;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(value) {
    const [first, last] = value.split(" ");
    this.firstName = first;
    this.lastName = last;
  }
}

const u = new User("Alice", "Smith");
console.log(u.fullName); // Alice Smith

u.fullName = "Bob Brown";
console.log(u.firstName); // Bob
console.log(u.lastName);  // Brown
JavaScript

例2:読み取り専用プロパティ

class Order {
  constructor(price, quantity) {
    this.price = price;
    this.quantity = quantity;
  }

  get total() {
    return this.price * this.quantity;
  }
}

const o = new Order(500, 3);
console.log(o.total); // 1500

// o.total = 9999;   // setter がないので書き込めない
JavaScript

例3:内部的な単位と外部インターフェースの分離

class Distance {
  #meters = 0;

  constructor(meters) {
    this.#meters = meters;
  }

  get meters() {
    return this.#meters;
  }

  set meters(value) {
    this.#meters = value;
  }

  get km() {
    return this.#meters / 1000;
  }

  set km(value) {
    this.#meters = value * 1000;
  }
}

const d = new Distance(1500); // 1500m
console.log(d.km);           // 1.5

d.km = 2;                    // 2km に変更
console.log(d.meters);       // 2000
JavaScript

まとめ

getter / setter の核心は、
「プロパティアクセス(obj.prop / obj.prop =)の裏側で、任意の処理を挟めるようにする」 ことです。

押さえておきたいポイントは次の通りです。

  • get name() / set name(value) の形でクラスに定義する
  • 読むときは obj.name、書くときは obj.name = ... と普通のプロパティのように扱える
  • 中ではプライベートフィールドと組み合わせて、チェック・変換・計算・隠蔽ができる
  • 「状態」を表現したい場合は getter / setter、「動作」は普通のメソッドという切り分けが有効
  • 後から内部実装を変えても、プロパティとしてのインターフェースを保ちやすい

まずは、自分のクラスの中で「実は読み取り専用にしたかった値」や
「セット時にチェックしたい値」を1つ選んで、getter / setter に置き換えてみてください。
そこから、「状態の出入り口を設計する」という感覚が掴めてきます。

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