JSON.stringifyの第2引数について

## 前置き
JavaScriptの関数[`JSON.stringify`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)は、JavaScriptのオブジェクトや値をJSON文字列に変換します。第1引数に、変換したいオブジェクトや値を設定します。
```JavaScript:JSON.stringify
let hoge = {
  a: 1,
  b: 'fuga',
  c: {
    d: 'piyo',
  },
  e: [5, 6, 7],
};
console.log(JSON.stringify(hoge));
/// 結果
{"a":1,"b":"fuga","c":{"d":"piyo"},"e":[5,6,7]}
```
参考までに、第3引数に数字を設定すると、その分だけインデントして出力してくれます。
```JavaScript:JSON.stringify 第3引数
let hoge = {
  a: 1,
  b: 'fuga',
  c: {
    d: 'piyo',
  },
  e: [5, 6, 7],
};
console.log(JSON.stringify(hoge, null, 2));
/// 結果
{
  "a": 1,
  "b": "fuga",
  "c": {
    "d": "piyo"
  },
  "e": [
    5,
    6,
    7
  ]
}

```
## JSON.stringifyの第2引数
第2引数は、出力を制御するために配列または関数を受け取ります。nullを指定した場合は、何も制御しないことになります。
### 第2引数が配列の場合
配列の場合は、その要素をキーとするプロパティだけが出力されるようになります。
```JavaScript:第2引数_配列①
let hoge = {
  a: 'b',
  c: 'd',
  e: 'f',
};
console.log(JSON.stringify(hoge, ['a', 'e'], 2));
/// 結果
{
  "a": "b",
  "e": "f"
}
```
キーが数値なら、配列の要素は数値で指定することも可能です。
```JavaScript:第2引数_配列②
let hoge = {
  1: 10,
  2: 20,
  3: 30,
};
console.log(JSON.stringify(hoge, [1, 3], 2));
/// 結果
{
  "1": 10,
  "3": 30
}
```
※JSONなので、出力結果のキー部分は二重引用符で囲まれた文字列扱いになります。

ネストされたオブジェクトの場合、指定されたキーに合致するなら末端まで処理されます。
```JavaScript:第2引数_配列③
let hoge = {
  1: 10,
  2: 20,
  3: 30,
  4: {
    1: 110,
    2: 120,
    3: 130,
  },
  5: {
    1: 210,
    2: 220,
    3: 230,
  },
};
console.log(JSON.stringify(hoge, [1, 3, 4], 2));
/// 結果
{
  "1": 10,
  "3": 30,
  "4": {
    "1": 110,
    "3": 130
  }
}
```
### 第2引数が関数の場合
関数を指定すると、それぞれのプロパティに対して文字列化するための処理として適用されます。関数は`key, value`を受け取り、`undefined`や`null`を返すプロパティは出力されなくなります。例えば、「`key`が奇数の場合は出力しない」というような条件を、以下のように記述することができます。
```JavaScript:第2引数_関数①
let hoge = {
  1: 10,
  2: 20,
  3: 30,
  4: 40,
};
console.log(JSON.stringify(hoge, (key, value) => {
  if (key % 2 !== 0) {
    return undefined;
  } else {
    return value;
  }
}, 2));
/// 結果
{
  "2": 20,
  "4": 40
}
```
それならばと、「`key`が偶数の場合は出力しない」という条件で、以下のように記述すると、うまくいきません。
```JavaScript:第2引数_関数②_うまくいかない
let hoge = {
  1: 10,
  2: 20,
  3: 30,
  4: 40,
};
console.log(JSON.stringify(hoge, (key, value) => {
  if (key % 2 === 0) {
    return undefined;
  } else {
    return value;
  }
}, 2));
/// 結果
undefined
```
実は、「それぞれのプロパティ」として処理される要素の一番最初に、「`key`=空文字、`value`=変換対象のオブジェクト」というものがあって、この`value`をいじってしまうと変換対象オブジェクトが変わってしまうようになっています。

試しに、処理される要素をコンソールに表示してみます。
```JavaScript:第2引数_関数①_要素を表示
let hoge = {
  1: 10,
  2: 20,
  3: 30,
  4: 40,
};
console.log(JSON.stringify(hoge, (key, value) => {
  console.log('"' + key + '"', value);
  if (key % 2 !== 0) {
    return undefined;
  } else {
    return value;
  }
}, 2));
/// 結果
"" {1: 10, 2: 20, 3: 30, 4: 40}
"1" 10
"2" 20
"3" 30
"4" 40
{
  "2": 20,
  "4": 40
}
```
このように、まず変換されるオブジェクト自身が最初の列挙対象になっています。前述の「`key`が偶数の場合は出力しない」のケースでは、`key`が空文字の場合に`key % 2 === 0`という条件に合致してしまい、変換されるオブジェクトが`undefined`になってしまったのです。
※空文字は数式内では`0`扱いになるため

