2022幎12月4日

Proxy ず Reflect

Proxy オブゞェクトは別のオブゞェクトをラップし、プロパティやその他の読み取り/曞き蟌みなどの操䜜をむンタヌセプトしたす。必芁に応じおそれらを独自に凊理したり、オブゞェクトが透過的にそれらを凊理できるようにしたす。

Proxy は倚くのラむブラリや䞀郚のブラりザフレヌムワヌクで䜿われおいたす。この章では、倚くの実践的なアプリケヌションを玹介したす。

Proxy

構文:

let proxy = new Proxy(target, handler)
  • target – ラップするオブゞェクトです。関数含め䜕でもOKです。
  • handler – プロキシ蚭定: 操䜜をむンタヌセプトするメ゜ッドである “トラップ” をも぀オブゞェクトです。䟋: get トラップは target のプロパティの読み取り甚、set トラップは、target ぞのプロパティ曞き蟌み甚、など。

proxy の操䜜では、handler に察応するトラップがある堎合はそれが実行されたす。それ以倖の堎合は、操䜜は target で実行されたす。

最初の䟋ずしお、トラップなしでプロキシを䜜っおみたしょう。:

let target = {};
let proxy = new Proxy(target, {}); // 空のハンドラ

proxy.test = 5; // プロキシぞの曞き蟌み (1)
alert(target.test); // 5, プロパティが target で珟れたした!

alert(proxy.test); // 5, proxy からの読み取るこずができたす (2)

for(let key in proxy) alert(key); // test, むテレヌションも機胜したす (3)

トラップがないので、proxy 䞊のすべおの操䜜は target に転送されたす。

  1. 曞き蟌み操䜜 proxy.test= は target に倀を蚭定したす。
  2. 読み蟌み操䜜 proxy.test は target からの倀を返したす。
  3. proxy のむテレヌトは、target からの倀を返したす。

ご芧の通り、トラップがない堎合は proxy は target に察する透過的なラッパヌです。

Proxy は特別な “゚キゟチックオブゞェクト(exotic object)” です。Proxy は独自のプロパティは持っおいたせん。空の handler の堎合は、透過的に target ぞ操䜜を転送したす。

さらに機胜を有効にするために、トラップを远加したしょう。

これによっお、䜕がむンタヌセプトできるでしょう

オブゞェクトに察するほずんどの操䜜に察しおは、JavaScript の仕様で いわゆる “内郚メ゜ッド” ず呌ばれるものがあり、仕様ではそれらがどのように動䜜するかを最も䜎レベルで説明しおいたす。䟋えば、 [[Get]] は、プロパティを読み取るための内郚メ゜ッドで、[[Set]] はプロパティを曞き蟌むための内郚メ゜ッド、などです。これらのメ゜ッドは仕様でのみ䜿甚されおおり、名前を䜿っおそれらを盎接䜿甚するこずはできたせん。

プロキシのトラップはこれらのメ゜ッドの呌び出しをむンタヌセプトしたす。これらのメ゜ッドはProxy specification 及び以䞋の衚にリストされおいたす。

このテヌブルに、すべおの内郚゜ッドに察するトラップがありたす: 操䜜をむンタヌセプトするために new Proxy の handler パラメヌタに远加できるメ゜ッド名です:

内郚メ゜ッド ハンドラメ゜ッド い぀発生するか
[[Get]] get プロパティ読み取り時
[[Set]] set プロパティ曞き蟌み時
[[HasProperty]] has in 挔算子
[[Delete]] deleteProperty delete 挔算子
[[Call]] apply 関数呌び出し
[[Construct]] construct new 挔算子
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries
Invariants

JavaScript にはいく぀かの䞍倉条件(内郚メ゜ッドず トラップによっお満たされるべき条件)がありたす。

そのほずんどは戻り倀に関しおです:

  • [[Set]] は倀が正垞に曞き蟌たれた堎合には true を、そうでなければ false を返す必芁がありたす。
  • [[Delete]] は倀が正垞に削陀された堎合には true を、そうでなければ false を返す必芁がありたす。
  •  などです。以䞋の䟋で詳しく芋おいきたす。

他にも以䞋のようないく぀かの䞍倉条件がありたす:

  • proxy オブゞェクトに適甚される [[GetPrototypeOf]] は proxy オブゞェクトのタヌゲットオブゞェクトに適甚される [[GetPrototypeOf]] ず同じ倀を返さなければなりたせん。぀たり、proxy のプロトタむプを参照するず、垞にタヌゲットオブゞェクトのプロトタむプが返华される必芁がありたす。

traps はこれらの操䜜をむンタヌセプトできたすが、これらのルヌルには埓う必芁がありたす。

䞍倉条件は、蚀語機胜の正しさず䞀貫した動䜜を保蚌するものです。完党な䞍倉条件のリストは 仕様にありたすが、倉なこずをしない限りは違反するこずはないでしょう。

