JavaScript入門(ES2021版)
本書をサポートしてください:購入 または 寄付
(広告です。ブロックしないでください)

28章 単一オブジェクト



本書では、JavaScriptのオブジェクト指向プログラミング(OOP)のスタイルを4つのステップで紹介します。本章ではステップ1を、次の章ではステップ2〜4を扱います。(図8参照)

  1. 単一オブジェクト(本章):JavaScriptのOOPの基本的な構成要素であるオブジェクトは、単体でどのように動作するか?
  2. プロトタイプチェーン(次の章):各オブジェクトは、ゼロ個以上のプロトタイプオブジェクトのチェーンを持ちます。プロトタイプはJavaScriptの中核となる継承メカニズムです。
  3. クラス(次の章):JavaScriptのクラスはオブジェクトのファクトリです。クラスとそのインスタンスの関係は、プロトタイプ継承に基づいています。
  4. サブクラス化(次の章):サブクラスとそのスーパークラスの関係も、プロトタイプ継承に基づいています。
Figure 8: This book introduces object-oriented programming in JavaScript in four steps.

28.1章 オブジェクトとは何か?

JavaScriptにおいて

28.1.1節 オブジェクトの役割:レコード vs. ディクショナリ

オブジェクトはJavaScriptにおいて2つの役割を果たします

これらの役割は、本章におけるオブジェクトの説明に影響を与えます

28.2章 レコードとしてのオブジェクト

まず、オブジェクトのレコードとしての役割を探求しましょう。

28.2.1節 オブジェクトリテラル:プロパティ

オブジェクトリテラルは、レコードとしてのオブジェクトを作成する1つの方法です。これはJavaScriptの際立った特徴です。クラスは不要で、直接オブジェクトを作成できます。以下は例です。

const jane = {
  first: 'Jane',
  last: 'Doe', // optional trailing comma
};

この例では、波括弧{}で始まるオブジェクトリテラルを使ってオブジェクトを作成しました。その中には、2つのプロパティ(キーと値のペア)を定義しました。

ES5以降、オブジェクトリテラルでは末尾のカンマが許可されています。

後でプロパティキーを指定する他の方法を見ることになりますが、この方法では、JavaScriptの変数名の規則に従う必要があります。例えば、first_nameはプロパティキーとして使用できますが、first-nameは使用できません。ただし、予約語は使用できます。

const obj = {
  if: true,
  const: true,
};

オブジェクトに対する様々な操作の効果を確認するために、本章のこの部分では時々Object.keys()を使用します。これはプロパティキーを一覧表示します。

> Object.keys({a:1, b:2})
[ 'a', 'b' ]

28.2.2節 オブジェクトリテラル:プロパティ値の省略記法

プロパティの値が変数名で定義されており、その名前がキーと同じである場合は、キーを省略できます。

function createPoint(x, y) {
  return {x, y};
}
assert.deepEqual(
  createPoint(9, 2),
  { x: 9, y: 2 }
);

28.2.3節 プロパティの取得

プロパティを取得(読み込み)する方法は次のとおりです(A行)。

const jane = {
  first: 'Jane',
  last: 'Doe',
};

// Get property .first
assert.equal(jane.first, 'Jane'); // (A)

未知のプロパティを取得するとundefinedが生成されます。

assert.equal(jane.unknownProperty, undefined);

28.2.4節 プロパティの設定

プロパティを設定(書き込み)する方法は次のとおりです。

const obj = {
  prop: 1,
};
assert.equal(obj.prop, 1);
obj.prop = 2; // (A)
assert.equal(obj.prop, 2);

設定によって既存のプロパティを変更しました。未知のプロパティを設定すると、新しいエントリが作成されます。

const obj = {}; // empty object
assert.deepEqual(
  Object.keys(obj), []);

obj.unknownProperty = 'abc';
assert.deepEqual(
  Object.keys(obj), ['unknownProperty']);

28.2.5節 オブジェクトリテラル:メソッド

次のコードは、オブジェクトリテラルを使用して.says()メソッドを作成する方法を示しています。

const jane = {
  first: 'Jane', // data property
  says(text) {   // method
    return `${this.first} says “${text}”`; // (A)
  }, // comma as separator (optional at end)
};
assert.equal(jane.says('hello'), 'Jane says “hello”');

メソッド呼び出しjane.says('hello')の間、janeはメソッド呼び出しのレシーバと呼ばれ、特別な変数thisに代入されます(thisの詳細については§28.4 「メソッドと特別な変数this」を参照)。これにより、メソッド.says()はA行で兄弟プロパティ.firstにアクセスできます。

28.2.6節 オブジェクトリテラル:アクセサ

