Techcookies

Understand proxy and reflect API

JavaScript | Sat Aug 24 2024 | 5 min read

Proxies enable us to intercept and customize operations performed on objects (such as like reading/writing properties). They are a metaprogramming feature

javascript
const target = {};
const handler = {
    get(target, propKey, receiver) {
        console.log(`GET ${propKey}`);
        return 'Sample name';
    },
    has(target, propKey) {
        console.log(`HAS ${propKey}`);
        return true;
    }
};

const proxy = new Proxy(target, handler);
console.log('proxy.name', proxy.name);
console.log('name in proxy', 'age ' in proxy);
proxy.age = 5;
console.log('target.age ', target.age);

//Outpot
//GET name
//proxy.name Sample name
//HAS age 
//name in proxy true
//target.age  5

As in above code, we are trying to set age property value to 5 target.age = 5. The proxy handler doesn’t implement the trap set (setting properties). Therefore, setting proxy.age is forwarded to target and leads to target.age being set:

If the handler does not intercept an operation, it is forwarded to target.

Objects are collections of properties but JavaScript doesn't provide any method to directly manipulate data stored in the object. For most the operations on objects, there's some internal methods specifying how it can be interacted with.

For every internal method, there’s a trap in the proxy available.

trap:

The function that define the behavior for the corresponding object internal method.

Internal Method Trap method Triggers
[[Get]] get reading a property
[[Set]] set writing to a property
[[HasProperty]] has in operator
[[Delete]] deleteProperty delete operator
[[Call]] apply function call
[[construct]] construct new operator
[[GetPrototypeOf]] getPrototypeOf Object.getPropertyOf
[[SetPrototypeOf]] setPrototypeof Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] 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

get trap

javascript
let translate = {
    "Hello": "हैलो",
    "World": "वर्ल्ड"
}

const handler= {
    get(target, propKey, receiver) {
        if(propKey in target){
            return target[propKey];
        } else {
            return propKey;
        }
    }
}
const proxy = new Proxy(translate, handler);

console.log(translate['Hello']);
console.log(translate['World']);
// output
//हैलो
//वर्ल्ड

set trap

The set trap should return true if setting is successful, and false otherwise (triggers TypeError).

javascript
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); // added successfully
numbers.push(2); // added successfully
console.log("Length is: " + numbers.length); // 2

numbers.push("No number value"); // TypeError ('set' on proxy returned false)

console.log("error in the line above");

Reflect

Reflect is a built-in global object that helps access/call object internal methods and simplifies creation of Proxy.

As we know, internal methods, such as [[Get]], [[Set]] can't be called directly.

The Reflect object makes it somewhat possible.

For every internal method, trappable by Proxy, there’s a corresponding method in Reflect, with the same name and arguments as the Proxy trap.

So we can use Reflect to forward an operation to the original object.

Operation Reflect Call Internal method
obj[prop] Reflect.get(obj, prop) [[Get]]
obj[prop] = value Reflect.set(obj, prop, value) [[Set]]
delete obj[prop] Reflect.deleteProperty(obj, prop) [[Delete]]
new F(value) Reflect.construct(F, value) [[construct]]

Use cases for Reflect

  • Proxying a getter, let's see an example to see why Reflect.get is better.
javascript
let student = {
    _name: "Madhukar",
    get name() {
        return this._name;
    }
}

const handler = {
    get(target, prop, receiver) {
        return target[prop]; // Reflect.get can be replaced here
    }
}
let studentProxy = new Proxy(student, handler);

let teacher = {
    __proto__: studentProxy,
    _name: "Teacher"
}
console.log(teacher.name);

// Output: Madhukar

Reading teacher.name should return "Teacher" but it returns "Madhukar".
The problem here is:

  • When we try to read teacher.name but teacher object doesn't have name property.
  • The prototype is studentProxy.
  • So proxy prototype triggers its get trap and returns get name from target object.

If we use Reflect in this place, it should solve this problem.

javascript
let student = {
    _name: "Madhukar",
    get name() {
        return this._name;
    }
}

const handler = {
    get(target, prop, receiver) {
        return Reflect.get(target, prop, receiver);
    }
}
let studentProxy = new Proxy(student, handler);

let teacher = {
    __proto__: studentProxy,
    _name: "Teacher"
}
console.log(teacher.name);

// Output: Teacher

receiver keeps a refrence to the correct this(teacher object), is passed to the getter of Reflect.get.

proxy limitation

  • Built-in objects: Internal slots
    Many built-in objects, for example Map, Set, Date, Promise and others make use of so-called “internal slots”.
    Map stores all data in its [[MapData]] internal slot. The proxy doesn’t have such a slot. The built-in method Map.prototype.set method tries to access the internal property this.[[MapData]], but because this=proxy, can’t find it in proxy and just fails.
javascript
let map = new Map();

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

proxy.set('test', 1);

A notable exception: built-in Array doesn’t use internal slots. So there’s no such problem when proxying an array.

  • Private class fields

private fields are implemented using internal slots, JavaScript doesn't use [[Get]]/[[Set]] accessing them.

javascript
class Student {
  name = "Madhukar";

  getName() {
    return this.name;
  }
}

let student = new Student();

student = new Proxy(student, {});

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

It can be fixed.

javascript
class Student {
  name = "Madhukar";

  getName() {
    return this.name;
  }
}

let student = new Student();

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

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

Proxies can’t intercept a strict equality test ===

Revocable proxy

Proxies can be revoked (disabled/switched off).

javascript
const target = {
    name: 'Madhukar'
}
const handler = {};

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

console.log(proxy.name); // Madhukar

revoke();

console.log(proxy.name); // Error

After we call the function revoke for the first time, any operation we apply to proxy causes a TypeError. Subsequent calls of revoke have no further effect.