前回の記事 では、JavaScriptのスコープとクロージャについて、基本的な概念から実践的な活用方法までを扱いました。今回は、JavaScriptのオブジェクトについて、基本的な操作からMap/Setなどの新しいデータ構造までを紹介します。
オブジェクトリテラル
オブジェクトは、関連するデータと機能をひとまとめにする基本的なデータ構造です。JavaScriptでは、オブジェクトリテラル記法を使って簡潔にオブジェクトを作成できます。
基本的なオブジェクトの作成
// 基本的なオブジェクトリテラル
const person = {
name: "田中太郎",
age: 30,
city: "東京"
};
console.log(person.name); // "田中太郎"
console.log(person["age"]); // 30
計算された(動的な)プロパティ名
ES2015以降、プロパティ名を動的に設定できるようになりました。
const key = "favoriteColor";
const value = "青";
// 計算された(動的な)プロパティ名
const user = {
name: "佐藤花子",
[key]: value,
[`${key}Updated`]: new Date()
};
console.log(user.favoriteColor); // "青"
console.log(user.favoriteColorUpdated); // 現在の日時
プロパティの省略記法
変数名とプロパティ名が同じ場合、省略して記述できます。
const name = "山田次郎";
const age = 25;
// 従来の記法
const oldUser = {
name: name,
age: age
};
// 省略記法
const newUser = {
name,
age
};
console.log(newUser); // { name: "山田次郎", age: 25 }
プロパティとメソッド
オブジェクトには、データ(プロパティ)と機能(メソッド)の両方を含めることができます。
メソッドの定義
const calculator = {
result: 0,
// メソッドの定義(ES2015の省略記法)
add(value) {
this.result += value;
return this; // メソッドチェーンを可能にする
},
subtract(value) {
this.result -= value;
return this;
},
getResult() {
return this.result;
},
reset() {
this.result = 0;
return this;
}
};
// メソッドチェーンの使用例
const finalResult = calculator
.add(10)
.subtract(3)
.add(5)
.getResult();
console.log(finalResult); // 12
thisの扱いとアロー関数
オブジェクトのメソッドでは、this
の扱いに注意が必要です。
const counter = {
count: 0,
// 通常の関数(thisはcounterオブジェクトを指す)
increment() {
this.count++;
console.log(`カウント: ${this.count}`);
},
// アロー関数(thisは外側のスコープを参照)
decrement: () => {
// 注意: ここでのthisはcounterオブジェクトではない。
// アロー関数は自身の`this`を持たず、定義された時点のレキシカルスコープの`this`を参照するため。
// この場合、トップレベルスコープの`this`(環境により異なる。例: ブラウザのグローバルオブジェクトやNode.jsのモジュールコンテキスト)を指す。
// そのため、`this.count` は期待どおりに動作しない。
console.log("アロー関数内のthis.count:", this.count); // undefined (またはエラーになることも)
// this.count++; // もし実行すると TypeError になる可能性がある
},
// コールバック内でのthis
delayedIncrement() {
setTimeout(() => {
// アロー関数なので、thisはcounterオブジェクトを指す
this.count++;
console.log(`遅延カウント: ${this.count}`);
}, 1000);
}
};
counter.increment(); // カウント: 1
counter.delayedIncrement(); // 1秒後に遅延カウント: 2
オブジェクトの分割代入
オブジェクトから特定のプロパティを効率的に取り出すことができます。
基本的な分割代入
const user = {
name: "鈴木一郎",
age: 28,
email: "suzuki@example.com",
address: {
city: "大阪",
zip: "550-0001"
}
};
// 基本的な分割代入
const { name, age } = user;
console.log(name); // "鈴木一郎"
console.log(age); // 28
// 変数名を変更
const { email: userEmail } = user;
console.log(userEmail); // "suzuki@example.com"
// デフォルト値の設定
const { phone = "未設定" } = user;
console.log(phone); // "未設定"
ネストされたオブジェクトの分割代入
const user = {
name: "鈴木一郎",
age: 28,
email: "suzuki@example.com",
address: {
city: "大阪",
zip: "550-0001"
}
};
// ネストされたオブジェクトの分割代入
const { address: { city, zip } } = user;
console.log(city); // "大阪"
console.log(zip); // "550-0001"
// より複雑な例
const company = {
name: "株式会社Example",
employees: {
ceo: {
name: "社長太郎",
age: 50
},
cto: {
name: "技術花子",
age: 42
}
}
};
const {
employees: {
ceo: { name: ceoName },
cto: { name: ctoName, age: ctoAge }
}
} = company;
console.log(ceoName); // "社長太郎"
console.log(ctoName); // "技術花子"
console.log(ctoAge); // 42
関数引数での分割代入
// 関数引数での分割代入
function createUser({ name, age, email = "未設定" }) {
return {
id: Math.random().toString(36),
name,
age,
email,
createdAt: new Date()
};
}
const newUser = createUser({
name: "新規ユーザー",
age: 22
});
console.log(newUser);
// { id: "...", name: "新規ユーザー", age: 22, email: "未設定", createdAt: ... }
オブジェクトのスプレッド構文
オブジェクトのコピーやマージが簡潔に行なえます。
オブジェクトのコピー
const original = {
name: "オリジナル",
age: 30,
hobbies: ["読書", "映画鑑賞"]
};
// コピー
const copy = { ...original };
copy.name = "コピー";
console.log(original.name); // "オリジナル"
console.log(copy.name); // "コピー"
// 注意: ネストされたオブジェクトは参照がコピーされる
copy.hobbies.push("料理");
console.log(original.hobbies); // ["読書", "映画鑑賞", "料理"]
オブジェクトのマージ
const defaults = {
theme: "light",
language: "ja",
notifications: true
};
const userPreferences = {
theme: "dark",
fontSize: "large"
};
// オブジェクトのマージ
const config = {
...defaults,
...userPreferences
};
console.log(config);
// {
// theme: "dark", // userPreferencesで上書き
// language: "ja",
// notifications: true,
// fontSize: "large"
// }
条件つきプロパティ
function createAPIRequest(url, options = {}) {
const baseHeaders = { // 基本ヘッダーを定義
"Content-Type": "application/json"
};
const request = {
url,
method: "GET",
headers: baseHeaders, // 基本ヘッダーをまず設定
// 条件つきでプロパティをマージして上書き
...(options.auth && {
headers: { // headersプロパティ全体を新しいオブジェクトで上書き
...baseHeaders, // 基本ヘッダーを展開
Authorization: `Bearer ${options.auth}`
}
}),
...(options.timeout && { timeout: options.timeout }),
...(options.data && { body: JSON.stringify(options.data) })
};
return request;
}
const requestWithAuth = createAPIRequest("/api/users", {
auth: "abc123",
timeout: 5000
});
console.log(requestWithAuth);
// {
// url: '/api/users',
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// Authorization: 'Bearer abc123'
// },
// timeout: 5000
// }
Map/Set/WeakMap/WeakSet
従来のオブジェクトに加えて、特定の用途に最適化されたデータ構造が利用できます。
Map - キーと値のマッピング
// Mapの基本的な使い方
const userMap = new Map();
// 任意の型をキーにできる
userMap.set("string-key", "文字列キー");
userMap.set(123, "数値キー");
userMap.set(true, "真偽値キー");
const objKey = { id: 1 };
userMap.set(objKey, "オブジェクトキー");
console.log(userMap.get("string-key")); // "文字列キー"
console.log(userMap.get(123)); // "数値キー"
console.log(userMap.get(objKey)); // "オブジェクトキー"
// サイズの取得
console.log(userMap.size); // 4
// 存在チェック
console.log(userMap.has("string-key")); // true
// 反復処理
for (const [key, value] of userMap) {
console.log(`${key}: ${value}`);
}
Mapとオブジェクトの比較
// オブジェクトの場合
const objData = {};
objData["key1"] = "value1";
objData[123] = "value2"; // 数値キーは文字列に変換される
console.log(Object.keys(objData)); // ["123", "key1"]
// Mapの場合
const mapData = new Map();
mapData.set("key1", "value1");
mapData.set(123, "value2"); // 数値キーはそのまま保持される
console.log([...mapData.keys()]); // ["key1", 123]
// パフォーマンスの違い
// Map: 頻繁な追加・削除に最適化
// Object: プロパティアクセスが高速
Set - 一意な値のコレクション
// Setの基本的な使い方
const uniqueNumbers = new Set();
uniqueNumbers.add(1);
uniqueNumbers.add(2);
uniqueNumbers.add(2); // 重複は無視される
uniqueNumbers.add(3);
console.log(uniqueNumbers.size); // 3
console.log(uniqueNumbers.has(2)); // true
// 配列から重複を除去
const numbers = [1, 2, 2, 3, 3, 4, 5, 5];
const uniqueArray = [...new Set(numbers)];
console.log(uniqueArray); // [1, 2, 3, 4, 5]
// 反復処理
for (const value of uniqueNumbers) {
console.log(value);
}
WeakMapとWeakSet
// WeakMap - オブジェクトキーのみ、ガベージコレクション対応
const weakMap = new WeakMap();
let obj1 = { id: 1 };
let obj2 = { id: 2 };
weakMap.set(obj1, "データ1");
weakMap.set(obj2, "データ2");
console.log(weakMap.get(obj1)); // "データ1"
// オブジェクトへの参照がなくなると自動的に削除される
obj1 = null; // weakMapからも自動的に削除される
// WeakSet - オブジェクトの一意コレクション
const weakSet = new WeakSet();
const element1 = { name: "要素1" };
const element2 = { name: "要素2" };
weakSet.add(element1);
weakSet.add(element2);
console.log(weakSet.has(element1)); // true
実践的なコード例
1. 設定オブジェクトの管理
class ConfigManager {
constructor(defaultConfig = {}) {
this.config = new Map();
this.defaults = new Map();
// デフォルト設定をセット
for (const [key, value] of Object.entries(defaultConfig)) {
this.defaults.set(key, value);
this.config.set(key, value);
}
}
get(key) {
return this.config.get(key);
}
set(key, value) {
this.config.set(key, value);
return this;
}
reset(key) {
if (key) {
this.config.set(key, this.defaults.get(key));
} else {
// 全てリセット
for (const [key, value] of this.defaults) {
this.config.set(key, value);
}
}
return this;
}
getAll() {
return Object.fromEntries(this.config);
}
}
// 使用例
const appConfig = new ConfigManager({
theme: "light",
language: "ja",
autoSave: true,
maxRetries: 3
});
appConfig.set("theme", "dark").set("maxRetries", 5);
console.log(appConfig.getAll());
// { theme: "dark", language: "ja", autoSave: true, maxRetries: 5 }
2. データの変換とフィルタリング
// 複雑なデータ構造の変換
const rawUserData = [
{
id: 1,
name: "田中太郎",
profile: { age: 30, department: "開発", active: true }
},
{
id: 2,
name: "佐藤花子",
profile: { age: 28, department: "デザイン", active: true }
},
{
id: 3,
name: "鈴木一郎",
profile: { age: 35, department: "開発", active: false }
}
];
// アクティブなユーザーのみを抽出し、構造を変換
const activeUsers = rawUserData
.filter(user => user.profile.active)
.map(({ id, name, profile: { age, department } }) => ({
id,
name,
age,
department,
displayName: `${name}(${department})`
}));
console.log(activeUsers);
// [
// { id: 1, name: "田中太郎", age: 30, department: "開発", displayName: "田中太郎(開発)" },
// { id: 2, name: "佐藤花子", age: 28, department: "デザイン", displayName: "佐藤花子(デザイン)" }
// ]
// 部署別グループ化
const usersByDepartment = rawUserData
.filter(user => user.profile.active)
.reduce((acc, user) => {
const { department } = user.profile;
if (!acc.has(department)) {
acc.set(department, []);
}
acc.get(department).push(user);
return acc;
}, new Map());
console.log(Object.fromEntries(usersByDepartment));
// {
// "開発": [
// { id: 1, name: "田中太郎", profile: { age: 30, department: "開発", active: true } }
// ],
// "デザイン": [
// { id: 2, name: "佐藤花子", profile: { age: 28, department: "デザイン", active: true } }
// ]
// }
3. キャッシュシステム
class LRUCache {
constructor(maxSize = 10) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
// 最近使用されたアイテムを最後に移動
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return null;
}
set(key, value) {
if (this.cache.has(key)) {
// 既存のキーは削除してから再追加
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
// 最も古いアイテムを削除
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
}
// 使用例
const cache = new LRUCache(3);
cache.set("user:1", { name: "ユーザー1" });
cache.set("user:2", { name: "ユーザー2" });
cache.set("user:3", { name: "ユーザー3" });
cache.set("user:4", { name: "ユーザー4" }); // user:1が削除される
console.log(cache.get("user:1")); // null
console.log(cache.get("user:2")); // { name: "ユーザー2" }
復習
基本問題
1. 以下のオブジェクトから分割代入を使って値を取り出す
const product = {
id: 123,
name: "ノートパソコン",
price: 98000,
specs: {
cpu: "Intel Core i7",
memory: "16GB",
storage: "512GB SSD"
},
tags: ["laptop", "intel", "business"]
};
name
、price
、specs
内のcpu
とmemory
、そしてtags
の最初の要素を取り出してください。
2. スプレッド構文を使って以下の操作を行なう
const baseConfig = {
host: "localhost",
port: 3000,
secure: false
};
const productionConfig = {
host: "api.example.com",
secure: true,
timeout: 5000
};
baseConfig
をベースに、productionConfig
の値で上書きした新しいオブジェクトを作成してください。
3. Mapを使って以下の機能を実装する
文字列をキーとして、アクセス回数をカウントするオブジェクトを作成してください。increment(key)
、getCount(key)
、reset(key)
メソッドを持つようにしてください。
実践問題
4. 以下のデータを変換する
const orders = [
{ id: 1, customerId: 101, items: [{ name: "商品A", price: 1000 }, { name: "商品B", price: 1500 }] },
{ id: 2, customerId: 102, items: [{ name: "商品C", price: 2000 }] },
{ id: 3, customerId: 101, items: [{ name: "商品D", price: 800 }, { name: "商品E", price: 1200 }] }
];
顧客ID別に、その顧客の注文総額を計算したMapオブジェクトを作成してください。
5. Setを使った重複チェック機能を実装する
配列から重複する要素を見つけて、重複している要素のSetを返す関数findDuplicates
を作成してください。
解答例
問題1
const {
name,
price,
specs: { cpu, memory },
tags: [firstTag]
} = product;
console.log(name); // "ノートパソコン"
console.log(price); // 98000
console.log(cpu); // "Intel Core i7"
console.log(memory); // "16GB"
console.log(firstTag); // "laptop"
問題2
const mergedConfig = {
...baseConfig,
...productionConfig
};
console.log(mergedConfig);
// {
// host: "api.example.com",
// port: 3000,
// secure: true,
// timeout: 5000
// }
問題3
class AccessCounter {
constructor() {
this.counts = new Map();
}
increment(key) {
const currentCount = this.counts.get(key) || 0;
this.counts.set(key, currentCount + 1);
return this;
}
getCount(key) {
return this.counts.get(key) || 0;
}
reset(key) {
if (key) {
this.counts.delete(key);
} else {
this.counts.clear();
}
return this;
}
}
// 使用例
const counter = new AccessCounter();
counter.increment("page1").increment("page1").increment("page2");
console.log(counter.getCount("page1")); // 2
console.log(counter.getCount("page2")); // 1
問題4
const customerTotals = orders.reduce((totals, order) => {
const { customerId, items } = order;
const orderTotal = items.reduce((sum, item) => sum + item.price, 0);
const currentTotal = totals.get(customerId) || 0;
totals.set(customerId, currentTotal + orderTotal);
return totals;
}, new Map());
console.log(customerTotals);
// Map(2) { 101 => 4500, 102 => 2000 }
// オブジェクト形式で確認
console.log(Object.fromEntries(customerTotals));
// { 101: 4500, 102: 2000 }
問題5
function findDuplicates(array) {
const seen = new Set();
const duplicates = new Set();
for (const item of array) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}
return duplicates;
}
// 使用例
const testArray = [1, 2, 3, 2, 4, 5, 3, 6, 1];
const duplicates = findDuplicates(testArray);
console.log([...duplicates]); // [2, 3, 1]
まとめ
JavaScriptのオブジェクト操作について、基本的な概念から実践的な活用方法まで紹介しました。
技術的メリット
-
データの構造化
オブジェクトリテラルにより、関連するデータを整理して管理できる -
柔軟性の向上
分割代入やスプレッド構文により、データの取り出しや変換が簡潔になる -
型安全性の確保
Map/Setなどの専用データ構造により、目的に応じた最適な実装が可能 -
パフォーマンスの最適化
適切なデータ構造の選択により、アプリケーションの性能が向上する
実践的な活用場面
-
設定管理
アプリケーションの設定値を階層的に管理 -
データ変換
API レスポンスをアプリケーション内で使いやすい形式に変換 -
キャッシュシステム
Map/WeakMapを使った効率的なデータキャッシュ -
状態管理
アプリケーションの状態を構造化して管理
注意すべきポイント
オブジェクトの操作では、オブジェクト渡しとコピーの違いを理解することが重要です。スプレッド構文によるコピーは、ネストされたオブジェクトでは参照が共有されることに注意が必要です。
また、Map/Setなどの新しいデータ構造は、従来のオブジェクトや配列では実現しにくい機能を提供しますが、用途に応じて適切に選択することが重要です。頻繁な追加・削除が必要な場合はMap、一意性の保証が必要な場合はSetというように、特性を理解して使い分けましょう。
次回 は、ES2015で導入されたクラス構文とオブジェクト指向プログラミングについて、従来のプロトタイプベースのアプローチとの違いを含めて紹介します。