🔥

day18

2023/05/22に公開

解答コード

function jsonStringify(object) {
  switch (typeof object) {
    case "object":
      if (Array.isArray(object)) {
        const elements = object.map((element) => jsonStringify(element));
        return `[${elements.join(",")}]`;
      } else if (object) {
        const keys = Object.keys(object);
        const keyValuePairs = keys.map(
          (key) => `"${key}":${jsonStringify(object[key])}`
        );
        return `{${keyValuePairs.join(",")}}`;
      } else {
        return "null";
      }
    case "boolean":
    case "number":
      return `${object}`;
    case "string":
      return `"${object}"`;
    default:
      return "";
  }
}
object = { a: "str", b: -12, c: true, d: null };
const keys = Object.keys(object);
const keyValuePairs = keys.map(
  (key) => `"${key}":"${object[key]}"`
);

console.log(keyValuePairs)

このような処理をすると以下のような結果になる。

Input: { a: "str", b: -12, c: true, d: null }
Output: [ '"a":"str"', '"b":"-12"', '"c":"true"', '"d":"null"' ]

オブジェクトのvalueを文字式にしたら、数値もbooleanもnullも文字列になった。
JSONは

  • 文字列
  • 数値
  • null
  • boolean
  • object
  • 配列
    の型を利用することができるのでvalueの値の型が全て文字列になってしまうので型の情報が失われてしまう。なのでvalueを""で囲わずに、jsonStringifyで囲む。
const keyValuePairs = keys.map(
    (key) => `"${key}":${jsonStringify(object[key])}`
);

jsonStringify()の場合分けはcase文により「object」か「object以外」で処理を分けている。objectの場合、配列かオブジェクトの2つのパターンがある。object以外の場合、boolean、number、string、null、undefined、に分けて処理をする。これはJavaScriptのプリミティブ型と呼ばれている。(正確にはbigintもあるがサンプルコードでは書かれてない。)

再帰関数のコードを読むとき、ベースケースがなんなのかを確認するのは大事だ。再帰処理は同じことをグルグル繰り返す処理なのだが、いつまでも同じことをするわけではない。いつかはループを抜け出す時が来るのだ。その抜け出すタイミングがベースケースであり、その目印がreturnである。コードを見ると、再帰関数を呼び出さずreturnで値を返してないタイミングがプリミティブ型の場合だ。

考えてみればobject型である配列とオブジェクトはプリミティブ型の集まりなのだから、処理を施すのは最終的にプリミティブ型に対してだ。

再帰処理のベースケース(再帰関数を呼び出さずreturnで値を返す場合)はobject以外であるboolean、number、string、null、undefinedの場合だ。
ベースケースの処理を見ていこう。

    case "boolean":
    case "number":
      return `${object}`;
    case "string":
      return `"${object}"`;
    default:
      return "";

booleanとnumberはreturn ${object};のようにそのまま値を返している。
stringの場合はreturn "${object}";のようにダブルクオーテーション""をつけて返している。わざわざ""をつけなくていいような気がするが、""をつけないとダメっぽい。それは次のサンプルコードで分かる。

object = "hello"

console.log(object)
console.log(`"${object}"`)

> hello
> "hello"

nullとundefinedはdefaultはひとくくりにして場合わけしてる。nullもundefinedも""にまとめていいようだ。
https://ajya.hatenablog.jp/entry/2020/03/25/180500

    default:
      return "";

次にobject型に関して見ていく。object型には配列とオブジェクト(連想配列)の二種類があるので、それぞれの場合に分けて処理を施す。場合わけの方法はArray.isArray()を使うといい。これがtrueなら配列でfalseならオブジェクトである。

配列は素直な処理しかしない。なぜならjsonの要素の中に配列を入れることができるし、配列はプリミティブ型を複数まとめただけの型に過ぎないからだ。この再帰処理のベースケースはプリミティブ型なのだから、配列の要素を取り出して全ての要素に再帰関数を施しもう一度配列に戻す。
jsonStringifyを

ここでmap&joinのよく見るパターンが使われている。mapを使い配列の各要素に対して同じ処理を行って配列に戻し、joinによって文字列に変換してる。join(",")の引数はセパレーターで、,を区切りとして連結した文字列を返す。このとき、[]で囲んでるから再び配列に戻している。なんでわざわざこういうふうにするのか意図はわからなかった。

ちなみにこんなふうにconsole.logで振る舞いを見てみた。仏まいの違いはchatgptで聞こう

if (Array.isArray(object)) 
  const elements = object.map((element) => jsonStringify(element));
  console.log(elements)                  // [ '3', '4', 'true', '"aa"' ]
  console.log(elements.join(","));       / /3,4,true,"aa"
  console.log([elements.join(",")]);     // [ '3,4,true,"aa"' ]
  console.log(`[${elements.join(",")}]`);// [3,4,true,"aa"]
  return `[${elements.join(",")}]`;
 }

わかってないこと

console.log([elements.join(",")]);
console.log(`[${elements.join(",")}]`);
これらの出力が違うのはなんで

この疑問はchatgptに聞ける

なんでmap&joinの組み合わせで一度文字列に変換したのを[]で配列に戻したのかわからない

Discussion