この章では、ES6 の主要な機能について説明します。これらの機能は簡単に採用できます。残りの機能は主にライブラリ作成者にとって興味深いものです。各機能について、対応する ES5 コードを使って説明します。
var から const/let へfor から forEach() 、そして for-of へarguments からレストパラメータへapply() からスプレッド演算子 (...) へMath.max()Array.prototype.push()concat() からスプレッド演算子 (...) へError のサブクラスへArray.prototype.indexOf から Array.prototype.findIndex へArray.prototype.slice() から Array.from() またはスプレッド演算子へapply() から Array.prototype.fill() へvar から const/let へ ES5 では、var を使って変数を宣言します。このような変数は *関数スコープ* であり、そのスコープは最も内側の囲み関数です。var の動作は時に紛らわしいことがあります。次に例を示します。
var x = 3;
function func(randomize) {
if (randomize) {
var x = Math.random(); // (A) scope: whole function
return x;
}
return x; // accesses the x from line A
}
func(false); // undefined
func() が undefined を返すのは驚きかもしれません。実際に何が起こっているかをより正確に反映するようにコードを書き換えると、その理由がわかります。
var x = 3;
function func(randomize) {
var x;
if (randomize) {
x = Math.random();
return x;
}
return x;
}
func(false); // undefined
ES6 では、追加で let および const を使用して変数を宣言できます。このような変数は *ブロックスコープ* であり、そのスコープは最も内側の囲みブロックです。let は、おおまかに言えば var のブロックスコープ版です。const は let と同様に機能しますが、値を変更できない変数を作成します。
let と const はより厳密に動作し、より多くの例外をスローします(例:宣言される前にスコープ内で変数にアクセスした場合)。ブロックスコープは、コードフラグメントの効果をより局所的に保つのに役立ちます(デモについては次のセクションを参照)。また、関数スコープよりも主流であり、JavaScript と他のプログラミング言語間の移動が容易になります。
最初のバージョンで var を let に置き換えると、異なる動作になります。
let x = 3;
function func(randomize) {
if (randomize) {
let x = Math.random();
return x;
}
return x;
}
func(false); // 3
つまり、既存のコードで var を let または const で盲目的に置き換えることはできません。リファクタリング中は注意が必要です。
私のアドバイスは以下のとおりです。
const を優先します。値が変更されないすべての変数に使用できます。let を使用します。var は避けてください。詳細情報: “変数とスコープ” の章。
ES5 では、変数 tmp のスコープをブロックに制限したい場合は、IIFE (即時実行関数式) と呼ばれるパターンを使用する必要がありました。
(function () { // open IIFE
var tmp = ···;
···
}()); // close IIFE
console.log(tmp); // ReferenceError
ECMAScript 6 では、ブロックと let 宣言 (または const 宣言) を使用するだけで済みます。
{ // open block
let tmp = ···;
···
} // close block
console.log(tmp); // ReferenceError
詳細情報: “ES6 で IIFE を避ける” のセクション。
ES6 では、JavaScript に文字列補間と複数行文字列のリテラルがついに導入されました。
ES5 では、文字列に値を入れるには、それらの値と文字列フラグメントを連結します。
function printCoord(x, y) {
console.log('('+x+', '+y+')');
}
ES6 では、テンプレートリテラルによる文字列補間を使用できます。
function printCoord(x, y) {
console.log(`(${x}, ${y})`);
}
テンプレートリテラルは、複数行文字列の表現にも役立ちます。
たとえば、ES5 でそれを表現するには、次のようにする必要があります。
var HTML5_SKELETON =
'<!doctype html>\n' +
'<html>\n' +
'<head>\n' +
' <meta charset="UTF-8">\n' +
' <title></title>\n' +
'</head>\n' +
'<body>\n' +
'</body>\n' +
'</html>\n';
バックスラッシュを使って改行をエスケープすると、少し見栄えが良くなります(ただし、明示的に改行を追加する必要があります)。
var HTML5_SKELETON = '\
<!doctype html>\n\
<html>\n\
<head>\n\
<meta charset="UTF-8">\n\
<title></title>\n\
</head>\n\
<body>\n\
</body>\n\
</html>';
ES6 のテンプレートリテラルは複数行にまたがることができます。
const HTML5_SKELETON = `
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
</html>`;
(例では、含まれる空白の量が異なりますが、この場合は問題ありません。)
詳細情報: “テンプレートリテラルとタグ付きテンプレート” の章。
現在の ES5 コードでは、関数式を使用している場合は常に、this に注意する必要があります。次の例では、UiComponent の this が B 行でアクセスできるように、ヘルパー変数 _this (A 行) を作成します。
function UiComponent() {
var _this = this; // (A)
var button = document.getElementById('myButton');
button.addEventListener('click', function () {
console.log('CLICK');
_this.handleClick(); // (B)
});
}
UiComponent.prototype.handleClick = function () {
···
};
ES6 では、this をシャドウしないアロー関数を使用できます (A 行)。
function UiComponent() {
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('CLICK');
this.handleClick(); // (A)
});
}
(ES6 では、コンストラクタ関数の代わりにクラスを使用することもできます。これについては後で説明します。)
アロー関数は、式の結果のみを返す短いコールバックに特に便利です。
ES5 では、このようなコールバックは比較的冗長です。
var arr = [1, 2, 3];
var squares = arr.map(function (x) { return x * x });
ES6 では、アロー関数ははるかに簡潔です。
const arr = [1, 2, 3];
const squares = arr.map(x => x * x);
パラメータを定義するとき、パラメータが単一の識別子である場合は、括弧を省略することもできます。したがって、(x) => x * x と x => x * x はどちらも許可されます。
詳細情報: “アロー関数” の章。
一部の関数またはメソッドは、配列またはオブジェクトを介して複数の値を返します。ES5 では、これらの値にアクセスする場合は常に、中間変数を作成する必要があります。ES6 では、分割代入によって中間変数を回避できます。
exec() は、Array のようなオブジェクトを介してキャプチャされたグループを返します。ES5 では、グループのみに関心がある場合でも、中間変数 (以下の例では matchObj) が必要です。
var matchObj =
/^(\d\d\d\d)-(\d\d)-(\d\d)$/
.exec('2999-12-31');
var year = matchObj[1];
var month = matchObj[2];
var day = matchObj[3];
ES6 では、分割代入によってこのコードが簡略化されます。
const [, year, month, day] =
/^(\d\d\d\d)-(\d\d)-(\d\d)$/
.exec('2999-12-31');
配列パターンの先頭にある空のスロットは、インデックス 0 の配列要素をスキップします。
メソッド Object.getOwnPropertyDescriptor() は、*プロパティ記述子* (プロパティに複数の値を保持するオブジェクト) を返します。
ES5 では、オブジェクトのプロパティのみに関心がある場合でも、中間変数 (以下の例では propDesc) が必要です。
var obj = { foo: 123 };
var propDesc = Object.getOwnPropertyDescriptor(obj, 'foo');
var writable = propDesc.writable;
var configurable = propDesc.configurable;
console.log(writable, configurable); // true true
ES6 では、分割代入を使用できます。
const obj = { foo: 123 };
const {writable, configurable} =
Object.getOwnPropertyDescriptor(obj, 'foo');
console.log(writable, configurable); // true true
{writable, configurable} は、
{ writable: writable, configurable: configurable }
詳細情報: “分割代入” の章。
for から forEach() 、そして for-of へ ES5 以前は、次のように配列を反復処理しました。
var arr = ['a', 'b', 'c'];
for (var i=0; i<arr.length; i++) {
var elem = arr[i];
console.log(elem);
}
ES5 では、Array メソッド forEach() を使用できます。
arr.forEach(function (elem) {
console.log(elem);
});
for ループには、そこから中断できるという利点があり、forEach() には簡潔さという利点があります。
ES6 では、for-of ループが両方の利点を兼ね備えています。
const arr = ['a', 'b', 'c'];
for (const elem of arr) {
console.log(elem);
}
各配列要素のインデックスと値の両方が必要な場合、for-of は、新しい Array メソッド entries() と分割代入を介して、それらにも対応できます。
for (const [index, elem] of arr.entries()) {
console.log(index+'. '+elem);
}
詳細情報: “for-of ループ” の章。
ES5 では、次のようにパラメータのデフォルト値を指定します。
function foo(x, y) {
x = x || 0;
y = y || 0;
···
}
ES6 には、より優れた構文があります。
function foo(x=0, y=0) {
···
}
追加の利点は、ES6 では、パラメータのデフォルト値は undefined によってのみトリガーされるのに対し、以前の ES5 コードでは任意の偽の値によってトリガーされることです。
詳細情報: “パラメータのデフォルト値” のセクション。
JavaScript でパラメータに名前を付ける一般的な方法は、オブジェクトリテラル (いわゆる *オプションオブジェクトパターン*) を使用することです。
selectEntries({ start: 0, end: -1 });
このアプローチの 2 つの利点は、コードがより自己記述的になることと、任意のパラメータを省略するのが簡単になることです。
ES5 では、selectEntries() を次のように実装できます。
function selectEntries(options) {
var start = options.start || 0;
var end = options.end || -1;
var step = options.step || 1;
···
}
ES6 では、パラメータ定義で分割代入を使用でき、コードが簡略化されます。
function selectEntries({ start=0, end=-1, step=1 }) {
···
}
ES5 でパラメータ options をオプションにするには、A 行をコードに追加します。
function selectEntries(options) {
options = options || {}; // (A)
var start = options.start || 0;
var end = options.end || -1;
var step = options.step || 1;
···
}
ES6 では、{} をパラメータのデフォルト値として指定できます。
function selectEntries({ start=0, end=-1, step=1 } = {}) {
···
}
詳細情報: “名前付きパラメータのシミュレーション” のセクション。
arguments からレストパラメータへ ES5 では、関数 (またはメソッド) で任意の数の引数を受け入れるようにする場合は、特殊な変数 arguments を使用する必要があります。
function logAllArguments() {
for (var i=0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
ES6 では、... 演算子を使用して、レストパラメータ (以下の例では args) を宣言できます。
function logAllArguments(...args) {
for (const arg of args) {
console.log(arg);
}
}
末尾のパラメータのみに関心がある場合、レストパラメータはさらに優れています。
function format(pattern, ...args) {
···
}
ES5 でこのケースを処理するのは面倒です。
function format(pattern) {
var args = [].slice.call(arguments, 1);
···
}
レストパラメータを使用すると、コードが読みやすくなります。パラメータ定義を見るだけで、関数に可変数のパラメータがあることがわかります。
詳細情報: “レストパラメータ” のセクション。
apply() からスプレッド演算子 (...) へ ES5 では、apply() を使用して配列をパラメータに変換します。ES6 には、この目的のためのスプレッド演算子があります。
Math.max() Math.max() は、その引数の中で数値的に最大なものを返します。任意の数の引数に対して機能しますが、配列に対しては機能しません。
ES5 – apply()
> Math.max.apply(Math, [-1, 5, 11, 3])
11
ES6 – スプレッド演算子
> Math.max(...[-1, 5, 11, 3])
11
Array.prototype.push() Array.prototype.push() は、その引数をすべて要素としてレシーバーに追加します。配列を別の配列に破壊的に追加するメソッドはありません。
ES5 – apply()
var arr1 = ['a', 'b'];
var arr2 = ['c', 'd'];
arr1.push.apply(arr1, arr2);
// arr1 is now ['a', 'b', 'c', 'd']
ES6 – スプレッド演算子
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
arr1.push(...arr2);
// arr1 is now ['a', 'b', 'c', 'd']
詳細情報: 「スプレッド演算子 (...)」のセクションを参照してください。
concat() からスプレッド演算子 (...) へ スプレッド演算子は、(非破壊的に)オペランドの内容を配列要素に変換することもできます。つまり、配列メソッド concat() の代替となります。
ES5 – concat()
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
console.log(arr1.concat(arr2, arr3));
// [ 'a', 'b', 'c', 'd', 'e' ]
ES6 – スプレッド演算子
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
console.log([...arr1, ...arr2, ...arr3]);
// [ 'a', 'b', 'c', 'd', 'e' ]
詳細情報: 「スプレッド演算子 (...)」のセクションを参照してください。
JavaScript では、メソッドは値が関数であるプロパティです。
ES5 のオブジェクトリテラルでは、メソッドは他のプロパティと同様に作成されます。プロパティの値は、関数式を介して提供されます。
var obj = {
foo: function () {
···
},
bar: function () {
this.foo();
}, // trailing comma is legal in ES5
}
ES6 には、メソッドを作成するための特別な構文であるメソッド定義があります。
const obj = {
foo() {
···
},
bar() {
this.foo();
},
}
詳細情報: 「メソッド定義」のセクションを参照してください。
ES6 のクラスは、ほとんどがコンストラクター関数に対するより便利な構文にすぎません。
ES5 では、コンストラクター関数を直接実装します。
function Person(name) {
this.name = name;
}
Person.prototype.describe = function () {
return 'Person called '+this.name;
};
ES6 では、クラスはコンストラクター関数に対するわずかに便利な構文を提供します。
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person called '+this.name;
}
}
メソッド定義のコンパクトな構文に注目してください。キーワード function は不要です。また、クラスの各部分の間にカンマがないことにも注意してください。
ES5 では、特にスーパークラスのコンストラクターやスーパープロパティを参照する場合、サブクラス化は複雑です。以下は、Person のサブコンストラクター Employee を作成するための標準的な方法です。
function Employee(name, title) {
Person.call(this, name); // super(name)
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.describe = function () {
return Person.prototype.describe.call(this) // super.describe()
+ ' (' + this.title + ')';
};
ES6 には、extends 句を介したサブクラス化の組み込みサポートがあります。
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}
describe() {
return super.describe() + ' (' + this.title + ')';
}
}
詳細情報: 「クラス」の章を参照してください。
Error のサブクラスへ ES5 では、例外のための組み込みコンストラクターである Error をサブクラス化することは不可能です。次のコードは、MyError コンストラクターにスタックトレースなどの重要な機能を与えるための回避策を示しています。
function MyError() {
// Use Error as a function
var superInstance = Error.apply(null, arguments);
copyOwnPropertiesFrom(this, superInstance);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;
function copyOwnPropertiesFrom(target, source) {
Object.getOwnPropertyNames(source)
.forEach(function(propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
};
ES6 では、すべての組み込みコンストラクターをサブクラス化できます。そのため、次のコードは ES5 のコードがシミュレートするしかないことを実現できます。
class MyError extends Error {
}
詳細情報: 「組み込みコンストラクターのサブクラス化」のセクションを参照してください。
文字列から任意の値を対応付けるマップ(データ構造)として言語構造であるオブジェクトを使用することは、JavaScript では常に一時的な解決策でした。これを行う最も安全な方法は、プロトタイプが null のオブジェクトを作成することです。それでも、キーが文字列 '__proto__' にならないようにする必要があります。これは、そのプロパティキーが多くの JavaScript エンジンで特別な機能をトリガーするためです。
次の ES5 コードには、オブジェクト dict をマップとして使用する関数 countWords が含まれています。
var dict = Object.create(null);
function countWords(word) {
var escapedWord = escapeKey(word);
if (escapedWord in dict) {
dict[escapedWord]++;
} else {
dict[escapedWord] = 1;
}
}
function escapeKey(key) {
if (key.indexOf('__proto__') === 0) {
return key+'%';
} else {
return key;
}
}
ES6 では、組み込みデータ構造 Map を使用でき、キーをエスケープする必要はありません。欠点として、Map 内の値を増分するのはあまり便利ではありません。
const map = new Map();
function countWords(word) {
const count = map.get(word) || 0;
map.set(word, count + 1);
}
Map のもう 1 つの利点は、文字列だけでなく任意の値をキーとして使用できることです。
詳細情報
ECMAScript 6 標準ライブラリは、文字列に対していくつかの新しいメソッドを提供します。
indexOf から startsWith へ
if (str.indexOf('x') === 0) {} // ES5
if (str.startsWith('x')) {} // ES6
indexOf から endsWith へ
function endsWith(str, suffix) { // ES5
var index = str.indexOf(suffix);
return index >= 0
&& index === str.length-suffix.length;
}
str.endsWith(suffix); // ES6
indexOf から includes へ
if (str.indexOf('x') >= 0) {} // ES5
if (str.includes('x')) {} // ES6
join から repeat へ (文字列を繰り返す ES5 の方法は、ハックのようなものです)
new Array(3+1).join('#') // ES5
'#'.repeat(3) // ES6
詳細情報: 「新しい文字列機能」の章
ES6 には、いくつかの新しい配列メソッドもあります。
Array.prototype.indexOf から Array.prototype.findIndex へ 後者は NaN を見つけるために使用できますが、前者は検出できません。
const arr = ['a', NaN];
arr.indexOf(NaN); // -1
arr.findIndex(x => Number.isNaN(x)); // 1
ちなみに、新しい Number.isNaN() は、NaN を検出するための安全な方法を提供します (非数値を数値に強制変換しないため)。
> isNaN('abc')
true
> Number.isNaN('abc')
false
Array.prototype.slice() から Array.from() またはスプレッド演算子へ ES5 では、Array.prototype.slice() は配列のようなオブジェクトを配列に変換するために使用されていました。ES6 では、Array.from() があります。
var arr1 = Array.prototype.slice.call(arguments); // ES5
const arr2 = Array.from(arguments); // ES6
値がiterableの場合(すべての配列のような DOM データ構造が現在そうであるように)、スプレッド演算子 (...) を使用して配列に変換することもできます。
const arr1 = [...'abc'];
// ['a', 'b', 'c']
const arr2 = [...new Set().add('a').add('b')];
// ['a', 'b']
apply() から Array.prototype.fill() へ ES5 では、apply() をハックとして使用して、undefined で埋められた任意の長さの配列を作成できます。
// Same as Array(undefined, undefined)
var arr1 = Array.apply(null, new Array(2));
// [undefined, undefined]
ES6 では、fill() がより簡単な代替手段です。
const arr2 = new Array(2).fill(undefined);
// [undefined, undefined]
任意の値を埋め込んだ配列を作成する場合は、fill() の方がさらに便利です。
// ES5
var arr3 = Array.apply(null, new Array(2))
.map(function (x) { return 'x' });
// ['x', 'x']
// ES6
const arr4 = new Array(2).fill('x');
// ['x', 'x']
fill() は、すべての配列要素を与えられた値に置き換えます。穴は要素であるかのように扱われます。
詳細情報: 「値で埋められた配列の作成」のセクション
ES5 でさえ、AMD 構文または CommonJS 構文に基づくモジュールシステムは、公開モジュールパターンなどの手書きのソリューションをほとんど置き換えています。
ES6 にはモジュールの組み込みサポートがあります。残念ながら、JavaScript エンジンはまだネイティブにサポートしていません。しかし、browserify、webpack、jspm などのツールを使用すると、ES6 構文を使用してモジュールを作成できるため、記述するコードは将来も使用できます。
CommonJS では、複数のエンティティを次のようにエクスポートします。
//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
return x * x;
}
function diag(x, y) {
return sqrt(square(x) + square(y));
}
module.exports = {
sqrt: sqrt,
square: square,
diag: diag,
};
//------ main1.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
または、モジュール全体をオブジェクトとしてインポートし、それを通して square と diag にアクセスすることもできます。
//------ main2.js ------
var lib = require('lib');
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
ES6 では、複数のエクスポートは名前付きエクスポートと呼ばれ、次のように処理されます。
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
//------ main1.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
モジュールをオブジェクトとしてインポートするための構文は次のようになります (A 行)。
//------ main2.js ------
import * as lib from 'lib'; // (A)
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
Node.js は CommonJS を拡張し、module.exports を介してモジュールから単一の値をエクスポートできます。
//------ myFunc.js ------
module.exports = function () { ··· };
//------ main1.js ------
var myFunc = require('myFunc');
myFunc();
ES6 では、同じことがいわゆるデフォルトエクスポート(export default を介して宣言) を介して行われます。
//------ myFunc.js ------
export default function () { ··· } // no semicolon!
//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
詳細情報: 「モジュール」の章を参照してください。
これで ES6 の第一歩を踏み出したので、章を参照して探索を続けることができます。各章では、機能または関連機能のセットについて説明し、概要から始まります。最後の章は、これらの概要セクションをすべて 1 か所に集めています。