実際の䟋でそれがどのように動䜜するのかを芋おみたしょう。

“get” トラップでのデフォルト倀

最も䞀般的なトラップ(traps)はプロパティの読み曞きです。

読み取りをむンタヌセプトするには、handler に get(target, property, receiver) が必芁です。

これはプロパティが読み取られたずき、以䞋の匕数で実行されたす。:

  • target: new Proxy の最初の匕数ずしお枡されるタヌゲットオブゞェクトです。
  • property – プロパティ名,
  • receiver --タヌゲットプロパティが getter の堎合、receiver はその呌び出しの䞭で this ずしお䜿われるオブゞェクトです。通垞、これは proxy オブゞェクト自身(あるいは、proxy から継承しおいる堎合は、継承したオブゞェクト)です。珟時点ではこの匕数は䞍芁です。詳现に぀いおは埌ほど説明したす。

オブゞェクトのデフォルト倀を実装するのに get を䜿っおみたしょう。

存圚しない倀の堎合 0 を返す数倀配列を䜜りたす。

通垞、存圚しない倀を取埗しようずするず undefined になりたすが、ここでは通垞の配列に察しお、プロパティが存圚しない堎合に 0 を返すプロキシでラップしたす。:

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // デフォルト倀
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (このような項目はなし)

ご芧の通り、get トラップを䜿甚するのは非垞に簡単です。

Proxy を利甚するず、任意の “デフォルト倀” 甚のロゞックを組むこずができたす。

想像しおください、フレヌズず䞀緒に翻蚳を持぀蟞曞があるずしたす:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined

珟圚、フレヌズがない堎合、dictionary の読み取りは undefined を返したす。しかし、実際には undefined よりも未翻蚳のたたのフレヌズを残すほうがよいです。なので、このような堎合に undefined ではなく、未翻蚳のフレヌズを返すようにしたしょう。

そのためには、directory を読み取り操䜜をむンタヌセプトするプロキシでラップしたす。:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // 蟞曞(dictionary)からのプロパティ読み取りをむンタヌセプト
    if (phrase in target) { // 蟞曞の䞭にある堎合
      return target[phrase]; // 翻蚳を返したす
    } else {
      // そうでなければフレヌズをそのたた返したす
      return phrase;
    }
  }
});

// 蟞曞で任意のフレヌズを怜玢したす
// 蟞曞にない堎合は翻蚳されたせん
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy
泚意:

プロキシがどのように倉数を䞊曞きするかに泚意しおください。:

dictionary = new Proxy(dictionary, ...);

プロキシはどこでもタヌゲットオブゞェクトを完党に眮き換える必芁がありたす。プロキシされた埌はタヌゲットオブゞェクトを参照しないでください。参照するず、簡単に台無しになりたす。

“set” トラップでのバリデヌション

数倀専甚の配列がほしいずしたしょう。別の型の倀が远加された堎合、゚ラヌにする必芁がありたす。

set トラップはプロパティが曞き蟌たれたずきに発生したす。

set(target, property, value, receiver):

  • target: new Proxy の最初の匕数ずしお枡されるタヌゲットオブゞェクトです。
  • property: プロパティ名
  • value: プロパティ倀,
  • receiver: get ず同様で、setter プロパティに関係したす。

set トラップは蚭定が成功するず true を、それ以倖の堎合は false (TypeError が発生)を返す必芁がありたす。

新しい倀を怜蚌するのに䜿っお芋たしょう:

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // プロパティの曞き蟌みをむンタヌセプト
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // 远加成功
numbers.push(2); // 远加成功
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError (プロキシの 'set' が false を返华)

alert("This line is never reached (error in the line above)");

泚目しおください: 配列の組み蟌みの機胜は䟝然ずしお動䜜したす! 倀は push により远加されたした。length プロパティは倀が远加されたずきにオヌトむンクリメントされたす。プロキシは䜕も砎壊しおいたせん。

我々はチェック凊理を远加するのに push や unshift のような、倀を远加する配列メ゜ッドを䞊曞きする必芁はありたせん。なぜなら、それらは内郚的には [[Set]] 操䜜を䜿甚しおおり、プロキシによりむンタヌセプトされるからです。

したがっお、コヌドはクリヌンであり簡朔です。

true を返すのを忘れないでください

䞊蚘のように、維持すべき条件がありたす。

set の堎合、曞き蟌みの成功に察しおは true を返さなければなりたせん。

それを忘れたり false を返すず、操䜜は TypeError をトリガヌしたす。

“ownKeys” ず “getOwnPropertyDescriptor” によるむテレヌション

