ES2015に追加、拡張された機能

2017年3月8日(水)
今井 宏明

今回はECMAScript2015(ES2015)で追加、拡張された標準ライブラリの機能について紹介します。

Unicode

新しいUnicodeリテラルが追加されました。これにより、サロゲートペア対象の文字列でも分割せずに表記できるようになりました。

リスト1:Unicodeリテラル

1// ES5でのサロゲートペアの表現
2let a = "\uD867\uDE49"
3console.log(a) // 𩹉(トビウオ)
4 
5// ES2015での新しいUnicodeリテラル
6let b = "\u{29E49}"
7console.log(b) // 𩹉(トビウオ)

また、正規表現ではUnicodeフラグをつけることで、サロゲートペアを分割せずに1文字として処理することができます。

リスト2:Unicodeフラグをつけると、サロゲートペアを1文字と処理できる

1let a = /\u{29E49}/
2a.test("\u{29E49}") // false
3 
4// Unicodeフラグを追加
5let b = /\u{29E49}/u
6b.test("\u{29E49}") // true

Stringのイテレータはサロゲートペアを考慮してくれるため、下記の記述が可能です。

リスト3:StringのイテレータはUnicodeを正しく認識する

01const fishes = "\u{29E49}\u{29E3d}"
02 
03// for...of文を使う
04for(let fish of fishes){
05  console.log(fish) // 𩹉 𩸽(ホッケ)
06}
07 
08// Spread operatorを使う
09let str = [...fishes]
10console.log(str[0]) // 𩹉(トビウオ)

効率的なデータ構造

データのコレクションを扱うための標準ライブラリが追加されました。

Map

単純なKey-Valueの値を扱うMapが追加されました。JavaScriptではKey-Value形式のデータを扱う場合、一般的にオブジェクトを使用してきました。ES2015で追加されたMapを使用することで、keyに任意の値をもたせることができたり、Mapのデータを扱うための便利なメソッドを使用できるようになりました。

リスト4:Key-Value型式のデータを扱うMap

1let map = new Map()
2map.set('1', 'one')
3map.set(2, 'two')
4map.get(2) // two

MapオブジェクトやvaluesメソッドはIterableなため、for..ofやSpreadオペレーターを使用できます。

リスト5:MapオブジェクトはIterable

1for(var [key, val] of map){
2  console.log(`key = ${key}, value = ${val}`)
3}
4// key = 1, value = one
5// key = 2, value = two
6 
7let arr = [...map.values()]  // ["one", "two"]

Set

Setは一意の値を格納できる集合を表すコレクションライブラリです。格納される値は一意となるため、重複している値を追加しようとしてもSetに値は追加されません。

リスト6:一意の値を格納するSet

1let set = new Set()
2let story = {'title': 'Alice in Wonderland'}
3set.add(story)
4set.add('Alice')
5set.add('WhiteRabbit')
6set.size // 3
7set.add('Alice') // 重複した値を追加してみる
8// 値が重複していたためデータ数は変わらない
9set.size // 3

WeakMap

WeakMapは任意のオブジェクトをKeyにもつMapです。MapではKeyに任意のデータを持たせることができましたが、WeakMapではオブジェクトのみである点に注意してください。

リスト7:WeakMapのKeyはオブジェクトのみ

1let wm = new WeakMap()
2wm.set('1', 'one') // Uncaught TypeError: Invalid value used as weak map key
3 
4let obj = {}
5wm.set(obj, 'one')
6wm.get(obj) // one

WeakMapではKeyへの参照が弱参照となるため、keyのオブジェクトが外部から参照されなくなった場合にガベージコレクションの対象となります。この仕様を利用することで、メモリ効率を考慮したオブジェクトの保存(例えばキャッシュなど)を実現することができます。

リスト8:WeakMapの仕様を利用したコード例

