ECMAScript 2018(ES2018)の新機能まとめ

[2018-01-30] Unicodeプロパティ関連で、オンラインのデータベースについて情報提供をいただいたので、追記しました。


Stage 4に到達したproposalをまとめました。今回は正規表現関連の追加が多いようです。

全体的にざっくりなまとめですので、詳細は各proposalや、ES2018のドラフトを参照してください。

今回の新機能は既にChrome 64が対応済みなので、現時点ですべての新機能を体験することができます。

以下、サンプルコードがある場合、その結果はすべて手元のChrome 64によるものですので、あらかじめご了承ください。

オブジェクトの Rest/Spread プロパティ(Object Rest/Spread Properties)

配列が既に持っていた機能ですが、オブジェクトでも利用できるようになります。

/* Rest Properties */
const { a, b, ...c } = { a: 1, b: 2, x: 3, y: 4, z: 5}
console.log(a)  // 1
console.log(b)  // 2
console.log(c)  // {x: 3, y: 4, z: 5}

/* Spread Properties */
console.log({ a, b, ...c })
// {a: 1, b: 2, x: 3, y: 4, z: 5}

Promise.prototype.finally()

Promiseの結果が、fulfilledでもrejectedでも実行される処理を設定します。jQueryのdeferred.always()や、try-catch-finallyfinallyがイメージに近いと思います。

new Promise((resolve, reject) => {
  window.setTimeout(resolve, 100)
}).then(() => {
  console.log('then')
}).catch(e => {
  console.log('catch')
}).finally(() => {
  console.log('finally')
})
// then
// finally

new Promise((resolve, reject) => {
  window.setTimeout(reject, 100)
}).then(() => {
  console.log('then')
}).catch(e => {
  console.log('catch')
}).finally(() => {
  console.log('finally')
})
// catch
// finally

finallyという名前から、チェーンのどこに置いても最後に実行されそうな印象を持つかもしれませんが、実際はチェーンの順序どおりに実行されるようです。

new Promise((resolve, reject) => {
  window.setTimeout(reject, 100)
}).finally(() => {
  console.log('finally')
}).then(() => {
  console.log('then')
}).catch(e => {
  console.log('catch')
})
// finally
// catch

テンプレートリテラルの改修(Template Literal Revision)

テンプレートリテラルで不正なエスケープシーケンスが使われた場合の取扱いを変更します。

const texCommand = `\usepackage[utf8]{inputenc}`
// Uncaught SyntaxError: Invalid Unicode escape sequence

\uの部分がUnicodeエスケープシーケンス(例:\u0020)の開始と解釈されますが、後に続く文字列が数値とは解釈できないので、不正なエスケープシーケンスとしてシンタックスエラーが発生します。

今後は、タグ付きテンプレートリテラルの場合に限り、エラーが出ないようになります。

const tag = strArray => {
  console.dir(strArray)
}

tag`\usepackage[utf8]{inputenc}`
// 0: undefined
// raw: ["\usepackage[utf8]{inputenc}"]

tag`hoge fuga piyo`
// 0: "hoge fuga piyo"
// raw: ["hoge fuga piyo"]

シンタックスエラーにならない代わりにundefinedになります。タグ付きではない、通常のテンプレートリテラルでは、引き続きシンタックスエラーとなります。

正規表現(RegExp)関係

sオプション(dotAllフラグ)

正規表現で.といえば、どの文字にもマッチする特殊文字ですが、実際には改行文字などマッチしない文字がありました。ES2018では、sオプションを指定すると、.がこれらの文字にもマッチするようになります。

例えば、HTML文字列から正規表現で値を引っこ抜くケースでは、.が改行にマッチしてくれないので[\s\S][^]を使うことがありましたが、今後はsオプションを指定して.で済むようになります。

名前付きキャプチャグループ(Named Capture Groups)

()を使って正規表現にマッチした文字列をキャプチャする際、これまではmatch[1]とかmatch[2]のように番号で結果を取り出していましたが、各グループに任意の名前を付けられるようになります。

const dateText = '2018-01-28'

/* Before */

