[Work/Class/ES2015で動的Webアプリ/ES2015Basic]

ES2015の基本 - 変数とそのスコープ,関数定義,Promiseによるコールバック / 2017-07-27 (木)

'use strict';

ソースコードの初めに'usr strict';を実行しておくと,ES2015以降に厳密なモードになり,間違って宣言時にグローバル変数になってしまったり等のミスを防ぐことができる.

今まで(つまり'use strict';を実行せずに)関数内でvar等を付けずにいきなり変数名が登場すると,それはグローバル変数の宣言と見なされていた.このようなミスがなくなる.

関数宣言内部で'use strict';を実行して,効果を関数内に限定することもできるが,上記のようなミスを防ぐためにソースコード全体で'use strict';を有効にするべきである.

変数

varとlet, const

今までのJavaScriptでは,変数を宣言する時にvar a = 1;のように宣言し,初期化してきた.ES2015ではこのほかに,let宣言とconst宣言がある.

varとletのスコープ

varのスコープは,グローバル(window, global)→クラス(オブジェクトインスタンス)→関数まで,である(PythonやRubyと同じ).

letのスコープは,グローバル→クラス→関数→ブロックまで可能である(C言語やJavaと同じ).すなわちletはvarの完全上位互換宣言である.

特に今までのJavaScriptではfor文を回す時に変数のスコープを気にしなければならなかった(逆にそれを使ったテクニックもある)が,for1回分のブロック内部でletで宣言してしまえば,ブロックの外への影響は全くなくなる.

const

constはC言語と同じく,一度宣言と同時に初期化してしまうと値を変更できない変数,つまり定数として使える.2017年現在,ES2015のみならず全ての言語で,可能なものは全てconst宣言する習慣が広まりつつある.

グローバルスコープ = window(global)インスタンス

ブラウザ内で実行されるJavaScriptは全てwindowというオブジェクトインスタンスの中で動作している.(Node.jsはwindowの代わりにglobalという名前になる)

つまり「グローバル変数」に見えていても,実際はwindowオブジェクトインスタンス内の変数であるといえ,window.変数名でアクセスすることができる.

関数の定義

ES2015での関数の定義は,以下の3つの方法がある.

関数名でのアクセス(静的関数定義と同じやり方)

function 関数名(引数){
    //処理内容
    return 戻り値
}

このように関数定義した場合,

let 戻り値を受け取る変数 = 関数名(引数);

という形で呼び出す.

関数を変数に打ち込む

JavaScript(Pythonも可能だが)は第一級関数言語と呼ばれる関数型言語である.関数そのものをオブジェクトインスタンスのように取り扱うことができる.

C言語ではあくまで関数は静的に存在するものでしかなく,Javaではクラス内に静的に存在するものと,クラスのオブジェクトインスタンス内部に存在することしかできない.

イメージとしてはJavaの関数一つだけを含んでいるオブジェクトインスタンスのようなものである.これを関数オブジェクトと呼称する.(関数インスタンスとは呼称しない)

let myFunction = function(引数){ /* 処理内容 */ };

という形で変数に代入され,

let 戻り値を受け取る変数 = myFunction(引数);

という式で実行できる.

ついでに「関数オブジェクトはFunctionクラスのオブジェクトインスタンス」であるので,Functionクラスのコンストラクタの引数に,引数名と処理内容を渡すことで関数オブジェクトを生成する.

let myFucntion = new Function("引数名", "{処理内容}");

この場合上記のように書く.引数名や処理内容がダブルクオーテーションで囲まれることに注意(しかしこれは,引数や処理内容そのものを文字列として扱えることも示している).内部的にはこの記法は他の記法とは結構違いがあり,あまり積極的に使うことはない.

アロー記法

ES2015では,アロー記法と言われる関数オブジェクトの生成手法が推奨されている(ただし古いバージョンのOSXのSafariではサポートされていないので注意すること).

以下のように書く.

let myFunction = (引数) => 戻り値;

