ES2015に追加、拡張された機能
今回はECMAScript2015(ES2015)で追加、拡張された標準ライブラリの機能について紹介します。
Unicode
新しいUnicodeリテラルが追加されました。これにより、サロゲートペア対象の文字列でも分割せずに表記できるようになりました。
// ES5でのサロゲートペアの表現 let a = "\uD867\uDE49" console.log(a) // 𩹉(トビウオ) // ES2015での新しいUnicodeリテラル let b = "\u{29E49}" console.log(b) // 𩹉(トビウオ)
また、正規表現ではUnicodeフラグをつけることで、サロゲートペアを分割せずに1文字として処理することができます。
let a = /\u{29E49}/ a.test("\u{29E49}") // false // Unicodeフラグを追加 let b = /\u{29E49}/u b.test("\u{29E49}") // true
Stringのイテレータはサロゲートペアを考慮してくれるため、下記の記述が可能です。
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のデータを扱うための便利なメソッドを使用できるようになりました。
let map = new Map() map.set('1', 'one') map.set(2, 'two') map.get(2) // two
MapオブジェクトやvaluesメソッドはIterableなため、for..ofやSpreadオペレーターを使用できます。
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に値は追加されません。
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ではオブジェクトのみである点に注意してください。
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のオブジェクトが外部から参照されなくなった場合にガベージコレクションの対象となります。この仕様を利用することで、メモリ効率を考慮したオブジェクトの保存(例えばキャッシュなど)を実現することができます。
// 参照するオブジェクトがなくなったタイミングでガベージコレクションの対象となる 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と同様にオブジェクトへの参照は弱参照となるため、格納されているオブジェクトの参照がない場合はガベージコレクションの対象となります。
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として使用しています。
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オブジェクトは一意な値となります。
let sym = Symbol('name') let obj = {} obj[sym] = 'Alice' console.log(obj[sym]) // Alice console.log(obj['name']) // Undefined
Symbolが一意な値になる性質を利用することで、列挙型にも活用することができます。
const log = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn') }
複数のファイルをまたいでSymbolを使用する場合は、グローバル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を追加することもできます。
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などを継承し、サブクラスを作成することができるようになりました。
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を底とする対数を返すメソッドなどが追加されています。
// 10を底とした対数を返します Math.log10(1000) // 3 // 2を底とした対数を返します Math.log2(16) // 4 // 渡した値と1の合計の自然対数を返します Math.log1p(2) // 1.0986122886681096 e(自然対数の底であるネイピア数)のx乗から-1した値を返します Math.expm1(1) // 1.718281828459045
三角関数関連のメソッド
双曲線関数、逆双曲線関数を返すメソッドが追加されています。
// ハイパボリックサイン(双曲線正弦)の値を返します 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
算術関連のメソッド
立方根や、与えられた値の符号(正、負、ゼロ)を返すメソッドなどが追加されています。
// 与えられた引数の二乗和の平方根を返します 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
ビット計算関連のメソッド
// 与えられた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オブジェクトで表現できる最小の正数を表します。
// Numberとして表現できる最小の値 Number.EPSILON // 2.220446049250313e-16
追加されたメソッド
// 与えられた数が有限数の場合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を返します。
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
与えられた文字数を指定された回数だけ繰り返して新しい文字列を作成します。
let res = 'FizzBuzz'.repeat(3) // FizzBuzzFizzBuzzFizzBuzz
Object
Object.assign
ターゲットオブジェクトに対して、一つ以上のオブジェクトをコピーすることができます。ES5まではオブジェクトを=で代入した場合は参照渡しとなってしまい、オブジェクトのコピーができませんでした。そのためオブジェクトをコピーするためにjQueryの$.extend()などを使用していました。
ES2015で追加されたObject.assignでは、オブジェクトがターゲットオブジェクトにコピーされるため、ライブラリなどを使用しなくとも意図したオブジェクトのコピーを実装することが可能となりました。
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()を使用したオブジェクトコピー
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オブジェクト)に設定されているオブジェクトへ他のオブジェクトがマージされるため、ベースとなったオブジェクトの内容も合わせて更新されます。
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()にてオブジェクトのコピー、マージが便利に実施できるようになりましたが、オブジェクトの階層が深くなった場合、深い階層のデータがマージできないという問題があります。
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()
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型に変換してから使用する必要がありました。
// 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型に変換することができます。
// 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関数を実行することができます。
let admissionFee = [1500, 1000, 700] Array.from(admissionFee, fee => fee / 2) // [750, 500, 350]
Array.of
可変長の引数から新しいArrayを作成できます。
Array.of('Welcome', 'to', 'Japan!') // ["Welcome", "to", "Japan!"]
Array.prototype.copyWithin
第1引数で指定するターゲットで始まる位置に、第2、第3引数で指定したstartとendに対応したインデックスをコピーします。コピーされる範囲は、startからendの1つ前までになります。
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を作成します。
let hello = ['My', 'name', 'is', 'Alice.'].entries() hello.next().value // [0, "My"] hello.next().value // [1, "name"]
keyのみ、もしくはvalueのみを新しいArrayが欲しい場合は、Array.prototype.keysやArray.prototype.valuesを使用して値を取り出すことができます。