JavaScript不慣れなプログラマーのための本(ES2022版)
本書をサポートしてください:購入 または 寄付
(広告、ブロックしないでください。)

28章 オブジェクト



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

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

28.1章 チートシート:オブジェクト

28.1.1項 単一オブジェクト

オブジェクトリテラルによるオブジェクトの作成(波括弧で始まり、波括弧で終わる)

const myObject = { // object literal
  myProperty: 1,
  myMethod() {
    return 2;
  }, // comma!
  get myAccessor() {
    return this.myProperty;
  }, // comma!
  set myAccessor(value) {
    this.myProperty = value;
  }, // last comma is optional
};

assert.equal(
  myObject.myProperty, 1
);
assert.equal(
  myObject.myMethod(), 2
);
assert.equal(
  myObject.myAccessor, 1
);
myObject.myAccessor = 3;
assert.equal(
  myObject.myProperty, 3
);

クラスなしで直接オブジェクトを作成できることは、JavaScriptのハイライトの1つです。

オブジェクトへのスプレッド構文

const original = {
  a: 1,
  b: {
    c: 3,
  },
};

// Spreading (...) copies one object “into” another one:
const modifiedCopy = {
  ...original, // spreading
  d: 4,
};

assert.deepEqual(
  modifiedCopy,
  {
    a: 1,
    b: {
      c: 3,
    },
    d: 4,
  }
);

// Caveat: spreading copies shallowly (property values are shared)
modifiedCopy.a = 5; // does not affect `original`
modifiedCopy.b.c = 6; // affects `original`
assert.deepEqual(
  original,
  {
    a: 1, // unchanged
    b: {
      c: 6, // changed
    },
  },
);

スプレッド構文を使用して、オブジェクトの変更されていない(シャロー)コピーを作成することもできます。

const exactCopy = {...obj};

28.1.2項 プロトタイプチェーン

プロトタイプはJavaScriptの基本的な継承メカニズムです。クラスもこれに基づいています。各オブジェクトは、nullまたはオブジェクトをプロトタイプとして持ちます。後者のオブジェクトもプロトタイプを持つことができます。一般的に、プロトタイプのチェーンが得られます。

プロトタイプは次のように管理されます。

// `obj1` has no prototype (its prototype is `null`)
const obj1 = Object.create(null); // (A)
assert.equal(
  Object.getPrototypeOf(obj1), null // (B)
);

// `obj2` has the prototype `proto`
const proto = {
  protoProp: 'protoProp',
};
const obj2 = {
  __proto__: proto, // (C)
  objProp: 'objProp',
}
assert.equal(
  Object.getPrototypeOf(obj2), proto
);

注記

各オブジェクトは、そのプロトタイプのすべてのプロパティを継承します。

// `obj2` inherits .protoProp from `proto`
assert.equal(
  obj2.protoProp, 'protoProp'
);
assert.deepEqual(
  Reflect.ownKeys(obj2),
  ['objProp'] // own properties of `obj2`
);

オブジェクトの非継承プロパティは、固有のプロパティと呼ばれます。

プロトタイプのもっとも重要なユースケースは、複数のオブジェクトが共通のプロトタイプから継承することでメソッドを共有できることです。

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

JavaScriptにおけるオブジェクト

28.2.1項 オブジェクトの2つの使用方法

JavaScriptでは、オブジェクトを2つの方法で使用できます。

2つの方法は混合することもできます。一部のオブジェクトは、固定レイアウトオブジェクトとディクショナリオブジェクトの両方です。

オブジェクトの使用方法はこの章での説明方法に影響します。

28.3章 固定レイアウトオブジェクト

まず、固定レイアウトオブジェクトを調べましょう。

28.3.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.3.2項 オブジェクトリテラル:プロパティ値の省略記法

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

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

28.3.3項 プロパティの取得

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

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

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

不明なプロパティを取得するとundefinedになります。

assert.equal(jane.unknownProperty, undefined);

28.3.4項 プロパティの設定

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

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.3.5項 オブジェクトリテラル:メソッド

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

