課題解決
INSIGHT
情報・インサイト
JavaエンジニアのためのJavaScript入門(ES5)【後編】
【前回の記事】の続きです
はじめに
これはJavaの既存の知識ベースを元にJavaScriptを体系的に学習する為の記事の後編です。
この記事はES5までのJavaScriptについてまとめています。
それでは始めましょう。
データ処理
配列、JSON、日付、正規表現と行ったデータに関する処理をまとめます。
配列
JavaScript の特徴は以下になります。
- 可変配列
- オブジェクトであり、プロパティや振る舞いを持つ
- 異なる型の値を格納できる
配列の生成
JavaScript の配列は、リテラルまたは new 式で生成できます。
配列の生成
リテラルの場合は、[]で囲み、,区切りで要素をします。
new 式の場合は、Array コンストラクタに引数を渡します。
var array = [0, 1, 2, 3]
typeof array // → 'object'
array.length // → 4
var array2 = [0, 'str', true, function() {}, {}]
array2.length // → 5
// コンストラクタの生成の際は、1つの数値を指定するとその要素数の配列となる
var array3 = new Array(5)
array3 // → [5 empty items]
// 2つ以上の引数を渡すと、それらを要素とした配列となる
var array4 = new Array(0, 3, 5)
array4 // → [0, 3, 5]
配列の要素の列挙
配列の要素の列挙には、for 文を使った以下のイディオムがよく使われます。
Java のイディオムとは異なるため、覚えておいてください
var array = [0, 1, 2]
for (var i = 0, len = array.length; i < len; i++) {
console.log(array[i])
}
// 以下のように出力される
// 0
// 1
// 2
インナーループ
繰り返し処理には、配列の各要素を順に呼び出すメソッドを利用した方法もあります。
これらは、ループ文が裏に隠れるためにインナーループと呼ばれることもあります。
インナーループ系のメソッドに、コールバック関数で処理を渡すことで、要素に対し処理を適用して行きます。
var array = ['x', 'y', 'z']
array.forEach(function(e) { console.log(e) })
インナーループ系のメソッドのコールバック関数には、以下の 3 つの引数が渡ってきます
- 第一引数 e : 要素
- 第二引数 i : インデックス値
- 第三引数 a : 配列オブジェクト
命名に指定はありませんが、上記のようにすると理解がしやすいと思います。
var array = ['zero', 'one', 'two']
array.forEach(function(e, i, a) {
console.log(e, i, a)
})
// 以下のように出力される
// zero 0 [ 'zero', 'one', 'two' ]
// one 1 [ 'zero', 'one', 'two' ]
// two 2 [ 'zero', 'one', 'two' ]
Array オブジェクトのメソッド
配列は、内部的には Array オブジェクトのインスタンスです。
このため、Array オブジェクトのメソッド呼び出しが可能です。
以下にその一覧を列挙しますが、名前から自明のものに関しては説明を省きます。。
操作系
- pop() : 配列の最後尾の要素を削除して取得
- push(e1, e2, …) : 要素を最後尾に追加、複数渡すことも可
- shift() : 配列の先頭の要素を削除して取得
- unshift(e1, e2, …) : 要素を先頭に追加、複数渡すことも可
- splice(start, count, [e1, e2, …]): 開始位置から指定の数の要素を削除後、要素の挿入
- sort(comparator): 配列の要素のソート
- reverse(): 配列を逆順に入れ替える
生成系
- concat(e1, e2, …) : 自身に引数の要素を追加した新しい配列の生成
- slice(start, end) : 引数の範囲の要素を持つ新たな配列の生成
- join(sep) : 配列の要素間に引数のセパレータを挟んだ文字列の生成
探索系
- indexOf(e[, fromIndex])
- lastIndexOf(e[, fromIndex])
インナーループ系
- forEach(callback[, thisArg])
- filter(callback[, thisArg])
- map(callback[, thisArg])
- some(callback[, thisArg])
- every(callback[, thisArg])
- reduce(callback[, initVal])
- reduceRight(callback[, initVal])
配列のイディオムまとめ
var array = ['1', '2', '0']
// 最後尾に要素を追加
array[array.length] = '3'
array // → ['1', '2', '0', '3']
// 数値配列に変換(要素が数値変換可能な場合)
array.map(function(e, i, a) {
a[i] = +e
})
array // → [1, 2, 0, 3]
// 数値配列のソート
array.sort(function(a, b) { return a - b; })
array // → [0, 1, 2, 3]
// 配列を使った文字列生成
var arr = []
arr.push('<div>')
arr.push(Date())
arr.push('</div>')
var s = arr.join('')
s // → '<div>Mon Jul 24 2017 22:41:00 GMT+0900 (JST)</div>'
// 文字列から配列生成
var s = 'Mon Jul 24 2017 22:41:00 GMT+0900 (JST)'
var a = s.split(/s/)
a // → [ 'Mon', 'Jul', '24', '2017', '22:41:00', 'GMT+0900', '(JST)' ]
// 配列のコピー(shallowコピー)
// concatを用いた場合
var arr = [3, 5, 4]
var arrCp = [].concat(arr)
arrCp // → [3, 5, 4]
// sliceを用いた場合
var arr = [3, 5, 4]
var arrCp = arr.slice(0, arr.length)
arrCp // → [3, 5, 4]
JSON
JSON は JavaScript Object Notation の略で、JavaScript のリテラル表記をベースにしたデータフォーマット形式です。
JSON は 4 つの基本形と 2 つの構造化型を表現可能です。
- 基本型
- 文字列値型
- 数値型
- 真偽値型
- null 型
- 構造化型
- オブジェクト型
- 配列型
JSON 文字列と JavaScript オブジェクトの相互変換
JSON 文字列と JavaScript オブジェクトの相互変換には、JSON オブジェクトを用います。
// JSON文字列からJavaScriptオブジェクト
var jsonStr = '{"x": 1, "y": 2, "val": "foobar"}';
var obj = JSON.parse(jsonStr);
obj // → { x: 1, y: 2, val: 'foobar' }
// JavaScriptオブジェクトからJSON文字列
var s = JSON.stringify(obj)
s // → '{"x":1,"y":2,"val":"foobar"}'
不正な形式の JSON 文字列
以下は JSON 文字列としては不正です。
- シングルクォートで囲った文字列
- 文字列でないプロパティ名
// シングルクォートで囲った文字列
var s = JSON.parse("'foo'"); // SyntaxError: JSON.parse
// 文字列でないプロパティ名
var s2 = JSON.parse("{x: 1}") // SyntaxError: JSON.parse
日付処理
日付処理を行う際には、Date クラスを用います。
var date = new Date() // 引数なしのコンストラクタで現在時刻のインスタンス生成
date // → 2017-07-24T14:12:14.415Z
// 日の数値を返す
date.getDate() // → 24
// 0を日曜とする曜日を返す
date.getDay() // → 1
// 年を設定する
date.setFullYear(2018) // → 1532441534415
// 文字列へ変換する
date.toDateString() → 'Tue Jul 24 2018'
date.toString() // → 'Tue Jul 24 2018 23:12:14 GMT+0900 (JST)'
日付処理は 4 つの形式間の相互変換
内部的には、時刻は基準時からの経過 ms の整数です。
エポックと呼ばれる基準時は GMT の 1970 年 1 月 1 日の 0 時 0 分とされています。
このことから、基準時からの経過時間をエポック値と呼びます。
Date クラスは、この値を隠蔽し、利用しやすくします。
4 つの形式
- エポック値: データベースの格納値、経過時間の計算
- Date クラス: JavaScript での内部表現、月、週処理、曜日判定
- 文字列: 利用者への表示、利用者からの入力値
- 年月日等の数値: 利用者への表示、利用者からの入力値
正規表現
JavaScript における正規表現は、RegExp オブジェクトを用いて利用します。
正規表現オブジェクトの利用
// コンストラクタによる生成
var regExp1 = new RegExp('^[0-9]')
regExp.test('foo') // → false
regExp.test('123Bar') // → true
// リテラルによる生成
var regExp2 = /^[0-9]/
regExp2.constructor === RegExp // → true
regExp2.test('123Foo') // → true
regExp2.exec('123') // → [ '1', index: 0, input: '123' ]
正規表現のフラグ
正規表現のフラグは、コンストラクタによる生成の場合は第二引数に蒸します。
リテラルの倍は、2 つ目の/の後ろに指定します
- g: グローバルマッチ
- i: 英字の大文字小文字を無視
- m: マルチライン。^と$がそれぞれ改行の先頭や末尾にもマッチするようになる
// フラグありの正規表現
var regExp1 = new RegExp('^\s+', 'g')
regExp1.test(' 123 asdf') // → true
var regExp2 = /^s[abc]/gi
regExp2.test(' A') // → true
文字列と正規表現オブジェクト
文字列オブジェクトには正規表現オブジェクトを引数にとるメソッドがいくつかあります。
- match(regexp): 正規表現のマッチ結果を返す
- replace(searchVal, replaceVal): 正規表現または文字列値で一致したものを置換する
- search(regexp): 正規表現がマッチした位置の index を返す
- split(seps, limit): 文字列または正規表現で分割を行い、配列を返す
関数
JavaScript における関数は単にクラスの持ち物であることを意味しません。
JavaScript の関数には以下の特徴があります。
- 関数はオブジェクトです。
- 関数は入れ子にして宣言することが可能です。
- 関数は独自のスコープを持ち、呼び出されるコンテキストによって挙動を変えます。
関数呼び出し
JavaScript における関数呼び出しは()演算子を使うことで行えます。
しかし、それらの挙動はコンテキストによって微妙に異なります。
以下に、関数呼び出しのコンテキストを列挙します
- メソッド呼び出し: レシーバオブジェクトを使った関数呼び出し
- コンストラクタ呼び出し: new 式での関数呼び出し
- 関数呼び出し: 上記 2 つ以外の関数呼び出し
これらは全て関数の呼び出しの分類であり、関数自体の分類ではありません。
しかし便宜上、それらの機能を指してメソッドやコンストラクタと言った名称を使うこともあります。
実引数と仮引数
JavaScript の関数では、宣言時に定義された引数を仮引数、実行時に渡された引数を実引数と呼びます。
関数の仮引数のことをパラメータ変数と呼ぶこともあります。
実引数と仮引数を区別することで、宣言時に定義されていない引数を呼び出しの際に渡すこともできることを意味します。
JavaScript には Java におけるメソッドのオーバロードのような機能はありませんが、実引数をうまく使うことで同様の機能は実現できます。
arguments オブジェクト
実引数を取得したい場合には arguments オブジェクトを利用します。
arguments.length で、実引数の数を取得できるため、例えば可変長引数を受け取る関数を書けます。
[]演算子で、0 から始まる index を指定することで、引数を取得できます。
arguments オブジェクトは、配列のように扱うことができますが、Array オブジェクトではないため、そのメソッドを使うことはできません。
function printArgs() {
console.log(arguments.length, arugments[0], arugments[1], arugments[2])
}
printArgs(0) // 1 0 undefined undefined
printArgs(0, 1) // 2 0 1 undefined
printArgs(0, 1, 2) // 3 0 1 2
再帰関数
再帰関数とは内部で自分自身を呼び出す関数のことです。
このような処理を一般に再帰処理や再帰呼び出しと呼びます。
有名な例である、階乗の計算を以下に示します。
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
再帰関数が常に自分自身を呼び続けた場合、実行が終わりません。
ループから抜けることがない処理を無限ループと呼びますが、再帰の場合も同様に無限再帰と呼びます。
JavaScript で無限再帰をした場合の挙動は環境依存となります。
再帰関数の停止条件
再帰関数には、再帰処理を停止する条件判定が必ず必要となります。
これを停止条件と呼びます。
階乗の計算の例では、引数が n の値が 1 以下かどうかを判定している部分が停止条件に当たります。
停止条件のコードは、先頭に書くことでコードの見通しがよくなるため、そうすると良いです。
ループ処理と再帰処理の対応
ループで書ける処理は必ず再帰で書くこともでき、その逆も真です。
しかし多くの場合は、ループで書く方が平易なコードになります。
JavaScript における再帰は必ずしも効率的に動作するとは限らないため、避ける方が無難です。
スコープ
JavaScript のスコープには、以下の特徴があります。
- 名前の探索はスコープの小さい方から大きい方へと行われる。
- 宣言された名前はスコープの範囲内で有効
- スコープ内の変数であっても、代入が実行されるまでは undefined 値となる
スコープの種類
JavaScript のスコープの種類は以下の 2 つになります
- グローバルスコープ: 関数の外(トップレベル)のスコープ
- 関数スコープ: 関数の内側のスコープ
関数内部で宣言した名前は関数スコープとなり、その関数内でのみ名前が有効となります。
なお、関数の仮引数であるパラメータ変数も関数スコープとなります。
ローカルスコープとローカル変数
グローバルスコープとの対比で関数スコープのことをローカルスコープと読んだり、
グローバル変数との対比で関数スコープ内の変数をローカル変数と呼ぶこともあります。
Java のローカルスコープと関数スコープの違い
関数スコープの動作は、Java のローカルスコープとは微妙に挙動が異なります。
Java のメソッドでは、ローカル変数は宣言した行以降のスコープを持ちます。
JavaScript の関数スコープは、宣言した行とは無関係です。
以下に例を示します。
var x = 1
function func() {
console.log('x = ' + x);
var x = 2
console.log('x = ' + x)
}
func()
// 上記のfuncの呼び出しは、以下のように出力する
// x = undefined
// x = 2
関数 func の最初の console.log はグローバル変数 x を表示するように見えます。
しかし実際には、変数 x は次行で宣言しているローカル変数 x となります。
なぜなら、ローカル変数 x のスコープは関数 func 内の全域となるためです。
最初の console.logx の呼び出しの時点では、この時点では代入は行われていないため、変数 x の値は undefined となります。
つまり、JavaScript では、先ほどの関数 func は以下の関数 func と等価になります
function func() {
var x
console.log('x = ' + x);
x = 2
console.log('x = ' + x)
}
このため、JavaScript ではローカル変数は関数の先頭でまとめて宣言することが推奨されます。
Java における、変数は使う直前で宣言すべきであると言う推奨とは異なるため、注意が必要です。
なぜこのような挙動になるのか
JavaScript においては、名前の参照は内側のスコープから外側のスコープへと探索を行います。
そこで宣言が発見された時点で、その名前の参照が取得できると言うわけです。
変数だけに限って考えるとピンときませんが、名前が関数を指す場合を想像してみてください。
宣言済みの関数の呼び出しの際の名前の探索は、下から上というよりは内から外への探索であって欲しいと思います。
そうでなければ、関数は全て呼び出し前に宣言する必要があるからです。
名前の探索において一貫してその挙動をすると考えれば、それほどわかりづらい挙動ではありません。
Web ブラウザとスコープ
クライアントサイド JavaScript では、各ウィンドウ(タブ)、各フレーム(iframe 含む)ごとにグローバルスコープがあります。
そのため、window 間で相互のグローバルスコープにアクセスはできません。
フレームに関しては親とフレームの間で相互にアクセス可能です。
ブロックスコープ
JavaScript では var をつけたローカル変数とつけないグローバル変数の 2 つを使用する場合においては、ブロックスコープが存在しません。
シャドーイング
シャドーイングとは、スコープの小さい同名の変数によって、スコープの大きい変数を隠すことを指し、多くは意図せずに起きてバグの原因になります。
関数オブジェクト
関数はオブジェクトです。
そのため Function コンストラクタを使って関数を生成することも出来ます。
しかし、通常は Function コンストラクタを使うことはありません。
var sum = Function('a', 'b', 'return a + b')
var div = new Function('a', 'b', 'return a - b')
sum(3, 5) // → 8
div(3, 5) // → -2
明示的に Function コンストラクタによって関数を生成しなくとも、関数は内部的には Function オブジェクトをプロトタイプ継承しています。
これは次にのように counstructor プロパティで確認することが出来ます。
function func() {}
func.constructor // → [Function: Function]
func.constructor === Function // → true
func.__proto__ === Function.prototype // → true
入れ子の関数宣言とクロージャ
誤解を恐れずに端的にいうとクロージャは状態を持つ関数です。
以下のコードはクロージャの一例です。
function closure() {
var cnt = 0;
return function() { return cnt++ }
}
var fn = closure()
fn() // → 0
fn() // → 1
fn() // → 2
クロージャの勘所は関数を抜けた後も生きているローカル変数です。
上記例の場合、ローカル変数 cnt が closure 関数の呼び出し後も生きています。
クロージャの仕組み
JavaScript では、関数を入れ子にして記述することができます。
関数宣言の中に関数宣言をすることが出来ることがクロージャの前提となります。
入れ子の関数
function f() {
function g() {
console.log('hello');
}
g()
}
f() // 'hello'と出力する
上記の例では、関数 f の中に関数 g の宣言と呼び出し行があります。
直感通り、関数 f を呼び出すと間接的に関数 g が呼ばれますが、以下にその動作の内容を説明します。
入れ子の関数の挙動
関数 f の宣言は、関数オブジェクトを生成後、変数 f にその実体を格納します。
トップレベルでの宣言のため、変数 f はグローバルオブジェクトのプロパティです。
JavaScript では関数を呼ぶたびに、Call オブジェクトが生成されます。
Call オブジェクトは、関数の呼び出しが終了すると消滅します。
以降は説明のため、関数 f を呼び出す Call オブジェクトを、Call-f オブジェクトと呼びます。
関数 f 内の関数 g の宣言は、関数 g の実体となる関数オブジェクトを生成します。
そして変数 g にその実体を格納します。
関数 f 内での宣言のため、変数 g は Call-f オブジェクトのプロパティとなっています。
Call オブジェクトは関数呼び出しのたびに独立して生成されます。
よって関数 g を呼び出すと対応する Call オブジェクトが生成されます。
これを Call-g オブジェクトと呼ぶことにします。
関数 g を抜けた時、Call-g オブジェクトは自動で消滅します。
関数 f を抜けた時、Call-f オブジェクトも同様に消滅します。
この時、変数 g が参照していた関数の実体は、ガベージコレクションされます。
唯一残っていた Call-f オブジェクトのプロパティ g からの参照がなくなったためです。
入れ子の関数とスコープ
では、入れ子の関数とスコープの関係を見ていきます。
function f() {
var s = 'hello'
function g() {
console.log(s);
}
g()
}
f() // 'hello'と出力する
これも直感通りの挙動をします。
入れ子に宣言した関数 g は外側の関数 f のローカル変数 s にアクセス可能だと考えられるからです。
関数内での変数名の解決
関数内での変数名の解決は、以下の優先順位を持っています。
- Call オブジェクトのプロパティ
- グローバルオブジェクトのプロパティ
入れ子の関数宣言をした場合は、内側の関数は自身の Call オブジェクトのプロパティを探索した後に、外側の関数の Call オブジェクトのプロパティを探索します。
この仕組みのことを、スコープチェーンと呼びます。
このスコープチェーンのために、入れ子の内側の関数オブジェクトは、外側の関数オブジェクトの Call オブジェクトの参照を保持します。
入れ子の関数を返す
では、外側の関数のローカル変数を参照している内側の関数を返した場合はどのような挙動をするでしょうか。
function f() {
var s = 'hello'
function g() {
console.log(s);
}
return g
}
f() // → [Function: g]
return g により、関数 f は関数の参照を返します。
つまり関数 f の呼び出し結果は関数オブジェクトとなります。
この時、関数 f に対応する Call-f オブジェクトは生成されますが、関数 g の呼び出しは発生していないために、関数 g に対応する Call-g オブジェクトは生成されていません。
クロージャを作る
上記例での関数 f を変数に代入してみます。
関数 f の返り値は関数 g であるため、returned g ということで変数 rg とします。
そして変数 rg に対し関数呼び出しを行ってみます。
function f() {
var s = 'hello'
function g() {
console.log(s);
}
return g
}
var rg = f()
rg() // → 'hello'と出力する
結果を見ると、関数 g を関数 f の外側から呼べたということになります。
さらに関数 f のローカル変数 s が関数 f の呼び出し後もまだ生きていることになります。
これは Java などの手続き型言語の常識に反していると思います。
通常、ある関数のローカル変数はその関数を抜けた時点で消滅するからです。
クロージャにおけるガベージコレクションの挙動
ガベージコレクションは、参照元が無くなった値を消滅させます。
最後の例で、関数 f を読んだ際の Call-f オブジェクトのプロパティ g には f の返り値である関数の実体が格納されています。
その関数の実体は、関数 f の呼び出しの終了後も変数 rg が参照し続けます。
変数 rg が有効な限り、関数オブジェクトはガベージコレクションの対象にはなりません。
そして、変数 rg に格納されている関数オブジェクトは、Call-f オブジェクトへの参照を持っています。
なぜなら、それがスコープチェーンのために必要だからです。
と言うことで、Call-f オブジェクトもガベージコレクションの対象にはなりません。
当然、Call-f オブジェクトがプロパティとして保持する関数 f のローカル変数 s も、対象にはなりません。
これが、関数 f を抜けたのちもローカル変数 s が生きている理由となります。
クロージャの複数回の呼び出し
関数 f を複数回呼び出した時、取得できる関数オブジェクトはそれぞれ異なります。
そしてそれらの関数オブジェクトは、それぞれ異なる Call-f オブジェクトを参照します。
なぜなら、Call オブジェクトは関数呼び出しごとに生成されるオブジェクトだからです。
取得できる関数オブジェクトが異なることがわかるように、以下のようにクロージャを変更してみます。
function greeting(name) {
var msg = 'hello ' + name + '.'
function g() {
console.log(msg);
}
return g
}
var greeting1 = greeting('anthony')
var greeting2 = greeting('john')
greeting1() // → 'hello anthony'と出力する
greeting2() // → 'hello john'と出力する
このように greeting1 と greeting2 の呼び出し結果は異なります。
これは同じコードから異なる状態を持つ関数を作れたことを意味します。
専門用語的にいうと、関数呼び出し時点における変数名解決の環境を保持した関数。となります。
これがクロージャです。
しかし、一般的には状態を持つ関数として理解していれば良いと思います。
クロージャのイディオム
クロージャのイディオムとして、以下のように関数リテラル式をそのまま return 文に書くことも多いので、覚えておきましょう。
function greeting(name) {
var msg = 'hello ' + name + '.'
return function() {
console.log(msg);
}
}
クロージャの応用
クロージャが状態を持つ関数であり、どのような挙動によって実現されており、どのように作るかについては理解できたと思います。
しかし、これを使う場面やメリットについては、まだイメージがわかないと思います。
クロージャの特徴
まず、クロージャの特徴を整理します。
- クロージャは状態を持つ関数オブジェクト
- クロージャの状態は作成時に設定できる
- クロージャの状態は不可視となる
JavaScript におけるオブジェクトは、基本的にプロパティが可視です。
対してクロージャは、不可視のプロパティを持てます。
そのため、クロージャを用いることで情報隠蔽を行い、スコープを狭めることができます。
クロージャの活用場面
上記の特徴を考えると、代表的なクロージャの活用場面は以下のようになります。
- モジュールを作りたい場合
- プロパティのアクセス制御を行いたい場合
モジュールとしてのクロージャ
じゃんけんの手を渡した時に、それに必ず勝つ手を返す関数を持つモジュールを考えます。
以下がその仕様です。
- じゃんけんの手は、文字列[‘グー’, ‘チョキ’, ‘パー’]で表す。
- 与えられた手に勝つ手を返す関数 win を作成する
- 不正な手の場合は文字列’そうかそうか君はそんなやつだったんだな’を返す
var rspModule = (function(x, y) {
var choise = {r: 'グー', s: 'チョキ', p: 'パー'}
var errorMsg = 'そうかそうか君はそんなやつだったんだな'
return {
win: function(input) {
switch(input) {
case choise.r: return choise.p
case choise.s: return choise.r
case choise.p: return choise.s
default: return errorMsg
}
}
}
})()
m.win('グー') // → 'パー'
m.win('チョキ') // → 'グー'
m.win('パー') // → 'チョキ'
m.win('ピストル') // → 'そうかそうか君はそんなやつだったんだな'
// モジュール自体にはプロパティが存在しないためアクセスできない
m.choise // → undefined
プロパティのアクセス制御を行う
Java におけるプロパティのアクセス制御と同等のことは、クロージャの特性を用いて実現することができます。
function counter(init) {
// 変数cntは、Javaにおけるprivateフィールド相当
// initがundefined、つまり引数が渡されなかった場合に0で初期化するidiom
var cnt = init || 0
return {
// 以下のプロパティはpublicメソッド相当
show: function() { console.log(cnt) },
up: function() { cnt++; return this },
down: function() {cnt--; return this }
}
}
var c = counter();
c.show() // → 0
c.up().show() // → 1
c.down().show() // → 0
例外処理
例外とは、予期せぬ事態に対応するための機構です。
エラーのないソフトウェアを開発したいと望むのは、全ての開発者にとって当たり前のことだと思います。
そのために、あらゆる場合を想定して処理フローを記述していきます。
しかし、ソフトウェアには予期せぬ事態が付き物であるため、予期せぬ事態を予期してあらかじめ代替パスを用意しておく必要があります。
それが、例外処理の機構です。
Error オブジェクト
Error オブジェクトは、エラー処理に利用することができます。
Error のインスタンス生成時には、エラーメッセージを指定することができます。
Error オブジェクトのプロパティ
- message: エラーメッセージを格納する
- stack: エラー発生までのスタックのトレース
try…catch
例外処理には、try…catch を用います。
何かを try して、それが例外を引き起こした時に catch によってその例外が捕捉されます。
try ブロックの内部でエラーが起こると、それ以降の文は実行されずに制御は catch ブロックに移ります。
catch ブロックへはエラーオブジェクトが渡されるため、任意の例外処理を行うことができます。
message や stack を出力することで、エラー発生時の状況の手がかりが掴めるため、デバッグが行いやすくなります。
throw
JavaScript の処理系が例外を発生させる場合もありますが、プログラマが例外を明示的に throw することもできます。
throw できるのは任意の値ですが、Error オブジェクトを throw することが一般的です。
自分が throw した例外を他の人が処理する場合もあり得るので、慣習に従いましょう。
サンプルコード
function validateEmail(email) {
return email.match(/@/);
}
try {
const email = 'これはメールアドレスです';
if (!validateEmail(email)) {
throw new Error(`メールアドレスが不正です: ${email}`);
// 例外がthrowされた場合、以降の文は実行されない
}
console.log(`メールアドレスは正常です: ${email}`);
} catch(err) {
console.error(`エラー: ${err.message}`);
}
// 実行結果 => エラー: メールアドレスが不正です: これはメールアドレスです
コールスタック
プログラムが大きくなると、関数の呼び出し階層はどんどん深くなっていきます。
JavaScript の処理系は、これらの関数の呼び出し情報をコールスタックと呼ばれる領域に保存しています。
関数a -> 関数b -> 関数cと関数呼び出しが行われている時に関数 c でエラーが起こると、このエラーは呼び出し階層を逆に辿るように、関数c -> 関数b -> 関数aと伝搬します。
エラーは、コールスタックのどの段階でも捕捉することができますが、エラーが捕捉されない場合、JavaScript 処理系は停止します。
これは、処理されない例外または、キャッチされない例外と呼ばれます。
エラーがキャッチされた場合、コールスタックがどこで問題が起こったかを示唆する情報を保持しています。
例えば上記の例では、エラーが関数 c で起こったということだけでなく、関数 a に呼ばれた関数 b に呼び出された場合にエラーが起こったこともわかります。
このような情報はデバッグの際に役立ちます。
Error オブジェクトの stack プロパティにはこのコールスタックの文字列表現が格納されています。
サンプルコード
function a() {
console.log('a: bを呼び出す前');
b();
console.log('a: 終了');
}
function b() {
console.log('b: cを呼び出す前');
c();
console.log('b: 終了');
}
function c() {
console.log('c: エラーをスローする');
throw new Error('c error');
console.log('c: 終了');
}
function d() {
console.log('d: cを呼び出す前');
c();
console.log('d: 終了');
}
try {
a();
} catch(err) {
console.log('--- a呼び出し後のerr.stack ---')
console.log(err.stack);
console.log('--- 終わり ---');
}
try {
d();
} catch(err) {
console.log('--- d呼び出し後のerr.stack');
console.log(err.stack);
console.log('--- 終わり ---');
}
/** 処理結果(一部省略) →
a: bを呼び出す前
b: cを呼び出す前
c: エラーをスローする
--- a呼び出し後のerr.stack ---
Error: c error
at c (stackTrace.js:15:9)
at b (stackTrace.js:9:3)
at a (stackTrace.js:3:3)
--- 終わり ---
d: cを呼び出す前
c: エラーをスローする
--- d呼び出し後のerr.stack
Error: c error
at c (stackTrace.js:15:9)
at d (stackTrace.js:21:3)
--- 終わり ---
*/
try…catch…finally
try ブロックにて何らかのリソースを扱う場合、それを必ず解放する必要があります。
try…catch では、これを保証することができません。
なぜなら、try ブロックに解放処理を書いた場合、何らかの例外により実行されない可能性があります。
また、catch ブロックに解放処理を書いた場合、例外が発生しない限り解放されません。
けれども、finally ブロックにて解放処理を書くことで、例外が発生しない場合も、発生した場合も必ず実行されることを保証することができます。
try {
console.log("1行目実行中");
throw new Error("エラー");
console.log("例外がthrowされると、この行は実行されない");
} catch(err) {
console.log("エラーが起こった");
console.log(err.stack);
} finally {
console.log("finally中のこの文はいつも実行される");
console.log("リソースの解放をここで行う");
}
/** 実行結果
1行目実行中
エラーが起こった
Error: エラー
...
finally中のこの文はいつも実行される
リソースの解放をここで行う
*/
エラーを throw している箇所をコメントアウトしても、finally ブロックは実行されます。
まとめ
今回は関数などの知識についてまとめました。
モダンなJSの開発では、ES5を直接書くことはなく、ES6やAltJSなどを使うことは多いです。
ただし、それらはトランスパイルやコンパイルされることで、ES5以前のJSになります。
それに、ES6やAltJSにも、JavaScriptから本質的な言語としての特徴は受け継がれていますので、ぜひ学んでみてください。
☆☆ウィズテクノロジーでは、大阪、東京を中心としたシステム開発を行っております
システムのご相談はもとより、一緒に働くエンジニアもお待ちしております。☆☆