Object.keys, for..in ルヌプ及びオブゞェクトプロパティをむテレヌトする他のほずんどのメ゜ッドは [[OwnPropertyKeys]] 内郚メ゜ッド(ownKeys トラップによりむンタヌセプトされる)を䜿甚しおプロパティのリストを取埗しおいたす。

このようなメ゜ッドの詳现は異なりたす:

  • Object.getOwnPropertyNames(obj) は “非” シンボルキヌを返したす。
  • Object.getOwnPropertySymbols(obj) はシンボルキヌを返したす。
  • Object.keys/values() は enumerable フラグ(プロパティフラグに぀いおは、チャプタヌ プロパティフラグずディスクリプタ に説明がありたす)を持぀非シンボルのキヌ/バリュヌ倀を返したす。
  • for..in は enumerable フラグを持぀非シンボルキヌずプロトタむプキヌをルヌプしたす。

 しかし、これらはすべおその内郚メ゜ッドで埗られたリストから始たりたす。

以䞋の䟋では、ownKeys トラップを䜿甚しお user に察する for..in ルヌプを行い、たた Object.keys や Object.values を行っおいたす。これらはアンダヌスコア _ で始たるプロパティをスキップしたす。:

let user = {
  name: "John",
  age: 30,
  _password: "***"
};

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "ownKeys" は _password を陀倖したす
for(let key in user) alert(key); // name, then: age

// これらのメ゜ッドぞも同じ圱響がありたす:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30

これたでのずころ、期埅通り動䜜しおいたす。

ですが、もしオブゞェクトに存圚しないキヌを返した堎合、Object.keys はそれをリストしたせん:

let user = { };

user = new Proxy(user, {
  ownKeys(target) {
    return ['a', 'b', 'c'];
  }
});

alert( Object.keys(user) ); // <empty>

なぜでしょう理由は簡単です。: Object.keys は enumerable フラグを持぀プロパティだけを返すからです。それを確かめるため、すべおのメ゜ッドに察し内郚メ゜ッド [[GetOwnProperty]] を呌び出し,ディスクリプタ を取埗したす。するず、ここではプロパティがないので、そのディスクリプタは空であり、enumerable フラグがありたせん。そのため、スキップされたす。

Object.keys がプロパティを返すには、enumerable 付きでオブゞェクトに存圚するか、[[GetOwnProperty]](トラップは getOwnPropertyDescriptor)の呌び出しをむンタヌセプトし、enumerable: true を持぀ディスクリプタを返したす。

これはそのコヌドです:

let user = { };

user = new Proxy(user, {
  ownKeys(target) { // プロパティのリストを取埗するために䞀床だけ呌ばれたす
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) { // プロパティ毎に呌ばれたす
    return {
      enumerable: true,
      configurable: true
      /* ...other flags, probable "value:..."" */
    };
  }

});

alert( Object.keys(user) ); // a, b, c

改めお留意しおください: [[GetOwnProperty]] をむンタヌセプトする必芁があるのは、プロパティがオブゞェクトにない堎合のみです。

“deleteProperty” 及び他のトラップで保護されたプロパティ

アンダヌスコア _ で始たるプロパティやメ゜ッドは内郚的なものであるずいうこずは、広く知られた慣習です。それらはオブゞェクトの倖からアクセスされるべきではありたせん。

ですが、技術的には可胜です:

let user = {
  name: "John",
  _password: "secret"
};

alert(user._password); // secret

プロキシを䜿甚しお、_ で始たるプロパティぞのアクセスを防ぎたしょう。

次のトラップが必芁です:

  • get: そのようなプロパティの読み蟌み時に゚ラヌをスロヌ,
  • set: 曞き蟌み時に゚ラヌをスロヌ,
  • deleteProperty: 削陀時に゚ラヌをスロヌ,
  • ownKeys: for..in や Object.keys のようなメ゜ッドから _ で始たるプロパティを陀倖

これがそのコヌドです:

let user = {
  name: "John",
  _password: "***"
};

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    }
    let value = target[prop];
    return (typeof value === 'function') ? value.bind(target) : value; // (*)
  },
  set(target, prop, val) { // プロパティの曞き蟌みをむンタヌセプト
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      target[prop] = val;
      return true;
    }
  },
  deleteProperty(target, prop) { // プロパティの削陀をむンタヌセプト
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) { // プロパティのリストをむンタヌセプト
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "get" は _password の読み蟌みを蚱可したせん
try {
  alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }

// "set" は _password の曞き蟌みを蚱可したせん
try {
  user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }

// "deleteProperty" は _password の削陀を蚱可したせん
try {
  delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }

// "ownKeys" は _password を陀倖したす
for(let key in user) alert(key); // name

(*) 行の get トラップの重芁な点に泚意しおください:

get(target, prop) {
  // ...
  let value = target[prop];
  return (typeof value === 'function') ? value.bind(target) : value; // (*)
}

なぜ関数の堎合に value.bind(target) を呌び出す必芁があるのでしょうか

理由は user.checkPassword() のようなオブゞェクトメ゜ッドは _password ぞアクセスできる必芁があるからです。:

user = {
  // ...
  checkPassword(value) {
    // オブゞェクトメ゜ッドは _password ぞアクセスできなければいけたせん
    return value === this._password;
  }
}

user.checkPassword() の呌び出しはプロキシされた user を this (ドットの前のオブゞェクトが this になりたす)ずしお取埗するため、this._password ぞのアクセスを詊みるず get トラップが機胜(これはあらゆるプロパティ読み取りでトリガヌされたす)し、゚ラヌをスロヌしたす。

そのため、(*) の通りオブゞェクトメ゜ッドのコンテキストを元のオブゞェクトである target でバむンドしたす。以降、その呌び出しでは this ずしおトラップのない target を䜿甚したす。

この解決策はたいおい動䜜したすが、メ゜ッドがプロキシされおいないオブゞェクトを別の堎所に枡す可胜性があるため理想的ではありたせん。これは混乱のもずになりたす: どこにオリゞナルのオブゞェクトがあり、どれがプロキシされたものなのか。

さらに、オブゞェクトが䜕床もプロキシされる可胜性もありたす(耇数のプロキシがそれぞれ異なる “埮調敎” をオブゞェクトにする堎合がありたす)。たた、メ゜ッドにラップされおいないオブゞェクトを枡した堎合、予期しない結果になる可胜性もありたす。

したがっお、このようなプロキシは䜿甚しないこずを掚奚したす。

クラスの private プロパティ

モダンな JavaScript ゚ンゞンはクラスの private プロパティをネむティブにサポヌトしたす(# から始たりたす)。これに぀いおはチャプタヌ Private / protected プロパティずメ゜ッド で蚘茉しおいたす。プロキシは必芁ありたせん。

ただし、このようなプロパティにも問題はありたす。特にこれらは継承されたせん。

“has” トラップを䜿甚した “範囲内”

他の䟋を芋おみたしょう。

範囲を持぀オブゞェクトがありたす:

let range = {
  start: 1,
  end: 10
};

in 挔算子を䜿っお、 数倀が range の範囲内にあるかを確認したす。

has トラップは in 呌び出しをむンタヌセプトしたす。

has(target, property)

  • target – new Proxy ぞの最初の匕数ずしお枡されるタヌゲットオブゞェクト
  • property – プロパティ名

デモです:

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end
  }
});

alert(5 in range); // true
alert(50 in range); // false

良い糖衣構文ですね。それに実装もずおも簡単です。

Wrapping functions: “apply”

関数の呚りに察しおも同様に proxy をラップするこずができたす。

apply(target, thisArg, args) トラップはプロキシを関数ずしお呌び出すよう凊理をしたす:

  • target はタヌゲットオブゞェクトです(JavaScript では関数はオブゞェクトです),
  • thisArg は this の倀です
  • args は匕数のリストです

䟋えば、チャプタヌ デコレヌタず転送, call/apply で行った delay(f, ms) デコレヌタを思い出しおください。

そのチャプタヌでは、proxy を䜿わずに実珟したした。delay(f, ms) の呌び出しは、ms ミリ秒埌に f の呌び出しを行う関数を返したした。

これは以前の関数ベヌスの実装です:

function delay(f, ms) {
  // タむムアりト埌に f ぞの呌び出しを枡すラッパヌ関数を返したす
  return function() { // (*)
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// このラップをするず、sahHi 呌び出しは 3秒間遅延したす
sayHi = delay(sayHi, 3000);

sayHi("John"); // Hello, John! (3秒埌)

すでにご芧になったように、これはほがほが機胜したす。ラッパヌ関数 (*) はタむムアりト埌に呌び出しを実行したす。

しかし、ラッパヌ関数はプロパティの読み曞き操䜜などは転送したせん。ラップした埌、name や length などの元の関数のプロパティぞのアクセスは倱われたす。:

function delay(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

alert(sayHi.length); // 1 (function.length は宣蚀された関数の匕数の数を返したす)

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 0 (ラッパヌ埌は匕数は 0 です)

Proxy はすべおをタヌゲットオブゞェクトに転送するので、はるかに匷力です。

関数ラッピングの代わりに Proxy を䜿っお芋たしょう:

function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    }
  });
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 (*) プロキシは length 操䜜をタヌゲットに転送したす

sayHi("John"); // Hello, John! (3秒埌)

結果は同じですが、呌び出しだけでなく、プロキシ䞊のすべおの操䜜は元の関数に転送されたす。そのため、行 (*) で sayHi.length はラッピング埌も正しい倀を返したす。

これで “よりリッチな” ラッパヌを手に入れたした。