01// 参照するオブジェクトがなくなったタイミングでガベージコレクションの対象となる
02class userCache {
03  constructor() {
04    this.userCache = new WeakMap()
05  }
06 
07  getCache(ID) {
08    return this.userCache.get(ID)
09  }
10 
11  setCache(ID, cache) {
12    this.userCache.set(ID, cache)
13  }
14}

WeakSet

WeakSetは任意のオブジェクトを格納するコレクションオブジェクトです。Setでは任意の値を追加できましたが、WeakSetの場合、追加できるのはオブジェクトのみとなります。

前述したWeakMapと同様にオブジェクトへの参照は弱参照となるため、格納されているオブジェクトの参照がない場合はガベージコレクションの対象となります。

リスト9:WeakSetを用いたコードの例

1let weakset = new WeakSet();
2let story = {'title': 'Alice in Wonderland'}
3weakset.add(story)
4weakset.add('Alice') // Invalid value used in weak set.

Proxies

Proxyを使用すると、オブジェクトに対しての基本的な動作(プロパティの参照、代入、列挙、関数の実行など)について独自に挙動を追加で定義することができます。

下記の例ではsetメソッドを定義することで、オブジェクトへの値代入時のValidationとして使用しています。

リスト10:Proxyの機能を利用したコードの例

01let validation = {
02  set: function(target, prop, value, receiver){
03    if(prop === 'age' && !(typeof value === 'number')){
04      throw new TypeError('年齢は数字で入力してください。')
05    }
06    return target[name]
07  }
08}
09 
10let target = {}
11let person = new Proxy(target, validation)
12person.name = 'Alice'
13person.age = 7
14person.age = '10' // Uncaught TypeError: 年齢は数字で入力してください。

Symbols

ES2015で追加されたSymbolは、ユニークなデータを保持するプリミティブ型のオブジェクトです。作成されたSymbolオブジェクトは一意な値となります。

リスト11:Symbolオブジェクト

1let sym = Symbol('name')
2let obj = {}
3 
4obj[sym] = 'Alice'
5console.log(obj[sym]) // Alice
6console.log(obj['name']) // Undefined

Symbolが一意な値になる性質を利用することで、列挙型にも活用することができます。

リスト12:Symbolを列挙型として利用する

1const log = {
2  DEBUG: Symbol('debug'),
3  INFO: Symbol('info'),
4  WARN: Symbol('warn')
5}

複数のファイルをまたいでSymbolを使用する場合は、グローバルSymbolレジストリを使用する必要があります。

リスト13:複数ファイル間でSymbolを使用する

1let globalSymbol = Symbol.for('global')
2console.log(Symbol.keyFor(globalSymbol)) // global

Well-known Symbols

Symbolは、言語内部のオブジェクトのふるまいをカスタマイズするWell-known Symbolsを持っています。前回の記事で説明した「Iterators + For..Of」に登場したSymbol.iteratorもWell-known Symbolsの一つです。

ES2015で導入された、より洗練された構文 Part 2 - Iterators + For..Of

Well-known Symbolsを使用することで、iteratorを持っていないオブジェクトに対して独自のiteratorを追加することもできます。

リスト14:独自のiteratorを追加する

01let heroine = {name: 'Alice', age: 7}
02 
03heroine[Symbol.iterator] = () => {
04  let iterator = {},
05      i = 0,
06      keys = Object.keys(heroine)
07 
08  iterator.next = () => {
09    let done = !(i < keys.length),
10        value = heroine[keys[i]]
11    i++
12    return done ? { done } : {value, done}
13  }
14  return iterator
15}
16 
17for(status of heroine){
18  console.log(status) // Alice 7
19}

その他のWell-known Symbolsについては、ECMAScript 2015 Language Specificationの「6.1.5.1 Well-Known Symbols」を参照してください。

ECMAScript® 2015 Language Specification - 6.1.5.1 Well-Known Symbols

Subclassable Built-ins

ES2015ではArrayやDate、DOM Elementsなどを継承し、サブクラスを作成することができるようになりました。

リスト15:Arrayを継承したサブクラスの作成

