チャプター データ型 で学んだ通り、JavaScriptには7つの型があります。そのうち6つは “プリミティブ” と呼ばれています。なぜなら、それらは1つの値だけを持つからです(文字列、数値、など任意の値になります)。

一方で、オブジェクトは様々なデータと、より複雑なエンティティのキー付けされた集合を保持するために使われます。JavaScriptでは、オブジェクトは言語のほぼすべての面で関連します。そのため、まず最初にオブジェクトを理解する必要があります。

オブジェクトは波括弧 {…}と任意の プロパティ の一覧から成ります。プロパティは “key:value” のペアで、key は文字列(もしくは"プロパティ名"と呼ばれます)で、value は何でも構いません。

オブジェクトは、署名されたファイルを持つキャビネットとしてイメージすることができます。すべてのデータは、キーによってそのファイルの中に格納されます。ファイルを名前で検索したり、ファイルの追加や削除は簡単です。

空のオブジェクト(“空のキャビネット”)は、次の2つ構文のいずれかで作ることができます:

let user = new Object(); // "オブジェクトコンストラクタ" 構文
let user = {};  // "オブジェクトリテラル" 構文

通常は波括弧 {...} が使われます。その宣言は オブジェクトリテラル と呼ばれます。

リテラルとプロパティ

プロパティを “key: value” のペアの形式で {...} に置くことができます。

let user = {     // オブジェクト
  name: "John",  // キー "name" に値 "John" が格納される
  age: 30        // キー "age" に値 30 が格納される
};

プロパティはコロン ":" の前がキー(“名前” もしくは “識別子” として知られています)で、コロンの右が値です。

ここでは、user オブジェクトは2つのプロパティを持っています:

  1. 最初のプロパティは名前 "name" とその値 "John" です。
  2. 2つ目は、名前 "age" とその値 30 です。

生成された user オブジェクトは、“name” と “age” とラベル付けされた2つのファイルを持つキャビネットと考えることができます。

私たちは、いつでもそこからファイルの追加、削除、参照ができます。

プロパティ値へは、ドット表記を使ってアクセスすることができます。:

// オブジェクトのフィールドを取得:
alert( user.name ); // John
alert( user.age ); // 30

値はどんな型にもなります。boolean の値を追加してみましょう:

user.isAdmin = true;

プロパティを削除するには、delete 演算子を使います:

delete user.age;

また、複数単語からなるプロパティ名を使うこともできます。この場合は引用符で囲む必要があります:

let user = {
  name: "John",
  age: 30,
  "likes birds": true  // 複数語のプロパティ名は引用符で囲まなければなりません
};
末尾のカンマ

このリストの最後のプロパティはカンマで終わることがあります:

let user = {
  name: "John",
  age: 30,
}

これは、「末尾」または「ぶら下がり」のカンマと呼ばれます。 これがあると、すべての行が同じ表記になるため、プロパティの追加/削除/移動が簡単になります。

角括弧

複数語のプロパティの場合、ドットを使用したアクセスは上手く動作しません:

// これは構文エラーになります
user.likes birds = true

これは、ドットはキーが有効な変数識別子であることが必要なためです。

代わりに、任意の文字列で動作する “角括弧表記” を使います:

let user = {};

// set
user["likes birds"] = true;

// get
alert(user["likes birds"]); // true

// delete
delete user["likes birds"];

これですべて問題ありません。括弧内の文字列は適切に引用符がつけられている(どのようなタイプの引用符でも動作します)ことに留意してください。

角括弧は、任意の式の結果としてプロパティ名を取得する方法も提供します – リテラル文字列とは逆に – 次のようになります:

let key = "likes birds";

// user["likes birds"] = true; と同じ
user[key] = true;

ここで、変数 key は実行時に計算されるかもしれないし、ユーザの入力に依存するかもしれません。そして、プロパティにアクセスするときにそれを使います。これは素晴らしい柔軟性をもたらします。ドッド表記の場合、同じようにはできません。

例:

let user = {
  name: "John",
  age: 30
};

let key = prompt("What do you want to know about the user?", "name");

// 変数でアクセス
alert( user[key] ); // John ("name" が入力された場合)

算出プロパティ

オブジェクトリテラルでは、角括弧を使うことができます。それは 算出プロパティ と呼ばれます。

例:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // プロパティ名は変数 fruit から取られます
};

alert( bag.apple ); // 5 fruit="apple" の場合

算出プロパティの意味はシンプルです: [fruit] は、プロパティ名は変数 fruit の値になることを意味します。

なので、もし訪問者が "apple" を入力すると, bag{apple: 5} になります。