JavaScriptには2種類のアクセサがあります。

28.2.6.1節 ゲッター

ゲッターは、メソッド定義の前に修飾子getを付けることで作成されます。

const jane = {
  first: 'Jane',
  last: 'Doe',
  get full() {
    return `${this.first} ${this.last}`;
  },
};

assert.equal(jane.full, 'Jane Doe');
jane.first = 'John';
assert.equal(jane.full, 'John Doe');
28.2.6.2節 セッター

セッターは、メソッド定義の前に修飾子setを付けることで作成されます。

const jane = {
  first: 'Jane',
  last: 'Doe',
  set full(fullName) {
    const parts = fullName.split(' ');
    this.first = parts[0];
    this.last = parts[1];
  },
};

jane.full = 'Richard Roe';
assert.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe');

  練習問題:オブジェクトリテラルによるオブジェクトの作成

exercises/single-objects/color_point_object_test.mjs

28.3章 オブジェクトリテラルへのスプレッド構文 (...) [ES2018]

関数呼び出し内では、スプレッド構文 (...) はイテラブルオブジェクトの反復可能な値を、引数に変換します。

オブジェクトリテラル内では、スプレッドプロパティは別のオブジェクトのプロパティを現在のオブジェクトに追加します。

> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}
{ foo: 1, bar: 2, baz: 3 }

プロパティキーが衝突する場合は、最後に記述されたプロパティが「勝ちます」。

> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}
{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}
{ foo: 1, bar: 2, baz: 3 }

undefinednullを含むすべての値はスプレッド可能です。

> {...undefined}
{}
> {...null}
{}
> {...123}
{}
> {...'abc'}
{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}
{ '0': 'a', '1': 'b' }

文字列と配列の.lengthプロパティはこの種の操作からは隠されています(列挙可能ではありません。§28.8.3 「プロパティ属性とプロパティディスクリプタ [ES5]」で詳細を参照してください)。

28.3.1節 スプレッド構文のユースケース:オブジェクトのコピー

スプレッド構文を使用して、オブジェクトoriginalのコピーを作成できます。

const copy = {...original};

注意:コピーは浅いコピーです。copyは、originalのすべてのプロパティ(キーと値のペア)の複製を持つ新しいオブジェクトです。しかし、プロパティ値がオブジェクトの場合は、それ自体がコピーされません。それらはoriginalcopyの間で共有されます。例を見てみましょう。

const original = { a: 1, b: {foo: true} };
const copy = {...original};

copyの最初のレベルは真のコピーです。そのレベルのプロパティを変更しても、オリジナルには影響しません。

copy.a = 2;
assert.deepEqual(
  original, { a: 1, b: {foo: true} }); // no change

しかし、より深いレベルはコピーされません。例えば、.bの値はオリジナルとコピーの間で共有されます。コピー内の.bを変更すると、オリジナルも変更されます。

copy.b.foo = false;
assert.deepEqual(
  original, { a: 1, b: {foo: false} });

  JavaScriptには、組み込みのディープコピーサポートがありません

オブジェクトのディープコピー(すべてのレベルがコピーされる)は、一般的に行うのが非常に困難です。そのため、JavaScriptには(今のところ)組み込みの操作がありません。このような操作が必要な場合は、自分で実装する必要があります。

28.3.2節 スプレッド構文のユースケース:欠損プロパティのデフォルト値

コードの入力がデータを含むオブジェクトである場合、欠損プロパティがある場合に使用されるデフォルト値を指定することで、プロパティをオプションにすることができます。そのための一つのテクニックは、プロパティにデフォルト値を含むオブジェクトを使用することです。次の例では、そのオブジェクトがDEFAULTSです。

const DEFAULTS = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};

const allData = {...DEFAULTS, ...providedData};
assert.deepEqual(allData, {foo: 1, bar: 'b'});

結果のオブジェクトallDataは、DEFAULTSをコピーし、そのプロパティをprovidedDataのプロパティで上書きすることで作成されます。

しかし、デフォルト値を指定するためにオブジェクトは必要ありません。個別にオブジェクトリテラル内で指定することもできます。

const providedData = {foo: 1};

const allData = {foo: 'a', bar: 'b', ...providedData};
assert.deepEqual(allData, {foo: 1, bar: 'b'});

28.3.3節 スプレッド構文のユースケース:非破壊的なプロパティ変更

これまでのところ、オブジェクトのプロパティ.fooを変更する方法は1つしか見ていません。それは設定(A行)によってオブジェクトをミューテートすることです。つまり、このプロパティの変更方法は破壊的です。

