dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

WEB+DB PRESS Vol.87のECMAScript 6特集を読んだ(+TypeScript)

特集1の「今すぐ活かす! 最新JavaScript 進化した仕様ECMAScript 6をまるごと解説」を読んでES6(またはES2015)について勉強している。

ES6 については既に web 上でたくさんの記事があったりスライドが上がってたりするが、
書籍として出版されているものは要点がコンパクトに分かりやすくまとまっており、これから学ぼうとする人にはこの特集記事が一番いい資料だと思う。
個人的には ES6 で導入される新しい文法が「今の(ES5 の文法での)JavaScript だとどう書くか」と対比されているため
それぞれの文法を導入するメリットが分かりやすかったです。

また、ES6 の文脈でたびたび TypeScript も耳にすることがあったんだけど
いまいち関係性を理解してなかったのであわせてこちらの記事も読んだ。

TypeScriptを使ってECMAScript 2015時代のJavaScriptを先取りしよう! | HTML5Experts.jp

で、ES6 の新機能であったり、開発環境の構築方法だったりいろいろ勉強になったのでメモ。
せっかく両方勉強したのでコードは現在 TypeScript で写経している。

なお、自分の勉強のため ES6 -> ES5 へのトランスパイルや TypeScript のコンパイルについては
gulp を使った開発環境構築に挑戦していますが、
gulp については実務レベルで使ったことはない初心者で、記載している方法は間違いを含んでいる可能性が大いにあります。
参考程度にとどめていただければ。
(そして間違ってたらツッコミいただければ幸いです)


はじめに〜CoffeeScript, TypeScript と ECMAScript 6、それから Babel

そもそもの話として、CoffeeScript や TypeScript と ES6 との関係性がよくわかっていなかった。
「ES6 を学んでみたいけど、TypeScript を勉強すれば ES6 を学んだことになるの?」とか、
CoffeeScript と TypeScript、どっちを勉強すべき?」といった話。

Babel

まず、WEB+DB PRESS で紹介されているのは Babel というツール。
これはトランスパイラと呼ばれ、ES6 の新しい文法を使ったソースコードを ES5 または ES3 までの文法に置き換えるソースコード変換ツール。
ソースコード変換ツールであって、言語ではない。

Babel を使った ES6 -> ES5 への変換(トランスパイル)のコマンドはこんな感じ。

$ npm install -g babel
# ES6 記法の js ファイルを ES5 記法に変換
$ babel es6/foo.js > es5/foo.js

CoffeeScript, TypeScript

Babel と比較して、CoffeeScript や TypeScript は AltJS と呼ばれる言語の一種。
どちらもコンパイルして JavaScript に変換する。

# CoffeeScript のコンパイル(dest ディレクトリに hello.js を生成)
$ coffee -o dest/ -c hello.coffee

# TypeScript のコンパイル
$ tsc --outDir dest/ hello.ts
TypeScript

JavaScript に静的型付けを導入した もの。
2015年6月に ES6(ES2015)が正式に策定されるよりも以前から、ES6 や ES7 の仕様を実装に取り込んでおり、記法もおおむね ES6 に準拠したものとなっている(という理解でいます)。

CoffeeScript

JavaScript をもうちょっとシンプルに書くことを目的とした 言語。
JavaScript よりもコード量が少なく、かつ可読性高く書けるのが特徴。

このあたりの話は、ちょっと古いけどこの記事が参考になった。
モダンな言語でHTML5を開発しよう! 俯瞰して理解するaltJSの比較 (前篇 – TypeScript, CoffeeScript, Haxe) | HTML5Experts.jp

以下に、ES6 と TypeScript, CoffeeScript で簡単なクラス定義の例を示す。
(ES6 については WEB+DB PRESS に掲載されているサンプルコードを引用)

厳密に三者が同じ結果になるか自信ないけど、TypeScript と ES6 のコードはほとんど同じ構文だということが理解できたので良し。


開発環境構築

Babel、TypeScript いずれも、ファイルを編集するたびトランスパイル/コンパイルのためのコマンドを実行するというのはめんどくさい。
そのため、gulp を利用してファイルを編集するたびトランスパイル/コンパイルされるようにする。

それぞれの gulpfile のサンプルを作ってみた。

Babel による開発環境構築

まず、単純に複数の js ファイルを個別にトランスパイルし、同名のファイルとして出力したい場合。 package.json と gulpfile.js はこんな感じになる。

package.json
{
  "name": "es6",
  "version": "0.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "gulp": "^3.9.0",
    "gulp-babel": "^5.2.0",
    "gulp-sourcemaps": "^1.5.2"
  }
}
gulpfile.js
var gulp = require('gulp');
var sourcemaps = require('gulp-sourcemaps');
var babel = require('gulp-babel');