const jane = {
  first: 'Jane', // value 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.5 「メソッドと特別な変数thisを参照)。これにより、メソッド.says()はA行で兄弟プロパティ.firstにアクセスできます。

28.3.6項 オブジェクトリテラル:アクセサー

アクセサーは、メソッドのように見えるオブジェクトリテラル内の構文によって定義されます。ゲッターと/またはセッターです。(つまり、各アクセサーには、これらの一方または両方が含まれます)。

アクセサーの呼び出しは、値プロパティへのアクセスのように見えます。

28.3.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.3.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/objects/color_point_object_test.mjs

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

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

> const obj = {one: 1, two: 2};
> {...obj, three: 3}
{ one: 1, two: 2, three: 3 }
const obj1 = {one: 1, two: 2};
const obj2 = {three: 3};
assert.deepEqual(
  {...obj1, ...obj2, four: 4},
  {one: 1, two: 2, three: 3,  four: 4}
);

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

> const obj = {one: 1, two: 2, three: 3};
> {...obj, one: true}
{ one: true, two: 2, three: 3 }
> {one: true, ...obj}
{ one: 1, two: 2, three: 3 }

undefinednullも含め、すべての値はスプレッド可能です。

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

文字列と配列の.lengthプロパティはこの種の操作からは隠されています(列挙可能ではありません。詳細は§28.8.1 「プロパティ属性とプロパティ記述子 [ES5]」を参照)。

スプレッドには、キーがシンボルであるプロパティも含まれます(Object.keys()Object.values()Object.entries()では無視されます)。

const symbolKey = Symbol('symbolKey');
const obj = {
  stringKey: 1,
  [symbolKey]: 2,
};
assert.deepEqual(
  {...obj, anotherStringKey: 3},
  {
    stringKey: 1,
    [symbolKey]: 2,
    anotherStringKey: 3,
  }
);

28.4.1項 スプレッド構文の使用方法:オブジェクトのコピー

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

const copy = {...original};

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

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

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

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

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

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

  JavaScriptには、深層コピーをサポートする組み込み機能はありません

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

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

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

const DEFAULTS = {alpha: 'a', beta: 'b'};
const providedData = {alpha: 1};

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

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

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

const providedData = {alpha: 1};

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

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

これまで、オブジェクトのプロパティ.alphaを変更する方法は1つしか見ていませんでした。それは、(A行)で設定してオブジェクトを変化させる方法です。つまり、このプロパティの変更方法は破壊的です。

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

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

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

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

exercises/objects/update_name_test.mjs

28.4.4 「破壊的なスプレッド」:Object.assign() [ES6]

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

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

この式は、source_1のすべてのプロパティをtargetに、次にsource_2のすべてのプロパティなどを代入します。最後に、targetを返します。例えば以下のように。

const target = { a: 1 };

const result = Object.assign(
  target,
  {b: 2},
  {c: 3, b: true});

assert.deepEqual(
  result, { a: 1, b: true, c: 3 });
// target was modified and returned:
assert.equal(result, target);

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

28.5 メソッドと特別な変数this

28.5.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.5.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はメソッド呼び出しのレシーバです。それは、thisという名前の暗黙的な(隠れた)パラメータを介して、obj.someMethodに格納されている関数に渡されます(A行)。

  thisの理解方法

thisを理解する最良の方法は、通常の関数(そしてメソッドも)の暗黙的なパラメータとして考えることです。

28.5.3 メソッドと.call()

メソッドは関数であり、関数はそれ自体がメソッドを持っています。そのメソッドの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.5.4 メソッドと.bind()

.bind()は、関数オブジェクトの別のメソッドです。次のコードでは、.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に設定することは、ここで非常に重要です。そうでなければ、thisはA行で使用されているため、func()は正しく動作しません。次のセクションでは、その理由を調べます。

28.5.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 properties of undefined (reading 'first')",
  });

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

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

これを修正するにはどうすればよいでしょうか?メソッド.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.5.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()を正しく抽出していません。代わりに、次のようにする必要があります。

const listener = this.handleClick.bind(this);
elem.addEventListener('click', listener);

// Later, possibly:
elem.removeEventListener('click', listener);

.bind()の各呼び出しは、新しい関数を生成します。そのため、後で削除したい場合は、その結果をどこかに保存する必要があります。

28.5.5.2 メソッドの抽出の落とし穴を回避する方法

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

  演習:メソッドの抽出

exercises/objects/method_extraction_exrc.mjs

28.5.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']),
  {
    name: 'TypeError',
    message: "Cannot read properties of undefined (reading 'prefix')",
  }
);

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']);

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

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.5.6.1 thisの意図しないシャドウイングの落とし穴を回避する

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

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

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

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

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

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

トップレベルのthisは混乱を招きやすく、その(少ない)ユースケースにはより良い代替手段があるため、私はそうしています。

28.6 プロパティ取得とメソッド呼び出しのためのオプションチェーン[ES2020](上級)

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

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

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

3つの構文のそれぞれについては、後で詳しく説明します。以下は最初のいくつかの例です。

> null?.prop
undefined
> {prop: 1}?.prop
1

> null?.(123)
undefined
> String?.(123)
'123'

