Everything you need to know from ES2016 to ES2019 日本語訳


原文はこちら:
https://inspiredwebdev.com/everything-from-es-2016-to-es-2019

こんにちは, shuzonです。

上記のES2016-2019のまとめ記事は非常に良い記事でした。
英語だから気持ちが乗らず読まない 人がいると勿体無いなーとふと思い、

日本語で気軽に広く読んでもらいたいなーと思い日本語訳をしてみました。

訳者のステータス

  • js歴は2年ちょっとくらい
    • jquery, vue, reactを少し
    • 最新のjsをちゃんと触り始めたのはここ3ヶ月

誤訳や解釈の誤りがあった場合はコメントにて指摘してください。

原文ママの方が伝わりやすいと判断した部分に関しては原文を採用します。

ちょっとしたメモ

本文中に僕のメモを混ぜ込んでいます。

本文と切り離すために以下のprefixと共に記述します。

  • FYI: for your information
  • IMO: in my opinion
  • MEMO: 一言メモ

では本文に入ります。

Everything you need to know from ES2016 to ES2019

JavaScriptは常に進化し続ける言語の1つであり、ここ数年で数多くの特徴がECMAScriptの仕様に追加された。

この記事は Complete Guide to Modern JavaScript から新規に追加されたES2016, ES2017, ES2018, ES2019に関する部分を抜粋する。

この記事の終わりで、全てを要約したチャートシートのlinkを見つけることができるだろう。

FYI: みながら読むとわかりやすいのでここに貼っておきます。
cheat sheet

Everything new in ES2016

ES2016に導入されたのはたった2つの特徴だ。

  • Array.prototype.includes()
  • The exponential operator (べき乗演算子)

Array.prototype.includes()

includes()

  • arrayが要素を含めば true
  • それ以外はfalse

を返す

let array = [1,2,4,5];

array.includes(2);
// true
array.includes(3);
// false

indexとの組み合わせ

includes() はあたえられたindexから検索を開始できる。
デフォルトは0で、負の数を与えることもできる。

第1引数は検索対象で, 第2引数はindexだ。

let array = [1,3,5,7,9,11];

array.includes(3,1);
// index 1(つまり1の位置)から値3を探す
// true
array.includes(5,4);
//false
array.includes(1,-1);
// 末尾スタートで1を探す
// false
array.includes(11,-3);
// true

array.includes(5,4); はarrayに5を含むにも関わらずfalseを返す。
なぜなら検索が4要素目から始まるからだ。 5を見つけられないため false が返る。

array.includes(1,-1); はfalseを返す。
index -1はarrayの最後尾を表し、検索がそれ以降から始まるからだ。

array.includes(11,-3); はtrueを返す。
index -3(つまり7) に戻ったとしても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はcoolな機能がたくさん増えた、ではみていこう。

String padding (.padStart() and .padEnd())

stringをpadding(うめる)機能がいくつか増えた。

  • 末尾からpaddingする .padEnd()
  • 先頭からpaddingする .padStart()
"hello".padStart(6);
// " hello"
"hello".padEnd(6);
// "hello "

6文字でpaddingすると指定したのに、なんで1つのspaceしか与えられないのか?

padStartpadEnd は空を埋める(fill the empty spaces)からだ。

helloは5文字だから空なのは1文字だけだ。

以下の例をみてみて欲しい。

"hi".padStart(10);
// 10 - 2 = 8 empty spaces
// "        hi"
"welcome".padStart(10);
// 10 - 6 = 4 empty spaces
// "   welcome"

Right align with padStart (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 (カスタム文字列で埋める)

実はspaceだけでなく、文字列や数字も利用できる。

"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",
}

前のversionのJavaScript では Objectの値にアクセスするときこんな方法を取っていた

Object.keys(family);
// ["father", "mother", "son"]
family.father;
"Jonathan Kent"

Object.keys() はObjectのkeysだけを返し、valuesにアクセスするためにはそのkeyを使う必要があった。

今は2つのアクセス方法が増えた。

Object.values(family);
// ["Jonathan Kent", "Martha Kent", "Clark Kent"]

Object.entries(family);
// ["father", "Jonathan Kent"]
// ["mother", "Martha Kent"]
// ["son", "Clark Kent"]

Object.values() は 全てのvalueを返し、 Object.entries() は keyとvalue両方を含むarrayを返す。

Object.getOwnPropertyDescriptors()

このmethodは 地震に関する全てのプロパティ情報
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 (ケツカンマの用法)

これはちょっとした文法のマイナーチェンジだ。

