ECMAScript 6のReflect API: 機能と使用方法の詳細解説

Reflect APIの概要

Reflect APIは、ECMAScript 6(ES6)で導入された組み込みオブジェクトであり、オブジェクト操作のための一貫性のあるインターフェースを提供します。これらのインターフェースはProxyハンドラのメソッドと対応しており、JavaScriptエンジン内部でのみ実装可能だった元操作を直接呼び出すことができます。

Reflect APIの主な設計目標

  • 統一されたオブジェクト操作APIを提供し、Objectの一部の静的メソッドを置き換える。
  • オブジェクト操作の結果をより合理的に返す(例:操作成功/失敗をブーリアン値で表す)。
  • Proxy APIと補完し合うことで、プロキシの実装を簡素化する。
  • 命令的な代わりに関数的な方法でオブジェクトを操作する。

Reflect APIと従来のオブジェクト操作の比較

ES6以前では、JavaScript開発者は通常、Objectオブジェクトの静的メソッドや直接操作子を使用してオブジェクトのメタデータを処理していました。Reflect APIは、これらよりも一貫性があり、強力なオブジェクト操作を可能にします。

従来の方法の制限


const obj = { a: 1 };
'property' in obj; // in演算子を使用
delete obj.property; // delete演算子を使用

// 返り値が一貫していない
Object.defineProperty(obj, 'b', { value: 2 }); // オブジェクト自体を返す
Object.getOwnPropertyDescriptor(obj, 'b'); // 記述子オブジェクトを返す

// 関数型プログラミングに適していない
const hasProperty = (obj, prop) => prop in obj; // 関数として呼び出すにはラッピングが必要

Reflect APIによる改善


const obj = { a: 1 };

// すべて関数呼び出しで統一
Reflect.has(obj, 'property'); // in演算子の代替
Reflect.deleteProperty(obj, 'property'); // delete演算子の代替

// 操作結果が明確
Reflect.defineProperty(obj, 'b', { value: 2 }); // 成功/失敗をブーリアン値で返す
Reflect.getOwnPropertyDescriptor(obj, 'b'); // Objectメソッドと同じ動作

// 関数型プログラミングに適している
const hasProperty = Reflect.has; // 直接関数として渡せる
[obj, 'a'].reduce((o, p) => Reflect.get(o, p)); // 関数型の組み合わせに適している

主要なメソッドの詳細

Reflect APIは13個の主要なメソッドを提供し、それらはProxyの13個のトラップに対応しています。

属性操作メソッド

Reflect.get(target, propertyKey [, receiver])

オブジェクトの属性値を取得します。`target[propertyKey]`と同等ですが、任意の`receiver`を`this`コンテキストとして使用できます。


const obj = {
  a: 1,
  get b() {
    return this.a + 1;
  }
};

Reflect.get(obj, 'a'); // 1
Reflect.get(obj, 'b'); // 2

// receiverを使用してthisの指向を変更
Reflect.get(obj, 'b', { a: 10 }); // 11

Reflect.set(target, propertyKey, value [, receiver])

オブジェクトの属性値を設定し、操作の成功/失敗をブーリアン値で返します。


const obj = { a: 1 };

Reflect.set(obj, 'a', 2); // true
obj.a; // 2

// 書き込み不可の属性への操作は失敗
const frozenObj = Object.freeze({ a: 1 });
Reflect.set(frozenObj, 'a', 2); // false (厳格モードではエラー)

定義と記述子メソッド

Reflect.defineProperty(target, propertyKey, attributes)

オブジェクトの属性を定義します。`Object.defineProperty`と同様ですが、成功/失敗をブーリアン値で返します。


const obj = {};

// 属性の定義に成功
Reflect.defineProperty(obj, 'a', { value: 1, writable: true }); // true
obj.a; // 1

// 既存の非構成可能な属性を再定義すると失敗
Reflect.defineProperty(obj, 'a', { value: 2, configurable: false }); // true
Reflect.defineProperty(obj, 'a', { value: 3, configurable: true }); // false

Reflect.getOwnPropertyDescriptor(target, propertyKey)

属性記述子を取得します。`Object.getOwnPropertyDescriptor`と同じ動作です。


const obj = { a: 1 };
Reflect.defineProperty(obj, 'b', { value: 2, writable: false });

Reflect.getOwnPropertyDescriptor(obj, 'a');
// { value: 1, writable: true, enumerable: true, configurable: true }

Reflect.getOwnPropertyDescriptor(obj, 'b');
// { value: 2, writable: false, enumerable: false, configurable: false }

プロトタイプ操作メソッド

Reflect.getPrototypeOf(target) と Reflect.setPrototypeOf(target, prototype)

オブジェクトのプロトタイプを取得および設定します。`Object.getPrototypeOf`と`Object.setPrototypeOf`の代替です。


const obj = {};
const proto = { a: 1 };

// プロトタイプの設定
Reflect.setPrototypeOf(obj, proto); // true
Reflect.getPrototypeOf(obj) === proto; // true

// 拡張不能なオブジェクトのプロトタイプを設定しようとすると失敗
Object.preventExtensions(obj);
Reflect.setPrototypeOf(obj, {}); // false

その他の常用メソッド

Reflect.apply(target, thisArgument, argumentsList)