01class MyArray extends Array{
02  rand(){
03    return this[Math.floor(Math.random() * this.length)]
04  }
05}
06 
07let arr = new MyArray(1, 2, 5, 9)
08arr.rand() // 2
09arr[4] = 7 // コンストラクタも継承しているため、後から値を追加することも可能
10arr.length // 5

既存の標準ライブラリ(Math/Number/String/Object/Array)に追加された新機能

ES2015では既存の標準ライブラリに対しても新しい機能が追加されています。

Math

数学的な定義や関数を定義するMathオブジェクトに対して、対数や三角関数などの数学関数や、ビット演算に関連するスタティックメソッドが追加されました。

対数関連のメソッド

常用対数(10を底とする対数)や2を底とする対数を返すメソッドなどが追加されています。

リスト16:追加された対数関連のメソッド

1// 10を底とした対数を返します
2Math.log10(1000) // 3
3// 2を底とした対数を返します
4Math.log2(16) // 4
5// 渡した値と1の合計の自然対数を返します
6Math.log1p(2) // 1.0986122886681096
7e(自然対数の底であるネイピア数)のx乗から-1した値を返します
8Math.expm1(1) // 1.718281828459045
三角関数関連のメソッド

双曲線関数、逆双曲線関数を返すメソッドが追加されています。

リスト17:追加された三角関数関連のメソッド

01// ハイパボリックサイン(双曲線正弦)の値を返します
02Math.sinh(1) // 1.1752011936438014
03// ハイパボリックコサイン(双曲線余弦)の値を返します
04Math.cosh(1) // 1.5430806348152437
05// ハイパボリックタンジェント(双曲線正接)の値を返します
06Math.tanh(1) // 0.7615941559557649
07 
08// ハイパボリックアークサイン(双曲線逆正弦)の値を返します
09Math.asinh(1) // 0.881373587019543
10// ハイパボリックアークコサイン(双曲線逆余弦)の値を返します
11Math.acosh(1) // 0
12// ハイパボリックアークタンジェント(双曲線逆正接)の値を返します
13Math.atanh(1) // Infinity
算術関連のメソッド

立方根や、与えられた値の符号(正、負、ゼロ)を返すメソッドなどが追加されています。

リスト18:追加された算術関連のメソッド

01// 与えられた引数の二乗和の平方根を返します
02Math.hypot(3, 4) // 5
03 
04// 与えられた数の小数点部分を削除し整数を返します
05Math.trunc(1.248) // 1
06Math.trunc('-1.123') // -1
07 
08// 与えられた値の符号(正、負)もしくは0を返します
09Math.sign(2) // 1
10Math.sign(-1) // -1
11Math.sign(0) // 0
12 
13// 与えられた値の立方根を返します
14Math.cbrt(8) // 2
ビット計算関連のメソッド

リスト19:追加されたビット計算関連のメソッド

1// 与えられた2つの引数の32ビット乗算の結果を返します
2Math.imul(3, 4) // 12
3// 与えられた引数に最も近い単精度float(32bit浮動小数点数)を返します
4Math.fround(3.14) // 3.140000104904175
5// 与えられた数を32ビット符号なし整数値で表した際、先頭に並ぶ0の個数を返します
6// 8 = 00000000:00000000:00000000:00001000
7Math.clz32(8) // 28

Number

数値を扱うNumberオブジェクトに対して、新たなプロパティやメソッドが追加されました。

追加されたプロパティ

EPSILON(ε)は、Numberオブジェクトで表現できる最小の正数を表します。

リスト20:EPSILON

1// Numberとして表現できる最小の値
2Number.EPSILON // 2.220446049250313e-16
追加されたメソッド

リスト21:追加されたNumberに関するメソッド