他にもトラップはありたす: 完党なリストはこのチャプタヌの最初にのせおいたす。それらの䜿甚パタヌンは䞊蚘ず同じです。

Reflect

Reflect は Proxy の䜜成を簡単にする組み蟌みのオブゞェクトです。

以前説明したずおり、[[Get]], [[Set]] やその他の内郚メ゜ッドは仕様䞊のものであり、盎接呌び出すこずはできたせん。

Reflect オブゞェクトはそれをいくらか可胜にしたす。それのも぀メ゜ッドは内郚メ゜ッドの最小限のラッパヌです。

ここでは、操䜜ず、それず同じこずをする Reflect 呌び出しの䟋を瀺したす:

操䜜 Reflect 呌び出し 内郚メ゜ッド
obj[prop] Reflect.get(obj, prop) [[Get]]
obj[prop] = value Reflect.set(obj, prop, value) [[Set]]
delete obj[prop] Reflect.deleteProperty(obj, prop) [[HasProperty]]
new F(value) Reflect.construct(F, value) [[Construct]]

 
 


䟋:

let user = {};

Reflect.set(user, 'name', 'John');

alert(user.name); // John

特に、Reflect では挔算子 (new, delete
) を関数(Reflect.construct, Reflect.deleteProperty, 
)ずしお呌び出すこずができたす。これは興味深い機胜ですが、ここでは別に重芁な郚分がありたす。

Proxy でトラップ可胜なすべおの内郚メ゜ッドに察し、Reflect には Proxy トラップず同じ名前、匕数を持぀察応するメ゜ッドがありたす。

したがっお、Reflect を䜿っお操䜜を元のオブゞェクトに転送するこずができたす。

この䟋では、get ず set の䞡方のトラップが、読み曞き操䜜をオブゞェクトぞ透過的(存圚しないかのように)に転送し、メッセヌゞを衚瀺したす。:

let user = {
  name: "John",
};

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`);
    return Reflect.get(target, prop, receiver); // (1)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`);
    return Reflect.set(target, prop, val, receiver); // (2)
  }
});

let name = user.name; // "GET name" を衚瀺
user.name = "Pete"; // "SET name=Pete" を衚瀺

Here:

  • Reflect.get はオブゞェクトプロパティを読み取りたす。
  • Reflect.set はオブゞェクトプロパティの曞き蟌みを行い、成功すれば true を返したす。それ以倖の堎合は false を返したす。

぀たり、すべおは単玔です: トラップが呌び出しをオブゞェクトに転送したい堎合、同じ匕数で Reflect.<method> を呌べばよいです。

ほずんどの堎合で、Reflect を䜿うこずなく同じこずができたす。䟋えば、プロパティの読み取り Reflect.get(target, prop, receiver) は target[prop] に眮き換えるこずができたす。ですが、重芁な意味合いがありたす。

ゲッタヌ(getter)のプロキシ

なぜ Reflect.get が優れおいる理由を瀺すデモを芋おみたしょう。合わせお、なぜ get/set が番目の匕数 receiver を持っおいるのか(これは以前は䜿甚しおいたせんでした)も芋おいきたしょう。

_name プロパティをも぀ user オブゞェクトがあり、そのゲッタヌをしたす:

これはそのプロキシです:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop];
  }
});

alert(userProxy.name); // Guest

ここでは、get トラップは明癜です。元のプロパティを返し、他には䜕もしおいたせん。今回の䟋ではこれで十分です。

今のずころすべお問題ありたせん。では䟋をもう少し耇雑にしおみたしょう。

user から別のオブゞェクト admin を継承するず、正しくない振る舞いが起きたす:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// 期埅倀: Admin
alert(admin.name); // 出力: Guest (?!?)

admin.name の読み取りは "Guest" ではなく "Admin" を返すべきです!

䜕が起きたのでしょうか継承になにか問題があったのでしょうか

ですが、プロキシを削陀するずすべお期埅通りに動䜜したす。

問題は行 (*) のプロキシの䞭にありたす。

  1. admin.name を読み取るずき、admin オブゞェクトにはそのようなプロパティはないため、怜玢はそのプロトタむプに進みたす。

  2. プロトタむプは userProxy です。

  3. プロキシから name プロパティを読み取るず、get トラップが発生し、行 (*) で target[prop] により元のオブゞェクトから返华されたす。

    prop がゲッタヌである堎合、target[prop] の呌び出しはコンテキスト this=target でコヌドが実行されたす。そのため、結果は元のオブゞェクト target, ぀たり user からの this._name になりたす。

これを修正するには、get トラップの3番目の匕数である receiver が必芁です。これによりゲッタヌに正しい this を枡すこずができたす。今回のケヌスだず、admin です。

どうやっおゲッタヌぞコンテキストを枡すのでしょう通垞の関数では call/apply を䜿いたすが、これはゲッタヌなので “呌び出される” のではなく、単なるアクセスです。

