ECMAScript 2015の新機能(その1)

2023/01/14更新

目次

概要

2015年に公開された新しいJavaScriptの標準仕様であるECMAScript 2015(ES6、ES2015)では、JavaScriptを便利にする数多くの新機能が盛り込まれた。それらをひと通りまとめておく。

かなりの分量となったため、2つの記事に分けている。また、型付き配列、非同期処理関連、モジュールはさらに別ページでまとめた。

進化した記法

letとconst

従来のvarと同じく変数を宣言するための新しいキーワード。varとの主な違いは、以下の通り。

  • letは再宣言ができない。constは再宣言に加えて再代入もできない。

  • varは有効範囲が関数内部となるfunctionスコープだったが、letconstはこれまでに無かった{~}によるブロックスコープを持つ。これにより、変数の有効範囲を今までよりも狭い範囲に限定することが可能となる。

  • letconstは、varと同じくスコープ内での変数宣言の巻上げ(上位スコープの同名変数の隠蔽)が行われるが、varとは異なり宣言前に参照するのはエラーとなる。

    console.log(a);  // undefined
    var a = 1;
    
    console.log(b);  // エラー
    let b = 1;
    

今後は、関数定義、定数、配列、オブジェクトなど大半の変数は基本的にconstを使い、for文で使うカウンタ変数のように再代入の可能性がある一部のものに限りletを使っていくのが良いとされている。また、ブロックスコープを活用することで、従来のようなスコープ確保のための即時関数が必要なくなる。

アロー関数

無名関数の代替記法。単純な関数を非常にシンプルに記述できる。

// 従来の無名関数
var func = function(x) { return x * 2; };

// アロー関数表記
const func = (x) => { return x * 2; };

// 引数が1つの場合は、()を省略可能
const func = x => { return x * 2; };

// 関数内が1つの式だけの場合は、{}とreturnを省略可能
const func = x => x * 2;

アロー関数の重要な性質として、自スコープのthisが存在せず、常に親スコープのthisを指す、という性質がある。

const outerObj = {
    value: 'outer',
    func0: function() {
        // thisはouterObjを指す
        console.log(this.value);  // "outer"
        const innerObj = {
            value: 'inner',
            func1: function() {
                // thisはinnerObjを指す
                console.log(this.value);
            },
            func2: () => {
                // thisはfunc0におけるthis、すなわちouterObjを指す
                console.log(this.value);
            }
        };
        innerObj.func1();  // "inner"
        innerObj.func2();  // "outer"
    }
};

分割代入とデフォルト値

さまざまな形の分割代入が使用可能となった。また、スプレッド演算子...を使った新しい表記法も導入されている(スプレッド構文も参照)。

// 配列形式
let [a, b] = [1, 2];       // a = 1, b = 2
let [a, b, c] = [1, 2];    // a = 1, b = 2, c = undefined;
let [a, b] = [1, 2, 3];    // a = 1, b = 2
let [a, , b] = [1, 2, 3];  // a = 1, b = 3

// 可変長
let [a, b, ...c] = [1, 2, 3, 4, 5];  // a = 1, b = 2, c = [3, 4, 5]

// デフォルト値の設定
let [a = 10, b = 20] = [1];  // a = 1, b = 20

// 値の入れ替え
[a, b] = [b, a];

// iterableなオブジェクトの配列化
let [a, b, c] = 'xyz';  // a = 'x', b = 'y', c = 'z'
let a = [...'xyz'];     // a = ['x', 'y', 'z']

// オブジェクト形式
let {a, b} = {a: 1, b: 2};        // a = 1, b = 2
let {a: x, b: y} = {a: 1, b: 2};  // x = 1, y = 2

// デフォルト値の設定
let {a = 10, b = 20} = {a: 1};       // a = 1, b = 20
let {a: x = 10, b: y = 20} = {a: 1}; // x = 1, y = 20

// 関数引数での利用
function func1({a, b}) { ~ }
func1({a: 1, b: 2});  // a = 1, b = 2

function func2(a = 10, b = 20) { ~ }
func2(1);             // a = 1, b = 20

function func3(a, b, ...c) { ~ }
func3(1, 2, 3, 4, 5); // a = 1, b = 2, c = [3, 4, 5]

