2022年4月13日

プロパティゲッターとセッター

オブジェクトプロパティには2種類あります。

1つ目はデータプロパティです。 これらの扱い方はすでに知っています。 これまで使用してきたすべてのプロパティはデータプロパティでした。

2つ目のタイプのプロパティは新しいものです。 それはアクセサプロパティです。 これらは本質的には値の取得と設定時に実行される関数ですが、外部コードからは通常のプロパティのように見えます。

ゲッターとセッター

アクセサプロパティは、「ゲッター」と「セッター」メソッドで表されます。 オブジェクトリテラルでは、getsetで表されます。

let obj = {
  get propName() {
    // getter, the code executed on getting obj.propName
  },

  set propName(value) {
    // setter, the code executed on setting obj.propName = value
  }
};

ゲッターはobj.propNameが読み取られるときに機能し、セッターはそれが代入されるときに機能します。

たとえば、namesurnameを持つuserオブジェクトがあるとします。

let user = {
  name: "John",
  surname: "Smith"
};

ここで、"John Smith"であるべきfullNameプロパティを追加したいとします。 もちろん、既存の情報をコピーペーストしたくないので、アクセサとして実装できます。

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

alert(user.fullName); // John Smith

外部からは、アクセサプロパティは通常のプロパティのように見えます。 これがアクセサプロパティの考え方です。 user.fullNameを関数として呼び出すのではなく、普通に読み取るだけです。ゲッターは裏で実行されます。

今のところ、fullNameにはゲッターしかありません。 user.fullName=を代入しようとすると、エラーが発生します。

let user = {
  get fullName() {
    return `...`;
  }
};

user.fullName = "Test"; // Error (property has only a getter)

user.fullNameにセッターを追加することで修正しましょう。

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// set fullName is executed with the given value.
user.fullName = "Alice Cooper";

alert(user.name); // Alice
alert(user.surname); // Cooper

結果として、「仮想」プロパティfullNameができました。 これは読み書き可能です。

アクセサ記述子

アクセサプロパティの記述子は、データプロパティの記述子とは異なります。

アクセサプロパティの場合、valueまたはwritableはありませんが、代わりにgetおよびset関数があります。

つまり、アクセサ記述子には以下が含まれる場合があります。

  • get – 引数のない関数で、プロパティが読み取られるときに機能します。
  • `set` – 1つの引数を持つ関数で、プロパティが設定されるときに呼び出されます。
  • enumerable – データプロパティと同じです。
  • configurable – データプロパティと同じです。

たとえば、definePropertyを使用してアクセサfullNameを作成するには、getsetを持つ記述子を渡すことができます。

let user = {
  name: "John",
  surname: "Smith"
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

alert(user.fullName); // John Smith

for(let key in user) alert(key); // name, surname

プロパティは、アクセサ(get/setメソッドを持つ)またはデータプロパティ(valueを持つ)のいずれかであり、両方を持つことはできないことに注意してください。

同じ記述子にgetvalueの両方を指定しようとすると、エラーが発生します。

// Error: Invalid property descriptor.
Object.defineProperty({}, 'prop', {
  get() {
    return 1
  },

  value: 2
});

よりスマートなゲッター/セッター

ゲッター/セッターは、「実際の」プロパティ値のラッパーとして使用して、それらに対する操作をより細かく制御できます。

たとえば、userの名前が短すぎるのを禁止したい場合は、セッターnameを用意し、値を別のプロパティ_nameに保持できます。

let user = {
  get name() {
    return this._name;
  },

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

user.name = "Pete";
alert(user.name); // Pete

user.name = ""; // Name is too short...

そのため、名前は_nameプロパティに格納され、アクセスはゲッターとセッターを介して行われます。

技術的には、外部コードはuser._nameを使用して名前に直接アクセスできます。 しかし、アンダースコア"_"で始まるプロパティは内部的なものであり、オブジェクトの外部から触れてはならないという広く知られた慣習があります。

互換性のための使用

アクセサの優れた用途の1つは、「通常の」データプロパティをいつでもゲッターとセッターに置き換えてその動作を微調整することで、制御できることです。

データプロパティnameageを使用してユーザーオブジェクトの実装を開始したとします。

function User(name, age) {
  this.name = name;
  this.age = age;
}

let john = new User("John", 25);

alert( john.age ); // 25

…しかし、遅かれ早かれ、状況は変わるかもしれません。 ageの代わりに、より正確で便利なので、birthdayを保存することに決めるかもしれません。

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

let john = new User("John", new Date(1992, 6, 1));

では、まだageプロパティを使用している古いコードはどうすればよいでしょうか?

そのような場所をすべて見つけて修正しようとすることができますが、時間がかかり、そのコードが他の多くの人々によって使用されている場合は困難になる可能性があります。 それに、ageuserにあると便利ですよね?

それを維持しましょう。

ageにゲッターを追加すると、問題が解決します。

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // age is calculated from the current date and birthday
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert( john.birthday ); // birthday is available
alert( john.age );      // ...as well as the age

これで古いコードも機能し、便利な追加プロパティができました。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHubのissueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。