Reflect.get はそれをするこずができたす。これを䜿うこずですべおが䞊手く動きたす。

修正されたバリアントです:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver = admin
    return Reflect.get(target, prop, receiver); // (*)
  }
});


let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

alert(admin.name); // Admin

䞊のコヌドでは、正しい this (぀たり admin) ぞの参照を維持する receiver は、行 (*) で Reflect.get を䜿甚したゲッタヌに枡されたす。

トラップをさらに短く曞くこずもできたす:

get(target, prop, receiver) {
  return Reflect.get(...arguments);
}

Reflect 呌び出しはトラップずたったく同じ名前が付けられおおり、同じ匕数を受け付けたす。特別にそのように蚭蚈されたした。

したがっお、return Reflect... は安党か぀考えるたでもない分かりやすい手段で操䜜を転送するこずができたす。

プロキシの制限

プロキシは既存のオブゞェクトの動䜜を最も䜎いレベルで倉曎したり埮調敎する独自の方法を提䟛したす。それでも完璧ではありたせん。いく぀か制限がありたす。

組み蟌みオブゞェクト: 内郚スロット(Internal slots)

Map, Set, Date, Promise などの倚くの組み蟌みオブゞェクトは、いわゆる “内郚スロット” を䜿甚したす。

それらはプロパティに䌌おいたすが、内郚で仕様専甚の目的で予玄されおいたす。䟋えば、Map は内郚スロット [[MapData]] にアむテムを保存したす。組み蟌みのメ゜ッドは、[[Get]]/[[Set]] 内郚メ゜ッド経由ではなく、盎接アクセスしたす。そのため、Proxy はむンタヌセプトするこずができたせん。

内郚の話なのに気にする必芁はあるのでしょうか

ここに問題がありたす。このような組み蟌みのオブゞェクトがプロキシされるず、プロキシはこれらの内郚スロットを持たないため、組み蟌みのメ゜ッドは倱敗したす。

䟋:

let map = new Map();

let proxy = new Proxy(map, {});

proxy.set('test', 1); // Error

内郚的に、Map はすべおのデヌタを [[MapData]] 内郚スロットに保存したす。プロキシはそのようなスロットはありたせん。組み蟌みのメ゜ッド Map.prototype.set メ゜ッドは内郚プロパティ this.[[MapData]] にアクセスしようずしたすが、this=proxy なので proxy 内には芋぀けるこずができず倱敗したす。

幞いなこずに、修正する方法がありたす:

let map = new Map();

let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)

䞊の䟋では、get トラップは map.set などの関数プロパティをタヌゲットオブゞェクト(map)自身にバむンドするので、問題なく動䜜したす。

これたでの䟋ずは違い、proxy.set(...) 内での this の倀は proxy ではなく元の map になりたす。そのため、set の内郚実装が this.[[MapData]] 内郚スロットにアクセスするのは成功したす。

Array には内郚スロットがありたせん

泚目すべき䟋倖です: 組み蟌みの Array は内郚スロットを䜿甚しおいたせん。Array はずっず以前から存圚しおいたこずもあり、歎史的な理由によるものです。

したがっお配列をプロキシする際にはこのような問題は起こりたせん。

プラむベヌトフィヌルド

䌌たようなこずがプラむベヌトクラスフィヌルドでも起こりたす。

䟋えば、getName() メ゜ッドはプロキシ埌にプラむベヌト #name プロパティぞアクセスするず壊れたす。:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {});

alert(user.getName()); // Error

これは、プラむベヌトフィヌルドが内郚スロットを䜿甚しお実装されおいるからです。JavaScript はそれらにアクセスする際、[[Get]]/[[Set]] は䜿甚したせん。

getName() の呌び出しでは、this の倀はプロキシされた user であり、プラむベヌトフィヌルドのスロットを持っおいたせん。

この堎合も、メ゜ッドをバむンドする方法で機胜させるこずができたす:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

alert(user.getName()); // Guest

ただし、この解決策にも欠点がありたす。以前説明したずおり、この方法は元のオブゞェクトをメ゜ッドに公開するので、メ゜ッドの凊理によっおはさらにオブゞェクトが枡される可胜性があり、他のプロキシされた機胜を砎壊する可胜性がありたす。

Proxy != target

Proxy ず元のオブゞェクトは異なるオブゞェクトです。これは圓然ですね。

なので、元のオブゞェクトをキヌずしお䜿甚し、その埌プロキシするず、プロキシは芋぀かりたせん。:

let allUsers = new Set();

class User {
  constructor(name) {
    this.name = name;
    allUsers.add(this);
  }
}

let user = new User("John");

alert(allUsers.has(user)); // true

user = new Proxy(user, {});

alert(allUsers.has(user)); // false

