Undefined

この記事は1年以上前に書かれたものです。現在の状況にそぐわない場合がございますのでご注意ください。

簡単なWebアプリを作成してES6を(ちょっと)学んだ

はじめに

概要

  • ECMAScript6(2015)の紹介
  • Google Maps APIを利用した簡単な『最寄り駅検索Webアプリケーション』を作成

現在、JavaScriptは大きく変化し多様化している印象です。JavaScriptの標準化規格ECMAScriptのバージョンも上がり、モバイル以外のブラウザでは規格に基づいた実装が着々と整ってきているようです。これが(私にとっては)随分書きかたが変わるんだなあ!っていう印象で、どうにも馴染めず利用することを先送りにしていました。

とはいえ、今やこのES6(2015)は最新トピックでも何でもなく、言語の今後を担う部分を使えない自分になってしまっているんで、まず手を動かしていこうと思い簡単なことからはじめていこうと思います。

完成図

こちらが『最寄り駅検索Webアプリケーション』の完成形になります。

画面上部の検索フォームで指定した位置を中心としたある一定の半径内にある駅を調べ、画面左側の情報エリアに駅ルート情報を表示するという(Google Maps API頼りの)アプリケーションです。

See the Pen簡単な最寄り駅検索を、ES6で (Simple nearest station search app) by Mizzz (@mizzz) on CodePen.

現在地から位置情報を取得する際は、一部ブラウザによってはhttpsによるアクセスじゃないとエラーになる場合がありますのでプロトコルをhttpsにしてみてくださいね。


練習ですので今回はCodepenでつくりました。トランスパイラにBabelを利用してES6で書いたスクリプトをES5に変換しています。ちなみにこのBabelのサイトのTry it outページでは気軽に変換を試せます。実際に開発に利用するときはGulpなどタスクランナーでトランスパイラを設定することになるかと思います。

最寄り駅検索Webアプリケーションについて

要件

最寄り駅検索Webアプリケーションでできることはこんな感じ。

Google Mapsの各APIには無料利用可能な回数に(主に1日単位での)上限があります。上限を超えた場合、APIへのリクエストにエラーが返ります。

構成

グローバル空間に定義されたKOKOKARAという名前空間にいくつかのクラスを持っています。このアプリケーションとグローバル空間の接点はこのKOKOKARAオブジェクトのみになります。

クラス以外にもいくつかの設定用変数と関数を持っています。

アプリケーションを構成するクラス

クラス名 主なプロパティ 役割
ScriptEvent
  • 登録済みのイベントリスナー
DOMに依存しないイベントの送出を提供します。
Main
  • 検索位置原点
  • 最寄り駅
  • アプリのルート要素
アプリの大枠を管理します。
Map
  • GoogleMapオブジェクト
  • 地図要素
地図を管理します。
Point
  • GoogleMapのマーカー
地図上の地点を司るオブジェクトの親クラスです。
OriginPoint 地図上の検索位置原点地点を司ります。
Station
  • 駅のプレイスオブジェクト
  • 駅のルート案内オブジェクト
  • 駅の情報エリアへの表示要素
地図上の最寄り駅地点を司ります。
Search
  •  検索フォーム要素
検索機能を提供します。
Info
  • 情報エリアルート要素
検索結果のデータを表示します。

クラスと画面要素の対応構成図

画面構成要素は各クラスに対応しています。PointクラスはOrigin PointクラスとStationクラスの親クラスで、Script Eventクラスだけは画面構成要素をもたない抽象的な機能クラスです。

ES6(2015)の新機能

ではでは、本題に。

以下、この練習制作に使用したES6(2015)の新機能です。
ちなみにこれ以外にもシンボル、バイナリ・8進数リテラルなどなど、単純に私が使いどこがわからなかったり、今回の練習制作に適していないと判断した新機能がまだまだあります。

定数 (const)

再代入できない読み取り専用の値を生成するconst宣言が追加されました。

const INITIAL_ZOOM = 16;
INITIAL_ZOOM = 8; // エラー

ただし、const宣言した場合でも配列やオブジェクトリテラルの添え字からのアクセスでは変更できてしまいます。このような場合はObject.freezeやObject.definePropertyなどで変更不可にする必要があります。

const object = { foo: 'bar' };
object.foo = 'hoge';
console.log(object) // Object {foo: "hoge"} 変更できる

主にアプリケーション全体の設定の定義に使用しています。

局所変数

変数をletで宣言することでスコープを定義されたブロック、文または式に限定することができます。const宣言以外のすべての変数の宣言に使用しています。

for (let i = 1; i <= 5; i++) {
  console.log(i); // iのスコープ内
}
console.log(i); // iのスコープ外

ブロックスコープ

従来、JavaScriptには関数によるスコープしか存在しませんでしたが、ES6では中括弧 (波括弧) の組で、文(コード)をグループ化できるようになりました。

グローバル空間でのKOKOKARA(名前空間)定義以外のすべてのコードをブロックスコープで囲ってグローバル空間から隔離するために利用しています。

