プロキシは本当にクールな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、見てみましょう getdelete. 補足:使用する場合 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サーバーがあるとします。 そのデータをコントローラーに保持したいと思います。 しかし、誤用されないようにしたいのかもしれません。 The 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

The has トラップは、を使用してオブジェクトにプロパティが存在するかどうかを確認しようとするメソッドをインターセプトします。 in オペレーター。

適用による関数呼び出し率の制御

apply 関数呼び出しをインターセプトするために使用されます。 ここでは、非常に単純なキャッシングプロキシについて説明します。

The 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に設定したためです。 列挙可能をに設定した場合 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 };
    }
  };
};

列挙する

列挙することで、インターセプトが可能になりました 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}