基本的には、次と同じように動作します。

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// fruit 変数からプロパティ名を取る
bag[fruit] = 5;

角括弧の中では、より複雑な式を使うこともできます。

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

角括弧はドット表記よりもはるかに強力です。それらは任意のプロパティ名や変数を許容します。しかし、書くのはドットより面倒です。

そのため、プロパティ名を知っていて単純な場合であれば、ドットが使われます。そして、もしより複雑な何かが必要なとき、角括弧に切り替えます。

予約語はプロパティ名として使用可能です

変数は “for”, “let”, “return” といった、予約語と同じものをもつことはできません。

しかし、オブジェクトプロパティではこのような制限はありません。どんな名前でも大丈夫です:

let obj = {
  for: 1,
  let: 2,
  return: 3
}

alert( obj.for + obj.let + obj.return );  // 6

基本的に、任意の名前が使用可能ですが、特別なものが1つあります: "__proto__" は歴史的な理由から特別な扱いを受けています。例えば、非オブジェクトにそれをセットすることはできません。:

let obj = {};
obj.__proto__ = 5;
alert(obj.__proto__); // [object Object], 期待通りには動作しません。

上のコードから分かるように、プリミティブ 5 への代入は無視されます。

もし、オブジェクトに任意の key-value のペアを保持し、訪問者がキーを指定できるようにしたとき、それはバグや脆弱性の元になる可能性があります。

そのケースで訪問者は、キーとして “__proto__” を選ぶかもしれません。すると代入のロジックは崩壊するでしょう(上にあるように)。

オブジェクトが、__proto__ を通常のプロパティとして扱うようにするための方法があります。それは後で説明しますが、まずはそれを理解するためにオブジェクトについてより知る必要があります。 記事 "map-set-weakmap-weakset" が見つかりません と呼ばれる別のデータ構造があり、チャプター 記事 "map-set-weakmap-weakset" が見つかりません で学びます。それもまた任意のキーをサポートします。

プロパティの短縮構文

実際のコードでは、既存の変数をプロパティ名の値として使用することがよくあります。

例えば:

function makeUser(name, age) {
  return {
    name: name,
    age: age
    // ...他のプロパティ
  };
}

let user = makeUser("John", 30);
alert(user.name); // John

上の例では、プロパティは変数と同じ名前を持っています。変数からプロパティを作るユースケースでは、非常に一般的です。そして、それを短く書くための特別な プロパティの短縮構文 があります。

name:name の代わりに、このように単に name と書くことができます:

function makeUser(name, age) {
  return {
    name, // name: name と同じ
    age   // age: age と同じ
    // ...
  };
}

同じオブジェクトで、通常のプロパティと短縮構文両方を使うこともできます:

let user = {
  name,  // name:name と同じ
  age: 30
};

存在チェック

注目すべきオブジェクトの機能は、どんなプロパティへもアクセスできることです。プロパティが存在しない場合でもエラーにはなりません! 存在しないプロパティへのアクセスは、単に undefined を返します。これはプロパティが存在するかどうかを確認する非常に一般的な方法です – その値を取得し、 undefined と比較します。:

let user = {};

alert( user.noSuchProperty === undefined ); // true は "そのようなプロパティはありません" を意味する

プロパティの存在チェックのための特別な演算子 "in" もあります。

構文は次の通りです:

"key" in object

例:

let user = { name: "John", age: 30 };

alert( "age" in user ); // true, user.age は存在する
alert( "blabla" in user ); // false, user.blabla は存在しない

in の左側は プロパティ名 である必要があることに注意してください。通常それは引用符で囲まれた文字列です。

もし引用符を除いた場合、テストされる実際のプロパティ名を持つ変数であることを意味します。例えば:

let user = { age: 30 };

let key = "age";
alert( key in user ); // true, キーから名前を取り、そのプロパティをチェック
undefined を格納しているプロパティに “in” を使う

通常、厳密等価演算子 "=== undefined" チェックは正しく動作します。しかし、それが失敗する特別なケースがあります。 "in" は正しく動作します。

それは、オブジェクトのプロパティは存在するが、undefined が格納されているときです:

let obj = {
  test: undefined
};

alert( obj.test ); // これは undefined, なので - このようなプロパティはなし?

alert( "test" in obj ); // true, プロパティは存在します!

上のコードでは、プロパティ obj.test は技術的には存在します。なので、 in 演算子は正しく動いています。

このようなシチュエーションは非常にまれです。なぜなら undefined は通常代入されないからです。殆どの場合、“不明” または “空” の値として null を使います。

“for…in” ループ