関数を呼び出します。`Function.prototype.apply`の代替です。


const sum = (a, b) => a + b;

// 伝統的な方法
sum.apply(null, [1, 2]); // 3

// Reflect.applyを使用
Reflect.apply(sum, null, [1, 2]); // 3

Reflect.construct(target, argumentsList [, newTarget])

コンストラクタのインスタンスを作成します。`new`演算子の代替です。


class Person {
  constructor(name) {
    this.name = name;
  }
}

// 伝統的な方法
new Person('Alice'); // Person { name: 'Alice' }

// Reflect.constructを使用
Reflect.construct(Person, ['Bob']); // Person { name: 'Bob' }

ReflectとProxyの協調

Reflect APIとProxy APIは互いに補完するように設計されており、多くのProxyトラップは対応するReflectメソッドを呼び出すことでデフォルトの動作を実現し、カスタムロジックを追加することができます。

ログ記録プロキシの実装


function createLoggingProxy(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      console.log(`属性取得: ${prop}`);
      return Reflect.get(target, prop, receiver); // デフォルトの動作を呼び出す
    },
    set(target, prop, value, receiver) {
      console.log(`属性設定: ${prop} = ${value}`);
      return Reflect.set(target, prop, value, receiver); // デフォルトの動作を呼び出す
    },
    deleteProperty(target, prop) {
      console.log(`属性削除: ${prop}`);
      return Reflect.deleteProperty(target, prop); // デフォルトの動作を呼び出す
    }
  });
}

const user = createLoggingProxy({ name: 'Alice', age: 30 });
user.name; // "属性取得: name"と表示し、"Alice"を返す
user.age = 31; // "属性設定: age = 31"と表示し、trueを返す
delete user.age; // "属性削除: age"と表示し、trueを返す

データ検証プロキシの実装


function createValidatedObject(target, validators) {
  return new Proxy(target, {
    set(target, prop, value) {
      // 対象の属性に対する検証器があるか確認
      if (validators.hasOwnProperty(prop)) {
        const isValid = validators[prop](value);
        if (!isValid) {
          console.error(`属性${prop}の検証に失敗: ${value}`);
          return false; // 検証失敗、falseを返す
        }
      }
      // 検証通過、デフォルトの設定動作を呼び出す
      return Reflect.set(target, prop, value);
    }
  });
}

// 検証器付きのオブジェクトを作成
const user = createValidatedObject({}, {
  name: (v) => typeof v === 'string' && v.length > 0,
  age: (v) => typeof v === 'number' && v >= 0 && v <= 150
});

user.name = 'Bob'; // 設定成功
user.age = 200; // エラーメッセージを表示し、falseを返し、設定失敗

実際の使用例

安全なオブジェクト属性アクセス


function safeGet(obj, path, defaultValue) {
  return path.split('.').reduce((current, key) => {
    if (current === null || current === undefined) return defaultValue;
    return Reflect.get(current, key) !== undefined 
      ? Reflect.get(current, key) 
      : defaultValue;
  }, obj);
}

// 使用例
const data = { user: { name: 'Alice', address: { city: 'Beijing' } } };

safeGet(data, 'user.name'); // 'Alice'
safeGet(data, 'user.age', 30); // 30 (デフォルト値)
safeGet(data, 'product.price', 0); // 0 (デフォルト値)

動的なメソッド呼び出し


class Calculator {
  add(a, b) { return a + b; }
  subtract(a, b) { return a - b; }
  multiply(a, b) { return a * b; }
}

const calculator = new Calculator();
const operations = [
  { method: 'add', args: [2, 3] },
  { method: 'subtract', args: [5, 2] },
  { method: 'multiply', args: [4, 5] }
];

const results = operations.map(op => 
  Reflect.apply(calculator[op.method], calculator, op.args)
);

console.log(results); // [5, 3, 20]

簡略化されたクラスファクトリ


function classFactory(classDef, ...args) {
  // クラス定義が有効かどうかチェック
  if (typeof classDef !== 'function') {
    throw new Error('最初の引数はクラスまたはコンストラクタである必要があります');
  }
  
  try {
    // Reflect.constructを使用してインスタンスを作成
    return Reflect.construct(classDef, args);
  } catch (e) {
    console.error('インスタンス作成に失敗:', e.message);
    return null;
  }
}

// 使用例
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const user = classFactory(User, 'Alice', 30);
console.log(user); // User { name: 'Alice', age: 30 }

ブラウザの互換性と使用上の推奨事項

Reflect APIはES6の一部であり、全ての現代的なブラウザでサポートされていますが、古い環境ではpolyfillが必要になる場合があります。

使用上の推奨事項

  1. オブジェクトのメタプログラミングを行う際は、Reflectを使用することを優先してください。
  2. Proxyトラップを実装する際は、常に対応するReflectメソッドを使用してデフォルトの動作を呼び出すことを推奨します。
  3. Reflect APIは主にメタプログラミングの場面で使用されますが、日常的なオブジェクト操作には従来の方法が適している場合もあります。
  4. 厳格モードでの使用を推奨します。いくつかのReflectメソッドは非厳格モードで異なる動作をすることがあります。

タグ: ECMAScript 6 Reflect API javascript proxy 元プログラミング

6月29日 17:31 投稿