はじめに

「ES2015」とは ECMAScript のバージョン 6 のことです。ES6 とも呼ばれます。「ECMAScript」とは、JavaScript の仕様を定めたものです。

Web を検索すると詳しい話が出てきますが、JavaScript というのはもともと Netscape Navigator に搭載されて世に出てきたもので、それを ECMA International という非営利の標準化団体が標準化することになり、その結果標準化されたものが「ECMAScript」です。

つまり、ECMAScript というのは仕様で、JavaScript はその実装となります。以降、特に両者を区別せず「JavaScript」と表記します。

ES2015 は 2015 年 6 月に公開されるまでは「Harmony」というコードネームで呼ばれていました。また、ECMAScript のバージョン 6 以降は毎年アップデートされることになり、以降 ECMAScript 2016, ECMAScript 2017 のようにリリース年をバージョン番号とすることになっているそうです。

これにより、ES2015 を表す呼称として「Harmony」、「ES6 Harmony」、「ES6」、「ES2015」、「ECMAScript 2015」など色々な呼び方がありますが全て同じものを指しています。本稿では「ES2015」と記載します。

ECMAScript の各バージョンについては Wikipedia の ECMAScript のページを参照するとよいでしょう。

現在 (2018 年 1 月) では、デスクトップ向け・スマートフォン向け問わず、多くの Web ブラウザで ES2015 のほとんどの機能がサポートされています。しかし、IE など一部のブラウザでは依然として対応されていない状態です。これらのブラウザについてもサポートする必要がある場合は Babel などのトランスコンパイラの使用を検討した方がよいでしょう。

各 Web ブラウザ、処理系毎の対応状況がこちらの Web ページ にまとめられています。

ES2015 は新しい構文や多くの新機能が追加された大規模なアップデートで、JavaScript はより近代的なプログラミング言語となりました。本稿では、その ES2015 での変更とそれ以降のバージョンでの変更について解説します。

let, const

従来、変数を宣言するのに var というキーワードを使っていましたが、これに let, const が加わりました。

var で変数を宣言した場合、関数内で宣言された場合は関数スコープの、関数の外で宣言された場合はグローバルスコープの変数となります。 また、var で変数を宣言する場合は、関数中のどこで宣言しようとも関数の実行前に宣言が処理されます (宣言より前の行で変数を使用できる)。

let を使用することによりブロックスコープの局所変数を宣言できます。 const を使用すると、let の特性に加えて再代入できない変数 (定数) を宣言できます。

なお、宣言せずに変数を使用した場合、その変数は暗黙的にグローバルスコープで宣言されます。

これらの話題について MDN に詳しく説明してありますので興味があれば参照してみて下さい: var, let

const declareTest = () => {
    {
        p('宣言前:')
        try { p(`qux: ${qux}`) } catch (e) { p(`qux ref err: ${e}`) }
        p(`foo: ${foo}`)
        foo = 'foo'
        p(`foo: ${foo}`)
        try { p(`bar: ${bar}`) } catch (e) { p(`bar ref err: ${e}`) }
        try { p(`baz: ${baz}`) } catch (e) { p(`baz ref err: ${e}`) }
        // 宣言
        qux = 'Qux'
        var foo = 'Foo'
        let bar = 'Bar'
        const baz = 'Baz'
        {
            qux = 'quuux'
            var foo = 'foooo'
            let bar = 'baaar'
            const baz = 'baaaz'
            p()
            p('サブブロック:')
            p(`qux: ${qux}`)
            p(`foo: ${foo}`)
            p(`bar: ${bar}`)
            p(`baz: ${baz}`)
        }
        p()
        p('同一スコープ:')
        p(`qux: ${qux}`)
        p(`foo: ${foo}`)
        p(`bar: ${bar}`)
        p(`baz: ${baz}`)
        p()
        p('再代入:')
        qux = 'QUX'
        foo = 'FOO'
        bar = 'BAR'
        try { baz = 'BAZ' } catch (e) { p(`baz assign err: ${e}`) }
        p(`qux: ${qux}`)
        p(`foo: ${foo}`)
        p(`bar: ${bar}`)
        p(`baz: ${baz}`)
    }
    p()
    p('スコープ外:')
    p(`qux: ${qux}`)
    p(`foo: ${foo}`)
    try { p(`bar: ${bar}`) } catch (e) { p(`bar ref err: ${e}`) }
    try { p(`baz: ${baz}`) } catch (e) { p(`baz ref err: ${e}`) }
}
declareTest()
p()
p('関数外:')
p(`qux: ${qux}`)
try { p(`foo: ${foo}`) } catch (e) { p(`foo ref err: ${e}`) }
try { p(`bar: ${bar}`) } catch (e) { p(`bar ref err: ${e}`) }
try { p(`baz: ${baz}`) } catch (e) { p(`baz ref err: ${e}`) }