したがって、以下のように関数内で第1引数とは全く別のオブジェクトにしてしまうことも可能です（使いどころはありませんが）。`key === ''`以降は、新しいオブジェクトを対象として処理されています。
```JavaScript:第2引数_関数③
let hoge = {
  a: 'b',
  c: 'd',
  e: 'f',
};
console.log(JSON.stringify(hoge, (key, value) => {
  if (key === '') {
    return {
      1: 10,
      2: 20,
      3: 30,
      4: 40,
    };
  } else if (key % 2 !== 0) {
    return undefined;
  } else {
    return value;
  }
}, 2));
/// 結果
{
  "2": 20,
  "4": 40
}
```
こういった事情があるため、例えば「元のオブジェクトの値部分に文字列を追加する」といった処理をしたい場合、「一番最初の処理」はスキップさせる（そのまま`value`を返す）必要があります。
```JavaScript:第2引数‗関数④
let hoge = {
  a: 'b',
  c: 'd',
  e: 'f',
  '': 'g',
};
let isFirst = true;
console.log(JSON.stringify(hoge, (key, value) => {
  if (isFirst) {
    isFirst = false;
    return value;
  } else {
    return value + '_add';
  }
}, 2));
/// 結果
{
  "a": "b_add",
  "c": "d_add",
  "e": "f_add",
  "": "g_add"
}
```
あるいは、`value`が`object`ならそのまま返す、というやり方もあります。ネストされたオブジェクトを扱うケースを考えると、そのほうがよいかもしれません。
```JavaScript:第2引数‗関数⑤
let hoge = {
  a: 'b',
  c: 'd',
  e: {
    f: 'g',
    h: 'i',
  },
};
console.log(JSON.stringify(hoge, (key, value) => {
  if (typeof value === 'object') {
    // hoge自身の場合とhoge.eの場合にここを通る
    return value;
  } else {
    return value + '_add';
  }
}, 2));
/// 結果
{
  "a": "b_add",
  "c": "d_add",
  "e": {
    "f": "g_add",
    "h": "i_add"
  }
}
```
#### TIPS: 関数要素を扱うケース
要素が関数の場合に`JSON.stringify`は要素を無視します。
```JavaScript:要素が関数の場合①
let hoge = {
  a: () => 'b',
  c: 'd',
  e: 'f',
};
console.log(JSON.stringify(hoge, null, 2));
/// 結果
{
  "c": "d",
  "e": "f"
}
```
関数は`toString()`で文字列化することが可能なので、以下のようにすれば関数部分も残すことができます。
```JavaScript:要素が関数の場合②
let hoge = {
  a: () => 'b',
  c: 'd',
  e: 'f',
};
console.log(JSON.stringify(hoge, (key, value) => {
  if (typeof value === 'function') {
    return value.toString();
  } else {
    return value;
  }
}, 2));
/// 結果
{
  "a": "() => 'b'",
  "c": "d",
  "e": "f"
}
```
#### TIPS: 循環参照を扱うケース
循環参照を含むオブジェクトについて`JSON.stringify`を適用すると、下記のようにエラーが発生します。
```JavaScript:循環参照を含む場合①
let hoge = {
  a: 'b',
};
hoge.c = hoge; // 循環参照
console.log(JSON.stringify(hoge));
/// 結果
Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'c' closes the circle
    at JSON.stringify (<anonymous>)
```
これについては、本質的に循環参照をJSONで記述することは不可能であるため、代替文字を利用する等の対応をすることになります。
```JavaScript:循環参照を含む場合②
let hoge = {
  a: 'b',
};
hoge.c = hoge; // 循環参照
hoge.d = {};
hoge.d.e = hoge.d; // 循環参照

let list = [];
console.log(JSON.stringify(hoge, (key, value) => {
  if (typeof value === 'object') {
    if (list.indexOf(value) > -1) {
      return '$'; // 循環参照が現れたら'$'で表す
    }
    list.push(value);
  }
  return value;
}, 2));
/// 結果
{
  "a": "b",
  "c": "$",
  "d": {
    "e": "$"
  }
}
```
ちなみに、これにしっかり対応したライブラリ[`cycle.js`](https://github.com/douglascrockford/JSON-js/blob/master/cycle.js)というのがあります。`decycle`、`retrocycle`という関数を導入しています。
```JavaScript:循環参照を含む場合：cycle.jsを利用
let hoge = {
  a: 'b',
};
hoge.c = hoge; // 循環参照
hoge.d = {};
hoge.d.e = hoge.d; // 循環参照

// stringify（JSON化する）
let fuga = JSON.stringify(JSON.decycle(hoge), null, 2);
console.log(fuga);

// parse（もとにもどす）
let piyo = JSON.retrocycle(JSON.parse(fuga));
console.log(_.isEqual(hoge, piyo)); // lodashを導入してオブジェクトの比較
/// 結果
{
  "a": "b",
  "c": {
    "$ref": "$"
  },
  "d": {
    "e": {
      "$ref": "$[\"d\"]"
    }
  }
}
true
```
[`lodash.js`](https://cdnjs.com/libraries/lodash.js) - オブジェクトの比較ができるライブラリ。べんり。
## まとめ
以上、`JSON.stringify`で情報を加工したい、関数の情報を残したい、循環参照があってもエラーにならないようにしたい、などという場合には第2引数をいじろう、というお話でした。