「プロトタイプベースのコードが読みにくい…」 「privateな変数ってどう実装するべき?」
最近のJavaScriptプロジェクトでは当たり前のように使われているクラス構文。しかし、this
の束縛の問題や、プライベート変数の扱いなど、使いこなせていない部分もあるのではないでしょうか。
この記事では、現場でよく遭遇する「困った」シチュエーションに焦点を当て、実践的な解決方法をお伝えします。特にAPIクライアントの実装やパターン設計でよく使うテクニックを中心に、明日から使える知識を解説していきます。
実践的なユースケースと解決策
基本的な実装パターン
プロトタイプベースの実装とクラス構文の比較から見ていきましょう:
javascript
// 従来のプロトタイプベースの実装
function User(name, email) {
this.name = name;
this.email = email;
}
User.prototype.getName = function() {
return this.name;
};
// クラス構文による実装
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
// getter/setterの活用
get displayName() {
return `${this.name} <${this.email}>`;
}
set displayName(value) {
[this.name, this.email] = value.split(' ');
}
}
privateフィールドの実装
クラス内部でのみ使用する変数を適切に隠蔽する方法を見ていきます:
javascript
class UserService {
// プライベートフィールド(新しい構文)
#apiKey;
#baseUrl;
constructor(apiKey) {
this.#apiKey = apiKey;
this.#baseUrl = 'https://api.example.com';
}
async fetchUser(userId) {
const response = await fetch(
`${this.#baseUrl}/users/${userId}`,
{
headers: {
'Authorization': `Bearer ${this.#apiKey}`
}
}
);
return response.json();
}
}
APIクライアントの実装例
実際のプロジェクトでよく使用する、APIクライアントクラスの実装パターンです:
javascript
class APIClient {
static #instance;
#retryCount = 3;
constructor(config = {}) {
if (APIClient.#instance) {
return APIClient.#instance;
}
this.baseURL = config.baseURL || 'https://api.example.com';
this.timeout = config.timeout || 5000;
APIClient.#instance = this;
}
// シングルトンパターンの実装
static getInstance(config) {
if (!APIClient.#instance) {
new APIClient(config);
}
return APIClient.#instance;
}
async get(endpoint, options = {}) {
return this.#request('GET', endpoint, options);
}
async post(endpoint, data, options = {}) {
return this.#request('POST', endpoint, {
...options,
body: JSON.stringify(data)
});
}
// プライベートメソッドでリクエスト処理を実装
async #request(method, endpoint, options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, this.timeout);
try {
const response = await fetch(
`${this.baseURL}${endpoint}`,
{
method,
headers: {
'Content-Type': 'application/json',
...options.headers
},
signal: controller.signal,
...options
}
);
if (!response.ok) {
throw new APIError(
'Request failed',
response.status
);
}
return response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new APIError('Request timeout');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
}
// 使用例
const api = APIClient.getInstance({
baseURL: 'https://api.myapp.com',
timeout: 10000
});
const user = await api.get('/users/1');
継承パターン
基底クラスを継承して機能を拡張する例を見てみましょう:
javascript
// 基底クラス
class BaseError extends Error {
constructor(message, statusCode) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
}
toJSON() {
return {
name: this.name,
message: this.message,
statusCode: this.statusCode
};
}
}
// 具体的なエラークラス
class APIError extends BaseError {
constructor(message, statusCode = 500) {
super(message, statusCode);
this.timestamp = new Date();
}
logError() {
console.error(
`[${this.timestamp.toISOString()}] ${this.message}`
);
}
}
よくある落とし穴と対処法
thisの束縛問題
イベントハンドラやコールバックでのthisの束縛には特に注意が必要です:
javascript
class UserList {
constructor() {
this.users = [];
// BAD: thisが失われる
document.getElementById('loadButton')
.addEventListener('click', function() {
this.loadUsers(); // thisがundefined
});
// GOOD: アロー関数を使用
document.getElementById('loadButton')
.addEventListener('click', () => {
this.loadUsers(); // thisが正しく束縛される
});
}
loadUsers() {
// ユーザー読み込み処理
}
}
メモリリークの防止
特にシングルトンパターンを使用する場合、メモリリークに注意が必要です:
javascript
class EventManager {
constructor() {
this.handlers = new Map();
}
addEventListener(event, handler) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event).add(handler);
}
removeEventListener(event, handler) {
const handlers = this.handlers.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.handlers.delete(event);
}
}
}
// クリーンアップメソッドの提供
destroy() {
this.handlers.clear();
}
}
実務でのベストプラクティス
TypeScriptでの型定義
TypeScriptを使用する場合、より堅牢なクラス設計が可能です:
typescript
interface APIConfig {
baseURL: string;
timeout?: number;
headers?: Record<string, string>;
}
class APIClient {
private static instance: APIClient;
private readonly config: Required<APIConfig>;
private constructor(config: APIConfig) {
this.config = {
timeout: 5000,
headers: {},
...config
};
}
static getInstance(config: APIConfig): APIClient {
if (!APIClient.instance) {
APIClient.instance = new APIClient(config);
}
return APIClient.instance;
}
}
レビュー時のチェックポイント
- 設計の一貫性
- 責任の範囲は適切か
- インターフェースは明確か
- 命名規則は統一されているか
- メモリ管理
- リソースは適切に解放されているか
- 循環参照はないか
- エラー処理
- 例外は適切にハンドリングされているか
- エラー情報は十分か
クラス構文は、オブジェクト指向プログラミングの考え方をJavaScriptで実現する強力な機能です。適切に使用することで、保守性の高いコードを書くことができます。本記事で紹介したパターンを参考に、プロジェクトに適した使い方を見つけてください。