function func4({a = 1, b = 2} = {a: 3, b: 4}) { ~ }
func4({a: 5, b: 6});  // a = 5, b = 6
func4({a: 5});        // a = 5, b = 2
func4({});            // a = 1, b = 2
func4();              // a = 3, b = 4

// 引数の展開
const a = [1, 2, 3];
const b = [4, 5, 6];
Math.max(...a);  // Math.max(1, 2, 3)
a.push(...b);    // a.push(4, 5, 6)

八進数・二進数リテラル

従来の十六進数リテラル(0x~)と同様に、八進数0o~)、二進数0b~)がリテラル表記できるようになった。

テンプレート文字列とタグ関数

従来文字列リテラルを表記していた'~'"~"の代わりに、バッククォート`~`で文字列を囲うと、その中では${~}によって変数展開や式展開が行える。${~}の中でさらにネストして`~`を使うこともできる。改行も直接含めることができる。

for (let i = 0; n > i; i++) {
    // 従来の文字列連結
    console.log('現在、' + (i + 1) + '個の処理が完了しました。');
    // テンプレート文字列
    console.log(`現在、${i + 1}個の処理が完了しました。`);
}

関数名`~`という新しい構文(タグ関数)を使うと、テンプレート文字列の生成前に関数に渡して自由に加工することができる。関数名(`~`)ではないことに注意。これは、例えば${~}で渡される値に対してバリデーションや共通の加工を施したい場合などに使える。

// 加工用のタグ関数の定義
const validatePrice = function(str, ...vals) {
    // 第1引数には、テンプレート文字列の${~}以外の部分が配列で渡される。
    // 第2引数以降には、${~}に渡された値が渡される。
    const output = [str[0]];
    // ${~}で渡された全ての値について処理
    for (let i = 0; vals.length > i; i++) {
        const val = Math.floor(vals[i]);
        if (isNaN(val)) {
            // 整数化できない場合は「価格不明」と出力する。
            output.push('価格不明');
        } else {
            // 整数化できた場合は、3桁ごとのカンマを挿入して最後に「円」をつける。
            output.push(`${String(val).replace(/(\d)(?=(?:\d\d\d)+$)/g, '$1,')}円`);
        }
        output.push(str[i + 1]);
    }
    // 文字列を結合して返す。
    return output.join('');
};

let a = 1000000;
let b = 1234.5678;
let c = 'hoge';
const msg = validatePrice`商品Aは${a}、商品Bは${b}、商品Cは${c}です。`;
// validatePriceの引数には以下のように渡される。
// str = ['商品Aは', '、商品Bは', '、商品Cは', 'です。']
// vals = [1000000, 1234.5678, 'hoge']
console.log(msg);
// 商品Aは1,000,000円、商品Bは1,234円、商品Cは価格不明です。

タグ関数の第1引数(上記例のstr)は、rawというプロパティを持っており、\によるエスケープがされていない生の文字列(例えば、\nが改行ではなく\nという2文字となる)が格納された配列となっている。String.rawは組み込みのタグ関数で、rawプロパティに格納された生の文字列を単に連結して返す。

const str1 = `Aは\x41と表記できます。`;
// AはAと表記できます。
const str2 = String.raw`Aは\x41と表記できます。`;
// Aは\x41と表記できます。
const str3 = String.raw`Aは${'\x41'}と表記できます。`;
// AはAと表記できます。
const str4 = String.raw({ raw: ['Aは', 'と表記できます。'] }, '\x41');
// AはAと表記できます。
const str5 = String.raw({ raw: ['Aは', 'と表記できます。'] }, '\\x41');
// Aは\x41と表記できます。

オブジェクトリテラルの新記法

オブジェクトリテラルに新しい記法が追加され、従来より簡潔な表記が可能となっている。

// 変数名と同じプロパティへの代入
const a = 1, b = 2;
const obj1 = { a, b }; // obj1 = { a: a, b: b } と同じ
console.log(obj1.a); // 1

// メソッド定義の省略表記
const obj2 = {
    myMethod(a, b) { // myMethod: function(a, b) { と同じ
        console.log(a);
    }
};

