Map Objectは JavaScriptで連想配列を実現するための機能です。
今まではObjectを連想配列として使っていたわけですが、ES6から、専用のクラスが用意されるようになりました。
今まであまり使ってこなかったですが、せっかくなので、よく使う表現をメモしておきます
使い方はざっと以下のような感じ、
const user1: User = {
userId: 1,
userName: "user1"
}
const user2: User = {
userId: 2,
userName: "user2"
}
const userMap = new Map<number, User>() // Map Objectを作成。キーとバリューになる型をジェネリックを使って定義できる。また、定義しなくても型推論してくれる
userMap.set(user1.userId, user1) // UserオブジェクトをMapに登録
userMap.set(user2.userId, user2)
userMap.get(1) // userIdが1のuserを取得
userMap.delete(2) // userIdが2のuserを削除
基本
上で紹介したもの以外に以下のようなメソッドが用意されています。
.has(key)
そのキーのメンバが存在するかどうかをチェックします。
.entries()
/ .keys()
/ .values()
それぞれ、Mapのメンバ、キー、バリューのイテレータを返します。
単体で使うことは少ないですが、後述する配列への変換処理だったり、マージ処理するときに活躍します。
.forEach()
Arrayで使えるforEachと同じような感じですね。Mapオブジェクトのメンバーを回しながら順番に処理してくれます。
userMap.forEach((val,key,self)=>{
console.log(key, val,self)
})
.size
Arrayで言うところの.length
ですね。Map Objectに入っている要素数を返してくれます。
userMap.size // => 2
配列への変換
Map Objectは便利なのですが、いかんせん繰り返し処理に使えるメソッドがforEach
しかなく、map()
やfilter()
reduce()
などが使えるArrayが恋しくなるときが多々あります。
以下のように書けば、Map ObjectをArrayに変換することができます。
const userArray = [...userMap.values()] // => [user1, user2 ....]
MDNのサイトで紹介されているのは以下のような方法ですね。
こちらでも同様です
const userArray = Array.from(userMap.values()) // => [user1, user2 ....]
もしもkeyだけの配列がほしいのであれば以下のような感じです
const userArray = [...userMap.keys()] // => [1, 2 ....]
配列からMap Objectへの変換
なにか処理をするためにArrayに変換するのであれば、処理が終わったあとはもう一度 Map Objectに戻したくなるでしょう。
もしくはAPIから取得した値は配列で帰ってくるかもしれません。
その場合は以下のように書くことでMap Objectに変換することができます。
Map Object → Arrayのときよりも若干面倒ですね
const userMap = new Map(userArray.map(user => [user.userId, user]))
// => Map<number,User>へ変換
Map Object同士をマージする
配列ではなくMap Objectを使う主な理由は、キーを使った値へのアクセスが容易(かつ高速)なことに加えて、常にキーが一意になるようにObjectの塊を取り扱うことができることです。
例えば、同じユーザーIDを持つUserが存在する可能性のある2つの配列(usersArray1
とusersArray2
)を一つに結合して、ユーザーIDが一意になるように重複を取り除く事を考えてみましょう。
配列に格納されている値がプリミティブな値であれば、Set
を用いて一意な配列を作る事はできます。
しかし今回扱いたいObjectの場合は、以下のようにうまく一意になってくれません。
const user2: User = {
userId: 2,
userName: "user2"
}
const sameUser2: User = {
userId: 2,
userName: "user2"
}
const duplicateUserArray = [user2, sameUser2]
// user2とsameUser2は、全く同じメンバーを持っているが、インスタンスが違うので、重複しているとはみなされない
console.log([...(new Set(duplicateUserArray))])
// => [ { userId: 2, userName: 'user2' }, { userId: 2, userName: 'user2' } ]
しかし、こんな時もMap Objectであれば解決です。以下のように書くことで、キーが重複した値を除去しつつ、2つのMap Objectをマージすることができます。
重複したキーが有る場合は、後に指定したMap(この場合はuserMap2
)が優先されることに注意してください。
// userMap1とuserMap2をマージ
const mergeUserMap = new Map([...userMap1.entries(),...userMap2.entries()])
const userMap1 = new Map() // Map Objectを作成
userMap1.set(user1.userId, user1) // user1を追加
userMap1.set(user2.userId, user2) // user2を追加
const userMap2 = new Map() // Map Objectを作成
userMap2.set(user1.userId, user1) // user1を追加
userMap2.set(user3.userId, user3) // user3を追加
// userMap1とuserMap2をマージ
const mergeUserMap = new Map([...userMap1.entries(),...userMap2.entries()])
// => Map {
// 1 => { userId: 1, userName: 'user1' },
// 2 => { userId: 2, userName: 'user2' },
// 3 => { userId: 3, userName: 'user3' } }
Objectで言うところのObject.assign()
と同じような動きですね
// userObjectとuserObject2をマージ
const mergeUserObject = Object.assign(userObject1, userObject2)
キーの差し替え
あまり頻度は多くないですが、Map Objectに設定したキーを別の値に置き換えたMap Objectを作りたくなるときがあります。
そんなときは以下のようにかけます。
// キーをuserIdからuserNameに変更
const nameMap = new Map([...userMap.values()].map(user => [user.userName, user]))
一度配列に変換してからMapに戻すというめんどくさいことをやっているのであまり良くないですね。ちなみに一意じゃない値をキーに設定すると、例によって重複削除されてしまうので注意してください。
オブジェクトへの変換
2019/05/24追記: コメントで、より良い書き方を頂きました。感謝!
Object.fromEntries()
を使う方法です。今はStage4の状態みたいですが、IE、Edge,Node.js以外の主要な環境では使えるみたいです。
TypeScript側での対応はまだみたいですが、2019/05/24時点でmasterにマージされているようなので、次のリリースでは使えるようになるのではないかと思います。
const userObject = Object.fromEntries(userMap) // userMapをuserObjectに変換
以前のやり方。`Object.fromEntries()`が使えるなら読む必要はない
JSON.stringify()
したいときなど、どうしてもObjectにしたいときがあります。
そういうときは以下のコードでオブジェクトに変換することができます。
本当は一文で書きたかったんですが、難しくなりすぎるのでやめました。
const userObject:any = {}
userMap.forEach((u,uId)=>{
userObject[uId] = u
})
いっそ関数化してしまっても良いかもしれません。
/**
* Map => Objectに変換する
*/
function mapToObject(map: Map<any,any>):any{
const object: any = {}
map.forEach((val, key) => {
userObject[key] = val
})
return object
}
オブジェクトからMap Objectへの変換
2019/05/24追記: コメントでより良い書き方を頂きました。感謝!
Object.entries()
を使うことでObject→Map Objectへ変換することができます。
const userMap = new Map(Object.entries(userObject)); // userObjectをuserMapに変換
ただし Symbol のキーは処理の過程で除かれてしまうので、それも含めるなら、以下のように書きます
const userMap = new Map()
for (const key of [...Object.keys(userObject), ...Object.getOwnPropertySymbols(userObject)]) {
userMap.set(key, userObject[key])
}
以上、
後半考えると、別にObjectでもいいのでは?とも思っちゃいますね。ただ、配列→オブジェクトへの変換はMapのほうが楽かなという印象があります。全体的に、Array FriendlyなObjectって印象ですね。
Object ⇔ Map Objectの変換も簡単にかけるので、連想配列にMap Objectを使わない理由はなさそうです。
これからはもっと使っていきたいと思います。
もっとこんな便利な書き方あるよって人がいれば教えてください