const obj = {foo: 'a', bar: 'b'};
obj.foo = 1; // (A)
assert.deepEqual(obj, {foo: 1, bar: 'b'});

スプレッド構文を使用すると、.foo非破壊的に変更できます。.fooが異なる値を持つobjのコピーを作成します。

const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
assert.deepEqual(updatedObj, {foo: 1, bar: 'b'});

  練習問題:スプレッド構文による非破壊的なプロパティ更新(固定キー)

exercises/single-objects/update_name_test.mjs

28.4章 メソッドと特別な変数this

28.4.1節 メソッドは値が関数のプロパティである

メソッドを紹介するために使用された例を再訪しましょう。

const jane = {
  first: 'Jane',
  says(text) {
    return `${this.first} says “${text}”`;
  },
};

やや驚くべきことに、メソッドは関数です。

assert.equal(typeof jane.says, 'function');

なぜそうなるのでしょうか?呼び出し可能値に関する章で学習したように、通常の関数はいくつかの役割を果たします。メソッドはその役割の1つです。したがって、内部的には、janeはおおよそ以下のようになります。

const jane = {
  first: 'Jane',
  says: function (text) {
    return `${this.first} says “${text}”`;
  },
};

28.4.2 特別な変数this

次のコードを考えてみましょう

const obj = {
  someMethod(x, y) {
    assert.equal(this, obj); // (A)
    assert.equal(x, 'a');
    assert.equal(y, 'b');
  }
};
obj.someMethod('a', 'b'); // (B)

B行では、objはメソッド呼び出しのレシーバです。これは、obj.someMethodに格納されている関数に、thisという名前の暗黙的(非表示)なパラメータを介して渡されます(A行)。

これは重要な点です。thisを理解する最善の方法は、通常の関数(したがってメソッドも)の暗黙のパラメータとして理解することです。

28.4.3 メソッドと.call()

メソッドは関数であり、§25.7「関数のメソッド:.call().apply().bind()で見たように、関数自体にもメソッドがあります。そのメソッドの1つは.call()です。このメソッドの動作を理解するために、例を見てみましょう。

前のセクションでは、次のメソッド呼び出しがありました。

obj.someMethod('a', 'b')

この呼び出しは、次の呼び出しと同等です。

obj.someMethod.call(obj, 'a', 'b');

これは次の呼び出しと同等でもあります。

const func = obj.someMethod;
func.call(obj, 'a', 'b');

.call()は、通常は暗黙的なパラメータであるthisを明示的にします。.call()を介して関数を呼び出す場合、最初の引数はthisであり、その後に通常の(明示的な)関数パラメータが続きます。

余談ですが、これは実際には2つの異なるドット演算子があることを意味します。

  1. プロパティへのアクセス:obj.prop
  2. メソッドの呼び出し:obj.prop()

それらは、(2)が(1)に続く関数呼び出し演算子()だけではないという点で異なります。(2)はさらにthisの値を提供します。

28.4.4 メソッドと.bind()

.bind()は、関数オブジェクトのもう1つのメソッドです。次のコードでは、.bind()を使用して、メソッド.says()をスタンドアロン関数func()に変換します。

const jane = {
  first: 'Jane',
  says(text) {
    return `${this.first} says “${text}”`; // (A)
  },
};

const func = jane.says.bind(jane, 'hello');
assert.equal(func(), 'Jane says “hello”');

.bind()を介してthisjaneに設定することは、ここで非常に重要です。そうでなければ、A行でthisが使用されているため、func()は正しく動作しません。次のセクションでは、その理由を説明します。

28.4.5 thisの落とし穴:メソッドの抽出

これで、関数とメソッドについてかなり理解できたので、メソッドとthisに関する最大の落とし穴を見てみましょう。オブジェクトから抽出したメソッドを関数呼び出しすると、注意しないと失敗する可能性があります。

次の例では、メソッドjane.says()を抽出し、変数funcに格納し、func()を関数呼び出しすると失敗します。

const jane = {
  first: 'Jane',
  says(text) {
    return `${this.first} says “${text}”`;
  },
};
const func = jane.says; // extract the method
assert.throws(
  () => func('hello'), // (A)
  {
    name: 'TypeError',
    message: "Cannot read property 'first' of undefined",
  });

A行では、通常の関数呼び出しを行っています。通常の関数呼び出しでは、thisundefinedです(厳格モードが有効な場合、ほとんど常に有効です)。したがって、A行は次の呼び出しと同等です。

assert.throws(
  () => jane.says.call(undefined, 'hello'), // `this` is undefined!
  {
    name: 'TypeError',
    message: "Cannot read property 'first' of undefined",
  });

これを修正するにはどうすればよいでしょうか?メソッド.says()を抽出するには.bind()を使用する必要があります。

