Search code examples
javascriptecmascript-6es6-proxy

Is there a way to use Javascript ES6 Proxy to spy on Object Methods


Is it possible, given the following object

let target = {
 foo:0,
 result:[],
 bar(){
   //some code
 }
}

to then wrap said object in a Proxy()

let handler = {
  get(){
    // code here
  },
  apply(){
    // code here
   }
 }

 target = new Proxy(target,handler);

that catch the call to bar() and save the result to results:[] ?

I've given it a few tries

let target = {
    called:0,
    results:[],
    foo(bar){
        return bar;
    },
}

let handler = {
    get(target,prop){
        console.log('Get Ran')
        return target[prop];
    },
    apply(target,thisArg,args){
        console.log('Apply Ran')
        // never runs
    }
}

target = new Proxy(target,handler);
target.foo();

This code misses [[ apply ]] but catches the [[ get ]] (if memory serves, object method calls are done as two operations , [[ get ]] [[ apply ]])

let target = {
    called:0,
    results:[],
    foo(bar){
        return bar;
    },
}

let handler = {
    get(target,prop){
        return target[prop];
    },
    apply(target,thisArg,args){
        let product = target.apply(thisArg,args)
        return product;
    },
}

let prox = new Proxy(target.foo,handler);
    console.log(prox('hello'));

If I instead wrap the object method in a proxy it catches the [[ apply ]] but I lose the reference to the original object ( this ) and therefore lose access to the result array

I've also tried nesting the method proxy inside of the object proxy.

any thoughts ?

CodeSandBox of my actual code

// Documentation for Proxy

Proxy MDN

Javascript.info/proxy

// Other questions , about proxy , but not this use case

Understanding ES6 javascript proxies


Solution

  • According to spec

    A Proxy exotic object only has a [[Call]] internal method if the initial value of its [[ProxyTarget]] internal slot is an object that has a [[Call]] internal method.

    So below does catch the invocation if target is a function

    (new Proxy(() => 'hello', { apply: () => console.log('catched') }))() // 'catched

    Reciprocally, if target does not have a call method, then no proxying

    try {
      (new Proxy({}, { apply: () => console.log('catched') }))() // throws
    } catch (e){ console.log('e', e.message)}

    So hint may be to proxy [get] then instead of returning the value (being the function bar, proxy that value to trap the eventual invocation

    const p = new Proxy(
      { results: [], bar: () => { console.log('rywhite') } }, {
      get: (target, prop) => {
        if (prop !== 'bar') return target[prop]
        return new Proxy (target.bar, {
          apply () {
            target.results.push('what')
          }
        })
      }
    })
    p.bar // nothing bud'
    p.bar(); console.log('res', p.results)
    p.bar(); console.log('res', p.results)
    p.bar(); console.log('res', p.results)


    edit: NB: it is not necessary to create a new proxy every time

    In code below, returning the same proxy is about twice faster

    const N = 1e6
    {
      const target = { results: 0, bar: () => { console.log('rywhite') } }
      const p = new Proxy(
        target, {
        get: (() => {
          const barProxy = new Proxy (target.bar, {
            apply () {
              target.results++
            }
          })
          return (target, prop) => {
            if (prop !== 'bar') return target[prop]
            return barProxy
          }
        })()
      })
      console.time('go')
      for (let i = 0; i < N; ++i) { p.bar() }
      console.timeEnd('go')
      console.log('res', p.results)
    }
    {
      const p = new Proxy(
        { results: 0, bar: () => { console.log('rywhite') } }, {
          get: (target, prop) => {
            if (prop !== 'bar') return target[prop]
            return new Proxy (target.bar, {
              apply () {
                target.results++
              }
            })
          }
        })
      console.time('neweverytime')
      for (let i = 0; i < N; ++i) { p.bar() }
      console.timeEnd('neweverytime')
      console.log('res', p.results)
    }