13個すべてのJavaScriptプロキシトラップを見る
プロキシは本当にクールなJavaScript機能です。 あなたがメタプログラミングが好きなら、あなたはおそらくすでにそれらに精通しているでしょう。 この記事では、プログラミングデザインパターンに触れたり、メタを取得したり、プロキシがどのように機能するかを理解したりすることはしません。
通常、トラップに関する記事には、プロキシを使用してプライベートプロパティを設定するための同じ例が常にあります。 それは素晴らしい例です。 ただし、ここでは、使用できるすべてのトラップについて説明します。 これらの例は、実際の使用例を意図したものではありません。目標は、Proxy
トラップがどのように機能するかを理解するのに役立つことです。
罠? 何? すでに不吉に聞こえます
トラップという言葉はあまり好きではありません。 私はどこでもその言葉がオペレーティングシステムの領域から来ていることを読みました(ブレンダンアイクでさえJSConfEU 2010でそれについて言及しています)。 しかし、その理由はよくわかりません。 オペレーティングシステムのコンテキストでのトラップが同期していて、プログラムの通常の実行を中断する可能性があるためかもしれません。
トラップは、内部メソッド検出ツールです。 オブジェクトを操作するときはいつでも、必須の内部メソッドを呼び出しています。 プロキシを使用すると、特定の内部メソッドの実行をインターセプトできます。
したがって、実行すると:
const profile = {}; profile.firstName = 'Jack';
JavaScriptエンジンにSET内部メソッドを呼び出すように指示しています。 したがって、set
トラップは、profile.firstName
が'Jack'
に設定される前に実行する関数を呼び出します。
const kickOutJacksHandler = { set: function (target, prop, val) { if (prop === 'firstName' && val === 'Jack') { return false; } target[prop] = val; return true; } }
ここで、set
トラップは、Jack
という名のプロファイルを作成しようとするプログラムを拒否します。
const noJackProfile = new Proxy ({}, kickOutJacksHandler); noJackProfile.firstName = 'Charles'; // console will show {} 'firstName' 'Charles' // noJackProfile.firstName === 'Charles' //This won't work because we don't allow firstName to equal Jack newProfileProxy.firstName = 'Jack'; // console will show {firstName: 'Charles'} 'firstName' 'Charles' // noJackProfile.firstName === 'Charles'
何をプロキシできますか?
満足するもの:
typeof MyThing === 'object'
これは、配列、関数、オブジェクト、さらには…を意味します
console.log(typeof new Proxy({},{}) === 'object') // logs 'TRUE' well actually just true... I got a bit excited...
プロキシ! 完全に機能するポリフィルまたはトランスパイルオプションがないため、ブラウザーがそれをサポートしていない場合は、何もプロキシできません(詳細については別の投稿を参照してください)。
すべてのプロキシトラップ
JavaScriptには13のトラップがあります! 私はそれらを分類しないことを選択しました。私が最も有用であると思うものからあまり役に立たないもの(ある種)までそれらを提示します。 これは公式の分類ではなく、自由に反対してください。 私は自分のランキングにも納得していません。
始める前に、ECMAScript仕様から抜粋した小さなチートシートを次に示します。
内部メソッド | ハンドラーメソッド |
---|---|
得る | 得る |
消去 | deleteProperty |
OwnPropertyKeys | ownKeys |
HasProperty | もっている |
電話 | 申し込み |
DefineOwnProperty | defineProperty |
GetPrototypeOf | getPrototypeOf |
SetPrototypeOf | setPrototypeOf |
IsExtensible | isExtensible |
PreventExtensions | PreventExtensions |
GetOwnProperty | getOwnPropertyDescriptor |
列挙 | 列挙する |
構築 | 構築する |
取得、設定、削除:超基本
すでにset
を見ましたが、get
とdelete
を見てみましょう。 補足:set
またはdelete
を使用する場合は、true
またはfalse
を返して、キーを変更する必要があるかどうかをJavaScriptエンジンに通知する必要があります。
const logger = [] const loggerHandler = { get: function (target, prop) { logger.push(`Someone accessed '${prop}' on object ${target.name} at ${new Date()}`); return target[prop] || target.getItem(prop) || undefined; }, } const secretProtectorHandler = { deleteProperty: function (target, prop) { // If the key we try to delete contains to substring 'secret' we don't allow the user to delete it if (prop.includes('secret')){ return false; } return true; } }; const sensitiveDataProxy = new Proxy ( {name:'Secret JS Object', secretOne: 'I like weird JavaScript Patterns'}, {...loggerHandler, ...secretProtectorHandler} ); const {secretOne} = sensitiveDataProxy; //logger = ['Someone tried to accessed 'secretOne' on object Secret JS Object at Mon Dec 09 2019 23:18:54 GMT+0900 (Japan Standard Time)'] delete sensitiveDataProxy.secretOne; // returns false it can't be deleted! // sensitiveDataProxy equals {name: 'Secret JS Object', secretOne: 'I like weird JavaScript Patterns'}
キーで遊ぶ
ルートにアプリケーションデータを取得するWebサーバーがあるとします。 そのデータをコントローラーに保持したいと思います。 しかし、誤用されないようにしたいのかもしれません。 ownKeys
トラップは、オブジェクトのキーにアクセスしようとすると1回アクティブになります。
const createProxiedParameters = (reqBody, allowed) => { return new Proxy (reqBody, { ownKeys: function (target) { return Object.keys(target).filter(key => allowed.includes(key)) } }); }; const allowedKeys = ['firstName', 'lastName', 'password']; const reqBody = {lastName:'Misteli', firstName:'Jack', password:'pwd', nefariousCode:'MWUHAHAHAHA'}; const proxiedParameters = createProxiedParameters(reqBody, allowedKeys); const parametersKeys = Object.keys(proxiedParameters) // parametersKeys equals ["lastName", "firstName", "password"] const parametersValues = parametersKeys.map(key => reqBody[key]); // parameterValues equals ['Misteli', 'Jack', 'pwd'] for (let key in proxiedParameters) { console.log(key, proxiedParameters[key]); } // logs: // lastName Misteli // firstName Jack // password pwd // The trap will also work with these functions Object.getOwnPropertyNames(proxiedParameters); // returns ['lastName', 'firstName', 'password'] Object.getOwnPropertySymbols(proxiedParameters); // returns []
実際のアプリケーションでは、このようにパラメータをクリーンアップしないでください。 ただし、プロキシに基づいてより複雑なシステムを構築できます。
配列のオーバーロード
in
演算子を配列で使用することを常に夢見ていましたが、常に恥ずかしがり屋で方法を尋ねることができませんでしたか?
function createInArray(arr) { return new Proxy(arr, { has: function (target, prop) { return target.includes(prop); } }); }; const myCoolArray = createInArray(['cool', 'stuff']); console.log('cool' in myCoolArray); // logs true console.log('not cool' in myCoolArray); // logs false
has
トラップは、in
演算子を使用してオブジェクトにプロパティが存在するかどうかを確認しようとするメソッドをインターセプトします。
適用による関数呼び出し率の制御
apply
は、関数呼び出しをインターセプトするために使用されます。 ここでは、非常に単純なキャッシングプロキシについて説明します。
createCachedFunction
はfunc
引数を取ります。 'cachedFunction'にはapply
(別名Call
)トラップがあり、cachedFunction(arg)
を実行するたびに呼び出されます。 ハンドラーには、関数の呼び出しに使用される引数と関数の結果を格納するcache
プロパティもあります。 Call
/ apply
トラップでは、関数がその引数ですでに呼び出されているかどうかを確認します。 その場合、キャッシュされた結果を返します。 そうでない場合は、キャッシュされた結果を使用してキャッシュに新しいエントリを作成します。
これは完全な解決策ではありません。 落とし穴がたくさんあります。 わかりやすくするために短くしてみました。 関数の入力と出力は単一の数値または文字列であり、プロキシされた関数は特定の入力に対して常に同じ出力を返すと想定しています。
const createCachedFunction = (func) => { const handler = { // cache where we store the arguments we already called and their result cache : {}, // applu is the [[Call]] trap apply: function (target, that, args) { // we are assuming the function only takes one argument const argument = args[0]; // we check if the function was already called with this argument if (this.cache.hasOwnProperty(argument)) { console.log('function already called with this argument!'); return this.cache[argument]; } // if the function was never called we call it and store the result in our cache this.cache[argument] = target(...args); return this.cache[argument]; } } return new Proxy(func, handler); }; // awesomeSlowFunction returns an awesome version of your argument // awesomeSlowFunction resolves after 3 seconds const awesomeSlowFunction = (arg) => { const promise = new Promise(function(resolve, reject) { window.setTimeout(()=>{ console.log('Slow function called'); resolve('awesome ' + arg); }, 3000); }); return promise; }; const cachedFunction = createCachedFunction(awesomeSlowFunction); const main = async () => { const awesomeCode = await cachedFunction('code'); console.log('awesomeCode value is: ' + awesomeCode); // After 3 seconds (the time for setTimeOut to resolve) the output will be : // Slow function called // awesomeCode value is: awesome code const awesomeYou = await cachedFunction('you'); console.log('awesomeYou value is: ' + awesomeYou); // After 6 seconds (the time for setTimeOut to resolve) the output will be : // Slow function called // awesomeYou value is: awesome you // We are calling cached function with the same argument const awesomeCode2 = await cachedFunction('code'); console.log('awesomeCode2 value is: ' + awesomeCode2); // IMMEDIATELY after awesomeYou resolves the output will be: // function already called with this argument! // awesomeCode2 value is: awesome code } main()
これは、他のコードスニペットよりも噛むのが少し難しいです。 コードがわからない場合は、開発者コンソールでコードをコピーして貼り付け、console.log()
を追加するか、独自の遅延関数を試してください。
DefineProperty
defineProperty
はset
と非常によく似ており、Object.defineProperty
が呼び出されるたびに呼び出されますが、=
を使用してプロパティを設定しようとすると呼び出されます。 descriptor
引数を追加すると、さらに細かくなります。 ここでは、バリデーターのようにdefineProperty
を使用します。 新しいプロパティが書き込み可能または列挙可能でないことを確認します。 また、定義されたプロパティage
を変更して、年齢が数値であることを確認します。
const handler = { defineProperty: function (target, prop, descriptor) { // For some reason we don't accept enumerable or writeable properties console.log(typeof descriptor.value) const {enumerable, writable} = descriptor if (enumerable === true || writable === true) return false; // Checking if age is a number if (prop === 'age' && typeof descriptor.value != 'number') { return false } return Object.defineProperty(target, prop, descriptor); } }; const profile = {name: 'bob', friends:['Al']}; const profileProxied = new Proxy(profile, handler); profileProxied.age = 30; // Age is enumerable so profileProxied still equals {name: 'bob', friends:['Al']}; Object.defineProperty(profileProxied, 'age', {value: 23, enumerable: false, writable: false}) //We set enumerable to false so profile.age === 23
構築する
apply
とcallは2つの関数トラップです。 construct
はnew
演算子をインターセプトします。 関数コンストラクター拡張に関するMDNの例は本当にクールだと思います。 それで、私はそれの私の簡略化されたバージョンを共有します。
const extend = (superClass, subClass) => { const handler = { construct: function (target, args) { const newObject = {} // we populate the new object with the arguments from superClass.call(newObject, ...args); subClass.call(newObject, ...args); return newObject; }, } return new Proxy(subClass, handler); } const Person = function(name) { this.name = name; }; const Boy = extend(Person, function(name, age) { this.age = age; this.gender = 'M' }); const Peter = new Boy('Peter', 13); console.log(Peter.gender); // 'M' console.log(Peter.name); // 'Peter' console.log(Peter.age); // 13
何をすべきか教えてはいけません!
Object.isExtensible
は、オブジェクトにプロパティを追加できるかどうかをチェックし、Object.preventExtensions
を使用すると、プロパティが追加されないようにすることができます。 このコードスニペットでは、トリックオアトリートトランザクションを作成します。 子供がお菓子を求めてドアに行くと想像してみてください。しかし、彼は自分が手に入れることができるキャンディーの最大量を知りません。 彼がいくら得ることができるか尋ねると、手当は下がるでしょう。
function createTrickOrTreatTransaction(limit) { const extensibilityHandler = { preventExtensions: function (target) { target.full = true; // this will prevent the user from even changing the existing values return Object.freeze(target); }, set: function (target, prop, val) { target[prop] = val; const candyTotal = Object.values(target).reduce((a,b) => a + b, 0) - target.limit; if (target.limit - candyTotal <= 0) { // if you try to cheat the system and get more that your candy allowance, we clear your bag if (target.limit - candyTotal < 0 ) target[prop] = 0; // Target is frozen so we can't add any more properties this.preventExtensions(target); } }, isExtensible: function (target) { // Kids can check their candy limit console.log( Object.values(target).reduce((a,b) => a + b, 0) - target.limit); // But it will drop their allowance by one target.limit -= 1; // This will return the sum of all our keys return Reflect.isExtensible(target); } } return new Proxy ({limit}, extensibilityHandler); }; const candyTransaction = createTrickOrTreatTransaction(10); Object.isExtensible(candyTransaction); // console will log 10 // Now candyTransaction.limit = 9 candyTransaction.chocolate = 6; // The candy provider got tired and decided to interrupt the negotiations early Object.preventExtensions(candyTransaction); // now candyTransaction equals to {limit: 9, chocolate: 6, full: true} candyTransaction.chocolate = 20; // candyBag equals to {limit: 9, chocolate: 6, full: true} // Chocolates did not go change to 20 because we called freeze in the preventExtensions trap const secondCandyTransaction = createTrickOrTreatTransaction(10); secondCandyTransaction.reeses = 8; secondCandyTransaction.nerds = 30; // secondCandyTransaction equals to {limit: 10, reeses: 8, nerds: 0, full: true} // This is because we called preventExtensions inside the set function if a kid tries to shove in extra candies secondCandyTransaction.sourPatch = 30; // secondCandyTransaction equals to {limit: 10, reeses: 8, nerds: 0, full: true}
GetOwnPropertyDescriptor
何か変なものを見たいですか?
let candies = new Proxy({}, { // as seen above ownKeys is called once before we iterate ownKeys(target) { console.log('in own keys', target); return ['reeses', 'nerds', 'sour patch']; }, // on the other end getOwnPropertyDescriptor at every iteration getOwnPropertyDescriptor(target, prop) { console.log('in getOwnPropertyDescriptor', target, prop); return { enumerable: false, configurable: true }; } }); const candiesObject = Object.keys(candies); // console will log: // in own keys {} // in getOwnPropertyDescriptor {} reeses // in getOwnPropertyDescriptor {} nerds // in getOwnPropertyDescriptor {} sour patch // BUT ! candies == {} and candiesObject == []
これは、enumerableをfalseに設定したためです。 enumerableをtrue
に設定すると、candiesObject
は['reeses', 'nerds', 'sour patch']
と等しくなります。
プロトタイプの取得と設定
これがいつ役立つかわからない。 setPrototypeOfがいつ便利になるかはわかりませんが、ここで役立ちます。 ここでは、setPrototypeトラップを使用して、オブジェクトのプロトタイプが改ざんされていないかどうかを確認します。
const createSolidPrototype = (proto) => { const handler = { setPrototypeOf: function (target, props) { target.hasBeenTampered = true; return false; }, getPrototypeOf: function () { console.log('getting prototype') }, getOwnProperty: function() { console.log('called: ' + prop); return { configurable: true, enumerable: true, value: 10 }; } }; };
列挙する
Enumerateを使用すると、for...in
をインターセプトできましたが、残念ながらECMAScript2016以降は使用できません。 この決定の詳細については、このTC39ミーティングノートを参照してください。
13個のトラップを約束したときに嘘をついたと言わないように、Firefox40でスクリプトをテストしました。
const alphabeticalOrderer = { enumerate: function (target) { console.log(target, 'enumerating'); // We are filtering out any key that has a number or capital letter in it and sorting them return Object.keys(target).filter(key=> !/\d|[A-Z]/.test(key)).sort()[Symbol.iterator](); } }; const languages = { france: 'French', Japan: 'Japanese', '43J': '32jll', alaska: 'American' }; const languagesProxy = new Proxy (languages, alphabeticalOrderer); for (var lang in languagesProxy){ console.log(lang); } // console outputs: // Object { france: 'French', japan: 'Japanese', 43J: '32jll', alaska: 'American' } enumerating // alaska // france // Usually it would output // france // Japan // 43J // alaska
物事を単純化するために`Reflect`を使用していないことに気づいたかもしれません。 reflect
については別の投稿で取り上げます。 それまでの間、楽しんでいただけたでしょうか。 また、次回はもう少し実践的になる実用的なソフトウェアを構築します。
テーブル{幅:100%; } table.color-names tr th、table.color-names tr td {font-size:1.2rem; }
table {border-collapse:collapse; ボーダー間隔:0; 背景:var(–bg); 境界線:1px solid var(–gs0); テーブルレイアウト:自動; マージン:0自動}テーブルthead {背景:var(–bg3)}テーブルthead tr th {パディング:.5rem .625rem .625rem; フォントサイズ:1.625rem; フォントの太さ:700; color:var(–text-color)} table tr td、table tr th {パディング:.5625rem .625rem; フォントサイズ:1.5rem; 色:var(–text-color); text-align:center} table tr:nth-of-type(even){background:var(–bg3)} table tbody tr td、table tbody tr th、table thead tr th、table tr td {display:table-cell ; 行の高さ:2.8125rem}