const resultBefore = dateText.match(/(\d{4})-(\d{2})-(\d{2})/)
console.log(resultBefore[1])  // 2018
console.log(resultBefore[2])  // 01
console.log(resultBefore[3])  // 28

/* After */

const resultAfter = dateText.match(/(?<year>\d{4})-(?<month>\d{2})-(?<date>\d{2})/)
console.log(resultAfter.groups.year)  // 2018
console.log(resultAfter.groups.month) // 01
console.log(resultAfter.groups.date)  // 28

各グループにはgroupsプロパティからアクセスすることができます。後方参照は\k<name>です。

また、この機能はString.prototype.replace()による置換にも使用可能です。

const dateText = '28/01/2018'

/* Before */

const regBefore = /(\d{2})\/(\d{2})\/(\d{4})/
const resultBefore = dateText.replace(regBefore, (m, c1, c2, c3) => {
  return `${c3}年${c2}月${c1}日`
})
console.log(resultBefore)  // 2018年01月28日

/* After(第2引数が文字列) */

const regAfter = /(?<date>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})/
const resultAfter = dateText.replace(regAfter, '$<year>年$<month>月$<date>日')
console.log(resultAfter)  // 2018年01月28日

/* After(第2引数がコールバック関数) */

const resultAfterWithCallback = dateText.replace(regAfter, (...args) => {
  const groups = args[args.length - 1]
  return `${groups.year}年${groups.month}月${groups.date}日`
})
console.log(resultAfterWithCallback)  // 2018年01月28日

replace()の第2引数にコールバックを渡して処理する場合、groupsはコールバックに渡される引数の末尾に入ります。ただし、引数の数がマッチの結果次第で変わるので、上例のようにlengthを使って「最後の引数」とするとすっきり書けます。

なお、アロー関数にはargumentsオブジェクトが無いので、アロー関数を使う場合は、仮引数を設定せずに中でarguments[arguments.length - 1]とはできません。

後読み(Lookbehind Assertions)

先読みはあるのにどうして…と散々言われてきましたが、JavaScriptの正規表現にもついに後読みがやってきました。

「先読み」「後読み」でどっちがどっちだっけとよく迷いますが、私の場合は、文字の進行方向(右)に対して、先読みなら前(右)、後読みなら後ろ(左)の文字列を読む、という覚え方をしています。

肯定後読み (?<=...)

const text = '+1 -2 3 +4'
const reg = /(?<=\+)\d+/g

text.match(reg)  // ["1", "4"]

この例では、「左に+がある数字」の数字部分にマッチしています。+は含みません。

否定後読み (?<!...)

const text = '+1 -2 3 +4'
const reg = /(?<!\+)\d+/g

text.match(reg)  // ["2", "3"]

この例では、「左に+が無い数字」の数字部分にマッチしています。2の前は-3の前はスペースなので、23が結果として得られています。

Unicodeプロパティエスケープ(Unicode property escapes)

Unicodeの1つ1つの文字には、それがどのような文字であるかを示す、たくさんのプロパティが指定されています。このプロパティを、正規表現の中でマッチングに使うことができるようになります。

例えば漢数字の「一」(\u4e00)は、以下のXML断片(後述のucd.all.flat.xmlから抜粋したもの)からわかるように、大量のプロパティを持っています。