gulp.task('compile', function() {
  return gulp.src('src/*.js')
    .pipe(sourcemaps.init())
    .pipe(babel())
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('dest'));
});

gulp.task('watch', function() {
  gulp.watch('src/*.js', ['compile'])
});

gulp.task('default', ['compile', 'watch']);

src ディレクトリ内の ES6 文法で書いた js ファイルをそのまま dest ディレクトリに出力する。
各ファイルは watch しているため、変更があればただちに再トランスパイルが行われる。


次に、複数ファイルの間で依存関係がある場合。
(実際の開発ではこちらの方が多いと思う。app.js みたいな基点となるファイルからそれぞれのモジュールを読み込んでいるような場合)

package.json
{
  "name": "es6",
  "version": "0.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babelify": "^6.1.3",
    "browserify": "^11.0.0",
    "gulp": "^3.9.0",
    "vinyl-source-stream": "^1.1.0"
  }
}
gulpfile.js
var gulp = require('gulp');
var browserify = require('browserify');
var babelify = require('babelify');
var source = require('vinyl-source-stream');

gulp.task('babelify', function() {
  browserify({ entries: 'src/app.js', debug: true })
    .transform(babelify)
    .bundle()
    .on('error', function (err) { console.log('Error : ' + err.message); })
    .pipe(source('app.js'))
    .pipe(gulp.dest('dest'))
});

gulp.task('watch', function() {
  gulp.watch('src/*.js', ['babelify'])
});

gulp.task('default', ['babelify', 'watch']);

参考:


TypeScript による開発環境構築

TypeScript の方は複数の ts ファイルをそのままばらばらの js ファイルに出力する方はできたんだけど
依存関係のあるファイルをまとめて 1 つのファイルとして出力する方はよくわからなかった。
たぶん Babel のときと同じノリでいけるはずなんだけど。。。

package.json
{
  "name": "es6",
  "version": "0.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "gulp": "^3.9.0",
    "gulp-typescript": "^2.8.0"
  }
}
gulpfile.js
var gulp = require('gulp');
var ts = require('gulp-typescript');

var config = {
  ts: {
    src: [
      'src/**/*.ts'
    ],
    dest: 'dest',
    options: {target: 'ES5', module: 'commonjs'}
  }
};

gulp.task('compile', function() {
  return gulp.src(config.ts.src)
          .pipe(ts(config.ts.options))
          .js
          .pipe(gulp.dest(config.ts.dest));
});

gulp.task('watch', function() {
    gulp.watch(config.ts.src, ['compile']);
});

gulp.task('default', ['compile', 'watch']);

参考:


リファレンス

http://www.slideshare.net/1000ch/begin-ecmascript6

  • YAPC Asia Tokyo 2015


(おまけ)TypeScript で書くときの注意点

クラスと継承

以下のように static メソッドをオーバーライドするとエラーになった。

class Person {
  // プロパティ
  private name: string;

  // コンストラクタ
  constructor(name: string) {
    this.name = name;
  }

  // スタティックメソッド
  static create(name: string) {
    return new Person(name);
  }
}

// クラスの継承
class Author extends Person {
  private book: string;

  // コンストラクタ
  // 省略しても親クラスのコンストラクタは必ず呼ばれる
  // constructor(...args) {
  //   super(...args);
  // }
  constructor(name: string, book: string) {
    super(name);
    this.book = book;
  }

  // スタティックメソッドのオーバーライド
  static create(name: string, book: string) {
    return new Author(name, book);
  }
}

var author = new Author('Gillian Flynn', 'Gone Girl');
author.greet();

error TS2417: Class static side 'typeof Author' incorrectly extends base class static side 'typeof Person'.
Types of property 'create' are incompatible.
Type '(name: string, book: string) => Author' is not assignable to type '(name: string) => Person'.

このへんに書いてあるのかも。。。

let, const によるブロックスコープ

TypeScript でも 1.4.1 から let, const をサポートしてる(参考)んだけど、
ループ内での let の使用はサポートしていないようだ。

以下は 1, 2, 3, 4, 5 と出力してほしいサンプルコードだが、5, 5, 5, 5, 5 となる。

for (let i = 0; i < 5; i++) {
  setTimeout(function() {console.log(i)}, i * 1000);
}

コンパイル時にも下記のエラーが出る。

error TS4091: Loop contains block-scoped variable 'i' referenced by a function in the loop. This is only supported in ECMAScript 6 or higher.

参考:TypeScript 1.5でのlet文 | 雑記帳