28.6.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.6.1.1 nullish 合体演算子を使用したデフォルトの処理

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

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

28.6.2 演算子の詳細(上級)

28.6.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.6.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.6.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.6.3 オプションのプロパティ取得による短絡評価

プロパティ取得とメソッド呼び出しのチェーンでは、最初のオプション演算子が左辺でundefinedまたはnullに遭遇すると、評価が停止します。

function invokeM(value) {
  return value?.a.b.m(); // (A)
}

const obj = {
  a: {
    b: {
      m() { return 'result' }
    }
  }
};
assert.equal(
  invokeM(obj), 'result'
);
assert.equal(
  invokeM(undefined), undefined // (B)
);

B行のinvokeM(undefined)を考えてみましょう。undefined?.aundefinedです。したがって、A行で.bが失敗することが予想されます。しかし、そうではありません。?.演算子は値undefinedに遭遇し、式全体の評価はすぐにundefinedを返します。

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

28.6.4 オプショナルチェイニング:欠点と代替案

オプショナルチェイニングにも欠点があります。

オプショナルチェイニングの代替案として、情報を一度に単一の位置で抽出する方法があります。

どちらのアプローチでも、チェックを実行し、問題があれば早期に失敗させることが可能です。

さらに読む

28.6.5 よくある質問

28.6.5.1 オプショナルチェイニング演算子(?.)の良いニーモニックとは?

オプショナルチェイニング演算子がドット(.?)で始まるのか、疑問符(?.)で始まるのか、時々迷うことはありませんか?その場合、このニーモニックが役立つかもしれません。

28.6.5.2 なぜ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.6.5.3 なぜnull?.propnullではなくundefinedを評価するのですか?

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

28.7 ディクショナリオブジェクト(上級)

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

まず、ディクショナリに関連するが、固定レイアウトオブジェクトにも役立つオブジェクトの機能を見ていきます。このセクションは、オブジェクトをディクショナリとして実際に使用するヒントで締めくくります。(ネタバレ:可能であれば、Mapを使用する方が良いでしょう)。

28.7.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.7.2 オブジェクトリテラルにおける計算されたキー

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

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

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

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

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

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

assert.equal(obj['p'+'r'+'o'+'p'], 123);
assert.equal(obj['==> prop'.slice(4)], 123);

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

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

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

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

  演習:スプレッド(計算されたキー)を使用してプロパティを非破壊的に更新する

exercises/objects/update_property_test.mjs

28.7.3 in演算子:指定されたキーを持つプロパティが存在するか?

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

const obj = {
  alpha: 'abc',
  beta: false,
};

assert.equal('alpha' in obj, true);
assert.equal('beta' in obj, true);
assert.equal('unknownKey' in obj, false);
28.7.3.1 真偽値によるプロパティの存在チェック

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

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

前のチェックは、obj.alphaが真であり、存在しないプロパティを読み取るとundefined(偽)が返されるため機能します。

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

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

28.7.4 プロパティの削除

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

const obj = {
  myProp: 123,
};

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

28.7.5 列挙可能性

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

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,
  },
});

// Non-enumerable properties are ignored by spreading:
assert.deepEqual(
  {...obj},
  {
    enumerableStringKey: 1,
    [enumerableSymbolKey]: 2,
  }
);

Object.defineProperties()についてはこの章の後半で説明します。次の小節では、これらの操作が列挙可能性によってどのように影響を受けるかを示します。

28.7.6 Object.keys()などによるプロパティキーの列挙

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

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

4つの操作を示すために、前節の例を再訪します。

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

const obj = {
  enumerableStringKey: 1,
  [enumerableSymbolKey]: 2,
}
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,
  ]
);

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

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

const firstName = Symbol('firstName');
const obj = {
  [firstName]: 'Jane',
  lastName: 'Doe',
};
assert.deepEqual(
  Object.values(obj),
  ['Doe']);

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

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

const firstName = Symbol('firstName');
const obj = {
  [firstName]: 'Jane',
  lastName: 'Doe',
};
assert.deepEqual(
  Object.entries(obj),
  [
    ['lastName', 'Doe'],
  ]);
28.7.8.1 Object.entries()の簡単な実装

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

function entries(obj) {
  return Object.keys(obj)
  .map(key => [key, obj[key]]);
}

  演習:Object.entries()

exercises/objects/find_key_test.mjs

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

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

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

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

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

  プロパティの順序

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

28.7.10 Object.fromEntries()によるオブジェクトのアセンブル [ES2019]

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

