2014.12.04

既存の Javascript のコードを ECMAScript 6 で書き換えてみる


こんにちは。社内で開発をしている D.M. です。今回は既存の Javascript のコードを ECMAScript 6 で書き換えてみようというお話です。

ECMAScript 6 とは


ECMAScript 6 は次期バージョンの Javascript です。現状の Javascript は ECMAScript 5 というバージョンで、2009年に策定されたものであり、ECMAScript 6 はその次のバージョンになります。
現在仕様策定中ですが、新しい機能と各ブラウザの実装状況は以下のページが非常に詳細な一覧を公開しています。対応しているものは大きく分けて、コンパイラ、ブラウザ、サーバサイドのJSがあります。
http://kangax.github.io/compat-table/es6/

ここでいうコンパイラは ECMAScript 6 で書かれたコードを現状のブラウザが実行可能な形式に変換してくれるツールを指しています。このツールを利用すれば、制約はあるものの新しい機能を先取りして利用することができます。今回は既存のプロジェクトで書いた古いJavascriptのソースコードを ECMAScript 6 を利用して書き換えることに挑戦してみたいと思います。

※ECMAScript には様々な種類があり、その方言については Wikipedia が詳しいです。
http://ja.wikipedia.org/wiki/ECMAScript

※いわゆる altJS には、現状の Javascript には無い仕様を持つものもあり、ECMAScript 6 の仕様を先取りして実現しているものもあります。


利用した ECMAScript 6 の機能


この新しいJavascriptの機能を詳しく解説しているサイトは数多くあるため、ここでは全機能を網羅的に紹介することはせず、今回の実装した機能についてのみを触れていこうと思います。

class

ECMAScript 6 では class を記述できるようになります。Javascriptではオブジェクト指向で記述する際にモジュールパターンやプロトタイプチェインなどの概念を利用することがよくあると思うのですが、他の言語でのオブジェクト指向に近いイメージで実装することができるようになるため、別言語での経験がある人にとって可読性の高いコードに仕上げることができると思います。extends によるクラスの継承も可能です。


for of 構文

この構文では、配列の様なオブジェクト内の全ての値を繰り返し取得できます。

既存の良く似た構文に for in 文と比較するとイメージしやすいです。
for in 構文ではループの中で key を扱います。
var hash = { k1: 'hoge', k2: 'piyo' }
for (var key in hash) {
    console.log(hash[key]);
}
実行結果

hoge
piyo



既存の for in 構文は配列に使用するための記法ではありませんでした。これは for in がプロパティを走査してしまう問題のためです。

var arr = [1,3,5,7];
Array.prototype.foo = 'foo-proto';
for(arr = 0 ; i < arr.length ; i++){
    console.log(arr[i]);
} 
実行結果

1
3
5
7
foo-proto

foo-proto が余計な値です。

したがって、配列には純粋な for 文を利用することが多いと思います。 for in 構文に比べるとやや煩雑です。

var arr = [1,3,5,7];
for(var i = 0 ; i < arr.length, i++){
    console.log(arr[i]);
} 
実行結果

1
3
5
7


この for in 構文の問題を解決したのは for of 構文です。配列の値を順次参照できるメリットがあります。

var arr = [1,3,5,7];
for( var val of arr ){
    console.log(val);
} 
実行結果

1
3
5
7


let 変数

変数のスコープを直近の中括弧内に限定してくれます。メリットとして他に影響を考えずに実装することができます。

var val = '1000';
var arr = [1,3,5,7];
for( let val of arr ){ //同じ変数名valを定義。
    console.log(val);
} 
console.log(val);
実行結果

1
3
5
7
1000

もともとの var val = ‘1000’ が残っています。

Map 変数

Map変数は連想配列オブジェクトに似ていますが、キーにファンクションやオブジェクトをそのままキーに使用することができる点が異なります。連想配列オブジェクトでは、キーに文字列を使う必要がありましたが、その制約を解消しています。以下のようなキー設定も可能です。
map.set(document.getElementById("prefecture"), { "isCapital": false });

また、連想配列オブジェクトをループする場合は、そのキーがプロトタイプでないか、ファンクションでないかを確認することが必要でしたが、これがやや煩雑な印象がありました。
var obj = {"k1":"value1","k2":"value2"};
for (let key in obj) {
    // プロトタイプでもファンクションでもないか確認。これが連想配列の中身
    if (obj.hasOwnProperty(key) && typeof object[key] !== "function") {
        console.log("Key : "+ key+", Value : "+ obj[key]);
    }
}

