VIVITABLOG

VIVITAで活躍するメンバーの情報発信サイト

今風なJavaScriptをキャッチアップ

f:id:itamoto:20180903150536p:plain

こんにちは。板本@itamotoです。

少し前まではAndroidのネイティブアプリをJavaで開発することが多かったのですが、 iOSはもちろん、PCでも動くアプリの要件があったことから、Cordovaというオープン開発フレームワークを使ってアプリ開発をはじめました。

元々はWebアプリ開発は経験していたものの業務でやっていた頃から間が空いていたこともあって、 技術的にもキャッチアップが必要な状況でした。 その際に知識としてアップデートできた、JavaScript関連のことをまとめてみようと思います。

ES2015 (ES6)で書く

ES6とはJavaScriptの仕様標準化で策定されたものです。

ES6の各ブラウザ対応表がこちら

これを見る限りはほとんどのブラウザでES6で書いたJavaScriptがそのまま動きそうですが、 少なくとも私が検証したAndroid端末のWebview内ではエラーとなってしまいました。

そこで、ES6以前の構文にトランスパイルするBabelを使ってwebpackで一つの.jsファイルにビルドしたものをアプリでは使うようにします。 その辺の環境設定についてはIonicという開発環境をつかっている人が多いみたいです。

Ionicは言語がTypeScript、フレームワークがAngularJSという開発環境になります。 ES6の標準的なJavaScriptで書きたかったというのと、使い慣れていたVue.jsを利用したかったという理由から、私はIonic利用はしませんでした。

今後、Ionicも他プロジェクトで実際に利用してみて開発効率の比較等はしてみたいと思っています。

ES6の構文いろいろ

今回の本題。
ES6以前と比較して何がどう変わったのか理解しながら復習してみます。
そしてできるだけ違いを完結に説明したいと思います。

JavaScriptをテスト実行できるようなWebサイトで試しながら見ていただけるとわかりやすいかと思います。 Eloquent JavaScript :: Code Sandbox

変数宣言とスコープ

varに加えてES6では
let、constがあります。 スコープがそれぞれvarは関数内。
letとconstはブロック内。

その違いがわかる例として

if(true){
  var name1 = "vivitaro";
  let name2 = "vivijiro";
}
console.log(name1);
console.log(name2);

実行結果は

vivitaro
ReferenceError: name2 is not defined

ifのブロック内のスコープになっているname2はifの外で参照しようとしてもできません。

constについてはletとスコープは一緒で再代入できないというものです。

const names = ["vivitaro","vivijiro","vivisaburo"];
for(const i in names){
    const name = names[i]; 
    console.log(name);
}

多用してしまいましたが、この例ではforブロックの中で一度取得したnameという変数の中身を書き換えたりしないように使っています。 namesの様に定数として宣言する使い方と合わせて、ブロック内で意図せず変数の中身を書き換えてしまったりしないようにできる限りconstを使うように意識しています。

varに関しては値の代入はもちろん同じ変数名でvarで再定義までできてしまったりと、なんでもありなので使用しないようにしています。

アロー関数

通常の関数とアロー関数では JavaScriptのthisの変数の参照先が変わってきます。
言葉では理解しにくい部分なので早速例を。

this.name = "vivitaro";
const normalFunc = function(){
    console.log(this.name);
}

const arrowFunc = () => {
    console.log(this.name);
}

let object = {
  name : "vivijiro",
  normalFunc: normalFunc,
  arrowFunc: arrowFunc
}

object.normalFunc();
object.arrowFunc();

実行結果は

vivijiro
vivitaro

通常の関数では呼び出し元であるobjectの中をthisとしていますが、 アロー関数では関数実行しているスコープにあるthisの変数を参照しています。

ES6構文ではclassを使えるようになっていて、constructorメソッド内でthis変数を結構利用しますので、このアロー関数と通常の関数との違いは嫌でも意識することになりそうです。

class

この流れでclassの説明です。 classを定義できることで、ファイルを分けたモジュール化がすごくやりやすくなりました。 アロー関数のスコープの復習も兼ねてサンプルコードを書いてみました。

class MyClass {
  constructor(name) {
    this.name = name;
  }
  