{ // ブロックスコープ
  let prison = '塀の中';
  console.log(prison); // "塀の中"
}
console.log(prison); // エラー

アロー関数

従来、使用されていたfunction式をより簡潔に書くための構文です。より少ないタイプで式を書くことができます。下記の3つは全て(thisのスコープ以外)等価です。

// 従来
var func = function(arg1) {
  return arg1;
}

// アロー関数
var func = (arg1) => {
  return arg1;
}

// さらに
// ・引数が一つの場合は()を省略できる(※引数がない場合は必要)
// ・処理が一文の場合は{}とreturnを省略できる。
var func = arg1 => arg1;

レキシカル(静的)なthisの束縛

アロー関数は全てレキシカルスコープになります。
レキシカルスコープとは、どこから呼び出されたかで変数のスコープが決まるのではなく、変数の定義時に文脈上からスコープが決定されるスコープのようです。
つまりwindow.setTimeout内でのメソッド呼び出しのような、実行時にthisコンテキストが変わってしまう場合にもアロー関数を使用することで、thisコンテキストを変えることなく処理を実行できます。

レキシカルなthisの束縛が必要な場所、レキシカルなthisの束縛が問題にならないところで使用しています。

let obj = {
  foo: 'bar',
  func: function() {
    window.setTimeout(() => {
        console.log(this.foo); // thisがobjに束縛されている
    }, 3000);
  }
}
obj.func(); // bar

デフォルト引数

関数の定義時に引数の初期値を指定できるようになりました。これにより関数内で引数値を確認する必要がなくなりました。関数の呼び出し時に該当の引数が指定されなかったり、undefinedの場合には初期値がデフォルト値として使用されます。

function f(eventName, handler, order = 10) {
  console.log(order);
}
function foo() {}
f('ckick', foo); // orderはデフォルト値の10になる

すでに指定されたデフォルト引数は後続のデフォルト引数で再利用可能です。

function f(foo = 'テスト', bar = foo + 'だよ') {
  console.log(bar);
}
f(); // テストだよ

メソッド定義などで共通に利用される事の多い引数の値の設定で使用しています。

残余引数

個数の定まらない引数を配列として取得します。残余引数による配列はargumentsオブジェクトと違い配列のようなオブジェクトではなく、本当の配列なので配列用メソッドを直接利用することができます。

function f(arg, ...args) {
  args.sort(); // 配列用メソッドを利用
  console.log(arg); // argは'd'
  console.log(args); // argsは残りの引数の配列表現 ['a', 'b', 'c']
}
f('d', 'c', 'b', 'a'); // argは'd'、argsは残りの引数の配列表現 ['a', 'b', 'c']

展開(スプレッド)演算子

配列を展開します。

function f(arg1, arg2, arg3) {
  console.log(arg1); // 'a'
  console.log(arg2); // 'b'
  console.log(arg3); // 'c'
}
let arr = ['a', 'b', 'c'];
f(...arr); // 配列を展開

let arr2 = ['d', 'e', 'f'];
let arr3 = [];
arr3.push(...arr, ...arr2); // 配列を展開してarr3に追加
console.log(arr3); // ["a", "b", "c", "d", "e", "f"]

テンプレートリテラル

テンプレートリテラルは文字列内に式を挿入することができます。また複数行にわたる文字列を明示的に改行文字列を指定することなく書くことができます。また、タグ付けという機能により結果文字列の出力を調整することもできます。

let todaysWether = '雨';
let tommorowsWether = '晴れ';
console.log(`「今日の天気は${todaysWether}です。
明日の天気は${tommorowsWether}の模様です。」`);
// 出力は変数の値に置き換えられ改行された以下の文字列
// 「今日の天気は雨です。
// 明日の天気は晴れの模様です。」

// 関数でタグを定義
function tag(strings, ...values) {
  console.log(strings[0]); // "昨日の天気は"
  console.log(strings[1]); // "でした。"
  console.log(values[0]); // "くもり"
}
let yewsterdaysWether = 'くもり';
tag`昨日の天気は${yewsterdaysWether}でした。`; // tagがタグ

オブジェクトプロパティ

ショートハンドでのプロパティ定義

定義済みの変数と同名のプロパティをオブジェクトに定義します。

let name = '山田太郎';
let age = 18;
let sex = 'male';
let person = { name, age, sex }; // ショートハンドプロパティ
console.log(person); // { name: "山田太郎", age: 18, sex: "male" }

計算されたプロパティ名でのプロパティ定義

オブジェクトのプロパティ定義で計算されたプロパティ名を使用できるようになりました。

let countory = 'japanese';
let person = {
  name: '山田太郎',
  [`is_${countory}`]: true // 計算されたプロパティ名
};
console.log(person); // {name: "山田太郎", is_japanese: true}

分割代入

配列かオブジェクトからデータを取り出して変数に代入します。あらかじめ既定値を指定することもできます。

// 配列
let [a, b, c, d = 4] = [1, 2]; // 配列からの分割代入
console.log(a); // 1
console.log(b); // 2
console.log(c); // undefined (値が無い)
console.log(d); // 4 (値が無いため規定値になる)