// 動的なプロパティ名
const prop = 'hoge';
const obj3 = {
    [prop]: 123,          // obj3[prop] = 123 と同じ
    [prop + 'fuga']: 456  // obj3[prop + 'fuga'] = 456 と同じ
};
console.log(obj3.hoge);     // 123
console.log(obj3.hogefuga); // 456

Unicodeの扱い

従来の文字列のUnicode表記\uxxxxは、十六進数表記で4桁となる16ビットの範囲の符号位置(code point)しか指定できず、サロゲートペアが必要となる範囲の符号位置はUTF-16の2つの符号単位(code unit)での表記が必要だったが、符号位置を直接記述できる\u{xxxx}という記法が使えるようになった。他にも、符号位置の取得にcodePointAt()、符号位置からの文字生成にString.fromCodePoint()、さらに正規表現の.を符号単位ではなく文字単位にマッチさせるuフラグなどが追加されている。

// リテラル表記
console.log('\u1f600');   // ×('\u1f60'と'0'で区切られてしまう)
console.log('\u{1f600}'); // '😀'(符号位置「0x1F600」の文字)

// 符号位置の取得
console.log('😀'.charCodeAt(0));  // 0xD83D(サロゲートペアの1つ目の符号単位の値)
console.log('😀'.codePointAt(0)); // 0x1F600(「😀」の正しい符号位置)

// 符号位置による文字の取得
console.log(String.fromCharCode(0x1f600));  // ×(下位16ビットしか認識されない)
console.log(String.fromCodePoint(0x1f600)); // '😀'(符号位置「0x1F600」の文字)

// 文字列の長さ
console.log('😀'.length);             // 2(符号単位の数)
console.log(Array.from('😀').length); // 1(Array.from()は文字単位で分割できる)

// 正規表現
console.log(/^.$/.test('😀'));  // false(従来の「.」は符号単位にマッチする)
console.log(/^.$/u.test('😀')); // true(「u」フラグで文字単位でマッチする)

追加されたメソッド

Object

Object.assign()を使うと、従来はjQueryなどのライブラリを必要としていたオブジェクトのコピーやマージが簡単にできる。ただし、ディープコピー(ネストしたオブジェクトも全てコピーすること)には対応していない。

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 3, c: 4 };
const obj3 = { a: 5, d: 6 };

// マージ
Object.assign(obj1, obj2, obj3);
// obj1 = { a: 5, b: 2, c: 4, d: 6 }
// ※obj1に対して、obj2、obj3が順にマージされる。戻り値は最初の引数として渡したオブジェクト。

// コピー
const obj4 = Object.assign({}, obj2);
// obj4 = { a: 3, c: 4 }
// ※第1引数として空オブジェクトを渡せば、コピーが簡単にできる。

Object.is(a, b)で様々な値の等価判定ができる。これは従来のa === bとほぼ同じであるが、以下の通り一部結果が異なるものがある。

console.log(0 === -0);            // true(0と-0は同じ)
console.log(Object.is(0, -0));    // false(0と-0は異なると見なす)

console.log(NaN === NaN);         // false(NaNは自分自身と等しくない)
console.log(Object.is(NaN, NaN)); // true(NaNはどれも同じと見なす)

Object.setPrototypeOf()を使うと、オブジェクトのプロトタイプを変更できる。プロトタイプと継承を参照。

Object.getOwnPropertySymbols()を使うと、Symbolをキーとしたプロパティの列挙ができる。Symbolをキーとしたプロパティを参照。

Array

Arrayオブジェクトには、値埋めや値検索などいくつかの便利なメソッドが追加されている。

// 値埋め
const arr = [1, 2, 3, 4, 5];
arr.fill(0);         // arr = [0, 0, 0, 0, 0]
arr.fill(123, 2);    // arr = [0, 0, 123, 123, 123]
arr.fill(456, 1, 3); // arr = [0, 456, 456, 123, 123]

