2013年11月30日(土)に開催された「HTML5 Conference 2013」の、Mozilla Japan浅井智也さんによるセッション「進化を続ける JavaScript ~次世代言語のステキな機能と高速化の行方~」のセッション内容をご紹介します。
JavaScriptの課題と改良への動き
現在のJavaScriptには、次のような課題があります。
- 高速化がなかなかできない
- クラス、モジュールがない
- プロトタイプベースであるため、普段Javaなどでクラスを使っている人にとって、メソッド定義や継承がわかりにくい
- メソッドとして呼び出したときはthisがそのメソッドのオブジェクトになるが、間違って関数として呼び出すとthisがwindowオブジェクトになるなど、thisの挙動を捉えるのが難しい
- argumentsなど、ArrayのようでArrayでなくついarguments.slice()とかしてエラー原因になる、紛らわしいオブジェクトがある
- コールバック地獄
- 実行時エラーが多く、コンパイルエラーが少ないので、デバッグが非常に大変である
JavaScriptの歴史について詳しくは過去の講演資料をご覧ください。簡単に言えば次の資料の通りです。次期ECMAScript 6thでは大きく改定されるため、この改定内容はぜひ知っておく必要があります。
ECMAScript 6thの概要
目標
ECMAScript 6thの目標は、より開発・テストを行いやすくし、相互運用性を確保し、静的検証(コンパイルエラーの検出)を行える構文・言語仕様を提供することにあります。
実装・対応状況
FirefoxのSpiderMonkeyの実装範囲が比較的広く、V8がそれに続く状況になっています。機能によってはV8の方が先に入っていたり、上手く実装されているものもあります。IEは非常に安定した、広く使われている仕様が実装されており、加えて、グローバリゼーション、国際化といったビジネス系の仕様が積極的に提案、実装されています。TypeScriptは、対応範囲がクラスなどに絞られている印象があるのですが、比較的綺麗なコードが生成されます。その他にも、letやconstといったブロックスコープだけを使いたいのでしたらdefs.jsを、あるいはModuleのimport/export機能だけを使いたい場合はes6-module-loaderといったコンパイラも使うことができます。
Syntax Sugar
分割代入 (Destructuring)
これまでのJavaScriptでは代入文の左辺に変数を一つしか指定できませんでしたが、ECMAScript 6thでは左辺に配列やオブジェクトを指定することができるようになります。これにより、例えば、関数から配列やオブジェクトなどで複数の返り値を受け取り、それをそのまま変数に格納するといった処理を記述できるようになります。JSONから特定のデータを抜き出す、といったことを行うこともできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 配列で受け取るサンプル: // 値の入れ替え [a, b] = [b, a]; // 関数から複数の値を返して一気に変数に代入 var [c,d] = (function f() { return [1,2]; })(); // -> c=1, d=2 // 一部省略や入れ子も可能 var [e,,[x,y]] = (function f(){ return [3,4,[10,20]] })(); // オブジェクトで受け取るサンプル var fx={ name:"Firefox", vendor:"Mozilla", ver:26 }; var ch={ name:"Chrome", vendor:"Google", ver:31 }; var browsers={ firefox: fx, chrome: ch } // 欲しいプロパティだけ一括代入 var { name: n, ver: v } = fx; // -> n="Firefox", v=26 // 関数の引数から必要なプロパティだけ受け取る (function ({ vendor: ven }) { console.log(ven); })(fx); // -> "Mozilla" |
default & rest parameter
引数のデフォルト値を設定できたり(default parameter)、関数の残りの引数を配列で受け取ったり(rest parameter)することができます。
1 2 3 4 5 6 7 8 9 10 11 12 |
e = document.body; // 何か適当な要素 function setBackgroundColor(element, color='orange') { element.style.backgroundColor = color; } setBackgroundColor(e); // オレンジに setBackgroundColor(e, 'blue'); // 青に setBackgroundColor(e, undefined); // オレンジに // デフォルト値は呼び出し毎に生成される // 同一オブジェクトが渡される Python などとは違う function getObject(o={}) { return o; } getObject() == getObject() // -> false |
1 2 3 4 5 6 7 8 9 10 |
function f(a, b, ...args) { return args; } f("IE", "Chrome"); // -> [] f("IE", "Chrome", "Firefox"); // -> ["Firefox"] ! // rest arguments は Array のメソッドが使える // [].slice.call(arguments) ハックとか不要に function sortRestArgs(...theArgs) { var sortedArgs = theArgs.sort(); return sortedArgs; } |
配列の内包表記 (Comprehensions)
for…ofなどを使って、直接、配列のリテラルを生成することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 配列のフィルタとマップ [for (x of [1,-4,5,3,-7]) if (x > 0) x] // -> [1, 5, 3] // ES5 なら次のように書く // [1,-4,5,3,-7].filter(function(x) { return x > 0 }); [for (x of [2,4,6]) x*x] // -> [4, 16, 36] // ES5 なら次のように書く: // [2,4,6].map(function (x) { return x*x }); // 配列のデカルト積やラベル生成もシンプルに [for (i of [0,2,4]) for (j of [5,3]) i*j] // -> [0, 0, 10, 6, 20, 12] [for (x of 'abc'.split('')) for (y of '123'.split('')) (x+y)]; // -> ["a1","a2","a3","b1","b2","b3","c1","c2","c3"] |
Modularity
ブロックスコープ (let, const)
今までのJavaScriptにはブロックスコープがなく、グローバルスコープと関数スコープしかありませんでした。変数をブロックスコープに納めるために、今まではわざわざ関数を作らなければなりませんでしたが、ECMAScript 6thでは、let, constを指定するだけでブロックスコープ変数を作成することができます。
1 2 3 4 5 6 7 8 9 10 11 12 |
{ // let 定義: ブロックスコープ let a = 1, b = 10; // let 式・文: let (...) に続く式・文中だけで有効 let (a = 100, c = 300) console.log(a); // -> 100 // for 文などでの let for (let a=0; a<3; a++) { console.log(a+b); // -> 10, 11, 12 } console.log(a); // -> 1 } console.log(a); // × ReferenceError: a is not defined |
1 2 3 4 5 6 7 8 9 10 |
// 不変定数を定義 const browser = "Firefox"; // 再定義は TypeError となる const browser = "Internet Explorer"; // TypeError: redeclaration of const browser ! // 定数への代入は単に無視される browser = "Chrome"; console.log(browser); // -> "Firefox" |
Class
待望のClassが使えるようになります。他のオブジェクト指向の言語と同じような形式でClassを記述することができます。constructorメソッドが初期化時の処理として使われます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// クラスベース OOP でよく見る感じ class Animal { constructor(name) { this.name = name; this.hungry = true; } eat() { this.hungry = false; } run() { this.hungry = true; } } // 派生クラスの定義がシンプル class LesserPanda extends Animal { constructor(name, tail) { super(name); this.tail = tail; } } |
Module
今まで、モジュールをインポート・エクスポートする様々なライブラリが作られてきましたが、この機能が公式のものとして提供されることになりました。
1 2 3 4 5 6 |
module 'math' { export function sum(x, y) { return x + y; } export var hbar = 1.054571726e-34; // ディラック定数 } |
1 2 3 4 5 6 |
import {sum, hbar} from 'math'; alert("2ħ = " + sum(hbar, hbar)); // オブジェクトのプロパティに読み込み module Math from 'math'; alert("2ħ = " + Math.sum(Math.hbar, Math.hbar)); |
Readable Code
Arrow Function
今まで、コールバックとして関数を渡す際に「function(…){…}」と記述する必要があり、ソースコードが冗長で見にくくなっていましたが、Arrow Functionを使うことでこの関数をシンプルに記載できるようになります。
1 2 3 4 5 6 7 8 |
// return するだけのコールバックがシンプルに [1,2,3].map(x => x * x); // ES5 ではこう書く必要があった: // [1,2,3].map(function (x) { return x * x; }); // n! (nの階乗) を求める関数もシンプルに var factorial=((f=n=>n>1 ?n*f(n-1):1)=>(f))(); factorial(10); // 3628800 |
Arrow Functionには、簡潔に書けるということに加えて、もう一つ良いことがあります。それは、thisが保持・バインドされるということです。今まで、コールバック関数内で呼び出し元のオブジェクトを参照しようとすると、thisがwindowオブジェクトを参照するようになるため、コールバック関数の前で「self = this」などと記載し、この場合はコールバック関数内で「self」を参照しなければなりませんでした。Arrow Functionですと、Arrow Function内のthisが、その外側の関数のthisを参照し続けるという特性があり、このような記載が不要となります。
Generator
nextメソッドを持ち、繰り返し処理が実行される際にnextメソッドが呼ばれる特殊なオブジェクトをイテレータ(iterator)と呼びます。イテレータを簡単に生成できるジェネレータ関数によって生成されたイテレータをジェネレータ(generator)と呼びます。ジェネレータ関数では、yieldというキーワードがreturnの代わりに使われており、yieldが実行されるとその引数が返却されて関数の処理が一時停止します。そして、再度関数が実行されるとyieldの次の行から処理が再開し、またyieldが実行されると引数を返却して処理が一時停止する、という処理の流れになります。ジェネレータ関数を使うことで、関数の再起呼び出しを行うことなく、何かしらの繰り返しの処理を行うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// ジェネレータ関数 (ジェネレータのコンストラクタ) function* fibonacci() { let [prev, curr] = [0, 1]; for (;;) { [prev, curr] = [curr, prev + curr]; yield curr; // 値を返して一時停止 } } for (n of fibonacci()) { if (n > 20) break; console.log(n); // 順に 1, 2, 3, 5, 8, 13 } |
先ほどのfor…ofの中でジェネレータ関数を使う方法以外にも、毎回明示的にイテレートする方法があります。こちらがイテレータの本来の使い方なのですが、例えば、カウンタを作成する場合、毎回一時停止して内部で保持している値をインクリメントするジェネレータ関数を定義すると、nextメソッドを呼ぶたびにインクリメントされた値を取得する処理を実装することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function* counterGenerator() { let c = 0; for (;;) { yield c++; // 値を返して一時停止 } } // ジェネレータを生成 let counter = counterGenerator(); // next() メソッドで {value, done} を得る counter.next(); // -> {value: 0, done: false} counter.next(); // -> {value: 1, done: false} counter.next().value; // -> 2 counter.next().value; // -> 3 |
Promise
Promiseを使うことで、今までよりもコールバックを非常に簡潔に書けるようになります。また、catchなどの構文も今まで以上に書きやすくなります。Promiseを生成するときに引数として関数を渡し、そこでresolve関数とreject関数を指定します。このresolve関数が呼ばれたときにこのPromiseインスタンスが解決し、このインスタンスのthenメソッドが実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
let p = new Promise(function (resolve, reject) { // 3 秒後に resolve 呼んでプロミスが解決する setTimeout(resolve, 3000); }); // 解決した (resolve が呼ばれた) ときに実行: p.then(function () { alert('3 秒たったよ!'); }).then(function () { // 解決済みなので即ここも実行される alert('既に 3 秒過ぎてるよ!'); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
p = new Promise(function (resolve, reject) { dream(); // 未定義の関数を呼ぶ (エラー発生) }); // エラー発生時は then をスキップして catch へ p.then(function (message) { // p は解決しないのでこのブロックは実行されない alert(message); }).catch(function (error) { // p でエラーが発生するのでこのブロックを実行 alert(error); // -> ReferenceError: dream is not defined }); |
New Type & API
Collections
Set型、Map型が追加されます。Setを使うことで、集合に対して要素を追加したり参照したりすることができます。Mapは、キーとそれに対応する値を保持できるものです。配列やオブジェクトではキーとして文字列しか設定できませんでしたが、Mapではキーとしてオブジェクトを設定することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var set = new Set(); // 集合に追加・確認・削除 set.add("Firefox"); set.add("Thunderbird"); set.add(+0); set.add(NaN); set.has("Firefox"); // -> true set.has("Sunbird"); // -> false set.delete("Firefox"); set.has("Firefox"); // -> false // -0 と +0 は区別される, NaN は区別されない set.has(-0); // -> false set.has(NaN); // -> true |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var map = new Map(); var str = "Mozilla", obj = {}, func = function(){}; // Map に値を格納 map.set(str, "Firefox"); map.set(obj, "Thunderbird"); map.set(func, "Japan"); // キーに対応する値を取得 map.get(str); // -> "Firefox" map.get(obj); // -> "Thunderbird" map.get(func); // -> "Japan" // 設定したキーと引数の比較は == ではないので注意 map.get("Mozilla"); // -> "Firefox" map.get({}); // -> undefined map.get(function(){}) // -> undefined |
ここまで次世代言語仕様の一部をご紹介してきましたが、このようにJavaScriptは構文や機能面でも大きな進化を遂げ、落とし穴が少なくシンプルな記法で書ける言語に進化するのです。
そして一方、広く使われる言語は書きやすいだけでなく高速でなければなりませんが、ここからは速度面の進化についてご紹介します。
Slow Parts & Fast Parts
JavaScriptが遅い原因として、JavaScriptが動的型付け言語であることや、JavaScriptにクラスと配列が存在しないことなどがありますが、これらの原因を回避して、JavaScriptエンジンが最適化しやすいコードだけを書くようにすることで、処理を高速化させることができます。例えば、型固定で変数を定義することや、一度定義したオブジェクトのプロパティの追加・削除を行わない、といったことなどに気をつければ、処理を高速化させることができます。このように、JavaScriptエンジンが最適化しやすく、高速化を図れるコードを、私の中で「JavaScript Fast Parts」と呼んでいます。
Typed Array
Fast Partsの代表的なものとして、Typed Arrayがあります。これを使うことで、今までJavaScriptではできなかった静的型付け・型の固定を行うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 16 バイト長のバッファを確保 var buffer = new ArrayBuffer(16); // 32bit 整数 x 4 として読み出すビューを定義 var int32View = new Int32Array(buffer); // 32bit 整数として 0, 2, 4, 6 を格納 for (var i=0; i<int32View.length; i+ +) { int32View[i]=i*2; } // 16bit 整数 x 8 として同じバッファを読み出すビュー var int16View = new Int16Array(buffer); // 実際に読み出してみる for (var i=0; i<int16View.length; i++) { console.log(int16View[i]); } // -> 0, 0, 2, 0, 4, 0, 6, 0 |
asm.js
以上のような高速化できる方法だけを使ってコードを書く、ということをあらかじめルールとして定めてしまい、JavaScriptエンジン側も最初から最適化された状態でコンパイルして高速に実行するようにしよう、という発想に基づいた高速化方法を、Mozillaがasm.jsとして提案しています。コードを自動生成するための様々なツールが存在していますが、これらは全て、JavaScriptエンジンが速く実行できるコードを生成しようとしています。このように、既存のエンジンにおける高速化のノウハウがある程度溜まっているため、それらのノウハウを結晶させたFast Partsを使って高速化を実現させるということが、asm.jsの発想です。
asm.jsの目的は、WebをNativeと同様の速度にすることです。Native同様の速度にするために、新しいコンパイルの方法を導入したりするわけではありません。現在のJavaScriptエンジンでも、最適化できるコードに関しては、既に型固定の高速なコードが生成されるようになっています。その際に、コード生成のためのオーバーヘッドや型変換時のチェックなどがあり、これらがなければNative並みの速度で実行されるようになりつつあります。このエンジンをそのまま利用して、今まで「このコードを最適化して良いか否か」をヒューリスティックに判別していたところを、asm.jsを利用すると、決め打ちで最適化できるようなります。
既存のJavaScriptエンジンでも、よく使われるコードを見つけて、それらを最適化することが幅広く行われています。しかし、簡単なコードは最適化できても、巨大なコードは最適化できない場合が多く、C言語よりも10倍、20倍遅い場合もあります。asm.jsを利用し、常に最適化するようにした場合は、asm.js導入直後の時点でもC言語の2倍の速度で済むレベルまで高速化されました。asm.jsでの高速化は、現在も改良が進められています。
結論:Always bet on JavaScript!
Webは非常に互換性を重視しており、既存のものを動作させ続けながら、機能を追加したり速度を改善したりする必要があります。本日ご説明しました通り、JavaScriptでは、必要な機能の追加や高速化が順次行われているため、JavaScriptはこれまで同様に生き続け、皆さんが勉強する価値のある言語になっていると思っており、これからもJavaScriptを愛して頂ければ嬉しいです。