複雑なデータ構造:ネストと配列/オブジェクトの組み合わせ

複雑なデータ構造のVSCODE画面
目次

なぜ「ネスト(入れ子)」が必要なのか?データの階層構造を理解する

プログラミングを学び始めたばかりの頃は、「変数」という箱に一つのデータ(数値や文字列)を入れて処理を行うことが基本でした。しかし、私たちが生きる現実世界のデータは、もっと複雑で立体的です。例えば、「一人のユーザー」を表現しようとしたとき、そこには名前や年齢だけでなく、住所、趣味、過去の注文履歴、持っている資格など、多岐にわたる情報が含まれています。これらをすべてバラバラの変数(userName, userAge, userAddress…)で管理しようとすると、変数の数が膨大になり、どのデータがどのユーザーのものなのか管理しきれなくなってしまいます。

ここで重要になるのが「データをまとめる」という考え方であり、その構造をより現実に即した形にするのが「ネスト(入れ子)」です。ネストとは、あるデータ構造の中に、さらに別のデータ構造が入っている状態を指します。マトリョーシカ人形や、パソコンのフォルダの中にフォルダが入っている状態をイメージすると分かりやすいでしょう。

JavaScriptにおいて、このネストを実現する主役が「オブジェクト」と「配列」です。オブジェクトはキーと値のペアで情報を持ち、配列は順序付きのリストとしてデータを持ちます。これらを組み合わせることで、「住所(オブジェクト)」の中に「都道府県(文字列)」や「市区町村(文字列)」を持たせたり、「ユーザー(オブジェクト)」の中に「趣味リスト(配列)」を持たせたりすることが可能になります。

Web開発の現場、特にフロントエンド開発においては、画面上の見た目を操作するDOM(Document Object Model)自体が巨大なネスト構造(ツリー構造)になっています。また、サーバーサイドとやり取りするデータ形式であるJSONも、オブジェクトと配列が幾重にも重なったネスト構造をしています。つまり、ネストされたデータを自在に読み書きできるスキルは、脱初心者を目指す上で避けては通れない、エンジニアにとっての必須科目なのです。このセクションでは、まず「なぜ複雑な構造が必要なのか」という根本的な動機を理解し、次のステップへ進むための土台を固めていきましょう。

オブジェクトの中にオブジェクトを入れる:基本的なネスト構文

JavaScriptにおけるオブジェクトは、{ key: value } という形式で記述しますが、この value(値)の部分には、数値や文字列だけでなく、別のオブジェクトをそのまま入れることができます。これが「オブジェクトのネスト」です。

例えば、ある人物のプロフィールを表現するコードを考えてみましょう。

const person = {
    name: "太郎",
    age: 25,
    address: {
        city: "東京",
        zipCode: "123-4567"
    }
};

このコードでは、person というオブジェクトの中に address というプロパティがあり、その値としてさらに { city: "東京", ... } というオブジェクトが入っています。これがネストです。

ネストされたデータにアクセスするには、ドット(.)をつなげて記述します。これを「ドット記法」と呼びます。 上記の例で「東京」というデータを取り出したい場合、以下のように記述します。 console.log(person.address.city);

このコードは、「person オブジェクトの中にある address プロパティの中にある city プロパティ」という意味になります。まるで住所を追うように、親から子、子から孫へと階層を掘り下げていくイメージです。

また、ドット記法だけでなく、ブラケット記法([])を使ってアクセスすることも可能です。 console.log(person["address"]["city"]); ブラケット記法は、プロパティ名が変数に入っている場合や、ハイフンなどの特殊文字が含まれる場合に必須となりますが、通常は読みやすいドット記法が好まれます。

ネストは1段階だけでなく、2段階、3段階と深くすることも可能です。しかし、あまりに深くしすぎるとコードが読みづらくなり、データが存在しない場合のエラー(後述します)も起きやすくなるため、適切な深さに設計することが重要です。まずはこの「ドットでつないで奥へ進む」という感覚を指に覚え込ませましょう。

配列とオブジェクトの組み合わせ(1):オブジェクトの中に配列を持つ

実務で扱うデータ構造の中で非常に頻度が高いのが、「オブジェクトのプロパティとして配列を持つ」パターンです。これは、「ある一つのモノ(オブジェクト)が、複数の要素(配列)を持っている」状況を表現するのに適しています。