objectを書くとき、要素が最後かどうかに関わらずカンマをつけることができるようになった。
FYI: つまりケツカンマ可能ってこと

// from this
const object = {
  prop1: "prop",
  prop2: "propop"
}

// to this
const object = {
  prop1: "prop",
  prop2: "propop",
}

例えば、僕が最後(2つめ)の要素にカンマを書き忘れたとしよう。

これは今は特にerrorが起きないけど、君の同僚やチームメイトの命を守るならつける方がベターだね。

IMO: これはlintで制御すればいいし人間がやるべきことではなさそう

// 僕が書いたやつ
const object = {
  prop1: "prop",
  prop2: "propop"
}

// 同僚が新しいプロパティを追加した
const object = {
  prop1: "prop",
  prop2: "propop"
  prop3: "propopop"
}
// 突然, 彼はerrorに見舞われる。なぜなら僕が最後にカンマをつけ忘れたことに気づけないからだ

Shared memory and Atomics

MEMO:
ここは理解度低いです。
どうもAtomicsはthread間でshared memoryを利用するためのAPIのよう。
いい感じに原子性(Atomicity)を担保してくれるAPI、という理解でとどまっています。
正直、原文もMDNをそのままコピペしてるっぽいのでMDN日本語訳を読む方が良さそうです。

From : MDN

// MEMO: MDNから抜粋
メモリーが共有されている場合、複数のスレッドがメモリー内の同じデータを読み書きできます。アトミック演算では、予測される値の書き込みと読み込みを保証するため、次の演算が開始される前に現在の演算が完了し、その演算が割り込まれないようにします。
アトミック演算は、Atomics モジュール上にインストールされます。他のグローバルオブジェクトと異なり、Atomics はコンストラクターではありません。new 演算子 を付けて使用することや Atomics オブジェクトを関数として実行することはできません。Atomics のすべてのプロパティとメソッドは静的です (例えば、Math オブジェクトの場合と同じです)。

例えばこんなmethodがある。
- add/sub
- and/ or / xor
- load / store

MEMO: 以下はMDN SharedArrayBuffer から抜粋

SharedArrayBuffer オブジェクトは、ジェネリックで固定長の生バイナリデータバッファーを表すために使用されます。

いくつか Atomics methodのサンプルをみてみよう。

Atomics.add(), Atomics.sub(), Atomics.load() and Atomics.store()

Atomics.add()array, index, value と3つの引数を取る。
加算とともにindexの位置に以前設定されていた値をreturnする。

// create a `SharedArrayBuffer`
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);
// add a value at the first position
uint8[0] = 10;
console.log(Atomics.add(uint8, 0, 5));
// 10
// 10 + 5 = 15
console.log(uint8[0])
// 15
console.log(Atomics.load(uint8,0));
// 15

ご覧の通り, Atomics.add() を呼び出すとarrayの指定位置に設定されていた前の値を返す。
この時, 再度 uint8[0] を呼び出すと加算結果の15が得られる。
array, indexの2つの引数をとる Atomics.load を使えば特定の値を取り出すことができる。
Atomics.sub()Atomics.add() と同じように動くが減算を行う。

// create a `SharedArrayBuffer`
const buffer = new SharedArrayBuffer(16);
const uint8 = new Uint8Array(buffer);
// add a value at the first position
uint8[0] = 10;
console.log(Atomics.sub(uint8, 0, 5));
// 10

// 10 - 5 = 5
console.log(uint8[0])
// 5
console.log(Atomics.store(uint8,0,3));
// 3
console.log(Atomics.load(uint8,0));
// 3

ここでは Atomics.sub() を使い uint8[0] に設定された値から5を減算する(つまり10-5と同じ)。
Atomics.add() と同じく前回設定された値が返る、この場合は10だ。

Atomics.store() を使うことで特定の値をstore(登録)できる。
この場合では, 値3をposition 0 つまりarrayの先頭に設定する。
Atomics.store() は 設定値をそのまま返す、この場合は3だ。
Atomics.load() も指定したindexの値を返す、この場合は3であり5はかえってこない。

Atomics.and(), Atomics.or() and Atomics.xor()

この3つのmethodは bit単位のAND, OR, XOR演算子(bitwise AND, OR and XOR operations)として与えられたarrayの位置に作用する。

もっとbitwise operationsについて知りたい人は以下のwikipediaをみて欲しい。
https://en.wikipedia.org/wiki/Bitwise_operation

ES2017 Async and Await

ES2017では promisesの新しい用法が導入された。その名も"async/await"だ。