<char cp="4E00" age="1.1" na="CJK UNIFIED IDEOGRAPH-#" JSN="" gc="Lo" ccc="0" dt="none" dm="#" nt="Nu" nv="1" bc="L" bpt="n" bpb="#" Bidi_M="N" bmg="" suc="#" slc="#" stc="#" uc="#" lc="#" tc="#" scf="#" cf="#" jt="U" jg="No_Joining_Group" ea="W" lb="ID" sc="Hani" scx="Hani" Dash="N" WSpace="N" Hyphen="N" QMark="N" Radical="N" Ideo="Y" UIdeo="Y" IDSB="N" IDST="N" hst="NA" DI="N" ODI="N" Alpha="Y" OAlpha="N" Upper="N" OUpper="N" Lower="N" OLower="N" Math="N" OMath="N" Hex="N" AHex="N" NChar="N" VS="N" Bidi_C="N" Join_C="N" Gr_Base="Y" Gr_Ext="N" OGr_Ext="N" Gr_Link="N" STerm="N" Ext="N" Term="N" Dia="N" Dep="N" IDS="Y" OIDS="N" XIDS="Y" IDC="Y" OIDC="N" XIDC="Y" SD="N" LOE="N" Pat_WS="N" Pat_Syn="N" GCB="XX" WB="XX" SB="LE" CE="N" Comp_Ex="N" NFC_QC="Y" NFD_QC="Y" NFKC_QC="Y" NFKD_QC="Y" XO_NFC="N" XO_NFD="N" XO_NFKC="N" XO_NFKD="N" FC_NFKC="#" CI="N" Cased="N" CWCF="N" CWCM="N" CWKCF="N" CWL="N" CWT="N" CWU="N" NFKC_CF="#" InSC="Other" InPC="NA" PCM="N" vo="U" RI="N" blk="CJK" kCompatibilityVariant="" kRSUnicode="1.0" kIRG_GSource="G0-523B" kIRG_TSource="T1-4421" kIRG_JSource="J0-306C" kIRG_KSource="K0-6C69" kIRG_KPSource="KP0-FCD6" kIRG_VSource="V1-4A21" kIRG_HSource="HB1-A440" kIRG_USource="" kIRG_MSource="" kIICore="AGTJHKMP" kGB0="5027" kGB1="5027" kCNS1986="1-4421" kCNS1992="1-4421" kJis0="1676" kKSC0="7673" kKPS0="FCD6" kCantonese="jat1" kHangul="일" kDefinition="one; a, an; alone" kHanYu="10001.010" kMandarin="yī" kCihaiT="1.101" kSBGY="468.40" kNelson="0001" kCowles="5133" kMatthews="3016" kPhonetic="1499" kGSR="0394a" kFenn="1A" kFennIndex="216.01 217.06 218.01 220.06" kKarlgren="175" kCangjie="M" kMeyerWempe="3837" kSpecializedSemanticVariant="U+58F9" kSemanticVariant="U+5F0C<kLau,kMatthews,kMeyerWempe U+58F9<kLau,kMatthews,kMeyerWempe" kVietnamese="nhất" kLau="3341" kTang="*qit qit" kJapaneseKun="HITOTSU HITOTABI HAJIME" kJapaneseOn="ICHI ITSU" kKangXi="0075.010" kBigFive="A440" kCCCII="213021" kDaeJaweon="0129.010" kEACC="213021" kFrequency="1" kGradeLevel="1" kHDZRadBreak="⼀[U+2F00]:10001.010" kHKGlyph="0001" kHanyuPinlu="yī(32747)" kHanyuPinyin="10001.010:yī" kIRGHanyuDaZidian="10001.010" kIRGKangXi="0075.010" kIRGDaeJaweon="0129.010" kIRGDaiKanwaZiten="00001" kKorean="IL" kMainlandTelegraph="0001" kMorohashi="00001" kPrimaryNumeric="1" kTaiwanTelegraph="0001" kXerox="241:042" kFourCornerCode="1000.0" kXHC1983="1351.020:yī 1360.040:yí 1368.160:yì" kRSKangXi="1.0" kRSAdobe_Japan1_6="C+1200+1.1.0" kTotalStrokes="1" isc="" na1=""/>

Unicodeのすべての字が同じ種類のプロパティを持っているわけではなく、漢字は比較的プロパティの数が多いようです。

多すぎてとても数え切れませんが、150~200個くらいはあるでしょうか。よくよく見ると画数や読み仮名も含まれています。

つまり、文字に対して、これだけの角度から条件化できるようになるということです。これまでは複数の文字をマッチング対象とする場合、[一-下]のようにコードポイント順で一括指定したり、[一ニ三四五]のように対象の文字を1つずつ羅列したりしていましたが、今後はそれ以外の形でも、文字を一括りにしてマッチングすることが可能となります。

一方、どの文字が同じグループに含まれているのかをきちんと把握していなければ、思わぬ文字を引っ掛けてしまう可能性もあるので、この機能の活用にはUnicodeについてしっかり知っておくことが必要となります。

プロパティの種類や値の調べ方