const func2 = jane.says.bind(jane);
assert.equal(func2('hello'), 'Jane says “hello”');

.bind()は、func()を呼び出すときにthisが常にjaneになるようにします。

アロー関数を使用してメソッドを抽出することもできます。

const func3 = text => jane.says(text);
assert.equal(func3('hello'), 'Jane says “hello”');
28.4.5.1 例:メソッドの抽出

以下は、実際のWeb開発で見られるコードの簡略版です。

class ClickHandler {
  constructor(id, elem) {
    this.id = id;
    elem.addEventListener('click', this.handleClick); // (A)
  }
  handleClick(event) {
    alert('Clicked ' + this.id);
  }
}

A行では、メソッド.handleClick()を正しく抽出していません。代わりに、次のようにする必要があります。

elem.addEventListener('click', this.handleClick.bind(this));
28.4.5.2 メソッドの抽出の落とし穴を回避する方法

残念ながら、メソッドの抽出の落とし穴を回避する簡単な方法はありません。メソッドを抽出するたびに、注意深く適切に行う必要があります。たとえば、thisをバインドするか、アロー関数を使用することです。

  練習問題:メソッドの抽出

exercises/single-objects/method_extraction_exrc.mjs

28.4.6 thisの落とし穴:thisの偶発的なシャドウイング

  thisの偶発的なシャドウイングは、通常の関数でのみ問題になります

アロー関数はthisをシャドウイングしません。

次の問題を考えてみましょう。通常の関数内では、通常の関数自身が独自のthisを持つため、周囲のスコープのthisにアクセスできません。言い換えれば、内部スコープの変数は外部スコープの変数を隠します。これはシャドウイングと呼ばれます。次のコードはその例です。

const prefixer = {
  prefix: '==> ',
  prefixStringArray(stringArray) {
    return stringArray.map(
      function (x) {
        return this.prefix + x; // (A)
      });
  },
};
assert.throws(
  () => prefixer.prefixStringArray(['a', 'b']),
  /^TypeError: Cannot read property 'prefix' of undefined$/);

A行では、.prefixStringArray()thisにアクセスしたいと考えています。しかし、周囲の通常の関数には独自のthisがあり、メソッドのthisシャドウイング(アクセスをブロック)しているため、アクセスできません。前者のthisの値は、コールバックが関数呼び出しされているためundefinedです。これがエラーメッセージの説明です。

この問題を解決する最も簡単な方法は、アロー関数を使用することです。アロー関数には独自のthisがないため、何もシャドウイングしません。

const prefixer = {
  prefix: '==> ',
  prefixStringArray(stringArray) {
    return stringArray.map(
      (x) => {
        return this.prefix + x;
      });
  },
};
assert.deepEqual(
  prefixer.prefixStringArray(['a', 'b']),
  ['==> a', '==> b']);

thisを別の変数(A行)に格納して、シャドウイングされないようにすることもできます。

prefixStringArray(stringArray) {
  const that = this; // (A)
  return stringArray.map(
    function (x) {
      return that.prefix + x;
    });
},

別の方法として、.bind()(A行)を使用して、コールバックのthisを固定することもできます。

prefixStringArray(stringArray) {
  return stringArray.map(
    function (x) {
      return this.prefix + x;
    }.bind(this)); // (A)
},

最後に、.map()を使用すると、コールバックの呼び出し時に使用するthis(A行)を指定できます。

prefixStringArray(stringArray) {
  return stringArray.map(
    function (x) {
      return this.prefix + x;
    },
    this); // (A)
},
28.4.6.1 thisの偶発的なシャドウイングの落とし穴を回避する

§25.3.4「推奨事項:通常の関数よりも特殊化された関数を優先する」のアドバイスに従うと、thisの偶発的なシャドウイングの落とし穴を回避できます。これが要約です。

28.4.7 さまざまなコンテキストにおけるthisの値(上級)

さまざまなコンテキストにおけるthisの値は何でしょうか?

呼び出し可能なエンティティ内では、thisの値は、呼び出し可能なエンティティがどのように呼び出され、どのような種類の呼び出し可能なエンティティであるかによって異なります。

一般的なトップレベルスコープでもthisにアクセスできます。

  ヒント:トップレベルスコープではthisが存在しないと仮定しましょう

トップレベルのthisは混乱を招きやすく、ほとんど役に立たないため、私はそうしています。

28.5 プロパティアクセスとメソッド呼び出しに対するオプションチェーン[ES2020](上級)

次の種類のオプションチェーン操作が存在します。

obj?.prop     // optional static property access
obj?.[«expr»] // optional dynamic property access
func?.(«arg0», «arg1») // optional function or method call

