読者です 読者をやめる 読者になる 読者になる

ES6時代のJavaScript

こんにちは会員事業部の丸山@です。

最近のWebフロントエンドの変化は非常に激しく、ちょっと目を離した間にどんどん新しいものが出てきますよね。そんな激しい変化の一つとしてES6という次期JavaScriptの仕様があります。このES6は現在策定中で、執筆時点ではDraft Rev31が公開されています。

JavaScriptはECMAScript(ECMA262)という仕様をもとに実装されています。
現在のモダンなWebブラウザはECMAScript 5.1th EditionをもとにしたJavaScript実行エンジンを搭載しています。
そして次のバージョンであるECMAScript 6th Editionが現在策定中で、略称としてES6という名前がよく使われます。

今回は、他の言語にはあってJavaScriptにも欲しいなと思っていた機能や、JavaScriptでよく頻出するパターンを統一的に書ける機能を中心に紹介していきます。

Class

JavaScriptは「プロトタイプベースのOOP」と呼ばれている通り、JavaやRubyなどの「クラスベースのOOP」とは少し毛色が違います。しかしプロトタイプベースの機能を効果的に使うということはこれまで少なかったように思います。むしろ擬似的なクラス機能を実装したり、クラスを実現するためのライブラリを使ってプログラムを書くことが多いはずです。これはnpmでclassで検索するとたくさんのパッケージがヒットすることからもわかると思います。そこでES6ではクラス機能が導入され、クラスを簡単に扱えるようになりました。

// ES5
'use strict';

function User(name){
  this._name = name;
}

User.prototype = Object.create(null, {
  constructor: {
    value: User
  },

  say: {
    value: function() {
      return 'My name is ' + this._name;
    }
  }
});

function Admin(name) {
  User.apply(this, arguments);
}

Admin.prototype = Object.create(User.prototype, {
  constructor: {
    value: Admin
  },

  say: {
    value: function() {
      var superClassPrototype =  Object.getPrototypeOf(this.constructor.prototype);
      return '[Administrator] ' + superClassPrototype.say.call(this);
    }
  }
});

var user = new User('Alice');
console.log(user.say()); // My name is Alice

var admin = new Admin('Bob');
console.log(admin.say()); // [Administrator] My name is Bob
// ES6
'use strict';

class User {
  constructor(name) {
    this._name = name;
  }

  say() {
    return 'My name is ' + this._name;
  }
}

class Admin extends User {
  say() {
    return '[Administrator] ' + super.say();
  }
}

var user = new User('Alice');
console.log(user.say()); // My name is Alice

var admin = new Admin('Bob');
console.log(admin.say()); // [Administrator] My name is Bob

Function Arguments

JavaScriptで関数のデフォルト引数や可変長引数を使いたいと思っても言語には直接的な方法がなかったため、||を使ったおまじない的な方法やargumentsを使ったメタプログラミング的な方法を取ってきました。そこでES6では関数の仮引数の宣言方法が強化され、自然に書くことができるようになりました。これは後でプログラムを読むときに、シグネチャだけを見ればその関数が期待する引数をある程度わかるようになるという効果もあります。

// ES5
'use strict';

function loop(func, count) {
  count = count || 3;
  for (var i = 0; i < count; i++) {
    func();
  }
}

function sum() {
  var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
}

loop(function(){ console.log('hello')}); // hello hello hello
console.log(sum(1, 2, 3, 4)); // 10
// ES6
'use strict';

function loop(func, count = 3) {
  for (var i = 0; i < count; i++) {
    func();
  }
}

function sum(...numbers) {
  return numbers.reduce(function(a, b) { return a + b; });
}

loop(function(){ console.log('hello')}); // hello hello hello
console.log(sum(1, 2, 3, 4)); // 10

実はこのデフォルト引数や可変長引数は関数の仮引数部だけで使えるというわけではなく、変数の代入処理全般が強化されたうちの一部分になります。ES6での変数の代入処理についてはDestructuring and parameter handling in ECMAScript 6にてサンプル付きで様々なパターンが紹介されています。

Arrow Function

JavaScriptではイベント駆動の処理をよく書きます。例えばDOMがクリックされたら何か処理する、XHRのリクエストが完了したら何か処理をする場合などです。このような処理をJavaScriptで実装するには、コールバック関数やイベントリスナと呼ばれるものを対象のオブジェクト(DOMやXHR)に設定します。このコールバック関数を登録する時点でのthisにコールバック関数内からアクセスしたくなる場面がよくありますが、これまではクロージャを使ってthisを保存しておいたり、Function.prototype.bindを使ってthisを束縛したりしていました。ES6ではArrow Functionと呼ばれる新たな関数定義|式が導入され、このthisに対する煩わしさを解消しています。

