echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Node.js】Node.js version16(ES2021)の新機能 ※2021/6/26更新

はじめに

2021年4月に、Node.jsのversion10がサポート終了となり、AWSからも「Lambdaのバージョンを更新してください」という案内が来てました。

そして、それと同じタイミングでversion16がリリースされました。
今年の10月にステータスがActiveになりますので、そうなったらLambdaでもサポートされると思われます。

そこで、Node.js version16(=ES2021)の新機能について、確認してみました。

version16の新機能一覧

  • replaceAll
  • Promise.any
  • WeakRefs
  • Finalizers
  • numeric Separators
  • Logical Assignment

※numeric separator, 及び演算子(operator)系は省略します。
→ 2021/6/26 追記しました。

参照ページ

replaceAll

これは、名前の通り「(文字列の)一括置換」を行うメソッドです。(直球)

今まで一括置換を行う場合、replace()関数で置換対象文字列に正規表現リテラルを指定しなければならなかったですが、replaceAll()関数を使えば、単に文字列を指定すればOKです。(下記ソース参照)

知らないと結構ハマる部分だと思うので(特に多言語を知っている人がJavaScriptを触る場合)、専用の関数ができたことで分かりやすくなったと思います。

const baseStr = 'hoge|fuga|piyo';  
  
// これだとreplacedは「hoge_fuga|piyo」になる。(最初の一致文字列しか置換しない)  
const replaced = baseStr.replace('|', '_');   
  
// replace関数の場合、一括置換は正規表現リテラルを指定しないといけない。  
const replacedGlobal = baseStr.replace(/|/g, '_');  
  
// replaceAll関数なら、正規表現リテラルを指定しなくても一括置換してくれる。    
const replacedAll = baseStr.replaceAll('|', '_');  

Promise.any

version12のPromise.allSettledに続き、Promiseの機能追加です。

これは「引数に指定した複数のPromiseのうち、いずれか1つでもresolveされたら、そのうち最初にresolveされたPromiseを返す」というものです。
逆に全Promiseがrejectされた場合は、「AggregateError」というエラーをthrowします。(≒rejectされる)

Promise.raceと似ていますが、Promise.raceとの違いは「いずれか1つでもresolveされたPromiseがあったら、エラーにならない」という点です。(Promise.raceは、最初に結果が返ってきたPromiseがrejectならその時点でエラーになる)

なので例えば下記ソースのexecute関数を実行した場合、

  • Promise.any:「promise3 is resolved」とINFOログが表示される。(=正常終了)
  • Promise.race:「promise1 is rejected」とERRORログが表示される(=エラー発生)

という違いがあります。

const execute = async () => {
    try {
        // ここでPromise.anyかPromise.raceのいずれかを実行する。  
        // const promise = await Promise.any([promise1(), promise2(), promise3()]);  
        // const promise = await Promise.race([promise1(), promise2(), promise3()]);
        console.info(promise);
    } catch (e) {
        console.error(`error message is ${e.message}`);
    }
};

// 指定ms間処理をwaitさせる処理(多言語のsleepみたいなもの)
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  
// 1秒待ってエラーをthrow(=reject)
async function promise1() {
    await _sleep(1000);
    throw new Error("promise1 is rejected");
}
  
// 2秒待ってエラーをthrow(=reject)
async function promise2() {
    await _sleep(2000);
    throw new Error("promise2 is rejected");
}
  
// 3秒待って正常終了(=resolve)
async function promise3() {
    await _sleep(3000);
    return "promise3 is resolved";
}  

ちなみに、Promise.xxx系の各メソッドの挙動は下記のとおりです。

関数 正常終了する条件 エラーになる条件 正常終了した場合の戻り値
Promise.race 最初に結果が返ってきたPromiseがresolveされた 最初に結果が返ってきたPromiseがrejectされた 最初に結果が返ってきたPromise
Promise.all 全Promiseがresolveされた 1つでもrejectされたPromiseがある 全Promise(配列)
Promise.allSettled 必ず正常終了する なし(エラーになることはない) 全Promiseのstatus、およびvalue(resolve時)またはreason(reject時)(配列)
Promise.any 1つでもresolveされたPromiseがある 全Promiseがrejectされた 最初にresolveされたPromise

WeakRefs

今回の新機能で、一番分かりにくい部分だと思います。

直訳すれば「弱い参照」、MSNのサイトでは、下記の通りに記載されています。

A WeakRef object lets you hold a weak reference to another object, without preventing that object from getting garbage-collected.

訳:WeakRefオブジェクトは、ガベージコレクションを妨げることのない他のオブジェクトへの弱い参照を保持します。

