ES6+ Features: Proxy and Reflect
Relevant source files
The following files were used as context for generating this wiki page: - [Jurassic/Library/Proxy/ProxyObject.cs](https://github.com/zhgabor/jurassic/blob/HEAD/Jurassic/Library/Proxy/ProxyObject.cs) - [Jurassic/Library/Reflect/Reflect.cs](https://github.com/zhgabor/jurassic/blob/HEAD/Jurassic/Library/Reflect/Reflect.cs) - [Jurassic/Library/Reflect/Reflect.g.cs](https://github.com/zhgabor/jurassic/blob/HEAD/Jurassic/Library/Reflect/Reflect.g.cs) - [Unit Tests/Library/ProxyTests.cs](https://github.com/zhgabor/jurassic/blob/HEAD/Unit%20Tests/Library/ProxyTests.cs) - [Unit Tests/Library/ReflectTests.cs](https://github.com/zhgabor/jurassic/blob/HEAD/Unit%20Tests/Library/ReflectTests.cs) - [Test Suite Runner/Kangax/compat-table.json](https://github.com/zhgabor/jurassic/blob/HEAD/Test%20Suite%20Runner/Kangax/compat-table.json) - [README.md](https://github.com/zhgabor/jurassic/blob/HEAD/README.md)The Jurassic engine provides a comprehensive implementation of the ECMAScript 6 Proxy and Reflect objects. These features enable powerful metaprogramming capabilities, allowing developers to intercept and define custom behavior for fundamental language operations. The implementation is highly compliant with the ES6 specification, passing 33 out of 34 Proxy tests and 18 out of 20 Reflect tests from the Kangax compatibility table.
Proxy objects are used to define custom behavior for fundamental operations (e.g., property lookup, assignment, enumeration, function invocation). A proxy is an object that wraps another object (the "target") and intercepts operations performed on it. Reflect is a built-in object that provides static methods for these interceptable operations. These methods have the same names as the proxy handler "traps" and are often used within traps to forward the operation to the target.
Sources: README.md, Test Suite Runner/Kangax/compat-table.json
Proxy
The Proxy implementation in Jurassic revolves around two main classes: ProxyConstructor and ProxyObject.
ProxyConstructor: Represents theProxyconstructor function in JavaScript. It is responsible for creating newProxyinstances.ProxyObject: Represents an instance of aProxy. This object wraps the target and handler, and its overridden internal methods contain the logic to invoke the appropriate handler traps.
Sources: Jurassic/Library/Proxy/ProxyObject.cs
Proxy Creation
A proxy is created using the new Proxy(target, handler) constructor. The constructor validates that both target and handler are objects.
This diagram shows the flow for creating a new Proxy instance.
Sources: Jurassic/Library/Proxy/ProxyObject.cs:33-51
Jurassic also implements Proxy.revocable(target, handler), which returns an object containing both the proxy and a revoke function. Calling revoke() disables the proxy, and any subsequent operations on it will throw a TypeError.
Sources: Jurassic/Library/Proxy/ProxyObject.cs:53-76, Unit Tests/Library/ProxyTests.cs:400-410
Proxy Traps
Proxy traps are methods on the handler object that intercept operations on the proxy. The ProxyObject class overrides internal methods to check for and call these traps. If a trap is not defined on the handler, the operation is forwarded to the target object.
The following diagram illustrates the sequence for a property access operation (proxy.foo) which triggers the get trap.
The table below summarizes the implemented proxy traps and their corresponding internal methods in ProxyObject.
| Trap Name | ProxyObject Method | Description |
|---|---|---|
getPrototypeOf | GetPrototype() | Intercepts Object.getPrototypeOf(). |
setPrototypeOf | SetPrototype() | Intercepts Object.setPrototypeOf(). |
isExtensible | IsExtensible() | Intercepts Object.isExtensible(). |
preventExtensions | PreventExtensions() | Intercepts Object.preventExtensions(). |
getOwnPropertyDescriptor | GetOwnPropertyDescriptor() | Intercepts Object.getOwnPropertyDescriptor(). |
defineProperty | DefineProperty() | Intercepts Object.defineProperty(). |
has | HasProperty() | Intercepts the in operator. |
get | GetPropertyValue() | Intercepts property getting. |
set | SetPropertyValue() | Intercepts property setting. |
deleteProperty | Delete() | Intercepts the delete operator. |
ownKeys | EnumerateProperties() | Intercepts Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), and Reflect.ownKeys(). |
apply | Call() | Intercepts a function call. |
construct | Construct() | Intercepts the new operator. |
Sources: Jurassic/Library/Proxy/ProxyObject.cs, Test Suite Runner/Kangax/compat-table.json
Invariants
Proxy traps are not allowed to violate fundamental language invariants. For example, a property that is non-writable and non-configurable on the target object cannot be reported as having a different value by the proxy's get trap. The ProxyObject implementation enforces these invariants.
// Example from SetPropertyValue, checking for non-writable properties.
if (existingDescriptor != null && existingDescriptor.IsDataDescriptor &&
existingDescriptor.IsConfigurable == false && existingDescriptor.IsWritable == false)
{
if (TypeComparer.SameValue(value, existingDescriptor.Value) == false)
throw new JavaScriptException(ErrorType.TypeError, "Proxy handler returned false for a non-writable, non-configurable property.");
}
Sources: Jurassic/Library/Proxy/ProxyObject.cs:302-307, Test Suite Runner/Kangax/compat-table.json
Reflect
The Reflect object is a built-in static object that provides methods for interceptable JavaScript operations. These methods correspond directly to the proxy traps and allow for forwarding the original operation to the target from within a handler.
The implementation consists of ReflectObject (in Reflect.cs), which defines the logic for each method, and Reflect.g.cs, which contains auto-generated stubs for binding these C# methods to the JavaScript Reflect object.
Sources: Jurassic/Library/Reflect/Reflect.cs, Jurassic/Library/Reflect/Reflect.g.cs
Reflect Methods
The following table lists the methods available on the Reflect object.
| Method | Description |
|---|---|
Reflect.apply(target, thisArgument, argumentsList) | Calls a function with a specified this value and arguments. |
Reflect.construct(target, argumentsList, [newTarget]) | Acts like the new operator. |
Reflect.defineProperty(target, propertyKey, attributes) | Similar to Object.defineProperty(). Returns a Boolean. |
Reflect.deleteProperty(target, propertyKey) | Acts like the delete operator. |
Reflect.get(target, propertyKey, [receiver]) | Gets a property from an object. |
Reflect.getOwnPropertyDescriptor(target, propertyKey) | Similar to Object.getOwnPropertyDescriptor(). |
Reflect.getPrototypeOf(target) | Same as Object.getPrototypeOf(). |
Reflect.has(target, propertyKey) | Acts like the in operator. |
Reflect.isExtensible(target) | Same as Object.isExtensible(). |
Reflect.ownKeys(target) | Returns an array of the target object's own property keys. |
Reflect.preventExtensions(target) | Same as Object.preventExtensions(). |
Reflect.set(target, propertyKey, value, [receiver]) | Sets a property on an object. |
Reflect.setPrototypeOf(target, prototype) | Same as Object.setPrototypeOf(). |
Sources: Jurassic/Library/Reflect/Reflect.g.cs:11-23, Jurassic/Library/Reflect/Reflect.cs, Unit Tests/Library/ReflectTests.cs
Relationship with Proxy
Reflect methods are particularly useful inside proxy traps to forward the intercepted operation to the target. This pattern ensures that the default behavior is applied correctly, including the proper handling of the receiver (the this value).
var target = {
_val: 0,
get value() { return this._val; },
set value(v) { this._val = v; }
};
var handler = {
get(target, key, receiver) {
console.log(`Getting ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`Setting ${key} to ${value}`);
return Reflect.set(target, key, value, receiver);
}
};
var proxy = new Proxy(target, handler);
proxy.value = 5; // Logs "Setting value to 5"
console.log(proxy.value); // Logs "Getting value", then prints 5
The diagram below shows how Reflect.get is used within a proxy's get trap.
This flow ensures that even complex operations like getter functions on the target are invoked with the correct this context (receiver).
Sources: Jurassic/Library/Proxy/ProxyObject.cs, Jurassic/Library/Reflect/Reflect.cs
Testing and Compatibility
The correctness of the Proxy and Reflect implementations is verified by a suite of unit tests. These tests cover the functionality of each trap and Reflect method, as well as the enforcement of invariants. Many of these tests are derived from the Kangax ES6 compatibility table, ensuring a high degree of compliance with the ECMAScript standard.
For example, the ProxyTests.cs file includes tests for constructor behavior, all handler traps, and revocable proxies. Similarly, ReflectTests.cs validates each method on the Reflect object.
Sources: Unit Tests/Library/ProxyTests.cs, Unit Tests/Library/ReflectTests.cs, Test Suite Runner/Kangax/compat-table.json
Conclusion
Jurassic's implementation of Proxy and Reflect provides a powerful and standards-compliant toolset for metaprogramming. By allowing the interception and customization of fundamental object operations, these features enable advanced patterns such as object virtualization, logging, and data binding directly within the .NET-based JavaScript engine. The close relationship between Proxy traps and Reflect methods provides a clean and reliable way to augment or observe object behavior while preserving default semantics.