以下は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を返します。
letarray=[1,2,4,5];array.includes(2);// truearray.includes(3);// false
Combine includes() with fromIndex
検索を開始するインデックスをincludes()
に渡すことができます。
デフォルトは0で、負の値を渡すこともできます。
最初の値は検索する要素で、2番目の値は開始インデックスです。
letarray=[1,3,5,7,9,11];array.includes(3,1);// true 1番目つまり最初から検索開始するarray.includes(5,4);// falsearray.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);// 4Math.pow(2,3);// 8
指数演算子の導入により、以下のように書けるようになりました。
2**2;// 42**3;// 8
複数の演算を組み合わせる際に特に役立ちます。
2**2**2;// 16Math.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が使えます。
conststrings=["short","medium length","very long string"];constlongestString=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を作成します。
constfamily={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です。
constmyObj={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
これは、本当に小さな変更です。
しかしこれで、そのパラメータが最後であるかどうかをいちいち気にせずに末尾カンマを書けるようになりました。
// 昔constobject={prop1:"prop",prop2:"propop"}// 現在constobject={prop1:"prop",prop2:"propop",}
2番目のプロパティは最後にカンマが増えていることに注目してください。
入れなくてもエラーにはなりませんが、同僚やチームメイトの生活を楽にするためにも入れておくことをお勧めします。
// カンマ入れないconstobject={prop1:"prop",prop2:"propop"}// 同僚が最終行をコピペしてプロパティを追加したconstobject={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作成constbuffer=newSharedArrayBuffer(16);constuint8=newUint8Array(buffer);// これに各計算するuint8[0]=10;console.log(Atomics.add(uint8,0,5));// 10console.log(uint8[0])// 15console.log(Atomics.load(uint8,0));// 15
最初のAtomics.add()
は加算を行いますが、返り値は計算する前の値です。
その後uint8[0]
を参照すると、addが実行されたあとなので値が15になっていることが確認できます。
配列値をAtomicに取得するにはAtomics.load()
を使い、第一引数が対象の配列、第二引数がインデックスです。
Atomics.sub()
はAtomics.add()
と同じ挙動で、減算を行います。
// SharedArrayBuffer作成constbuffer=newSharedArrayBuffer(16);constuint8=newUint8Array(buffer);// これに各計算するuint8[0]=10;console.log(Atomics.sub(uint8,0,5));// 10console.log(uint8[0])// 5console.log(Atomics.store(uint8,0,3));// 3console.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で返すreturnres.json();}).then(res=>{// 全てが正常に動作したらここに来るconsole.log(res);}).catch(err=>{// エラーがあったらここに来るconsole.log(err);})
GitHubからユーザを取得してコンソールに出力するだけの簡単な例です。
また別の例を見てみましょう。
functionwalk(amount){returnnewPromise((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);returnwalk(500);}).then(res=>{console.log(res);returnwalk(700);}).then(res=>{console.log(res);returnwalk(800);}).then(res=>{console.log(res);returnwalk(100);}).then(res=>{console.log(res);returnwalk(400);}).then(res=>{console.log(res);returnwalk(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
で書き換えるとこうなります。
functionwalk(amount){returnnewPromise((resolve,reject)=>{if(amount<500){reject("the value is too small");}setTimeout(()=>resolve(`you walked for ${amount}ms`),amount);});}// asyncなfunctionを作成asyncfunctiongo(){// awaitがあれば終わるまで待つconstres=awaitwalk(500);console.log(res);constres2=awaitwalk(900);console.log(res2);constres3=awaitwalk(600);console.log(res3);constres4=awaitwalk(700);console.log(res4);constres5=awaitwalk(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ではない関数functionfunc(){letpromise=Promise.resolve(1);letresult=awaitpromise;}func();// SyntaxError: await is only valid in async functions and async generators// トップレベルletresponse=Promise.resolve("hi");letresult=awaitresponse;// SyntaxError: await is only valid in async functions and async generators
復習:await
は、async
関数内でのみ使用可能。
Error handling
Promiseではエラーを.catch()
でキャッチします。
これについては特に違いはありません。
asyncfunctionasyncFunc(){try{letresponse=awaitfetch('http:your-url');}catch(err){console.log(err);}}asyncFunc();// TypeError: failed to fetch
関数内でエラーハンドリングしていない場合は、以下のように書くこともできます。
asyncfunctionasyncFunc(){letresponse=awaitfetch('http:your-url');}asyncFunc();// Uncaught (in promise) TypeError: Failed to fetchasyncFunc().catch(console.log);// TypeError: Failed to fetch
ES2018 Async Iteration and more?
ES2018で導入された機能も見ていきましょう。
Rest / Spread for Objects
ES6 (ES2015) でスプレッド構文が導入されたことを覚えていますか?
constveggie=["tomato","cucumber","beans"];constmeat=["pork","beef","chicken"];constmenu=[...veggie,"pasta",...meat];console.log(menu);// Array [ "tomato", "cucumber", "beans", "pasta", "pork", "beef", "chicken" ]
スプレッド構文にRestパラメータがオブジェクトに対しても使用可能になりました。
letmyObj={a:1,b:3,c:5,d:8,}// zは残り全部let{a,b,...z}=myObj;console.log(a);// 1console.log(b);// 3console.log(z);// {c: 5, d: 8}// スプレッド構文でクローンletclone={...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にすることができます。
constiterables=[1,2,3];asyncfunctiontest(){forawait(constvalueofiterables){console.log(value);}}test();// 1// 2// 3
ループの実行中、[Symbol.asyncIterator]()
を用いてデータソースから非同期イテレータを作成します。
次のループにアクセスするたびに、返ってきたPromiseを暗黙的にawaitします。
Promise.prototype.finally()
Promiseが終了したときに呼び出されます。
constmyPromise=newPromise((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ではなく元の値です。
constmyPromise=newPromise((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プロパティからアクセスすることができます。
以下の例のように、名前付きキャプチャグループと同時に、番号付きの参照も作成されます。
letre=/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;letresult=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
フラグの指定された正規表現で使用可能な新しいタイプのエスケープシーケンスです。
これを使うと、以下のような記述が可能になります。
constregexGreekSymbol=/\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
を指定すると、無制限にネストを解除します。
constletters=['a','b',['c','d',['e','f']]];// デフォルトは1letters.flat();// ['a', 'b', 'c', 'd', ['e', 'f']]// 2段階letters.flat(2);// ['a', 'b', 'c', 'd', 'e', 'f']// 2段 = 1段 * 2letters.flat().flat();// ['a', 'b', 'c', 'd', 'e', 'f']// ネストがなくなるまで再帰的にフラット化letters.flat(Infinity)// ['a', 'b', 'c', 'd', 'e', 'f']
Array.prototype.flatMap()
は、引数の取り扱いはflat()
と同じです。
配列を単純にフラット化するのではなく、関数を渡して任意の処理を行うことができます。
letgreeting=["Greetings from","","Vietnam"];// 普通のmapgreeting.map(x=>x.split(""));// ["Greetings", "from"]// ["", ""]// ["Vietnam"]// mapしてflatgreeting.flatMap(x=>x.split(""))// ["Greetings", "from", "", "", "Vietnam"]
普通にmap()
を使うと、ネストした配列になります。flatMap()
を使うことでフラットな配列にすることができます。
Object.fromEntries()
Key-value
ペアからオブジェクトに変換します。
constkeyValueArray=[['key1','value1'],['key2','value2']]constobj=Object.fromEntries(keyValueArray)// {key1: "value1", key2: "value2"}
Object.fromEntries()
は引数として配列、Map、その他の反復可能プロトコルを受け取ります。
反復可能プロトコルの詳細についてはこちらをご覧ください。
String.prototype.trimStart() / .trimEnd()
String.prototype.trimStart()
は文字列の先頭にある空白を削除し、String.prototype.trimEnd()
は文字列の末尾にある空白を削除します。
letstr=" this string has a lot of whitespace ";str.length;// 42str=str.trimStart();// "this string has a lot of whitespace "str.length;// 38str=str.trimEnd();// "this string has a lot of whitespace"str.length;// 35
trimStart()
のエイリアスとしてtrimLeft()
が、trimEnd()
のエイリアスとしてtrimRight()
が存在します。
Optional Catch Binding
ES2019より前は、catch句に必ず例外変数を取る必要がありました。
ES2019では省略することができます。
// Beforetry{...}catch(error){...}// ES2019try{...}catch{...}
エラーを無視したいときに便利です。
この機能のユースケースについてはこの記事を強くお勧めします。
Function.prototype.toString()
関数の.toString()
は、ソースコードを文字列として返します。
functionsum(a,b){returna+b;}console.log(sum.toString());// function sum(a, b) {// return a + b;// }
コメントも含みます。
functionsum(a,b){// perform a sumreturna+b;}console.log(sum.toString());// function sum(a, b) {// // perform a sum// return a + b;// }
Symbol.prototype.description
.description
はSymbol
の値を文字列で返します。
constme=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円弱と大変お買い得、これは持ってて正解ですね。