とありますが、概要としてはこんな感じです。

  • WeakRefsを使用して、あるオブジェクトを参照できる。(値の取得など)
  • 「あるオブジェクト」は、WeakRefs以外から一切参照されなくなったら、ガベージコレクションの対象になる。(=破棄される)
  • 「あるオブジェクト」がガベージコレクションされたら、WeakRefsからも参照不可になる。

なおWeakRefsを使用するケースとしては「サイズが大きいオブジェクトのキャッシュやマッピング」です。

ソースで説明すると、下記の様な感じになります

  1. obj.valueにオブジェクトを定義。
  2. WeakRefインスタンスを作成し、参照するオブジェクトにobj.valueを設定し、その内容をログ出力。
  3. obj.valueをnullにし、WeakRef以外からの参照を無くし(=ガベージコレクションの対象にする)、その直後のWeakRefの弱参照オブジェクトの内容を出力
  4. 1秒ごとに、WeakRefの弱参照オブジェクトの内容を出力。(どこかでobj.valueガベージコレクションされる)
let logs = "";  
  
// ログの記載(これは毎回使う)
const log = (...args) => {
    log += args.map(a => JSON.stringify(a)).join("  ") + "\n"
    console.log(logs);
}
  
// 1
const obj = {
    value: { hoge: 'fuga'}
}
  
// 2  
// WeakRefの引数は、オブジェクトじゃないとダメ。(1とか'a'とかはNG)  
// WeakRef.deref()メソッドで、弱参照しているオブジェクトを取得できる
const ref = new WeakRef(obj.value)
log(ref.deref())
  
// 3
obj.value = null
log(ref.deref())
  
// 4
let n = 0
setInterval(() => {
    log(++n, ref.deref())
}, 1000)

で、結果はこうなりました。
f:id:Makky12:20210605074840p:plain

obj.valueをnullにしてから17秒後にガベージコレクションにより、ref.deref()の値が参照できなくなりました。

とはいえ、いつ参照できなくなるかはガベージコレクション次第なので、当然結果は毎回違います。
なので、MDNを始めいろんなサイトに書かれている通り、WeakRefは「できる限り使用しない」のが良いと思います。(使用するとしたら「異常にメモリを使用しているソースで、何がメモリを大量に使用しているか調べる」場合でしょうか?)

Finalizer

Finalizerは先程のWeakRefsに関連する機能で「WeakRefが弱参照しているオブジェクトがガベージコレクションにより参照不可になったこと」をトリガにして起動するコールバック関数を提供する機能です。
FinalizationRegistryクラス、およびそのregisterメソッドを使用して、Finalizerを実装します。

詳細はソースを見た方が早いと思いますので、下記ソースを参照。(WeakRefsのソースにFinalizerの実装を追加してます)

const log = (...args) => {
    document.getElementById("weakRefs").innerText += args.map(a => JSON.stringify(a)).join("  ") + "\n"
}
  
const obj = {
    value: { hoge: 'fuga'}
}
  
const ref = new WeakRef(obj.value);
  
// ここからがFinalizer実装部分。  
// FinalizationRegistryインスタンスを生成する。  
// コンストラクタ引数に、コールバック関数を定義する。  
// valueには、register()メソッドの第2引数が代入される。
const registry = new FinalizationRegistry(value => {
    log(`${value} は参照不可になりました。`);
});  
  
// FinalizationRegistry.register()メソッドにて対象のオブジェクト、  
// およびコールバック関数に渡す引数を設定する。  
// ここの第二引数(今回は文字列「obj.value.hoge」)が、コールバック関数のvalueに入る。  
// 
// なお第三引数のtokenObjは「登録解除トークンオブジェクト」で、unregister関数では  
// この「登録解除トークンオブジェクト」をキーに、対象のオブジェクト(今回はobj.value)を 
// Finalizerの登録から解除する。
// 今回は便宜上別のオブジェクトにしたが、第一引数と同じオブジェクトでOK。  
// (というより、同じにするのが一般的らしい)
const tokenObj = {token : 'token'};
registry.register(obj.value, "obj.value.hoge", tokenObj);  
  
// unregister関数を用いることで、Finalizerに登録したオブジェクト(今回はobj.value)を登録解除できる。  
// 引数に指定するのは「登録解除トークンオブジェクト」で、対象のオブジェクト自体ではない。  
// registry.unregister(tokenObj);
  
log(ref.deref())
  
obj.value = null
log(ref.deref())
  
let n = 0
setInterval(() => {
    log(++n, ref.deref())
}, 1000)