(2018-01-30 追記)Unicode Utilitiesというツールで、オンラインでもデータを閲覧することができました。ある文字の持っているプロパティはCharacter Propertiesで、プロパティや値の一覧はCharacter Property Indexでそれぞれ調べることができます。
Twitterで@mashabow‏さんから情報提供いただきました。ありがとうございました。なお、このツールはHTTPSではアクセスできないようで、ツイートのリンクから飛ぶと503エラーとなりますので、ご注意ください。)

どのようなプロパティの種類と値があり、それぞれどのようなエイリアスがあるかは、Unicode Character Databaseからデータをダウンロードして調べることができます。

オンラインのデータベースがあってもよさそうですが、無いですよね…?もしあったら教えてください。

「About the Unicode Character Database」のスクリーンショット。「http://www.unicode.org/Public/UCD/latest/」のURLが赤枠で囲われている

Latest VersionのURLをクリック

URLをクリックするとディレクトリインデックスの画面になるので、「ucd/」を選択します。

「Index of /Public/UCD/latest」のスクリーンショット。「ucd/」が赤枠で囲われている

「ucd/」を選択する。

ファイル一覧からPropertyAliases.txtとPropertyValueAliases.txtを選択します。前者がプロパティ名とそのエイリアス、後者が各プロパティの取り得る値を掲載しています。

「Index of /Public/UCD/latest/ucd」のスクリーンショット。「PropertyAliases.txt」及び「PropertyValueAliases.txt」が1つの赤枠で囲われている

PropertyAliases.txtとPropertyValueAliases.txtを選択する(ダウンロードorクリックで開く)

文字ごとのプロパティの調べ方

ある文字がどのようなプロパティを持っているかを知りたい場合も、同様にUnicode Character Databaseでデータをダウンロードすることで調べられます。

「Index of /Public/UCD/latest」のスクリーンショット。「ucdxml/」が赤枠で囲われている

「ucdxml/」を選択する。

いくつかのファイルに分かれていますが、とりあえず全部入りの「ucd.all.flat.zip」をダウンロードし、解凍します。

「Index of /Public/UCD/latest/ucdxml」のスクリーンショット。「ucd.all.flat.zip」が赤枠で囲われている

「ucd.all.flat.zip」を選択し、ダウンロードして解凍する。

ucd.all.flat.zipを解凍すると、1つだけXMLファイルが入っています。このファイルにすべての文字とそのプロパティが定義されています。解凍後のファイルサイズが巨大(約172MB)なので、ブラウザやテキストエディタで読み込む際はご注意ください。

他にもテキスト形式で同じ情報が得られるファイルもあるようですが、1つのファイルですべての情報が得られ、かつある程度読みやすいのは、このXMLファイルではないかと思います。

具体例

プロパティを使った正規表現で、あるアルファベットが「小文字であるかどうか」を判定してみます。

まず、この判定に使えそうなプロパティがあるか調べます。PropertyAliases.txtを見ると、Binary Propertiesの中にLowercaseというプロパティがあります。

# ================================================
# Binary Properties
# ================================================

...
Lower                    ; Lowercase
...

きっと「小文字であるかどうか」を示すプロパティでしょう。そういうことにします。この調子でUppercaseもありそうですが、どちらか一方があれば足りそうなので、ここではLowercaseが見つかったのでよしとします。

続いて、PropertyValueAliases.txtで、Lowercaseについて書かれている箇所を探します。

# Lowercase (Lower)

Lower; N                              ; No                               ; F                                ; False
Lower; Y                              ; Yes                              ; T                                ; True

Lowercaseの取り得る値はYes/No(True/False)とわかりました。「小文字であるかどうか」なので、当然と言えば当然です。

実際に正規表現の中でLowercaseプロパティを使ってみます。なお、この機能を使うには、RegExpオブジェクトにuオプションを指定する必要があります。

const chars = ['a', 'B', 'C', 'd', 'e']

chars.forEach(char => {
  console.log(char, /\p{Lowercase}/u.test(char))
  // /\p{Lower}/u としても可
})

// a true
// B false
// C false
// d true
// e true