const symbolKey = Symbol('symbolKey');
assert.deepEqual(
  Object.fromEntries(
    [
      ['stringKey', 1],
      [symbolKey, 2],
    ]
  ),
  {
    stringKey: 1,
    [symbolKey]: 2,
  }
);

Object.fromEntries()Object.entries()の反対を行います。ただし、Object.entries()はシンボルキーのプロパティを無視しますが、Object.fromEntries()は無視しません(前の例を参照)。

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

28.7.10.1 例:pick()

Underscore関数pick()は、次のシグネチャを持っています。

pick(object, ...keys)

これは、末尾の引数に記載されているキーを持つプロパティのみを持つ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.7.10.2 例:invert()

Underscore関数invert()は、次のシグネチャを持っています。

invert(object)

これは、すべてのプロパティのキーと値が入れ替えられた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.7.10.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/objects/omit_properties_test.mjs

28.7.11 オブジェクトをディクショナリとして使用することの落とし穴

プレーンオブジェクト(オブジェクトリテラルを介して作成)をディクショナリとして使用する場合、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.7.11.1 オブジェクトをディクショナリとして安全に使用する方法

では、どのようにして2つの落とし穴を回避するのでしょうか?

次のコードは、プロトタイプのないオブジェクトをディクショナリとして使用する方法を示しています。

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

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

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

2つの落とし穴を回避しました。

  演習:オブジェクトをディクショナリとして使用

exercises/objects/simple_dict_test.mjs

28.8 プロパティ属性とオブジェクトの凍結(上級)

28.8.1 プロパティ属性とプロパティディスクリプタ [ES5]

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

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

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

そして、obj.myPropの属性は次のように変更します。

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

// Hide property `myProp` from Object.keys()
// by making it non-enumerable
Object.defineProperty(obj, 'myProp', {
  enumerable: false,
});

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

さらに読む

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'/,
  });

内部的には、Object.freeze()はプロパティの属性(例:書き込み不可にする)とオブジェクトの属性(例:拡張不可にする、つまりプロパティを追加できなくなる)を変更します。

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

  詳細情報

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

28.9 プロトタイプチェーン

プロトタイプはJavaScript唯一の継承メカニズムです。各オブジェクトには、nullかオブジェクトのいずれかのプロトタイプがあります。後者の場合、オブジェクトはそのプロトタイプのすべてのプロパティを継承します。

オブジェクトリテラルでは、特別なプロパティ__proto__を使用してプロトタイプを設定できます。

const proto = {
  protoProp: 'a',
};
const obj = {
  __proto__: proto,
  objProp: 'b',
};

// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);

プロトタイプオブジェクト自体にプロトタイプを持つことができるため、オブジェクトのチェーン(いわゆるプロトタイプチェーン)が作成されます。継承により、単一のオブジェクトを扱っているように見えますが、実際にはオブジェクトのチェーンを扱っています。

9は、objのプロトタイプチェーンを示しています。

Figure 9: obj starts a chain of objects that continues with proto and other objects.

継承されていないプロパティは、固有プロパティと呼ばれます。objには、.objPropという1つの固有プロパティがあります。

28.9.1 JavaScriptの演算子:すべてのプロパティと固有プロパティ

一部の演算子は、すべてのプロパティ(固有プロパティと継承プロパティ)を考慮します。たとえば、プロパティの取得などです。

> const obj = { one: 1 };
> typeof obj.one // own
'number'
> typeof obj.toString // inherited
'function'

他の演算子は、固有プロパティのみを考慮します。たとえば、Object.keys()などです。

> Object.keys(obj)
[ 'one' ]

固有プロパティのみを考慮する別の演算子について、以下で説明します。それはプロパティの設定です。

28.9.2 陥りやすい点:プロトタイプチェーンの最初のメンバーのみが変更される

プロトタイプオブジェクトのチェーンを持つオブジェクトobjの場合、objの固有プロパティを設定すると、objのみが変更されるのは理にかなっています。ただし、objを介して継承プロパティを設定した場合も、objのみが変更されます。継承されたプロパティを上書きするobjに新しい固有プロパティが作成されます。次のオブジェクトを使用して、その動作を見てみましょう。

const proto = {
  protoProp: 'a',
};
const obj = {
  __proto__: proto,
  objProp: 'b',
};

次のコードスニペットでは、継承プロパティobj.protoProp(A行)を設定します。これは、固有プロパティを作成することで「変更」されます。obj.protoPropを読み取ると、まず固有プロパティが見つかり、その値が継承プロパティの値を上書きします。

// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);

obj.protoProp = 'x'; // (A)

// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);

// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');

// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');

objのプロトタイプチェーンは、図10に示されています。