で、上記ソースの実行結果はこちら。(ちなみにWeakRefの時と、ガベージコレクションのタイミングが違っていることがわかると思います。このことからもWeakRefの挙動はガベージコレクション次第だというのが分かると思います。)
f:id:Makky12:20210605080605p:plain

確かにobj.valueガベージコレクションにより参照不可になった時点でFinalizerのコールバック関数が起動し、「obj.value.hogeは参照不可になりました」というログが出力されています。

ただ、WeakRefsが「できる限り使用しないのが良い」ので、Finalizerも出番は少ないかもしれませんね。(出番があれとすれば、やはり調査系でしょうか)

numeric separator(2021/6/26追加)

これは一言でいえば「数値の桁区切り」です。(「,」みたいなもの)
ソースコード上の数値を「_」で桁区切り記載する子tができます。

また10進数だけではなく、2進数や16進数の数値にも対応しています。

これはソースを見た方が早いので、ソースで説明します。

// NGの書き方の一例  
// const ng1 = _100_200_  // _が先頭や末尾にある   
// const ng2 = 100__200  // _が2つ以上連続してる
// const ng3 = 0_100_200 // 先頭(桁区切りの最上位の値)が0である  
    
// ちなみに区切り桁数に制限はないので、各区切りで桁数がバラバラという書き方も可能。  
// もちろん可読性が悪くなるだけなので、やる意味は皆無。
// const sample = 1_23_456_7890;  
  
// 10進数の桁区切り
const num1 = 100_000_000;
const num2 = 12_345_678;  
  
// もちろん普通に計算も可能
console.log(`num1 + num2 = ${num1 + num2}`);
  
// 16進数&2進数の桁区切り。  
// もちろんparseIntでの基数変換も可能
const hexNum = 0x01_2F;
console.log(`hexNum is ${parseInt(hexNum)}`);

const binNum = 0b1010_0101;
console.log(`binNum is ${parseInt(binNum)}`);
  
// parseIntで基数変換する場合、第一引数の指定に注意。  
// 本来文字列を指定するが、桁区切り文字列を指定すると、  
// 正しく動作しないので注意。
console.log(`${parseInt(hexNum)}`);  // OK  
console.log(`${parseInt(hexNum.toString(16), 16)}`);  // OK
console.log(`${parseInt("0x01_2F", 16)}`);  // NG(値は「1(=_の左側のみ)」になる

Logical Assignment(2021/6/26追加)

これは「代入演算子」ですが、下表の3つが追加されました。
それぞれ、下記のような代入が可能です。

代入演算子 主な使い方 動作の説明 詳細
??= a ??= b aがnullかundefinedの時のみ、aの値にbを代入する null, undefiend以外の場合は何もしない。
||= a ||= b aがfalsyな値の場合のみ、aの値にbを代入する falsyな値:null, undefiend, 0, false, 空文字
&&= a &&= b aがtruthyな値の場合のみ、aの値にbを代入する truthyな値:falsyではない値

サンプルソースはこちら。

// ??=の確認。  
// aはnullなので、a='a'となる。
let a = null;
a ??= 'a';
console.log(a);
  
// bはnullでもundefinedでもないので、b=''とならない。(0のまま)
let b = 0;
b ??= 'b';
console.log(b);
  
// ||=の確認。  
// cはfalsyな値なので、c='c'となる。
let c = false;
c ||= 'c';
console.log(c);
  
// dはfalsyな値ではないので、d='d'とならない。(trueのまま)
let d = true;
d ||= 'd';
console.log(d);
  
// &&=の確認。  
// eはtruthyな値ではないので、e='e'とならない。(falseのまま)
let e = false;
e &&= 'e';
console.log(e);
  
// fはtruthyな値なので、f='f'となる。
let f = true;
f &&= 'f';
console.log(f);

まとめ

一部省略しましたが、Node.js verison16(ES2021)の新機能をざっと紹介しました。

個人的に劇的に大きな変更というのはあまり無い感じですが、replaceAll, Promise.anyなんかは使いどころがありそうだなあ、と思いました。(特に前者)

便利な機能はどんどん実際のプロダクトにも導入して、ソース実装を便利にしていきたいですね。

告知

6/28(月) 19:00開催の「Node学園 36時限目 オンライン」というイベントで「node10→12/14で便利になった点」という内容で登壇することになりました。

内容は主にLambda実装の観点で「node version10→version12以降で役に立った機能」について紹介する予定です。
あと「node10→12/14」とありますが、時間があれば一部version16の機能も紹介する予定です。

※2021/6/26修正
登壇内容ですが、都合により「Node.js version16の新機能」に変更となりました。(ちょうど今回のブログのような登壇内容となります)
nodejs.connpass.com

それでは、今回はこの辺で。