ECMAScript
第3章 ECMAScript
この章ではECMAScriptについて説明します。ECMAScriptの全部を紹介するにはページがいくらあっても足りませんので、中でも必要度の高いものだけを紹介します。
3.1 コメント
コメントはC/C++同様に、/* コメント */もしくは//のある行です。その中でも特に/** */のコメントはJSDocと呼ばれる形式のものでドキュメントの自動生成に使えるもので、関数やクラスの定義にはなるべく付けるべきです。
1: /**
2: * hoge - 2つの引数をほげる
3: * @param {number} arg1 この数に1を足した数が画面に出力される
4: * @param {number} arg2 この数に2 を足した数が画面に表示される
5: * @returns {number} arg1 + arg2
6: */
7: function hoge(arg1, arg2) {
8:
9: // ここの行はコメントです。
10:
11: console.log(arg1 + 1)
12: console.log(arg2 + 2)
13: return arg1 + arg2
14: }
15:
16: /*
17: ブロックコメントです。
18: */
コメントの是非
どのプログラミング言語にもコメント機能はありますが、どのようなコメントを書くべきなのでしょうか?もちろんあなたの置かれている環境によりますが、一般論としてコメントはある種の必要悪だと考えられています。
以前、説明したDRY原則を思い出してください。コメントはDRY原則に反しがちなのです。もちろんリスト3.2は極端な例ですが、そのプログラムがどういう処理をしているか?という情報は本来ソースコードを読めば読み取れるものです。
1: // 変数aを0で初期化する
2: let a = 0
3:
4: // 10を足す
5: a += 10
たとえばリスト3.3もDRY原則に反したコメントの例です。readFileという命名からファイルを読み込むというのは簡単に読み取れるのだからこのコメントには情報量がありません。
リスト3.3: 既に名前で説明されているもの
1: // ファイルを読み込む関数
2: function readFile(filename) {
3: ...
4: }
リスト3.4はもう少し少し意味のあるコメントです。値段に1.08を掛けるというコメントであれば意味はありませんが、消費税を加えるという説明であれば意味があるのです。むしろ何の説明もなしにいきなり1.08というマジックナンバーがあるのは良くありません。
1: // 消費税を加える
2: price *= 1.08
よりベターなのは、先ほど説明したJSDoc形式です。単体の数式ではなく関数化することで更新に強くなりますし、見ただけで意味を把握できるようになるのです。今回のサンプル程度であれば簡単に推測は立ちますが、APIにおいて引数や戻り値については明示しておいた方が親切です。JSDocであれば、変換ツールを通すことでAPIドキュメントを自動作成することができます。
1: /**
2: * 消費税を加えた価格を返す
3: * @param {number} price 税別価格
4: * @return {number} 消費税が加えられた価格
5: */
6: function addConsumptionTax(price) {
7: return price * 1.08
8: }
DRY原則に反するコメントには2つの問題があります。まず改修コストが重複の分だけ増えてしまいます。そしてDRY原則に反したプロダクトを長期メンテナンスすると、たいていの場合は間違ったコメントやドキュメントを生み出します。これらによって生じた矛盾は、計り知れない害になるのです。
3.2 変数
変数は再代入可能なletか再代入不可能なconstで定義します。基本的にはconstを使い、必要なときだけletを使うスタイルがお勧めです。constでほとんど困りませんし、バグを防ぎやすくなります。
1: let hoge = 'hoge'
2: const fuga = 1
3:
4: hoge = 'fuga' // letで宣言してるので再代入ができる
5: fuga = 2 // constで宣言してるので再代入ができない
6:
7: const piyo = [1]
8: piyo.push(2) // constで定義された配列の中身はいじることができる
9: console.log(piyo) // --> [1, 2]
ひとつ注意しなければいけないのですが、constは再代入は不可能ではあるもの、配列やオブジェクトの場合はpiyo.push(2)のように再代入が発生しないものに関しては実行できるため、中身の書き換えが発生します1。
1: let hoge = 1
2:
3: function foo() {
4: console.log(hoge) // hogeはここでも参照できる
5: if (hoge === 1) {
6: let fuga = 2
7: }
8: console.log(fuga) // fugaはここでは参照できない
9: }
constやletはブロックスコープです2。ブロックスコープとは、波括弧で表されたブロックの中に対しては変数定義が有効ですが、そのブロックから外に出るとその定義がクリアされるものです。
3.3 関数
ECMAScriptで関数定義する方法は複数ありますが、functionとアロー関数がよく使われます。
1: // function定義構文
2: function hoge(arg) {
3: console.log(arg)
4: }
5:
6: // function式
7: const fuga = function(arg) {
8: console.log(arg)
9: }
10:
11: // アロー関数
12: const piyo = (arg) => {
13: console.log(arg)
14: }
function定義構文もfunction式も、ECMAScriptの鬼門であるthis3の扱いが面倒になってしまいます。基本的にはアロー関数を使用すべきです。
3.3.1 アロー関数
アロー(=>)の前が仮引数の宣言部で後ろは関数の中身です。仮引数宣言部は引数が1つだけなら括弧を省略できます。後ろの関数の中身についても、単一の式であれば波括弧を省略可能です。省略した場合は、その式が戻り値になります。この省略形は、関数の引数にコールバックとして渡す場合によく使われます。
JavaScriptでは関数をコールバックなどで多用してきたため、個別にfunctionとタイピングするのが面倒という側面もあります。一時期人気のあったCoffeeScriptというAltJS4などで使われていたのですが、ES2015に取り込まれたのです。
1: const hoge = (arg1, arg2) => {
2: ...
3: }
4:
5: const fuga = arg => {
6: ...
7: }
8:
9: const piyo = ['const', 'let'].map(v => v.length)
10: console.log(piyo) // --> [5, 3]
3.3.2 クロージャー
constやletはブロックスコープです。これは内側のブロックでは外側で定義された変数を参照できるというものです。
1: const hoge = () => {
2: let a = 1
3:
4: const fuga = () => {
5: // ブロックの内側なのでaにアクセスできる
6: a += 10
7: console.log(a)
8: }
9: return fuga
10: }
11:
12: const fuga = hoge()
13: fuga() // --> 11
14: fuga() // --> 21
hogeの定義の外からは本来aにはアクセスできません。しかしfugaを経由して間接的にアクセスができるのです。
3.3.3 デフォルト引数
functionやアロー関数では引数にデフォルト値を設定できます。デフォルト引数の場合、引数を省略するとそのデフォルト値が使われます。デフォルト引数ではないのに引数が足りない場合はundefinedが入ります。
1: const hoge = (arg = 'hoge') => console.log(arg)
2: hoge('fuga') // --> fuga
3: hoge() // --> hoge
4:
5: const fuga = arg => console.log(arg)
6: fuga() // --> undefined
3.3.4 レストパラメータ
レストパラメータは可変長引数を実現するためのものです。
1: const hoge = (a, b, ...args) => console.log(args)
2:
3: hoge(1, 2) // --> []
4: hoge(1, 2, 3) // --> [3]
5: hoge(1, 2, 3, 4) // --> [3, 4]
3.4 型とリテラル
ECMAScriptは動的言語ですが、型そのものがないわけではありません。文字列はstring型ですし数字はnumber型です。boolean、null、undefined、number、string、symbolの6つはECMAScriptにおいてはプリミティブ型で、それ以外はすべてオブジェクトになります。
3.4.1 null
nullは中身が存在しないことを示すものです。たとえば文字列を入れる変数に文字列が入っていない場合などを示すときに使われます。ただし、nullは色々と事故を起こしがちなのであまり使われるべきではありません。if文などではfalseとして扱われます。
3.4.2 undefined
nullと似ていますが初期化されてない変数にはundefinedが入っています。null同様にif文などではfalseとして扱われます。
文字列化されるnullとundefined
nullやundefinedは文字列として評価されるとそのままnullやundefinedという文字に化けるためバグの温床になりやすいです。
1: let hoge
2: console.log(hoge) // --> undefined
3: console.log('hoge: ' + hoge) // --> hoge: undefined
3.4.3 string
文字列には三種類のリテラルがあります。シングルクォート ' とダブルクォート " とバッククォート ` です。RubyやCのような他の言語とは異なり、シングルクォートもダブルクォートも違いはありません。バッククォートによる文字列リテラルはテンプレートリテラルと呼ばれ、中で変数や式が展開されます。大変便利なのでテンプレートリテラルを使える場合にはうまく使っていきたいものです。
1: console.log('hoge') // --> hoge
2: console.log('ho'ge') // エラーになる
3: console.log('fu"ga') // --> fu"ga
4: console.log('\"piyo\"') // --> "piyo"
5:
6: console.log("ho'ge") // --> ho'ge
7:
8: const hoge = 'hoge'
9: console.log(`"${hoge}"`) // --> "hoge"
10: console.log(`1 + 2 = ${1 + 2}`) // 1 + 2 = 3
テンプレートリテラルには、ちょっとマイナーなタグ付きテンプレートというものもあります。
1: const sql = (strings, ...values) => {
2: const result = []
3: values.forEach((value, index) => {
4: result.push(strings[index])
5: result.push(escape(value))
6: })
7: result.push(strings[strings.length - 1])
8: return result.join('')
9: }
10:
11: const key = 'hoge'
12: console.log(sql`select * from hoge where key=${key}`)
タグ付きテンプレートは驚くほどマイナーですが、任意の関数に入力することができるもので、使いこなすととても効果的です。文字列のエスケープに苦しめられた人も多いと思いますが、そういった問題を解決できるものです。また、返せるのは文字列だけではありません。任意のオブジェクトを生成したり、数字に変換したりできるのです。
3.4.4 boolean
真偽値でリテラルはtrue、falseです。
3.4.5 number
ECMAScriptでは数値型は、整数も浮動小数点もnumberという単一の型で表します。
タイプ | 正規表現 | 例 |
10進数 | /[+-]?(0|[1-9][0-9]*)?(\.[0-9]*)?([eE][+-]?[0-9]+)?/ | 0, +42, -29, .1, 1.6e+1 |
2進数 | /[+-]?0[bB][01]+/ | 0b01011010 |
8進数 | /[+-]?0[oO][0-7]+/ | 0o755 |
16進数 | /[+-]?0[xX][0-9a-fA-F]+/ | 0xdeadbeaf |
numberはtoString()に引数を渡すことで、10進数以外の文字列にできます。文字列から数字への変換はいくつか方法がありますが、Number.parseInt()やNumber.parseFloat()が良いでしょう。parseIntは2つ目の引数で基数を渡します。10進数の場合は省略可能です。分かりやすくするために明示的に指定するのも一つの方法です。
1: const num = 42
2: console.log(num.toString(16)) // --> 2a
3: console.log(num.toString(8)) // --> 52
4: console.log(num.toString(2)) // --> 101010
5:
6: console.log(Number.parseInt('42', 10)) // --> 42
7: console.log(Number.parseInt('1f', 16)) // --> 31
8: console.log(Number.parseFloat('4.2')) // --> 4.2
3.4.6 オブジェクト
ECMAScriptではプリミティブ型以外はすべてオブジェクトなので正規表現(RegExp)や配列(Array)などもオブジェクトです。オブジェクトはメンバーやメソッドを持っています。
1: const obj = {
2: hoge: 'hoge',
3: fuga: () => {console.log('fuga')}
4: }
5:
6: console.log(obj.hoge) // --> hoge
7: obj.fuga() // --> fuga
オブジェクトを作成するときに、キー名と同じ名前の変数の場合、キー名を省略可能です。
1: const s = 'hoge'
2: console.log({s}) // --> {s: 'hoge'}
NULLオブジェクトパターン
NULLオブジェクトパターンはnullと同じ意味をより安全に実現するためのデザインパターンです。
nullやundefinedは問題を起こしがちです。たとえばこれらが入った変数を関数呼び出ししようとしたり、メンバーをアクセスしようとするとエラーになってしまいます。そのため普通ならば対象がnull, undefineではないかどうかを判定する必要があります。
1: const hoge = null
2: hoge() // エラーになる
3: hoge.fuga() // エラーになる
4:
5: if (hoge && typeof hoge.fuga === 'function') {
6: hoge.fuga() // hoge.fugaがある時だけ呼び出されるからエラーにならない
7: }
しかし、たとえば最初からhogeにfugaというメソッドがダミーで定義されていれば、条件分岐は必要ありません。リスト3.20ではnullObjectPattern関数のデフォルト引数に、ダミーのfugaメソッドが入ったオブジェクトが指定されているため条件判定が不要です。
1: const nullObjectPattern = (hoge = {fuga: () => {}}) => {
2: hoge.fuga() // デフォルト引数のおかげでエラーにならない
3: }
この事例では関数ですが、もちろん配列でも文字列でも他のオブジェクトでもかまいません。何もしない、という初期値を与えたオブジェクトを使うことで、コードのもつ複雑さを減らすことができます。もちろん万能ではありませんが、NULLオブジェクトパターンを使える場合は是非とも検討したいものです。
3.4.7 Arrayオブジェクト
Arrayは配列で、定義方法はいくつかあります。new Array(10)だとundefinedが10個入った配列(未初期化配列)が生成されます。初期化済み配列の作成には[0, 1, 2]のようなリテラルが一般的に使われます。new Array(0, 1, 2)でも可能ですがnew Array(10)と紛らわしいのでリテラルで生成するのが一般的です。
1: const ar1 = [0, 1, 2]
2: console.log(ar1.length) // --> 3
3: console.log(Array.isArray(ar1)) // --> true
4:
5: const ar2 = Array.from(iterable)
Arrayでよく使われるプロパティやメソッドにはlengthプロパティ(配列の長さを取得する)や、配列に見えて配列ではないオブジェクト5、後述するイテラブルを配列に変換するためのArray.from()、配列かどうかを判定するArray.isArray()などがあります。
3.4.8 RegExpオブジェクト
/^hoge+$/はhogehogeeeeにマッチする正規表現オブジェクトを生成するリテラルです。new RegExp(‘^hoge+$‘)のように文字列で正規表現を渡すことでも生成可能です。/^hoge+$/はhogeやhogeeeeにマッチする正規表現のリテラルです。new RegExp('^hoge+$')でも同じ物を生成できます。正規表現に合致するか判定するだけならtestメソッドを使い、文字列の部分文字列(かっこでくくった部分)を取得する時にはmatchを使います。
1: const re = /^Ja(.+)Script$/
2:
3: console.log(re.test('JapariScript')) // --> true
4: console.log(re.test('JapariPark')) // --> false
5:
6: const matched1 = re.exec('JavaScript')
7: console.log(matched1) // --> ['JavaScript', 'va']
8:
9: const matched2 = re.exec('Java')
10: console.log(matched2) // --> null
3.5 制御構文
ECMAScriptではC言語と同じような、少し古いタイプの制御構文が採用されています。
3.5.1 if
if文は条件分岐の構文で、他の多くの言語に見られるものと同様です。if文の条件判定ではbooleanであればそのまま、それ以外ならbooleanに変換がされます。ECMAScriptの場合の注意点は、undefined, null, 0, NaN, ''がそれぞれ偽になることです。
ただし「0」や「''」の場合は、暗黙の型変換にたよらずにちゃんとif (hoge === 0)やif (hoge === '')のように明示する方がバグを防ぎやすくコードが読みやすくなります。NaNの場合は、Number.isNaN()を使う必要があります6。
比較演算子
JavaScriptでは==, !=よりは===, !==を使う方がよいとされます。PHPと同じで、前者は暗黙の型変換をしたり判りにくいロジックで判定をするので、バグの温床になるからです。
1: console.log(1 == '1') // --> true
2: console.log([] == 0) // --> true
3: console.log('' == 0) // --> true
4: console.log('\t' == 0) // --> true
5:
6: console.log(1 === '1') // --> false
7: console.log([] === 0) // --> false
8: console.log('' === 0) // --> false
9: console.log('\t' === 0) // --> false
ちなみにオブジェクトの比較の場合は工夫が必要になります。
1: const a = {hoge: 1}
2: const b = {hoge: 1}
3:
4: console.log(a == b) // false
5: console.log(a === b) // false
6: console.log(a.toString() === b.toString()) // true
7: console.log(JSON.stringify(a) === JSON.stringify(b)) // true
元々==でも===でもオブジェクトを中身で比較することはできません。一番確実なのは再帰的なメンバーの比較です。場合によってtoStringによる文字列化やJSON化して比較できるパターンもあります。
3.5.2 for
for文は一般的な繰り返し構文です。リスト3.25はC言語と同様のおなじみのパターンです。ただ昔ながらのfor文を使うことはまれです。
1: for (let i = 0; i < 10; i++) {
2: console.log(i)
3: }
4:
5: const arr = ['hoge', 'fuga', 'piyo']
6: for (let i = 0; i < a.length; i++) {
7: console.log(arr[i])
8: }
9:
10: const obj = {hoge: 'hoge', fuga: 'fuga'}
11: for (let i = 0; i < Object.keys(a).length; i++) {
12: console.log(obj[Object.keys(a)[i]])
13: }
リスト3.26のようなfor inやfor ofの方が圧倒的に使い勝手がよいです。forは配列などに使われることが多いため、昔ながらの添え字を使ったアクセスでは煩雑になりやすいためです。
for inは添え字を取得するもので、for ofは中身を取得するものです。添え字を取得するよりは中身を取得する方がよくあるパターンなので、for ofの方をよく見かけると思います。
1: for (let i in Array(10)) {
2: console.log(i)
3: }
4:
5: const arr = ['hoge', 'fuga', 'piyo']
6: for (let value of arr) {
7: console.log(value)
8: }
9:
10: const obj = {hoge: 'hoge', fuga: 'fuga'}
11: for (let value of arr) {
12: console.log(value)
13: }
リスト3.27のようにArrayのforEachメソッドを使うのもよく使われる手です。
1: Array(10).forEach((v, i) => console.log(i))
2:
3: const arr = ['hoge', 'fuga', 'piyo']
4: arr.forEach(value => console.log(value))
5:
6: const obj = {hoge: 'hoge', fuga: 'fuga'}
7: Object.keys(obj).forEach(key => console.log(obj[key]))
便利なArrayメソッド
ArrayにはforEach以外にもmapというメソッドがあります。mapはforEach同様に配列の中身で繰り返しを行いますが、mapに渡した関数の戻り値を使って配列を再構築します。配列の加工ですね。
1: Array(10).forEach((v, i) => console.log(i))
2:
3: const arr = ['hoge', 'fuga', 'piyo']
4: arr.forEach(value => console.log(value))
5:
6: const obj = {hoge: 'hoge', fuga: 'fuga'}
7: Object.keys(obj).forEach(key => console.log(obj[key]))
filterメソッドは名前のとおり配列の中身をフィルタリングして配列の個数を減らします(もちろん全部trueであれば減りません)。
1: const hoge = ['hoge', 'fuga', 'JavaScript', 'CoffeeScript']
2: const fuga = hoge.filter(word => word.length === 4)
3:
4: console.log(fuga) // ['hoge', 'fuga']
他にもArrayのメソッドには色々便利なメソッドがありますので是非研究してみてください。
イテレータプロトコル
for inやfor ofの対象はイテレータプロトコルが実装されているオブジェクト(イテラブルとかイテレータと呼びます)である必要があります。ArrayやStringなどではあらかじめ実装されているのですが、自前でイテレータプロトコルを実装するためにはSymbol.iteratorというメソッドを自前で実装するか、ジェネレータを使う必要があります。
1: class Hoge {
2: constructor() {
3: this.value = 0
4: }
5:
6: [Symbol.iterator]() {
7: return {
8: next: () => {
9: let result
10: if (this.value < 10) {
11: result = {value: this.value, done: false}
12: } else {
13: result = {done: true}
14: }
15: this.value++
16: return result
17: }
18: }
19: }
20: }
21:
22: const hoge = new Hoge()
23: for (let value of hoge) {
24: console.log(value)
25: }
ジェネレータなら簡単にイテレータを定義できます。ただし注意点として、アロー関数でジェネレータは作成できません。
1: function *hogeCreator() {
2: for (let i = 0; i < 10; i++) {
3: yield i
4: }
5: }
6:
7: const hoge = hogeCreator()
8: for (let value of hoge) {
9: console.log(value)
10: }
イテレータプロトコルを自前で実装していたリスト3.30よりもジェネレータを使ったリスト3.31の方が圧倒的にシンプルになったことが分かると思います。function *hogeCreator()はイテレータそのものではなく、イテレータを生成する関数です。引数を渡せばイテレータを自由にカスタマイズできます。yieldキーワードはいってみればそのジェネレータを一次停止して元のコードに戻る役割があります7。ちなみにyeild*はイテレータを渡すと、分解して1つずつyieldしてくれます。
1: class Hoge {
2: *[Symbol.iterator]() {
3: for (let i = 0; i < 10; i++) {
4: yield i
5: }
6: }
7: }
8:
9: const hoge = new Hoge()
10: for (let value of hoge) {
11: console.log(value)
12: }
リスト3.32は、クラス定義の中でイテレータのメソッドをジェネレータとして定義したものです。リスト3.31のようにむき出しで使うよりも、利用者側としては分かりやすいかもしれません。
イテレータによってHaskellにあるような無限リストのようなものも簡単に実装が可能です。
3.5.3 while
while文やdo while文は他の言語でも良く見かけられる一般的なものです。現代においては昔ながらのfor文同様にあまり使う機会はないかもしれません。
1: while ((let line = readLine()) !== null) {
2: console.log(line)
3: }
4:
5: let i = 0
6: do {
7: console.log(i)
8: } while (++i < 10)
3.5.4 switch
switch文は言語によって形式が異なることが多いのですが、残念ながらECMAScriptではCと同じ昔ながらのswitch文です。そのため注意点として明示的にbreakをしないと次のcase文に処理が移ってしまいます8。
1: switch (hoge) {
2: case 'HOGE':
3: {
4: console.log('hoge')
5: }
6:
7: case 'FUGA':
8: {
9: // case 'HOGE'の処理は引き続きここに来る
10:
11: console.log('fuga')
12: break
13: }
14:
15: default:
16: {
17: console.log('default')
18: }
19: }
hogeが'HOGE'の場合に画面には'hoge'とfuga'の両方が出力されます9。eslintのようなツールを使えばそういう場合に警告してくれるので、なるべく導入しましょう。
また、swtich文はひとつのブロックでスコープが形成されているので、caseラベルの中ではブロックを明示的に書いてスコープを作る方が安全です。
ブロックを使いましょう
if文などの制御構文には必ずブロックを使いましょう。リスト3.35はiOSにあった重大かつ深刻なバグで大きな影響を与えました。
1: if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
2: goto fail;
3: goto fail;
どこにバグがあるか気付いたでしょうか?ひとつ目のgoto fail;はif文によって実行されますが、ブロックではないためにふたつ目が無条件で実行されてしまいます。つまり条件判定に関係なく必ずgoto fail;してしまっていたのです。
ぱっと見た感じではついつい、ふたつ目もif文の影響をうけるのかと思いますよね。
3.6 例外
例外は行儀の良い挙動では無いと考えられているため、最近の多くの言語では例外は使われず別の方法で表現されることが多いです。ECMAScriptにも例外はありますし、昔ながらのライブラリでは例外を投げるものもありますが、例外をなるべく使わないことは可能です。
1: hoge = () => {
2: throw new Error('hoge')
3: }
4:
5: try {
6: hoge()
7: } catch(e) {
8: console.log(e) // Error: hoge
9: } finally {
10: console.log('finally')
11: // ここは例外あるなしに関わらず必ず呼び出される
12: }
例外が行儀悪いのだとすれば、どうするのが良いのでしょうか。ECMAScriptでは関数のコールバックが多用されますが、この手のコールバックの多くは第一引数をエラー変数として扱います。リスト3.37はよくあるコールバックの使われ方です。
1: const hoge = (callback) => {
2: ...
3: callback(err, result)
4: }
5:
6: hoge((err, result) => {
7: if (err) {
8: console.log('エラー')
9: } else {
10: console.log(result)
11: }
12: })
戻り値をオブジェクトにしてエラーも含めるというテクニックもあります。他にはPromiseに例外処理の仕組みが組み込まれています。これについては詳しくは後ほど説明します。
3.7 class
他の多くのオブジェクト指向言語同様に、ECMAScriptの2015以後ではリスト3.38のようにclassがサポートされています。
1: class Hoge {
2: // new Hoge()される時に呼び出されるコンストラクター
3: constructor(param = 'default string') {
4: this.param = param
5: }
6:
7: // 通常のメソッド
8: print() {
9: console.log(this.param)
10: }
11:
12: // 静的なメソッド
13: static create() {
14: return new Hoge('CREATED')
15: }
16: }
17:
18: const hoge = new Hoge()
19: hoge.print() // --> 'default string'
20:
21: const fuga = Hoge.create()
22: fuga.print() // --> 'CREATED'
23:
24: hoge.create() // エラー
25: fuga.create() // エラー
constructorは予約語でコンストラクタの定義に使われます。コンストラクタはnewによるインスタンスの生成時に呼び出されるもので、クラスのプロパティを初期化するために使われます。
クラスのプロパティはthis.paramのような形でアクセスします。他の言語だとメソッドの中では省略形が使えることがありますが、JavaScriptではメンバーにアクセスするためにはthis.を付ける必要があります。
静的メソッドはオブジェクトのインスタンスとは無関係にクラス自体に結びついたメソッドで、ファクトリーメソッドパターンやシングルトンパターンなどによく使われます。
3.7.1 継承
他の言語同様にクラスの継承もサポートされています。注意点として、ECMAScriptでは子クラスのコンストラクタは親クラスのコンストラクタを自動では呼び出さないので、constructorを定義する場合には必ずsuper()の呼び出しを忘れないようにしましょう。
1: class Fuga extends Hoge {
2: constructor() {
3: ...
4: super() // Hogeのコンストラクターが呼び出される
5: ...
6: }
7: }
3.7.2 プライベート
プライベートメンバーはサポートされていません。外部からアクセスされたくないメンバーやメソッドにはアンダースコアを先頭に付けることが通例になっています。静的メンバーも用意されていません。こちらはクロージャーを活用して実現するのが定石となっています。
1: let hoge = 1
2: class Hoge {
3: fuga() {
4: hoge += 10
5: console.log(hoge)
6: }
7: }
8:
9: const hoge1 = new Hoge()
10: hoge1.fuga() // 11
11:
12: const hoge2 = new Hoge()
13: hoge2.fuga() // 21
14:
15: hoge1.fuga() // 31
eslint
ECMAScriptを書いていく上の必須ツールがeslintです。lintというのは元々Cコンパイラがエラーとして指摘しないけど、コーディング作法から反するようなコードに警告を出すプログラムでした。伝統的にhogelintのような(linterと呼ばれるジャンル)ものがさまざまな言語には用意されています。JavaScriptではjslint、jshintなどさまざまなlinterがありましたが、最近ではECMAScriptにはeslintが使われており、TypeScriptにはtslintが使われています。
eslint/tslintを使うメリットは何でしょうか?行儀の悪いコードを教えてくれるというのは大きなメリットです。バグが出やすいよというのを教えてくれるのでそこを修正することで、コードの品質が向上するのです。
またチーム開発のときに、チームで「こういうルールを使おう」というのを決めておけば、そのルールに従わないコードには警告・エラーが出てくれます。これはチーム開発においてはとても重要です。そして、インデントや空白の開け方など、一部のルールでは自動修正機能もあります。いちいちコーディングルールを人間が確認しなくても機械が勝手に見てくれるのです。
プログラマの美徳は怠惰です。人間がわざわざ書式を考えるのではなく、機械が勝手にフォーマットしたりアラートをだしてくれれば、その分人間は本来の課題に集中することができます。
eslintを使う時にお勧めなのがVSCodeとESLint拡張です。エディタ上で勝手に警告を出してくれますし、eslintを用いた自動修正をすることも可能です。拙作のwaterslideを使っているならデフォルトでeslintが導入されていると思いますので、ご活用ください。
3.8 便利な演算子・構文
3.8.1 分割代入
分割代入は1つのオブジェクトや配列を使って複数の変数に代入するというものです。
配列の分割代入の基本形はconst [a, b] = [1, 2]です。この場合、aに1が入りbに2が入ります。
1: let a, b
2: [a, b] = [1, 2] // a = 1, b = 2
3:
4: const [a, b, c] = [1, 2] // a = 1, b = 2, c = undefined
5:
6: const [a] = [1, 2] // a = 1
7:
8: const [a, , c] = [1, 2, 3] // a = 1, c = 3
9:
10: const [a, ...b] = [1, 2, 3] // a = 1, b = [2, 3]
11:
12: const [a, ...b] = [1] // a = 1, b = []
13:
14: const [a = 1, b = 2] = [0] // a = 0, b = 2
元の配列の数が足りない場合はundefinedが入りますし、逆に配列の方が多い場合は単に無視されます。...bやa = 0のように関数の引数と同様の指定も可能です。
1: let a, b
2: ({a, b} = {a: 1, b: 2}) // a = 1, b = 2
3: // ()でくくらないとエラーになる
4:
5: const {a, b, c} = {a: 1, b: 2} // a = 1, b = 2, c = undefined
6:
7: const {a} = {a: 1} // a = 1
8:
9: const {a = 1, b = 2} = {a: 0} // a = 0, b = 2
10:
11: const {a: hoge} = {a: 1} // hoge = 1
オブジェクトの場合も配列と似たような形です。基本形はconst {a, b} = {a: 'hoge', b:'fuga'}です。なんとなく想像がつくと思いますが、a = 'hoge', b = 'fuga'になります。関数の仮引数に使うととても便利です。
1: const func = ({a, b, c = 3}) => {
2: console.log(a) // --> 1
3: console.log(b) // --> 2
4: console.log(c) // --> 3
5: }
6:
7: func({a: 1, b: 2})
3.8.2 スプレッド構文
スプレッド構文は分割代入と似ていますが動作は全く逆です。1つの配列(正確にはイテラブル)を複数に展開するものです。
1: const a = [1, 2]
2: const b = [0, ...a, 3] // b = [0, 1, 2, 3]
3:
4: const a = {a: 1, b: 2}
5: const b = [...a] // error: イテラブルじゃないとダメ
6:
7: const hoge = (a, b) => console.log(a + b)
8: const fuga = [1, 2]
9: hoge(...fuga) // --> 3
3.8.3 || と &&
||, &&は論理和と論理積ですが、ショートサーキットと呼ばれる性質を利用して、LL言語によく見られるちょっとしたハックが使えます。
1: const hoge = (a, b) => a || b
2:
3: console.dir(hoge(1, 2)) // --> 1
4: console.dir(hoge(undefined, 1)) // --> 1
5: console.dir(hoge(undefined, null)) // --> null
6: console.dir(hoge(null, undefined)) // --> undefined
7:
8: const fuga = a => a && console.log(a)
9: fuga() // 何もしない
10: fuga(null) // 何もしない
11: fuga(1) // --> 1
a || bはaが真と判定されるならaがそのまま評価され、偽と判定されればbが評価されるというものです。a && console.log(a)の場合は、aが真として判定されればconsole.log(a)も処理されるけど、偽として判定されると処理がスキップするというものです。どちらもイディオムとして覚えておくといいかもしれません。
3.9 モジュール
ウェブブラウザ上で実行されるJavaScriptは現時点ですらまだほとんどモジュールの読み込みには対応してないのですが、さすがに現代にそれはあり得ないので、webpackかbrowserifyなどのバンドラを使ってモジュールをあらかじめ結合しておくというのが常套手段になっています。モジュール処理をする時に使われる構文には、requireとimport/exportがそれぞれありますが、どちらも一長一短ある上に流動的なので使いやすい方を使いつつ、情勢を見定める必要があります。
3.9.1 require
requireはNode.jsで使われるモジュールを読み込む仕組みです10。const fs = require('fs')のように使いますが、引数の文字列の頭に./, ../を付けるかどうかで挙動がかわります。
1: const hoge = require('./hoge.js')
2:
3: console.log(hoge()) // --> 'hoge'
リスト3.46のような相対パスだとローカルのファイルを参照します。付けていない場合は、Node.jsの組み込みモジュールもしくはnpmでインストールされたモジュールを参照しようとします。
読み込む対象のファイルではリスト3.47のようにしないとエクスポートがされません11。
1: const hoge = () => 'hoge'
2:
3: module.exports = hoge
3.9.2 import/export
import, exportはrequireと同じようなものですが、ECMAScriptで規定された構文です。現時点では少しずつウェブブラウザでも実装が始まっていますが、デフォルトでは機能が無効になっていたり、まだ安心して使える状態ではありません。またウェブブラウザ上ではnpmパッケージの読み込みはできませんので、npmパッケージに関してはバンドラであらかじめ結合していく必要があります。Node.jsも現時点ではimport/exportを処理できません。webpackを通すか、Babel + babel-preset-node6を通す必要があります。
リスト3.48はimport及びexportのサンプルです。よくあるパターンはexport defaultで何らかのオブジェクト12やプリミティブをエクスポートして、import name from 'module_name'のように読み込むものです。
1: import fs from 'fs'
2:
3: const hoge = () => 'hoge'
4: export default hoge
import fromで指定するモジュール名のルールはrequireのものと同じです。ローカルのものならrequire同様にimport hoge from './hoge'のようにする必要があります。
require/module.exportsとimport/exportを組み合わせた場合、少し面倒なことになります。
export | require | import |
module.exports = hoge | const hoge = require('./hoge') | import hoge from './hoge' |
export {hoge} | const {hoge} = require('./hoge') | import {hoge} from './hoge' |
export default hoge | const hoge = require('./hoge').default | import hoge from './hoge' |
module.exportsを使った場合には、const hoge = require('./hoge')やimport hoge from './hoge'で読み込めるのですが、export defaultだとimport hoge from './hoge'は変わらないのに、const hoge = require('./hoge').defaultのように.defaultが必要になってしまうという非対称性があります。知らないと苦労するので注意しましょう。広くライブラリを公開するようなときは、module.export = hogeのスタイルが無難かもしれません。
3.10 非同期プログラミング
JavaScriptはあるパーツをクリックしたらこのコードが動く、のようにイベントドリブンで動くことが多く、そのため非同期プログラミングが発達してきた言語です。
非同期処理はどうやって書くのでしょうか。DOMのイベントハンドラに登録するというスタイルやAPIにコールバックを渡すというスタイルが長らく使われてきましたが、コールバックには問題もあって、よくコールバックヘルといわれる多重呼び出しによって極端にネストが深まるというようなことがありました。
1: bot.receiveMessage(message => {
2: api.access('http://example.net/api/hoge', (err, res) => {
3: if (err) {
4: throw err
5: } else {
6: res.getResultAll((err2, body) => {
7: if (err2) {
8: throw err2
9: } else {
10: const obj = JSON.parse(body.toString())
11: api.access(`http://example.net/api/fuga/${obj.fuga}`, (err3, res2) => {
12: // 以後省略
13: })
14: }
15: })
16: }
17: })
18: })
解決策はいくつか生み出されていて、言語仕様としては Promise, Async/Awaitが使われ、Node.js的にはEventEmitterやStreamが使われます。また将来的にECMAScriptに取り込まれるかもしれないのがRxJSのObservableです。
3.10.1 Promise
PromiseはES2015で導入された機構です。Promiseを生成するとノンブロッキングで処理が走り出しますが、その処理が完了した時点で、Promiseのインスタンスにぶら下がった.then()や.catch()が呼び出されます。
リスト3.50は1秒経ったらdoneと表示されるというものです
1: const p = new Promise((resolve, reject) => {
2: setTimeout(() => {
3: resolve()
4: },1000)
5: })
6:
7: p.then(() => console.log('done!'))
Promiseのコールバックのresolveは処理の完了、rejectは処理の失敗を知らせるものです。もうひとつPromiseを使った事例を見てみましょう。リスト3.51はNode.jsでファイルの読み込みをPromise化したものです。
1: const fs = require('fs')
2:
3: const fileRead = filename => new Promise((resolve, reject) => {
4: fs.readFile(filename, (err, content) => {
5: err ? reject(err) : resolve(content)
6: })
7: })
8:
9: fileRead('README.md')
10: .then(content => console.log(content.toString()))
11: .catch(err => console.error(err))
resolve()の引数が.then()のコールバックの引数に、reject()の引数が.catch()のコールバックの引数に渡されます。まだこの時点ではあまりメリットがあるように見えないかもしれません。
Promiseの最大の特徴は合成できることです。.then()や.catch()の戻り値はPromiseなのでメソッドチェーンでつなぐことができることと、.then()の引数のコールバックがPromiseを返すことで、Promiseが複数つながるのです。リスト3.52はファイル読み込みのPromiseとファイル書き込みのPromiseを合成したものです。
1: const fs = require('fs')
2:
3: const fileRead = filename => new Promise((resolve, reject) => {
4: fs.readFile(filename, (err, content) => {
5: err ? reject(err) : resolve(content)
6: })
7: })
8:
9: const fileWrite = (filename, content) => new Promise((resolve, reject) => {
10: fs.writeFile(filename, content, (err) => {
11: err ? reject(err) : resolve()
12: })
13: })
14:
15: fileRead('README.md')
16: .then(content => fileWrite('hoge.md', content))
17: .then(console.log('file copy done'))
18: .catch(err => console.error(err))
もしPromiseを使わずに書いたとしたらfs.readFileとfs.writeFileがネストすることになります。まだふたつだけなのでネストもそんなに深くないですが、節の冒頭で述べたとおりコールバックヘルでは何重ものネストがたやすく発生します13。しかしPromiseであれば非同期処理が多数あったとしても、その分のPromiseの定義と.then()を並べるだけで済むのです。拡張性が増したともいえるでしょう。
もうひとつ特徴に気付いたでしょうか?エラー処理をするための.catch()をひとつしか書いていません。この場合、fileReadでreject()してもfileWriteでreject()しても、どっちも同じ.catch()にキャッチされるのです。この点もPromiseの楽な点です。
Promiseの注意点
Promiseにはいくつか注意すべき点があります。まずnew Promiseを実行した時点で最初のコールバックが呼び出され、処理が始まってしまいます。たとえば同時に10個のPromiseを作った場合、それらは並列で動いてしまうのです。これらをうまく直列に動かす為には、.then(() => promise())のように記述するか、Async/Awaitを使う必要があります。
次の注意点として、resolve(), reject()を実行してかつ、その関数を終了した時点で.then(), .catch()に処理が移ります。リスト3.53のコードは、なんとなくresolve()をした瞬間に.then()が呼び出されるような気がしてしまうのですが、脱出するまでは処理はされないため永遠に.then()は呼び出されません。
1: const p = new Promise((resolve, reject) => {
2: resolve()
3: for (;;) {;}
4: })
5:
6: p
7: .then(() => console.log('done'))
8: .catch(err => console.error(err))
resolve(), reject()を実行するとPromiseのステートはそこで変化してしまい、完了以前の状態に戻ることはできません。リスト3.54ではconsole.logが3回実行されるように見えますが、console.log(1)だけしか実行されません。
1: const p = new Promise((resolve, reject) => {
2: resolve(1)
3: resolve(2)
4: reject('hoge')
5: })
6:
7: p
8: .then(n => console.log(n))
9: .catch(err => console.log(err))
3.10.2 Async/Await
Async/AwaitはES2017で導入された構文で、非同期処理を同期処理的に扱うものです。リスト3.55ではさきほどのPromiseのチェーンで使ったファイル読み込みと書き込みのPromise自体はそのままで、.thenの代わりにAsync/Awaitで書き直しました。
1: const fs = require('fs')
2:
3: const fileRead = filename => new Promise((resolve, reject) => {
4: fs.readFile(filename, (err, content) => {
5: err ? reject(err) : resolve(content)
6: })
7: })
8:
9: const fileWrite = (filename, content) => new Promise((resolve, reject) => {
10: fs.writeFile(filename, content, err => {
11: err ? reject(err) : resolve()
12: })
13: })
14:
15: const copy = async (src, dest) => {
16: const content = await fileRead(src)
17: await fileWrite(dest, content)
18: }
19:
20: copy('README.md', 'fuga.md')
21: .then(() => console.log('copy done'))
async関数はPromiseを返すのでcopy.then()みたいなことも書けます。今回の例だとcopy関数は戻り値を返していませんが、戻り値を返していたら.thenの引数に渡されます。
なんとなく.thenのチェーンよりも素直なコードに見えませんか?複数の.thenとコールバックを並べるよりも、手続き的でわかりやすいコードになっています。async関数の戻り値がPromiseなのでawaitに入力したりと、PromiseとAsync/Awaitは組み合わせて使えるのです。RxJSをつかっている場合はObservableをtoPromise()でPromiseに変換させてからawaitで待つととても便利になります14。
3.10.3 EventEmitter
EventEmitterはNode.jsで使われているイベントドリブンに用いる仕組みで、メッセージパッシングができます。
1: const {EventEmitter} = require('events')
2:
3: const ev = new EventEmitter()
4: ev.on('hoge', msg => {
5: console.log(msg)
6: })
7: setTimeout(() => {
8: ev.emit('hoge', 'ほげー')
9: }, 1000)
EventEmitterをインスタンス化すると、任意のハンドラを.onメソッドで登録することができます。そのインスタンスの.emitメソッドを叩くことでハンドラを起動できるのです。.emitのひとつめの引数がメッセージの種類で、ふたつめの引数が送信されるメッセージです。巡視側は.onの第一引数で受け取るべきメッセージの種類を指定し、コールバックの引数でメッセージを受け取ります。Promiseとは違い、.emitは何回でも送信することができます。
EventEmitterは単にインスタンス化するだけではなく、継承をして使うこともできます。
1: const {EventEmitter} = require('events')
2:
3: class Hoge extends EventEmitter {
4: constructor() {
5: super()
6:
7: this.on('hoge', msg => {
8: console.log(msg)
9: })
10: }
11: }
12:
13: const hoge = new Hoge()
14: hoge.emit('hoge', 'ほげー')
継承をした場合の利点はクラスのメソッドとしてemitがたたけるということです。継承ではなく、委譲というテクニックもあります。
1: const {EventEmitter} = require('events')
2:
3: class Hoge {
4: constructor() {
5: this.ev = new EventEmitter()
6:
7: this.ev.on('hoge', msg => {
8: console.log(msg)
9: })
10: }
11:
12: emit(...args) {
13: this.ev.emit(...args)
14: }
15: }
16:
17: const hoge = new Hoge()
18: hoge.emit('hoge', 'ほげー')
継承で直接emitを晒すのではなく、自分でemitメソッドを作ったうえでEventEmitterに受け流すことで委譲をしているのです。継承のときよりも少しコードは増えてしまいますが、分量としてはそれほどでもありません。委譲ができる場合は委譲すべきでしょう。
継承より委譲
「継承による差分プログラミング」というのはJavaで広まった概念ですが、現代においては不適切なスタイルであるとされています。
継承は密結合的になりがちな仕組みのため、変更にとても弱いのです。継承元(スーパークラス)から別のものに乗り換えたくなったとして、同一の仕組み(メソッド名・引数・メンバー変数など)を持っていなければ、自分の仕様も大きく変更しなければなりません。委譲であれば自分のクラス自体には変更を加えずに、アクセス方法(委譲)を変更するだけで対応できます。EventEmitterを継承した場合にはどうしてもEventEmitterに引きずられますが、EventEmitter以外のメッセージパッシングの仕組みを使いたい場合、委譲であれば問題なくいくらでも対応できます。密結合よりも疎結合の方が望ましいのです。
また、継承をする場合JavaScriptにはprotectedがありませんが、それがある言語の場合はpublicとprivateとprotectedの切り分けを行う必要があります。それなら委譲を前提にする方が設計がシンプルになると思いませんか?
継承には多重継承という問題もあります。継承ベースで設計する場合は複数のクラスから継承をしたくなりがちですが、多重継承は好ましくないとされてサポートされていない言語も多いのです。この場合よくあるのはmixinという仕組みです。継承よりも少し軽めのモジュールを定義してモジュールを混ぜ込むのです。
継承はスーパークラスの仕様でサブクラスを叩けるという利点もありますが、それはインターフェースに任せるべきです。ECMAScriptはダックタイピングなLL言語のためインターフェースはありませんが、TypeScriptやFlowにはあります。
継承は設計をとても複雑にしがちです。そのため現代では「継承よりも委譲」といわれるようになりました。Javaも困った概念を広めてくれたものです。
(次回に続く)