この記事は JavaScript Advent Calendar 2019 - Qiita の初日の記事です。
2019年を締めくくるアドベントカレンダーの初日ということで、今年のJavaScriptを簡単に振り返りたいと思います。2020年のJavaScriptについても予習しましょう。
2019年、2020年が何を指しているかは後述します。
カテゴリが「プログラミング言語」なので、React、Vue、AngularやNode.jsなどJavaScriptを使った技術ではなく、JavaScriptの言語機能にフォーカスしたいと思います。
はじめに
※この章はJavaScriptの初歩的な内容になっています。JavaScript機能に関する内容だけ知りたい方は読み飛ばしていただいても大丈夫です。
JavaScriptには実行環境(JavaScriptエンジン)が複数あります。
- Google Chrome内で使われているV8
- Firefoxで使われているSpiderMonkey
- Safariで使われているJavaScriptCore
- EdgeはChakraCore。Chromiumベースになると、Chromeと同じV8になると思います。
- Node.jsはV8
- denoもV8。※denoはNode.jsを開発したRyan Dahl氏が現在開発しているTypeScript/JavaScriptの実行環境です。
上記のように実行環境は様々あり、開発している方々も違います。そのため、実行環境間で差異が発生することもあります。
そのため、JavaScriptにはTC39という標準化団体のような組織がありECMAScriptの仕様を作っています。JavaScriptはこのECMAScriptのECMA-262に準拠しています。
このECMAScriptは毎年アップデートされ、ECMAScript 2019やECMAScript 2020など年がついたネーミングがされています。ES2019やES2020と省略して使われることが多いです。
ES2019は今年6月にpublishされました。
ECMAScriptの仕様に追加されるには5段階のステージングがあり、Stage 4になったものが仕様に追加されます。
- Stage 0: Strawperson(たたき台)
- Stage 1: Proposal(提案)
- Stage 2: Draft(下書き)
- Stage 3: Candidate(候補)
- Stage 4: Finished(完了)
それぞれのステージングに上がる条件など、詳しくはこちらをご確認ください。
ES2016以降のどの機能はどの実行環境で使用可能かを有志の方が一覧化してくれています。この一覧を見れば何の機能が何のバージョンの何のブラウザで使用可能か確認することができます。
今回はES2019で追加された機能やES2020に追加される機能について紹介します。
ES2019
Optional catch binding
try...catch - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 58 | 66 | 11.1 | 10 |
例外をキャッチするために書くtry-catchですが、従来では必ずcatch節に例外を識別するオブジェクトを指定しなければいけませんでしたが、必ずしも例外オブジェクトが必要ではありません。このOptional catch bindingにより指定しなくても良くなりました。
| try { | |
| someFunc() // thorws Error | |
| } catch { | |
| console.log('Error occurs') | |
| } |
JSON superset
| |
|
|
|
|
|---|---|---|---|---|
| No | 62 | 66 | Yes | 10 |
ES2019より前ではJSONはECMAScriptのサブセットではありませんでした。
ECMAScriptではu+2028は行区切り文字(LS)として扱います。
そのためeval('"\u+2028"')とするとSyntaxErrorが発生します。
文字列を出力するためには
しかし、ES2019になるまでJSON.parse('"\u+2028"')はエラーになりませんでした。
そこで挙動をあわせるために修正したのが、このJSON supersetです。
| const sourceCode = '"\u+2028"'; | |
| eval(sourceCode) // SyntaxError: Invalid Unicode escape sequence | |
| JSON.parse(sourceCode) // >=ES2019 SyntaxError: Invalid Unicode escape sequence |
Symbol.prototype.description
Symbol.prototype.description - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 63 | 70 | 12.1 | 11.0 |
Symbolの引数に指定した値を参照することができます。descriptionプロパティは読み取り専用です。
| console.log(Symbol('desc').description); | |
| // expected output: "desc" | |
| console.log(Symbol.iterator.description); | |
| // expected output: "Symbol.iterator" |
Object.fromEntries
Object.fromEntries() - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 63 | 73 | 12.1 | 12.0 |
key, valueのペアの配列をオブジェクトに変換します。
| const entries = new Map([ | |
| ['foo', 'bar'], | |
| ['baz', 42] | |
| ]); | |
| const obj = Object.fromEntries(entries); | |
| console.log(obj); | |
| // expected output: Object { foo: "bar", baz: 42 } |
Well-formed JSON.stringify
JSON.stringify() - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 64 | 72 | 12.1 | No |
不正なサロゲートペアがエスケープされた文字列として出力されるようになります。
以前までは
JSON.stringify("\uD800");
// --> '"�"'
だったのが
JSON.stringify("\uD800");
// --> '"\ud800"'
になります。
String.prototype.{trimStart,trimEnd}
Array.prototype.{flat,flatMap}
Array#flat
Array.prototype.flat() - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 62 | 69 | 12 | 11.0 |
ネストされた配列をフラットにします。以下MDNから引用です。
| var arr1 = [1, 2, [3, 4]]; | |
| arr1.flat(); | |
| // [1, 2, 3, 4] | |
| var arr2 = [1, 2, [3, 4, [5, 6]]]; | |
| arr2.flat(); | |
| // [1, 2, 3, 4, [5, 6]] | |
| var arr3 = [1, 2, [3, 4, [5, 6]]]; | |
| arr3.flat(2); | |
| // [1, 2, 3, 4, 5, 6] |
Array#flatMap
Array.prototype.flatMap() - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 62 | 69 | 12 | 11.0 |
配列の値をmap関数でマッピングし、結果の配列をフラット化します。以下MDNから引用です。
| var arr1 = [1, 2, 3, 4]; | |
| arr1.map(x => [x * 2]); | |
| // [[2], [4], [6], [8]] | |
| arr1.flatMap(x => [x * 2]); | |
| // [2, 4, 6, 8] | |
| // only one level is flattened | |
| arr1.flatMap(x => [[x * 2]]); | |
| // [[2], [4], [6], [8]] |
ES2020
ES2020はまだ仕様が確定していません。しかし、いくつかの機能は既にStage 4となっているためES2020に含まれることが確定しています。この記事では2019/12/01時点でStage 4になっているES2020の機能を紹介します。
String.prototype.matchAll
String.prototype.matchAll() - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 63 | 73 | No | 12.0 |
String.prototype.matchAllは指定した正規表現のキャプチャに該当するものを文字列の配列として取得できる機能です。
また、matchAllの戻り値はiteratorオブジェクトです。
そのため、値の取り出しにはfor-of構文で取り出したりnext()関数を使ったりArrayに一度変換する必要があります。
JavaScriptのiteratorに関して詳細はこちらを御覧ください。
| let regexp = /t(e)(st(\d?))/g; | |
| let str = 'test1test2'; | |
| str.match(regexp); | |
| // Array ['test1', 'test2'] | |
| let array = [...str.matchAll(regexp)]; | |
| console.log(array[0]); | |
| // Array ["test1", "e", "st1", "1"] | |
| console.log(array[1]); | |
| // Array ["test2", "e", "st2", "2"] | |
| for (const arr of str.matchAll(regexp)) { | |
| console.log(arr) | |
| } | |
| // Array ["test1", "e", "st1", "1"] | |
| // Array ["test2", "e", "st2", "2"] |
import()
| |
|
|
|
|
|---|---|---|---|---|
| No | 67 | 63 | 11.1 | Experimental |
import()はDynamic importと呼ばれるモジュールを動的に読み込む関数です。
非同期で実行されPromiseでラップされたモジュールが返ってきます。
使い所としては処理によって読み込むモジュールを変更したり、非同期でモジュールを読み込む場面でしょう。
Node.jsでは--experimental-modulesというフラグを付けて実行する必要があります。。ただし、13.2からはこのフラグは不要です。
| export default () => { | |
| console.log("default export") | |
| } | |
| export function printArg(arg) { | |
| console.log(arg) | |
| } | |
| export function throwError() { | |
| throw Error("error!"); | |
| } |
| export default () => { | |
| console.log("default export2") | |
| } | |
| export function printArg(arg) { | |
| console.log(arg, '2') | |
| } | |
| export function throwError() { | |
| throw Error("error2!"); | |
| } |
| let importModule; | |
| if (someBool) { | |
| importModule = import('./export.js'); | |
| } else { | |
| importModule = import('./export2.js'); | |
| } | |
| importModule.then(module => { | |
| module.default(); | |
| module.printArg('Hello'); | |
| module.throwError(); | |
| }).catch(e => { | |
| console.error(e) | |
| }); |
BigInt
| |
|
|
|
|
|---|---|---|---|---|
| No | 68 | 67 | No | 10.4 |
JavaScriptのNumber型は53bitまでしか扱えません。そのため2 ** 53 = 9007199254740992が限界値です。これ以上足しても9007199254740992になります。
9007199254740992を超える数値を扱うためにBigIntが提案されました。構文は1nのように数値のあとにnを付けます。
またBigIntはBigIntとしか演算ができません。1n + 1はTypeErrorになります。
BigIntは任意精度の整数です。任意精度な浮動小数点数についてはStage 0のdecimalとして提案されようとしています。
| console.log(2 ** 53) | |
| // 9007199254740992 | |
| console.log(2 ** 53 + 1) | |
| // 9007199254740992 | |
| console.log(2n ** 53n + 1n) | |
| // 9007199254740993n | |
| console.log(2n ** 53n * 1000n) | |
| // 9007199254740992000n |
Promise.allSettled
Promise.allSettled() - JavaScript | MDN
| |
|
|
|
|
|---|---|---|---|---|
| No | 71 | 76 | 13 | 12.9 |
Promise.allSettledは複数のPromiseな非同期の処理を並列に実行する関数です。すべて完了した状態になったときに、Promiseの結果をオブジェクトの配列を返します。
引数にはPromiseオブジェクトの配列を渡します。
戻り値はiterableなオブジェクトです。
| const promise1 = Promise.resolve('foo'); | |
| const promise2 = Promise.reject('bar') | |
| const promises = [promise1, promise2]; | |
| Promise.allSettled(promises). | |
| then((results) => { | |
| for (const result of results) { | |
| if (result.status === 'fulfilled') console.log(result.value, 'is fulfilled'); | |
| if (result.status === 'rejected') console.log(result.reason, 'is rejected'); | |
| } | |
| console.log(JSON.stringify([...results])); | |
| }); | |
| // "foo is fulfilled" | |
| // "bar is rejected" | |
| // "[{"status":"fulfilled","value":"foo"},{"status":"rejected","reason":"bar"}]" |
従来からPromise.allというものがありました。これも同様に複数のPromiseを並列実行します。すべての処理が成功しresolveされれば良いのですが、どれか一つでも失敗してrejectされたときにどこまで実行されたのかはわかりません。
そこで提案されたのがPromise.allSettledです。とにかくすべて完了させて、成功/失敗かは完了後に判定できるようにオブジェクトに内包して返してあげようというものです。
このためreject(失敗)したものがどのPromiseか判定でき、必要に応じてリトライなどの処理が行えます。
globalThis
| |
|
|
|
|
|---|---|---|---|---|
| No | 65 | 71 | 12.1 | 12.0 |
globalThisは環境を超えてグローバルオブジェクトにアクセスするためのオブジェクトです。
これまでブラウザではwindow、Node.jsではglobalがグローバルオブジェクトとして使われていました。
もちろんこれらは今まで通り各環境で使えますが、どちらでもアクセス可能なオブジェクトとしてglobalThisが追加されました。
globalThisを使うことで以下のような使い分けをする必要がなくなりました。
| const getGlobal = () => { | |
| if (typeof self !== 'undefined') { return self; } | |
| if (typeof window !== 'undefined') { return window; } | |
| if (typeof global !== 'undefined') { return global; } | |
| throw new Error('unable to locate global object'); | |
| }; | |
| const globals = getGlobal(); |
最後に
こう見ると2019年もJavaScriptに様々な機能が追加されました。2020年も様々な機能が追加されると思います。
現在のECMA-262のmasterブランチはES2020がターゲットになっており、https://tc39.es/ecma262/ は更新されています。
もし気になる機能でES2020として追加が確定になるかはECMA-262かproposalのFinishedを見ると良いかと思います。
他にも様々な機能が提案されているので、興味があればtc39/proposalsのリポジトリを御覧ください。
また、TC39は定期的にミーティングを行っており仕様の提案について話し合っています。アジェンダやミーティングノートが公開されているので興味があれば御覧ください。
以上、最後までお読みいただきありがとうございました。不備や質問はTwitter - @shisama_やコメントで受け付けています。気兼ねなくメンションください。