何を言っているかわからねーと思うが(以下略,具体的には以下の通りである.

従来のfunctionキーワードを使った関数オブジェクト生成の記法では,

let addTwoValue = function(x, y){
    return x + y;
};

と書くべきところを,

let addTwoValue = (x, y) => {
    return x + y;
};

つまり,(引数) => {関数の処理}と書くのがアロー記法である.

処理内容が戻り値を返すreturnの1行の時は,さらに中括弧とreturnキーワードを省略して,

let addTwoValue = (x, y) => x + y;

と書くことができる,という具合である.

クラス指定の所で出てくるが,クラスを使うプログラムの時のコールバック関数に設定する関数オブジェクトの生成手法としては,このアロー記法が推奨される.

関数の引数のデフォルト値

オーバーロードと同じような感じで,引数を省略して関数を実行した場合使われる「デフォルトの値」を関数定義時に与えることができる.

例えば,

function myFunction(a = 3, b = 4, c = 5){
    return a + b + c;
}

という関数定義があった時myFunction(10);と呼び出すと,aには実際に指定した10,bにはデフォルト値の4,cにはデフォルト値の5が自動的に入り,関数は19をreturnする.

window.onload関数とwindow.onunload関数

前述の「グローバル環境そのもの」であるオブジェクトインスタンス,windowは当然ながら変数だけでなく関数も持つことができる.一番よく使われるのが,window.onload関数とwindow.onunload関数である.(正確には次項で出てくるプロパティである.onloadプロパティに関数を登録する,と考える)

window.onload = function(){
    //その画面が最初に開かれてHTMLやJavaScript全体の読み込みが終了した時に実行される処理内容
};
window.onunload = function(){
    //その画面が閉じられたり,他のページへ遷移したりする時に呼ばれる処理内容
};

である.windowオブジェクトインスタンスは元々これらの関数(正確には関数オブジェクト)を持っていることが前提なので,これらの指定はいわゆるオーバーライドと同じ概念として捉えることができる.(windowオブジェクトが元々持っているonloadイベントハンドラ関数をオーバーライドしている,と考えると良い)

関数コールバックとPromise

関数コールバック

関数コールバックとは,「この処理が発生,終了,成功した後に,この関数の処理を実行してね」と,次に処理する内容を関数としてまとめておいて,指定することである.

今までイベント関数と呼んできた「とあるイベントが起こったら,この関数を実行せよ」というものは,だいたいこのコールバック関数である.前述のwindow.onload = function(){};, window.onunload = function(){};もコールバック関数の指定である.

例えばProcessingならsetup関数やdraw関数など「その名前をつけた関数を定義(オーバーライド)しておけば,自動的に呼ばれる」関数,Javaで使われるActionListenerインタフェースのactionPerformed関数はコールバック関数であり,SwingのGUI部品にそれを指定するaddActionListener関数は「コールバック関数を指定」する手続きのことである.

PythonではHandlerという名前がつけられることが多い.

コールバック地獄による可読性の低下

JavaScriptは前述の通り「関数オブジェクト」が存在し,またlamda関数の自由度がPythonやJava等に比べ非常に高いため,コールバック関数をその場で定義して渡すことが非常に多く,それを前提にしたライブラリの設計も多い.その結果,コールバック関数の指定にさらにコールバック関数が入ると言う多重の呼び出しの指定が何重にも起こる,通称コールバック地獄に陥ることが多かった.

特にAJAXの処理をする中では,処理の内容としてコールバック地獄は必要不可欠なのだが,コールバック地獄はコードの可読性が非常に下がってしまう.

Promiseクラスとthen, catchキーワード

そこでES2015で標準化されたPromiseというクラスと,then, catchキーワードを使い,可読性に優れたコールバック地獄を書くことができる.

例えばグローバル(window, global)のインスタンスが持っているsetTimeout関数を例に取る.これは第一引数に指定した関数を,第2引数に指定したミリ秒後に,実行する関数である.

このsetTimeout関数の第一引数に指定した関数の中で,resolve(引数);関数と,reject(引数);関数を,それぞれ実行しておく.

その関数をPromiseクラスのコンストラクタに与える,その時引数の指定をresolveキーワードとrejectキーワードにしておく.

生成したPromiseクラスのインスタンスのメンバとして追加するthen関数の最初のブロックに「成功した時に実行する処理」,それ続けるcatchブロックに「失敗した時に実行する処理」をそれぞれ指定しておくと,成功した時,失敗した時にそれぞれ処理を行うことができる.

// Promiseクラスのインスタンスを作る
let myPromise = new Promise(function(resolve, reject){
    // 実際はここで何らかの処理を行う
    window.setTimeout(function(){ 
        if(/* 上で行った処理の結果が */ true){
           resolve('成功したよ!'); //成功した時に呼ばれる関数に渡したい引数を指定する.
	   //ここでは引数は文字列.
	}
	else{
	   reject('失敗したよ!'); //失敗した時に呼ばれる関数に渡したい引数を指定する.
	   // setTimeout内部で失敗することはないので,if(true)にしてあるが,
	   // 実際のコードでは,何らかの処理を行い,
	   // その処理が成功か失敗かを適切に判別するための条件式を書いて,
	   // 成功した時にresolve()を実行し,失敗した時にreject()を実行する.
	}
     }, 1000); //1000ミリ秒待つ
});

// Promiseクラスのインスタンスにthenプロパティを追加して,
// 成功した時と失敗した時の関数を登録する.(実は既に実行されているのだが……)
myPromise.then(function(result) {
    // 成功した時に呼ばれる関数を作る.
    // Promiseのインスタンスを作る時にresolveに指定した引数が入ってくる.
    alert('1秒待つのに成功した.引数:' + result);
}).catch(function(err) {
    // 失敗した時に呼ばれる関数を作る.
    // Promiseのインスタンスを作る時にrejectに指定した引数が入ってくる.
    alert('1秒待つのに失敗した.引数:' + err);
});

多段化は以下のように,resultを引数にとってPromiseクラスのインスタンスを返す関数をあらかじめ定義しておき,thenでそれを実行するように指定すると,可読性が非常に上がる.

let multiPromiseFunction1 = function(){
    return new Promise(function(resolve, reject){
	window.setTimeout(function(){
	    if(true){
	        resolve('最初のsetTimeoutが成功したよ!');
	    }
	    else{
	        reject('最初のsetTimeoutが失敗したよ!');
	    }
	}, 1000);
    });
}

let multiPromiseFunction2 = function(result){
    return new Promise(function(resolve, reject){
	window.setTimeout(function(){
	    console.log(result);
	    if(true){
	         resolve('2番目のsetTimeoutが成功したよ!');
	    }
	    else{
	         reject('2番目のsetTimeoutが失敗したよ!');
	    }
	}, 1000);
    });
}

let multiPromiseFunction3 = function(result){
    return new Promise(function(resolve, reject){
	window.setTimeout(function(){
	    console.log(result);
	    if(true){
	         resolve('3番目のsetTimeoutが成功したよ!');
	    }
	    else{
	         reject('3番目のsetTimeoutが失敗したよ!');
	    }
	}, 1000);
    });
}

let multiPromiseFunctionFinalSuceeded = function(result){
    console.log(result);
};

let multiPromiseFunctionErrorHandler = function(err){
    console.log(err);
};

multiPromiseFunction1()
    .then(multiPromiseFunction2)
    .then(multiPromiseFunction3)
    .then(multiPromiseFunctionFinalSuceeded)
    .catch(multiPromiseFunctionErrorHandler);

変数の型……JSDocの記述の仕方

コメント中のノーテーションに記述する

「JavaScriptの変数にはintやfloatなどの型がない」などと言われることもあるが,実際には存在していて,ユーザに見えないだけである.この「ユーザに見えない」という点が複数人数開発の時には非常に問題になるため,可視化することができる.それがコメント中のノーテーションで型を指定することである.

一般的にコメントは/* コメントコメント */であるが,このノーテーションを使う時には,/** コメント */とコメント開始時のアスタリスクが2つになる.

このコメント中のノーテーションは束縛するほどの効果はないが,自分以外の人間がコードを触る時にもわかりやすく,かつエディタなどで勝手に変数リストを作ってくれて候補を上げてくれるようになる(JSDoc)というメリットがある.

アロー関数などの時は難しいが,次に出てくるクラスの中のfunction定義などでは積極的に使うこと.

変数宣言の型指定

変数の役割や意味のコメントと,@type {変数の型}を書く.

/** 
 * この変数はX座標を保持する変数です
 * @type {number}
 */
let xPos = 235;

関数宣言の引数の型指定

関数の役割や戻り値,@param {引数の型}を書く.

/**
 * この関数は,x座標を減少させる関数です
 * 戻り値は減少した後のx座標です
 * @param {number} decreseWidth X座標を減少させる幅です
 */
function decreseXPosition(decreaseWidth) {
    return xPos - decreaseWidth;
}