オブジェクト

オブジェクトとは

オブジェクトはプロパティの集合です。プロパティとはキー(名前)と値から構成されるものを言います。 キーには文字列またはSymbolが利用でき、値には任意のデータが利用できます。

オブジェクトを作成するには、オブジェクトリテラル({})を利用する方法が簡単です。 また、オブジェクトリテラルのプロパティ名はクオート("')を省略することが可能です。

// プロパティ名はクオートを省略することが可能
const object = {
    key: "value"
};

またES2015からは、プロパティ名と値となる変数名が同じ場合は{ name }のように省略して書くことができます。

const name = "名前";
// `name`というプロパティ名で`name`の変数を値に設定
const objectA = {
    name
};
// 次のように書いた場合と同じ
const objectB = {
    name: name
};

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

プロパティへのアクセス

オブジェクトのプロパティにアクセスする方法として、ドット記法(.)を使う方法とブラケット記法([])があります。

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

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

object.key; // OK!
// プロパティ名が数字から始まる識別子は利用できない
object.123; // NG!

一方、ブラケット記法では、[]の間に任意の式を書くことができます。 その式の評価結果の文字列をプロパティ名として利用できます。 そのため、ブラケット記法では、プロパティ名に任意の文字列や変数を利用できます。

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

基本的にはドット記法(.)を使い、ドット記法で書けない場合はブラケット記法([])を使うとよいでしょう。

プロパティの作成

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

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

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

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

先ほども紹介したように、ドット記法は変数の識別子として利用可能なプロパティ名しか利用できません。 一方、ブラケット記法はobject[式]の評価結果を文字列にしたものをプロパティ名として利用できます。 そのため、次のものをプロパティ名として扱う場合にはブラケット記法を利用します。

  • 変数
  • 識別子として書けない文字列
  • Symbol

const key = "key-string";
const object = {};
// `key`の評価結果 "key-string" をプロパティ名に利用
object[key] = "value of key";
// 取り出すときも同じく`key`変数を利用
console.log(object[key]); // => "value of key"
// Symbolは例外的に文字列化されず扱える
const symbolKey = Symbol("シンボルは一意な値");
object[symbolKey] = "value of symbol";
console.log(object[symbolKey]); // => "value of symbol"

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

const key = "key-string";
// Computed Propertyでプロパティを定義する
const object = {
    [key]: "value"
};
console.log(object[key]); // => "value"

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

function doSomething(object) {
    object.key = "value";
    // 色々な処理...
}
const object = {};
doSomething(object); // objectが変更されている
console.log(object.key); // => "value"

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

[コラム] constしたオブジェクトが変更可能

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

const object = { key: "value" };
object.key = "Hi!"; // constで定義したobjectが変更できる
console.log(object.key); // => "Hi!"

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

const object = { key: "value" };
object = {}; // => SyntaxError

作成したオブジェクトのプロパティの変更を防止するにはObject.freezeメソッドを利用する必要があります。 ただし、strict modeでないと例外が発生せず、無言で変更を無視するだけとなります。 そのため、Object.freezeメソッドを利用する場合は必ずstrict modeと合わせて使います。

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

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

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

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

このように、JavaScriptでは存在しないプロパティへアクセスした場合に例外が発生しません。 そのため、プロパティ名を間違えた場合にundefinedが返るため、気づきにくいという問題があります。 オブジェクトはネストできるため、次のようなプロパティ名を途中で間違えていた場合にも気づきにくいという問題が起きやすいです。

const widget = {
    window: {
        title: "ウィジェットのタイトル"
    }
};
// `window`を`windw`と間違えている
console.log(widget.windw); // => undefined
// `undefined.title`と書いたのと同じなので、この時初めて例外が投げられる
// "widget.windw is undefined"などの例外が発生する
console.log(widget.windw.title); // => TypeError
// 例外が発生した文以降は実行されない

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

undefinedとの比較

存在しないプロパティへアクセスした場合に、undefinedを返すため実際にアクセスして比較することでも判定できます。

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

しかし、この方法はプロパティの値がundefinedであった場合に、プロパティがないからundefinedなのかが区別できないという問題があります。 次のような例は、keyプロパティは存在していますが、値がundefinedであるため、存在の判定が上手くできていないことがわかります。

const object = { key: undefined };
// `key`プロパティの値が`undefined`
if (object.key !== undefined) {
    // 実行されない文
}

in演算子を使う

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

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

次のように、objectkeyプロパティが存在するなら、trueを返します。

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

しかし、in演算子は、for...in文と同じく、対象となるオブジェクトのプロパティを列挙する場合、親オブジェクトまで探索し列挙します。 そのため、object自身が持っていなくても、親オブジェクトが持っているならばtrueを返してしまいます。

object自身がそのプロパティを持っているかを判定するには、Object#hasOwnPropertyメソッドを使うのが確実です。

Object#hasOwnPropertyメソッド

Object#hasOwnPropertyメソッドを使うことで、オブジェクト自身が指定したプロパティを持っているかを判定できます。

オブジェクト.hasOwnProperty("プロパティ名"); // true or false

hasOwnPropertyメソッドは引数に存在を判定したいプロパティ名を渡し、該当するプロパティを持っている場合はtrueを返します。

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

Object#toStringメソッド

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

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

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

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

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

Object以外のArrayNumberなどもそれぞれ独自のtoStringメソッドを定義しています。 そのため、それぞれのオブジェクトでtoStringメソッドの結果は異なります。

const number = [1, 2, 3];
// Array#toStringが定義されているため、`Object#toString`とは異なる形式となる
console.log(number.toString()); // => "1,2,3";

Objectはすべての元

ここまでは、Object自身の機能について見てきましたが、 Objectには、他のArrayStringFunctionといった他のオブジェクトとは異なる特徴があります。

すべてのオブジェクトはObjectprototypeオブジェクトを継承しています。 prototypeオブジェクトはすべてのオブジェクトに備わっている特別なオブジェクトです。 そのため、Objectはすべてのオブジェクトが共通して利用できるプロパティやメソッドを提供するベースのオブジェクトともいえます。

他のオブジェクトは`Object`の`prototype`を継承している

具体的にどういうことかを見てみます。 先ほども登場した、Object#hasOwnPropertyメソッドは、ObjectprototypeオブジェクトにhasOwnPropertyメソッドの定義があります。

// `Object`の`prototype`オブジェクトに`hasOwnProperty`メソッドの定義がある
console.log(typeof Object.prototype.hasOwnProperty); // => "function"

このObject.prototype.hasOwnPropertyメソッドの定義は、 Objectprototypeオブジェクトがデフォルトで持っているため、あまり意識する必要はありません。

// このような定義が自動的に行われているイメージ
// `Object`の`prototype`オブジェクトに`hasOwnProperty`メソッドの定義を行う
Object.prototype.hasOwnProperty = (propertyName) => {
    // hasOwnPropertyの処理
};

Objectのインスタンスは、このprototypeオブジェクトに定義されたメソッドやプロパティをインスタンス化時に継承します。 つまり、オブジェクトリテラルやnew Objectでインスタンス化したオブジェクトは、Object.prototypeに定義されたものが利用できるということです。

// var object = new Object()も同じ
const object = {};
// インスタンスがprototypeオブジェクトに定義されたものを継承する
console.log(object.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

そのため、Object.prototypeに定義されているtoStringメソッドやhasOwnPropertyメソッドが、 Objectのインスタンスで利用できます。

Objectのインスタンス -> Object.prototype

in演算子とObject#hasOwnPropertyメソッドの違い

先ほど学んだin演算子とObject#hasOwnPropertyメソッドの違いからもここから生じています。

hasOwnPropertyメソッドは、そのオブジェクト自身が指定したプロパティを持っているかを判定します。 一方、in演算子はオブジェクト自身が持っていなければ、そのオブジェクトの親オブジェクトまで順番に探索して持っているかを判定します。

const object = {};
// `object`のインスタンス自体に`toString`メソッドが定義されているわけではない
console.log(object.hasOwnProperty("toString")); // => false
// `in`演算子は指定されたプロパティ名が見つかるまで親を辿るため、`Object.prototype`まで見に行く
console.log("toString" in object); // => true

これによりObjectのインスタンス自身がtoStringメソッドを持っているわけではなく、Object.prototypetoStringメソッドを持っていることが分かります。

オブジェクトの継承元を明示するObject.createメソッド

Object.createメソッドを使うと、第一引数に指定したprototypeオブジェクトを継承した新しいオブジェクトを作成できます。

先ほど、オブジェクトリテラルはObject.prototypeオブジェクトを自動的に継承したオブジェクトを作成していることがわかりました。 オブジェクトリテラルで作成する新しいオブジェクトは、Object.createメソッドを使うことで次のように書くことができます。

// var object = {} と同じ
const object = Object.create(Object.prototype);
// `object`は`Object.prototype`を継承している
console.log(object.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

ArrayもObjectを継承している

ObjectObject.prototypeの関係と同じく、ArrayコンストラクタもArray.prototypeを持っています。 そのため、ArrayコンストラクタのインスタンスはArray.prototypeを継承します。 さらに、Array.prototypeObject.prototypeを継承しているため、ArrayのインスタンスはObject.prototypeも継承しているのです。

Arrayのインスタンス -> Array.prototype -> Object.prototype

Object.createメソッドを使ってArrayObjectの関係をコードとして表現してみます。 Arrayコンストラクタの実装などは実際のものとは異なるので、あくまで関係の例示でしかないことに注意してください。

// `Array`コンストラクタ自身は関数でもある
const Array = function() {};
// `Array.prototype`は`Object.prototype`を継承している
Array.prototype = Object.create(Object.prototype);
// `Array`のインスタンスは、`Array.prototype`を継承している
const array = Object.create(Array.prototype);
// `array`は`Object.prototype`を継承している
console.log(array.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

このように、ArrayのインスタンスもObject.prototypeを継承しているため、 Object.prototypeに定義されているメソッドを利用できます。

// var array = new Array(); と同じ
var array = [];
// `Array`のインスタンス -> `Array.prototype` -> `Object.prototype`
console.log(array.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

この継承の仕組みは、prototype継承と呼ばれるJavaScriptのコアとなる概念です。 詳しくは、第n章の"関数"で詳しく解説します。

  • [ ] TODO: 関数の章を書いたら変更する

ここでは、Objectはすべてのオブジェクトの親となるオブジェクトであることだけを覚えておくだけで問題ありません。 これにより、ArrayStringなどのインスタンスもObject.prototypeがもつメソッドを利用できる点を覚えておきましょう。

[コラム] Object.prototypeを継承しないオブジェクト

Objectはすべてのオブジェクトの親となるオブジェクトである言いましたが、例外もあります。

イディオムに近いのですが、Object.create(null)とすることでObject.prototypeを継承しないオブジェクトを作ることができます。 これにより、プロパティやメソッドをなどを全く持たない本当に空のオブジェクトを作ることができます。

// 親がnull、つまり親がいないオブジェクトを作る
const object = Object.create(null);
// Object.prototypeを継承しないため、hasOwnPropertyが存在しない
console.log(object.hasOwnProperty); // => undefined

Object.createメソッドはES5から導入され、Object.create(null)というイディオムは、一部ライブラリなどでMap(連想配列とも言われる)の代わりとして利用されています。 Mapはあらゆる文字列をキー名にできますが、ObjectのインスタンスはデフォルトでObject.protptypeにあるものがキーとして存在してしまうためです。

// ただのオブジェクト
const object = {};
// "toString"という値を定義してないのに、"toString"が存在している
console.log(object["toString"]);// Function 
// Mapのようなオブジェクト
const mapLike = Object.create(null);
// toStringキーは存在しない
console.log(mapLike["toString"]); // => undefined

しかし、ES2015からは、本物のMapが利用できるため、Object.create(null)Mapの代わりに利用する必要はありません。

const map = new Map();
// toStringキーは存在しない
console.log(map.has("toString")); // => false

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

最後にObjectの静的メソッドについて見ていきましょう。

オブジェクトの列挙

オブジェクトはプロパティの集合です。 そのオブジェクトのプロパティを列挙する方法として、Object.keysメソッド、Object.valuesメソッド、Object.entriesメソッドがあります。 これらのメソッドは、そのオブジェクト自身がもつ列挙可能なプロパティだけを扱います。(「ループと反復処理」を参照)

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

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

オブジェクトのコピー/マージ

Object.assignを使うことで、あるオブジェクトを別のオブジェクトに代入(assign)できます。 これを使うことでオブジェクトのコピーやオブジェクト同士のマージを行うできます。

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

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では、基本的な処理は左から順番に行います。 そのため左から順にオブジェクトが代入されていくと考えるとよいです。

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

オブジェクトの複製

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

// `object`を浅く複製したオブジェクトを返す
const shallowClone = (object) => {
    return Object.assign({}, object);
};
const object = { a: "a" };
const cloneObject = shallowClone(object);
console.log(cloneObject); // => { a: "a" }
console.log(object === cloneObject); // => false

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

const shallowClone = (object) => {
    return Object.assign({}, object);
};
const object = { 
    level: 1,
    nest: {
        level: 2
    },
};
const cloneObject = shallowClone(object);
// `nest`オブジェクトは複製されていない
console.log(cloneObject.nest === object.nest); // => true

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

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

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

一方、JavaScriptという言語はコアにある機能が最低限であるため、ユーザーが作成した小さな機能をもつライブラリが数多く公開されています。 それらのライブラリはnpmと呼ばれるJavaScriptのパッケージ管理ツールで公開され、JavaScriptのエコシステムを築いています。

results matching ""

    No results matching ""