// 独自関数による値の検索
const arr = [111, 222, 333, 444, 555];
const val = arr.find((value, index) => {
    return value % 2 == 0 && index >= 2;
}); // 444(最初に見つかった値)
const pos = arr.findIndex((value, index) => {
    return value % 2 == 0 && index >= 2;
}); // 3(最初に見つかった位置)
arr.find((value, index) => value > 1000);      // undefined
arr.findIndex((value, index) => value > 1000); // -1

// 配列内コピー
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.copyWithin(6, 1, 4); // 1番目から3個(=4-1)を6番目にコピー
console.log(arr);        // [0, 1, 2, 3, 4, 5, 1, 2, 3, 9]

keys()values()entries()は、それぞれキー、値、キー/値のペアを列挙するイテレーターを返す。

const arr = ['A', 'B', 'C', 'D', 'E'];
const keys = arr.keys();
const values = arr.values();
const entries = arr.entries();

// イテレーターとして使う
console.log(keys.next().value);    // 0
console.log(values.next().value);  // 'A'
console.log(entries.next().value); // [0, 'A']

// for-of文で使う
for (const [i, value] of arr.entries()) {
    console.log(`${i}: ${value}`);
}

Array.from()を使うと、配列風オブジェクトやiterableなオブジェクトから配列を作成することができる。また、第2引数に関数を与えることもでき、連番など規則性を持つ値の配列などが簡単に作成できる。

// 配列風オブジェクトから配列を生成
const typedArr = new Uint8Array([1, 2, 3, 4, 5]);
const arr = Array.from(typedArr);
// [1, 2, 3, 4, 5]

// iterableなオブジェクトから配列を生成
const str = 'ABCDE';
const arr = Array.from(str);
// ['A', 'B', 'C', 'D', 'E']

// 独自関数を作用させてから配列を生成
const str = 'ABCDE';
const arr = Array.from(str, (x, i) => `${i}=${x}`);
// ['0=A', '1=B', '2=C', '3=D', '4=E']

// 連番を生成
const arr1 = Array.from(Array(5), (x, i) => i);
// [0, 1, 2, 3, 4]
const arr2 = Array.from(Array(26), (x, i) => String.fromCharCode(i + 65));
// ['A', 'B', 'C', …, 'X', 'Y', 'Z']

Array.of()を使うと、可変長引数から配列を作成することができる。これは、従来のArray()コンストラクタとほぼ同じであるが、Array.of()の方は引数に整数が1個だけ与えられた場合でも、その値を要素とする配列を作成する点が異なる。

// 可変長引数から配列を生成
const arr1 = Array.of(1, 2, 3); // [1, 2, 3] ※Array(1, 2, 3)と同じ。
const arr2 = Array.of(1);       // [1]       ※Array(1)とは異なる。

String

Stringオブジェクトには、文字列反復や存在判定のメソッドが追加されている。

// 文字列の反復
str = 'abc'.repeat(3); // 'abcabcabc'

// 部分文字列の存在判定
x = 'abc'.includes('a'); // true(従来の「'abc'.indexOf('a') >= 0」と同等)

Number

Numberオブジェクトには、従来からグローバルに存在していた判定関数やパース関数が再定義されている。再定義されたNumber.isNaN()Number.isFinite()は、暗黙の型変換をせずにNumberでないものは全てfalseとする点が従来のisNaN()isFinite()と異なる。

// isNaN
x = Number.isNaN(NaN);       // true
x = Number.isNaN(undefined); // false(isNaNと異なり、Numberでないものは全てfalse)
x = isNaN(undefined);        // true(isNaNは、Numberに変換してから判定)

// isFinite
x = Number.isFinite(100);   // true
x = Number.isFinite('100'); // false(isFiniteと異なり、Numberでないものは全てfalse)
x = isFinite('100');        // true(isFiniteは、Numberに変換してから判定)

// isInteger
x = Number.isInteger(1234); // true(Numberかつ整数ならばtrue)

// parseFloatとparseInt
x = Number.parseFloat('3.14'); // 3.14(従来のparseFloatと同じ)
x = Number.parseInt('1234');   // 1234(従来のparseIntと同じ)

Math

Mathオブジェクトには、数学関連のメソッドがいくつか追加されている。

// 整数部分(0への丸め)
x = Math.trunc(2.5);  // 2
x = Math.trunc(-2.5); // -2
x = Math.floor(-2.5); // -3(従来のfloorは負の無限大への丸め)

