オブジェクト

オブジェクトはプロパティの集合です。プロパティとは名前(キー)と値(バリュー)が対になったものです。 プロパティのキーには文字列またはSymbolが利用でき、値には任意のデータを指定できます。 また、1つのオブジェクトは複数のプロパティを持てるため、1つのオブジェクトで多種多様な値を表現できます。

今までも登場してきた、配列や関数などもオブジェクトの一種です。 JavaScriptには、あらゆるオブジェクトの元となるObjectというビルトインオブジェクトがあります。 ビルトインオブジェクトは、実行環境にあらかじめ定義されているオブジェクトのことです。 ObjectというビルトインオブジェクトはECMAScriptの仕様で定義されているため、あらゆるJavaScriptの実行環境で利用できます。

この章では、オブジェクトの作成や扱い方、Objectというビルトインオブジェクトについて見ていきます。

オブジェクトを作成する

オブジェクトを作成するには、オブジェクトリテラル({})を利用します。

// プロパティを持たない空のオブジェクトを作成
const obj = {};

オブジェクトリテラルでは、初期値としてプロパティを持つオブジェクトを作成できます。 プロパティは、オブジェクトリテラル({})の中にキーと値を:(コロン)で区切って記述します。

// プロパティを持つオブジェクトを定義する
const obj = {
    // キー: 値
    "key": "value"
};