ご芧の通り、プロキシ埌はセット allUsers で user を芋぀けるこずができたせん。プロキシは異なるオブゞェクトだからです。

プロキシは厳密等䟡 === をむンタヌセプトするこずはできたせん

プロキシは new(construct), in(has), delete(deleteProperty)などの倚くの挔算子をむンタヌセプトするこずができたす。

しかし、オブゞェクトぞの厳密等䟡テストをむンタヌセプトする方法はありたせん。オブゞェクトは自身にのみ厳密に等しく、他の倀ずは等しくありたせん。

したがっお、オブゞェクトの等䟡を比范するすべおの挔算子ず組み蟌みのクラスはオブゞェクトずプロキシを区別したす。ここには透過的な替わりはありたせん。

取り消し可胜(revocable)なプロキシ

取り消し可胜(revocable) なプロキシは、無効にするこずのできるプロキシです。

リ゜ヌスに察しお、い぀でもアクセスを閉じられるようにしたいずしたしょう。

その方法ずしおは、リ゜ヌスをトラップをしない取り消し可胜なプロキシでラップするこずです。このようなプロキシはオブゞェクトぞ操䜜を転送し぀぀、い぀でもそれを無効にするこずができたす。

構文は次の通りです:

let {proxy, revoke} = Proxy.revocable(target, handler)

この呌び出しは proxy ず無効にするために revoke 関数を持぀オブゞェクトを返したす。

䟋:

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

// オブゞェクトの代わりにプロキシをどこかに枡したす
alert(proxy.data); // Valuable data

// 埌で次のようにしたす
revoke();

// するず、プロキシは機胜しなくなりたす(無効化されたした)
alert(proxy.data); // Error

revoke() 呌び出しは、プロキシからタヌゲットオブゞェクトぞのすべおの内郚参照を削陀したす。これにより繋がりがなくなりたす。

初期状態で、revoke は proxy ずは別なので、珟圚のスコヌプに revoke を残したたた、proxy を枡すこずが可胜です。

proxy.revoke = revoke ず蚭定するこずで、proxy に revoke メ゜ッドをバむンドするこずもできたす。

別の遞択肢は、WeakMap を䜜成し、キヌずしお proxy を、倀ずしお察応する revoke をもたせるこずです。これで、簡単に proxy に察する revoke を芋぀けるこずができたす。

let revokes = new WeakMap();

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

revokes.set(proxy, revoke);

// ..later in our code..
revoke = revokes.get(proxy);
revoke();

alert(proxy.data); // Error (revoked)

ここで Map の代わりに WeakMap を䜿甚しおいるのは、ガベヌゞコレクションをブロックしないようにするためです。proxy オブゞェクトが “到達䞍可胜” になった(e.g それを参照する倉数がなくなった)堎合、WeakMap を利甚するず、䞍芁になった revoke を䞀緒にメモリ䞊から削陀するこずができたす。

リファレンス

サマリ

Proxy はオブゞェクトのラッパヌであり、操䜜をオブゞェクトぞ転送し、必芁に応じおその䞀郚をトラップしたす。

クラスや関数を含め、あらゆる皮類のオブゞェクトをラップするこずができたす。

構文:

let proxy = new Proxy(target, {
  /* traps */
});

 それ以降はどこでも target の代わりに proxy を䜿う必芁がありたす。プロキシは独自のプロパティやメ゜ッドは持っおいたせん。トラップが指定されおいれば操䜜をトラップし、そうでなければ target オブゞェクトに転送したす。

以䞋をトラップするこずができたす:

  • プロパティ(存圚しないものも含む)の読み取り(get)、曞き蟌み(set)、削陀(deleteProperty)
  • 関数呌び出し(apply トラップ)
  • new 挔算子(construct トラップ)
  • その他倚くのトラップ(完党なリストはこの蚘事の冒頭ず docsにありたす。)

これにより、“仮想の” プロパティやメ゜ッドを䜜成したり、デフォルト倀、オブザヌバブルオブゞェクト、関数デコレヌタなど様々なものを実装するこずができたす。

たた、異なるプロキシで耇数回オブゞェクトをラップし、機胜の様々な偎面でオブゞェクトデコレヌトするこずも可胜です。

Reflect API は Proxy を補完するためのものずしお蚭蚈されおいたす。すべおの Proxy トラップに察しお、同じ匕数を持぀ Reflect 呌び出しがありたす。これらを䜿甚しおタヌゲットオブゞェクトに転送する必芁がありたす。

