Proxy in JavaScript | Part 1

Proxy in JavaScript | Part 1

·

7 min read

A Proxy object wraps another object and intercepts operations on it. While intercepting operations like reading, writing properties on the object, the proxy may choose to handle these operations and modify results.

Proxy

Syntax: let proxy = new Proxy(target, handler);

target: the object that has to be proxied.
handler: the proxy configuration object, it may register traps. A trap is a handler for a particular kind of operation. By registering a trap handler it can intercept the operation and do its own thing.

If there is a trap for the operation onhandler only then the operation will be trapped and handled by proxy else operation directly occurs on the object itself.

let user = {}; 
// target object -- object to be proxied
let userProxy = new Proxy(user, {}); 
// proxy for user, note empty handler

// operations on proxy
userProxy.name = 'Aniket'; 
// set operation
// should be intercepted by
// `set` trap on handler
// no `set` trap registerd so 
// operations are performed on object itself 
console.log(userProxy.name); // 'Aniket;
// get opertaion
// should be intercepted by `get` trap
// no `get` trap registerd so opertaion 
// is performed on object directly
console.log(user.name); // 'Aniket'
// Thus we can see name property 
// directly on target itself

For most of the operations on objects, there are “Internal Methods” in JavaScript that describes how operations work at a low level, what proxy trap does is that it can intercept these methods and do its own thing.

Proxy visualisation

Below we show some of the “Internal Methods” and their corresponding proxy traps.

Proxy trap and corresponding Internal method

Internal Methods have some rules that our traps must follow, for eg: set the trap must return true if property setting was success else false.[[GetPrototypeOf]] must always return the target’s prototype when applied on proxy as well.

The problem statement

It is common practice to use *_* is in the beginning of the property name to denote a private property. You cannot get/set/loop this property. Write a proxy to achieve this.

let user = {
  name: 'Aniket',
  _password: 'Password', // private property
  isCorrectPassword(pswd) {
    return this._password === pswd;
    // `this` here is a gotcha
  },
};

“set” trap

We will register a set trap on the handler to intercept write operation on the object.

Syntax: set(target, prop, value, receiver).

target : target object.
prop : property name that is being set.
value : the value of the property to be set.
receiver : the object that is utilised as in getters.

let userProxy = new Proxy(user, {
  set(target, prop, value, reciver) { 
    // intercepts property write
    if (prop.startsWith('_')) {
      // check if property name start with `_`
      // then it is a private property so
      // don't allow to write or create a property
      throw new Error("Access denied 💣 ");
    } else {
      target[prop] = val;
      // normally write on object
      return true; // must return true [[Set]] rule
    }
  }
});

“get” trap

We will register a get trap to prevent direct access user._password to private property. Also, we have to ensure that isCorrectpassword works correctly as it does indirect access this._password.

Syntax: get(target, property, receiver).
The arguments mean the same as above.

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    // intercept property read
    if (prop.startsWith('_')) {
      // if property name starts with `_` then
      // we don't allow to read and raise an error
      throw new Error("Access denied 💣 ");
    } else {
      // the property value may be a function or something else
      let propValue = target[prop];
      // in case it is a function
      // it may have `this` inside it
      // where `this` will ref to `userProxy` 
      // as it will be invoked as `userProxy.isCorrectPassword(pswd)` 
      // so `this == userProxy` but that will 🔥 our code
      // so we need to make sure that our function `this` ref `user`
      // and so we bind it
      return (
        typeof propValue === "function" 
          ? propValue.bind(target) : propValue
      );
    }
  }  
});

“deleteProperty” trap

We will register deleteProperty so that we can't delete a private property.
Syntax: deleteProperty(target, property)

let userProxy = new Proxy(user, {
  deleteProperty(target, prop) {
    // deleteProperty trap to handle property delete
    if(prop.startsWith('_')) {
      throw new Error("Access denied 💣 ");
    } else {
      // delete property on object
      delete target[prop];
      return true; // successfully deleted
    }
  }
});

