6時だョ!!全員集合!!

Rails・JavaScrictを中心にアウトプットします。

2022年3月5日 [ES2015] Map/Set - マップとしてのObjectとMap (JavaScript Primer)

ES2015でMapが導入されるまで、JavaScriptにおいてマップ型を実現するためにObjectが利用されてきました。 ただし、"マップとしてのObject"にはいくつかの問題があります。

  • Objectのprototypeオブジェクトから継承されたプロパティによって、意図しないマッピングを生じる危険性がある
  • プロパティとしてデータを持つため、キーとして使えるのは文字列かSymbolに限られる

マッピングとは →2つの要素を関連付けること

以下はMapオブジェクトのような動きをオブジェクトで表現しています。 prototypeオブジェクトから継承したプロパティと衝突することもわかります。

const map = {};
// マップがキーを持つことを確認する
function has(key) {
    return typeof map[key] !== "undefined";
}
console.log(has("foo")); // => false
// Objectのプロパティが存在する
console.log(has("constructor")); // => true
  • typeofを使う事でプロパティがなかった場合にundefined を返します。
  • hasメソッドにプロパティ名を渡すとそのプロパティが存在するかを真偽値で返します。
  • 存在しないプロパティ名を渡すとundefinedを返します。
  • オブジェクトのプロパティにブラケット記法でアクセスする場合ブラケット記法は文字列リテラルで囲んで渡す必要があります。

衝突に関してはprototypeオブジェクトを継承しないオブジェクトをObject.create(null)で(初期化)作成することで回避出来ます。

上記のprototype内部プロパティに定義されているプロパティと衝突する問題を回避し、上記のようなよく使うであろうメソッドをはじめから定義したMapをES2015からビルトインオブジェクトとして導入されました。

その他のMapを導入したことのメリットが3つあります。

  • マップのサイズを簡単に知ることができる
  • マップが持つ要素を簡単に列挙できる
  • オブジェクトをキーにすると参照ごとに違うマッピングができる

これらのメリットの例としてショッピングカートのコードがありました。

// ショッピングカートを表現するクラス
class ShoppingCart {
    constructor() {
        // 商品とその数を持つマップ
        this.items = new Map();
    }
    // カートに商品を追加する
    addItem(item) {
        // `item`がない場合は`undefined`を返すため、Nullish coalescing演算子(`??`)を使いデフォルト値として`0`を設定する
        const count = this.items.get(item) ?? 0;
        this.items.set(item, count + 1);
    }
    // カート内の合計金額を返す
    getTotalPrice() {
        return Array.from(this.items).reduce((total, [item, count]) => {
            return total + item.price * count;
        }, 0);
    }
    // カートの中身を文字列にして返す
    toString() {
        return Array.from(this.items).map(([item, count]) => {
            return `${item.name}:${count}`;
        }).join(",");
    }
}
const shoppingCart = new ShoppingCart();
// 商品一覧
const shopItems = [
    { name: "みかん", price: 100 },
    { name: "リンゴ", price: 200 },
];

// カートに商品を追加する
shoppingCart.addItem(shopItems[0]);
shoppingCart.addItem(shopItems[0]);
shoppingCart.addItem(shopItems[1]);

// 合計金額を表示する
console.log(shoppingCart.getTotalPrice()); // => 400
// カートの中身を表示する
console.log(shoppingCart.toString()); // => "みかん:2,リンゴ:1"

前提

  • ショッピングカートを表現するShoppingCartクラスを定義しています。
  • コンストラクタ関数内でMapをインスタンス化しthis.itemsに代入します。
  • const shoppingCart変数に代入のところで、ShoppingCartをインスタンス化した時にコンストラクタ関数が一度だけ実行されます。
  • this.itemsはMapの持つメソッドが使えます。
  • shopItems変数に配列で2つのオブジェクトを持たせています。

addItemメソッド

  • 実引数はshopItems変数のインデックス番号で要素を指定しています。
  • Nullish coalescing演算子(??)は左辺がnullundefinedのときに右辺を返します。
  • this.items.get(item)はインスタンス化されたMapのエントリーに対してgetメソッドに引数のキーを指定してバリューを取り出します。
  • 1度目の実行時にcount変数に代入されるのは、エントリーに何もない状態なのでundefinedになるため右辺の0になります。
  • this.items.set(item, count + 1)ではsetメソッドで空のMapのエントリーに要素が追加されます。
  • 2度目の実行時はcount変数に1度目の結果のエントリーのキーをgetメソッドで指定できるため、バリューのcountの1が代入されます。
  • 2度目のsetメソッド実行時はキーが1度目と同じためバリューが更新されます。
  • 3度目はshopItemsのインデックス番号1の要素で実行されます。

getTotalPriceメソッド

  • 前提としてaddItemメソッドの結果値がthis.itemsのエントリーにあります。
  • addItemメソッドの結果値を元に合計金額を返すメソッドです。
  • Array.fromの返り値this.itemsを配列化したもので以下ように出力されます。
[ [ { name: 'みかん', price: 100 }, 2 ], [ { name: 'リンゴ', price: 200 }, 1 ] ]
  • reduceメソッドで第一引数にコールバック、第二引数に初期値の0を設定し実行しています。
  • コールバック関数には第一引数に初期値の0が入り、第二引数にエントリ内のオブジェクト(上のコード参照)とエントリ内の個数が順番に渡されます。
  • オブジェクトのpriceにアクセスし、total+個数と掛け算した結果がtotalに代入され、繰り返します。
  • 繰り返し処理の結果として400が返ります。(みかん100円が2個とりんご200円が1個)

toSrtingメソッド

  • ショッピングカート内を文字列で表現するメソッドです。
  • Array.fromの返り値は先程と同じく以下のようになります。
[ [ { name: 'みかん', price: 100 }, 2 ], [ { name: 'リンゴ', price: 200 }, 1 ] ]
  • mapのコールバック関数には第一引数にエントリ内のオブジェクト(上のコード参照)と第二引数にエントリー内の数値(個数)が順番に渡されます。
  • テンプレートリテラルを使いオブジェクトのnameにアクセスし、カートの商品名とcountである個数を出力します。
  • joinメソッドは配列を連結し、間にjoinの引数に指定した文字を挿入します。
  • 結果的に"みかん:2,リンゴ:1"が返ります。

    参考

    マップとしてのObjectとMap