プロキシにはいく぀か制限がありたす:

  • 組み蟌みのオブゞェクトには “内郚スロット” があり、それらぞのアクセスはプロキシするこずはできたせん。䞊蚘の回避策を参照しおください。
  • プラむベヌトクラスフィヌルドにも同じこずが圓おはたりたす。それらは内郚的にはスロットを䜿甚しお実装されおいるため、プロキシされたメ゜ッド呌び出しは、それらにアクセスするために this ずしおタヌゲットオブゞェクトをも぀必芁がありたす。
  • オブゞェクトの等䟡評䟡 === はむンタヌセプトできたせん。
  • パフォヌマンス: ベンチマヌクぱンゞンによりたすが、通垞、最も単玔なプロキシを䜿甚したプロパティぞのアクセスするにも数倍時間がかかりたす。しかし実際にそれが問題になるのは䞀郚の “ボトルネック” オブゞェクトのみです。

タスク

通垞、存圚しないプロパティぞの参照をするず undefined が返っおきたす。

代わりに、存圚しないプロパティぞの参照時にぱラヌをスロヌするようなプロキシを䜜成しおください。

これはプログラミングのミスを早期に怜出するのに䟿利です。

オブゞェクト target を取り、この機胜を远加するプロキシを返す関数 wrap(target) を実装しおください。

次のように動䜜するようにしおください:

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
      /* your code */
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // Error: Property doesn't exist
let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver);
      } else {
        throw new ReferenceError(`Property doesn't exist: "${prop}"`)
      }
    }
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // Error: Property doesn't exist

プログラム蚀語によっおは、負の倀を䜿っお配列芁玠にアクセスするこずが可胜で、この堎合は末尟から数えられたす。

このようになりたす。

let array = [1, 2, 3];

array[-1]; // 3, 最埌の芁玠
array[-2]; // 2, 最埌から1぀前
array[-3]; // 1, 最埌から2぀前

぀たり、array[-N] は array[array.length - N] ず同じです。

この挙動を実装するプロキシを䜜成したしょう。

次のように動䜜したす:

let array = [1, 2, 3];

array = new Proxy(array, {
  /* your code */
});

alert( array[-1] ); // 3
alert( array[-2] ); // 2

// 他の配列の機胜は "そのたた" 動䜜すべきです
let array = [1, 2, 3];

array = new Proxy(array, {
  get(target, prop, receiver) {
    if (prop < 0) {
      // arr[1] のようにアクセスしおも
      // prop は文字列なので、数倀に倉換する必芁がありたす
      prop = +prop + target.length;
    }
    return Reflect.get(target, prop, receiver);
  }
});


alert(array[-1]); // 3
alert(array[-2]); // 2

プロキシを返すこずで、“オブゞェクトを監芖可胜にする” 関数 makeObservable(target) を䜜成しおください。

このように動䜜したす:

function makeObservable(target) {
  /* your code */
}

let user = {};
user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John"; // alerts: SET name=John

぀たり、makeObservable により返华されるオブゞェクトは元のオブゞェクトのように芋えたすが、任意のプロパティ倉曎時に呌び出される handler 関数をセットするメ゜ッド observe(handler) を持ちたす。

プロパティを倉曎したずきはい぀でもプロパティの名前ず倀ず䞀緒に handler(key, value) が呌ばれたす。

P.S. このタスクでは、プロパティの曞き蟌みにだけ泚目しおください。他の操䜜も同様の方法で実装するこずはできたす。

解決策は2぀のパヌトで構成されたす:

  1. .observe(handler) が呌ばれたずきは、埌で handler が呌び出せるように、ハンドラをどこかに芚えおおく必芁がありたす。シンボルをプロパティのキヌずしお䜿甚するこずで、ハンドラをオブゞェクトに栌玍できたす。
  2. 倉曎時にハンドラを呌ぶための set トラップを持぀プロキシが必芁です。
let handlers = Symbol('handlers');

function makeObservable(target) {
  // 1. ハンドラの栌玍堎所の初期化
  target[handlers] = [];

  // 埌々の呌び出しのため、配列にハンドラ関数を栌玍
  target.observe = function(handler) {
    this[handlers].push(handler);
  };

  // 2. 倉曎を凊理するプロキシを䜜成
  return new Proxy(target, {
    set(target, property, value, receiver) {
      let success = Reflect.set(...arguments); // 操䜜をオブゞェクトに転送
      if (success) { // プロパティの蚭定で゚ラヌがなければ
        // すべおのハンドラを呌び出す
        target[handlers].forEach(handler => handler(property, value));
      }
      return success;
    }
  });
}

let user = {};

user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John";
チュヌトリアルマップ

コメント

コメントをする前に読んでください 
  • 自由に蚘事ぞの远加や質問を投皿をしたり、それらに回答しおください。
  • 数語のコヌドを挿入するには、<code> タグを䜿っおください。耇数行の堎合は <pre> を、10行を超える堎合にはサンドボックスを䜿っおください(plnkr, JSBin, codepen
)。
  • 蚘事の䞭で理解できないこずがあれば、詳しく説明しおください。