Skip to main content

Proxy

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

The Proxy object allows you to create a proxy for another object which can intercept and redefine fundamental operations for the object.

new Proxy(target, handler);

target:-- A target is any sort of object including array, function or another proxy. \ handler:-- An object who's properties are functions. these functions in object would define the behavior of the proxy.

An empty handler {} will create a proxy that behaves in almost like target. By defining any set group of functions on the handler object it is possible to customize the proxy's behavior. By defining get() it is possible to provide a customized version of the targets property_accessor

const target = {
originalValue: "This is original value",
proxiedValue: "This is going to proxied",
};

const handler = {
get(targetObject, key, receiver) {
if (key === "proxiedValue") {
return "Replaced by proxy value";
}
return Reflect.get(...args);
},
};

const proxyObject = new Proxy(target, handler);

console.log(proxyObject.originalValue); // Returns original value from target which is "This is original value"
console.log(proxyObject.proxiedValue); // Handler will handle the value and returns "Replaced by proxy value"

Handler functions

All Objects will have following internal methods

  • [[Call]]
  • [[Construct]]
  • [[DefineOwnProperty]]
  • [[Delete]]
  • [[Get]]
  • [[GetOwnProperty]]
  • [[GetPrototypeOf]]
  • [[HasProperty]]
  • [[IsExtensible]]
  • [[OwnPropertyKeys]]
  • [[PreventExtensions]]
  • [[Set]]
  • [[SetPrototypeOf]]

It is important to understand that all interactions with object eventually boils down to invocating one of these internal methods. All these methods are customizable through proxy. That also means it is not grunted any behavior in JavaScript as any of them can be customizable (Except certain critical invariants).

This means when you run delete object.x, there is no guarantee that ("x" in obj) returns false afterwards. A delete object.x may used to log something to consul, or modify some global state, or may be can define a new property instead of deleting the existing one.
All internal methods

All internal methods are used internally by language and can't be accessed in JavaScript code. The Reflect namespace offers methods that do little more than call the internal methods, besides some input normalization/validation.

Most of the internal functions are straight forward and would do as per their name, but may be confusable are [[Set]] and [[DefineOwnProperty]]. For normal object Set invokes setter and its internally calls DefineOwnProperty. i.e the obj.x=7 syntax uses [[Set]], and Object.defineProperty() uses [[DefineOwnProperty]] It may also imply that these methods or syntaxes behave differently depending on the context or data, making it harder to intuit their behavior without clear knowledge.

For example class fields use the [[DefineOwnProperty]] semantic, that is why setters defined in the superclass are not invoked when a field is declared in derived class.

info

Equivalent to object internal functions, there are list of functions can be implemented in proxy handler, They are also called sometimes as trap, because they trap calls to the underlying target object.

Below are list of handler functions/traps that helps override object behavior on proxy.

  • apply(target, thisArg, argumentsList)
  • construct(target, arguments, newTarget)
  • defineProperty(target, property, descriptor)
  • deleteProperty(target, property)
  • get(target, property, receiver)
  • getOwnPropertyDescriptor(target, property)
  • getPropertyOf(target)
  • has(target, property)
  • isExtensible(target)
  • ownKeys(target)
  • preventExtensions(target)
  • set(target, property, value, receiver)
  • setPrototypeOf(target, prototype)

Click Handler methods in detail to get in depth information about each method.

Using Proxy in detail

Plain usage of proxy

const handler = {
get(obj, key, receiver) {
return key in obj ? obj[key] : -1;
},
};
const original = { x: 5 };
const proxy = new Proxy(original, handler);
proxy.a = 1;
proxy.b = undefined;

// as proxy.b explicitly defined with undefined value the key would be exist in proxy and returns assigned value.
console.log(proxy.a, proxy.b); // 1, undefined

//as "c" is not defined as per proxy handler it will return -1 as value.
console.log("c" in proxy, proxy.c); // false, -1

No-op forwarding proxy

The proxy object would forward all its operations to the target object. it means from above example though the const original created with one prop x after assigning the proxy with value a and b the original would become {x: 5, a: 1, b: undefined}

But please note that unlike proxy.c the original.c would return undefined value only.

No private property forwarding

A proxy is just another object with different identity, its a proxy to target object. The proxy object does not have direct access to the original objects private properties.

