本記事ではJavaScriptのオブジェクトを中心に取り上げ、
JavaScriptではオブジェクトの一種である配列についても取り上げます。
JavaScriptにおけるオブジェクトは、
Rubyなどの他のオブジェクト指向言語とは全体的に異なる部分が多く、
突き詰めると結構複雑かと思います。
そこで、JavaScriptにおけるオブジェクトについて
触れていこうと思います。
オブジェクト
オブジェクトとはなにか。
オブジェクト指向プログラミングとかの文脈でいうと、
モノ、値みたいに語られることもあると思います。
ただ、JavaScriptのオブジェクトは、
それとは少し毛色が変わっていて、
複合型のデータ型で、プロパティの集合と言えると思います。
(プロパティについては後に説明します)
言うなればプリミティブ型でないものが、
オブジェクトと言えるでしょう。
その点、Rubyとかに代表されるオブジェクト指向プログラミングの、
オブジェクトとはやや定義が異なるかと思います(Rubyとかは、
すべてがオブジェクトといえると思うので)
まあ、ぐだぐだ説明するより、
具体例を出すと
let object1 = {};
let object2 = { x: 1, y: 2 }
みたいな{}
のデータ構造が、
もっとも著名なオブジェクトといえるでしょう。
ちなみに、{}
のことをオブジェクトリテラルと言ったりもします。
上記のように、プロパティは、プロパティ名(キー)とプロパティの値(バリュー)を
: (コロン)で区切って定義し、プロパティ間は、, (カンマ)で区切ります。
ちなみに、new Object()
やObject.create()
のようにオブジェクトを生成することもできます。
オブジェクトの値へのアクセス方法としては、
obj.value
obj["value"]
といった、配列表記とドット識別子を使う方法の2通りあります。
配列表記のほうが、例えばobj[answer]
みたいに、answerといった変数名を渡して、
動的に値を取得したりすることもできます。
(プロパティ名がループなどで動的に変化するときなどはこちらの記述が便利だったりします)
ちなみに、関数をオブジェクトとともに使うと、
メソッドと呼びます。
let a = []
a.push(1,2,4) //まさにpushメソッド
上記は配列ですが、
pushメソッドはArrayプロトタイプのメソッドです。
プロパティとは何か
さて、オブジェクトの概要はさらいましたが、
改めて少し深堀りしようと思います。
そこで、プロパティとは何かというと名前と値を持つもので、
JavaScript Primerでは下記のように説明されていました。
https://jsprimer.net/basic/object
プロパティとは名前(キー)と値(バリュー)が対になったものです。 プロパティのキーには文字列または
JavaScript PrimerよりSymbol
が利用でき、値には任意のデータを指定できます。 また、1つのオブジェクトは複数のプロパティを持てるため、1つのオブジェクトで多種多様な値を表現できます。
要するにオブジェクトのなかにあるキーとバリュー2つを合わせたもので、
文脈によってプロパティ名(キー)とか、プロパティの値(バリュー)って言ったりするかと思います。
このプロパティという用語ですが、
実際にプログラムを書くうえでは知らなくても
そこまで問題ないのですが、
本とかドキュメントを読む際に結構プロパティという用語を使って、
オブジェクトを説明していることが多いので、
知っておくと便利かもしれません。
そもそもJavaScriptは、もともとプロトタイプベースということもあり、
オブジェクトに自分で定義したプロパティ以外に、
組み込みで定義されているプロパティとかもあったりします。
なので、組み込みではなくて、自分で定義したプロパティを、
独自プロパティといったりします。
また、独自プロパティと組み込みのプロパティでは、
諸々挙動が異なり、その違いを下記の属性で表すと
わかりやすいと思います。
- 書き込み可能 → プロパティに値を設定できる
- 列挙可能 → for/inループなどでプロパティ名を調べることができる
- 再定義可能 → プロパティの削除、属性変更が可能
JavaScriptの組み込みのプロパティは、読み取り専用なので、
列挙や再定義はできないのに対し、
独自プロパティは、デフォルトでは上記すべて可能になっています
(Object.defineProperty()
などを使って、
この属性を変更することもできます。)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
このように、JavaScriptのプロパティはこのように
オブジェクトを理解するうえで重要な概念です。
プロパティの簡略記法
ES6以降では、プロパティ名と値が同じ場合、
省略することができます
この構文は個人的にはちょっと気に入っています。
let x = 1, y = 3
let o = { x: x, y: y}
// ↑こう書いていたものを下記のように省略できる
let z = { x, y }
この記法は、Nuxt.jsとかのuseAsyncDataあたりを使う際に、
地味に使ったりするので、こういう書き方もできることは
知っておくと良いかもしれません。
プロトタイプ
JavaScriptのオブジェクトは、
プロトタイプと大きな関係を持っています。
そもそも、JavaScriptは、
Rubyなどとは異なり、プロトタイプベースの
オブジェクト指向をルーツとしていることは、下記の記事で解説しました。
そのため、classを使うオブジェクト指向とは異なり、
プロトタイプを使ってオブジェクト指向を体現するという特徴があります。
https://developer.mozilla.org/ja/docs/Glossary/Prototype-based_programming
すなわち、プロトタイプとは、JavaScriptのオブジェクトが
他から機能を継承する仕組みのようなものとMDNで解説しているように、
オブジェクトに関連付けられている、もう一つの別のオブジェクトのようなものです。
例えば下記のようなオブジェクトを定義してみます。
const test = {
color: "red",
hello() {
console.log('hello world');
},
}
test.hello() // hello world
testというオブジェクトには、
独自プロパティとして、colorとhelloの2つを持っています。
(ちなみに最初ちらっと触れましたが、このようにオブジェクトに
定義されている、helloのような関数をメソッドといいます)
独自プロパティかどうかは、Object.hasOwn
を使って、
Object.hasOwn(test, 'color')
Object.hasOwn(test, 'hello')
のようにして調べられます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn
これだけ見るとtestというオブジェクトは、
2つのプロパティしか持っていないようにも思えますが、
test.toString()
みたいなメソッドを呼び出せることがわかります。
これこそがプロトタイプの正体で、
オブジェクトは、このようなプロトタイプと呼ばれる組み込みプロパティを持っています。
プロトタイプ自体もオブジェクトなので、さらにそれらのプロトタイプを持つという、
プロトタイプチェーンが成立します。
プロトタイプにnullを持つプロトタイプに到達すると、
プロトタイプチェーンは終わります。
classでのオブジェクト指向的に言うと、
親のようなものから継承しているといったらわかりやすいかもしれません。
(すなわちオブジェクトの親のオブジェクトが持っているプロパティを子も使えるみたいな)
なので、test自体にtoString()というメソッドがなくても、
testのプロトタイプがtoString()を持っているから
呼び出すことができます。
オブジェクトのプロパティをアクセスした際、
探す順番としては、
- 独自プロパティ
- プロトタイプ
- プロトタイプのプロトタイプ
※以下探すまで繰り返してnullに到達したらundefinedを返す
オブジェクトのプロトタイプを調べるには、Object.getPrototypeOf()
を使います
Object.create
先ほどプロトタイプは、継承のようなものと触れたように、
JavaScriptのObject.create
を使えば、
classの継承のようなものを表現できます。
// 親: 機械 (machine)
const machine = {
isPowered: false,
start: function () {
console.log('機械が動き始めました');
},
}
// 子: 車 (car)
const car = Object.create(machine)
car.name = 'Aqua' // 車の名前を設定
car.isPowered = true // 親から継承したプロパティを上書き
car.drive = function () {
console.log(`${this.name} が走り始めました`)
};
// 孫: 電気自動車 (electricCar)
const electricCar = Object.create(car)
electricCar.batteryLevel = 100 // 孫の独自プロパティ
electricCar.charge = function () {
this.batteryLevel = 100;
console.log(`${this.name} のバッテリーが充電されました`)
}
// 出力
console.log('--- 機械 (machine) の動作 ---')
machine.start() // 機械が動き始めました
console.log('\n--- 車 (car) の動作 ---')
car.start() // 親から継承したメソッド
car.drive() // Aqua が走り始めました
console.log('\n--- 電気自動車 (electricCar) の動作 ---')
electricCar.start() // 親から継承したメソッド
electricCar.drive() // Aqua が走り始めました
electricCar.charge() // Aqua のバッテリーが充電されました
// プロトタイプチェーン
console.log('\n--- プロトタイプチェーンの確認 ---')
console.log(Object.getPrototypeOf(electricCar)) // car オブジェクト
console.log(Object.getPrototypeOf(car)) // machine オブジェクト
console.log(Object.getPrototypeOf(machine)) // Object オブジェクト
console.log(Object.getPrototypeOf(Object.getPrototypeOf(machine))) // null
ちょっと長いコードになってしまいましたが、
classのオブジェクト指向と似たようなことが実装できることがわかります。
またObject.getPrototypeOf
でプロトタイプを確認できるので、
上記のコードの場合は親のオブジェクトを参照できるような構造になっているかと思います。
JSON.stringifyとJSON.parse
割と見かけることが多いこれらのメソッド。
JSONを扱う上で、シリアライズと復元に、
nullはサポートされているが、undefinedやNaNとかは
サポートされていなかったりnullに変換されるという特徴があります。
let x = {x:1, y:undefined, z: {x: NaN, y:Infinity, z: -Infinity }}
JSON.stringify(x) // undefinedはサポートされず'{"x":1,"z":{"x":null,"y":null,"z":null}}'が返される
やや細かめではありますが、
頭の片隅に入れておくと役に立つかもしれません。
細かい復元の法則は下記のMDNのドキュメントが詳しいです。
特にundefinedがオブジェクトの中だと省略されたり、
配列の中だとnull に変換されたりする特徴はnullとの違いとして、
留意しておくと良いかもしれません。
in演算子とfor in
指定されたプロパティがオブジェクトに存在するか調べたいとき
in演算子が使えます。
プロパティが存在する場合、 true を返します
in演算子は、独自プロパティだけでなく、
継承プロパティについても、オブジェクトが指定したものを
持つ場合、trueを返すという特徴があります。
そのため、独自プロパティだけを調べたいときは、Object.hasOwn()
を使う方がいいでしょう。
let x = {}
"toString" in x // trueを返す
"x" in x // falseを返す
次にfor in周りに触れていきたいと思います。
for inループは、
継承されたプロパティも含めて、
列挙可のプロパティであれば、それについてループを実行します。
このとき、オブジェクトが継承する組み込みメソッド(toStringとか)は、
列挙可ではないので、for inループでは通常出力されません。
(最初に述べた列挙可という特徴は主にfor inで重要になってくるかと思います)
let x = {x: 'test' }
let y = Object.create(x)
for (let p in y) {
console.log(p) // 継承プロパティのxも出力される
}
y.hasOwnProperty("x") // y自体にはxというプロパティはないのでfalseを返す
配列
さて次は配列についてです。
とはいってもJavaScriptにおいて配列もオブジェクトのひとつなので、
概ねの考え方はオブジェクトと大幅に変わるわけではありません。
JavaScriptの配列のインデックスは0から始まります。
配列は、オブジェクトの特別な形式で、Array.prototype
からプロパティを継承します
配列を作成するには、
配列リテラルで作成できます
let array = []
let array2 = [2, 4, 5]
配列の中にオブジェクトや配列とかをいれることもできます
let array3 = ['test']
let array = [[1,3,5], {x: 1, y: 3}, array3]
ちなみに、存在しないインデックスを指定するとundefindが返されます。
スプレッド演算子を使えば、
反復可能なオブジェクトに対して機能し、
配列を作成できます。
スプレッド演算子とは、...
という構文で
既存オブジェクトを新しいオブジェクトに展開できる機能です。
文字列も反復可能なので、
例えば、A から Zまでのアルファベットの配列を作りたかったら、
[...'ABCDEFGHIJKLMNOPQRSTUVWXYZ']
みたいにすれば、それぞれのアルファベットが格納された配列を作成できます。
また、くどいですがJavaScriptにおいて、
配列はオブジェクトの一種です。
つまり、配列をアクセスすときに使う、
角括弧はオブジェクトのプロパティへのアクセスと同じように処理します。
そのため整数以外をプロパティとすることもできます。
a = []
a['test'] = 12
a[true] = 100
a[12.30] = 20
a.length // 0が返される
しかし、この場合、オブジェクトのプロパティ名として扱われるので、
lengthプロパティは更新されず、0となります。
あくまでオブジェクトとして
これらのプロパティ名が入っている形式になっています。
このlengthプロパティは配列において重要なプロパティで、
配列はこれを監視し、lengthの値を配列と同期させます。
プロパティに、整数を渡したときだけ、
インデックスとして処理し、lengthプロパティを更新するのが、
配列のようなオブジェクトです。
(配列のようなというのは、厳密にはオブジェクトについて、
lengthプロパティとかを持たせて配列のように扱うことも可能であるからです)
a = []
a['test'] = 1
a[32] = 1
a[0] = 1
a.length // 33と返される [1, empty × 31, 1, test: 1]
ちなみに、存在しないプロパティを参照しようとした場合、
undefinedが返されます。
ただ、上記で作成されたemptyというものも、
インデックスアクセスするとundefinedが返されますが、
ループなどで処理する場合はスキップされます。
lengthについて、現在の長さよりも、
小さい値を代入すれば、配列を縮めることもできます。
a = []
a['test'] = 1
a[32] = 1
a[0] = 1
a.length // 33と返される [1, empty × 31, 1, test: 1]
a.length = 10
a.length // 10 [1, empty × 9, test: 1]
要素が0から埋まっていない配列を疎な配列といい、
この場合、要素がなくてもlengthプロパティが要素数を表します。
let a = new Array(10)
a.length // 10
こうした疎な配列を密な配列に変換したいときは、
filterメソッドなどが便利です。
a = []
a['test'] = 1
a[32] = 1
a[0] = 1
a = a.filter(() => true)
a.length // 2
ちなみに、配列はオブジェクトの一種なので、
オブジェクトみたいにa.0
みたいにアクセスできるのかというと
できません。($をつけるといけるみたいです)
数字で始まるプロパティはブラケット記法でないとエラーになります。
なので、配列に定義したa['test']
みたいなやつはドット記法でもアクセスできます