大まかな考え方は次のとおりです。

28.5.1 例:オプションの静的プロパティアクセス

次のデータを考えてみましょう。

const persons = [
  {
    surname: 'Zoe',
    address: {
      street: {
        name: 'Sesame Street',
        number: '123',
      },
    },
  },
  {
    surname: 'Mariner',
  },
  {
    surname: 'Carmen',
    address: {
    },
  },
];

オプションチェーンを使用して、安全に通りの名前を抽出できます。

const streetNames = persons.map(
  p => p.address?.street?.name);
assert.deepEqual(
  streetNames, ['Sesame Street', undefined, undefined]
);
28.5.1.1 ナル結合演算子を使用したデフォルト値の処理

ナル結合演算子を使用すると、undefinedの代わりにデフォルト値'(no street)'を使用できます。

const streetNames = persons.map(
  p => p.address?.street?.name ?? '(no name)');
assert.deepEqual(
  streetNames, ['Sesame Street', '(no name)', '(no name)']
);

28.5.2 詳細な演算子(上級)

28.5.2.1 オプションの静的プロパティアクセス

次の2つの式は同等です。

o?.prop
(o !== undefined && o !== null) ? o.prop : undefined

assert.equal(undefined?.prop, undefined);
assert.equal(null?.prop,      undefined);
assert.equal({prop:1}?.prop,  1);
28.5.2.2 オプションの動的プロパティアクセス

次の2つの式は同等です。

o?.[«expr»]
(o !== undefined && o !== null) ? o[«expr»] : undefined

const key = 'prop';
assert.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1);
28.5.2.3 オプションの関数またはメソッド呼び出し

次の2つの式は同等です。

f?.(arg0, arg1)
(f !== undefined && f !== null) ? f(arg0, arg1) : undefined

assert.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123');

この演算子の左側が呼び出し可能でない場合、エラーが発生することに注意してください。

assert.throws(
  () => true?.(123),
  TypeError);

なぜでしょうか?この考え方は、演算子が意図的な省略を許容することです。undefinednull以外の呼び出し不可能な値はおそらくエラーであり、回避策を講じるのではなく、報告する必要があります。

28.5.3 短絡評価(上級)

プロパティアクセスと関数/メソッド呼び出しのチェーンでは、最初のオプション演算子が左側にundefinedまたはnullを検出すると、評価が停止します。

function isInvoked(obj) {
  let invoked = false;
  obj?.a.b.m(invoked = true);
  return invoked;
}

assert.equal(
  isInvoked({a: {b: {m() {}}}}), true);
  
// The left-hand side of ?. is undefined
// and the assignment is not executed
assert.equal(
  isInvoked(undefined), false);

この動作は、JavaScriptが演算子/関数を評価する前に常にすべてのオペランド/引数を評価する通常の演算子/関数とは異なります。これは短絡評価と呼ばれます。その他の短絡評価演算子:

28.5.4 よくある質問

28.5.4.1 o?.[x]f?.()にドットがあるのはなぜですか?

次の2つのオプション演算子の構文は理想的ではありません。

obj?.[«expr»]          // better: obj?[«expr»]
func?.(«arg0», «arg1») // better: func?(«arg0», «arg1»)

残念ながら、よりエレガントではない構文は、理想的な構文(最初の式)と条件演算子(2番目の式)を区別することが複雑すぎるため、必要です。

obj?['a', 'b', 'c'].map(x => x+x)
obj ? ['a', 'b', 'c'].map(x => x+x) : []
28.5.4.2 null?.propundefinedではなくnullを評価するのはなぜですか?

演算子?.は主にその右側に関するものです。プロパティ.propは存在しますか?存在しない場合、早期に停止します。したがって、その左側の情報を保持することはめったに役に立ちません。ただし、単一の「早期終了」値だけにすることで、事態が簡素化されます。

28.6 オブジェクトを辞書として(上級)

オブジェクトはレコードとして最適に機能します。しかし、ES6以前は、JavaScriptには辞書用のデータ構造がありませんでした(ES6でMapが導入されました)。そのため、オブジェクトは辞書として使用しなければならず、これは重要な制約を課しました。キーは文字列でなければなりませんでした(シンボルもES6で導入されました)。

最初に、辞書に関連するオブジェクトの機能、またレコードとしてのオブジェクトにも役立つ機能を見ていきます。このセクションは、オブジェクトを辞書として実際に使用するためのヒントで締めくくります(ネタバレ:可能であればMapを使用してください)。

28.6.1 任意の固定文字列をプロパティキーとして