01// 与えられた数が有限数の場合trueを、それ以外の場合はfalseを返します
02Number.isFinite(3) // true
03Number.isFinite(Infinity) // false
04 
05// 与えられた値が整数の場合はtureを、それ以外の場合はfalseを返します
06Number.isInteger(2) // true
07Number.isInteger(Math.PI) // false
08 
09// 与えられた値がNaNの場合はtureを、それ以外の場合はfalseを返します
10Number.isNaN(0/0) // true
11// 下記の例だとグローバルのisNaN()ではtrueとなってしまっていましたが、
12// Number.isNaNを使用することでより堅牢にチェックできます
13Number.isNaN("NaN") // false
14 
15// 与えられた文字をパースし、浮動小数点数を返します
16Number.parseFloat('123Daah!') // 123
17// 最初の文字を数値に変換できない場合は、NaNを返します
18Number.parseFloat('route53') // NaN
19 
20// 受け取った文字列を基数にもとづいて整数に変換します
21// Number.parseIntはグローバルのparseInt()と機能的には同じです
22// (目的はグローバル関数のモジュール化)
23Number.parseInt('1000', 2) // 8

String

String.prototype.includes

対象の文字列の中に特定の文字列が存在するかどうかを判別します。検索する文字列が含まれていた場合はtrueを、含まれていなかった場合はfalseを返します。

リスト22:Stringに追加されたincludesメソッド

1let alankay = 'The best way to predict the future is to invent it.'
2alankay.includes('The best', 0) // true
3alankay.includes('The best', 1) // false 検索開始のインデックスが1のため
String.prototype.repeat

与えられた文字数を指定された回数だけ繰り返して新しい文字列を作成します。

リスト23:Stringに追加されたrepeatメソッド

1let res = 'FizzBuzz'.repeat(3) // FizzBuzzFizzBuzzFizzBuzz

Object

Object.assign

ターゲットオブジェクトに対して、一つ以上のオブジェクトをコピーすることができます。ES5まではオブジェクトを=で代入した場合は参照渡しとなってしまい、オブジェクトのコピーができませんでした。そのためオブジェクトをコピーするためにjQueryの$.extend()などを使用していました。

ES2015で追加されたObject.assignでは、オブジェクトがターゲットオブジェクトにコピーされるため、ライブラリなどを使用しなくとも意図したオブジェクトのコピーを実装することが可能となりました。

ES5でのオブジェクトコピー

リスト24:ES5での例

1let name = {name: 'Alice'}
2let heroine = name
3heroine.age = 7
4console.log(heroine) // Object {name: "Alice", age: 7}
5// 参照渡しのため、元のオブジェクトにも値が追加されてしまう
6console.log(name) // Object {name: "Alice", age: 7}
ES2015でObject.assign()を使用したオブジェクトコピー

リスト25:Object.assignメソッドを用いたES2015での例

1let name = {name: 'Alice'}
2let heroine = Object.assign({}, name, {age: 7})
3console.log(heroine) // Object {name: "Alice", age: 7}
4console.log(name) // Object {name: "Alice"}
Object.assign() を使用したオブジェクトのマージ

Object.assign()を使用することで、オブジェクトをマージすることも可能です。オブジェクトのマージの際は、Object.assignの第一引数(targetオブジェクト)に設定されているオブジェクトへ他のオブジェクトがマージされるため、ベースとなったオブジェクトの内容も合わせて更新されます。

リスト26:オブジェクトのマージ

1let name = {name: 'Alice'}
2let age = {age: 7}
3let gender = {gender: 'female'}
4let heroine = Object.assign(name, age, gender)
5console.log(heroine) // Object {name: "Alice", age: 7, gender: "female"}
6// ベースとなるオブジェクトの値も修正される
7console.log(name) // Object {name: "Alice", age: 7, gender: "female"}
オブジェクトの階層が深い場合のマージ

上記のとおりObject.assign()にてオブジェクトのコピー、マージが便利に実施できるようになりましたが、オブジェクトの階層が深くなった場合、深い階層のデータがマージできないという問題があります。

リスト27:深い階層のデータのマージに失敗する例