// 対数関数と指数関数
x = Math.log10(1000); // 3(10を底とした対数)
x = Math.log2(1024);  // 10(2を底とした対数)
x = Math.log1p(y);    // Math.log(1 + y)と同じ
x = Math.expm1(y);    // Math.exp(y) - 1と同じ

// 双曲線関数と逆双曲線関数
x = Math.sinh(0);
x = Math.cosh(0);
x = Math.tanh(0);
x = Math.asinh(0);
x = Math.acosh(1);
x = Math.atanh(0);

// 二乗和平方根と立方根
x = Math.hypot(3, 4); // 5(=√(3×3+4×4))
x = Math.cbrt(27);    // 3

// 符号関数
x = Math.sign(10); // 1
x = Math.sign(0);  // 0
x = Math.sign(-5); // -1

型付き配列

型付き配列については、別記事「JavaScriptの型付き配列」にまとめた。

イテレーター関連

イテレーターという新たな概念は、ES2015で追加された様々な関数や構文で登場する。

イテレーターとiterable

イテレーター(iterator)とは、値を順番に取り出すことができるようなもののことを言う。あるオブジェクトが「イテレーターである」とは、以下のような規約を満たした状態であると定められている。

  • next()という無引数の関数が定義されている。

  • next()の戻り値は、真偽値を値に持つdoneと、任意の値を持つvalueという2つのプロパティを持つオブジェクトである。

  • doneはイテレーターからの値の取り出しが終わったかどうかを表す値であり、trueの場合はvalueは不要となる。

  • valueはイテレーターから順に取り出した値である。

これを満たすオブジェクトは全てイテレーターであると言える。例えば、以下のmyIteratorは1~10までの数字を順に取り出すことができるイテレーターである。

const getMyIterator = function() {
    let myValue = 0;
    return {
        next: function() {
            if (myValue >= 10) return { done: true };
            return {
                done: false,
                value: ++myValue
            };
        }
    };
};
const myIterator = getMyIterator();

さらに、あるオブジェクトがイテレーターを生成できるとき、そのオブジェクトはiterableであるという。具体的には、[Symbol.iterator]プロパティにイテレーターを生成する関数(上記コードではgetMyIterator)が実装されていることである。

const myIterableObject = {};
myIterableObject[Symbol.iterator] = getMyIterator;
// これでmyIterableObjectはiterableとなる。

// iterableであることが必要な関数や構文に使うことができる。
for (const a of myIterableObject) {
    console.log(a); // 1~10の数字が列挙される。
}

イテレーター自体は本来iterableではないが、自分自身を返すイテレーター生成関数を実装すれば、イテレーターをiterableとすることもできる。StringArrayTypedArrayMapSetの各オブジェクトは、標準でiterableであり、生成されるイテレーターもiterableとなっている。

const str = 'ABCDE';

// 文字列からはイテレーターを取得できる。
const itr1 = str[Symbol.iterator]();
itr1.next().value; // 'A'
itr1.next().value; // 'B'

// 取得するたびに別の新しいイテレーターとなる。
const itr2 = str[Symbol.iterator]();
itr2.next().value; // 'A' ※別のイテレーターなので最初の「A」からとなる。
itr2.next().value; // 'B'
console.log(itr1 === itr2); // false

// イテレーターからは自分自身を取得できる。
const itr3 = itr2[Symbol.iterator]();
itr3.next().value; // 'C' ※自分自身なので続きの「C」からとなる。
itr3.next().value; // 'D'
console.log(itr2 === itr3); // true

for-of文

従来からあるfor-in文は、オブジェクトの列挙可能プロパティを列挙するものであったが、それと同様に、iterableなオブジェクトの値を列挙する新しい記法がfor-of文である。

for-of文ではまず、渡されたiterableなオブジェクトの[Symbol.iterator]()が実行されてイテレーターが取得され、そのイテレーターのnext()を実行して得られた値が順に渡されてくる。つまり、以下のようなコードと同等である。

// for-of文
for (const val of iterableObj) {
    // valを使った処理
}