class Owner {
//In js class a variable started with '#' would be considered as private variable.
#value;
constructor(value) {
this.#value = value;
}
get value() {
return this.#value.replace(/\d+/, "<REPLACED>"); // Replace number with "<REPLACED>"
}
}

const owner = new Owner("Test123");
console.log(owner.value); // Test<REPLACED>

const proxy = new Proxy(owner, {});
console.log(proxy.value); // Uncaught TypeError: Cannot read private member #value from an object whose class did not declare it

The proxy.value would throw error because when the proxy's get trap is invoked, the this value is pointing to proxy object. so #value is not accessible.

const proxy = new Proxy(owner, {
get(target, key, receiver) {
// fetching value from target, which will have different value of `this`
return target[key];
},
});
console.log(proxy.value); // Test<REPLACED>

When a function defined in class, and if the function uses this key word, it is required to replace this with target object in proxy. Below example explains how to handle it.

class Owner {
//In js class a variable started with '#' would be considered as private variable.
#value;
constructor(value) {
this.#value = value;
}
get value() {
return this.#value.replace(/\d+/, "<REPLACED>"); // Replace number with "<REPLACED>"
}
originalValue() {
return this.#value;
}
}

const owner = new Owner("Test123");
console.log(owner.value); // Test<REPLACED>
console.log(owner.originalValue()); // Test123

const proxy = new Proxy(owner, {
get(target, key, receiver) {
const value = target[key];
if (value instanceof Function) {
return function (...args) {
const obj = this === receiver ? target : this;
return value.apply(obj, args);
};
}
return value;
},
});
console.log(proxy.value); // Test<REPLACED>
console.log(proxy.originalValue()); // Test123

It is important to note that there are some native objects which will have properties called internal slots, which are not accessible from the JavaScript code.
For example the Map object will have internal slot called [[MapData]], which stores the key-value pairs of the map. Due to this it is not possible to trivially create a forwarding proxy for a map.

Validation

It is very helpful to write handlers on proxy that uses set trap and do some checks and validations before setting value and throw error if not met the expectation. Below example explains how it works

const validationHandler = {
set(obj, key, value) {
if (key === "age") {
if (!Number.isInteger(value)) {
throw new TypeError("The age is not an integer");
}
if (value < 18 || value > 100) {
throw new RangeError("The person must be adult and still living");
}
}

// The default behavior to store the value
obj[key] = value;

// Indicate success
return true;
},
};

const boy = new Proxy({}, validationHandler);

boy.age = 10; // Uncaught RangeError: The person must be adult and still living

Handling value correction

const student = new Proxy(
{
skills: ["js", "ts", "cpp"],
},
{
get(obj, key) {
if (key === "recentSkill") return obj.skills.at(-1);
return obj[key];
},
set(obj, key, value) {
if (key === "recentSkill") {
obj.skills.push(value);
return true;
}

if (key === "skills" && typeof value === "string") {
value = [value];
}
obj[key] = value;
return true;
},
}
);

console.log(student.skills); // ["js", "ts", "cpp"]
console.log(student.recentSkill); // cpp

student.skills = "php";
console.log(student.skills); // ["php"]
student.recentSkill = "rust";

console.log(student.skills); // ["php", "rust"]
console.log(student.recentSkill); // rust

Multiple traps used

/*
const docCookies = ... get the "docCookies" object here:
https://reference.codeproject.com/dom/document/cookie/simple_document.cookie_framework
*/

const docCookies = new Proxy(docCookies, {
get(target, key) {
return target[key] ?? target.getItem(key) ?? undefined;
},
set(target, key, value) {
if (key in target) {
return false;
}
return target.setItem(key, value);
},
deleteProperty(target, key) {
if (!(key in target)) {
return false;
}
return target.removeItem(key);
},
ownKeys(target) {
return target.keys();
},
has(target, key) {
return key in target || target.hasItem(key);
},
defineProperty(target, key, descriptor) {
if (descriptor && "value" in descriptor) {
target.setItem(key, descriptor.value);
}
return target;
},
getOwnPropertyDescriptor(target, key) {
const value = target.getItem(key);
return value
? {
value,
writable: true,
enumerable: true,
configurable: false,
}
: undefined;
},
});

/* Cookies test */

console.log((docCookies.myCookie1 = "First value"));
console.log(docCookies.getItem("myCookie1"));

docCookies.setItem("myCookie1", "Changed value");
console.log(docCookies.myCookie1);