Node.js
Node.jsDay 24

util.isDeepStrictEqual がリリースされた経緯

これで Node Advent Calendar としての util 小ネタ切れです。
切れた所で Advent Calendar が埋まりきって良かったです。

util.isDeepStrictEqual がNode v9でリリースされた経緯

蓋を開けてみればなんてこと無い話で、元々 assert.deepStrictEqual 関数が内部で使っている関数を public に変更したという話です。元からあったものをみんなにも使える形にするというだけなので、 Less is More の原則からも反していないわけですね。

https://github.com/nodejs/node/pull/16084

object 同士を比較するというよくあるケースは lodashunderscore にもあるし、 ava にも qunit にも存在しているということで、moduleとしては公開してもいいか、という発想ですね。

ちなみに中を見ると、かなり複雑なことをしています。

util.isDeepStrictEqual の中身

https://github.com/nodejs/node/blob/master/lib/internal/util/comparisons.js

function strictDeepEqual(val1, val2, memos) {

  // deepStrictEqual(1, 1) => true
  // deepStrictEqual(NaN, NaN) => true (NaN でも同じことになる)
  // deepStrictEqual(1, 2) => false
  if (typeof val1 !== 'object') {
    return typeof val1 === 'number' && Number.isNaN(val1) &&
      Number.isNaN(val2);
  }

  // deepStrictEqual(null, undefined) => false (null, undefined 違い)
  // deepStrictEqual(null, null) => true 
  if (typeof val2 !== 'object' || val1 === null || val2 === null) {
    return false;
  }
  const val1Tag = objectToString(val1);
  const val2Tag = objectToString(val2);

  // deepStrictEqual([1,2], {a:1, b:2}) => false (型違い)
  if (val1Tag !== val2Tag) {
    return false;
  }

  // class A {}
  // class B extends A {}
  // deepStrictEqual(B, A) => false (prototype 違い)
  if (Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2)) {
    return false;
  }

  // deepStrictEqual([1,2,3], [1,2,3]) => true Arrayのときの比較
  if (val1Tag === '[object Array]') {
    // Array の長さが違ったらfalse
    if (val1.length !== val2.length)
      return false;
    // 長さが等しければ中身のチェック
    return keyCheck(val1, val2, true, memos);
  }

  // deepStrictEqual({a: 1, b: 2}, {a: 1, b: 2}) => true objectのときの比較
  if (val1Tag === '[object Object]') {
    // 中身チェック
    return keyCheck(val1, val2, true, memos);
  }

  // Date の時は getDate したもの同士で比較
  if (isDate(val1)) {
    if (val1.getTime() !== val2.getTime()) {
      return false;
    }
  // Regexp の場合は source と flag のチェックして、違ったら false
  } else if (isRegExp(val1)) {
    if (!areSimilarRegExps(val1, val2)) {
      return false;
    }
  // Error の場合はメッセージでのみチェック
  // (ここは本来 Error のIDがNodeの場合はあるからそれを確認してもいいか?)
  } else if (val1Tag === '[object Error]') {

    if (val1.message !== val2.message) {
      return false;
    }
  // Bufferの時は Buffer.compare で比較する
  // 普通のTypedArrayは一つ一つ中身をチェックする、ただし300個の要素だけ比較する
  // (全部比較するのは高コストのため)
  } else if (isArrayBufferView(val1)) {
    if (!areSimilarTypedArrays(val1, val2,
                               isFloatTypedArrayTag(val1Tag) ? 0 : 300)) {
      return false;
    }
    // ラフなチェックで true になる場合は中身をきっちりチェックする。
    return keyCheck(val1, val2, true, memos, val1.length,
                    val2.length);
  // valueOf 関数がある場合
  } else if (typeof val1.valueOf === 'function') {
    const val1Value = val1.valueOf();
    // valueOf を実行後、もう一度 deepStrictEqualを実行する
    if (val1Value !== val1) {
      if (!innerDeepEqual(val1Value, val2.valueOf(), true))
        return false;
      var lengthval1 = 0;
      var lengthval2 = 0;

      // valueOf 関数実行後の value が 文字列の場合は長さを入れて比較する 
      if (typeof val1Value === 'string') {
        lengthval1 = val1.length;
        lengthval2 = val2.length;
      }
      return keyCheck(val1, val2, true, memos, lengthval1,
                      lengthval2);
    }
  }
  // 全ての型にマッチしない場合は中身をきっちりチェックする
  return keyCheck(val1, val2, true, memos);
}

Map だったり Set だったりの場合は keyCheck 関数の中で詳細にチェックしています。ただし、 WeakMap だったり、 WeakSet は対象外です。弱参照のコレクション型はそのGC状況によって中身がどうなるかわからないですし、何よりも比較するコレクションではないです。

ちなみに WeakMap もしくは WeakSet 同士の比較を util.isDeepStrictEqual を実行すると 必ず true になります。
(ただこれは必ず false の方が誤解を生まないのでまだ良いような。。。 issue 出してみるか、、、)