uhyohyo.net

JavaScript初級者から中級者になろう

十六章第十八回 ES2015とプリミティブ

このページの最終更新日:

今回はプリミティブに関するES2015以降の新要素を紹介します。最近こういう網羅系の記事ばかりで申し訳ないですが、それだけ最先端のJavaScriptに近づいているということです。次の章も面白いので、飽きたらこの章は一旦先に置いておいて先に進んでみるのもよいでしょう。

プリミティブは、ご存知の通り文字列、数値、真偽値、undefined、null、そしてES2015で追加されたシンボル、の6種類があります。これらにも、特に文字列と数値に対して、ES2015で新機能が追加されました。

数値リテラル

ますは数値を見ていきましょう。ES2015では新しい文法として、数値リテラルの種類が増えました。それは、2進数リテラル8進数リテラルです。

2進数は、0と1だけで数を表記する方法です。とくにビット列の操作などをするときは、数値を2進数で書けると分かりやすくなります。そこで、ES2015では数値を2進数で表すためのリテラルが追加されました。2進数リテラルは、0bのあとに0と1を用いて数を記述します。


console.log(0b1010); // 10
console.log(0B00100000); // 32

このように、大文字を用いて0Bとしても構いません。これは16進数リテラルが0xで始まるのと似ていますね。ちなみに、bというのは2進(数)を表す単語 binaryの頭文字です。

余談ですが、16進数のxは16進を表すhexadecimalの3文字目です。hexaが6でdecimalが10なのでxだと6なのか16なのか分からないような気もしますが、コンピュータの世界では6進数は使われないので問題ありません。

上の例では、2進数で1010は10進数の10であり、2進数で100000は10進数の32に相当するためこのような結果になります。

また、プログラムでは8進数も多少使われます。そこで、8進数を表すリテラルも追加されました。8進数はoctalなので、0oまたは0Oとします。ゼロとオーが紛らわしいような気がしますが、小文字を使えば大丈夫でしょう。


console.log(0o755); // 493

なお、昔のJavaScriptでは、頭に0oではなく0をつけることで8進数リテラルになる機能がありました。恐らく今のブラウザでも同様の動作をするはずです。


console.log(0755); // 493

しかし、これは8進数を意図していることが分かりにくいという問題があり、今は非推奨になっています。桁数を揃えるために頭に0を付けたのに数値が変わってしまうというような問題も起こるかもしれません。なので、8進数を表したい場合は0oリテラルを使いましょう。なお、この古い8進数リテラルはstrictモードでは禁止されており、文法エラーとなります。したがって、strictモードをちゃんと使う偉い人にとってはES5において8進数リテラルが存在せず、ES2015で改めて追加されたという形です。


function foo(){
  "use strict";
  console.log(0755); // ここで文法エラー
}
foo();

冪乗演算子

これはES2016の話なのですが、数値に関する新しい演算子があります。それは冪乗演算子 ** です。冪乗というのは、23 = 8 のような演算のことです。


console.log(2 ** 3); // 8

冪乗を行いたい場面はそんなに多くないかもしれませんが、この演算子があると簡単に行うことができます。ちなみに、従来はMath.powメソッドで冪乗を計算できました。

ちなみに、この演算子はなかなか愉快な性質を持ちます。例えば、ほとんどの計算ではNaNを含む計算の結果はNaNになりましたが、この演算子でNaNの0乗を計算すると1になります。


console.log(Number.NaN ** 0); // 1

他に、Infinityを含む計算もだいたい直感通りの結果になります。たとえば正の数を-Infinity乗すると0になります。

Numberの静的メソッド

いままで、数値にかかわるメソッドをいくつか紹介してきました。isFiniteisNaNのように数値の性質を判定するメソッド、そしてparseIntやparseFloatのように文字列を数値に変換するメソッドです。

これらの関数は、ES2015ではNumberの静的メソッドとなりました。つまり、Number.isFiniteNumber.parseIntのように使うということです。ただし、従来の関数を消してしまうと動かないコードが大量にできてしまい困るので、従来のものも使用できます。しかし、将来的にはNumberの静的メソッドバージョンが推奨されていくことになるでしょうから、こちらを使うことをおすすめします。

このような機能追加が行われた背景には、JavaScriptをきれいにモジュールに分けたいという意図があります。つまり、isFiniteやparseIntなどの個々の関数がグローバル変数として散らばっているのはよろしくないということです。数値に関わるメソッドはNumberの下に置くことにすれば、グローバル変数として存在するのはNumberのみで済みますね。前回紹介したようにオブジェクトに関わる種々のメソッドはObjectの静的メソッドとして存在しているのも同じような理由からでしょう。

Number.isInteger

さて、ES2015では、数値の判定に関するメソッドがいくつか追加されました。これらは新しいので、Numberの静的メソッドとしてのみ利用できます。

ひとつはNumber.isIntegerです。Integerというのは整数のことなので、これは与えられた数値が整数かどうか判定するというメソッドです。当然、返り値は真偽値となります。

ご存知の通り、JavaScriptの数値には整数と小数(浮動小数点数)の区別がありません。そこで、数値の中でも整数のみを特別扱いする場面もきっとあることでしょう。このメソッドを使うと、楽に判定できます。


console.log(Number.isInteger(3)); // true
console.log(Number.isInteger(2.5)); // false
console.log(Number.isInteger(2 ** 128)); // true

なお、InfinityやNaNをNumber.isIntegerに渡すとfalseが返ります。

Number.isSafeInteger

このメソッドは、与えられた数値がsafe integer(安全な整数)かどうか判定します。

safe integerは、絶対値が253-1 (9007199254740991) 以下の整数です。

これは浮動小数点数(特にIEEE 754 倍精度形式)にかかわる概念です。実は、IEEE 754 倍精度形式の浮動小数点数(すなわちJavaScriptにおける小数)では、絶対値が253より大きい整数は正確に表すことができません。これはビット数が足りないからです。例えば、次のように253 + 1という数を作ったつもりでも、それは253と同じになります。一方、この大きさ帯の浮動小数点で表すことができる最小の差は2であるため、253 + 2は253とは異なる数として表現されます。


console.log(2 ** 53 === 2 ** 53 + 1); // true
console.log(2 ** 53 === 2 ** 53 + 2); // false

この現象は浮動小数点数を使う限り避けられないことです。ただ、一般に浮動小数点数を扱う場合はその精度が問題となりますから、253などの大きな数を扱う場合に1とか2とかという小さい誤差を気にすることはありません。一方、整数を扱う場合あまり精度が落ちるというイメージはわかないでしょう。JavaScriptの場合、整数だけを扱っている場合でもあまりに絶対値が大きくなった場合は精度が落ちます。

今のところ、JavaScriptでこのレベルに大きな整数を精度を保ったまま簡単に扱う方法はありませんが(BigIntという機能が提案されています)、せめて計算結果が正確でない領域に突入したことを簡単に知ることができると嬉しい場面もあるかもしれません。

計算結果がsafe integerならば、その計算結果は十分正確である(計算結果が実は他の整数であるということはありえない)ことが保証されます。Number.isSafeIntegerはそのような判定に用いることができるのです。

この理屈でいけば、253はsafe integerではありません。なぜなら、上で見たように、253 + 1 という別の整数が253に変化しているかもしれないからです。絶対値が253-1の整数ではこのようなことは起こらないのです。

以上でNumber.isSafeIntegerの話は終わりです。浮動小数点数に関する込み入った話が出てきましたが、そして多分これを呼んでいるほとんどの人はこのメソッドのお世話になることはないかもしれませんが、JavaScriptの数値のあり方と結構深く関わっているメソッドですので、小話と思って知っておくのもよいのではないかと思います。

なお、safe integerという用語を検索するとこのNumber.isSafeIntegerの話ばかり出てきます。どうも浮動小数点数にかかわる一般的な用語ではないような気がします。

Numberの静的プロパティ

浮動小数点数の話をもう少しだけ続けます。特定の数値を知るためのNumberの静的プロパティを以前紹介しましたね。例えばNumber.MAX_VALUEには1.7976931348623157e+308が入っています。

ES2015でも3つそのようなプロパティが追加されましたので、紹介だけしておきます。

Number.EPSILONには2.220446049250313e-16が入っています。これは、浮動小数点数の世界で1 + x !== xとなるような(絶対値が)最初のxです。このときの1 + xは1より大きい最小の浮動小数点数です。

Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGERはそれぞれ最大・最小のsafe integerです。上の説明の通り、具体的にはそれぞれ253 - 1と-(253 - 1)です。

文字列

以上で数値の話は終わり、次は文字列の話です。

Unicodeサポート

ES2015で文字列に関して特筆すべきことは、Unicodeサポートの充実です。

以前説明したように、JavaScriptの文字列はUTF-16コードユニットの列として表されています。ひとつひとつの文字はこのコードユニット1つに相当します。ということは、UTF-16でサロゲートペアにより表される文字(言い方を変えれば、コードポイントU+10000以上を持つ文字、あるいはUTF-8で4バイトで表される文字)は、JavaScriptにおいて2文字として数えられるということです。

例えば、絵文字はほとんどが4バイト文字です。絵文字1つだけからなる文字列はJavaScriptでは2文字と数えられるでしょう。


console.log("😂".length); // 2

要するに、従来のJavaScriptは文字の数を数えることすらまともにできなかったのです。非常に残念ですね。

そこで、ES2015ではU+10000以上の文字もちゃんと扱えるようなメソッドが追加されています。それらを紹介します。

それはString#codePointAtです。これはString#charCodeAtの4バイト文字対応版です。指定した位置にある文字のコードポイントを返します。例えば、😂のコードポイントはU+1F602(10進数に直すと128514)なので、次のようになります。


console.log("😂".codePointAt(0)); // 128514

これはコードポイントを簡単に取得できて便利ですね。従来のcharCodeAtを使って取得できるのは個々のコードユニット(サロゲートペア)なので、コードポイントを得るには自分で計算する必要がありました。

ただし、指定した位置というのがどういう意味かについては注意してください。例えば、"😂🙈"という2文字の文字列において、🙈のコードポイントを取得するには位置として2を指定する必要があります。


console.log("😂🙈".codePointAt(2)); // 128584

これはなぜかというと、codePointAtに指定する位置というのはあくまでJavaScript文字列(すなわちUTF-16コードユニットの列)における位置で指定する必要があるからです。😂はサロゲートペアで表される文字列なので、JavaScriptの文字列においてはコードユニット2つで表されます。それが位置0と位置1のコードユニットになります。よって、🙈がはじまるのは位置2となるのです。

なお、codePointAtに文字の開始位置ではない位置を指定した場合はその位置のコードユニットが返されます。

ちなみに、コードポイントを渡すと対応する文字を返す静的メソッドString.fromCodePointもあります。


console.log(String.fromCodePoint(128514)); // "😂"

もうひとつUnicode対応が進んでいるのは、文字列をイテレータとして扱った場合です。以前、文字列をイテレータとして扱った場合は1文字ずつ文字が出力されるイテレータになると解説しましたが、ここでいう1文字というのは、ちゃんとU+10000以上の文字も1文字として扱ってくれます。


console.log(Array.from("😂🙈")); // ["😂", "🙈"]

上で見たようにcodePointAtの位置指定は少し扱いにくいので、文字数にちょうど対応した文字の位置指定をしたい場合はこのようにイテレータ経由で配列にしてしまうとよいかもしれません。

さらに、文字列リテラルでもUnicodeサポートが追加されています。それは、"\u{1f602}"のようなエスケープシーケンスです。\u{ }という形の中にUnicodeコードポイントを16進数で記述することで、そのコードポイントの文字となります。従来のユニコードエスケープシーケンスは、"\u28ff"のような形でした。これも同じく\uという形を使いますが、その後ろの16進数は4桁固定です。これはコードポイントではなくコードユニットを表すエスケープシーケンスとなっているので、U+10000以上の文字を表す場合はサロゲートペアを書く必要がありました。新しい方は好きな桁数で書くことができ、コードポイントを用いて文字を表すことができるので簡単ですね。

Unicodeサポートの話は実はこれだけです。しかし、次回の記事(正規表現)でも少しUnicodeの話が出てきます。

startsWith, endsWith

文字列のメソッドstartsWith, endsWithは、文字列がそれぞれある文字列で始まるか、及びある文字列で終わるかどうかを判定するメソッドです。返り値はもちろん真偽値です。


var str = "こんにちは。私は田中です。";

console.log(str.startsWith("こんにちは")); // true
console.log(str.startsWith("こんばんは")); // false
console.log(str.endsWith("です。")); // true
console.log(str.endsWith("です!")); // false

これらのメソッドには第2引数として数値を指定することができます。startsWithの第2引数に数値を指定した場合、文字列の一番最初の代わりに指定した位置から判定を行います。例えば、上の文字列strにおいて6文字目から先は"私は田中です。"なので、


var str = "こんにちは。私は田中です。";
console.log(str.startsWith("私は", 6)); // true

となります。

この機能により、startsWithは実質指定した位置に指定した文字列があるかどうか判定するメソッドとして利用できますね。

なお、残念なことに、このメソッドは従来と同様に、U+10000以上の文字は2文字として扱います。


console.log("😂🙈".startsWith("🙈", 2)); // true

endsWithの第2引数に数値を渡した場合は、文字列のその位置で終わるように指定した文字列があるかどうかを判定してくれます。


var str = "こんにちは。私は田中です。";
console.log(str.endsWith("田中", 10)); // true

これがtrueになる理由は、文字列strは"田中"の直後がちょうど10文字目となっているからです。

repeat

repeatメソッドは、指定した回数だけ文字列を繰り返した文字列を返すメソッドです。便利ですね。


console.log("foobar".repeat(5)); // "foobarfoobarfoobarfoobarfoobar"

includes

includesメソッドは、指定した文字列がもとの文字列に含まれているかどうかを判定するメソッドです。Array#includesと似ていますね。String#indexOfを使えば似たようなことができるのも同じですが、便利で分かりやすいので使えるときは使うとよいでしょう。


console.log("こんにちは。私は田中です。".includes("田中")); // true

normalize

normalizeメソッドは文字列を指定した方法で正規化するメソッドです。方法は第1引数で指定します。指定できるのは"NFC", "NFD", "NFKC", "NFKD"の4種類の文字列です。

正規化というのはUnicodeの概念です。ここでは詳しく解説しませんので、知りたい方は調べてみてください。

以上でES2015で追加された文字列のメソッドは終わりですが、実はES2017で追加されたメソッドが2つあるのでそれも紹介しておきます。

padStart, padEnd

これらのメソッドは、文字列の長さが足りないときに指定した文字で埋めてしまうことができるメソッドです。padStartは文字列の先頭に、padEndは文字列の最後に文字列を付け足します。

例えば、数値の桁数が足りない時に先頭に0を付け足したい場合は、次のようにできます。


console.log("1234".padStart(10, "0")); // "0000001234"

このように、padStart及びpadEndの第1引数には数値を渡します。文字列の長さが指定された数値より短い場合、足りない分だけ第2引数に指定された文字を付け足した文字列が返されます。

なお、第2引数には複数文字からなる文字列を渡すことができます。


console.log("12345".padStart(10, "-*")); // "-*-*-12345"

また、第2引数は省略することができ、その場合は" "(半角スペース)が補われます。

padEndの場合も同様に、文字列の最後に第2引数で指定した文字列が付け足されます。


console.log("12345".padEnd(10, "-*")); // "12345-*-*-"

文字列の長さを揃えたいという需要は結構いろいろな場面であります。padStart, padEndはそのような需要を叶えるメソッドです。とても便利ですね。

@@toPrimitive

以上で文字列のメソッドも全部紹介しました。だんだんJavaScript中級者に近づいてきましたね。

この記事ではあと2つ紹介することがあります。まずは@@toPrimitiveです。ここまで読んだ読者の方なら、これがwell-knownシンボルのひとつであるSymbol.toPrimitiveのことであるのはお分かりだと思います。

Well-knownシンボルは、@@isConcatSpreadableなどの例に顕著なように、オブジェクトの動作をカスタマイズするのに使われることが多いものです。@@toPrimitiveも例外ではありません。名前から類推できるかもしれませんが、このwell-knownシンボルはオブジェクトをプリミティブに変換するときの動作をカスタマイズするのに使われます。

オブジェクトが@@toPrimitiveメソッドを持つ場合、そのオブジェクトがプリミティブに変換される場合にそのメソッドが呼ばれます。以前、オブジェクトがプリミティブに変換される場合に呼ばれるメソッドはtoStringやvalueOfであると説明しました。ES2015で追加された@@toPrimitiveはそれらよりも優先されます。

@@toPrimitiveメソッドが呼ばれる場合、第1引数として期待されている型が渡されます。具体的には、"string""number""default"の3種類の文字列のうちどれかが渡されます。以前、文字列に変換したい場合はtoStringが呼ばれ、数値に変換したい場合はvalueOfが呼ばれると説明しましたが、@@toPrimitiveの場合はどの場合でも@@toPrimitiveが呼ばれます。よって、各場合に対してより柔軟な対応をすることができます。

言うまでもなく、引数に"string"が渡されるのはオブジェクトが文字列に変換されようとしているときです。同様に、数値に変換されようとしているときは"number"が渡されます。プリミティブなら何でも良い場合(というとちょっと語弊がありますが)は"default"が渡されることになっています。

ただし、返り値の型は必ずしもこれに従う必要はありません。"string"が渡されたのに数値を返すようなことも一応は可能です。ただし、その場合その数値はどうせ文字列に変換されることになりますが。

しかし、返り値としてオブジェクトを返した場合はエラーが発生します。返すのはプリミティブでなければいけません。

では、例を見ましょう。


var obj = {
  [Symbol.toPrimitive](hint){
    if (hint === "string"){
      return "三百";
    } else {
      return 300;
    }
  },
};

console.log(String(obj)); // "三百"
console.log(Number(obj)); // 300
console.log(obj * 3); // 900

ここで作ったオブジェクトobjは、数値に変換されるときは300という数値になりますが、文字列に変換されるときはなぜか漢数字になるという意味不明なオブジェクトです。String関数やNumber関数で文字列や数値に変換するとたしかにその通りになっていることが分かります。また、obj * 3という式ではobjは数値に変換されます。なぜなら、掛け算は数値どうしで行う必要があるからです。

これはやや細かい話ですが、引数として"default"が来る場合、すなわちプリミティブなら何でもいいという場合はあまり多くありません。そのひとつは+演算子の場合です。+演算子は足し算ですが、片方が文字列の場合は文字列の連結になります。よって、+演算子にオブジェクトが渡された場合、とりあえず指定なし(引数"default")で両辺をプリミティブに変換してみて、どちらかが文字列だったら文字列の連結になるという動作をします。運良く両方とも数値(もしくは真偽値など)に変換された場合は数値の足し算になります。

もうひとつは==演算子に渡された場合です。この演算子は===とは異なり、両辺が異なる型の場合は型変換を行って型を合わせようとするというはた迷惑な演算子でしたね。

==の片方が数値または文字列、もう片方がオブジェクトの場合はオブジェクトが"default"モードでプリミティブに変換されます。よって、例えば次のような比較はtrueになります。


var obj = {
  [Symbol.toPrimitive](hint){
    if (hint === "string"){
      return "hello";
    } else if (hint === "number"){
      return 100;
    } else {
      return 5;
    }
  },
};

console.log("5" == obj); // true

あともうひとつはDateコンストラクタに渡された場合ですが、詳細は省略します。

今回紹介した@@toPrimitiveにより、オブジェクトがプリミティブに変換されるときの挙動を制御できます。楽しいですね。

@@toStringTag

オブジェクトをプリミティブに変換するという話題が出てきましたが、普通のオブジェクトを文字列に変換した場合はどんな文字列になるのでしょうか。


var obj = {
  foo: 3,
};

console.log(String(obj)); // "[object Object]"

var obj2 = Promise.resolve(4);

console.log(String(obj2)); // "[object Promise]"

このように、普通のオブジェクトは"[object Object]"という文字列になります。また、Promiseを文字列にすると"[object Promise]"となります。

このような変換結果はObject.prototype.toStringメソッドによって生成されます。このメソッドの結果は必ず"[object なんとか]"という形になります。普通のオブジェクトの場合はなんとかの部分はObjectとなりますが、Promiseの場合はPromiseとなります。

こうなるとこの部分はコンストラクタの名前が入るように思えますが、実はそうではありません。自分でクラスを作るとそのことを確かめることができます。


class MyClass{}

var obj = new MyClass();

console.log(obj.toString()); // "[object Object]"

このように、自分でクラスを作った場合もこの部分はObjectとなります。

ちなみに、配列などは独自のtoStringメソッドを持っているのでこの形にはなりません。


var arr = ['foo', 'bar'];

console.log(String(arr)); // "foo,bar"

ただし、Object.prototype.toStringを無理やり呼び出すと面白い結果を見ることができます。


var arr = ['foo', 'bar'];

console.log(Object.prototype.toString.call(arr)); // "[object Array]"

なんと、オブジェクトではなくnullやundefinedなどにも独自の結果が用意されています。オブジェクトでないのにobjectと書いてあるのは面白いですね。


console.log(Object.prototype.toString.call(null)); // "[object Null]"

なお、ものによってはこの部分が一単語ではないこともあります。


console.log(Object.prototype.toString.call([].entries())); // "[object Array Iterator]"

では本題に入ります。@@toStringTagを使うと、この部分をカスタマイズすることができるのです。このプロパティには文字列で自分の好きな値を入れましょう。


var obj = {
  [Symbol.toStringTag]: 'Hello',
};
console.log(String(obj)); // "[object Hello]"

もちろん、prototypeにこれを入れておくことで自分のクラスのインスタンスのtoString結果をカスタマイズすることもできます。


class MyClass{
  get [Symbol.toStringTag](){
    return 'MyClass';
  }
}

var obj = new MyClass();

console.log(String(obj)); // "[object MyClass]"

なお、先ほど説明した通り、@@toStringTagプロパティはObject.prototype.toStringからのみ使われます。なので、独自のtoStringメソッドを定義したり、上で紹介したばかりの@@toPrimitiveメソッドを定義した場合は@@toStringTagは意味がありません。(上の例でやったように、無理やりObject.prototype.toStringを適用される可能性もあるので全くないというわけではありませんが。)

余談ですが、組み込みのオブジェクトに対する独自の結果は@@toStringTagを使っている場合と使っていない場合があります。一貫していない理由は歴史的経緯です。


var arr = [];
console.log(arr[Symbol.toStringTag]); // undefined

var iterator = [].entries();
console.log(iterator[Symbol.toStringTag]); // "Array Iterator"