Promise review

新しいsyntaxに飛び込む前に、ちょっとだけ書き慣れたpromiseの書き方をみてみよう。

// fetch a user from github
fetch('api.github.com/user/AlbertoMontalesi').then( res => {
  // return the data in json format
  return res.json();
}).then(res => {
  // if everything went well, print the data
  console.log(res);
}).catch( err => {
  // or print the error
  console.log(err);
})

これはgithubからuserを取得してconsoleに表示するとてもシンプルなpromiseだ。
違う例もみてみよう。

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

このpromiseを新しいasync/await syntaxで書き直してみよう。

Async and 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);
  });
}

// create an async function
async function go() {
  // use the keyword `await` to wait for the response
  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 functionを作る。functionの前に async キーワードをおく必要がある。
  • async キーワードによって関数は常にpromiseを返すことをJavaScriptに伝えることができる。
  • もし promiseでない値を関数内で返したとしても, async functionが値をpromiseでラップ(wrapped) してくれる。
  • await キーワードは async function内で のみ 動作する
  • 名前が示すように、 awaitはpromiseの結果が返るまで待つことをJavaScriptに伝える。

もし、awaitasync functionの外で使った時どうなるか試してみよう。


// 普通の関数内でawaitを使う
function func() {
  let promise = Promise.resolve(1);
  let result = await promise;
}
func();
// SyntaxError: await is only valid in async functions and async generators


// codeのtop-levelでawaitを使う
let response = Promise.resolve("hi");
let result = await response;
// SyntaxError: await is only valid in async functions and async generators

注意書き: awaitはasync function内でしか使えない

Error handling

通常のpromiseではerrorを捕捉する場合 .catchを使う。
asyncもそんなに変わらない。

async function asyncFunc() {

  try {
    let response = await fetch('http:your-url');
    } catch(err) {
        console.log(err);
      }
}

asyncFunc();
// TypeError: failed to fetch

try...catch を使えerrorを捕捉できるが、それがなくてもこんな感じでerrorの捕捉ができる。

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/spread syntax によって objectに対しても行えるようになった。
どうやるのかみてみよう。

let myObj = {
  a:1,
  b:3,
  c:5,
  d:8,
}

// rest演算子を使えばobject内に余った全部の要素を捕捉できる
let { a, b, ...z } = myObj;
console.log(a);     // 1
console.log(b);     // 3
console.log(z);     // {c: 5, d: 8}

// spread sytaxで Objectをクローンできる
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}

spread演算子によってObjectのクローンが簡単に作れるようになった。

そしてoriginalのObjectを変更したとしても、クローンされたObjectは変更されない。

arrayで見られる挙動とよく似ている。

Asynchronous Iteration

Asynchronous Iteration(非同期イテレーション) によって非同期でデータをイテレーションすることができるようになった。

documentから引用する

async iteratorはnext() methodが{value, done}のペアで構成されるpromiseを返すことを除いて、iteratorにとてもよく似ている、

どうするかというと for-await-of ループを使えばいい。
イテレーションが1つでない限りはイテレート対象をPromiseに変換することにより動作する。

const iterables = [1,2,3];

async function test() {
    for await (const value of iterables) {
        console.log(value);
    }
}
test();
// 1
// 2
// 3

実行中、async iterator は [Symbol.asyncIterator]() method によってデータソースが作られる。
シーケンス中に次の値にアクセスしようとする時、暗黙的にiterator methodがpromiseを返すまで待つ。

Promise.prototype.finally()

promiseが終了した後もcallbackの呼び出しが可能だ。

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を返し、thencatch でチェーン(chain)することができる。
ただし、それらのPromiseはチェーンされたPromiseに基づいて実行される。

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
// Done!
// still working

finally の後に then をチェーンすることができるが、返される値は finally によって作られたものではなく最初の then によるものとなる。

RegExp features

新しい正規表現機能がECMAScriptに追加された。

  • s(dotAll) flag for regular expressions (sフラグ)
  • RegExp named capture groups (名前付きキャプチャグループ)
  • RegExp Lookbehind Assertions (後置アサーション)
  • RegExp Unicode Property Escapes (ユニコードエスケープ)

s (dotAll) flag for regular expression (sフラグ)

新しいs flagは改行を含む全ての文字にマッチする . を利用可能にする。

/foo.bar/s.test('foo\nbar');
// true

RegExp named capture groups (名前付きキャプチャグループ)

documentより

数字付きキャプチャグループ(Numbered capture groups) は正規表現にマッチした特定位置の文字列を参照する。