// オブジェクト
let person = {
  name: '山田太郎',
  age: 18
};
let { name, age, sex = 'male' } = person; // オブジェクトからの分割代入
console.log(name); // '山田太郎'
console.log(age); // 18
console.log(sex); // 'male' (値が無いため規定値になる)

クラス

既存のプロトタイプオブジェクトによるクラス(のような)定義をより簡単に記述できるようになりました。これは単に今までのクラス定義方法のシュガーシンタックスです。

クラス宣言

classキーワードを使って定義して、constructorメソッドでオブジェクトの生成時に初期化を行います。staticキーワードを利用すると静的メソッドを定義できます。

クラス宣言は巻き上げられないので使用される前に定義される必要があります。宣言されていない場合、ReferenceErrorが送出されます。

new Person(); // ReferenceError

class Person {
  constructor({name, age, sex} = args) { // コンストラクタ
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
  getName() { // メソッド
    return `${this.name}さん`;
  }
  static getSpecies() { // 静的メソッド
    return '人間';
  }
}

let options = {
  name: '山田太郎',
  age: 18,
  sex: 'male'
};

let person = new Person(options); // クラス使用
console.log(person); // {name: "山田太郎", age: 18, sex: "male"}
console.log(`${person.getName()} (${Person.getSpecies()})`); // 山田太郎さん (人間)

クラスは式としても定義可能です。式では従来の関数式のように無名クラスを定義することもできます。

let Person = class { // 無名クラス
 :
}

すべてのクラス定義に使用しています。

クラス継承・親クラスへのアクセス

extendsキーワードで子クラスを定義することができます。またsuperキーワードで親クラスのメソッドを呼び出すことができます。

OriginPointクラスとStationクラスで、Pointを継承しています。共通に使用するメソッドを基本クラスへのアクセスから使用しています。

// 『クラス宣言』のPersonクラスを継承
class Zombie extends Person {
  constructor(options) {
    super(options); // 親クラスのコンストラクタ呼び出し
  }
  getName() { // メソッド
    let name = super.getName(); // 親クラスのメソッド呼び出し
    return `${name}だったゾンビ`;
  }
}
let thing = new Zombie(args); // {name: "山田太郎", age: 18, sex: "male"}
console.log(thing.getName()); // 山田太郎さんだったゾンビ

Promise

Promiseは非同期処理などに適切な処理の延期を提供して、実行順序を整頓することのできる仕組みです。これにより非同期的な処理でもコールバックをネストせずに意図した通りの順番を保つことができます。

Promiseの流れは大まかに以下のようになります。

  1. Promiseコンストラクタから、3つの状態を持つPromiseオブジェクトを生成する
    Promiseコンストラクタに指定するコールバックにはresolveまたはrejectメソッドが引数として渡される
  2. 処理状況に応じて引数で渡されたresolveまたはrejectメソッドを実行する
  3. 2のメソッド実行によってPromiseオブジェクトの状態が変更され、各状態に関連付けられた後続の処理がthenまたはcatchで実行される。

Promiseの状態

状態 メソッド 説明
pending 初期状態。
fulfilled resolve 処理が成功して完了。
rejected reject 処理が失敗。
function get(url) {
  return new Promise(function(resolve, reject) { // Promiseオブジェクトを生成
    let req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = () => {
      if (req.status == 200) {
        resolve(req.response); // 成功時にはresolveの引数でデータを返す
      } else {
        reject(new Error('1. ひとつめの関数からのエラー')); // 失敗時にはrejectでエラーオブジェクトを返す(必ずエラーオブジェクトを返す必要はありません)
      }
    };

    req.onerror = () => {
      reject(new Error('1. 関数からのエラー')); // 失敗時にはrejectでエラーオブジェクトを返す(必ずエラーオブジェクトを返す必要はありません)
    };

    req.send();
  });
}

get('/fail.html')
  .then( // 返されたPromiseオブジェクト.then( 正常時の処理 , エラー時の処理 ) 
    function(response) { // 正常時の処理。データを引数で受け取れる
      console.log(1);
      return 2; // 新しくPromiseオブジェクトを生成して返す
    },
    function(error) { // エラー時の処理。データを引数で受け取れる
      console.error(error);
  })
  .then(function(data) { // returnで返されたデータも引数で受け取れる
    console.log(data);
    throw new Error('2. thenからのエラー'); // 例外が発生するとエラー時の処理へ
  })
  .catch(function(error) { // エラー時の処理はcatchメソッドでもOK。データを引数で受け取れる
    console.error(error);
});

おわりに

ES6の新機能は使ってみればどれも非常に便利で、もはや今までの書きかたには戻れない自分がいます。その感覚は、初めてSassを使った時にちょっとだけ似ていました。はやくこの利便性にトランスパイラのいらない日がくればいいなあ。

参考サイト

学習の参考にさせていただいたサイト様です。どのサイト様も非常に勉強になりました。

  • mizzzshunnn

    テストコメント返信

  • mizzzshunnn

    テストコメント

to top