具体的な例として、ユーザーのプロフィールに「趣味」という項目を追加する場合を考えてみます。趣味は一つとは限らないため、配列で表現するのが自然です。

const person = {
    name: "太郎",
    hobbies: ["読書", "旅行", "ゲーム"]
};

ここでは、person オブジェクトの hobbies プロパティが配列になっています。

このデータから「旅行」という文字を取り出すにはどうすればよいでしょうか。まず、person.hobbies で配列全体にアクセスします。配列の中身を取り出すにはインデックス(添字)が必要ですので、旅行(2番目の要素)を取り出すにはインデックス 1 を指定します。 console.log(person.hobbies); // "旅行"

このように、オブジェクトの操作(ドット記法)と配列の操作(ブラケットとインデックス)を組み合わせることで、複合的なデータにアクセスします。

この構造は、ECサイトの商品データなどでもよく見られます。例えば、「商品(オブジェクト)」の中に「サイズ展開(配列)」や「カラーバリエーション(配列)」、「商品画像URLリスト(配列)」などが含まれているケースです。

const product = {
    id: 101,
    name: "Tシャツ",
    colors: ["白", "黒", "青"],
    sizes: ["S", "M", "L"]
};

このデータ構造を理解しておけば、例えば「この商品の最初のカラーを表示する」といった処理も product.colors とスムーズに記述できるようになります。オブジェクトと配列が混ざると混乱しがちですが、「{} ならドット、[] ならインデックス」という基本ルールに立ち返れば迷うことはありません。

配列とオブジェクトの組み合わせ(2):配列の中にオブジェクトを持つ

前節とは逆に、「配列の要素としてオブジェクトを持つ」パターンも非常に重要です。これは、同じ属性を持つデータが複数存在する場合、つまり「リスト(一覧)」を表現する際によく使われる構造です。

例えば、クラスの名簿や、商品一覧、SNSの投稿一覧などは、すべてこの「オブジェクトの配列」という形をとります。

const users = [
    { id: 1, name: "太郎", age: 25 },
    { id: 2, name: "花子", age: 30 },
    { id: 3, name: "次郎", age: 22 }
];

この users は配列ですので、全体としては [] で囲まれています。そして、その中身(要素)の一つ一つが {} で囲まれたオブジェクトになっています。

このデータから「花子さんの年齢」を取り出したい場合を考えてみましょう。

1. まず、配列の中から花子さんのデータ(2番目の要素)を特定します。インデックスは 1 です。 users

2. これで { id: 2, name: "花子", age: 30 } というオブジェクトが取得できました。

3. 次に、このオブジェクトの age プロパティにアクセスします。 users.age

これで 30 という値が取得できます。

このデータ構造は、Webアプリケーションにおいて最も頻出する形式の一つです。なぜなら、データベースから取得したデータは、基本的に「レコード(行)の集合」であり、それをJavaScriptで表現すると「オブジェクトの配列」になるからです。 例えば、Webページに「ユーザー一覧」を表示する機能を実装する場合、この users 配列をループ処理(for文やforEachメソッド)で回し、各要素(各ユーザーオブジェクト)から name プロパティを取り出してHTMLに埋め込む、という手順を踏みます。このパターンをマスターすることは、動的なWebサイトを作る上で必須のスキルと言えます。

実際のWeb開発におけるJSONデータの構造

ここまで見てきた複雑なデータ構造は、Web開発の現場では「JSON(JavaScript Object Notation)」という形式で頻繁に目にすることになります。JSONは、サーバーとブラウザ間でデータを通信するための標準的なフォーマットですが、その構造はJavaScriptのオブジェクトリテラル(書き方)とほぼ同じです。

Web API(アプリケーション・プログラミング・インターフェース)を利用して外部からデータを取得すると、多くの場合、巨大なネスト構造を持ったJSONデータが返ってきます。 例えば、Qiitaのような技術ブログサービスのAPIから、特定のユーザーの記事一覧を取得する場合を想像してください。返ってくるデータは以下のような構造になっていることが一般的です(簡略化しています)。

[
    {
        "id": "abc12345",
        "title": "JavaScript入門",
        "likes_count": 100,
        "user": {
            "id": "user01",
            "profile_image_url": "https://..."
        },
        "tags": [
            { "name": "JavaScript" },
            { "name": "初心者" }
        ]
    },
    {
        "id": "def67890",
        ...
    }
]

