タイトルは適当で、JavascriptのObjectと、それに付随する
Javascriptの特徴的な色々を理解したいのでまとめ。
- (ES5以前の)prototypeによるオブジェクトを理解する
- ES6以降におけるclass構文とprototypeの相互関係
- Objectインスタンスとは何か
new Object()
と{...}
とObject.create()
- オブジェクトとプリミティブな値
- Javascriptにおけるイミュータブルな値
- おまけ
一通り書いてみて思ったのが、この記事の内容は、
「開眼JavaScript」っていうオライリーの本読めば、内容が若干古いものもあるにせよ、
だいたい書いてあることで、その前提知識があれば、ES6関連を流し読みするだけで
理解できる程度のものだと思います。
1. (ES5以前の)prototypeによるオブジェクトを理解する
(ES5以前)JavascriptにはClassは存在しない
- 参考 (Google流 JavaScript におけるクラス定義の実現方法)
- 参考 Qiita JavaScriptのクラス?コンストラクタ??
- 参考 Qiita JavaScriptのプロトタイプからオブジェクト指向を学ぶ
- 参考 Qiita JavaScriptでクラスを実現するための基本
prototypeでJava等のクラスみたいなことをやりたいとき。
Javascriptにはクラスという概念は存在せずに、コンストラクタが存在する。
コンストラクタとは何か、というとJavascriptではnew
を使った関数呼び出しのこと。
1 2 3 4 5 6 7 8 |
function Hoge() { console.dir('hello') } Hoge() // consoleに 'hello' が出力されるだけで戻り値はundefined new Hoge() // consoleに 'hello' が出力されて、空のオブジェクト{}が帰ってくる |
new
を使ってコンストラクタ呼び出しを行うと、内部的には暗黙的に以下のような処理がされる
1 2 3 4 5 6 7 8 |
function Hoge() { console.dir('hello') } //コンストラクタ呼び出しをすると、以下のような処理がされる function Hoge() { var this = {} console.dir('hello') return this } |
なので空のオブジェクトが返ってくる。
参考リンクにもあるように、以下のコードでメソッドを定義することができるが、
よりベターなのはprototype
を利用すること。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function Hoge() { this.f = function() { // newするたびに関数が生成される console.dir('hello') } } var h1 = new Hoge() h1.f() // 'hello' // better function Hoge() {} Hoge.prototype.f = function() { console.dir('hello') } var h2 = new Hoge() h1.f() // 'hello' |
new
を使っても使わなくても同じような挙動をさせたいときは、
明示的にローカル変数をreturnすればOKです。
Qiita JavaScriptのクラス?コンストラクタ??
よりベターな書き方として、下記のような記法も参考リンクで紹介されています。
1 2 3 4 5 6 7 8 9 10 11 12 |
var Hoge = (function() { var h = function() {} var p = h.prototype p.f = function() { console.dir('hello') } return h })() let h1 = new Hoge() |
これに関しては好みなので、好きなほうで書いたらいいと思います。
それじゃ以降にも使えるように、簡単なサンプルを宣言してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<br />function ProgrammingLanguage(name, version) { this.name = name this.version = version } ProgrammingLanguage.prototype.show = function() { return this.name + ':' this.version } ///または下記のようにかける var ProgrammingLanguage = (function() { // constructor var c = function(name, version) { this.name = name this.version = version } var p = c.prototype p.show = function() { return this.name + ":" + this.version } })() let java = new ProgrammingLanguage('java', 1.8) let scala = new ProgrammingLanguage('scala', 2.12) let ruby = new ProgrammingLanguage('ruby', 2.4) |
こんな感じ。
アクセス修飾子に関しては参考リンクに紹介されていますが、本質ではないので
一旦省略します。
所謂Javaのクラスみたいに振る舞っているけど、プロトタイプベースで
あることに注意
継承どうやるのか
定義はわかっても、それを継承できないときつい。
MDN web docs 継承とプロトタイプチェーン
POSTD JavaScriptにおける継承のパターン4種類の概要と対比
Microsoft Developer Network prototypeプロパティ
色々説明や継承を実現するための方法がかかれていますが、抑えて置くべきポイントは以下
prototype
というプロパティとは何か > 自身の親への参照のようなもので、それを元にオブジェクトが生成される
厳密にはもっともっと細かい話が入るんですが、一旦シンプルにするために
説明はこの程度にしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var ProgrammingLanguage = (function() { // constructor var c = function(name, version) { this.name = name this.version = version } var p = c.prototype p.show = function() { return this.name + ":" + this.version } })() console.dir(ProgrammingLanguage.prototype) |
このようにprototypeをコンソールに出してみると、showをメンバに持ったインスタンスをprototype
にもっていることが確認できると思います。
それではshowをメンバに持ったインスタンス
のprototypeは何になっているでしょうか。
掘り下げられるだけ掘り下げてみましょう。
1 2 3 |
console.dir(ProgrammingLanguage.prototype.__proto__) // Object console.dir(ProgrammingLanguage.prototype.__proto__.__proto__) // null |
__proto__
はオブジェクトにが持つprototypeへのgetter/setterになります。(詳しくはMDN見てほしい)
雑な説明だけど関数はprototype
、オブジェクトは__proto__
でprototypeにアクセスできる
と思っておいていただければ。
この結果を見てもらうとわかるように
ProgrammingLanguage > Object(showを定義したやつ) > Object > null
という感じになってます。
で、例えば
1 2 3 4 |
var p = new ProgrammingLanguage('java', 1.8) p.show() // java:1.8 p.toString() |
ここでtoString()
メソッドはObjectで定義されていて、それをprototypeを順番に追っかけて
探します。これが所謂prototypeチェーン。
つまりprototype
に親を入れてあげれば継承が実現できるのでやってみるんだけど、そのための
関数が標準であります。
1 2 |
Object.setPrototypeOf(child, parent); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
const Person = (function() { // constructor let c = function(name, age) { this.name = name this.age = age } let p = c.prototype p.profile = function() { return this.name + ":" + this.age } return c })() const Employee = (function() { let c = function(name, age, salary) { Person.call(this, name, age) this.salary = salary } let p = c.prototype p.profile = function() { return this.prototype.profile.call(this) + this.salary } return c })() //Employee extends Person を宣言 Object.setPrototypeOf(Employee, Person) let per = new Person('fuga', 29) console.dir(per.profile()) //'fuga:29' let emp = new Employee('hoge', 25, 100) console.dir(emp.profile()) // 'hoge:25:100' |
こんな感じ。Employee
がPerson
を継承し、呼び出されているのが確認できます。
これで、ES5以前のプロトタイプによるクラスと同等の機能が実現できるし、プロトタイプチェーン
の概要もつかめました。
(多重継承と、アクセス修飾子は仕様上難しい。後者は命名規則で運用されているケースが多いようです。)
2. ES6以降におけるclass構文とprototypeの相互関係
僕にとっては、既にこっちのほうが馴染みがありますが、ES6でサポートされたclass
構文。
これはJavaのような言語のクラスをJavascriptで可能にしたものではなく、あくまで前述の
プロトタイプベースのシンタックスシュガーであることに注意してください
書き方はJavaみたいな書き方ができます。
これをまず書いてみて、その後、同等のprototypeで書かれたコードに置き換えてみます。
まず最小限のコード
1 2 3 4 |
class Hoge {} console.dir(new Hoge()) // {} |
これでHogeインスタンスが生成されます。
メンバもプロパティも存在しません。
これは下記のコードと同等
1 2 3 4 5 6 7 |
var Hoge = (function() { var c = function() { } var p = c.prototype return c })() |
まずはコンストラクタを定義してみます。
1 2 3 4 5 6 7 8 |
class Hoge { constructor(name) { this.name = name } } console.dir(new Hoge('kkomai')) // {name: "kkomai"} |
これは下記と同等
1 2 3 4 5 6 7 8 9 10 |
var Hoge = (function() { var c = function(name) { this.name = name } var p = c.prototype return c }) console.dir(new Hoge('kkomai')) // {name: "kkomai"} |
次はメソッドを定義してみます。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Hoge { constructor(name) { this.name = name } greet() { return 'Hello, My name is ' + this.name } } let h = new Hoge('kkomai') h.greet() // 'Hello, My name is kkomai' |
これは以下と同等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var Hoge = (function() { var c = function(name) { this.name = name } var p = c.prototype p.greet = function() { return 'Hello, My name is ' + this.name } return c })() let h = new Hoge('kkomai') h.greet() |
こんな感じで、従来のprototypeによる宣言と、互換性があるのが
確認できたと思います。
継承もextends
キーワードで同じようにできます。
static
とかget
とかsuper
とかその他のキーワードも
ありますが、省略します。
(前項の同じ例を何回も出すだけになるので)
MDNのサンプルで紹介されてるMix-inおもしろかった。
3. Objectインスタンスとは何か
デフォルトでインスタンスを生成すると(new
すると)prototype(proto)に
設定されているObjectというオブジェクトが何者なのか、見ていきます。
一旦参考リンクの用語について、書いておきます。
- コンストラクタプロパティ
- Javaでいう静的プロパティ
- コンストラクタメソッド
- Javaでいう静的メソッド
- プロパティ
- Javaでいうメンバ変数
- メソッド
- Javaでいうメンバメソッド
コンストラクタ、という文言が、Java等に馴染んでる自分には最初混乱しました。
これは、ちゃんと説明できる知識が僕になくて申し訳ないんですが、コンストラクタを提供してる
Objectプロトタイプオブジェクトと、それをnewして生成されるインスタンスを
明確に区別して整理する必要があります。
(クラスベースでいう、クラスがプロトタイプオブジェクト、っていう感覚でここはよいと思います。
プロトタイプベースと呼ばれる言語をJavascript以外に触ったことがないので、おそらく
明確な定義で違いはたくさんありそうですが。)
よく使うけど、よくわかってなかったメソッドとかの一部を紹介してみようと思います。
- Object.assign
- 参考
- とりあえず雑にオブジェクトのコピーしたいときとかに使います。
Object.assign({}, source)
みたいな感じで
- Object.create
- setPrototypeOfあるからあんまり使わないかも。
- 引数に渡したオブジェクトをprototypeに設定して返す。
- Object.create({a:1, b:2, c:3})
- これの戻り値を他のオブジェクトの
prototype
に設定することで継承を実現するサンプルが参考リンクにあります。 - Object.createは、他のことと一緒に後述します。
- Object.freeze
- 参考
- オブジェクトを不変(イミュータブル)にする。
- ネストしているオブジェクトまでは不変にできるわけじゃないので注意
- そして、上書きしようとしてもエラーが出るわけではないのでそこも注意
Javascript
let g = Object.freeze({a:1, b:2, c:{d:3, e:4}})
g.a = 100
console.dir(g.a) // 1 書き換わってない。でもエラーが出るわけじゃないg.c.d = 100
console.dir(g.c.d) // 100 ネストしたオブジェクトまでfreezeできるわけじゃない
- Object.isFrozen
- 引数に受け取ったオブジェクトがfreezeされてるかBoolean型で返す。
- Object.values/Object.keys
- Object.values
- 引数に渡されたオブジェクトの値のリストを返す
- Object.keys
- 引数に渡されたオブジェクトのキー(プロパティ、メソッド名)のリストを返す
- Object.values
- Object.setPrototypeOf
- 第一引数のprototypeに第二引数のオブジェクトを設定する
- Object.prototype.constructor
- これはこのメソッドを覚えるっていうよりはprototype.constructorを意識できると○
- 要するにコンストラクタ呼び出しされたときに実行される関数が入ってる。
Javascript
var Hoge = (function() {
var c = function(name) {
this.name = name
}
var p = c.prototype
p.greet = function() {
return 'Hello, My name is ' + this.name
}
return c
})()- ここでいう、cがprototype.constructorにあたる。
まとめると
- Objectプロトタイプオブジェクト
- Objectの生成に関するいろいろな機能を提供する
- Objectインスタンス
- JavaScriptにおけるすべてのオブジェクトのプロトタイプになる
4. new Object()と{…}とObject.create()
ここではインスタンス生成の色々な方法について紹介します。
- コンストラクタ関数によるオブジェクト生成
1 2 3 4 |
let numberObj = new Object(1) let stringObj = new Object('hello') let booleanObj = new Object(true) |
- Object.createによるオブジェクト生成
既存のObjectから新しいオブジェクトを作成する。
1 2 3 |
let numberObj = new Object(1) let newNumberObj = Object.create(numberObj) |
これを他のオブジェクトのprototypeに設定することで継承と同等
のものが実現できる。
setPrototypeOf
はこれのシンタックスシュガー的なもの、だと思ってるけど
合ってるかな。。。
- オブジェクト初期化子によるオブジェクト生成
これが簡単で色々できて、一番おなじみかもしれない。
1 2 3 4 5 6 7 8 |
let object = { member1: 1, member2: 2, func1: function() { console.dir('hello') } } |
- コンストラクタ関数にオブジェクト、もしくはプリミティブな値を入れてみる
- プリミティブな値を渡してみる。
1 2 3 |
let a = new Object(1) // Number {[[PrimitiveValue]]: 1} |
Number,String,Booleanといったプリミティブな値を渡すと、それをラップした
オブジェクトが返ってくる。(これはプリミティブな型とは同盟のオブジェクトの型であることに注意)
- オブジェクトを渡してみる。
1 2 3 |
let a = new Object({a:1, b:2}) // Object {a: 1, b: 2} |
オブジェクトが返ってくる。
次は、「プリミティブな値」と「オブジェクト(インスタンス)」を調べてみる。
5. オブジェクト型とプリミティブな値
String, Boolean, Number, Null, UndefinedがそれぞれJavasciptの
プリミティブな型になります。
NullとUndefinedはそれぞれ値が1種類しかないので、省略します。
それ以外のものに関して、調べてみます。
1. String型
1 2 3 4 5 6 7 8 9 |
let pStr = "abcde" let oStr = new String("abcde") console.log(pStr==oStr) // true console.log(pStr===oStr) // false typeof(pStr) // string typeof(oStr) // object |
こんな感じになってます。
Javascriptの等価演算子の==
は比較時に型をチェックしませんので、true
が返ってきますが、
===
はfalseになってます。
それぞれstring
型とobject
型となっています。
この結果からNumberとBooleanも想像ができますが、一応書いておきます。
2. Number型
1 2 3 4 5 6 7 8 9 |
let pNum = 12345 let oNum = new Number(12345) console.log(pNum==oNum) // true console.log(pNum===oNum) // false typeof(pStr) // number typeof(oStr) // object |
3. Boolean型
1 2 3 4 5 6 7 8 9 |
let pBool = true let oBool = new Boolean(true) console.log(pBool==oBool) // true console.log(pBool===oBool) // false typeof(pStr) // boolean typeof(oStr) // object |
想像の通り。型は違うのが確認できました。
String,Number,Booleanは文法が用意されていますが、同名の
オブジェクトとは型が違うことに注意しなければなりません。
(通常、String,Number,Booleanをオブジェクト型で用いるケースは多くないと思いますが。)
6. Javascriptにおけるイミュータブルな値
まずは参考リンクをいくつか。
- 参考 イミュータブル wikipedia
- 参考 不変性と再代入不可能性、或いは「valかつmutable」と「varかつimmutable」について Qiita
- 参考 Object.freeze() MDN web docs
注意してほしいのが、参考リンクにおける再代入不可能製とイミュータブルを混同しないことで、
ここで説明したいのはイミュータブルについて、です。
(Scala始めたばっかりの頃、少し混乱したので念のため。再代入不可能性はconst
キーワードを
用いることで実現できます。)
本題に戻ると
Object.freeze
を利用することで、オブジェクトをイミュータブルにすることができます。
1 2 3 4 5 6 7 8 9 10 11 |
let obj = {a:1, b:2} console.dir(obj) // {a:1, b:2} obj.a = 100 console.dir(obj) // {a:100, b:2} Object.freeze(obj) // ここでobjがイミュータブルになる。 obj.a = 1000 console.dir(obj) // {a:100, b:2} |
この通り、Object.freeze()
に渡したオブジェクトは、変更をしても、その値を
オブジェクトが保持していない(変更していない)のが確認できると思います。
ただ、注意点として、Object.freeze()
実行後に、変更しようとしても
変更されませんが、エラー等が出るわけではありません。
そしてもう一つ、ネストしたオブジェクトに対しては、ネストしているオブジェクトに
対してもObject.freeze()
していないとイミュータブルではありません。
以下に例を挙げます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let obj = { a:1, b:2, c: { d: 3, e: 4 } } console.dir(obj) // {a:1, b:2, c: {d:3, e:4}} obj.c.d = 100 console.dir(obj) // {a:100, b:2, c: {d:100, e:4}} Object.freeze(obj) // ここでobjがイミュータブルになる。 obj.c.d = 1000 console.dir(obj) // {a:100, b:2, c: {d:1000, e:4}} |
このように、ネストしているc
はイミュータブルではありません。
再帰的に処理する等の対処法が必要です。
Facebook製のImmutable.jsという、イミュータブルなオブジェクトとかコレクションを
使えるようにしてくれるライブラリがあるので、それの実装を
どこかで見れたらいいなぁ。
7. おまけ
この記事を書くにあたって色々調べた過程で見つけた、面白い参考リンク
貼っておきます。