オブジェクトのすべてのキーを見て回るための、ループの特別な形があります: for..in です。これは以前学んだ for(;;) 構造と完全に異なるものです。

構文:

for(key in object) {
  // オブジェクトプロパティの各キーに対して本体を実行
}

例えば、user のすべてのプロパティを出力してみましょう:

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for(let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // values for the keys
  alert( user[key] ); // John, 30, true
}

すべての “for” 構造は、ここでの let key のように、ループ内でループする変数を宣言することに留意してください。

また、ここでの key の代わりに、別の変数名を使うこともできます。例えば、"for(let prop in obj)" もまた広く使われています。

オブジェクトの順序付け

オブジェクトは順序付けられますか?つまり、オブジェクトをループするとき、追加したのと同じ順序ですべてのプロパティを取得しますか?それを保証することはできるでしょうか?

回答は、“特別な方法で順序付けられます”。: 整数値のプロパティはソートされます、それ以外は作成した順になります。以下、その詳細です。

例として、電話のコードをもつオブジェクトを考えてみましょう:

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}

オブジェクトはユーザに対してオプションの一覧を提案をするのに使われるかもしれません。もし主にドイツのユーザをターゲットにしたサイトを作る場合、恐らく最初に 49 が出て欲しいです。

しかし、コードを実行すると完全に異なったものが見えます:

  • USA (1) が最初に来ます
  • 次に Switzerland (41) などが並びます.

電話コードは昇順にソートされて表示されます。なぜなら、それらが整数だからです。なので、1, 41, 44, 49 と見えます。

整数プロパティとは?

ここで “整数プロパティ” という用語は、変更なしで整数に変換できる文字列を意味します。

したがって、“49” は整数のプロパティ名です。なぜなら、整数の数に変換されて戻っても、それは変わらないからです。 しかし、 "+49"と "1.2"はそうではありません:

// Math.trunc は小数部を取り除く組み込み関数
alert( String(Math.trunc(Number("49"))) ); // "49", 同じ, 整数プロパティ
alert( String(Math.trunc(Number("+49"))) ); // "49", 同じではない ⇒ 非整数プロパティ
alert( String(Math.trunc(Number("1.2"))) ); // "1", 同じではない ⇒ 非整数プロパティ

…一方、もしキーが整数でない場合、作られた順でリストされます。例です:

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // 1つ追加

// 非整数プロパティは作成順にリストされます
for (let prop in user) {
  alert( prop ); // name, surname, age
}

従って、電話コードの問題を直すためには、整数でないコードを作ることで対応することができます。 各コードの前にプラス "+" 記号をつければ十分です。