// ES5
'use strict';

var ClickCounter = {
  _count: 0,

  start: function(selector) {
    var node = document.querySelector(selector);
    node.addEventListener('click', function(evt){
      this._count++;
    }.bind(this));
  }
};

ClickCounter.start('body');
// ES6
'use strict';

var ClickCounter = {
  _count: 0,

  start: function(selector) {
    var node = document.querySelector(selector);
    node.addEventListener('click', (evt)=>{
      this._count++;
    });
  }
};

ClickCounter.start('body');

Promise

これまでXHRなどの非同期処理は開始時にコールバック関数を設定して、非同期処理が終わったらそのコールバック関数が呼び出されるというのが一般的ですが、様々なコールバック関数の設定方法がありました。例えば非同期処理の関数にコールバック関数を引数として渡したり(setTimeoutsetInterval)、非同期処理を行うオブジェクトにコールバック関数を登録したり(XHRWebWorker)、非同期処理の戻り値にコールバック関数を登録したり(IndexedDB)などがあります。

このように様々な方法があるため、使う側としてはそれぞれの方法を使い分ける必要があります。そこでES6ではPromiseという非同期処理を統一的に扱う方法が言語として提供されるようになりました。使い方は、非同期処理を行う関数はPromiseを戻り値として返し、呼び出し側はPromiseにコールバック関数を登録するというものです。

// ES5
'use strict';

function sleep(callback, msec) {
  setTimeout(callback, msec);
}

sleep(function(){
  console.log('wake!')
}, 1000);
// ES6
'use strict';

function sleep(msec) {
  return new Promise(function(resolve, reject){
    setTimeout(resolve, msec);
  });
}

sleep(1000).then(function(){
  console.log('wake!');
});

また非同期処理では例外処理が問題になります。単純にtry-catchで囲っても非同期で例外が起きると補足できません。そこでPromiseでは非同期処理の例外処理を統一的に行える方法も提供しています。このPromiseについてはWeb上で無料で読めるJavaScript Promiseの本が大変参考になります。

Generator

最後にGeneratorについて紹介します。ここまではすでにJavaScriptに存在するけど使いにくかったり、統一されていなかったものを改善したという機能でしたが、このGeneratorというのは全く新しい概念としてES6に取り込まれています*1

Generatorというのは関数処理内の任意の場所で処理を中断/再開できる仕組みを提供するものです。この仕組は一般的にコルーチン(co-rutine)と呼ばれています。コルーチンを使うと無限リストやイテレータなどを実装することができます。

このGeneratorとPromiseを組み合わせることで非同期処理を同期処理のように直列に書くことができるようになります。基本的な考え方は「非同期処理が開始されたら処理を中断し、非同期処理が完了したら処理を再開し後続の処理を実行してく」というものです。先ほどのPromiseの項で紹介したサンプルコードをGeneratorを使って直列に書いてみます。以下のサンプルコードではGeneratorとPromiseを使って非同期処理を直列に書くことができるcoというライブラリを使っています。

// ES6
'use strict';

co(function*(){
  console.log('sleep...');
  yield sleep(1000);
  console.log('wake!');
});

今回はcoを使って解説しましたが、coを使わずに非同期処理を直列に書く仕組みとしてasync/awaitという機能がES7に提案されています*2

Generatorについては私のブログでES6 Generatorを使ってasync/awaitを実装するメモとして解説しているので興味のある方は御覧ください。

まとめ

今回はJavaScriptで歯痒い思いをしていた所が、ES6でどのように変わるのかを中心に紹介しました。ここで紹介した内容はES6の一部であり、他にもModules, Symbol, Data Structures, Proxy, Template Stringなどの様々な機能が追加されています。現時点ではES6で書いたコードをそのままブラウザやnodeで実行するのは難しい状況ですが、ES6をES5にトランスパイルするツールとしてtraceur-compiler6to5があるのでお手軽に試してみることができます。また各ブラウザやツールがES6のどの機能に対応しているかはECMAScript compatibility tableが参考になります。

ES6時代のJavaScriptに備えて今から少しずつ触ってみるのはいかがでしょうか?

*1:Firefoxでは2006年ごろにはすでに実装されていました

*2:C#のasync/awaitと同様のもの

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/