run clear

テンプレートリテラル

文字列リテラルを表現するにはクォート (')、またはダブルクォート (") を使いますが、新たに、バッククォート (`) で囲むことによって文字列を表現できるようになりました。 バッククォートを使ったリテラルは「テンプレートリテラル」と呼ばれます。

p(`複数行の文字列を
記述できる`)

// ${} 記法により、テンプレートリテラル中に好きに式を記述できる
const [a, b] = [1, 2]
const obj = { name: 'foo', val: 42 }
p(`a + b: ${a + b}, obj.name: ${obj.name}, obj: ${JSON.stringify(obj)}`)

run clear

function tag(strings, ...values) { ... }
tag`...`

という呼び出し方でテンプレートリテラルを関数に食わせる「タグ付けされたテンプレートリテラル」(Tagged Template Literals) という仕組みもあります。詳しくは MDN をどうぞ。

オブジェクトプロパティ

    let foo = 'Foo'
    let bar = 'Bar'

    // { a: a, b: b } を次のように記述できるようになった
    let obj = { foo, bar }
    p(`obj: ${JSON.stringify(obj)}`)

    // オブジェクトプロパティ名に式の評価結果を使用できるようになった
    // また、オブジェクトプロパティの関数をメソッド記法で記述できるようになった
    // これについてはジェネレータについても同様 (ジェネレータについては後述)
    let obj2 = {
        [foo + bar]: 'foobar',
        method() { p(`method called. this: ${JSON.stringify(this)}`) },
        *generator() { yield 'foo'; yield 'bar'; yield 'baz' }
    }
    obj2.method()
    for (elem of obj2.generator())
        p(`generated: ${elem}`)

run clear

分割代入

Perl や Python のような「分割代入」ができるようになりました。

{
    p()
    p('配列の分割代入:')
    {
        // 次のような構文で変数に値を代入できる
        let [a, b, c] = [1, 2, 3]
        p(`a: ${a}, b: ${b}, c: ${c}`)

        // つまり、配列を右辺に指定できる。また、宣言済みの変数に再代入できる。
        const arr = [1, 2, 3];  // セミコロンを置かない場合、次の行がこの行に「くっついて」しまいシンタックスエラーとなってしまう
        [c, b, a] = arr
        p(`a: ${a}, b: ${b}, c: ${c}`)
    }
    {
        // 一部の値だけ取り出すことができる
        const [a, , b] = [1, 2, 3, 4, 5]
        p(`a: ${a}, b: ${b}`)
    }
    {
        // デフォルト値を指定できる
        let [a = 7, b = 8, c = 9] = [1, 2]
        p(`a: ${a}, b: ${b}, c: ${c}`);

        // 退避用の変数を使わずに値の入れ替えができる
        [ a, b ] = [ b, a ]
        p(`a: ${a}, b: ${b}`)
    }
    {
        // 次の構文によって、[1, 2, 3, 4, 5] でなく [1, 2, [3, 4, 5]] と展開される。
        // つまり、「...」を付与することにより rest に配列が代入される。
        // この「...」のことをスプレッド演算子と呼ぶ (詳細については後述)。
        const [a, b, ...rest] = [1, 2, 3, 4, 5]
        p(`a: ${a}, b: ${b}, rest: ${rest}`)
    }
}

// オブジェクトについても上記と同じような規則が当てはまる
{
    p()
    p('オブジェクトの分割代入:')
    {
        // 次のような構文で変数に値を代入できる
        let { a, b, c } = { a: 1, b: 2, c: 3 }
        p(`a: ${a}, b: ${b}, c: ${c}`)

        // つまり、オブジェクトを右辺に指定できる。また、宣言済みの変数に再代入できる。
        const obj = { a: 1, b: 2, c: 3 };
        ({ c, b, a } = obj) // 周囲を () で囲まなかった場合、左辺の {} がブロックと見なされてシンタックスエラーとなってしまう
        p(`a: ${a}, b: ${b}, c: ${c}`)
    }
    {
        // 一部の値だけ取り出すことができる
        const {a, c} = {a: 1, b: 2, c: 3, d: 4, e: 5}
        p(`a: ${a}, c: ${c}`)
    }
    {
        // デフォルト値を指定できる
        let { a = 7, b = 8, c = 9 } = { a: 1, b: 2 }
        p(`a: ${a}, b: ${b}, c: ${c}`);

        // 退避用の変数を使わずに値の入れ替えができる
        ({ a, b } = { a: b, b: a })
        p(`a: ${a}, b: ${b}`)
    }
    {
        // 次の構文によって、{ a: 1, b: 2, c: 3, d: 4, e: 5 } でなく { a: 1, b: 2, rest: { c: 3, d: 4, e: 5 } } と展開される。
        // つまり、「...」を付与することにより rest にオブジェクトが代入される。
        const {a, b, ...rest} = { a: 1, b: 2, c: 3, d: 4, e: 5 }
        p(`a: ${a}, b: ${b}, rest: ${JSON.stringify(rest)}`)
    }
}

// 分割代入の仕組みは関数の仮引数部分にも適用できる
{
    p()
    p('関数の引数に対する分割代入:')
    const foo = ({ name = 'foo', addr = { zipcode: '000-0000', addr1: '', addr2: '' }, age = 42 } = {}) => {
        p(`name: ${name}, addr: ${JSON.stringify(addr)}, age: ${age}`)
    }
    foo({
        name: 'taro',
        addr: { zipcode: '100-1000', addr1: 'hoge-ken', addr2: 'fuga-shi' },
        age: 32
    })
}

run clear

スプレッド演算子 (「...」による要素展開)

スプレッド演算子によって、

  • 配列・オブジェクトの要素を展開された状態で渡す
  • 配列・オブジェクトの要素の一部を配列・オブジェクトとして受け取る

ことができます。

// 関数を呼び出す際に、スプレッド演算子を使って配列の要素を展開しつつ引き渡すことができる
const f1 = (a, b, c) => { p(`a: ${a}, b: ${b}, c: ${c}`) }
const arr = ['foo', 'bar', 'baz']
f1(...arr)

// 関数の仮引数において、スプレッド演算子を使って可変長の引数を受け取るようにできる
const f2 = (a, ...rest) => {
    p(`a: ${a}`)
    for (let elem of rest)
        p(`elem: ${elem}`)
}
f2('foo', 'bar', 'baz')
f2(...arr)

// 関数の引数の受け渡しにおいてできることと同様のことが配列・オブジェクト・分割代入 (*1) においてもできる
// *1: 分割代入の例を参照

const arr2 = [ 'qux', ...arr, 'quux', 'quuz', ...arr ]
p(`arr2: ${arr2}`)

run clear

アロー関数

「アロー関数」によって、より短い構文で関数を定義できる様になりました。次のように記述します。

const f = (a) => { return a + 1 }
p('0 + 1: ' + f(0))
p('1 + 1: ' + ( (a) => { return a + 1 } )(1))
p('2 + 1: ' + ( a => { return a + 1 } )(2)) // 引数が 1 つのみの場合は () を省略可能
p('3 + 1: ' + ( a => a + 1 )(3))    // 関数の内容が 1 文のみの場合は {} と「return」を省略可能

p('return のみ省略: ' + ( a => { a + 1 } )(4))  // {} で囲んだ場合は return を省略できない (この場合関数の結果は undefined となる)

p('5 + 6: ' + ( (a, b) => a + b )(5, 6))    // 引数が複数の場合は () を省略できない

p('foo: ' + JSON.stringify( ( () => { return { name: 'foo' } } )() ))   // 引数がない場合は () を省略できない
p('bar: ' + JSON.stringify( ( () => ({ name: 'bar' }) )() ))    // 「return」を省略してオブジェクトリテラルを返す場合は () で囲む必要があることに注意する
p('オブジェクトリテラルを返す場合に注意: ' + JSON.stringify( ( () => { name: 'baz' } )() ))   // 省略した場合は関数のブロックであると見なされ、関数の結果は undefined となる

run clear

アロー関数での「this」の取り扱いに関する改善

アロー関数では、通常の関数・関数式・メソッドとは「this」の取り扱いが異なります (より直感的に理解できる挙動となるように改善されました)。

// 非 strict モードの関数では this はグローバルオブジェクト (Web ブラウザでは window オブジェクト) となる
const f = function() { return this }
p(`f() === window: ${f() === window}`)

// strict モードの関数では this は undefined となる
const sf = function() { "use strict"; return this }
p(`sf() === undefined: ${sf() === undefined}`)

// call, apply を使用して「this となるオブジェクト」を指定することができる
const foo = {}
p(`f.call(foo) === foo: ${f.call(foo) === foo}`)
p(`f.apply(foo) === foo: ${f.apply(foo) === foo}`)

// メソッドでは、そのメソッドを包含するオブジェクトが「this」となる
const obj = { of() { return this } }
p(`obj.of() === obj: ${obj.of() === obj}`)

// ただし、オブジェクトからメソッドを「取り出し」て (オブジェクトを経由せずに) 直接呼び出すと通常の関数と同じ動作となる
const nf = obj.of
p(`nf() === window: ${nf() === window}`)

{
    "use strict";

    // 次の例では arr.map() に渡した関数における this が undefined となるため期待した動作とならない
    const obj2 = {
        value: 's',
        append(arr) {
            return arr.map( function(v) { return v + this.value } )
        }
    }
    p(`obj2.append(['foo', 'bar', 'baz']): ${obj2.append(['foo', 'bar', 'baz'])}`)

    // これを回避するためにはメソッドでの this を別の変数に退避させて置く必要があった
    const obj3 = {
        value: 's',
        append(arr) {
            var self = this
            return arr.map( function(v) { return v + self.value } )
        }
    }
    p(`obj3.append(['foo', 'bar', 'baz']): ${obj3.append(['foo', 'bar', 'baz'])}`)
}

// アロー関数では、this はそのアロー関数の一つ外側のスコープでの this となる
// 一つ外側も更にアロー関数である場合は、変数のスコープチェーンと同様に外側に向かって this の定義が探索される
// たどり着いた外側のスコープがグローバルスコープである場合、this はグローバルオブジェクト (Web ブラウザの場合は window) となる
const af = () => { return this }
p(`af() === window: ${af() === window}`)

// 外側のスコープがオブジェクトである場合、this はそのオブジェクトとなる。
// 次の例は期待通りに動作する。
const obj4 = {
    value: 's',
    append(arr) {
        return arr.map( (v) => v + this.value )
    }
}
p(`obj4.append(['foo', 'bar', 'baz']): ${obj4.append(['foo', 'bar', 'baz'])}`)

// メソッドをアロー関数とした場合、メソッドの this は undefined となることに注意する。
// 次の例は期待通りに動作しない。
const obj5 = {
    value: 's',
    append: (arr) => {
        return arr.map( (v) => v + this.value )
    }
}
p(`obj5.append(['foo', 'bar', 'baz']): ${obj5.append(['foo', 'bar', 'baz'])}`)

run clear

デフォルトパラメータ

関数パラメータにデフォルト値を指定できるようになった。

// 関数のパラメータにデフォルト値を指定できるようになった
const fn = (a, b = 1) => a * b
p(`fn(3): ${fn(3)}`)
p(`fn(3, 4): ${fn(3, 4)}`)

run clear

リテラル

2 進数, 8 進数リテラル表記 文字列・正規表現でのユニコード対応?

エクスポート・インポート

グローバル名前空間を汚染しない、モジュール間でのエクスポート・インポートがサポートされるようになりました (現在、どのブラウザでもサポートされていないらしい。別途 Babel などを使う必要がある)。

export default function foo() { ... }
export function bar(x) { ... }
export var baz = ...

のようなコードによってエクスポートされた関数・オブジェクトを、

import foo, { bar as b, baz } from 'lib/math'
foo()
b(baz)

などのように使用できます。 このとき、「export default」でエクスポートした関数・値はインポート側でブレースを省略できます。

クラス構文

class, extends, constructor 構文でクラスを定義できるようになりました (これらの構文は従来の prototype 定義を行うためのシンタックスシュガー)。

super() で基底クラスのコンストラクタを、super.〜 で基底クラスのメソッドを呼び出せます。

static 構文で static メソッド (インスタンスを作らずに、クラスに対して呼び出せるメソッド) を定義できます。

get, set 構文でプロパティの getter, setter を定義できます。

イテレータ

for ... of 構文によってオブジェクトプロパティの「値」に対してイテレーションできるようになりました (for ... in はオブジェクトプロパティの「キー」に対するイテレーション)。

for ... of 構文で使用されるイテレータオブジェクトは、例えば次のように処理されます。

let iter = arr.iterator()
for (let elem = iter.next(), value = elem.value; !elem.done; elem = iter.next()) {
    ...
}

つまりイテレータオブジェクトは、後続の要素が存在する場合は、donefalse が設定され、value に要素の値が設定されたオブジェクトを next() メソッドで返します。イテレーションが終了した場合は donetrue が設定されたオブジェクトを返します。

ジェネレータ

function* 構文によってジェネレータを定義できます (メソッドの場合は *foo() { ... } のように記述する)。 「ジェネレータ」とは、連続して生成されるオブジェクトにイテレータと同じインタフェースでアクセスできるようにする特別な関数のことを指します。 ジェネレータでは yield 文で評価した値が生成した値として返され、ジェネレータの処理はそこで一旦中断されます。 yield 文で値が返された後に再びジェネレータを呼び出した場合、ジェネレータの処理は最後に実行した yield 文の箇所から再開されます。 また、このとき yield 文を評価した結果は、次回ジェネレータが呼び出された際に渡される引数となります。

シンボル

Symbol という新たな型が導入されました。

  • Symbol('foo') === Symbol('foo')false となる
  • const foo = Symbol('foo'); obj[foo] = 'foo' としたときに Object.keys(obj) で返されない。for ... in ループの対象とならない
  • Object.getOwnPropertySymbols(obj) と呼び出したときにのみ返される

といった性質を持ちます。互換性を維持しつつ、JavaScript (ECMAScript) の標準クラスに特殊メソッド・プロパティを追加するといった用途に使われるようです。 現状、Symbol.iterator, Symbol.match といった「標準 Symbol」が定義されており、for ... of 構文でイテレーションしたり、正規表現マッチに使用できるかどうかを判定する用途に使用されています。つまり、iterator, match といったメソッドを定義してもこれらの仕組みの妨げになることはありません。また、Symbol.iterator という Symbol のメソッドを定義することにより for ... of 構文でその戻り値のイテレーションが可能となります。

データ構造

Set, Map, WeakSet, WeakMap クラスが追加されました。

TypedArray

バイト列を扱うための ArrayBuffer, ArrayBuffer を読み書きするための「TypedArray」が追加されました。 「TypedArray」には次のような種類があり、ArrayBuffer をどのように読み書きするかによって TypedArray を使い分けます。

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array

Promise, async/await

非同期処理 (何らかの処理完了後にコールバックさせる処理) を簡潔に書くための Promise クラスが導入されました。 ES2017 (ES8) では、新たに async/await という仕組みが追加され、更に簡潔に書けるようになっています。

また、ジェネレータが yield 文によって処理を中断する特性を利用した非同期処理ライブラリが存在します。非同期処理呼び出しを行う関数をジェネレータとして定義し、非同期処理呼び出しの箇所で yield する事により同期処理的にコーディングすることが可能になります。この用途に使用できるライブラリとしては、

  • Bluebird
  • Async
  • Q
  • co
  • aa

といったものがあるようです。

その他、Promise には同時並行に処理を実行する仕組みがあります。´Peomise.all()´, および Promise.race() というメソッドに ´Promise´ の配列 (iterable) を渡すことにより、同時並行に処理された複数の Promise の、全ての処理結果、または最初に完了した処理結果を処理することができます。

以降、それぞれの使用例を掲載します。

Promise の使用例

const msgAfterTimeout = (msg, who, timeout) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${msg} Hello ${who}!`), timeout)
    })
}
msgAfterTimeout("", "Foo", 50)
    .then((msg) => msgAfterTimeout(msg, "Bar", 100))
    .then((msg) => msgAfterTimeout(msg, "Baz", 150))
    .then((msg) => p(`done after 300ms:${msg}`))

run clear

async/await の使用例

const msgAfterTimeout = (msg, who, timeout) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => { return resolve(`${msg} Hello ${who}!`) }, timeout)
    })
}
let msg = ""
msg = await msgAfterTimeout(msg, "Foo",  50)
msg = await msgAfterTimeout(msg, "Bar", 100)
msg = await msgAfterTimeout(msg, "Baz", 150)
p(`done after 300ms:${msg}`)

run clear

generator/yield を使用した例

// 次の「asyncCall()」のような関数は、通常は一般的なライブラリ (co など) を利用する
const asyncCall = (proc) => {
    let procIter = proc()
    return new Promise((resolve, reject) => {
        let loop = (value) => {
            let result
            try {
                p(`procIter.next: ${value}`)
                result = procIter.next(`${value},`)
            }
            catch (err) {
                reject(err)
            }
            if (result.done)
                resolve(result.value)
            else if (  typeof result.value === "object"
                    && typeof result.value.then === "function")
                result.value.then(
                    value => { p(`then: ${value}`); loop(value) },
                    err => reject(err)
                    )
            else
                loop(result.value)
        }
        loop()
    })
}
const msgAfterTimeout = (msg, who, timeout) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(msg.length === 0 ? `Hello ${who}` : `${msg} ${who}`), timeout)
    })
}
asyncCall(function* () {    // * () => { ... } とは書けない
    let value = ""
    value = yield msgAfterTimeout(value, "Foo",  50)    // yield の値 (value に代入される値) は次回 generator 呼び出し時に渡される引数となる
    p(`yield value: ${value}`)
    value = yield msgAfterTimeout(value, "Bar", 100)
    p(`yield value: ${value}`)
    value = yield msgAfterTimeout(value, "Baz", 150)
    p(`yield value: ${value}`)
    return `done after 300ms:${value}!`
})
.then( msg => p(`msg: ${msg}`) )

run clear

Prpmise 導入以前のコールバック地獄

function msgAfterTimeout(msg, who, timeout, onDone) {
    setTimeout(function () {
        onDone(msg + " Hello " + who + "!");
    }, timeout);
}
msgAfterTimeout("", "Foo", 50, function (msg) {
    msgAfterTimeout(msg, "Bar", 100, function (msg) {
        msgAfterTimeout(msg, "Baz", 150, function (msg) {
            p("done after 300ms:" + msg);
        });
    });
});

run clear

Promise.all() の使用例

const fetchAsync = (url, resolve, reject) => {
    fetch(url)
        .then(res  => res.json())
        .then(json => resolve(`res.json: ${JSON.stringify(json)}`))
}
const fetchPromised = (url) => {
    return new Promise((resolve, reject) => fetchAsync(url, resolve, reject))
}
Promise.all([
        fetchPromised('json/foo.json'),
        fetchPromised('json/bar.json'),
    ])
    .then(data => {
        for (let elem of data)
            p(`elem: ${elem}`)
    })

国際化

Intl というクラスを使用することにより、

  • 数値
  • 通貨
  • 日付・時刻

をフォーマットできるようになりました。

参考 URL