Help us understand the problem. What is going on with this article?

続・Array、運命の操作

闇の魔術に対する防衛術 Advent Calendar 2019 の6日目の記事 にこんなことを書きました。

a = new Uint16Array([10, 20]);
b = new Array(10, 20);
console.log(JSON.stringify(a)); // {"0":10,"1":20}
console.log(JSON.stringify(b)); // [10,20]

ArrayとUint16Arrayでは同じ配列なのに、なぜかこのように挙動が異なります。
なぜ Array を名乗るのに少し動きが違うのか?
今回はもう少し深掘りします。

JSON.stringify() の挙動

JSON - JavaScript | MDN

ポリフィル
JSON オブジェクトは古いブラウザでサポートされていません。この問題はスクリプトの先頭に以下のコードを挿入して、(Internet Explorer 6 のような) JSON をネイティブにサポートしないブラウザでの JSON オブジェクトの利用を可能にすることで回避できます。

以下のアルゴリズムは、ネイティブな JSON オブジェクトを模倣するものです:
...

なるほど、ポリフィルに書いてあるコードを見ればなにかわかるかもしれないですね。

if (!window.JSON) {
  window.JSON = {
    parse: function(sJSON) { return eval('(' + sJSON + ')'); },
    stringify: (function () {
      var toString = Object.prototype.toString;
      var hasOwnProperty = Object.prototype.hasOwnProperty;
      var isArray = Array.isArray || function (a) { return toString.call(a) === '[object Array]'; };
      var escMap = {'"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t'};
      var escFunc = function (m) { return escMap[m] || '\\u' + (m.charCodeAt(0) + 0x10000).toString(16).substr(1); };
      var escRE = /[\\"\u0000-\u001F\u2028\u2029]/g;
      return function stringify(value) {
        if (value == null) {
          return 'null';
        } else if (typeof value === 'number') {
          return isFinite(value) ? value.toString() : 'null';
        } else if (typeof value === 'boolean') {
          return value.toString();
        } else if (typeof value === 'object') {
          if (typeof value.toJSON === 'function') {
            return stringify(value.toJSON());
          } else if (isArray(value)) {
            var res = '[';
            for (var i = 0; i < value.length; i++)
              res += (i ? ', ' : '') + stringify(value[i]);
            return res + ']';
          } else if (toString.call(value) === '[object Object]') {
            var tmp = [];
            for (var k in value) {
              // in case "hasOwnProperty" has been shadowed
              if (hasOwnProperty.call(value, k))
                tmp.push(stringify(k) + ': ' + stringify(value[k]));
            }
            return '{' + tmp.join(', ') + '}';
          }
        }
        return '"' + value.toString().replace(escRE, escFunc) + '"';
      };
    })()
  };
}

parseメソッドは一行で終わるのに対し、stringifyメソッドはやたらと長いですね...
それでは見ていきましょう。

        // ...
        } else if (typeof value === 'object') {
          if (typeof value.toJSON === 'function') {
            return stringify(value.toJSON());
          } else if (isArray(value)) {
            var res = '[';
            for (var i = 0; i < value.length; i++)
              res += (i ? ', ' : '') + stringify(value[i]);
            return res + ']';
          } else if (toString.call(value) === '[object Object]') {
            var tmp = [];
            for (var k in value) {
              // in case "hasOwnProperty" has been shadowed
              if (hasOwnProperty.call(value, k))
                tmp.push(stringify(k) + ': ' + stringify(value[k]));
            }
            return '{' + tmp.join(', ') + '}';
          }
        }
        // ...

おやおや、 else if (isArray(value)) なんてものが気になります。
Array.isArray ではなく、stringifyメソッド内で利用できるものが次のようにまた定義されています。

var isArray = Array.isArray || function (a) { return toString.call(a) === '[object Array]'; };

ただ、ほとんど Array.isArray と同じですね。
Array.isArray については、オブジェクトがArrayであればtrue、それ以外ならfalseが返るようになっています。

a = new Uint16Array([10, 20]);
b = new Array(10, 20);
console.log(Array.isArray(a)); // false
console.log(Array.isArray(b)); // true
console.log(toString.call(a)); // [object Uint16Array]
console.log(toString.call(b)); // [object Array]

Arrayの場合は isArray(value) でtrueを返すので、角括弧を使った変換がされて [10,20] という文字列が返ります。

Uint16Arrayの場合は isArray(value) でfalseを返すので、Arrayと同じような変換がされません。
ポリフィルではただのカンマ区切りになります。

まとめると、 似たような配列のオブジェクトであっても挙動が異なる というわけです。
あくまでポリフィルのコードを見ただけなので、実際のJavaScriptの仕様は厳密に異なることがあります。その点はご留意くださいませ。

おわりに

今回は JSON.stringify() の動きが、ArrayとUint16Arrayで異なることを紹介しました。
Uint16Arrayを完全なArrayと思わないことが防衛術になるでしょうか...
もしかしたら、思わぬところで沼にはまることがあるかもしれないので備忘録として書き留めておきます:grinning:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away