日々の仕事の中で役に立つES2015(ES6)のティップス、コツ、ベストプラクティス、プログラムの見本をご紹介します。コントリビューション歓迎です!
目次
- var vs. let / const
- IIFEからブロックベースへ
- アロー関数
- 文字列
- デストラクチャリング
- モジュール
- パラメータ
- クラス
- シンボル
- マップ
- WeakMaps
- Promises
- ジェネレータ
- Async/Await
var vs. let / const
var
の他に、値を格納するlet
とconst
という識別子が新たに追加されました。var
とは異なって、let
とconst
はクロージャのスコープ内で最初に記述されることはありません。
var
の使用例です。
var snack = 'Meow Mix'; function getFood(food) { if (food) { var snack = 'Friskies'; return snack; } return snack; } getFood(false); // undefined
しかし、var
をlet
へ置き換えると以下のようになります。
let snack = 'Meow Mix'; function getFood(food) { if (food) { let snack = 'Friskies'; return snack; } return snack; } getFood(false); // 'Meow Mix'
上記の挙動の変化で明確になるのは、var
を使った過去のプログラムをリファクタリングする際には注意しなければならないということです。やみくもにインスタンスのvar
をlet
に置き換えると、予想外の挙動を引きおこします。
注:
let
とconst
はブロックスコープです。よって、定義する前にリファレンスされるとReferenceError
が出てしまいます。
console.log(x); let x = 'hi'; // ReferenceError: x is not defined
ベストプラクティス: リファクタリングには注意が必要だとはっきり示すために、レガシーコード内の
var
宣言を残して下さい。新しいコードベース使う場合には、時間の経過とともに値が変わる変数にはlet
を、再アサインが許されない変数にはconst
を使います。
(目次へ戻る)
IIFEからブロックベースへ
一般に、即時呼び出し関数式はスコープの中に値を収めるために使います。ES6では、ブロックベースのスコープを作ることができ、関数ベースのスコープに完全に限定されることがなくなりました。
(function () { var food = 'Meow Mix'; }()); console.log(food); // Reference Error
ES6のブロックを使います。
{ let food = 'Meow Mix'; } console.log(food); // Reference Error
(目次へ戻る)
アロー関数
レキシカルスコープからthis
のコンテキストを守るために、今まではよく関数をネストしていました。下記のような例です。
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; // Cannot read property 'name' of undefined }); };
この問題の一般的な解決法は、this
コンテキストを変数として格納することです。
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { var that = this; // Store the context of this return arr.map(function (character) { return that.name + character; }); };
this
の正しいコンテキストを受け渡すこともできます。
this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }, this); };
下記のようにコンテキストを結びつけることもできます。
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(function (character) { return this.name + character; }.bind(this)); };
アロー関数を使うと、this
の値を隠さずに上記を書き換えることができます。
function Person(name) { this.name = name; } Person.prototype.prefixName = function (arr) { return arr.map(character => this.name + character); };
ベストプラクティス:
this
の値を保つためにはアロー関数を使いましょう。
単純に値を返す関数式として使う場合、アロー関数はもっと簡潔になります。
var squares = arr.map(function (x) { return x * x }); // Function Expression const arr = [1, 2, 3, 4, 5]; const squares = arr.map(x => x * x); // Arrow Function for terser implementation
ベストプラクティス: 関数式が使えるところではアロー関数を使いましょう。
(目次へ戻る)
文字列
ES6では、標準ライブラリが膨大に増えました。この変更にともない、.includes()
や.repeat()
のような、文字列にも使える新たなメソッドも追加されています。
.includes( )
var string = 'food'; var substring = 'foo'; console.log(string.indexOf(substring) > -1);
文字列が含まれているかどうかを明らかにするために戻り値が> -1
か否かを確認する代わりに、ブーリアン値を返す.includes()
を使うだけで足ります。
const string = 'food'; const substring = 'foo'; console.log(string.includes(substring)); // true ing.includes(substring)); // true
.repeat( )
function repeat(string, count) { var strings = []; while(strings.length < count) { strings.push(string); } return strings.join(''); }
ES6では、もっと簡単に実装できます。
// String.repeat(numberOfRepetitions) 'meow'.repeat(3); // 'meowmeowmeow'
テンプレートリテラル
テンプレートリテラルを使うと、特殊文字を明示的にエスケープしなくても文字列内で使うことができます。
var text = "This string contains \"double quotes\" which are escaped.";
let text = `This string contains "double quotes" which are escaped.`;
また、テンプレートリテラルは補間をサポートしているので、文字列と数値を結びつけるタスクもできます。
var name = 'Tiger'; var age = 13; console.log('My cat is named ' + name + ' and is ' + age + ' years old.');
もっと簡単にします。
const name = 'Tiger'; const age = 13; console.log(`My cat is named ${name} and is ${age} years old.`);
ES5では、改行を次のように追加していました。
var text = ( 'cat\n' + 'dog\n' + 'nickelodeon' );
あるいは、こんな感じです。
var text = [ 'cat', 'dog', 'nickelodeon' ].join('\n');
テンプレートリテラルでは明示的に改行を示す必要がありません。
let text = ( `cat dog nickelodeon` );
テンプレートリテラルでは式も同じように扱えます。
let today = new Date(); let text = `The time and date is ${today.toLocaleString()}`;
(目次へ戻る)
デストラクチャリング
デストラクチャリングによって、配列やオブジェクトから(深くネストされていたとしても)値を抽出し、より便利な構文で変数の中に保存することができます。
配列のデストラクチャリング
var arr = [1, 2, 3, 4]; var a = arr[0]; var b = arr[1]; var c = arr[2]; var d = arr[3];
let [a, b, c, d] = [1, 2, 3, 4]; console.log(a); // 1 console.log(b); // 2
オブジェクトのデストラクチャリング
var luke = { occupation: 'jedi', father: 'anakin' }; var occupation = luke.occupation; // 'jedi' var father = luke.father; // 'anakin'
let luke = { occupation: 'jedi', father: 'anakin' }; let {occupation, father} = luke; console.log(occupation); // 'jedi' console.log(father); // 'anakin'
(目次へ戻る)
モジュール
ES6以前は、クライアント側にモジュールを作るためにはBrowserifyのようなライブラリを、Node.jsではrequireを使っていました。ES6では、全てのタイプのモジュール(AMDとCommonJS)を直接使うことができます。
CommonJSでのエクスポート
module.exports = 1; module.exports = { foo: 'bar' }; module.exports = ['foo', 'bar']; module.exports = function bar () {};
ES6でのエクスポート
ES6では、いろいろなエクスポートが可能で、名前付きエクスポートを実行することができます。
export let name = 'David'; export let age = 25;
オブジェクトのリストのエクスポートもできます。
function sumTwo(a, b) { return a + b; } function sumThree(a, b, c) { return a + b + c; } export { sumTwo, sumThree };
また、関数やオブジェクト、値(など)も、export
というキーワードを使うだけでエクスポートできます。
export function sumTwo(a, b) { return a + b; } export function sumThree(a, b, c) { return a + b + c; }
最後に、デフォルトのバインディングもエクスポートできます。
function sumTwo(a, b) { return a + b; } function sumThree(a, b, c) { return a + b + c; } let api = { sumTwo, sumThree }; export default api;
ベストプラクティス:常に
export default
メソッドを、モジュールの最後で使ってください。これにより、何がエクスポートされるのかを明らかにし、何という名前でエクスポートされるのかをはっきりさせることで時間を節約します。さらに、CommonJSモジュールにおける一般的な慣習は、1つの値やオブジェクトをエクスポートすることです。このパラダイムを順守することで、コードを簡単に読めるようにし、私たち自身がCommonJSとES6モジュールの間に入り込めるようにするのです。
ES6でインポートする
ES6は、様々なインポート機能を提供してくれます。以下に示すように、ファイルを丸ごとインポートすることができます。
import 'underscore';
ファイルを丸ごとインポートすることで、そのファイルのトップレベルで全てのコードを実行するということに注意してください。
Pythonと同様に、名前付きインポートがあります。
import { sumTwo, sumThree } from 'math/addition';
また、名前付きインポートの名前を変更することもできます。
import { sumTwo as addTwoNumbers, sumThree as sumThreeNumbers } from 'math/addition';
それに加え、全てをインポートすることもできます(名前空間インポートと呼ばれることもあります)。
import * as util from 'math/addition';
最後に、モジュールから値のリストをインポートすることもできます。
import * as additionUtil from 'math/addition'; const { sumTwo, sumThree } = additionUtil;
デフォルトのオブジェクトをインポートする時は、どの機能をインポートするか選択することができます。
import React from 'react'; const { Component, PropTypes } = React;
以下のように、これをさらに簡略化することもできます。
import React, { Component, PropTypes } from 'react';
注:エクスポートされた値は参照ではなく、バインディングです。ですから、あるモジュール内で値のバインディングを変更する時は、エクスポートされたモジュール内の値に影響を及ぼします。これらのエクスポートされた値のパブリックインターフェースは変えないようにしましょう。
(目次へ戻る)
パラメータ
ES5では、デフォルトの値、不定の引数、名前付きパラメータを必要とする関数を扱うために様々な方法をとっていました。ES6を使えば、より簡潔な構文でES5以上のことができます。
デフォルトのパラメータ
function addTwoNumbers(x, y) { x = x || 0; y = y || 0; return x + y; }
ES6では、関数の中でパラメータにデフォルトの値を簡単に与えることができます。
function addTwoNumbers(x=0, y=0) { return x + y; } addTwoNumbers(2, 4); // 6 addTwoNumbers(2); // 2 addTwoNumbers(); // 0
残りのパラメータ
ES5では、引数の個数が定まっていない場合このように扱いました。
function logArguments() { for (var i=0; i < arguments.length; i++) { console.log(arguments[i]); } }
restオペレータを使うことで、個数の定まっていない引数を渡すことができます。
function logArguments(...args) { for (let arg of args) { console.log(arg); } }
名前付きパラメータ
ES5で名前付きパラメータを扱うパターンの1つには、jQueryから適用された、options objectパターンを使うというものがありました。
function initializeCanvas(options) { var height = options.height || 600; var width = options.width || 400; var lineStroke = options.lineStroke || 'black'; }
これと同じ機能性を、関数の正式なパラメータとしてデストラクチャリングを使うことによって成し遂げることができます。
function initializeCanvas( { height=600, width=400, lineStroke='black'}) { // Use variables height, width, lineStroke here }
全ての値をオプションにしたい場合は、空のオブジェクトをデストラクチャリングすることによってできます。
function initializeCanvas( { height=600, width=400, lineStroke='black'} = {}) { // ... }
Spread演算子
ES5では、以下のようにMath.max
でapply
メソッドを使うことで、配列内の最大値を見つけることができていました。
Math.max.apply(null, [-1, 100, 9001, -32]); // 9001
ES6では、関数のパラメータとして使うための値の配列を渡すためにspread演算子を使うことができます。
Math.max(...[-1, 100, 9001, -32]); // 9001
理解しやすい構文を使って、簡単にリテラルの配列を連結することができます。
let cities = ['San Francisco', 'Los Angeles']; let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']
(目次へ戻る)
クラス
ES6より前でクラスの実装を行う時は、プロトタイプを拡張してプロパティを追加し、コンストラクタ関数を生成していました。
function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Person.prototype.incrementAge = function () { return this.age += 1; };
そして下記のように拡張したクラスを生成していました。
function Personal(name, age, gender, occupation, hobby) { Person.call(this, name, age, gender); this.occupation = occupation; this.hobby = hobby; } Personal.prototype = Object.create(Person.prototype); Personal.prototype.constructor = Personal; Personal.prototype.incrementAge = function () { Person.prototype.incrementAge.call(this); this.age += 20; console.log(this.age); };
ES6ではこれを内部で処理してくれる糖衣構文が提供されているため、直接クラスを生成することができます。
class Person { constructor(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } incrementAge() { this.age += 1; } }
キーワード extends
を使って拡張できます。
class Personal extends Person { constructor(name, age, gender, occupation, hobby) { super(name, age, gender); this.occupation = occupation; this.hobby = hobby; } incrementAge() { super.incrementAge(); this.age += 20; console.log(this.age); } }
ベストプラクティス:ES6のクラス生成の構文によって、実装とプロトタイプの働きが見えにくくなります。これは初心者にとって優れた機能で、クリーンなコードを書くことができます。
(目次へ戻る)
シンボル
ES6以前もシンボルは存在していましたが、ES6ではシンボルを直接扱えるパブリックインターフェースを備えています。シンボルは変更不可能かつユニークで、どのハッシュにおいてもキーとして使うことができます。
Symbol()
Symbol()
またはSymbol(description)
をコールすれば、グローバルに参照のできないユニークなシンボルを生成できます。Symbol()
のユースケースでは、あなたのロジックにサードパーティのオブジェクトや名前空間をパッチすることができますが、ライブラリのアップデートと衝突しないという確信がある場合だけにしてください。例えば、クラスReact.Component
にメソッドrefreshComponent
を追加したい場合、後のアップデートの時にメソッドを侵害しないことを確認してください。
const refreshComponent = Symbol(); React.Component.prototype[refreshComponent] = () => { // do something }
Symbol.for(key)
Symbol.for(key)
も不変でユニークなシンボルを生成しますが、こちらはグローバルに参照できます。Symbol.for(key)
を同じように2回コールすると、同じシンボルのインスタンスを返します。注:Symbol(description)
の場合は違います。
Symbol('foo') === Symbol('foo') // false Symbol.for('foo') === Symbol('foo') // false Symbol.for('foo') === Symbol.for('foo') // true
シンボルに共通のユースケース、特にSymbol.for(key)
を使うのは相互運用性のためです。これを行うには、既知のインターフェースを含むサードパーティのオブジェクト引数上で、コードにシンボルのメンバを探させます。以下のようになります。
function reader(obj) { const specialRead = Symbol.for('specialRead'); if (obj[specialRead]) { const reader = obj[specialRead](); // do something with reader } else { throw new TypeError('object cannot be read'); } }
他のライブラリでも同様です。
const specialRead = Symbol.for('specialRead'); class SomeReadableType { [specialRead]() { const reader = createSomeReaderFrom(this); return reader; } }
相互運用性のためのシンボルを使用する顕著な例としては、ES6の全てのiterableとイテレータ型に存在する
Symbol.iterable
、すなわち配列、文字列、ジェネレータなどがあります。メソッドとして呼ばれた時、イテレータのインターフェースと共にオブジェクトを返します。
(目次へ戻る)
マップ
マップはJavaScriptにおいて非常に重要なデータ構造です。ES6以前はオブジェクトを介してハッシュマップを生成していました。
var map = new Object(); map[key1] = 'value1'; map[key2] = 'value2';
しかしこの場合、過って関数をプロパティ名で上書きしてしまう危険性があります。
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned'); > TypeError: Property 'hasOwnProperty' is not a function
マップを使えば、値のset
やget
そしてsearch
が(もっと多くのことも)できます。
let map = new Map(); > map.set('name', 'david'); > map.get('name'); // david > map.has('name'); // true
マップの最も素晴らしい点は文字列以外も使えるということです。キーとしてあらゆる型を、文字列に型変換せずに使えます。
let map = new Map([ ['name', 'david'], [true, 'false'], [1, 'one'], [{}, 'object'], [function () {}, 'function'] ]); for (let key of map.keys()) { console.log(typeof key); // > string, boolean, number, object, function }
注:非プリミティブ型の値、すなわち関数やオブジェクトを使うと、
map.get()
などのメソッドでの一致条件が使えません。ですから、文字列、ブーリアンや数値などプリミティブ型の値を使うようにしましょう。
.entries()
を用いてマップを反復処理させることもできます・
for (let [key, value] of map.entries()) { console.log(key, value); } (back to table of contents)
(目次へ戻る)
WeakMap
ES6以前のバージョンでプライベートデータを保存するには様々な方法がありましたが、その1つに命名規則を用いる方法がありました。
class Person { constructor(age) { this._age = age; } _incrementAge() { this._age += 1; } }
しかし命名規則はコードベースに混乱をきたしかねず、規則の一貫性を保つのも難しいものです。代わりに、WeakMapを使って値を保存しましょう。
let _age = new WeakMap(); class Person { constructor(age) { _age.set(this, age); } incrementAge() { let age = _age.get(this) + 1; _age.set(this, age); if (age > 50) { console.log('Midlife crisis'); } } }
プライベートデータを保存する時にWeakMapを使う利点は、キーによってプロパティ名が表に出ないところです。プロパティ名はReflect.ownKeys()
を使って参照します。
> const person = new Person(50); > person.incrementAge(); // 'Midlife crisis' > Reflect.ownKeys(person); // []
WeakMapを使うデータ保存のより実用的な例としては、DOMそのものを汚さずにDOM要素に関連したデータの保存ができるということが挙げられます。
let map = new WeakMap(); let el = document.getElementById('someElement'); // Store a weak reference to the element with a key map.set(el, 'reference'); // Access the value of the element let value = map.get(el); // 'reference' // Remove the reference el.parentNode.removeChild(el); el = null; value = map.get(el); // undefined
ガベージコレクションによってオブジェクトが破棄されたら、WeakMapはそのオブジェクトが指定したキー値のペアを自動的に削除します。
注:この例の有用性を更に説明するため、jQueryがどのようにして参照先を持つDOM要素に対応するオブジェクトのキャッシュを保存するか考えてみてください。jQueryは、ドキュメントから削除されたらすぐに特定のDOM要素と関連したメモリを自動的に開放します。一般的に、WeekMapはDOM要素をラップするライブラリに対して便利に使えます。
(目次へ戻る)
Promise
Promiseは下記のような階層の深いコード(コールバック地獄)を解消します。
func1(function (value1) { func2(value1, function (value2) { func3(value2, function (value3) { func4(value3, function (value4) { func5(value4, function (value5) { // Do something with value 5 }); }); }); }); });
次のような縦型のコードにしてくれます。
func1(value1) .then(func2) .then(func3) .then(func4) .then(func5, value5 => { // Do something with value 5 });
ES6以前はbluebirdやQを用いていましたが、今はPromiseがネイティブに実装しています。
new Promise((resolve, reject) => reject(new Error('Failed to fulfill Promise'))) .catch(reason => console.log(reason));
2つのハンドラがあり、resolve(Promiseが解決された時に呼ばれる関数)と、reject(Promiseが拒否された時に呼ばれる関数)です。
Promiseの利点:多重にネストされたコールバックのエラー処理は泥沼状態になりがちです。Promiseは起きているエラーに対する明確なパスを提示し、適切に処理することができます。更に、解決/拒否の後もPromiseの値は不変で、決して変わることがありません。
Promiseを使用した具体例を挙げます。
var fetchJSON = function(url) { return new Promise((resolve, reject) => { $.getJSON(url) .done((json) => resolve(json)) .fail((xhr, status, err) => reject(status + err.message)); }); };
Promise.all()
を使えば、Promiseを並列化して非同期処理の配列を扱うこともできます。
var urls = [ 'http://www.api.com/items/1234', 'http://www.api.com/items/4567' ]; var urlPromises = urls.map(fetchJSON); Promise.all(urlPromises) .then(function (results) { results.forEach(function (data) { }); }) .catch(function (err) { console.log('Failed: ', err); });
(目次へ戻る)
ジェネレータ
Promiseがどのようにしてコールバック地獄を解消するのに役立つかという話とも似ているのですが、ジェネレータはコードをフラット化するのに役立ちます。つまり、非同期コードを同期的に処理できます。ジェネレータは本質的に、実行を一時停止する関数で、結果的に式の値を返します。
ジェネレータを利用したシンプルな例を下記に挙げます。
function* sillyGenerator() { yield 1; yield 2; yield 3; yield 4; } var generator = sillyGenerator(); var value = generator.next(); > console.log(value); // { value: 1, done: false } > console.log(value); // { value: 2, done: false } > console.log(value); // { value: 3, done: false } > console.log(value); // { value: 4, done: false }
nextはジェネレータを更に押し進め、新しい式を評価します。上記の例は非常によく練られていますが、ジェネレータを使えば、同期的な手法で非同期コードを書くことができます。
// Hiding asynchronousity with Generators function request(url) { getJSON(url, function(response) { generator.next(response); }); }
ここではデータを返すジェネレータ関数を書いています。
function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2); }
yield
の力によって、entry1
がパースに必要なデータを備え、data1
に保存されることが保証されます。
ジェネレータを用いれば同期的な方法で非同期コードが書けます。しかし、エラー伝搬に対する明確で簡単なパスはありませんから、Promiseでジェネレータを補います。
function request(url) { return new Promise((resolve, reject) => { getJSON(url, resolve); }); }
それからnext
を使ってジェネレータをたどっていく関数を書きます。この時、今度は上記のメソッドrequest
を活用してPromiseを生成します。
function iterateGenerator(gen) { var generator = gen(); var ret; (function iterate(val) { ret = generator.next(); if(!ret.done) { ret.value.then(iterate); } })(); }
Promiseでジェネレータを補うことにより、Promiseの.catch
とreject
を使ってエラーを伝搬する明確な方法が手に入りました。新しく補強されたジェネレータの使用法は以前と同様に簡単です。
iterateGenerator(function* getData() { var entry1 = yield request('http://some_api/item1'); var data1 = JSON.parse(entry1); var entry2 = yield request('http://some_api/item2'); var data2 = JSON.parse(entry2); });
以前と同じようにジェネレータを使い、実装を再利用することができ、とても便利ですね。ジェネレータとPromiseを使えば同期的な方法で非同期コードを書くことができ、優れたエラー伝搬の能力を取得できますが、同等のメリットがある、もっとシンプルな構造もご紹介しましょう。async awaitです。
(目次へ戻る)
Async/Await
ES2016から搭載予定の機能async await
を使えば、ジェネレータとPromiseを使って達成したのと同等の内容を、より少ない労力で達成することができます。
var request = require('request'); function getJSON(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body); }); }); } async function main() { var data = await getJSON(); console.log(data); // NOT undefined! } main();
内部的にはジェネレータと似たような挙動ですが、ジェネレータとPromiseの組み合わせよりもこちらの利用を強くお勧めします。ES7とBabelをすぐ活用したいなら、優れた資料がこちらにあります。
(目次へ戻る)