これまでのところ、常にオブジェクトをレコードとして使用してきました。プロパティキーは、有効な識別子でなければならない固定トークンであり、内部的には文字列になりました。

const obj = {
  mustBeAnIdentifier: 123,
};

// Get property
assert.equal(obj.mustBeAnIdentifier, 123);

// Set property
obj.mustBeAnIdentifier = 'abc';
assert.equal(obj.mustBeAnIdentifier, 'abc');

次のステップとして、プロパティキーのこの制限を超えます。このセクションでは、任意の固定文字列をキーとして使用します。次のサブセクションでは、キーを動的に計算します。

任意の文字列をプロパティキーとして使用するには、2つの技法があります。

まず、オブジェクトリテラルを介してプロパティキーを作成する場合、プロパティキーを引用符で囲むことができます(一重引用符または二重引用符)。

const obj = {
  'Can be any string!': 123,
};

次に、プロパティを取得または設定する場合、内部に文字列を含む角括弧を使用できます。

// Get property
assert.equal(obj['Can be any string!'], 123);

// Set property
obj['Can be any string!'] = 'abc';
assert.equal(obj['Can be any string!'], 'abc');

これらの技法をメソッドにも使用できます。

const obj = {
  'A nice method'() {
    return 'Yes!';
  },
};

assert.equal(obj['A nice method'](), 'Yes!');

28.6.2 計算されたプロパティキー

これまでのところ、プロパティキーはオブジェクトリテラル内の常に固定文字列でした。このセクションでは、プロパティキーを動的に計算する方法を学びます。これにより、任意の文字列またはシンボルを使用できます。

オブジェクトリテラルにおける動的に計算されたプロパティキーの構文は、プロパティへの動的なアクセスに触発されています。つまり、角括弧を使用して式を囲むことができます。

const obj = {
  ['Hello world!']: true,
  ['f'+'o'+'o']: 123,
  [Symbol.toStringTag]: 'Goodbye', // (A)
};

assert.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye');

計算されたキーの主なユースケースは、プロパティキーとしてシンボルを使用することです(A行)。

プロパティを取得および設定するための角括弧演算子は、任意の式で動作することに注意してください。

assert.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 123);

メソッドにも計算されたプロパティキーを持つことができます。

const methodKey = Symbol();
const obj = {
  [methodKey]() {
    return 'Yes!';
  },
};

assert.equal(obj[methodKey](), 'Yes!');

この章の残りの部分では、主に固定プロパティキーを再び使用します(構文的により便利であるため)。しかし、すべての機能は任意の文字列とシンボルにも利用できます。

  練習問題:スプレッド(計算されたキー)を使用したプロパティの非破壊的な更新

exercises/single-objects/update_property_test.mjs

28.6.3 in演算子:指定されたキーを持つプロパティはありますか?

in演算子は、オブジェクトに指定されたキーを持つプロパティがあるかどうかを確認します。

const obj = {
  foo: 'abc',
  bar: false,
};

assert.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false);
28.6.3.1 真理値を使用してプロパティの存在を確認する

真理値チェックを使用して、プロパティが存在するかどうかを確認することもできます。

assert.equal(
  obj.foo ? 'exists' : 'does not exist',
  'exists');
assert.equal(
  obj.unknownKey ? 'exists' : 'does not exist',
  'does not exist');

以前のチェックは、obj.fooが真理値であり、欠損プロパティの読み取りがundefined(偽値)を返すため機能します。

ただし、重要な注意点が1つあります。プロパティが存在するが、偽の値(`undefined`、`null`、`false`、`0`、`""`など)を持つ場合、真偽値チェックは失敗します。

assert.equal(
  obj.bar ? 'exists' : 'does not exist',
  'does not exist'); // should be: 'exists'

28.6.4 プロパティの削除

`delete`演算子を使用してプロパティを削除できます。

const obj = {
  foo: 123,
};
assert.deepEqual(Object.keys(obj), ['foo']);

delete obj.foo;

assert.deepEqual(Object.keys(obj), []);

28.6.5 プロパティキーの列挙

表19:独自の(継承されていない)プロパティキーを列挙するための標準ライブラリメソッド。これらはすべて、文字列と/またはシンボルを含む配列を返します。
列挙可能 非列挙 文字列 シンボル
Object.keys()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Reflect.ownKeys()

19の各メソッドは、パラメーターの独自のプロパティキーを含む配列を返します。メソッド名から、以下の区別が行われていることがわかります。

次のセクションでは、列挙可能という用語について説明し、各メソッドを示します。

28.6.5.1 列挙可能性

