babel.jsでECMAScript6
参考: http://yoshiko-pg.github.io/slides/20150425-jsfes/
babel.jsのLearn ES2015を読みつつECMAScript6を色々勉強してみた
※一部省略しているのもあり
検証環境の構築
.jsを書いてテスト(jest)を書いてみたいな事をするのでそこら辺をサポートしてくれる環境を構築する。なのでとりあえずnpmでやるのでpackage.jsonを定義
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"babel-jest": "^5.2.0",
"jest-cli": "^0.4.5"
},
"jest": {
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"collectCoverage": true,
"unmockedModulePathPatterns": [
"node_modules",
"js"
]
}
}
今回、(jestの)モック的な機能を一切使わないのでunmockedModulePathPatternsを指定しておく。でECMAScript6で書く場合にはbabel-jestをscriptProcessorに指定する必要があるっぽいので
という感じの環境で.jsを書いてテスト書いてみた事をしつつ進める
Arrows
export default () => {
var messages = ["hoge", "fuga", "foobar"];
return messages.map(v => { return v.toUpperCase(); }).join(' ');
//return messages.map(v => v.toUpperCase()).join(' ');
}
まぁECMAScript5とかだと引数にはfunctionを指定しないといけなかったんだけど、ECMAScript6ではブロック定義する事でさらっとかけるようになる。ちなみに
関数の中身の式がひとつの場合、中カッコとreturnを省略できる
引数が1つの場合は引数を囲むカッコを省略できる
— kinjouj (@kinjou__j) 2015, 5月 30
んまぁやってる事は単純なので
var printMessages = require("../Exam1");
describe('Exam1', () => {
it("printMessages", () => {
var message = printMessages();
expect(message).toBe("HOGE FUGA FOOBAR");
});
})
Classes
まぁ要はクラス定義みたいにオブジェクトを定義出来ますよ的な
class Hoge {
say() {
return Hoge.filter("hoge")
}
static filter(s) {
return s.toUpperCase();
}
}
class Fuga extends Hoge {
say1() {
// 継承したクラスのstaticメソッドを自前クラスで呼んでもイケるっぽい
return Fuga.filter("fuga");
}
say2() {
// 親クラスのsayを呼んだ後にtoLowerCase
return super.say().toLowerCase();
}
}
export default Fuga;
もちろんコンストラクタもあるけど今回は使ってない。あと上記参考の19ページにも書いてるけど
ここ(setTimeout(function内))ではthisがグローバルオブジェクトになる アロー関数内のthisは外側のthisと同じ
っていうように若干thisに伴うスコープの扱いがややこしそう。という事で必要無いと思うけどテスト書く
var Fuga = require("../Exam2");
describe("Fuga", () => {
it("say", () => {
var fuga = new Fuga();
expect(fuga.say1()).toBe("FUGA");
expect(fuga.say2()).toBe("hoge");
});
});
Template Strings
backticks(`の事)で文字列表現を使えばそこで${}によって変数展開が出来る
export default () => {
var name = "hoge";
return `name: ${name}`;
}
んまぁほとんど言う事もないと思うので
var say = require("../Exam3");
describe("Exam3", () => {
it("say", () => {
expect(say()).toBe("name: hoge");
});
});
Destructuring
日本語でいうと「分配束縛」っていうらしい。まぁわかりづらいのでやってみた方が早いかと
export default class Exam4 {
array() {
return ["hoge", "fuga", "foobar"];
}
hash() {
return {
"name": "hoge",
"age": 20
};
}
}
まぁ単純に適当なクラスを定義しておいて
var Exam4 = require("../Exam4");
describe("Exam4", () => {
var o;
beforeEach(() => {
o = new Exam4();
});
it("array", () => {
// o.arrayから返ってくる値は["hoge", "fuga", "foobar"]
var [a, b] = o.array();
expect(a).toBe("hoge");
expect(b).toBe("fuga");
var [, c, d] = o.array();
expect(c).toBe("fuga");
expect(d).toBe("foobar");
});
it("hash", () => {
// キーがハッシュのキー名で値がそれを格納する変数名
var { name: _name, age: _age } = o.hash();
expect(_name).toBe("hoge");
expect(_age).toBe(20);
// 以下でも出来る
var { name, age } = o.hash();
expect(name).toBe("hoge");
expect(age).toBe(20);
});
});
っていう事。配列とかから値をまとめて取りたいとか、ハッシュから特定のキーをまとめて変数に個々に展開したいとかそういう場合に使える記法みたいな感じなのかなと
それと引数とかにハッシュ等のオブジェクトを指定する場合にそのキーに対応する変数に展開する事も出来る模様
export default class Exam4 {
// 引数に指定された連想配列のキーを値に指定した変数で部分的に展開出来る
// 展開される変数にはデフォルト値を設定可能
ppos({ name: _name = "a" }) {
return _name.toUpperCase();
}
}
っていうように引数に指定したオブジェクトのnameキーを_name変数に展開
var Exam4 = require("../Exam4");
describe("Exam4", () => {
it("ppos", () => {
var o = new Exam4();
var name = o.ppos({ name: "hoge" });
expect(name).toBe("HOGE");
expect(o.ppos([])).toBe("A");
});
});
っていうように
引数に指定したのがObjectじゃないとか、連想配列じゃないとかそういう場合にバインドされるデフォルトの値を突っ込むみたいな感じ
— kinjouj (@kinjou__j) 2015, 5月 30
んまぁざっくりしているけどArrayだとかHashだとかで値を取り出してまとめて変数に個々に展開するのはさくっと出来るみたいな感じ(ざっくりしすぎてるけど)
Rest Parameter & Spread Operator
...から始まる変数名を引数に持つと可変長引数として利用出来る(それをES6ではRest Parameterっていう?)。でES5とかで良くあるあるとして配列のデータを可変長引数っていう形として展開したいような場合はapply等を使ってやってたのも出来る(その方式をSpread Operatorという?)
export default class Exam5 {
sum(...nums) {
var count = 0;
nums.forEach(v => count += v);
return count;
}
sum_array(nums = []) {
// 配列のデータを可変長引数っていう形で引数に展開したい場合にも可能
return this.sum(...nums);
}
}
っていうように...で始まる変数をメソッドの引数に定義、更に別のメソッドで可変長引数を持つメソッドに配列を流す場合にはその配列の変数の前に...を指定してやれば展開されるような仕組みかと
var Exam5 = require("../Exam5");
describe("Exam5", () => {
it("sum", () => {
var o = new Exam5();
expect(o.sum(1, 2)).toBe(3);
expect(o.sum_array([1, 2])).toBe(3);
});
});
最後の引数以外には使用できない > Rest Parmaeters
— kinjouj (@kinjou__j) 2015, 5月 30
Iterator
Symbol.iteratorを使ってIteratorを利用する事も出来るっぽい
// browserifyして実行する場合には必要無いけど、テストする場合の環境によっては必要
var Symbol = require("es6-symbol");
var one_two_three = {
[Symbol.iterator]() {
var values = [1, 2, 3];
var cur = 0;
return {
next() {
if (cur < 3) {
var value = values[cur];
cur++;
return { done: false, value: value };
} else {
return { done: true };
}
}
}
}
};
export default one_two_three
っていうようにdone: trueになるまで処理が継続するような感じ
var Symbol = require("es6-symbol");
var one_two_three = require("../Exam6");
describe("123", () => {
it("123", () => {
var count = 0;
for (var n of one_two_three) {
count += n;
}
expect(count).toBe(6);
});
});
「for var n of」ってなってるのに注意しないとイケないと思うんすけど。んな感じでIteratorのような実装を利用する事も出来る。でこれブラウザで動くのかっていう所なので
var one_two_three = require("./Exam6");
for (var n of one_two_three) {
console.log(n);
}
をindex.jsとして作っておいて
browserify -t babelify js/exam6/index.js > index.js
っていう風にして一度browserifyしてからHTMLで読み込むと普通に処理されてる模様(ちなみに余談に書いてるbabel --modules ignoreでやっても出来る。但しrequireは取り除く必要あり)
Generator
Iteratorではnextメソッドを持つ物をを定義して、それでdone: trueになるまでやるかループ自体を制御するかっていう話だと思うんすけど。それをよりシンプルにかけるのがgenerator的な所(詳しくはPythonのイテレータとジェネレータとか)
var generator = {
[Symbol.iterator]: function*() {
var n = 0;
for (;;) {
yield n;
n++;
}
}
};
export default generator
まぁiteratorと違ってnextメソッドが必要だったりとかじゃなくてyield利用して
import {generator} from "./g.js"
for (var n of generator) {
if (n >= 10) {
break;
}
console.log("yield", n);
}
というように一定になったらbreakみたいな事をする。でブラウザで動かすので
babel --modules ignore g.js app.js > index.js
な感じでコンパイルする。でgenerator周りをブラウザ側で使うにはbrowser-polyfill.jsが必要なのでそれをコピってくる(babel-coreパッケージに入ってる)
<html>
<head>
<script src="browser-polyfill.js"></script>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
っていう感じで動かすとyieldされた一定回数のconsole.logが出力されてる(めんどくさいのでスクショは無し)
という感じでyieldされた値がブロックで利用できるっていう話なんすけど、これ普通に関数オブジェクトを提供しているだけでクラスで利用する場合はという事でやってみた
export default class Echo {
echo() {
var messages = ["hoge", "fuga", "foobar"];
var len = messages.length;
return {
[Symbol.iterator]: function*() {
for (var i = 0; i < len; i++) {
var [m] = messages.splice(0, 1);
yield m;
}
}
}
}
}
メソッドから「[Symbol.iterator]: function*」を持つオブジェクトを返すようにする
import {Echo} from "./echo";
var echo = new Echo();
var g = echo.echo();
for (var n of g) {
console.log("echo", n);
}
というようにメソッドから取得したのを適切なブロックコンテキストで利用すれば良いだけ
まぁ今回はブラウザで動かすっていうのを前提にしているけど、そうじゃないようなケースの場合にはbrowser-polyfill.jsを持ってくるんじゃなくて「require("babel/polyfill")」しろっとの事
Symbols
Symbolは基本的に同じのが作れない(インスタンス的な所として?)らしいのでそれを使ってクラスプロパティに格納する値のキーとかに使えば文字列として参照するのではなくSymbolとして参照するような事も出来るとか。まぁ用途は色々だろうと
/* hoge.js
const key = Symbol("key");
export default class Hoge {
set(s) {
this[key] = s;
}
get() {
return this[key];
}
}
*/
import Hoge from "./hoge";
var hoge = new Hoge();
hoge.set("hoge");
console.log(hoge.get());
hoge.jsで内部的にSymbolを作ったので格納しているのでそのオブジェクト側の外から作ったSymbolを使ってもアクセス出来ない
以上。色々これからも勉強なり調査するなりで得られた事はレポートする予定で
余談1: デフォルト値に関して
undefinedを渡した場合はデフォルト値が使用される
— kinjouj (@kinjou__j) 2015, 5月 30
デフォルト値のない引数より後にしか置けない
— kinjouj (@kinjou__j) 2015, 5月 30
っていう事らしい
余談2: babelの--modules ignore
babel --modules ignore hoge.js fuga.js
なんてやると
"use strict";
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Hoge = (function () {
function Hoge() {
_classCallCheck(this, Hoge);
}
_createClass(Hoge, [{
key: "say",
value: function say() {
return Hoge.say("hoge");
}
}], [{
key: "say",
value: function say(s) {
return s.toUpperCase();
}
}]);
return Hoge;
})();
"use strict";
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }
var Fuga = (function (_Hoge) {
function Fuga() {
_classCallCheck(this, Fuga);
if (_Hoge != null) {
_Hoge.apply(this, arguments);
}
}
_inherits(Fuga, _Hoge);
_createClass(Fuga, [{
key: "say",
value: function say() {
return Fuga.say("fuga");
}
}]);
return Fuga;
})(Hoge);
っていう風に出力される。特にrequire等でmoduleを使う前提とかでなければこの方法でやっても使える場合もあるっぽい
まぁbrowserifyとか使うのが無難では無いかと