それぞれのキャプチャグループには一意の数字が振り分けられ、参照時にはその数字を利用できる。

しかしこれは正規表現の把握やリファクタリングを難しくさせる。

例えば /(\d{4})-(\d{2})-(\d{2})/ にマッチする日付について、どのグループが月や日に対応するのか周辺のコード無しでは理解できない。

また、月と日の順序が変わった時はグループの参照を更新しなければらない。

(?<name>...) syntaxを用いることでキャプチャグループに名前を与え識別することが可能になる。

先ほどの正規表現はこう書き換えることができる /(?<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 (後置アサーション)

documentより

Lookbehind Assertions (後置アサーション) はパターンの前に別のパターンがあるかどうかを確認する。

例えば, ドルの金額を$記号なしでマッチしたい。

肯定マッチ(positive lookbehind assertion) では (?<=...) で示されたパターンが次のパターンよりも前に存在している場合にマッチする。

例えば、ドルの金額を $ 記号なしでマッチしたい時は /(?<=$)\d+(\.\d*)?/ を使えばできる。
$10.53 の場合は 10.53 を返すが、€10.53 にはマッチしない。

否定マッチ(Negative lookbehind assertions) では (?<!...) で示されたパターンが次のパターンの前に存在しない場合にマッチする。
例えば、 /(?<!$)\d+(?:\.\d*)/$10.53 にはマッチしないが €10.53 にはマッチする。

RegExp Unicode Property Escapes (ユニコードエスケープ)

documentより

ユニコードエスケープは \p{…}\P{…} を使えばいい。
新しいタイプのエスケープシーケンスで u フラグを設定することで使える。

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π');
// true

Lifting template literals restriction

memo: ここは省略しますね

What's new in ES2019?

最新のES2019について紹介しよう。

Array.prototype.flat() / Array.prototype.flatMap()

Array.prototype.flat() は指定した深さまで再帰的にarrayを平坦化(flatten)する。
特に何も指定しなければ階層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階層ずつflatten
letters.flat().flat();
// ['a', 'b', 'c', 'd', 'e', 'f']

// 全階層flatten
letters.flat(Infinity)
// ['a', 'b', 'c', 'd', 'e', 'f']

Array.prototype.flatMap() は階層の扱いに関しては flat() と同じである。
しかし、単にarrayを平坦化する代わりに、flatMap() はマッピングを行い結果をもった新しいarrayを返す。

let greeting = ["Greetings from", " ", "Vietnam"];

// 普通のmap()をみてみよう
greeting.map(x => x.split(" "));
// ["Greetings", "from"]
// ["", ""]
// ["Vietnam"]


greeting.flatMap(x => x.split(" "))
// ["Greetings", "from", "", "", "Vietnam"]

.map() は複数階層になってしまっているが、 解決方法として.flatMap() を使うことで平坦化されたarrayを得ることができる。

MEMO: flatten 2019で導入されたのまじ!?遅くない!?と読んでいて思いました。

Object.fromEntries()

Object.fromEntries() は key-value pairのlistをobjectに変換する

const keyValueArray = [
  ['key1', 'value1'],
  ['key2', 'value2']
]

const obj = Object.fromEntries(keyValueArray)
// {key1: "value1", key2: "value2"}

Object.fromEntries() はイテレータなものは引数にとることができる。ArrayやMap, iterable protocolを実装しているObjectであれば可能だ。

もっと知りたい人はここを読もう。

MDN iteration protocols 日本語版

FYI: Object.entries() の逆バージョンですね

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

.trimLeft().trimStart()のalias,
.trimRight().trimEnd(). のaliasだ。

Optional Catch Binding

ES2019以前は常にcatch 節に 例外変数をとる必要があった。
ES2019は省略可能だ。

// Before
try {
   ...
} catch(error) {
   ...
}

// ES2019
try {
   ...
} catch {
   ...
}

これはerrorを無視したいときに便利だ。(memo: いや...それどうなの...?)

もっと詳しく知りたいならこれを読んでほしい http://2ality.com/2017/08/optional-catch-binding.html

Function.prototype.toString()

.toString() method は function のコードを文字列として出力してくれる。

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

.descriptionSymbol Objectのoptionalな説明を出してくれる。

const me = Symbol("Alberto");
me.description;
// "Alberto"

me.toString()
//  "Symbol(Alberto)"

以上です

javascriptがここ数年で着実に進化してきたことがわかるとてもいい記事ですね。
読んでくださってありがとうございます。