4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

闇の魔術に対する防衛術Advent Calendar 2019

Day 8

続・Array、運命の操作

Last updated at Posted at 2019-12-07

闇の魔術に対する防衛術 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:

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?