はじめに
こんにちは。東京も猛暑続きで、バテ気味ですが、ブログはしっかり書いていこうと思います。
先月よりRE:ENGINESは、あるスタートアップ企業からの依頼でReact.jsでサービス開発を行うことになりました。そこで、今回は改めてReactを始める前に最低限知っておきたいES2015/ES6の機能を紹介しようと思います。
ES2015の全ての機能を網羅している訳ではありませんが、従来のJavaScript(以降 ES5)の知識があった上で、このブログをお読みいただければ、ReactをES2015で始められるようになると思います。また、今後はReact.jsの記事も書いていこうと思います。
ECMAScript – ES2015/ES6とは
ECMAScriptとは、非営利の国際標準化団体である Ecma Internationalが定めた仕様になります。そして、JavaScriptは、その仕様を実装したものです。
Ecma Internationalは、2011年6月にECMAScriptのバージョン5.1(ES5)を公表し、現在使われているほとんどのブラウザでこの仕様を対応しています。トランスパイルをせずにJavaScriptを利用して、動きのあるページを作ったことのある方であれば、おそらくはこのES5の仕様で開発されていたと思います。
その後、Ecmaは2015年6月にES2015/ES6を公表しました。これは、2015年に公表されたので、ECMAScript 2015やES2015と呼ばれることもあれば、バージョン6ということで、ECMAScript 6やES6と呼ばれることもあります。ES6の方が一般に浸透しているようですが、ES2015が正式名称のようですので、以降はES2015と表記します。
ES2015の仕様に対して各社のブラウザ対応も徐々に進み、またBabelといったトランスパイルで従来のブラウザでも動くES5への変換も手軽にできるようになったことから、最近の新規開発はほぼES2015の仕様を実装したJavaScriptで行われるようになっているのではないでしょうか。
特にReact.jsのプロジェクトでは、トランスパイルが前提のため、ES2015で実装するのが当然のようになっています。
それでは、次から具体的にES2015で追加された主な機能を見ていきましょう。
変数と定数とスコープ
ES5までは、 var を使っていたり、 var すら使わずに変数に値を代入したりしていたのではないでしょうか。ES2015からこの var に加えて、ブロックスコープな変数を表す let と同じくブロックスコープな定数を表す const が追加されました。 let は変数なので再代入可能ですが、 const は定数なので再代入するとエラーとなります。
それでは、実際にコードを見て見ましょう。
1 2 3 4 | for (var i = 0; i < 2; i++) { console.log(i); // 0 1 と順に表示される } console.log(i); // 2と出力される |
1 2 3 4 | for (let i = 0; i < 2; i++) { console.log(i); // 0 1 と順に表示される } console.log(i); // ReferenceError: i is not defined |
var と let では、for文の終了後に変数 i が参照できるかできないかの違いがあります。ブロックスコープである let では、for文のブロックを抜けると参照できなくなりますが、関数スコープである var では、for文のブロックを抜けたあとでもそのまま参照が可能です。他の言語を習得されている方であれば、let の挙動の方が慣れているのではないでしょうか。
次は const を見てみましょう。
1 2 | const x = 10; x = 11; // TypeError: Assignment to constant variable. |
定数のため、再代入を行おうとするとエラーが発生します。私は他の言語で開発するときも値が変更されないものは、定数で値を保持するようにしています。
ES2015で開発をする場合は、var ではなく、この let と constで値を保持し、 var は限定的な場所での利用になると思います。
テンプレート文字列
文字列の中で変数を展開するための機能です。
ES5までは、+ を使って文字列を「連結」させることで表現していましたが、ES2015からはバッククォート「`」で、囲むと変数などの値が展開されます。他の言語でもある仕様なので、もうお分かりと思いますが、サンプルでES5の時とES2015を比較掲載しておきます。
1 2 3 4 5 6 7 8 9 | let name = "Alice"; // ES5までに方法(文字列を連結される) const message1 = "Hi, " + name + "! How are you?"; console.log(message1); // Hi, Alice! How are you? // ES2015から可能な方法(変数 name が展開される) const message2 = `Hi, ${name}! How are you?`; console.log(message2); // Hi, Alice! How are you? |
今回のサンプルでは、message1とmessage2を見比べると、message2の方が読みやすいのではないでしょうか。状況によって、+ で連結させることもありますが、多くの場合は、このテンプレート文字列を利用する方が読みやすいと思います。
分割代入
分割代入は、Reactでの開発でもよく利用しますので、ぜひ覚えてください。
それでは、早速コードを見てみましょう。
1 2 3 4 5 | const obj = { x: 10, y: 11, z: 12 } const { x, y, a } = obj console.log(x); // 10 console.log(y); // 11 console.log(a); // undefined |
定数 x, y に objのプロパティ x, y が分割されて代入されています。一方で定数 a は、obj に a と一致するプロパティがないので、 undefined となっています。
また、分割代入は配列でも使えます。
1 2 3 4 | const arr = [10, 11, 12, 13]; const [x, y] = arr console.log(x); // 10 console.log(y); // 11 |
定数 x, y には、arr の0番目と1番目の要素が代入されています。
デフォルト引数
関数やメソッドの引数にデフォルト値を指定できるものです。これは、Rubyなど他の言語でもよくある仕様なので、サンプルだけ掲載しておきます。
1 2 3 4 5 6 7 | function hello(word = "hello", name = "world") { console.log(`${word}, ${name}`) } hello(); // hello, world hello("こんにちは"); // こんにちは, world hello("やあ", "太郎"); // やあ、太郎 |
通常、実引数が指定されないと undefined が仮引数に代入されますが、サンプルのようにデフォルト引数を指定すると、undefined ではなく、指定のデフォルト引数が代入されます。
残余引数(レストパラメータ)
関数の引数の個数が決まっていない場合にレストパラメータが利用できます。(可変長引数と呼ぶ言語もあると思います。)
ES5では、arguments という特別な変数を使って同じようなことを実現していましたが、ES2015からは下記のサンプルのように、関数定義の最後の引数に展開演算子(…)を利用することでより直感的に使えるようになりました。
1 2 3 4 5 6 7 8 9 10 | function sayHello(word, ...members) { for (let i = 0; i < members.length; i++) { console.log(`${word}, ${members[i]}!`); } } sayHello("Hi", "Alice", "Bob", "Carol"); // Hi, Alice! // Hi, Bob! // Hi, Carol! |
“Hi” が sayHello関数の第1引数であるwordに渡され、残りの引数がmembers配列に0番目から順に渡されているのがわかると思います。
展開(スプレッド)演算子
既に上記の残余引数の説明でも登場しましたが、展開演算子の別の使い方を紹介します。
ここでのサンプルでは、変数を関数に渡す場合に展開させる方法です。
1 2 3 4 5 6 7 8 9 | function showProfile(name, age, hobby) { console.log(`I am ${name}, ${age} years old.`); console.log(`My hobby is ${hobby}.`); } // 展開演算子で配列のそれぞれの要素が、引数に展開されます const alice = ["Alice", 30, "watching movies"] showProfile(...alice); // I am Alice, 30 years old. // My hobby is watching movies. |
配列の0番目の要素が、第1引数に、1番目の要素が第2引数に、2番目の引数が第3引数に渡されていいます。
for..ofでの繰り返し処理
集合に対して繰り返し処理を行う場合に利用する機能です。今回のブログ記事では詳細に記載しませんが、イテレーション可能なオブジェクトに使うことができます。既にJavaScriptには、for 文はあるので、それとどう違うのかをサンプルを見ながら理解していただければと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const players = ["Alice", "Bob", "Carol", "Dave", "Eve"]; // ES5 for (var i = 0; i < players.length; i++) { console.log(players[i]); } // ES2015 for (let player of players) { console.log(player); } // Alice // Bob // Carol // Dave // Eve |
各要素のインデックスが不要である場合は、従来の方式よりも for…ofループの方が見やすいですね。
このfor…ofループは冒頭で触れたとおり、配列だけでなく、イテレーション可能なオブジェクトにも使うことができ、次に説明するMapなどでも利用可能です。
MapとSet
Map は、キーと値の対応関係を表現するために利用します。ES5まではオブジェクトを利用して表現していたものです。オブジェクトとは異なり、キーには文字列・シンボルだけでなく、オブジェクトなども使うことが可能であったり、イテレーション可能であったり、純粋にハッシュマップとしての利用であれば、オブジェクトよりもMapの方が優れている点があります。
それでは、サンプルを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // Mapオブジェクトを生成します。 const players = new Map(); // Mapにkeyとvalueの対応をセットします。(チェインできます) players .set('Alice', 30) .set('Bob', 25) .set('Carol', 35) .set('Dave', 18); // 全てのキーを取得します for (let name of players.keys()) { console.log(name); } // Alice // Bob // Carol // Dave // 全ての値を取得します。 for (let age of players.values()) { console.log(age); } // 30 // 25 // 35 // 18 // 全てのキーと値を取得します。 for (let [name, age] of players) { console.log(`${name} is ${age} years old.`); } // Alice is 30 years old. // Bob is 25 years old. // Carol is 35 years old. // Dave is 18 years old. // 削除します。 players.delete('Alice'); console.log(players); // Map { 'Bob' => 25, 'Carol' => 35, 'Dave' => 18 } |
次に Set ですが、これは重複を許さない集合になります。これも単純なのでサンプルだけ掲載しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // Setオブジェクトを生成します。 const players = new Set(); // Setに値を追加していきます。(チェインできます。) players .add("Alice") .add("Bob") .add("Carol") .add("Dave"); console.log(players); // Set { 'Alice', 'Bob', 'Carol', 'Dave' } // Aliceをもう一度追加しても結果は変わらない players.add("Alice"); console.log(players); // Set { 'Alice', 'Bob', 'Carol', 'Dave' } // イテレーション可能です。 for (let player of players) { console.log(player); } // 削除します。 players.delete("Alice"); console.log(players); // Set { 'Bob', 'Carol', 'Dave' } |
Map と Set には、それぞれ弱参照となるWeakMap と WeakSet があります。興味のある方は文末に記載した参考よりご確認ください。
モジュールのexportとimport
ブログラムの規模が大きくなると複数のファイルに分けて、関数やクラスを管理したくなると思います。そこで、ES6からはファイル名をモジュール名として扱い、ファイル単位でモジュールを管理できるようになりました。
モジュール内のクラスや関数で、外部のモジュールに公開しても良いものにexport属性をつけ、外部のモジュールからは、import文を使うことでexportされたクラスや関数を利用できます。
下記のサンプルでは、3つのファイル(モジュール)に分割されている前提でご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 | // member.jsファイル export class MemberTable {} export class SubMemberTable {} // audience.jsファイル export default class AudienceTable {} // index.jsファイル // memberモジュールから2つのクラスをimportする import { MemberTable, SubMemberTable } from 'member'; // audienceモジュールからdefaultとしてexportされているクラスをimportする import AudienceTable from 'audience'; |
アロー関数
関数を定義する方法でアロー関数という表記法が使えるようになりました。無名関数などで利用されることが多いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const sayHello1 = function() { console.log("hello_1"); } const sayHello2 = () => console.log("hello_2"); const say1 = function(greeting, word) { console.log(`${greeting}, ${word}_1`); } const say2 = (greeting, word) => console.log(`${greeting}, ${word}_2`); sayHello1(); // hello_1 sayHello2(); // hello_2 say1("hello", "world"); // hello, world_1 say2("hello", "world"); // hello, world_2 // 無名関数での利用 const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; evenNumbers = numbers.filter(number => number % 2 == 0); console.log(evenNumbers); // [ 2, 4, 6, 8, 10 ] |
アロー関数は慣れないと逆に読みにくく感じてしまうこともあるかもしれませんが、無名関数を別の関数の引数に渡す場合などで今後は多用すると思うので、ぜひ慣れてください。
クラス定義
ES2015から class という構文が導入されました。ES5からある prototype を利用して実現しているだけではあるのですが、他の言語と同じように class を宣言したり、利用したりできるので、これもぜひ覚えておきたいものになります。
詳しい説明は割愛しますが、サンプルとして、クラスの宣言、継承、メソッド、静的メソッド、getter/setterなどの主要な機能を掲載しますので、ご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class Person { // コンストラクタ constructor(firstName, lastName, age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } sayHello() { console.log(`Hello, I am ${this.fullName}.`); } // getter instace.fullNameでアクセスできる get fullName() { return `${this.firstName} ${this.lastName}`; } // setter instanc.fullName = "first last"で設定する set fullName(value) { [this.firstName, this.lastName] = value.split(" "); } } // グローバル変数 let driverCount = 0; class Driver extends Person { // コンストラクタ constructor(firstName, lastName, age, carType) { super(firstName, lastName, age); // 親クラスのコンストラクタを呼び出す this.carType = carType; driverCount += 1; } sayHello() { super.sayHello(); // 親クラスのメソッドを呼び出す console.log(`I am driving a ${this.carType}.`); } // staticなgetterメソッド static get driverCount() { return driverCount; } } const alice = new Driver("Alice", "White", 30, "F1"); alice.sayHello(); // Hello, I am Alice White.\nI am driving a F1. console.log(Driver.driverCount); // 1 const bob = new Driver("Bob", "Brown", 25, "Sedan"); bob.sayHello(); // Hello, I am Bob Brown.\nI am driving a Sedan. console.log(Driver.driverCount); // 2 bob.fullName = "Carol Black" bob.sayHello(); // Hello, I am Carol Black.\nI am driving a Sedan. |
非同期処理 Promise
ES5など従来の方式で非同期処理を行う場合は、コールバックで記載していたと思います。一定時間後に処理を行う setTimeout の第一引数に渡す関数はまさにコールバック関数となります。ただ、コールバック処理でさらに非同期処理を行おうとするとコールバック地獄というような状態になり、非常に可読性が悪くなります。そこで、コールバックの欠点を補うために、Promiseが考案されました。(ただ、ES2017のasync/awaitの方がさらに可読性が良いと思います。)
サンプルでは、setTimeoutを使い擬似的に非同期処理を表現しています。詳細はサンプル内のコメントをご確認ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | function sayAfter(seconds) { // Promiseのコンストラクタの引数に非同期処理を行う関数を渡す return new Promise((successCallback, failureCallback) => { // 今回は指定の秒数後に乱数の値を生成するだけの処理 setTimeout(() => { successCallback(`${Math.floor(Math.random() * 10)}`); }, seconds) }) } // 非同期処理が上から順に処理されてます。 // 約4秒後にメッセージが表示されます。 let longMessage = ""; sayAfter(1000) .then((msg) => { longMessage += msg; return sayAfter(1000); }) .then((msg) => { longMessage += msg; return sayAfter(1000); }) .then((msg) => { longMessage += msg; return sayAfter(1000); }) .then((msg) => { longMessage += msg console.log(`Promise chaining ${longMessage}`) }).catch((error) => console.log(error)); // failureCallbackが呼ばれるとここに処理が来ます // この例では順序は関係ないので同時に処理させることも可能です。 // 約1秒後にメッセージが表示されます。 Promise.all([sayAfter(1000), sayAfter(1000), sayAfter(1000), sayAfter(1000)]) .then((results) => { const longMessage = results.reduce((acc, result) => acc + result, ""); console.log(`Promise all ${longMessage}`); }); // Promise all 9860 // Promise chaining 9457 |
おまけ1(ES2017の機能)
async/await
ES2017で追加された仕様の一つである、async / awaitについて少しだけ触れておきます。既に紹介した Promiseチェイニング のサンプルと見比べるとわかると思いますが、手続き的に呼ばれており、例外処理も単純で可読性が高いとと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | function sayAfter(seconds) { // Promiseのコンストラクタの引数に非同期処理を行う関数を渡す return new Promise((successCallback, failureCallback) => { // 今回は指定の秒数後に乱数の値を生成するだけの処理 setTimeout(() => { successCallback(`${Math.floor(Math.random() * 10)}`); }, seconds) }) } async function sayAll() { try { let longMessage = await sayAfter(1000); longMessage += await sayAfter(1000); longMessage += await sayAfter(1000); longMessage += await sayAfter(1000); console.log(`async/await ${longMessage}`) } catch (e) { console.log(e); } } sayAll(); |
おまけ2(ES5でもおさえておきたい機能)
プリミティブ型
一覧だけ掲載しておきます。SymbolがES2015からの機能で、個人的には定数か列挙型を表現するときに使えるかなというところです。興味のある方は末尾の参考をご確認ください。
型 | 補足説明 |
数値(Number) | 倍精度浮動小数点数。整数型、浮動小数点型の区別がなく、数値を表すデータ型は一つしかない。 |
文字列(String) | Unicodeのテキスト。 |
論理値(Boolean) | true/false。「undefined/null/false/0/NaN/空文字(”)」は「false」、それ以外は「true」となる |
null | 値がまだ不確定の状態でnullを代入したりして利用する |
undefined | まだ値が指定されていないことを表す |
シンボル(Symbol) | ES2015からの機能。作成したシンボルはユニークになる。 |
厳密等価演算子(===)と等価演算子(==)
演算子 | trueとなる条件(いずれかが満たされる場合) |
厳密等価演算子(===) | ・同じオブジェクトを参照している場合 ・プリミティブ型でデータの型も同じである場合 |
等価演算子(==) | ・厳密等価である場合 ・同じオブジェクトを参照している場合 ・同じ値に変換される場合 |
サンプルも掲載しておきます。
特に最後の「同じ値に変換される場合」は、要注意です。基本的には、厳密等価演算子を利用する方が安全でしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // オブジェクトの比較 const obj1 = { message: 'object' } const obj2 = { message: 'object' } const obj3 = obj1 console.log(obj1 === obj2); // false console.log(obj1 === obj3); // true console.log(obj1 == obj2); // false console.log(obj1 == obj3); // true // プリミティブ型の比較 const num = 10; const str = "10"; console.log(num === str); // false console.log(num == str); // true(型は異なるが同じ値に変換される) |
数値の比較(NaN)
NaN(Not A Number)と比較する際に注意すべきことがあるので、おまけで記載しておきます。
サンプルをみていただければ、直ぐに理解できると思いますが、数値でないことを確認する場合は、組み込み関数の isNaN を使うべきということです。
1 2 3 4 5 6 7 8 9 | const notANumber = "3a"; console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(notANumber == NaN); // false console.log(notANumber === NaN); // false console.log(isNaN(NaN)); // true console.log(isNaN(notANumber)); // true |
最後の isNaN を利用した結果のみが意図した結果になっていると思います。
さいごに
ES2015には、ここに掲載した内容以外にも仕様が追加されていたり、便利な利用方法が多くありますが、ギュッと要点だけに絞って、紹介させていただきました。(イテレータ・ジェネレータも追加された仕様ですが、紹介していません。詳しくは、末尾の参考をご確認ください。)
JavaScriptなどフロントエンドまわりは、流行り廃りが激しい領域だと思います。ただ、今後React.jsやVue.jsで開発される場合は、ES2015以降のシンタックスで書くことになると思いますので、JavaScriptがES5の知識で止まっている場合は、知識のアップデートをぜひ行って、モダンなフロントエンドの開発を行ってみてください。
今後も当ブログでは、React.jsなどの記事も掲載していきたいと思っています。
参考
ES2015のブラウザ実装状況
ECMAScript6にシンボルができた理由
イテレーターとジェネレーター
WeakMap
WeakSet