PR

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

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

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

Unicode

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

リスト1:Unicodeリテラル

// ES5でのサロゲートペアの表現
let a = "\uD867\uDE49"
console.log(a) // 𩹉(トビウオ)

// ES2015での新しいUnicodeリテラル
let b = "\u{29E49}"
console.log(b) // 𩹉(トビウオ)

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

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

let a = /\u{29E49}/
a.test("\u{29E49}") // false

// Unicodeフラグを追加
let b = /\u{29E49}/u
b.test("\u{29E49}") // true

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

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

const fishes = "\u{29E49}\u{29E3d}"

// for...of文を使う
for(let fish of fishes){
  console.log(fish) // 𩹉 𩸽(ホッケ)
}

// Spread operatorを使う
let str = [...fishes]
console.log(str[0]) // 𩹉(トビウオ)

効率的なデータ構造

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

Map

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

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

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

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

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

for(var [key, val] of map){
  console.log(`key = ${key}, value = ${val}`)
}
// key = 1, value = one
// key = 2, value = two

let arr = [...map.values()]  // ["one", "two"]

Set

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

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

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

WeakMap

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

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

let wm = new WeakMap()
wm.set('1', 'one') // Uncaught TypeError: Invalid value used as weak map key

let obj = {}
wm.set(obj, 'one')
wm.get(obj) // one

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

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

// 参照するオブジェクトがなくなったタイミングでガベージコレクションの対象となる
class userCache {
  constructor() {
    this.userCache = new WeakMap()
  }

  getCache(ID) {
    return this.userCache.get(ID)
  }

  setCache(ID, cache) {
    this.userCache.set(ID, cache)
  }
}

WeakSet

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

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

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

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

Proxies

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

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

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

let validation = {
  set: function(target, prop, value, receiver){
    if(prop === 'age' && !(typeof value === 'number')){
      throw new TypeError('年齢は数字で入力してください。')
    }
    return target[name]
  }
}

let target = {}
let person = new Proxy(target, validation)
person.name = 'Alice'
person.age = 7
person.age = '10' // Uncaught TypeError: 年齢は数字で入力してください。

Symbols

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

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

let sym = Symbol('name')
let obj = {}

obj[sym] = 'Alice'
console.log(obj[sym]) // Alice
console.log(obj['name']) // Undefined

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

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

const log = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn')
}

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

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

let globalSymbol = Symbol.for('global')
console.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を追加する

let heroine = {name: 'Alice', age: 7}

heroine[Symbol.iterator] = () => {
  let iterator = {},
      i = 0,
      keys = Object.keys(heroine)

  iterator.next = () => {
    let done = !(i < keys.length),
        value = heroine[keys[i]]
    i++
    return done ? { done } : {value, done}
  }
  return iterator
}

for(status of heroine){
  console.log(status) // Alice 7
}

その他の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を継承したサブクラスの作成

class MyArray extends Array{
  rand(){
    return this[Math.floor(Math.random() * this.length)]
  }
}

let arr = new MyArray(1, 2, 5, 9)
arr.rand() // 2
arr[4] = 7 // コンストラクタも継承しているため、後から値を追加することも可能
arr.length // 5

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

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

Math

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

対数関連のメソッド

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

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

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

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

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

// ハイパボリックサイン(双曲線正弦)の値を返します
Math.sinh(1) // 1.1752011936438014
// ハイパボリックコサイン(双曲線余弦)の値を返します
Math.cosh(1) // 1.5430806348152437
// ハイパボリックタンジェント(双曲線正接)の値を返します
Math.tanh(1) // 0.7615941559557649

// ハイパボリックアークサイン(双曲線逆正弦)の値を返します
Math.asinh(1) // 0.881373587019543
// ハイパボリックアークコサイン(双曲線逆余弦)の値を返します
Math.acosh(1) // 0
// ハイパボリックアークタンジェント(双曲線逆正接)の値を返します
Math.atanh(1) // Infinity
算術関連のメソッド

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

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

// 与えられた引数の二乗和の平方根を返します
Math.hypot(3, 4) // 5

// 与えられた数の小数点部分を削除し整数を返します
Math.trunc(1.248) // 1
Math.trunc('-1.123') // -1

// 与えられた値の符号(正、負)もしくは0を返します
Math.sign(2) // 1
Math.sign(-1) // -1
Math.sign(0) // 0

// 与えられた値の立方根を返します
Math.cbrt(8) // 2
ビット計算関連のメソッド

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

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

Number

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

追加されたプロパティ

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

リスト20:EPSILON

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

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

// 与えられた数が有限数の場合trueを、それ以外の場合はfalseを返します
Number.isFinite(3) // true
Number.isFinite(Infinity) // false

// 与えられた値が整数の場合はtureを、それ以外の場合はfalseを返します
Number.isInteger(2) // true
Number.isInteger(Math.PI) // false

// 与えられた値がNaNの場合はtureを、それ以外の場合はfalseを返します
Number.isNaN(0/0) // true
// 下記の例だとグローバルのisNaN()ではtrueとなってしまっていましたが、
// Number.isNaNを使用することでより堅牢にチェックできます
Number.isNaN("NaN") // false

// 与えられた文字をパースし、浮動小数点数を返します
Number.parseFloat('123Daah!') // 123
// 最初の文字を数値に変換できない場合は、NaNを返します
Number.parseFloat('route53') // NaN

// 受け取った文字列を基数にもとづいて整数に変換します
// Number.parseIntはグローバルのparseInt()と機能的には同じです
// (目的はグローバル関数のモジュール化)
Number.parseInt('1000', 2) // 8

String

String.prototype.includes

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

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

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

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

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

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

Object

Object.assign

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

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

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

リスト24:ES5での例

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

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

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

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

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

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

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

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

let heroine = Object.assign({name: 'Alice'}, {status: {age: 7}}, {status: {gender: 'female'}})

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

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

deep-assign - Recursive Object.assign()

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

const deepAssign = require('deep-assign');
let heroine = deepAssign({name: 'Alice'}, {status: {age: 7}}, {status: {gender: 'female'}})

// deep-assignを使用することで深い階層のデータもマージされている
console.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での例

// arguments
function joinString(){
  let args = Array.prototype.slice.call(arguments);
  console.log(args.join(' ')) // Welcome to Japan!
}
joinString('Welcome', 'to', 'Japan!')

// NodeList
let el = document.querySelectorAll('div')
let divs = Array.prototype.slice.call(el)
console.log(divs); // [div, div]

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

リスト30:ES2015での例

// arguments
function joinString(){
  let args = Array.from(arguments);
  console.log(args.join(' ')) // Welcome to Japan!
}
joinString('Welcome', 'to', 'Japan!')

// NodeList
let el = document.querySelectorAll('div')
let divs = Array.from(el)
console.log(divs); // [div, div]

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

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

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

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

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

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

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

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

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

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

リスト34:

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

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

リクルートテクノロジーズ ITマネジメント統括部 ディベロップメント1部所属

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

連載記事一覧

開発言語技術解説
第5回

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

2017/3/28
ES2015に追加された非同期処理の新しい記述方法について学ぶ。
Web開発技術解説
第3回

ES2015で導入された、より洗練された構文 Part 2

2017/2/14
前回に続いて、ECMAScript 2015(ES2015)で追加された構文を紹介する。

Think IT会員サービスのご案内

Think ITでは、より付加価値の高いコンテンツを会員サービスとして提供しています。会員登録を済ませてThink ITのWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスのご案内

関連記事