// 実際に行なわれていること
const itr = iterableObj[Symbol.iterator](); // まずイテレーターを取得
while (true) {
    const res = itr.next();
    if (res.done) break; // イテレーターが終端まで来たら終了
    const val = res.value;
    // valを使った処理
}

for-of文を使うと、通常の配列の値を列挙できるほか、文字列の場合は1文字ずつ取り出すことができる。

for (const val of [1, 2, 3, 4, 5]) {
    console.log(val);
}
// 1 2 3 4 5

for (const val of 'ABCDE') {
    console.log(val);
}
// A B C D E

なお、通常のオブジェクトはiterableでないので、for-of文でプロパティや値を列挙することはできないが、自前でイテレーターを実装すれば可能となる。また、ES2017で追加されたObject.values()Object.entries()は配列を返すため、for-of文で列挙可能である。

const obj = { a: 1, b: 2 };
for (const [key, value] of Object.entries(obj)) {
    console.log(`key=${key}, value=${value}`);
}
// key=a, value=1
// key=b, value=2

スプレッド構文

分割代入の項でも触れているが、iterableなオブジェクトの前に...をつけることで、そのオブジェクトから列挙される値を、配列の要素や関数の引数に展開することができる。

// 配列や文字列はiterableなオブジェクト
const arr1 = [1, 2, 3];
const str1 = 'ABC';

// 配列の要素として展開
const arr2 = [...arr1, 4, 5];    // [1, 2, 3, 4, 5]
const arr3 = [...arr1, ...str1]; // [1, 2, 3, 'A', 'B', 'C']

// 関数の引数として展開
func(...arr1, 4, 5);    // func(1, 2, 3, 4, 5)
func(...arr1, ...str1); // func(1, 2, 3, 'A', 'B', 'C')

逆に、配列や関数引数の可変長な部分を1つの配列にまとめることもできる。こちらは残余構文と呼ばれている。スプレッド構文は、自由な位置で何個もiterableなオブジェクトを展開できるが、残余構文の場合は最後の位置でのみ1回しか使用できない。

// 配列の残りの要素を1つの配列にまとめる
const [a, b, ...c] = [1, 2, 3, 4, 5]; // c = [3, 4, 5]

// 関数の残りの引数を1つの配列にまとめる
const func = function(a, b, ...c) {
    // c = [3, 4, 5]
};
func(1, 2, 3, 4, 5);

// 以下はエラー
const [...a, b c] = [1, 2, 3, 4, 5];
const [...a, ...b] = [1, 2, 3, 4, 5];

ジェネレーター

ジェネレーターは、function*によって定義されるジェネレーター関数が返すオブジェクトで、イテレーターであり、それ自身iterableでもある。yieldによってコードの任意の時点で処理を中断して値を返すことができるため、素数やフィボナッチ数など無限に続くものを列挙するイテレーターなどが簡単に作れる。

// ジェネレーター関数は「function*」で定義する。
function* myGeneratorFunc() {
    let i = 0;
    while (true) {
        yield ++i;
    }
}
const myGenerator = myGeneratorFunc();
// myGeneratorはイテレーター。この時点では何も実行されない。
console.log(myGenerator.next().value); // 1
console.log(myGenerator.next().value); // 2
console.log(myGenerator.next().value); // 3
// next()を呼び出すたびに、yieldまでが実行される。

yieldは式の右辺などに置いて、値の入力待ちに使うこともできる。

function* myGeneratorFunc() {
    let i = 0;
    let m = yield;
    while (true) {
        yield ++i * m;
    }
}
const myGenerator = myGeneratorFunc();
console.log(myGenerator.next());
// m = の部分で一時停止する。
console.log(myGenerator.next(10).value); // 10
// 直前に停止したyieldに「10」が渡されて、次のyieldまで実行される。
console.log(myGenerator.next().value); // 20
console.log(myGenerator.next().value); // 30

yield*を使うと、iterableなオブジェクトの各値に対してyieldすることができる。

function* myGeneratorFunc() {
    let i = 0;
    while (true) {
        yield* [++i, i * 10, i * 100];
        // yield ++i;
        // yield i * 10;
        // yield i * 100; と同じ。
    }
}

関連記事

外部リンク