列挙可能性は、プロパティの属性です。非列挙可能なプロパティは、一部の操作(たとえば、`Object.keys()`(表19参照)やスプレッドプロパティ)では無視されます。デフォルトでは、ほとんどのプロパティは列挙可能です。次の例は、それを変更する方法を示しています。また、プロパティキーを列挙するさまざまな方法も示しています。

const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');

// We create enumerable properties via an object literal
const obj = {
  enumerableStringKey: 1,
  [enumerableSymbolKey]: 2,
}

// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
  nonEnumStringKey: {
    value: 3,
    enumerable: false,
  },
  [nonEnumSymbolKey]: {
    value: 4,
    enumerable: false,
  },
});

assert.deepEqual(
  Object.keys(obj),
  [ 'enumerableStringKey' ]);
assert.deepEqual(
  Object.getOwnPropertyNames(obj),
  [ 'enumerableStringKey', 'nonEnumStringKey' ]);
assert.deepEqual(
  Object.getOwnPropertySymbols(obj),
  [ enumerableSymbolKey, nonEnumSymbolKey ]);
assert.deepEqual(
  Reflect.ownKeys(obj),
  [
    'enumerableStringKey', 'nonEnumStringKey',
    enumerableSymbolKey, nonEnumSymbolKey,
  ]);

`Object.defineProperties()`については、この章の後半で説明します。

28.6.6 `Object.values()`によるプロパティ値の列挙

`Object.values()`は、オブジェクトのすべての列挙可能なプロパティの値を列挙します。

const obj = {foo: 1, bar: 2};
assert.deepEqual(
  Object.values(obj),
  [1, 2]);

28.6.7 `Object.entries()`によるプロパティエントリの列挙 [ES2017]

`Object.entries()`は、列挙可能なプロパティのキーと値のペアを列挙します。各ペアは、2要素の配列としてエンコードされます。

const obj = {foo: 1, bar: 2};
assert.deepEqual(
  Object.entries(obj),
  [
    ['foo', 1],
    ['bar', 2],
  ]);

  練習問題:`Object.entries()`

exercises/single-objects/find_key_test.mjs

28.6.8 プロパティは決定論的に列挙される

オブジェクトの独自の(継承されていない)プロパティは、常に次の順序で列挙されます。

  1. 整数インデックスを含む文字列キーを持つプロパティ(配列インデックスを含む)
    昇順の数値順
  2. 残りの文字列キーを持つプロパティ
    追加された順序
  3. シンボルキーを持つプロパティ
    追加された順序

次の例は、プロパティキーがこれらのルールに従ってどのようにソートされるかを示しています。

> Object.keys({b:0,a:0, 10:0,2:0})
[ '2', '10', 'b', 'a' ]

  プロパティの順序

ECMAScript仕様では、プロパティの順序付けについてより詳細に説明されています。

28.6.9 `Object.fromEntries()`によるオブジェクトの組み立て [ES2019]

[キー、値]ペアのイテラブルが与えられると、`Object.fromEntries()`はオブジェクトを作成します。

assert.deepEqual(
  Object.fromEntries([['foo',1], ['bar',2]]),
  {
    foo: 1,
    bar: 2,
  }
);

`Object.fromEntries()`は、`Object.entries()`の逆を行います。

両方を示すために、Underscoreライブラリの2つのツール関数を次の小節で実装します。

28.6.9.1 例:`pick(object, ...keys)`

`pick`は、引数として指定されたキーを持つプロパティのみを持つ、`object`のコピーを返します。

const address = {
  street: 'Evergreen Terrace',
  number: '742',
  city: 'Springfield',
  state: 'NT',
  zip: '49007',
};
assert.deepEqual(
  pick(address, 'street', 'number'),
  {
    street: 'Evergreen Terrace',
    number: '742',
  }
);

`pick()`は次のように実装できます。

function pick(object, ...keys) {
  const filteredEntries = Object.entries(object)
    .filter(([key, _value]) => keys.includes(key));
  return Object.fromEntries(filteredEntries);
}
28.6.9.2 例:`invert(object)`

`invert`は、すべてのプロパティのキーと値が入れ替えられた、`object`のコピーを返します。

assert.deepEqual(
  invert({a: 1, b: 2, c: 3}),
  {1: 'a', 2: 'b', 3: 'c'}
);

`invert()`は次のように実装できます。

function invert(object) {
  const reversedEntries = Object.entries(object)
    .map(([key, value]) => [value, key]);
  return Object.fromEntries(reversedEntries);
}
28.6.9.3 `Object.fromEntries()`の簡単な実装

次の関数は、`Object.fromEntries()`の簡略版です。