このデータは、「配列(記事リスト)」の中に「オブジェクト(各記事)」があり、その記事オブジェクトの中に「オブジェクト(投稿ユーザー情報)」や「配列(タグリスト)」が含まれ、さらにタグ配列の中に「オブジェクト(タグ詳細)」が入っている、という多重のネスト構造になっています。

JavaScriptで fetch 関数などを使ってこのデータを取得した後、私たちはこの深い森のようなデータ構造の中から、必要な情報(例えば「1つ目の記事の、投稿ユーザーの、プロフィール画像URL」)を的確に取り出さなければなりません。 コードにすると data.user.profile_image_url となります。このように、JSONを理解し操作できるということは、すなわちネストされたオブジェクトと配列を自在に扱えるということと同義なのです。

DOMツリー:ブラウザが持つ巨大なネスト構造

JavaScriptで操作する対象である「DOM(Document Object Model)」もまた、巨大なオブジェクトのネスト構造であることを理解しておく必要があります。Webページが表示されるとき、ブラウザはHTMLを解析して、メモリ上にDOMツリーと呼ばれる構造を構築します。

HTMLタグの親子関係(<html>の中に<body>があり、その中に<div>がある…)は、そのままDOMオブジェクトの親子関係として表現されます。 document というグローバルオブジェクトを頂点として、プロパティを辿っていくことでページ内のあらゆる要素にアクセスできます。

例えば、document.body<body> 要素を表すオブジェクトです。さらに document.body.children とすると、<body> 直下のすべての子要素をリスト(HTMLCollection)として取得できます。 もし、ページの一番最初の <div> の中にあるテキストを変更したい場合、querySelector などの便利なメソッドを使わずにプロパティだけで辿るとすれば、以下のようなイメージになります(実際の構造によります)。 document.body.children.textContent = "こんにちは";

このように、私たちが普段 document.getElementByIdquerySelector で何気なく取得している「要素(Element)」は、実は巨大な document オブジェクトの深い階層にある一部品(ノード)です。 そして、取得した要素自体もまたオブジェクトであり、style プロパティ(オブジェクト)を持っていたり、classList プロパティ(オブジェクトのようなリスト)を持っていたりします。 element.style.color = "red" というコードは、「要素オブジェクトの中の、style オブジェクトの中の、color プロパティに値を代入する」という、まさにネストされたオブジェクトの操作そのものです。DOM操作の実体は、複雑なオブジェクト構造の読み書きに他なりません。

複雑なデータ構造のループ処理と展開

ネストされたデータを扱う際、単に一つのデータを取り出すだけでなく、ループ処理を使ってデータを一覧表示したり、加工したりすることが頻繁にあります。特に「オブジェクトの配列」に対する操作は、配列メソッド(forEach, map, filter など)とオブジェクトのプロパティアクセスを組み合わせる必要があります。

先ほどのユーザー一覧(users 配列)を使って、全員の名前をコンソールに表示する例を見てみましょう。

const users = [
    { id: 1, name: "太郎" },
    { id: 2, name: "花子" }
];

users.forEach((user) => {
    console.log(user.name);
});

forEach メソッドは配列の要素を一つずつ順番に取り出し、コールバック関数の引数(ここでは user)に渡します。この user{ id: 1, name: "太郎" } というオブジェクトそのものです。したがって、関数内で user.name と記述することで、名前を取り出すことができます。

さらに応用的な例として、APIから取得した記事データの中に含まれる「タグ(配列)」を展開する場合を考えます。 記事データが item 変数に入っているとして、その中の tags 配列を処理するには、二重のループ構造になることがあります。

// 記事の配列をループ
items.forEach((item) => {
    console.log(`記事タイトル: ${item.title}`);
    
    // 記事の中にあるタグの配列をループ(ネストされたループ)
    item.tags.forEach((tag) => {
        console.log(`- タグ: ${tag.name}`);
    });
});

このように、データの構造に合わせてループ処理もネストさせることで、階層の深いデータまで漏れなく処理することができます。ただし、ネストが深くなりすぎるとコードの可読性が下がるため、処理を別の関数に切り出すなどの工夫も求められます。