chars.forEach(char => {
  console.log(char, /\P{Lowercase}/u.test(char))
  // /\P{Lower}/u としても可
})

// a false
// B true
// C true
// d false
// e false

Lowercaseプロパティを使って、「小文字であるかどうか」を判定することができました。\P\pの否定で、\pとは反対の結果が得られています。

なお、Binary Propertiesでは\p{name=value}のように値は指定せず、\p{name}のようにプロパティ名のみ指定します。例えば、\p{Lowercase=True}と書くことはできません。一応やってみましたが、エラーになりました。

それ以外のプロパティでは値も指定しますが、General_Categoryプロパティは値だけでも良いとされているようです。

今度は、同じ「小文字であるかどうか」の判定をGeneral_Categoryプロパティを使って行ってみます。

再びPropertyValueAliases.txtを開き、General_Categoryを探すと、次のような指定となっています。

# General_Category (gc)
...
gc ; Ll                               ; Lowercase_Letter
...

Lowercase_LetterLl)というカテゴリがありましたので、同じ判定が可能のようです。

const chars = ['a', 'B', 'C', 'd', 'e']

chars.forEach(char => {
  console.log(char, /\p{General_Category=Lowercase_Letter}/u.test(char))
  // /\p{Lowercase_Letter}/u や /\p{Ll}/u としても可
})

// a true
// B false
// C false
// d true
// e true

chars.forEach(char => {
  console.log(char, /\P{General_Category=Lowercase_Letter}/u.test(char))
  // /\P{Lowercase_Letter}/u や /\P{Ll}/u としても可
})

// a false
// B true
// C true
// d false
// e false

Lowercaseプロパティを使った判定と、同じ結果が得られました。

念のため答え合わせもしておきます。ucd.all.flat.xmlからaBの行を抜き出します(他の文字は割愛)。

<!-- a -->
<char cp="0061" age="1.1" na="LATIN SMALL LETTER A" JSN="" gc="Ll" ccc="0" dt="none" dm="#" nt="None" nv="NaN" bc="L" bpt="n" bpb="#" Bidi_M="N" bmg="" suc="0041" slc="#" stc="0041" uc="0041" lc="#" tc="0041" scf="#" cf="#" jt="U" jg="No_Joining_Group" ea="Na" lb="AL" sc="Latn" scx="Latn" Dash="N" WSpace="N" Hyphen="N" QMark="N" Radical="N" Ideo="N" UIdeo="N" IDSB="N" IDST="N" hst="NA" DI="N" ODI="N" Alpha="Y" OAlpha="N" Upper="N" OUpper="N" Lower="Y" OLower="N" Math="N" OMath="N" Hex="Y" AHex="Y" NChar="N" VS="N" Bidi_C="N" Join_C="N" Gr_Base="Y" Gr_Ext="N" OGr_Ext="N" Gr_Link="N" STerm="N" Ext="N" Term="N" Dia="N" Dep="N" IDS="Y" OIDS="N" XIDS="Y" IDC="Y" OIDC="N" XIDC="Y" SD="N" LOE="N" Pat_WS="N" Pat_Syn="N" GCB="XX" WB="LE" SB="LO" CE="N" Comp_Ex="N" NFC_QC="Y" NFD_QC="Y" NFKC_QC="Y" NFKD_QC="Y" XO_NFC="N" XO_NFD="N" XO_NFKC="N" XO_NFKD="N" FC_NFKC="#" CI="N" Cased="Y" CWCF="N" CWCM="Y" CWKCF="N" CWL="N" CWT="Y" CWU="Y" NFKC_CF="#" InSC="Other" InPC="NA" PCM="N" vo="R" RI="N" blk="ASCII" isc="" na1=""/>

