別にしんどくないブログ

技術のことや読書メモを書いています

2019年、2020年のJavaScript

この記事は JavaScript Advent Calendar 2019 - Qiita の初日の記事です。

2019年を締めくくるアドベントカレンダーの初日ということで、今年のJavaScriptを簡単に振り返りたいと思います。2020年のJavaScriptについても予習しましょう。
2019年、2020年が何を指しているかは後述します。
カテゴリが「プログラミング言語」なので、React、Vue、AngularやNode.jsなどJavaScriptを使った技術ではなく、JavaScriptの言語機能にフォーカスしたいと思います。

はじめに

※この章はJavaScriptの初歩的な内容になっています。JavaScript機能に関する内容だけ知りたい方は読み飛ばしていただいても大丈夫です。

JavaScriptには実行環境(JavaScriptエンジン)が複数あります。

上記のように実行環境は様々あり、開発している方々も違います。そのため、実行環境間で差異が発生することもあります。
そのため、JavaScriptにはTC39という標準化団体のような組織がありECMAScriptの仕様を作っています。JavaScriptはこのECMAScriptECMA-262に準拠しています。

このECMAScriptは毎年アップデートされ、ECMAScript 2019やECMAScript 2020など年がついたネーミングがされています。ES2019やES2020と省略して使われることが多いです。
ES2019は今年6月にpublishされました。

f:id:Shisama:20191128041317p:plain
ES2019

ECMAScriptの仕様に追加されるには5段階のステージングがあり、Stage 4になったものが仕様に追加されます。

それぞれのステージングに上がる条件など、詳しくはこちらをご確認ください。

ES2016以降のどの機能はどの実行環境で使用可能かを有志の方が一覧化してくれています。この一覧を見れば何の機能が何のバージョンの何のブラウザで使用可能か確認することができます。

kangax.github.io

今回はES2019で追加された機能やES2020に追加される機能について紹介します。

ES2019

Optional catch binding

try...catch - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
No 58 66 11.1 10

例外をキャッチするために書くtry-catchですが、従来では必ずcatch節に例外を識別するオブジェクトを指定しなければいけませんでしたが、必ずしも例外オブジェクトが必要ではありません。このOptional catch bindingにより指定しなくても良くなりました。

try {
someFunc() // thorws Error
} catch {
console.log('Error occurs')
}
view raw optional_catch.js hosted with ❤ by GitHub
gist.github.com

JSON superset

JSON - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
No 62 66 Yes 10

ES2019より前ではJSONECMAScriptのサブセットではありませんでした。
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
view raw JSON_superset.js hosted with ❤ by GitHub
gist.github.com

Symbol.prototype.description

Symbol.prototype.description - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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]
view raw array_flat.js hosted with ❤ by GitHub

Array#flatMap

Array.prototype.flatMap() - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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]]
view raw array_flatMap.js hosted with ❤ by GitHub

ES2020

ES2020はまだ仕様が確定していません。しかし、いくつかの機能は既にStage 4となっているためES2020に含まれることが確定しています。この記事では2019/12/01時点でStage 4になっているES2020の機能を紹介します。

String.prototype.matchAll

String.prototype.matchAll() - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
No 63 73 No 12.0

String.prototype.matchAllは指定した正規表現のキャプチャに該当するものを文字列の配列として取得できる機能です。
また、matchAllの戻り値はiteratorオブジェクトです。
そのため、値の取り出しにはfor-of構文で取り出したりnext()関数を使ったりArrayに一度変換する必要があります。 JavaScriptiteratorに関して詳細はこちらを御覧ください。

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"]
view raw string_matchAll.js hosted with ❤ by GitHub
gist.github.com

import()

import - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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!");
}
view raw export.js hosted with ❤ by GitHub
export default () => {
console.log("default export2")
}
export function printArg(arg) {
console.log(arg, '2')
}
export function throwError() {
throw Error("error2!");
}
view raw export2.js hosted with ❤ by GitHub
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)
});
view raw import.js hosted with ❤ by GitHub
gist.github.com

BigInt

BigInt - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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
view raw BigInt.js hosted with ❤ by GitHub
gist.github.com

Promise.allSettled

Promise.allSettled() - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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"}]"
gist.github.com

従来からPromise.allというものがありました。これも同様に複数のPromiseを並列実行します。すべての処理が成功しresolveされれば良いのですが、どれか一つでも失敗してrejectされたときにどこまで実行されたのかはわかりません。
そこで提案されたのがPromise.allSettledです。とにかくすべて完了させて、成功/失敗かは完了後に判定できるようにオブジェクトに内包して返してあげようというものです。
このためreject(失敗)したものがどのPromiseか判定でき、必要に応じてリトライなどの処理が行えます。

globalThis

globalThis - JavaScript | MDN

Edge Edge Firefox Firefox Chrome Chrome Safari Safari f:id:Shisama:20191128111950p:plain Node.js
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();
view raw globalThis-shim.js hosted with ❤ by GitHub

最後に

こう見ると2019年もJavaScriptに様々な機能が追加されました。2020年も様々な機能が追加されると思います。
現在のECMA-262のmasterブランチはES2020がターゲットになっており、https://tc39.es/ecma262/ は更新されています。
もし気になる機能でES2020として追加が確定になるかはECMA-262proposalのFinishedを見ると良いかと思います。
他にも様々な機能が提案されているので、興味があればtc39/proposalsのリポジトリを御覧ください。 また、TC39は定期的にミーティングを行っており仕様の提案について話し合っています。アジェンダミーティングノートが公開されているので興味があれば御覧ください。

以上、最後までお読みいただきありがとうございました。不備や質問はTwitter - @shisama_やコメントで受け付けています。気兼ねなくメンションください。