  nameTest() {
    const normalFunc = function(){
      //console.log(this.name); 
    }
   normalFunc();
    
    const arrowFunc = ()=>{
      console.log(this.name); 
    }
   arrowFunc();
  }
}

const myClass = new MyClass("vivitaro");
myClass.nameTest();

newを使ってインスタンス化して、class内のメソッドを呼び出すというお馴染みの流れです。

個人的に最初慣れなかったのはclass内でのスコープ範囲である変数であるインスタンス変数(メンバ変数)の定義の仕方です。 constructorの中でthis.変数名で定義します。 毎度Javaとの比較で申し訳ないですが、Javaっぽくclass直下に変数を書きたくなってしまいますが、JavaScriptではclass直下には関数のみ定義できる仕様のようです。

また、constructorには引数を指定できるように書いています。 Java感覚だと引数なしのconstructorをオーバーロードして定義したくなってしまうところですが、オーバーロードという言語仕様は無いようです。

そして前の項目で触れたアロー関数内ではthis.nameというインスタンス変数を参照できるのに対し、 通常の関数では参照できません。 スコープの違いがわかります。

Promise、async、awaitで非同期処理の制御

async、awaitはES7の仕様みたいですが細かいことは置いておいて、
早速サンプルコードから

const timeoutMessageLog = (msg) => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      console.log(msg);
      if(msg == "error"){
        reject();
      }else{
        resolve();
      }
    }, 1000);
    
  });
};

async function mainFunc () {
  try {
    await timeoutMessageLog("Call1");  //1秒後にログ表示
    await timeoutMessageLog("Call2");  //2秒後にログ表示
    timeoutMessageLog("Call3_1");  //3秒後にログ表示
    timeoutMessageLog("Call3_2");  //3秒後にログ表示
    await timeoutMessageLog("error");  //Promiseでreject発生させる
    timeoutMessageLog("Call4");  //ここは実行されません
  }catch(e){
    console.log("error発生");
  }
}

mainFunc();

シンプルにしたかったのですが、説明したい要素がここは多くなってしまいました。 mainFunctionの中でtimeoutMessageLogというfunctionをawaitを付けたり付けなかったりして呼び出しています。
timeoutMessageLogの中では呼び出されて1秒後に引数で渡した文字列をログ出力するという処理になっています。

JavaScriptでは関数の呼び出しが並列で処理されてしまうので、awaitをすべて付けずに呼び出した場合には1秒後に全てのログ出力がされるように動作します。

awaitを付けた場合にはその関数の戻り値であるPromiseのresolveまたはrejectまで次の処理へ進まないようなスクリプトにしています。 何かの処理を待って次の処理を変更させたりする場合に必要になってくるかと思います。
また、直前の処理結果のPromiseがrejectを呼べば、例外を発生してそれ以降の処理を行わなくすることができます。

asyncとawaitを使わずにPromiseのみで.then()と.catch()を使った書き方もありますが、 asyncとawaitを利用したほうが可読性としては良いと個人的には思います。

同様のことをcallback関数を引数に入れて行おうとすると深いネストができてしまったりするので2つ以上callbackするような場合は必ずasync、awaitを使うようにしています。

テンプレート文字列

最後に軽くTips的なもの

const name1 = "vivitaro";
const name2 = "vivijiro";
console.log("name1:" + name1 + "\nname2:" + name2);
console.log(
`以下はテンプレート文字列で
name1:${name1}
name2:${name2}`
);

1つ目のログ出力は文字列を+で結合して出力内容を編集しています。 2つ目は文字列テンプレートの仕様を使って ``で括った文字列、その中に${}で括った変数名で出力内容を編集しています。

文字列結合が多い場合だとこの文字列テンプレートで書いたほうが可読性が優れていると思います。 改行も改行文字列ではなく、そのまま改行すれば良いだけです。

開発中には外部のAPIの呼び出し結果を固定のjson文字列で定義しておいたりすることがよくあるのですが、 json文字列も改行やインデントを含んだまま書いておけるので、jsonの特定の値を変更したりするのはすごく楽です。

最後に

ES6以前から比べるとJavaScriptで本格的なアプリ開発をするための言語仕様になった印象でした。

今後もWebアプリ開発は引き続きやっていくつもりですので、自分の中での知識の整理含めてまたJavaScript関連は書いてみようかと思います。