1let heroine = Object.assign({name: 'Alice'}, {status: {age: 7}}, {status: {gender: 'female'}})
2 
3// {age: 7} の情報が上書きされてしまっている
4console.log(heroine) // Object {name: "Alice", status: {gender: "female"}}

これについては、npmで公開されているdeep-assignなどを活用して対応可能です。

deep-assign - Recursive Object.assign()

リスト28:deep-assignの使用例

1const deepAssign = require('deep-assign');
2let heroine = deepAssign({name: 'Alice'}, {status: {age: 7}}, {status: {gender: 'female'}})
3 
4// deep-assignを使用することで深い階層のデータもマージされている
5console.log(heroine) // { name: 'Alice', status: { age: 7, gender: 'female' } }

Array

Array.from

Array.fromメソッドは、array-likeオブジェクトやiterableオブジェクトから新しいArrayを作成します。

ES5ではdocument.querySelectorAll()が返すNodeListやargumentsはarray-likeオブジェクトのため、下記のように一度Array型に変換してから使用する必要がありました。

リスト29:ES5での例

01// arguments
02function joinString(){
03  let args = Array.prototype.slice.call(arguments);
04  console.log(args.join(' ')) // Welcome to Japan!
05}
06joinString('Welcome', 'to', 'Japan!')
07 
08// NodeList
09let el = document.querySelectorAll('div')
10let divs = Array.prototype.slice.call(el)
11console.log(divs); // [div, div]

Array.fromを使用することで、array-likeオブジェクトをArray型に変換することができます。

リスト30:ES2015での例

01// arguments
02function joinString(){
03  let args = Array.from(arguments);
04  console.log(args.join(' ')) // Welcome to Japan!
05}
06joinString('Welcome', 'to', 'Japan!')
07 
08// NodeList
09let el = document.querySelectorAll('div')
10let divs = Array.from(el)
11console.log(divs); // [div, div]

また、Array.fromはmapFnを持つため、作成した配列に対してmap関数を実行することができます。

リスト31:Array.fromで作成した配列にmap関数を実行

1let admissionFee = [1500, 1000, 700]
2Array.from(admissionFee, fee => fee / 2) // [750, 500, 350]
Array.of

可変長の引数から新しいArrayを作成できます。

リスト32:Array.ofを用いたArrayの作成

1Array.of('Welcome', 'to', 'Japan!') // ["Welcome", "to", "Japan!"]
Array.prototype.copyWithin

第1引数で指定するターゲットで始まる位置に、第2、第3引数で指定したstartとendに対応したインデックスをコピーします。コピーされる範囲は、startからendの1つ前までになります。

リスト33:Array.prototype.copyWithinの使用例

1let arr = [0, 1, 2, 3, 4, 5]
2arr.copyWithin(1, 3, 5) // [0, 3, 4, 3, 4, 5]
Array.prototype.entries

配列内の各要素に対するkeyとvalueの組み合わせを持つ新しいArrayを作成します。

リスト34:

1let hello = ['My', 'name', 'is', 'Alice.'].entries()
2hello.next().value // [0, "My"]
3hello.next().value // [1, "name"]

keyのみ、もしくはvalueのみを新しいArrayが欲しい場合は、Array.prototype.keysArray.prototype.valuesを使用して値を取り出すことができます。

リクルートテクノロジーズ ITマネジメント統括部 オフショアソリューション部所属

2015年4月中途入社。前職はSIerにてWebサービスやアプリの開発からインフラ構築まで幅広い業務を担当。リクルートテクノロジーズ入社後はオフショアマネジメントを経てフロントエンドチームへ参画。週末は子育てエンジニア。ラーメンは毎日食べても飽きない。

連載バックナンバー

Web開発技術解説
第6回

ES2015のモジュール管理

2017/5/9
ES2015に追加されたモジュール管理の機能を紹介し、現行のブラウザから使用する方法を解説する。
開発言語技術解説
第5回

ES2015が備えるモダンな非同期処理

2017/3/28
ES2015に追加された非同期処理の新しい記述方法について学ぶ。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています