Node.jsのVersion16の新機能をひととおり紹介

鈴木正樹氏(以下、鈴木):私からは「Node.jsのVersion16の新機能」ということで紹介したいと思います。よろしくお願いします。

簡単に自己紹介させてもらうと、鈴木正樹といいまして、愛知県の半田市というところでフリーランスエンジニアやっています。得意分野は、AWSのバックエンドです。アーキテクチャ、設計、開発です。テストもやりますが、そこらへんが得意です。

主な技術スタックは、先ほど挙げたAWSだったり、サーバーレスフレームだったり、サーバーレスです。メインではNode.jsやTypeScriptを使って開発しています。あとは、いろいろSNSでアウトプットもやっています。

(スライドに)今回紹介する新機能とあるのですが、Version16の新機能はとりあえずひととおりという感じになります。これはSlideShareで発表していて、connpassにもリンクが貼ってあるので、そちらから見てもらえればと思います。

参考資料のサイトですが、今回細かいソースを追っていくのはちょっとできないので、ブログの一番下のやつですね、『【Node.js】 Node.js Version16(ES2021)の新機能』。これが私が書いたブログで、こちらにソースのことも書いているので、ソースを追いたい場合は、こちらを見てもらえればなと思います。

行区切りが可能な「Numeric Separators」でソースの可読性が向上

では、本編を始めたいと思います。まず最初の機能として、Numeric Separatorsという、桁区切りですね。ソースコード上の数値について、アンダーバーでの桁区切りが可能になりました。

右にチラッとソースを書いたのですが、こういう大きい数字が、桁区切りすることで見やすくなりました。ソースの可読性の向上という意味では、けっこう機能がよくなりました。

10進以外に、2進数や16進数でも桁区切りを行えるので、そういったソースの見やすさという意味で非常によくなりました。もちろん、桁区切りしたまま計算もできます。

あとは、右側のソースの一番下ですね。parseIntは文字列の数値変換や、2進数や16進数の10進数への変換をやると思うのですが、注意しなきゃいけないのは、一番下のような、文字列での桁区切りです。一番下みたいなやつは、parseIntしても正しく動かないので注意してください。

parseIntをやる時は、数値や下から2番目みたいに数値で桁区切りする分にはいいのですが、文字列での桁区切りは正しく動かないので、そこだけちょっと注意をしてください。

文字列の全置換専用関数「String.prototype.replaceAll」

次はString.prototype.replaceAllで、文字列全置換ですね。今までもreplaceでの全置換はできたのですが、今回、replaceAllという文字列の全置換専用の関数が追加されました。

これはreplace関数とは違って、置換対象の文字列も正規表現で指定する必要がなくなりました。上から3つ目、「hoge」と書いてあるのですが、単純に文字列で指定ができるようになりました。

今までのreplace関数と同様、正規表現で指定してもOKです。ただその場合、グローバルフラグ付けないと、必ずエラーになるので注意してください。

置換対象の文字を、文字列で指定できるので、C#やJavaなど、ほかの言語と同じような指定の仕方でよくなりました。ほかの言語から来た時にハマらないで済むかなと思います。

Javaは、replaceAllという正規表現での指定ができる関数があるのですが、JavaScriptのreplaceAllも、正規表現の指定して大丈夫なので、ごっちゃになったりとかは心配しなくても大丈夫だと思います。

あとは、文字列ですね。関数名で直接全置換だよというのがわかりやすくていいなと思います。

どれか1個でもresolveされればOK「Promise.any」

次ですね。Promise.anyということで、Promiseについてまた機能が1個追加されました。

複数のPromiseを指定するのは、ほかのPromise関連の関数と一緒なのですが、最初にresolveされたPromiseを返す関数になります。言い換えれば、どれか1個でもresolveされればいいので、anyという関数名になっています。

似たような動きをするもので、Promise.raceというものがあるのですが、このPromise.anyはraceと違って、どれか1個でもresolveされればOKです。

Promise.raceはresolve、reject関係なく、一番最初に返ってきたPromiseで判定するのですが、Promise.anyは一番最後に返ってきたPromiseだけがresolveされるケースでも、エラーになりません。

先ほど言ったとおり、どれか1個でも正常終了すればOKなので、そういった実装が可能になりました。今までanyがなかった時は、実装がけっこう面倒くさかったと思うので、これは地味にうれしいかなと思います。

コード例を書いたのですが、右の枠にPromise、非同期関数ですね、「async function」があります。この「sleep」は今回ここで追加した、ほかの言語でいうところのThread.Sleepみたいなものです。

例えば一番遅く結果が返ってくる「promise3」だけがresolveされたとして、ここの「const」、「await Promise.any」というのが、左の真ん中にいるのですが、Promise.anyの場合は、「promise3」だけresolveされるので、エラーになりません。

その代わり、その下のPromise.raceですね、これは「promise1」の結果だけで判定するのですが、「promise1」は1秒後にrejectされる関数なので、このPromise.raceの場合は、これがエラーになってしまうという違いがあります。これがPromise.anyの説明になります。

Version16で一番ややこしい「WeakRef」「FinalizationRegistry」

さっき、一番ややこしくて、わかりにくい、コアと言っていたのが、次の機能です。WeakReferenceというオブジェクトの弱参照として、WeakRefとFinalizationRegistryがあります。これが今回、Version16で一番ややこしい部分だと思います。

まず、WeakRefですね。これはなにかというと、オブジェクトへの弱い参照を提供する機能というのが、本家そのままの説明になります。

「弱い参照って何?」ということなのですが、値の取得が可能なのは、通常の参照と同じなのですが、もう1個、ガベージコレクションの挙動を邪魔しないというのが特徴です。

「ガベージコレクションの挙動を邪魔しないって何?」ということなのですが、右のソースの上から4行目ぐらいですね、「new WeakRef」というところ。「obj.value」というオブジェクトを参照をしているのですが、これがWeakRef以外で参照されることがなくなった場合。例えばWeakRefでオブジェクトをなにか参照していて、そのオブジェクトがこのWeakRef以外で参照されなくなった場合、たとえWeakRefが参照していても、ガベージコレクションによってこのオブジェクトが消されてしまうんですね。メモリ上から消されてしまうので、WeakRefが参照していたからといって、この参照してるオブジェクトがメモリ上にあるかどうかは保証されないんです。

なので、参照しているオブジェクトがメモリ上にあることが保証されない参照と言い換えることができるかもしれません。

とにかく挙動がガベージコレクションに大きく依存するというのも、このWeakRefの特徴です。

次に、FinalizationRegistryです。先ほど、ガベージコレクションというものが出てきたのですが、FinalizationRegistryというのは、オブジェクトがガベージコレクションされたことをトリガーにして、起動するコールバック関数を提供する機能です。

C#がわかる方は、IDisposableインターフェイスみたいなものをイメージしてもらうとわかりやすいかもしれません。

WeakRefと組み合わせて、例えば対象のオブジェクトがガベージコレクションされた場合にログを出すことが可能になります。

これもオブジェクトがガベージコレクションされたことをトリガーにするので、やはりガベージコレクションに大きく依存するのが特徴です。

「WeakRef」と「FinalizationRegistry」の挙動

ここまで言葉で説明してきたのですが、ちょっとわかりにくいかもしれないので、ちょっとデモを見せますね。

今、ソースが見えていると思うのですが、HTMLで書きました。例えばここの「const obj」を参照するとして、ここのWeakRefでそれぞれ「value」と「value2」を参照するとします。

「deref」という、メソッドが弱参照しているオブジェクトの値を取得する関数があります。1回参照したあと、それぞれobjの、この「value」と「value2」をnullにすると、このWeakRef以外から参照されなくなるので、この「deref」で参照してる、この「value」と「value2」はガベージコレクションされるという挙動になります。

ガベージコレクションされた時に、こっちの「obj.value」はFinalizationRegistryで登録して、このメッセージを出すのですが、objの「value2」は登録したあとすぐ登録解除して、「この参照不可になりました」というログを出さないのを実行するデモを表示します。

ちょっと動画をお見せします。

最初に、上の2行はオブジェクトを参照して、そのあとオブジェクト「value」と「value2」をnullにしたあとの挙動になります。

急ぎ足でわかりにくかったかもしれませんが、最初の2行では……一番最初のソースでいう、「value」と「value2」を「deref」します。

それぞれnullにしたあとの挙動を1秒ごとに追っていくと、8秒経った時点でガベージコレクションされます。「value」、「value2」それぞれがガベージコレクションされるので、9秒以降はそもそも参照すらできなくなります。

ガベージコレクションされた時点で、「obj.value」はFinalizationRegistryのコールバック関数が走って、「参照不可になりました」というログが出ます。「value2」は、登録したあとにすぐ登録解除しているので、そのログが出ません。

公式の見解は「可能な限り使用は避けるべき」

「WeakRefとFinalizationRegistryの挙動はわかったけど、どんなところに使うの?」というところなのですが、公式の見解では、主な使い道として、大きいオブジェクトのキャッシュやマッピングと書いています。

例えば繰り返し参照するDBテーブルのレコードセットなどを、このWeakRefなどで参照するという使い方が公式の見解として書いてありました。

ただ同じく公式の見解として、可能な限り使用は避けるべきと思いっきり明言されています。

挙動がガベージコレクションに大きく依存するし、肝心のそのガベージコレクションの挙動制御が困難なので、可能な限り使用は避けるべきというのが公式の見解です。

なので、強いて使い道を挙げるなら、例えば開発時や障害発生時の、メモリの挙動の調査とかに用いるのかなというのが、個人的に使い方の1つかなと思います。

少なくとも、お客さまだったり、プロダクトコードだったりで使用するのは避けたほうがいいというのは、なんとなく個人的な感想としてありました。

実際のさっきのデモのコードを書いていると、ちょっとややこしいのもありました。さっきは8秒でしたが、やるたびに秒数も変わってくるので、やはり本番のプロダクトコードでは避けたほうが無難かなと感じました。

代入演算子の機能も追加

最後ですね。Logical Assignment、代入演算子です。代入演算子で、このリストにある3つが追加されました。それぞれの使い方や、どんな挙動をするかはこの表のとおりです。

こういった代入演算子が新たに追加されましたよというのがVersion16の新規の機能として1個あります。

地味な部分で便利になったVersion16 WeakRefやFinalizationRegistryの扱いは注意

というわけで、最後にまとめですが、Version16の個人的な感想は、大きい変更はないけど、地味な部分で便利になったという印象を受けました。

実際、replaceAllやPromise.anyなんかは確かに、劇的な変更ではありませんが、やはりあるとうれしいというか、ありがたい機能だなと実感しました。

ただ、WeakRefやFinalizationRegistryですね。ここらへんはちょっと扱いに注意が必要だと、コード書いていたり、触ったりしていても思いました。

なので、知識だけを持っておいて、あまり本番のコードなどでは導入しないほうがいいんじゃないかなと個人的に思いました。私の発表は以上です。ご清聴ありがとうございました。