オブジェクトリテラルのプロパティ名(キー)はクォート("')を省略できます。 そのため、次のように書いても同じです。

// プロパティ名(キー)はクォートを省略することが可能
const obj = {
    // キー: 値
    key: "value"
};

ただし、変数名として利用できないプロパティ名はクォート("')で囲む必要があります。 次のmy-propというプロパティ名は、変数名として利用できない-が含まれているため定義できません(「変数と宣言」の章の「変数名に使える名前のルール 」を参照)

const object = {
    // キー: 値
    my-prop: "value" // NG
};

my-propというプロパティ名を定義する場合は、クォート("')で囲む必要があります。

const obj = {
    // キー: 値
    "my-prop": "value" // OK
};

オブジェクトリテラルでは複数のプロパティ(キーと値の組み合わせ)を持つオブジェクトも作成できます。 複数のプロパティを定義するには、それぞれのプロパティを,(カンマ)で区切ります。

const color = {
    // それぞれのプロパティは`,`で区切る
    red: "red",
    green: "green",
    blue: "blue"
};

プロパティの値に変数名を指定すれば、そのキーは指定した変数を参照します。

const name = "名前";
// `name`というプロパティ名で`name`の変数を値に設定したオブジェクト
const obj = {
    name: name
};
console.log(obj); // => { name: "名前" }

またES2015からは、プロパティ名と値に指定する変数名が同じ場合は{ name }のように省略して書けます。 次のコードは、プロパティ名nameに変数nameを値にしたプロパティを設定しています。

const name = "名前";
// `name`というプロパティ名で`name`の変数を値に設定したオブジェクト
const obj = {
    name
};
console.log(obj); // => { name: "名前" }

この省略記法は、モジュールや分割代入においても共通した表現です。 そのため、{}の中でプロパティ名が単独で書かれている場合は、この省略記法を利用していることに注意してください。

{}Objectのインスタンスオブジェクト

ObjectはJavaScriptのビルトインオブジェクトです。 オブジェクトリテラル({})は、このビルトインオブジェクトであるObjectを元にして新しいオブジェクトを作成するための構文です。

オブジェクトリテラル以外の方法として、new演算子を使うことで、Objectから新しいオブジェクトを作成できます。 次のコードでは、new Object()でオブジェクトを作成していますが、これは空のオブジェクトリテラルと同じ意味です。

// プロパティを持たない空のオブジェクトを作成
// = `Object`からインスタンスオブジェクトを作成
const obj = new Object();
console.log(obj); // => {}

オブジェクトリテラルのほうが明らかに簡潔で、プロパティの初期値も指定できるため、new Object()を使う利点はありません。

new Object()でオブジェクトを作成することは、「Objectのインスタンスオブジェクトを作成する」と言います。 しかしながら、Objectやインスタンスオブジェクトなどややこしい言葉の使い分けが必要となってしまいます。 そのため、この書籍ではオブジェクトリテラルとnew Objectどちらの方法であっても、単に「オブジェクトを作成する」と呼びます。

オブジェクトリテラルは、Objectから新しいインスタンスオブジェクトを作成していることを意識しておくとよいでしょう。

プロパティへのアクセス

オブジェクトのプロパティにアクセスする方法として、ドット記法(.)を使う方法とブラケット記法([])があります。 それぞれの記法でプロパティ名を指定すると、その名前を持ったプロパティの値を参照できます。

const obj = {
    key: "value"
};
// ドット記法で参照
console.log(obj.key); // => "value"
// ブラケット記法で参照
console.log(obj["key"]); // => "value"

ドット記法(.)では、プロパティ名が変数名と同じく識別子の命名規則を満たす必要があります(「変数と宣言」の章の「変数名に使える名前のルール 」を参照)。

obj.key; // OK
// プロパティ名が数字から始まる識別子は利用できない
obj.123; // NG
// プロパティ名にハイフンを含む識別子は利用できない
obj.my-prop; // NG

一方、ブラケット記法では、[]の間に任意の式を書けます。 そのため、識別子の命名規則とは関係なく、任意の文字列をプロパティ名として指定できます。 ただし、プロパティ名は文字列へと暗黙的に変換されることに注意してください。

const obj = {
    key: "value",
    123: 456,
    "my-key": "my-value"
};

console.log(obj["key"]); // => "value"
// プロパティ名が数字からはじまる識別子も利用できる
console.log(obj[123]); // => 456
// プロパティ名は暗黙的に文字列に変換されているため、次も同じプロパティを参照している
console.log(obj["123"]); // => 456
// プロパティ名にハイフンを含む識別子も利用できる
console.log(obj["my-key"]); // => "my-value"

また、ブラケット記法ではプロパティ名に変数も利用できます。 次のコードでは、プロパティ名にmyLangという変数をブラケット記法で指定しています。

const languages = {
    ja: "日本語",
    en: "英語"
};
const myLang = "ja";
console.log(languages[myLang]); // => "日本語"

ドット記法ではプロパティ名に変数は利用できないため、プロパティ名に変数を指定したい場合はブラケット記法を利用します。 基本的には簡潔なドット記法(.)を使い、ドット記法で書けない場合はブラケット記法([])を使うとよいでしょう。

[ES2015] オブジェクトと分割代入

同じオブジェクトのプロパティに何度もアクセスする場合に、何度もオブジェクト.プロパティ名と書くと冗長となりやすいです。 そのため、短い名前で利用できるように、そのプロパティを変数として定義し直すことがあります。

次のコードでは、変数jaenを定義し、その初期値としてlanguagesオブジェクトのプロパティを代入しています。

const languages = {
    ja: "日本語",
    en: "英語"
};
const ja = languages.ja;
const en = languages.en;
console.log(ja); // => "日本語"
console.log(en); // => "英語"

このようにオブジェクトのプロパティを変数として定義し直すときには、分割代入(Destructuring assignment)が利用できます。

オブジェクトの分割代入では、左辺にオブジェクトリテラルのような構文で変数名を定義します。 右辺のオブジェクトから対応するプロパティ名が、左辺で定義した変数に代入されます。

次のコードでは、先ほどのコードと同じようにlanguagesオブジェクトからjaenプロパティを取り出して変数として定義しています。 代入演算子のオペランドとして左辺と右辺それぞれにjaenと書いていたのが、分割代入では一箇所に書くことができます。

const languages = {
    ja: "日本語",
    en: "英語"
};
const { ja, en } = languages;
console.log(ja); // => "日本語"
console.log(en); // => "英語"

プロパティの追加

オブジェクトは、一度作成した後もその値自体を変更できるというミュータブル(mutable)の特性を持ちます。 そのため、作成したオブジェクトに対して、後からプロパティを追加できます。

プロパティの追加方法は単純で、作成したいプロパティ名へ値を代入するだけです。 そのとき、オブジェクトに指定したプロパティが存在しないなら、自動的にプロパティが作成されます。

プロパティの追加はドット記法、ブラケット記法どちらでも可能です。

// 空のオブジェクト
const obj = {};
// `key`プロパティを追加して値を代入
obj.key = "value";
console.log(obj.key); // => "value"

先ほども紹介したように、ドット記法は変数の識別子として利用可能なプロパティ名しか利用できません。

一方、ブラケット記法はobject[式]の評価結果を文字列にしたものをプロパティ名として利用できます。 そのため、次のものをプロパティ名として扱う場合にはブラケット記法を利用します。

  • 変数
  • 変数の識別子として扱えない文字列
  • Symbol

const key = "key-string";
const obj = {};
// `key`の評価結果 "key-string" をプロパティ名に利用
obj[key] = "value of key";
// 取り出すときも同じく`key`変数を利用
console.log(obj[key]); // => "value of key"

ブラケット記法を用いたプロパティ定義は、オブジェクトリテラルの中でも利用できます。 オブジェクトリテラル内でのブラケット記法を使ったプロパティ名はComputed property namesと呼ばれます。 Computed property namesはES2015から導入された記法ですが、の評価結果をプロパティ名に使う点はブラケット記法と同じです。

次のコードでは、Computed property namesを使いkey変数の評価結果である"key-string"をプロパティ名にしています。

const key = "key-string";
// Computed Propertyで`key`の評価結果 "key-string" をプロパティ名に利用
const obj = {
    [key]: "value"
};
console.log(obj[key]); // => "value"

JavaScriptのオブジェクトは、作成後にプロパティが変更可能というmutableの特性を持つことを紹介しました。 そのため、関数が受け取ったオブジェクトに対して、勝手にプロパティを追加できてしまいます。

次のコードは、changeProperty関数が引数として受け取ったオブジェクトにプロパティを追加している悪い例です。

function changeProperty(obj) {
    obj.key = "value";
    // いろいろな処理...
}
const obj = {};
changeProperty(obj); // objのプロパティを変更している
console.log(obj.key); // => "value"

このように、プロパティを初期化時以外に追加してしまうと、そのオブジェクトがどのようなプロパティを持っているかがわかりにくくなります。 そのため、できる限り作成後に新しいプロパティは追加しないほうがよいでしょう。 オブジェクトの作成時のオブジェクトリテラルの中でプロパティを定義することを推奨します。

プロパティの削除

オブジェクトのプロパティを削除するにはdelete演算子を利用します。 削除したいプロパティをdelete演算子の右辺に指定して、プロパティを削除できます。

const obj = {
    key1: "value1",
    key2: "value2"
};
// key1プロパティを削除
delete obj.key1;
// key1プロパティが削除されている
console.log(obj); // => { "key2": "value2" }

[コラム] constで定義したオブジェクトは変更可能

先ほどのコード例で、constで宣言したオブジェクトのプロパティがエラーなく変更できていることがわかります。 次のコードを実行してみると、値であるオブジェクトのプロパティが変更できていることがわかります。

const obj = { key: "value" };
obj.key = "Hi!"; // constで定義したオブジェクト(`obj`)が変更できる
console.log(obj.key); // => "Hi!"

JavaScriptのconstは値を固定するのではなく、変数への再代入を防ぐためのものです。 そのため、次のようなobj変数への再代入は防げますが、変数に代入された値であるオブジェクトの変更は防げません(「変数と宣言」のconstを参照)。

const obj = { key: "value" };
obj = {}; // => TypeError

作成したオブジェクトのプロパティの変更を防止するにはObject.freezeメソッドを利用する必要があります。 Object.freezeはオブジェクトを凍結します。凍結されたオブジェクトでプロパティの追加や変更をすると例外が発生するようになります。

ただし、Object.freezeメソッドを利用する場合は必ずstrict modeと合わせて使います(詳細は「JavaScriptとは」のstrict modeを参照)。 strict modeではない場合は、凍結されたオブジェクトのプロパティを変更しても例外が発生せずに単純に無視されます。

"use strict";
const object = Object.freeze({ key: "value" });
// freezeしたオブジェクトにプロパティを追加や変更できない
object.key = "value"; // => TypeError: "key" is read-only

プロパティの存在を確認する

JavaScriptでは、存在しないプロパティに対してアクセスした場合に例外ではなくundefinedを返します。 次のコードは、objには存在しないnotFoundプロパティにアクセスしているため、undefinedという値が返ってきます。

const obj = {};
console.log(obj.notFound); // => undefined

このように、JavaScriptでは存在しないプロパティへアクセスした場合に例外が発生しません。 プロパティ名を間違えた場合に単にundefinedという値を返すため、間違いに気づきにくいという問題があります。

次のようにプロパティ名を間違えていた場合にも、例外が発生しません。 さらにプロパティ名をネストしてアクセスした場合に、初めて例外が発生します。

const widget = {
    window: {
        title: "ウィジェットのタイトル"
    }
};
// `window`を`windw`と間違えているが、例外は発生しない
console.log(widget.windw); // => undefined
// さらにネストした場合に、例外が発生する
// `undefined.title`と書いたのと同じ意味となるため
console.log(widget.windw.title); // => TypeError: widget.windw is undefined
// 例外が発生した文以降は実行されません

undefinednullはオブジェクトではないため、存在しないプロパティへアクセスすると例外が発生してしまいます。 あるオブジェクトがあるプロパティを持っているかを確認する方法として、次の4つがあります。

  • undefinedとの比較
  • in演算子
  • Object.hasOwn静的メソッド[ES2022]
  • Object.prototype.hasOwnPropertyメソッド

プロパティの存在確認: undefinedとの比較

存在しないプロパティへアクセスした場合にundefinedを返すため、実際にプロパティアクセスすることでも判定できそうです。 次のコードでは、keyプロパティの値がundefinedではないという条件式で、プロパティが存在するかを判定しています。

const obj = {
    key: "value"
};
// `key`プロパティが`undefined`ではないなら、プロパティが存在する?
if (obj.key !== undefined) {
    // `key`プロパティが存在する?ときの処理
    console.log("`key`プロパティの値は`undefined`ではない");
}

しかし、この方法はプロパティの値がundefinedであった場合に、プロパティそのものが存在するかを区別できないという問題があります。 次のコードでは、keyプロパティの値がundefinedであるため、プロパティが存在しているにもかかわらずif文の中は実行されません。

const obj = {
    key: undefined
};
// `key`プロパティの値が`undefined`である場合
if (obj.key !== undefined) {
    // この行は実行されません
}

このような問題があるため、プロパティが存在するかを判定するにはin演算子かObject.hasOwn静的メソッドを利用します。

プロパティの存在確認: in演算子を使う

in演算子は、指定したオブジェクト上に指定したプロパティがあるかを判定し真偽値を返します。

"プロパティ名" in オブジェクト; // true or false

次のコードではobjkeyプロパティが存在するかを判定しています。 in演算子は、プロパティの値は関係なく、プロパティが存在した場合にtrueを返します。

const obj = { key: undefined };
// `key`プロパティを持っているならtrue
if ("key" in obj) {
    console.log("`key`プロパティは存在する");
}

[ES2022] プロパティの存在確認: Object.hasOwn静的メソッド

Object.hasOwn静的メソッドは、対象のオブジェクトが指定したプロパティを持っているかを判定できます。 このObject.hasOwn静的メソッドの引数には、オブジェクトとオブジェクトが持っているかを確認したいプロパティ名を渡します。

const obj = {};
// objが"プロパティ名"を持っているかを確認する
Object.hasOwn(obj, "プロパティ名"); // true or false

次のコードではobjkeyプロパティが存在するかを判定しています。 Object.hasOwn静的メソッドも、プロパティの値は関係なく、オブジェクトが指定したプロパティを持っている場合にtrueを返します。

const obj = { key: undefined };
// `obj`が`key`プロパティを持っているならtrueとなる
if (Object.hasOwn(obj, "key")) {
    console.log("`obj`は`key`プロパティを持っている");
}

in演算子とObject.hasOwn静的メソッドは同じ結果を返していますが、厳密には動作が異なるケースもあります。 この動作の違いを知るにはまずプロトタイプオブジェクトという特殊なオブジェクトについて理解する必要があります。 そのため、in演算子とObject.hasOwn静的メソッドの違いについては、次の章の「プロトタイプオブジェクト」で詳しく解説します。

プロパティの存在確認: Object.prototype.hasOwnPropertyメソッド

Object.hasOwn静的メソッドはES2022で導入されたメソッドです。 ES2022より前では、Object.prototype.hasOwnPropertyメソッドというよく似たメソッドが利用されていました。 hasOwnPropertyメソッドは、Object.hasOwn静的メソッドとよく似ていますが、オブジェクトのインスタンスから呼び出す点が異なります。

const obj = { key: undefined };
// `obj`が`key`プロパティを持っているならtrueとなる
if (obj.hasOwnProperty("key")) {
    console.log("`obj`は`key`プロパティを持っている");
}

しかし、hasOwnPropertyメソッドには欠点があるため、Object.hasOwn静的メソッドが利用できる状況では使う理由はありません。 この欠点もプロトタイプオブジェクトに関係するため、次の章の「プロトタイプオブジェクト」で詳しく解説します。

[ES2020] Optional chaining演算子(?.

プロパティの存在を確認する方法として4つの方法を紹介しました。 プロパティが存在するかが重要な場合は、基本的にはin演算子またはObject.hasOwn静的メソッドを使います。

しかし、最終的に取得したいものがプロパティの値であるならば、if文でundefinedと比較しても問題ありません。 なぜなら、値を取得したい場合には、プロパティが存在するかどうかとプロパティの値がundefinedかどうかの違いを区別する意味はないためです。

次のコードでは、widget.window.titleプロパティに値が定義されているなら(undefinedではないなら)、そのプロパティの値をコンソールに表示しています。

function printWidgetTitle(widget) {
    // 例外を避けるために`widget`のプロパティの存在を順番に確認してから、値を表示している
    if (widget.window !== undefined && widget.window.title !== undefined) {
        console.log(`ウィジェットのタイトルは${widget.window.title}です`);
    } else {
        console.log("ウィジェットのタイトルは未定義です");
    }
}
// タイトルが定義されているwidget
printWidgetTitle({
    window: {
        title: "Book Viewer"
    }
});
// タイトルが未定義のwidget
printWidgetTitle({
    // タイトルが定義されてない空のオブジェクト
});

このwidget.window.titleのようなネストしたプロパティにアクセスする際には、プロパティの存在を順番に確認してからアクセスする必要があります。 なぜなら、widgetオブジェクトがwindowプロパティを持っていない場合はundefinedという値を返すためです。このときに、さらにネストしたwidget.window.titleプロパティにアクセスすると、undefined.titleという参照となり例外が発生してしまいます。

しかし、プロパティへアクセスするたびにundefinedとの比較をAND演算子(&&)でつなげて書いていくと冗長です。

この問題を解決するために、ES2020ではネストしたプロパティの存在確認とアクセスを簡単に行う構文としてOptional chaining演算子(?.)が導入されました。 Optional chaining演算子(?.)は、ドット記法(.)の代わりに?.をプロパティアクセスに使います。

Optional chaining演算子(?.)は左辺のオペランドがnullish(nullまたはundefined)の場合は、それ以上評価せずにundefinedを返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。

つまり、Optional chaining演算子(?.)では、存在しないプロパティへアクセスした場合でも例外ではなく、undefinedという値を返します。

const obj = {
    a: {
        b: "objのaプロパティのbプロパティ"
    }
};
// obj.a.b は存在するので、その評価結果を返す
console.log(obj?.a?.b); // => "objのaプロパティのbプロパティ"
// 存在しないプロパティのネストも`undefined`を返す
// ドット記法の場合は例外が発生してしまう
console.log(obj?.notFound?.notFound); // => undefined
// undefinedやnullはnullishなので、`undefined`を返す
console.log(undefined?.notFound?.notFound); // => undefined
console.log(null?.notFound?.notFound); // => undefined

先ほどのウィジェットのタイトルを表示する関数もOptional chaining演算子(?.)を使うと、if文を使わずに書けます。 次のコードのprintWidgetTitle関数では、widget?.window?.titleにアクセスできる場合はその評価結果が変数titleに入ります。 プロパティにアクセスできない場合はundefinedを返すため、Nullish coalescing演算子(??)によって右辺の"未定義"が変数titleのデフォルト値となります。

function printWidgetTitle(widget) {
    const title = widget?.window?.title ?? "未定義";
    console.log(`ウィジェットのタイトルは${title}です`);
}
printWidgetTitle({
    window: {
        title: "Book Viewer"
    }
}); // "ウィジェットのタイトルはBook Viewerです" と出力される
printWidgetTitle({
    // タイトルが定義されてない空のオブジェクト
}); // "ウィジェットのタイトルは未定義です" と出力される

また、Optional chaining演算子(?.)はブラケット記法([])と組み合わせることもできます。 ブラケット記法の場合も、左辺のオペランドがnullish(nullまたはundefined)の場合は、それ以上評価せずにundefinedを返します。一方で、プロパティが存在する場合は、そのプロパティの評価結果を返します。

const languages = {
    ja: {
        hello: "こんにちは!"
    },
    en: {
        hello: "Hello!"
    }
};
const langJapanese = "ja";
const langKorean = "ko";
const messageKey = "hello";
// Optional chaining演算子(`?.`)とブラケット記法を組みわせた書き方
console.log(languages?.[langJapanese]?.[messageKey]); // => "こんにちは!"
// `languages`に`ko`プロパティが定義されていないため、`undefined`を返す
console.log(languages?.[langKorean]?.[messageKey]); // => undefined

toStringメソッド

オブジェクトのtoStringメソッドは、オブジェクト自身を文字列化するメソッドです。 Stringコンストラクタ関数を使うことでも文字列化できます。 この2つにはどのような違いがあるのでしょうか?(Stringコンストラクタ関数については「暗黙的な型変換」を参照)

実はStringコンストラクタ関数は、引数に渡されたオブジェクトのtoStringメソッドを呼び出しています。 そのため、Stringコンストラクタ関数とtoStringメソッドの結果はどちらも同じになります。

const obj = { key: "value" };
console.log(obj.toString()); // => "[object Object]"
// `String`コンストラクタ関数は`toString`メソッドを呼んでいる
console.log(String(obj)); // => "[object Object]"

このことは、オブジェクトにtoStringメソッドを再定義してみるとわかります。 独自のtoStringメソッドを定義したオブジェクトをStringコンストラクタ関数で文字列化してみます。 すると、再定義したtoStringメソッドの返り値が、Stringコンストラクタ関数の返り値になることがわかります。

// 独自のtoStringメソッドを定義
const customObject = {
    toString() {
        return "custom value";
    }
};
console.log(String(customObject)); // => "custom value"

[コラム] オブジェクトのプロパティ名は文字列化される

オブジェクトのプロパティへアクセスする際に、指定したプロパティ名は暗黙的に文字列に変換されます。 ブラケット記法では、オブジェクトをプロパティ名に指定することもできますが、これは意図したようには動作しません。 なぜなら、オブジェクトを文字列化すると"[object Object]"という文字列になるためです。

次のコードでは、keyObject1keyObject2をブラケット記法でプロパティ名に指定しています。 しかし、keyObject1keyObject2はどちらも文字列化すると"[object Object]"という同じプロパティ名となります。 そのため、プロパティは意図せず上書きされてしまいます。

const obj = {};
const keyObject1 = { a: 1 };
const keyObject2 = { b: 2 };
// どちらも同じプロパティ名("[object Object]")に代入している
obj[keyObject1] = "1";
obj[keyObject2] = "2";
console.log(obj); //  { "[object Object]": "2" }

唯一の例外として、Symbolだけは文字列化されずにオブジェクトのプロパティ名として扱えます。

const obj = {};
// Symbolは例外的に文字列化されず扱える
const symbolKey1 = Symbol("シンボル1");
const symbolKey2 = Symbol("シンボル2");
obj[symbolKey1] = "1";
obj[symbolKey2] = "2";
console.log(obj[symbolKey1]); // => "1"
console.log(obj[symbolKey2]); // => "2"

基本的にはオブジェクトのプロパティ名は文字列として扱われることを覚えておくとよいでしょう。 また、Mapというビルトインオブジェクトはオブジェクトをキーとして扱えます(詳細は「Map/Set」の章で解説します)。 そのため、オブジェクトをキーに指定したい場合はMapを利用します。

オブジェクトの静的メソッド

最後にビルトインオブジェクトであるObjectの静的メソッドについて見ていきましょう。 静的メソッド(スタティックメソッド)とは、インスタンスの元となるオブジェクトから呼び出せるメソッドのことです。

ObjectのtoStringメソッドなどは、Objectのインスタンスオブジェクトから呼び出すメソッドでした。 これに対して、Object.hasOwn静的メソッドのような静的メソッドはObjectそのものに実装されているメソッドです。

ここでは、オブジェクトの処理でよく利用されるいくつかの静的メソッドを紹介します。

オブジェクトの列挙

最初に紹介したように、オブジェクトはプロパティの集合です。 そのオブジェクトのプロパティを列挙する方法として、次の3つの静的メソッドがあります。

  • Object.keysメソッド: オブジェクトのプロパティ名の配列にして返す
  • Object.valuesメソッド[ES2017]: オブジェクトの値の配列にして返す
  • Object.entriesメソッド[ES2017]: オブジェクトのプロパティ名と値の配列の配列を返す

それぞれ、オブジェクトのキー、値、キーと値の組み合わせを配列にして返します。

const obj = {
    "one": 1,
    "two": 2,
    "three": 3
};
// `Object.keys`はキーを列挙した配列を返す
console.log(Object.keys(obj)); // => ["one", "two", "three"]
// `Object.values`は値を列挙した配列を返す
console.log(Object.values(obj)); // => [1, 2, 3]
// `Object.entries`は[キー, 値]の配列を返す
console.log(Object.entries(obj)); // => [["one", 1], ["two", 2], ["three", 3]]

これらのプロパティを列挙する静的メソッドと配列のforEachメソッドなどを組み合わせれば、プロパティに対して反復処理ができます。 次のコードでは、Object.keysメソッドで取得したプロパティ名の一覧をコンソールへ出力しています。

const obj = {
    "one": 1,
    "two": 2,
    "three": 3
};
const keys = Object.keys(obj);
keys.forEach(key => {
    console.log(key);
});
// 次の値が順番に出力される
// "one"
// "two"
// "three"

オブジェクトのマージと複製

Object.assignメソッド[ES2015]は、あるオブジェクトを別のオブジェクトに代入(assign)できます。 このメソッドを使うことで、オブジェクトの複製やオブジェクト同士のマージができます。

Object.assignメソッドは、targetオブジェクトに対して、1つ以上のsourcesオブジェクトを指定します。 sourcesオブジェクト自身が持つ列挙可能なプロパティを第一引数のtargetオブジェクトに対してコピーします。 Object.assignメソッドの返り値は、targetオブジェクトになります。

const obj = Object.assign(target, ...sources);

オブジェクトのマージ

具体的なオブジェクトのマージの例を見ていきます。

次のコードでは、新しく作った空のオブジェクトをtargetにしています。 この空のオブジェクト(target)にobjectAobjectBをマージしたものが、Object.assignメソッドの返り値となります。

const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = Object.assign({}, objectA, objectB);
console.log(merged); // => { a: "a", b: "b" }

第一引数には空のオブジェクトではなく、既存のオブジェクトも指定できます。 第一引数に既存のオブジェクトを指定した場合は、そのオブジェクトのプロパティが変更されます。

次のコードでは、第一引数に指定されたobjectAに対してプロパティが追加されています。

const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = Object.assign(objectA, objectB);
console.log(merged); // => { a: "a", b: "b" }
// `objectA`が変更されている
console.log(objectA); // => { a: "a", b: "b" }
console.log(merged === objectA); // => true

空のオブジェクトをtargetにすることで、既存のオブジェクトには影響を与えずマージしたオブジェクトを作ることができます。 そのため、Object.assignメソッドの第一引数には、空のオブジェクトリテラルを指定するのが典型的な利用方法です。

このとき、プロパティ名が重複した場合は、後ろのオブジェクトのプロパティにより上書きされます。 JavaScriptでは、基本的に処理は先頭から後ろへと順番に行います。 そのため、空のオブジェクトへobjectAを代入してから、その結果にobjectBを代入するという形になります。

// `version`のプロパティ名が被っている
const objectA = { version: "a" };
const objectB = { version: "b" };
const merged = Object.assign({}, objectA, objectB);
// 後ろにある`objectB`のプロパティで上書きされる
console.log(merged); // => { version: "b" }

[ES2018] オブジェクトのspread構文でのマージ

ES2018では、オブジェクトのマージを行うオブジェクトの...(spread構文)が追加されました。 ES2015で配列の要素を展開する...(spread構文)はサポートされていましたが、オブジェクトに対してもES2018でサポートされました。 オブジェクトのspread構文は、オブジェクトリテラルの中に指定したオブジェクトのプロパティを展開できます。

オブジェクトのspread構文は、Object.assignとは異なり必ず新しいオブジェクトを作成します。 なぜならspread構文はオブジェクトリテラルの中でのみ記述でき、オブジェクトリテラルは新しいオブジェクトを作成するためです。

次のコードではobjectAobjectBをマージした新しいオブジェクトを返します。

const objectA = { a: "a" };
const objectB = { b: "b" };
const merged = {
    ...objectA,
    ...objectB
};
console.log(merged); // => { a: "a", b: "b" }

プロパティ名が被った場合の優先順位は、後ろにあるオブジェクトが優先されます。 そのため同じプロパティ名を持つオブジェクトをマージした場合には、後ろにあるオブジェクトによってプロパティが上書きされます。

// `version`のプロパティ名が被っている
const objectA = { version: "a" };
const objectB = { version: "b" };
const merged = {
    ...objectA,
    ...objectB,
    other: "other"
};
// 後ろにある`objectB`のプロパティで上書きされる
console.log(merged); // => { version: "b", other: "other" }

オブジェクトの複製

JavaScriptには、オブジェクトを複製する関数は用意されていません。 しかし、新しく空のオブジェクトを作成し、そこへ既存のオブジェクトのプロパティをコピーすれば、それはオブジェクトの複製をしていると言えます。 次のように、Object.assignメソッドを使うことでオブジェクトを複製できます。

// 引数の`obj`を浅く複製したオブジェクトを返す
const shallowClone = (obj) => {
    return Object.assign({}, obj);
};
const obj = { a: "a" };
const cloneObj = shallowClone(obj);
console.log(cloneObj); // => { a: "a" }
// オブジェクトを複製しているので、異なるオブジェクトとなる
console.log(obj === cloneObj); // => false

注意点として、Object.assignメソッドはsourcesオブジェクトのプロパティを浅くコピー(shallow copy)する点です。 shallow copyとは、sourcesオブジェクトの直下にあるプロパティだけをコピーするということです。 そのプロパティの値がオブジェクトである場合に、ネストした先のオブジェクトまでも複製するわけではありません。

const shallowClone = (obj) => {
    return Object.assign({}, obj);
};
const obj = {
    level: 1,
    nest: {
        level: 2
    },
};
const cloneObj = shallowClone(obj);
// `nest`プロパティのオブジェクトは同じオブジェクトのままになる 
console.log(cloneObj.nest === obj.nest); // => true

逆にプロパティの値までも再帰的に複製してコピーすることを、深いコピー(deep copy)と呼びます。 deep copyは、再帰的にshallow copyすることで実現できます。 次のコードでは、deepCloneshallowCloneを使うことで実現しています。

// 引数の`obj`を浅く複製したオブジェクトを返す
const shallowClone = (obj) => {
    return Object.assign({}, obj);
};
// 引数の`obj`を深く複製したオブジェクトを返す
function deepClone(obj) {
    const newObj = shallowClone(obj);
    // プロパティがオブジェクト型であるなら、再帰的に複製する
    Object.keys(newObj)
        .filter(k => typeof newObj[k] === "object")
        .forEach(k => newObj[k] = deepClone(newObj[k]));
    return newObj;
}
const obj = {
    level: 1,
    nest: {
        level: 2
    }
};
const cloneObj = deepClone(obj);
// `nest`オブジェクトも再帰的に複製されている
console.log(cloneObj.nest === obj.nest); // => false

このように、JavaScriptのビルトインメソッドは浅い(shallow)実装のみを提供し、深い(deep)実装は提供していないことが多いです。 言語としては最低限の機能を提供し、より複雑な機能はユーザー側で実装するという形式を取るためです。

JavaScriptは言語仕様で定義されている機能が最低限であるため、それを補うようにユーザーが作成した小さな機能を持つライブラリが数多く公開されています。 それらのライブラリはnpmと呼ばれるJavaScriptのパッケージ管理ツールで公開され、JavaScriptのエコシステムを築いています。 ライブラリの利用については「ユースケース: Node.jsでCLIアプリケーション」の章で紹介します。

まとめ

この章では、オブジェクトについて学びました。

  • Objectというビルトインオブジェクトがある
  • {}(オブジェクトリテラル)でのオブジェクトの作成や更新方法
  • プロパティの存在を確認するにはin演算子かObject.hasOwn静的メソッドを使う
  • Optional chaining演算子(?.)はネストしたプロパティの存在確認とアクセスを同時に行う記法
  • オブジェクトのインスタンスメソッドと静的メソッド

JavaScriptのObjectは他のオブジェクトのベースとなるオブジェクトです。 次の「プロトタイプオブジェクト」の章では、Objectがどのようにベースとして動作しているのかを見ていきます。