Mapを使うと上記のプロトタイプの問題を意識することなくシンプルにループできるメリットがあります。
var map = new Map[["k1","value1"],["k2","value2"};
for (let [key, val] of map){
    console.log("Key : "+ key+", Value : "+ val);     
}


既存の Javascript をECMAScript 6 で書き換える


既存のコード


http://9199.jp/index.html

このサイトの中央部分にある「駅の周辺検索」のリストボックスを題材に取り上げます。

まずHTML部分。既存のコードでは県の部分はゴリッとベタ書きされていました。

<select name="stationpref" id="stationpref" class="l" onchange="setLineCode(this.options[this.selectedIndex].value)">
    <option value="0">県を選択</option>
    <option value="1">北海道</option>
    <option value="2">青森県</option>
    ....
    <option value="47">沖縄県</option>
<li><select name="linecode" id="linecode" class="l" onchange="setStationCode(this.options[this.selectedIndex].value)">
            <option value="0">路線を選択</option>
        </select></li>
        <li><select name="stationcd" id="stationcode" class="l">
            <option value="0">駅名を選択</option>
        </select></li>


JS部分。onchangeで動作する裸のファンクションです。


//県を選ぶと路線を値をセットして初期化している。
function setLineCode(pref){

    //路線セレクトボックスと駅セレクトボックスを取得
   line_box = document.getElementById("linecode");
   station_box = document.getElementById("stationcode");

    //セレクトボックスの現在の値を一度クリアする
    line_idx = line_box.options.length;
    staion_idx = station_box.options.length;
    for ( i = 0 ; i <= line_idx; i++ )	line_box.options[0] = null;
    for ( i = 0 ; i <= staion_idx; i++ )	station_box.options[0] = null;
    station_box.options[0] = new Option("駅名を選択", 0);

    //県セレクトボックスの選択に応じて路線セレクトボックスを設定する
    if (pref == 0){
        line_box.options[0] = new Option("路線を選択", 0);
    }else{
        count = 0;
        line_box.options[count++] = new Option("路線を選択", "0");
        //路線セレクトボックスの中身を JSON API で取ってくる。ブラウザにあわせて若干形式を変えて作成してあった。
        requestUrl = "";
    	var ua = $.browser;
		if(ua.mozilla)	requestUrl = '/common/json/station/line_mozilla/' + pref + '.json';
		else			requestUrl = '/common/json/station/line_ie/' + pref + '.json';

	    jQuery.getJSON(requestUrl, function(json) {
				$.each(json.line, function(i,l){
					if( l ){
						line_box.options[count++] = new Option(l.line_name,l.line_cd);
					}
      		});
        });
	}
}


◆ECMAScript 6 バージョン
class を用いて書いてみる。


//県を扱うクラス!JSなのにクラス!
class Prefecture {

  //コンストラクタ。コードと名称で初期化を行う。
  constructor(prefCode, prefName) {
      this.prefName = prefName;//県名称
      this.prefCode = prefCode;//県コード
  }

  //路線リストを初期化する処理
  initLineList(callbackFn) {
          this.lineList = new LineList(this.prefCode);
          this.lineList.init( callbackFn );
  }
  
}

//県の一覧クラス
class PrefectureList{

  constructor( prefSelectBox, callbackRenderFn ){ //prefList  の初期化
    this.prefList = [];
    
    //ECMAScript 6 の Map 変数を使用する。
    var map = new Map( [[1,"北海道"], [2,"青森県"]] );

    //ECMAScript 6 の for of 構文を使用する。
    //let変数でスコープを絞る。
    for (let [code, name] of map){
        this.prefList.push(new Prefecture( code,name ));
    }

    callbackRenderFn( this.prefList , prefSelectBox );
  }
  
  //Prefectureが変更されたらLineを取得し初期化する処理
  change(prefCode, callbackFn){   
          console.log( prefCode ) ;
          this.selectPref = this.prefList[prefCode-1];
          console.log( this.selectPref ) ;
          this.selectPref.initLineList( callbackFn );
  }

  //県にも紐付く路線を返す処理
  getPrefLineList(){
      return this.selectPref.lineList;
  }

}

//路線クラス
class Line{
    //初期化
    constructor( lineCode, lineName ){
        this.lineName = lineName;
        this.lineCode = lineCode;
    }
}

//路線一覧クラス
class LineList{

  //コンストラクタ。県を指定する。
  constructor(prefCode) 
  { 
     this.prefCode = prefCode;
     this.url = this.requestUrl(prefCode);
     this.lineList = [];
  }

    //路線情報を持っているJSONを返すAPI
    requestUrl(prefCode) {
                  if($.browser.mozilla)   return '/common/json/station/line_mozilla/' + prefCode + '.json';
                  else                    return '/common/json/station/line_ie/' + prefCode + '.json';
    }
    
   //路線の初期化処理
   init( callbackFn ){
       console.log("initLineList this");
       console.log(this);

       var self = this;
       
       jQuery.getJSON(this.url, function(json) {
              $.each(json.line, function(i,l){
                 if( l ){
                     self.lineList.push(new Line(l.line_name,l.line_cd));
                 }
              });
              callbackFn();
       });

     
   }


}

//HTML描画用のクラスを初期化
class Viewer{

    //初期化。HTML上のタグのidを設定。
    constructor( prefSelectBox , lineSelectBox , stationSelectBox ) {

        this.prefList = new PrefectureList( prefSelectBox, this.renderPrefList );
        this.lineSelectBox = lineSelectBox;
        this.stationSelectBox = stationSelectBox;
        self=this;

        //リストボックスのチェンジイベントにバインドする
        $( '#'+ prefSelectBox  ).change( function(){ 
            console.log( self );
            self.prefList.change( $(this).children(':selected').val(), self.renderLine );
             
        } ) ;   
    }

    //県リストのHTML描画
    renderPrefList( prefList , prefSelectBox ){

      var prefBox = document.getElementById( prefSelectBox );

      //初期化
      var prefIdx = prefBox.options.length;
      for ( var i = 0 ; i <= prefIdx; i++ )   prefBox.options[0] = null;
      prefBox.options[0] = new Option("県名を選択", 0);

        var count = 0;
        prefBox.options[count++] = new Option("路線を選択", "0");
        $.each( prefList, function(key,pref){
                if( pref ){
                        prefBox.options[count++] = new Option(pref.prefName,pref.prefCode);
                }
    });

  }

   //路線リストのHTML描画
   renderLine(){
   
     if(!self) self=this;

     var line_box = document.getElementById( self.lineSelectBox  );
     var station_box = document.getElementById( self.stationSelectBox  );

     //初期化
     var line_idx = line_box.options.length;
     var staion_idx = station_box.options.length;
     for ( var i = 0 ; i <= line_idx; i++ )  line_box.options[0] = null;
     for ( var i = 0 ; i <= staion_idx; i++ )    station_box.options[0] = null;
     station_box.options[0] = new Option("駅名を選択", 0);

     //選択されたPrefにおうじてListBoxのOptionを初期化する
     if (self.prefCode === 0){
         line_box.options[0] = new Option("路線を選択", 0);
     }else{
         var count = 0;
         line_box.options[count++] = new Option("路線を選択", "0");
         var lineList = self.prefList.getPrefLineList();
         $.each(lineList.lineList, function(i,l){
             if( l ){
                 line_box.options[count++] = new Option(l.lineCode,l.lineName);
             }
         });
     }
   }
}


//画面初期処理
$(document).ready(function(){
    new Viewer( "stationpref" , 'linecode' , 'stationcode' );
});


//駅の変更処理。ここもClassに書くこともできるが既存ファンクションから変更無し
function setStationCode(line) {
	var ua = $.browser;
	var station_box = document.getElementById("stationcode");
	var staion_idx = station_box.options.length;

	for ( var i = 0 ; i <= staion_idx; i++ )	station_box.options[0] = null;
	station_box.options[0] = new Option("路線を選択", 0);

	if (line == 0){
		station_box.options[0] = new Option("路線を選択", 0);
	}else{
		var count = 0;
		var requestUrl = "";
		station_box.options[count++] = new Option("駅名を選択", "0");
		if(ua.mozilla)	requestUrl = '/common/json/station/station_mozilla/' + line + '.json';
		else			requestUrl = '/common/json/station/station_ie/' + line + '.json';
	    jQuery.getJSON(requestUrl, function(json) {
				$.each(json.station, function(i,l){
					if(l){
						station_box.options[count++] = new Option(l.station_name, l.station_cd);
					}
	  		});
	    });
	}
}
function checkstation() {
	var pref_box = document.getElementById("stationpref");
	var line_box = document.getElementById("linecode");
	var station_box = document.getElementById("stationcode");
	
	if (pref_box.selectedIndex == 0) {
	  alert('都道府県を選択して下さい。');
	  return false;
	}
	if (line_box.selectedIndex == 0) {
	  alert('路線を選択して下さい。');
	  return false;
	}
	if (station_box.selectedIndex == 0) {
	  alert('駅を選択して下さい。');
	  return false;
	}
}



考察


めっちゃ冗長になってしまいました。(もともと40行程度のJSが120行になってしまった。。)

問題を分けて考えます。今回は2つのことをしています。

1.既存コードをオブジェクト指向で書き換える。
2.既存コードを ECMAScript 6 で書き換える。

1に関しては、既存のコードがシンプルだったところをけっこう無理をしてオブジェクト指向っぽくしています。結局冗長なコードになってしまいました。

2について、ポイントは Javascript で class の概念を用いて記述することが、既存の Javascript のコードよりも読みやすくなるということです(いわゆる糖衣構文、シュガーシンタックス)。
この点を補足するために、既存のバージョンである ECMAScript 5 で同じコードを実現してみます。

ECMAScript 5 バージョン


今回は ECMAScript 6 のコードを 6to5 という ECMAScript コンパイラで ECMAScript 5 に変換します。この種のコンパイラでは Google が発表している traceur というものがメジャーですが、実行時に専用のランタイムの javascript が必要となるため、シンプルで読みやすい実行可能なファイルを生成したいと考えた場合は 6to5 は充分な役割を果たしてくれます。


https://github.com/sebmck/6to5

先ほど紹介した ECMAScript 6 の機能一覧のとおり、コンパイラによって対応機能に差があります。今回は充分比較等は行っていないため用途に応じて調査し使い分けていこうと思っています。

結果のコードです。以下で動作しています。

検証サイト
http://9199.jp/js-test/es6-test-1.html


'use strict';

//Javascriptらしく変数と式で表す。
var Prefecture = function() {

  //コンストラクタ
  var Prefecture = function Prefecture(prefCode, prefName) {
      this.prefName = prefName;//県名
      this.prefCode = prefCode;//県コード
  };

  
  //オブジェクトにプロパティを定義するかたちでfunctionを定義する。
  Object.defineProperties(Prefecture.prototype, {
    initLineList: {
      writable: true,

      value: function(callbackFn) {
              this.lineList = new LineList(this.prefCode);
              this.lineList.init( callbackFn );
      }
    }
  });

  //オブジェクトを公開する。
  return Prefecture;
}();

//県リスト
var PrefectureList = function() {

  //コンストラクタ
  var PrefectureList = function PrefectureList(prefSelectBox, callbackRenderFn) //
  {
    this.prefList = [];

    var map = new Map( [[1,"北海道"], [2,"青森県"]] );

    //コンパイラが for of 構文を for 文に変換している
    for (var _iterator = map[Symbol.iterator](), _step; !(_step = _iterator.next()).done; ) {
      var _ref = _step.value;
      var _code = _ref[0];
      var _name = _ref[1];
      this.prefList.push(new Prefecture( _code,_name ));
    }

    //Prefectureが変更されたらLineを取得し初期化する
    callbackRenderFn( this.prefList , prefSelectBox );

  };

  //県と路線の変更を行うファンクション
  Object.defineProperties(PrefectureList.prototype, {
    change: {
      writable: true,

      value: function(prefCode, callbackFn) {   
              console.log( prefCode ) ;
              this.selectPref = this.prefList[prefCode-1];
              console.log( this.selectPref ) ;
              this.selectPref.initLineList( callbackFn );
      }
    },

    getPrefLineList: {
      writable: true,

      value: function() {
          return this.selectPref.lineList;
      }
    }
  });

  return PrefectureList;
}();

//路線
var Line = function() {
  var Line = function Line(lineCode, lineName) {
      this.lineName = lineName;
      this.lineCode = lineCode;
  };

  return Line;
}();

//路線リスト
var LineList = function() {
  var LineList = function LineList(prefCode) { 
     this.prefCode = prefCode;
     this.url = this.requestUrl(prefCode);
     this.lineList = [];
  };

  Object.defineProperties(LineList.prototype, {
    requestUrl: {
      writable: true,

      value: function(prefCode) {
                    if($.browser.mozilla)   return '/common/json/station/line_mozilla/' + prefCode + '.json';
                    else                    return '/common/json/station/line_ie/' + prefCode + '.json';
      }
    },

    init: {
      writable: true,

      value: function(callbackFn) {
          console.log("initLineList this");
          console.log(this);

          var self = this;
          
          jQuery.getJSON(this.url, function(json) {
                 $.each(json.line, function(i,l){
                    if( l ){
                        self.lineList.push(new Line(l.line_name,l.line_cd));
                    }
                 });
                 callbackFn();
          });

        
      }
    }
  });

  return LineList;
}();


//画面の描画処理
var Viewer = function() {
  //コンストラクタ
  var Viewer = function Viewer(prefSelectBox, lineSelectBox, stationSelectBox) {

      this.prefList = new PrefectureList( prefSelectBox, this.renderPrefList );
      this.lineSelectBox = lineSelectBox;
      this.stationSelectBox = stationSelectBox;
      self=this;

      //onchegeイベント
      $( '#'+ prefSelectBox  ).change( function(){ 
          console.log( self );
          self.prefList.change( $(this).children(':selected').val(), self.renderLine );
           
      } ) ;   
  };

  Object.defineProperties(Viewer.prototype, {

    //県の描画
    renderPrefList: {
      writable: true,

      value: function(prefList, prefSelectBox) {

        var prefBox = document.getElementById( prefSelectBox );

            //HTML要素を初期化
            var prefIdx = prefBox.options.length;
            for ( var i = 0 ; i <= prefIdx; i++ )   prefBox.options[0] = null;
            prefBox.options[0] = new Option("県名を選択", 0);

            var count = 0;
            prefBox.options[count++] = new Option("路線を選択", "0");
            $.each( prefList, function(key,pref){
                    if( pref ){
                            prefBox.options[count++] = new Option(pref.prefName,pref.prefCode);
                    }
        });

      }
    },


    //路線描画
    renderLine: {
      writable: true,

      value: function() {
      
        if(!self) self=this;

        var line_box = document.getElementById( self.lineSelectBox  );
        var station_box = document.getElementById( self.stationSelectBox  );

        //各HTML要素を初期化
        var line_idx = line_box.options.length;
        var staion_idx = station_box.options.length;
        for ( var i = 0 ; i <= line_idx; i++ )  line_box.options[0] = null;
        for ( var i = 0 ; i <= staion_idx; i++ )    station_box.options[0] = null;
        station_box.options[0] = new Option("駅名を選択", 0);

        //選択されたPrefにおうじてListBoxのOptionを初期化する
        if (self.prefCode === 0){
            line_box.options[0] = new Option("路線を選択", 0);
        }else{
            var count = 0;
            line_box.options[count++] = new Option("路線を選択", "0");
            var lineList = self.prefList.getPrefLineList();
            $.each(lineList.lineList, function(i,l){
                if( l ){
                    line_box.options[count++] = new Option(l.lineCode,l.lineName);
                }
            });
        }
      }
    }
  });

  return Viewer;
}();


//初期処理
$(document).ready(function(){
    new Viewer( "stationpref" , 'linecode' , 'stationcode' );
});


/路線選択時のonchangeファンクションは今回はそのままにしてあります
function setStationCode(line) {
    var ua = $.browser;
    var station_box = document.getElementById("stationcode");
    var staion_idx = station_box.options.length;

    for ( var i = 0 ; i <= staion_idx; i++ )	station_box.options[0] = null;
    station_box.options[0] = new Option("路線を選択", 0);

    if (line == 0){
        station_box.options[0] = new Option("駅名を選択", 0);
    }else{
        var count = 0;
        var requestUrl = "";
        station_box.options[count++] = new Option("", "0");
        if(ua.mozilla)	requestUrl = '/common/json/station/station_mozilla/' + line + '.json';
        else			requestUrl = '/common/json/station/station_ie/' + line + '.json';
        jQuery.getJSON(requestUrl, function(json) {
                $.each(json.station, function(i,l){
                    if(l){
                        station_box.options[count++] = new Option(l.station_name, l.station_cd);
                    }
            });
        });
    }
}
function checkstation() {
    var pref_box = document.getElementById("stationpref");
    var line_box = document.getElementById("linecode");
    var station_box = document.getElementById("stationcode");
    
    if (pref_box.selectedIndex == 0) {
      alert('都道府県を選択して下さい。');
      return false;
    }
    if (line_box.selectedIndex == 0) {
      alert('路線を選択して下さい。');
      return false;
    }
    if (station_box.selectedIndex == 0) {
      alert('駅を選択して下さい。');
      return false;
    }
}


ECMAScript 5 では Object.defineProperties(Prefecture.prototype や、ひたすらファンクションを定義することなどが必要でしたが、 ECMAScript 5 の class として実装するとそのあたりが不要となるため、すっきりとして見やすいコードになることが確認できると思います。

今後の展望

今回は既存のコードから即席で俺俺なオブジェクトを定義してしまいましたが、 Backbone などのフレームワークを利用して Model の継承や HTML のテンプレート的な機能を利用した描画の最適化を行うことも面白いかなと考えています。


次世代システム研究室では、グループ全体のアプリケーション開発を担うアーキテクトやアプリケーションエンジニアを募集しています。Webアプリケーション開発経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。