以下はAlbertoM( Webサイト / Twitter / GitHub / dev.to )による記事、Everything you need to know from ES2016 to ES2019の日本語訳です。
Everything you need to know from ES2016 to ES2019
JavaScriptは絶え間なく進化し続けている言語であり、この数年で多くの新機能がECMAScriptの仕様に追加されました。
この記事は私の著書Complete Guide to Modern JavaScriptからの抜粋であり、そしてこの本はES2016・ES2017・ES2018・ES2019の新しい機能について解説しています。
記事の最後に、全てを要約したチートシートを用意しています。
Everything new in ES2016
ES2016で追加された機能はわずか二つです。
・Array.prototype.includes()
・指数演算子
Array.prototype.includes()
includes()
メソッドは、配列に特定の値が含まれる場合はtrueを返し、含まれていなければfalseを返します。
let array = [1,2,4,5];
array.includes(2); // true
array.includes(3); // false
Combine includes() with fromIndex
検索を開始するインデックスをincludes()
に渡すことができます。
デフォルトは0で、負の値を渡すこともできます。
最初の値は検索する要素で、2番目の値は開始インデックスです。
let array = [1,3,5,7,9,11];
array.includes(3,1); // true 1番目つまり最初から検索開始する
array.includes(5,4); // false
array.includes(1,-1); // false 最後尾から検索開始する
array.includes(11,-3); // true
array.includes(5,4);
はfalseを返します。
4番目の値から検索を開始するため、2番目にある値5は検索範囲外だからです。
array.includes(1,-1);
はfalseを返します。
インデックス-1
は配列の最後の要素という意味で、そこから検索を開始するためです。
array.includes(11,-3);
はtrueを返します。
インデックス-3
は配列の最後から3番目の要素という意味で、そこから検索を始めたため範囲内に11を発見しました。
The exponential operator
ES2016より前は、以下のように書いていました。
Math.pow(2,2); // 4
Math.pow(2,3); // 8
指数演算子の導入により、以下のように書けるようになりました。
2**2; // 4
2**3; // 8
複数の演算を組み合わせる際に特に役立ちます。
2**2**2; // 16
Math.pow(Math.pow(2,2),2); // 16
Math.pow()
を使うと長く面倒な書式になります。
指数演算子を使うと、同じことをより早くよりクリーンに記述することができます。
ES2017 string padding, Object.entries(), Object.values() and more
ES2017では、多くのクールな新機能が追加されました。
それらを以下に解説していきます。
String padding (.padStart() and .padEnd())
文字列の先頭もしくは末尾にパディングを付けられます。
"hello".padStart(6); // " hello"
"hello".padEnd(6); // "hello "
パディング値に6を指定したのに、スペースが1しか入らなかったのはなぜでしょう。
padStartとpadEndは規定文字数になるように穴埋めするからです。
上記例の場合、"hello"は5文字あるため、不足した1文字だけがパディングされました。
"hi".padStart(10); // " hi" 8文字埋められた
"welcome".padStart(10); // " welcome" 4文字埋められた
Right align with padStart
値を右揃えしたいときにもpadStartが使えます。
const strings = ["short", "medium length", "very long string"];
const longestString = strings.sort(str => str.length).map(str => str.length)[0];
strings.forEach(str => console.log(str.padStart(longestString)));
// very long string
// medium length
// short
まず、最も長い文字列の長さを取得します。
次いですべての文字列にpadStartを適用し、文字列長を最も長い文字列に合わせました。
Add a custom value to the padding
スペースだけではなく、任意の文字でパディングすることができます。
"hello".padEnd(13," Alberto"); // "hello Alberto"
"1".padStart(3,0); // "001"
"99".padStart(3,0); // "099"
Object.entries() and Object.values()
とりあえずObjectを作成します。
const family = {
father: "Jonathan Kent",
mother: "Martha Kent",
son: "Clark Kent",
}
かつてのJavaScriptは、プロパティに以下のようにアクセスしていました。
Object.keys(family); // ["father", "mother", "son"]
family.father; // "Jonathan Kent"
Object.keys()
は、プロパティのキーのみを返します。
ES2017では、プロパティにアクセスする方法が2種類増えました。
Object.values(family); // ["Jonathan Kent", "Martha Kent", "Clark Kent"]
Object.entries(family);
// ["father", "Jonathan Kent"]
// ["mother", "Martha Kent"]
// ["son", "Clark Kent"]
Object.values()
はプロパティ値のみの配列を返し、Object.entries()
はキーと値の両方を含む配列の配列を返します。
Object.getOwnPropertyDescriptors()
このメソッドは、オブジェクトの持つ全てのプロパティディスクリプタを返します。
ディスクリプタの属性はvalue・writable・get・set・configurable・enumerableです。
const myObj = {
name: "Alberto",
age: 25,
greet() {
console.log("hello");
},
}
Object.getOwnPropertyDescriptors(myObj);
// age:{value: 25, writable: true, enumerable: true, configurable: true}
// greet:{value: ƒ, writable: true, enumerable: true, configurable: true}
// name:{value: "Alberto", writable: true, enumerable: true, configurable: true}
Trailing commas in function parameter lists and calls
これは、本当に小さな変更です。
しかしこれで、そのパラメータが最後であるかどうかをいちいち気にせずに末尾カンマを書けるようになりました。
// 昔
const object = {
prop1: "prop",
prop2: "propop"
}
// 現在
const object = {
prop1: "prop",
prop2: "propop",
}
2番目のプロパティは最後にカンマが増えていることに注目してください。
入れなくてもエラーにはなりませんが、同僚やチームメイトの生活を楽にするためにも入れておくことをお勧めします。
// カンマ入れない
const object = {
prop1: "prop",
prop2: "propop"
}
// 同僚が最終行をコピペしてプロパティを追加した
const object = {
prop1: "prop",
prop2: "propop"
prop3: "propopop"
}
// 突然エラーが出るようになった
Shared memory and Atomics
メモリーが共有されている場合、複数のスレッドがメモリー内の同じデータを読み書きできます。アトミック演算では、予測される値の書き込みと読み込みを保証するため、次の演算が開始される前に現在の演算が完了し、その演算が割り込まれないようにします。
Atomicはコンストラクタではありません。
プロパティとメソッドは全て静的であり、newしたりinvokeしたり関数として呼び出したりすることはできません。
Atomicが持つメソッドは以下のようなものがあります。
・ add / sub
・ and / or / xor
・ load / store
Atomicは、汎用固定長バイナリデータバッファSharedArrayBuffer
などで使用されます。
いくつかの例を見てみましょう。
Atomics.add(), Atomics.sub(), Atomics.load() and Atomics.store()
// SharedArrayBuffer作成
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);
// これに各計算する
uint8[0] = 10;
console.log(Atomics.add(uint8, 0, 5)); // 10
console.log(uint8[0]) // 15
console.log(Atomics.load(uint8,0)); // 15
最初のAtomics.add()
は加算を行いますが、返り値は計算する前の値です。
その後uint8[0]
を参照すると、addが実行されたあとなので値が15になっていることが確認できます。
配列値をAtomicに取得するにはAtomics.load()
を使い、第一引数が対象の配列、第二引数がインデックスです。
Atomics.sub()
はAtomics.add()
と同じ挙動で、減算を行います。
// SharedArrayBuffer作成
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);
// これに各計算する
uint8[0] = 10;
console.log(Atomics.sub(uint8, 0, 5)); // 10
console.log(uint8[0]) // 5
console.log(Atomics.store(uint8,0,3)); // 3
console.log(Atomics.load(uint8,0)); // 3
Atomics.sub()
を使って10から5を引きました。
計算自体の返り値は、Atomics.add()
と同じく計算する前の値であり、すなわち10です。
次にAtomics.store()
を使い特定の値、今回は配列の0番目のインデックスに3を登録しました。
Atomics.store()
は渡した値をそのまま返します。
Atomics.load()
すると、値は書き替えられた後なので5ではなく3になります。
Atomics.and(), Atomics.or() and Atomics.xor()
これらはそれぞれAND、OR、XORのビット演算を行います。
ビット演算の詳細はWikipediaなどで読むことができます。
ES2017 Async and Await
ES2017では、async/await
と呼ばれる新たなPromiseが導入されました。
Promise review
その前に、まず普通のPromise構文を簡単に復習しましょう。
// GitHubからユーザを取得
fetch('api.github.com/user/AlbertoMontalesi').then( res => {
// 値をJSONで返す
return res.json();
}).then(res => {
// 全てが正常に動作したらここに来る
console.log(res);
}).catch( err => {
// エラーがあったらここに来る
console.log(err);
})
GitHubからユーザを取得してコンソールに出力するだけの簡単な例です。
また別の例を見てみましょう。
function walk(amount) {
return new Promise((resolve,reject) => {
if (amount < 500) {
reject ("the value is too small");
}
setTimeout(() => resolve(`you walked for ${amount}ms`),amount);
});
}
walk(1000).then(res => {
console.log(res);
return walk(500);
}).then(res => {
console.log(res);
return walk(700);
}).then(res => {
console.log(res);
return walk(800);
}).then(res => {
console.log(res);
return walk(100);
}).then(res => {
console.log(res);
return walk(400);
}).then(res => {
console.log(res);
return walk(600);
});
// you walked for 1000ms
// you walked for 500ms
// you walked for 700ms
// you walked for 800ms
// uncaught exception: the value is too small
Async and Await
これをasync/await
で書き換えるとこうなります。
function walk(amount) {
return new Promise((resolve,reject) => {
if (amount < 500) {
reject ("the value is too small");
}
setTimeout(() => resolve(`you walked for ${amount}ms`),amount);
});
}
// asyncなfunctionを作成
async function go() {
// awaitがあれば終わるまで待つ
const res = await walk(500);
console.log(res);
const res2 = await walk(900);
console.log(res2);
const res3 = await walk(600);
console.log(res3);
const res4 = await walk(700);
console.log(res4);
const res5 = await walk(400);
console.log(res5);
console.log("finished");
}
go();
// you walked for 500ms
// you walked for 900ms
// you walked for 600ms
// you walked for 700ms
// uncaught exception: the value is too small
非同期関数を作成するには、まずasync
キーワードを記載します。
このキーワードが入った関数はPromiseを返すようになります。
Promiseでない値を返そうとした場合、自動的にPromiseでラップされて返されます。
await
キーワードは、async
関数内でのみ機能します。
await
を書くと、プログラムはPromiseが結果を返すまでそこで停止します。
async
関数の外でawait
キーワードを使うとどうなるでしょうか。
// asyncではない関数
function func() {
let promise = Promise.resolve(1);
let result = await promise;
}
func();
// SyntaxError: await is only valid in async functions and async generators
// トップレベル
let response = Promise.resolve("hi");
let result = await response;
// SyntaxError: await is only valid in async functions and async generators
復習:await
は、async
関数内でのみ使用可能。
Error handling
Promiseではエラーを.catch()
でキャッチします。
これについては特に違いはありません。
async function asyncFunc() {
try {
let response = await fetch('http:your-url');
} catch(err) {
console.log(err);
}
}
asyncFunc(); // TypeError: failed to fetch
関数内でエラーハンドリングしていない場合は、以下のように書くこともできます。
async function asyncFunc(){
let response = await fetch('http:your-url');
}
asyncFunc(); // Uncaught (in promise) TypeError: Failed to fetch
asyncFunc().catch(console.log); // TypeError: Failed to fetch
ES2018 Async Iteration and more?
ES2018で導入された機能も見ていきましょう。
Rest / Spread for Objects
ES6 (ES2015) でスプレッド構文が導入されたことを覚えていますか?
const veggie = ["tomato","cucumber","beans"];
const meat = ["pork","beef","chicken"];
const menu = [...veggie, "pasta", ...meat];
console.log(menu); // Array [ "tomato", "cucumber", "beans", "pasta", "pork", "beef", "chicken" ]
スプレッド構文にRestパラメータがオブジェクトに対しても使用可能になりました。
let myObj = {
a:1,
b:3,
c:5,
d:8,
}
// zは残り全部
let { a, b, ...z } = myObj;
console.log(a); // 1
console.log(b); // 3
console.log(z); // {c: 5, d: 8}
// スプレッド構文でクローン
let clone = { ...myObj };
console.log(clone); // {a: 1, b: 3, c: 5, d: 8}
myObj.e = 15;
console.log(clone) // {a: 1, b: 3, c: 5, d: 8}
console.log(myObj) // {a: 1, b: 3, c: 5, d: 8, e: 15}
スプレッド構文を使うとオブジェクトのクローンが簡単に作成できます。
元のオブジェクトを変更しても、クローンしたオブジェクトは変更されません。
Asynchronous Iteration
非同期イテレータを用いて、データを非同期的に反復することができます。
ドキュメントによると、
非同期イテレータは、
next()
メソッドが{ value, done }
のペアを返すこと以外、イテレータにそっくりです。
従って、for-await-of
ループで反復処理してPromiseにすることができます。
const iterables = [1,2,3];
async function test() {
for await (const value of iterables) {
console.log(value);
}
}
test();
// 1
// 2
// 3
ループの実行中、[Symbol.asyncIterator]()
を用いてデータソースから非同期イテレータを作成します。
次のループにアクセスするたびに、返ってきたPromiseを暗黙的にawaitします。
Promise.prototype.finally()
Promiseが終了したときに呼び出されます。
const myPromise = new Promise((resolve,reject) => {
resolve();
})
myPromise
.then( () => {
console.log('still working');
})
.catch( () => {
console.log('there was an error');
})
.finally(()=> {
console.log('Done!');
})
.finally()
もPromiseを返すので、さらにthen
やcatch
を続けることも可能ですが、そこに渡ってくるPromiseはfinallyではなく元の値です。
const myPromise = new Promise((resolve,reject) => {
resolve();
})
myPromise
.then( () => {
console.log('still working');
return 'still working';
})
.finally(()=> {
console.log('Done!');
return 'Done!';
})
.then( res => {
console.log(res); // still working
})
見てのとおり、最後のthen
に渡ってくるPromise
は、finally
によるものではなく最初のthen
が作ったものです。
RegExp features
正規表現の機能が4種類追加されました。
s(dotAll) flag for regular expressions
s
フラグが導入されました。
これにより、'.'が改行を含む任意の1文字に一致するようになります。
/foo.bar/s.test('foo\nbar'); // true
RegExp named capture groups
ドキュメントによると、
番号付きキャプチャグループを使用して、正規表現が一致した文字列の特定の個所を参照することができます。
キャプチャグループには一意の番号が割り当てられており、その番号で参照することができますが、これにより正規表現の把握とリファクタリングが難しくなります。
日付に一致する/(\d{4})-(\d{2})-(\d{2})/
を例に取ると、どの番号が月に対応し、どの番号が日に対応しているかは、コードをよく調べてみないと理解できません。
また月と日の順番を入れ替えたいとなったら、参照する番号の方まで書き換えなければなりません。
ここでキャプチャグループに(?<name>...)
構文を用いて、識別子name
で参照することができるようになります。
日付の正規表現を/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
と書けます。
各識別子は一意であり、ECMAScript
の命名規則に従う必要があります。
名前付きキャプチャグループには、返り値のgroupsプロパティからアクセスすることができます。
以下の例のように、名前付きキャプチャグループと同時に、番号付きの参照も作成されます。
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';
// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
console.log(`one: ${one}, two: ${two}`); // one: foo, two: bar
RegExp Lookbehind Assertions
ドキュメントによると、
後読みアサーションを使用すると、手前に別のパターンが存在するパターンにマッチすることができます。
たとえば$
記号を含まずに金額だけマッチするような使い方ができます。
肯定後読みアサーションは(?<=...)
と記述します。
$
記号を含まずに金額だけマッチさせたい場合は/(?<=$)\d+(\.\d*)?/
とし、これは$10.53
にマッチして10.53
がキャプチャされます。
しかし€10.53
にはマッチしません。
否定後読みアサーションは(?<!...)
と記述し、手前に別のパターンが存在しないパターンにマッチします。
/(?<!$)\d+(?:\.\d*)/
は$10.53
にマッチしませんが、€10.53
にはマッチします。
RegExp Unicode Property Escapes
ドキュメントによると、
\p{…}
および\P{…}
形式のUnicodeプロパティエスケープが追加されます。
Unicodeプロパティエスケープは、u
フラグの指定された正規表現で使用可能な新しいタイプのエスケープシーケンスです。
これを使うと、以下のような記述が可能になります。
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π'); // true
Lifting template literals restriction
タグ付きテンプレートリテラルを使うと、エスケープシーケンスの制限を気にする必要がなくなります。
詳細はこちらをご覧ください。
What's new in ES2019?
ECMAScriptの最新バージョンである、ES2019で追加されたものを見ていきましょう。
Array.prototype.flat() / Array.prototype.flatMap()
Array.prototype.flat()
は、指定された深さまでの配列を再帰的にフラット化します。
深さ引数のデフォルトは1です。
Infinity
を指定すると、無制限にネストを解除します。
const letters = ['a', 'b', ['c', 'd', ['e', 'f']]];
// デフォルトは1
letters.flat(); // ['a', 'b', 'c', 'd', ['e', 'f']]
// 2段階
letters.flat(2); // ['a', 'b', 'c', 'd', 'e', 'f']
// 2段 = 1段 * 2
letters.flat().flat(); // ['a', 'b', 'c', 'd', 'e', 'f']
// ネストがなくなるまで再帰的にフラット化
letters.flat(Infinity) // ['a', 'b', 'c', 'd', 'e', 'f']
Array.prototype.flatMap()
は、引数の取り扱いはflat()
と同じです。
配列を単純にフラット化するのではなく、関数を渡して任意の処理を行うことができます。
let greeting = ["Greetings from", " ", "Vietnam"];
// 普通のmap
greeting.map(x => x.split(" "));
// ["Greetings", "from"]
// ["", ""]
// ["Vietnam"]
// mapしてflat
greeting.flatMap(x => x.split(" ")) // ["Greetings", "from", "", "", "Vietnam"]
普通にmap()
を使うと、ネストした配列になります。
flatMap()
を使うことでフラットな配列にすることができます。
Object.fromEntries()
Key-value
ペアからオブジェクトに変換します。
const keyValueArray = [
['key1', 'value1'],
['key2', 'value2']
]
const obj = Object.fromEntries(keyValueArray)
// {key1: "value1", key2: "value2"}
Object.fromEntries()
は引数として配列、Map、その他の反復可能プロトコルを受け取ります。
反復可能プロトコルの詳細についてはこちらをご覧ください。
String.prototype.trimStart() / .trimEnd()
String.prototype.trimStart()
は文字列の先頭にある空白を削除し、String.prototype.trimEnd()
は文字列の末尾にある空白を削除します。
let str = " this string has a lot of whitespace ";
str.length; // 42
str = str.trimStart(); // "this string has a lot of whitespace "
str.length; // 38
str = str.trimEnd(); // "this string has a lot of whitespace"
str.length; // 35
trimStart()
のエイリアスとしてtrimLeft()
が、trimEnd()
のエイリアスとしてtrimRight()
が存在します。
Optional Catch Binding
ES2019より前は、catch句に必ず例外変数を取る必要がありました。
ES2019では省略することができます。
// Before
try {
...
} catch(error) {
...
}
// ES2019
try {
...
} catch {
...
}
エラーを無視したいときに便利です。
この機能のユースケースについてはこの記事を強くお勧めします。
Function.prototype.toString()
関数の.toString()
は、ソースコードを文字列として返します。
function sum(a, b) {
return a + b;
}
console.log(sum.toString());
// function sum(a, b) {
// return a + b;
// }
コメントも含みます。
function sum(a, b) {
// perform a sum
return a + b;
}
console.log(sum.toString());
// function sum(a, b) {
// // perform a sum
// return a + b;
// }
Symbol.prototype.description
.description
はSymbol
の値を文字列で返します。
const me = Symbol("Alberto");
me.description; // "Alberto"
me.toString() // "Symbol(Alberto)"
Download the cheatsheet
このリンクから、これらのチートシートをダウンロードすることができます。
よかったらAmazonやLeanpubで私の本を買ったり、Educativeのコースを受けてみてください。
感想
全てと言いつつ全てではありませんが、これは元々著者がThe Complete Guide to Modern JavaScriptという書籍を出していて、この記事はその抜粋だからです。
抜粋といってもわりとけっこうな分量でしたが、元の本は全300ページという更に相当な力作となっています。
最初は変数や関数といった基礎部分から順にステップアップしていく内容で、この記事で紹介されている新機能は200ページ以降に出てくる、いわばおまけ部分です。
全編英語なのでなかなかたいへんですが、読み通せば現代のJavaScript事情に詳しくなれることは間違いありません。
しかもKindleなら2000円弱と大変お買い得、これは持ってて正解ですね。