ネストされたデータの変更とイミュータブル(不変性)

オブジェクトや配列の中にさらにオブジェクトが入っている場合、データの変更(更新)には少し注意が必要です。JavaScriptのオブジェクトは「参照渡し」という性質を持っており、変数をコピーしたつもりでも、中身の場所(メモリ上のアドレス)を共有していることがあります。

特にネストされたオブジェクトをコピーする際、浅いコピー(Shallow Copy)と呼ばれる方法(スプレッド構文 ... など)では、一番外側のガワはコピーされますが、中に入っているオブジェクト(ネストされた部分)は参照のまま残ってしまいます。

const original = { name: "A", detail: { age: 20 } };
const copy = { ...original };

copy.detail.age = 30; // コピーの方を変更したつもりでも...
console.log(original.detail.age); // 元のデータの年齢も30に変わってしまう!

これは、detail プロパティが指し示しているオブジェクトの実体が一つしかないためです。

Reactなどのモダンなフレームワークを使った開発では、データの「不変性(イミュータビリティ)」が重要視されます。つまり、元のデータを直接書き換えるのではなく、新しいデータを作り直して置き換えるという手法です。 ネストされたデータを安全に更新するためには、変更したい階層までスプレッド構文を使って丁寧に展開してコピーを作るか、structuredClone などのディープコピー機能を使う必要があります。 初心者にとっては少し難易度が高い概念ですが、「ネストされたオブジェクトの変更は、思わぬ副作用を生む可能性がある」ということだけは頭の片隅に置いておきましょう。

エラー回避のテクニック:オプショナルチェーン

深くネストされたデータにアクセスする際、最も恐ろしいのが「存在しないプロパティにアクセスしてエラーになる」ことです。 例えば、user.address.city を取得しようとしたとき、もし user データの中に address オブジェクトが存在しなかったら(nullundefined だった場合)、JavaScriptは「undefinedに対してcityプロパティを探すことはできない」というエラー(TypeError)を出して、プログラム全体を停止させてしまいます。

Webアプリでは、サーバーからの通信状況やデータの不備によって、あるはずのデータが欠けていることがよくあります。 従来は、これを防ぐために if (user && user.address && user.address.city) のように、一つ一つ存在確認を行う冗長なコードを書く必要がありました。

しかし、現代のJavaScript(ES2020以降)では、「オプショナルチェーン(Optional Chaining)」という非常に便利な機能が導入されました。これは ?. という記号を使います。 console.log(user?.address?.city);

このように記述すると、もし user がなくても、あるいは user.address がなくても、エラーにならずに undefined を返してくれます。「もしあれば、その先へ進む。なければそこでストップしてundefinedを返す」という安全装置のような役割を果たします。 APIから取得した複雑なJSONデータを扱う際には、このオプショナルチェーンを活用することで、エラーによるアプリのクラッシュを防ぎ、堅牢なコードを書くことができます。

まとめ:構造をイメージする力を養う

ここまで、オブジェクトと配列を組み合わせた複雑なデータ構造(ネスト)について解説してきました。 ポイントを振り返りましょう。

1. オブジェクト in オブジェクト: ドット記法で階層を掘り下げる(a.b.c)。

2. 配列 in オブジェクト: プロパティから配列にアクセスし、インデックスで要素を取る(a.b)。

3. オブジェクト in 配列: インデックスでオブジェクトを取り出し、プロパティにアクセスする(a.b)。

4. JSONとDOM: Web開発のデータは基本的にすべてネスト構造である。

プログラミングの上達において重要なのは、コードを見たときに、頭の中でそのデータの「形」をイメージできるかどうかです。user.posts.title というコードを見た瞬間に、「userオブジェクトの中にpostsという配列があって、その最初の投稿オブジェクトのタイトルだな」と構造が浮かぶようになれば、あなたはもう初心者ではありません。

最初は console.log でデータをブラウザのコンソールに出力し、そのツリー構造をクリックして展開しながら、データの形を確認する癖をつけましょう。複雑に見えるデータも、分解してみれば基本的な「オブジェクト(キーと値)」と「配列(リスト)」の組み合わせに過ぎません。この構造を自在に操れるようになったとき、あなたはどんな複雑なWebアプリケーションでも開発できる基礎力を手に入れているはずです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次