“ownKeys” trap

for..in, Object.keys, Object.values and other methods utilise an “Internal Method” called [[OwnPropertyKeys]] to get a list of keys. For eg:
Object.getOwnPropertyNames() to get a list of non-symbol keys,
Object.getOwnPropertySymbols() to get a list of symbol keys,
Object.keys() to get a list of non-symbol enumerable keys, etc.

They all call [[OwnPropertyKeys]] but tweak it a bit to return keys according to their use case. So we will register ownKeys(target) trap to return only public keys.

let userProxy = new Proxy(user, {
  ownKeys(target) {
    // ownKeys will return a list of keys
    // we must get keys on target then filter
    // to remove all private keys
    return Object.keys(target).filter((key)=>!key.startsWith('_'));
  }
});

Note: Our traps must follow the rules defined for “Internal Method”. The rule defined for ownKeys with Object.keys() is that it must return non-symbol enumerable keys. Look at the example below to understand this gotcha.

let userProxy = new Proxy(user, {
  ownKeys(target) {
    // this will return list of keys 
    // and the calling method (Object.keys) tweak this list
    // to select and return a list of 
    // non-symbolic and enumberable: true keys
    // thus for each item in list returned by ownKeys
    // it will only select item which is 
    // non-symbolic and enumberable: true
    return ['email', 'phone'];
  }
});
console.log(Object.keys(userProxy)); // [] empty 😱 gotcha

// solution 
let userProxy = new Proxy(user, {
  ownKeys(target) {
    // Object.keys will check property descriptor
    // for each key returned by ownKeys and see if
    // enumberable: true
    return ['email', 'phone'];
  },
  getOwnPropertyDescriptor(target, prop) {
    // checking for enumberablity of keys
    // is accessing its descriptor and seeing
    // if enumberable is true
    // here we are returning descriptor obj
    // with enumberable true in all cases
    return {
      enumerable: true,
      configurable: true,
    };
  }
});

“has” trap

This trap work with the in operator that intercepts the [[hasProperty]] Internal Method. Let’s register a has(target, property) trap.

let range = {
  from: 1,
  to: 10,
};
// we need to check if 5 in range
// 5 in range if 5 >= range.from && 5 <= range.to
let rangeProxy = new Proxy(range, {
  has(target, prop) {
    // 5 >= 1 && 5 <= 10
    return prop >= target.from && prop <= target.to;
  },
});
console.log(5 in rangeProxy); // true

“apply” trap

Until now all examples we have seen were on objects and now we will see an example of function as target.

Syntax: apply(target, thisArgs, args).
thisArgs : it is the value of this
args : it is a list of arguments for function

// Let us write a function `delay`
// that delay exceution of any 
// function `f` by `ms` milliseconds


// solution 1 closure way
function delay(f, ms) {
   return function (name) { // *
    setTimeout(() => f.bind(this, arguments), ms);
   }
}

var hi = (name) => {
  console.log('Hi! ' + name);
};
console.log(hi.length); // 1
// function.length returns number a params 
hi = delay(hi, 3000);
// hi is now function at line *
console.log(hi.length); // 0 😱
// we lost orignal hi function 
// and function at line * has no params so 0 
hi('Aniket'); // 'Hi! Aniket'
// runs after 3s

// solution 2 proxy way
function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArgs, args) {
      setTimeout(() => target.bind(thisArgs, args), ms);
    }
  });
}
var hi = (name) => {
  console.log('Hi! ' + name);
};
console.log(hi.length); // 1
hi = delay(hi, 3000);
console.log(hi.length); // 1 😎
hi('Aniket'); // 'Hi! Aniket'

The End

Now teach the Proxy you learnt here to your friend for whom you have put proxy 😂. Here is the next part of the post Part 2. Stay tuned for more content.

Did you find this article valuable?

Support Aniket Jha by becoming a sponsor. Any amount is appreciated!