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.
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);