<!-- B -->
<char cp="0042" age="1.1" na="LATIN CAPITAL LETTER B" JSN="" gc="Lu" ccc="0" dt="none" dm="#" nt="None" nv="NaN" bc="L" bpt="n" bpb="#" Bidi_M="N" bmg="" suc="#" slc="0062" stc="#" uc="#" lc="0062" tc="#" scf="0062" cf="0062" jt="U" jg="No_Joining_Group" ea="Na" lb="AL" sc="Latn" scx="Latn" Dash="N" WSpace="N" Hyphen="N" QMark="N" Radical="N" Ideo="N" UIdeo="N" IDSB="N" IDST="N" hst="NA" DI="N" ODI="N" Alpha="Y" OAlpha="N" Upper="Y" OUpper="N" Lower="N" OLower="N" Math="N" OMath="N" Hex="Y" AHex="Y" NChar="N" VS="N" Bidi_C="N" Join_C="N" Gr_Base="Y" Gr_Ext="N" OGr_Ext="N" Gr_Link="N" STerm="N" Ext="N" Term="N" Dia="N" Dep="N" IDS="Y" OIDS="N" XIDS="Y" IDC="Y" OIDC="N" XIDC="Y" SD="N" LOE="N" Pat_WS="N" Pat_Syn="N" GCB="XX" WB="LE" SB="UP" CE="N" Comp_Ex="N" NFC_QC="Y" NFD_QC="Y" NFKC_QC="Y" NFKD_QC="Y" XO_NFC="N" XO_NFD="N" XO_NFKC="N" XO_NFKD="N" FC_NFKC="#" CI="N" Cased="Y" CWCF="Y" CWCM="Y" CWKCF="Y" CWL="Y" CWT="N" CWU="N" NFKC_CF="0062" InSC="Other" InPC="NA" PCM="N" vo="R" RI="N" blk="ASCII" isc="" na1=""/>

属性が多すぎて目眩がしてきますが、aLower="Y"(Y=Yes)でありgc="Ll"(Ll=Lowercase_Letter)、BLower="N"(N=No)でありgc="Lu"(Lu=Uppercase_Letter)ですので、小文字と大文字で関係するプロパティがちゃんと異なっていたことが確認できました。

Asynchronous Iterators

非同期のイテレータやジェネレータが利用可能となります。新たな構文として、for-await-ofが登場します。

const urls = [
  'https://github.com/tc39/proposal-object-rest-spread',
  'https://github.com/tc39/proposal-regexp-lookbehind',
  'https://github.com/tc39/proposal-regexp-unicode-property-escapes',
  'https://github.com/tc39/proposal-promise-finally',
  'https://github.com/tc39/proposal-async-iteration'
]

async function * a () {
  for (const url of urls) {
    console.log(`Fetching: ${url}`)
    const response = await fetch(url)
    const iterable = response.text()
    yield iterable
  }
}

async function b () {
  for await (const i of a()) {
    const title = i.match(/<title>(.+)<\/title>/)[1]
    console.log(`Title: ${title}`)
  }
}

b()

// Fetching: https://github.com/tc39/proposal-object-rest-spread
// Title: GitHub - tc39/proposal-object-rest-spread: Rest/Spread Properties for ECMAScript
// Fetching: https://github.com/tc39/proposal-regexp-lookbehind
// Title: GitHub - tc39/proposal-regexp-lookbehind: RegExp lookbehind assertions
// Fetching: https://github.com/tc39/proposal-regexp-unicode-property-escapes
// Title: GitHub - tc39/proposal-regexp-unicode-property-escapes: Proposal to add Unicode property escapes `\p{…}` and `\P{…}` to regular expressions in ECMAScript.
// Fetching: https://github.com/tc39/proposal-promise-finally
// Title: GitHub - tc39/proposal-promise-finally: ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
// Fetching: https://github.com/tc39/proposal-async-iteration
// Title: GitHub - tc39/proposal-async-iteration: Asynchronous iteration for JavaScript

fetchを使って取得する処理と、その結果を利用する処理を、awaitを絡めたうえですっぱり分けられました。

まとめ

「おっ、これはいいな」と思える機能が、1つでもあったでしょうか。

すべてではありませんが、既にBabelが対応している機能もあるので、今日からでも実践に取り入れることができる機能もあります。

次々出てくる新機能を追いかけていくのも一苦労ですが、より良いコードを書くために、使えそうなものは積極的に取り入れていきたいですね。

コメント

  1. […] ECMAScript 2018(ES2018)の新機能まとめ | あるいてっく https://arui.tech/es2018-new-features/#Lookbehind_Assertions […]