Figure 10: The own property .protoProp of obj overrides the property inherited from proto.

28.9.3 プロトタイプを操作するためのヒント(上級)

28.9.3.1 プロトタイプの取得と設定

__proto__に関する推奨事項

プロトタイプの取得と設定の推奨方法は次のとおりです。

これらの機能の使用方法を次に示します。

const proto1 = {};
const proto2a = {};
const proto2b = {};

const obj1 = {
  __proto__: proto1,
  a: 1,
  b: 2,
};
assert.equal(Object.getPrototypeOf(obj1), proto1);

const obj2 = Object.create(
  proto2a,
  {
    a: {
      value: 1,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    b: {
      value: 2,
      writable: true,
      enumerable: true,
      configurable: true,
    },  
  }
);
assert.equal(Object.getPrototypeOf(obj2), proto2a);

Object.setPrototypeOf(obj2, proto2b);
assert.equal(Object.getPrototypeOf(obj2), proto2b);
28.9.3.2 オブジェクトが別のオブジェクトのプロトタイプチェーン内にあるかどうかを確認する

これまで「protoobjのプロトタイプである」とは常に「protoobj直接プロトタイプである」ことを意味していました。しかし、これはより緩やかに使用され、「protoobjのプロトタイプチェーン内にある」ことを意味することもできます。このより緩やかな関係は、.isPrototypeOf()を使用して確認できます。

例:

const a = {};
const b = {__proto__: a};
const c = {__proto__: b};

assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);

assert.equal(c.isPrototypeOf(a), false);
assert.equal(a.isPrototypeOf(a), false);

このメソッドの詳細については、§29.8.5「Object.prototype.isPrototypeOf()を参照してください。

28.9.4 Object.hasOwn(): 指定されたプロパティは固有(非継承)ですか? [ES2022]

in演算子(A行)は、オブジェクトに指定されたプロパティがあるかどうかを確認します。これに対して、Object.hasOwn()(B行とC行)は、プロパティが固有かどうかを確認します。

const proto = {
  protoProp: 'protoProp',
};
const obj = {
  __proto__: proto,
  objProp: 'objProp',
}
assert.equal('protoProp' in obj, true); // (A)
assert.equal(Object.hasOwn(obj, 'protoProp'), false); // (B)
assert.equal(Object.hasOwn(proto, 'protoProp'), true); // (C)

  ES2022以前の代替手段: .hasOwnProperty()

ES2022以前は、別の機能を使用できます。§29.8.8「Object.prototype.hasOwnProperty()。この機能には欠点がありますが、参照されているセクションでは、それらの回避方法について説明しています。

28.9.5 プロトタイプによるデータの共有

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

const jane = {
  firstName: 'Jane',
  describe() {
    return 'Person named '+this.firstName;
  },
};
const tarzan = {
  firstName: 'Tarzan',
  describe() {
    return 'Person named '+this.firstName;
  },
};

assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');

非常に似た2つのオブジェクトがあるとします。どちらも名前が.firstName.describeである2つのプロパティを持っています。さらに、メソッド.describe()は同じです。このメソッドの重複を避けるにはどうすればよいでしょうか?

オブジェクトPersonProtoに移動し、そのオブジェクトをjanetarzanの両方のプロトタイプにすることができます。

const PersonProto = {
  describe() {
    return 'Person named ' + this.firstName;
  },
};
const jane = {
  __proto__: PersonProto,
  firstName: 'Jane',
};
const tarzan = {
  __proto__: PersonProto,
  firstName: 'Tarzan',
};

プロトタイプの名前は、janetarzanの両方が人であることを反映しています。

Figure 11: Objects jane and tarzan share method .describe(), via their common prototype PersonProto.

11は、3つのオブジェクトがどのように接続されているかを示しています。下部のオブジェクトには、janetarzanに固有のプロパティが含まれています。上部のオブジェクトには、それらで共有されるプロパティが含まれています。

メソッド呼び出しjane.describe()を行うと、thisはメソッド呼び出しのレシーバーであるjane(図の左下隅)を指します。そのため、メソッドは引き続き機能します。tarzan.describe()も同様に機能します。

assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');

クラスに関する次の章を先取りすると、クラスは内部的にこのように構成されています。

§29.3「クラスの内部構造」でより詳細に説明されています。

28.10 FAQ: オブジェクト

28.10.1 なぜオブジェクトはプロパティの挿入順序を保持するのですか?

原則として、オブジェクトは順序付けられていません。プロパティを順序付ける主な理由は、エントリ、キー、または値をリストする演算子を決定論的にするためにあります。これにより、たとえばテストが容易になります。

  クイズ

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