このように:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for(let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

これで意図した通りに動作します。

参照をコピーする

オブジェクトとプリミティブの基本的な違いの1つは、オブジェクトは “参照によって” 格納、コピーされることです。

プリミティブな値: 文字列、数値、真偽値 – は “値として” 代入/コピーされます。

例:

let message = "Hello!";
let phrase = message;

上記は、結果として2つの独立した変数をもち、それぞれ文字列 "Hello!" を保持しています。

オブジェクトはそうではありません。

変数はオブジェクト自体ではなく、その “メモリ内のアドレス”、つまりそれへの “参照” を格納します。

オブジェクトの絵はこうなります:

let user = {
  name: "John"
};

ここで、オブジェクトはメモリ上のどこかへ格納されています。そして変数 user はその “参照” を持っています。

オブジェクト変数がコピーされるとき、 – 参照がコピーされます、オブジェクトは複製されません。

もしオブジェクトをキャビネットとしてイメージするとしたら、変数はそれへの鍵です。変数のコピーは鍵の複製であり、キャビネット自身ではありません。

例:

let user = { name: "John" };

let admin = user; // 参照のコピー

今、私たちは2つの変数を持っており、それぞれ同じオブジェクトへの参照を持っています。:

私たちは任意の変数を使って、キャビネットにアクセスしその中身を変更することができます:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // "admin" 参照によって変更される

alert(user.name); // 'Pete', 変更は "user" 参照から見えます

上の例は1つのオブジェクトしかないことのデモです。私たちがキャビネットと2つの鍵を持っていて、それらの1つ(admin)を使ってその中のものを取得し変更します。その後、別の鍵(user)を使ったときには、先ほど変更した結果が見えます。

参照による比較

オブジェクトの等価 == と厳密等価 === 演算子は、全く同じように動作します。

2つのオブジェクトは、同じオブジェクトのときだけ等しくなります。

例えば、2つの変数が同じオブジェクトを参照しているとき、それらは等しいです:

let a = {};
let b = a; // 参照のコピー

alert( a == b ); // true, 両方の変数は同じオブジェクトを参照しています
alert( a === b ); // true

また、2つの独立したオブジェクトは等しくありません。たとえそれらが空だとしても:

let a = {};
let b = {}; // 2つの独立したオブジェクト

alert( a == b ); // false

obj1 > obj2 のような比較、もしくは反対にプリミティブ obj == 5 のような比較では、オブジェクトはプリミティブに変換されます。私たちはオブジェクト変換がどのように動作するのか、この後すぐに学ぶでしょう。ただし、真実を言うと、このような比較はほとんど必要とされず、通常はコードの誤りです。

Const オブジェクト

const として宣言されたオブジェクトは変更 できます

例:

const user = {
  name: "John"
};

user.age = 25; // (*)

alert(user.age); // 25

(*) はエラーを起こすように見えるかもしれませんが、それらは全く問題ありません。constuser 自身の値を固定するからです。そしてここで user は常に同じオブジェクトへの参照を保持します。行 (*) はオブジェクトの 内側 へ行っており、user の再代入ではありません。

const は、もし user に他の何かをセットしようとしたときにエラーになります。例えば:

const user = {
  name: "John"
};

// エラー (user の再代入ができない)
user = {
  name: "Pete"
};

…そうすると、もしオブジェクトのプロパティを定数にしたい場合はどうすればいいでしょう? user.age = 25 がエラーになるように。そうすることも可能です。 これについては、チャプター プロパティフラグとディスクリプタ で説明します。

クローンとマージ, Object.assign

これまでの通り、オブジェクト変数のコピーは、同じオブジェクトへの参照をもう1つ作ります。

しかし、もしオブジェクトの複製が必要な場合はどうしましょう?独立したコピー、クローンを作るには?

それは可能ですが、JavaScriptには組み込みのメソッドがないため多少難しいです。実際、それはめったに必要ありません。参照によるコピーはたいていの場合で問題ありません。

しかし、もし本当にそうしたい場合は、新しいオブジェクトを作り、プリミティブなレベルでそのプロパティを繰り返しコピーしていくことで、既存のものの構造を複製する必要があります。

このようになります:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新しいからオブジェクト

// すべての user プロパティをその中にコピーしましょう
for (let key in user) {
  clone[key] = user[key];
}

// 今、clone は完全に独立したクローンです
clone.name = "Pete"; // その中のデータを変更

alert( user.name ); // 依然としてオリジナルのオブジェクトは John

また、そのために、Object.assign 関数を使うことができます。

構文はこうです:

Object.assign(dest[, src1, src2, src3...])
  • 引数 dest, そして src1, ..., srcN (必要なだけ) はオブジェクトです。
  • すべてのオブジェクト src1, ..., srcN のプロパティを dest にコピーします。言い換えると、2つ目から始まる全ての引数のプロパティは、最初の引数のオブジェクトにコピーされます。そして dest を返します。

例えば、いくつかのオブジェクトを1つにマージするために使います:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// permissions1 and permissions2 のすべてのプロパティを user にコピー
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

もし、受け取ったオブジェクト (user) が既に同じプロパティ名のものをもっていたら、上書きします:

let user = { name: "John" };

// name を上書き, isAdmin を追加
Object.assign(user, { name: "Pete", isAdmin: true });

// now user = { name: "Pete", isAdmin: true }

また、単純なクローンをする場合のループ処理を置き換えるために、Object.assign を使うこともできます。

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

これは user のすべてのプロパティを空のオブジェクトにコピーし、返します。ループの場合と同じことをしますが、より短い記載になります。

今まで、user のすべてのプロパティがプリミティブであると仮定していましたが、プロパティは他のオブジェクトの参照になることもあります。それらはどうなるでしょう?

このような:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

今、user.sizes はオブジェクトであり、参照によるコピーがされるため、clone.sizes = user.sizes というコピーでは不十分です。なので、cloneuser は同じ sizes を共有します:

このようになります:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, 同じオブジェクト

// user と clone は sizes を共有します
user.sizes.width++;       // 一方からプロパティを変更します
alert(clone.sizes.width); // 51, 他方から変更した結果が見えます

これを修正するには、user[key] の各値を調べ、それがオブジェクトの場合はその構造も複製するクローンのループを使用する必要があります。 これは “ディープクローン(ディープコピー)” と呼ばれます。

上記のケースやより複雑なケースを処理するディープクローン作成のための標準的なアルゴリズムがあります、それはStructured cloning algorithm と呼ばれています。 車輪の再発明をしないために、JavaScript ライブラリlodash にある処理を利用することができます。そのメソッドは _.cloneDeep(obj) と呼ばれています。

サマリ

オブジェクトはいくつかの特別な機能を持つ連想配列です。

それらはプロパティ(key-valueペア)を格納します:

  • プロパティのキーは文字列またはシンボル(通常は文字列)です。
  • 値は任意の型になります。

プロパティにアクセスするには、次のようにします:

  • ドット表記: obj.property
  • 角括弧表記: obj["property"]。角括弧は変数からキーを取ることもできます。obj[varWithKey] のように。

追加の演算子:

  • プロパティを削除: delete obj.prop
  • 与えられたキーを持つプロパティの存在チェック: "key" in obj
  • オブジェクトのイテレート: for(let key in obj) ループ

オブジェクトは、参照によって代入やコピーがされます。つまり、変数は “オブジェクトの値” ではなく、 値への “参照” (メモリ上のアドレス)を格納します。従って、このような変数をコピーしたり、それを関数の引数として渡すと、オブジェクトではなく参照がコピーされます。 コピーされた参照(プロパティの追加/削除など)によるすべての操作は、同じ単一のオブジェクトに対して実行されます。

“本当のコピー” (クローン) をするためには、Object.assign または _.cloneDeep(obj) を使います。

このチャプターで学んだのは、“普通のオブジェクト”、あるいは単に “オブジェクト” と呼ばれています。

JavaScriptには他にも多くの種類のオブジェクトがあります:

  • 順序付けされたデータコレクションを格納する Array
  • 日付と時刻に関する情報を格納する Date
  • エラーに関する情報を格納する Error
  • …等々

後で勉強しますが、それらは特別な機能を持っています。また、それらは “Array型” もしくは “Data型” と言われることがありますが、形式的には自身の型ではなく、単一の「オブジェクト」データ型に属しています。 そして、それをさまざまな方法で拡張しています。

Javascript のオブジェクトはとても強力です。ここでは本当に巨大なトピックのほんの始まりを学びました。この後に続くチャプターでは、オブジェクトをより深く見ていき、それらについてもっと学んでいきます。

タスク

重要性: 5

アクションごとに1行ずつコードを記述してください。:

  1. 空のオブジェクト user を作ります。
  2. John という値を持つ name プロパティを追加します。
  3. Smith という値を持つ surname プロパティを追加します。
  4. name の値を Pete に変えます。
  5. オブジェクトから name プロパティを削除します。
let user = {};
user.name = "John";
user.surname = "Smith";
user.name = "Pete";
delete user.name;
重要性: 5

オブジェクトがプロパティを持っていない場合に true を、それ以外の場合には false を返す関数 isEmpty(obj) を書きなさい。

このように動く必要があります:

let schedule = {};

alert( isEmpty(schedule) ); // true

schedule["8:30"] = "get up";

alert( isEmpty(schedule) ); // false

テストと一緒にサンドボックスを開く

単にオブジェクトをループし、少なくとも1つプロパティがある場合にはすぐに return false を返します。

function isEmpty(obj) {
  for (let key in obj) {
    return false;
  }
  return true;
}

サンドボックスでテストと一緒に解答を開く

重要性: 5

const で宣言されたオブジェクトを変更することは可能でしょうか?どう思いますか?

const user = {
  name: "John"
};

// 動作する?
user.name = "Pete";

もちろん動きます。問題ありません。

const は変数自身のみ変更から保護します。

つまり、user オブジェクトへの参照を格納しています。そしてそれは変更できません。しかし、オブジェクト中身は可能です。

const user = {
  name: "John"
};

// works
user.name = "Pete";

// error
user = 123;
重要性: 5

我々のチームの給与を格納しているオブジェクトがあります。:

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
}

すべての給与を合計し変数 sum に格納するコードを書いてください。上の例では 390 になるはずです。

もし salaries が空の場合、結果は 0 である必要があります。

let salaries = {
  John: 100,
  Ann: 160,
  Pete: 130
};

let sum = 0;
for (let key in salaries) {
  sum += salaries[key];
}

alert(sum); // 390
重要性: 3

obj のすべての数値プロパティに 2 を掛ける関数 multiplyNumeric(obj) を作成しなさい。

例:

// 呼び出し前
let menu = {
  width: 200,
  height: 300,
  title: "My menu"
};

multiplyNumeric(menu);

// 呼び出し後
menu = {
  width: 400,
  height: 600,
  title: "My menu"
};

multiplyNumeric は何も返却する必要がないことに注意してください。オブジェクトをその場で変更する必要があります。

P.S. ここでは数値のためのチェックに typeof を使います。

テストと一緒にサンドボックスを開く

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。