function fromEntries(iterable) {
  const result = {};
  for (const [key, value] of iterable) {
    let coercedKey;
    if (typeof key === 'string' || typeof key === 'symbol') {
      coercedKey = key;
    } else {
      coercedKey = String(key);
    }
    result[coercedKey] = value;
  }
  return result;
}

  練習問題:`Object.entries()`と`Object.fromEntries()`

exercises/single-objects/omit_properties_test.mjs

28.6.10 オブジェクトを辞書として使用することの落とし穴

プレーンオブジェクト(オブジェクトリテラルを使用して作成されたもの)を辞書として使用する場合、2つの落とし穴に注意する必要があります。

最初の落とし穴は、`in`演算子が継承されたプロパティも検出することです。

const dict = {};
assert.equal('toString' in dict, true);

`dict`は空として扱われるべきですが、`in`演算子は、そのプロトタイプである`Object.prototype`から継承したプロパティを検出します。

2番目の落とし穴は、特別な権限を持つ(オブジェクトのプロトタイプを設定する)ため、プロパティキー`__proto__`を使用できないことです。

const dict = {};

dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);
28.6.10.1 オブジェクトを辞書として安全に使用する方法

では、これらの2つの落とし穴をどのように回避すればよいでしょうか?

次のコードは、プロトタイプを持たないオブジェクトを辞書として使用する方法を示しています。

const dict = Object.create(null); // no prototype

assert.equal('toString' in dict, false); // (A)

dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);

両方の落とし穴を回避しました。

  練習問題:オブジェクトを辞書として使用

exercises/single-objects/simple_dict_test.mjs

28.7 標準メソッド(高度なトピック)

`Object.prototype`は、オブジェクトが言語によってどのように扱われるかを構成するためにオーバーライドできるいくつかの標準メソッドを定義しています。重要なものの2つは次のとおりです。

28.7.1 `.toString()`

`.toString()`は、オブジェクトが文字列に変換される方法を決定します。

> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'

28.7.2 `.valueOf()`

`.valueOf()`は、オブジェクトが数値に変換される方法を決定します。

> Number({valueOf() { return 123 }})
123
> Number({})
NaN

28.8 高度なトピック

以下の小節では、いくつかの高度なトピックの概要を簡単に説明します。

28.8.1 `Object.assign()` [ES6]

`Object.assign()`はツールメソッドです。

Object.assign(target, source_1, source_2, ···)

この式は、`source_1`のすべてのプロパティを`target`に、次に`source_2`のすべてのプロパティなどを割り当てます。最後に、`target`を返します。例:

const target = { foo: 1 };

const result = Object.assign(
  target,
  {bar: 2},
  {baz: 3, bar: 4});

assert.deepEqual(
  result, { foo: 1, bar: 4, baz: 3 });
// target was modified and returned:
assert.equal(result, target);

`Object.assign()`のユースケースは、スプレッドプロパティの場合と似ています。ある意味、破壊的にスプレッドします。

28.8.2 オブジェクトの凍結 [ES5]

`Object.freeze(obj)`は、`obj`を完全に不変にします。プロパティの変更、プロパティの追加、プロトタイプの変更はできません。例:

const frozen = Object.freeze({ x: 2, y: 5 });
assert.throws(
  () => { frozen.x = 7 },
  {
    name: 'TypeError',
    message: /^Cannot assign to read only property 'x'/,
  });

注意点が1つあります。`Object.freeze(obj)`は浅い凍結です。つまり、`obj`のプロパティのみが凍結され、プロパティに格納されているオブジェクトは凍結されません。

  詳細情報

オブジェクトの凍結やその他のロックダウン方法の詳細については、Deep JavaScriptを参照してください。

28.8.3 プロパティ属性とプロパティ記述子 [ES5]

オブジェクトがプロパティで構成されているのと同様に、プロパティは属性で構成されています。プロパティの値は、いくつかの属性の1つに過ぎません。その他には次のものがあります。

プロパティ属性を処理するための操作の1つを使用する場合、属性はプロパティ記述子を介して指定されます。各プロパティが1つの属性を表すオブジェクトです。たとえば、プロパティ`obj.foo`の属性は次のように読み取ります。

const obj = { foo: 123 };
assert.deepEqual(
  Object.getOwnPropertyDescriptor(obj, 'foo'),
  {
    value: 123,
    writable: true,
    enumerable: true,
    configurable: true,
  });

そして、プロパティ`obj.bar`の属性は次のように設定します。

const obj = {
  foo: 1,
  bar: 2,
};

assert.deepEqual(Object.keys(obj), ['foo', 'bar']);

// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
  enumerable: false,
});

assert.deepEqual(Object.keys(obj), ['foo']);

さらに読む

  クイズ

クイズアプリを参照してください。