JavaScript Reflect API Advanced Architecture
Master the JavaScript Reflect API for meta-programming. Covers all 13 Reflect methods, their relationship to Proxy traps, proper forwarding semantics, prototype-safe operations, property descriptor manipulation, function invocation patterns, and architectural best practices.
The Reflect API provides 13 static methods that mirror the internal operations of JavaScript objects. Unlike legacy approaches (Object methods, operators), Reflect methods have consistent return values, proper error handling, and map directly to Proxy traps for clean forwarding patterns.
For how Reflect works with Proxy, see Using Reflect and Proxy Together in JavaScript.
All 13 Reflect Methods
// Reflect provides one method for each Proxy trap
// Each method performs the same operation as the corresponding
// internal object operation, but as a regular function call
const target = { x: 1, y: 2 };
// 1. Reflect.get(target, property, receiver?)
// Equivalent to: target[property]
console.log(Reflect.get(target, "x")); // 1
// 2. Reflect.set(target, property, value, receiver?)
// Equivalent to: target[property] = value
Reflect.set(target, "z", 3);
// 3. Reflect.has(target, property)
// Equivalent to: property in target
console.log(Reflect.has(target, "x")); // true
// 4. Reflect.deleteProperty(target, property)
// Equivalent to: delete target[property]
Reflect.deleteProperty(target, "z");
// 5. Reflect.ownKeys(target)
// Returns ALL own keys (strings + symbols, enumerable and non-enumerable)
console.log(Reflect.ownKeys(target)); // ["x", "y"]
// 6. Reflect.getOwnPropertyDescriptor(target, property)
// Equivalent to: Object.getOwnPropertyDescriptor(target, property)
console.log(Reflect.getOwnPropertyDescriptor(target, "x"));
// { value: 1, writable: true, enumerable: true, configurable: true }
// 7. Reflect.defineProperty(target, property, descriptor)
// Equivalent to: Object.defineProperty(target, property, descriptor)
// But returns boolean instead of throwing
Reflect.defineProperty(target, "readonly", {
value: 42,
writable: false,
enumerable: true,
configurable: false
});
// 8. Reflect.getPrototypeOf(target)
// Equivalent to: Object.getPrototypeOf(target)
console.log(Reflect.getPrototypeOf(target)); // Object.prototype
// 9. Reflect.setPrototypeOf(target, proto)
// Equivalent to: Object.setPrototypeOf(target, proto)
// Returns boolean instead of throwing
const proto = { greet() { return "hello"; } };
Reflect.setPrototypeOf(target, proto);
// 10. Reflect.isExtensible(target)
// Equivalent to: Object.isExtensible(target)
console.log(Reflect.isExtensible(target)); // true
// 11. Reflect.preventExtensions(target)
// Equivalent to: Object.preventExtensions(target)
// Returns boolean
Reflect.preventExtensions(target);
// 12. Reflect.apply(target, thisArg, args)
// Equivalent to: Function.prototype.apply.call(target, thisArg, args)
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
console.log(Reflect.apply(greet, { name: "Alice" }, ["Hello"]));
// "Hello, Alice"
// 13. Reflect.construct(target, args, newTarget?)
// Equivalent to: new target(...args)
class Point {
constructor(x, y) { this.x = x; this.y = y; }
}
const point = Reflect.construct(Point, [10, 20]);
console.log(point); // Point { x: 10, y: 20 }Reflect vs Legacy Approaches
// PROBLEM 1: Object.defineProperty throws on failure
// Reflect.defineProperty returns false
const frozen = Object.freeze({ x: 1 });
// Legacy: throws TypeError
try {
Object.defineProperty(frozen, "y", { value: 2 });
} catch (e) {
console.log("Threw:", e.message);
}
// Reflect: returns false
const success = Reflect.defineProperty(frozen, "y", { value: 2 });
console.log("Success:", success); // false
// PROBLEM 2: delete operator returns true even for non-configurable
// Reflect.deleteProperty returns false for non-configurable
const obj = {};
Object.defineProperty(obj, "fixed", {
value: 42,
configurable: false
});
// Legacy: returns false silently (or throws in strict mode)
// delete obj.fixed; // false or TypeError
// Reflect: returns false
console.log(Reflect.deleteProperty(obj, "fixed")); // false
// PROBLEM 3: in operator cannot be called as a function
// Reflect.has works as a function (composable)
const hasProperty = (obj, key) => Reflect.has(obj, key);
// PROBLEM 4: Function.prototype.apply is verbose and hackable
// Reflect.apply is cleaner
// Legacy (vulnerable to prototype pollution):
// Function.prototype.apply.call(fn, thisArg, args);
// Or even worse if someone modified Function.prototype:
// fn.apply(thisArg, args);
// Reflect (always safe):
function fn() { return "test"; }
const thisArg = null;
const args = [];
Reflect.apply(fn, thisArg, args);
// COMPARISON TABLE (code examples)
const examples = {
"Property check": {
legacy: "'x' in obj",
reflect: "Reflect.has(obj, 'x')"
},
"Property delete": {
legacy: "delete obj.x",
reflect: "Reflect.deleteProperty(obj, 'x')"
},
"Define property": {
legacy: "try { Object.defineProperty(...) } catch(e) { ... }",
reflect: "if (!Reflect.defineProperty(...)) { ... }"
},
"Get prototype": {
legacy: "Object.getPrototypeOf(obj)",
reflect: "Reflect.getPrototypeOf(obj)"
},
"All keys": {
legacy: "Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))",
reflect: "Reflect.ownKeys(obj)"
}
};Receiver Parameter Deep Dive
// The 'receiver' parameter in Reflect.get/set controls 'this'
// for getter/setter property descriptors
// WITHOUT receiver: getter 'this' is the target
const base = {
_name: "Base",
get name() {
return this._name;
}
};
const child = Object.create(base);
child._name = "Child";
// Direct access uses the correct 'this'
console.log(child.name); // "Child" (this = child)
// Reflect.get without receiver
console.log(Reflect.get(base, "name")); // "Base" (this = base)
// Reflect.get WITH receiver
console.log(Reflect.get(base, "name", child)); // "Child" (this = child)
// WHY THIS MATTERS IN PROXY TRAPS
const handler = {
get(target, property, receiver) {
// WRONG: ignores receiver, breaks getter inheritance
// return target[property];
// CORRECT: passes receiver so getters use the right 'this'
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
// WRONG: ignores receiver, breaks setter inheritance
// target[property] = value;
// return true;
// CORRECT: passes receiver for proper setter behavior
return Reflect.set(target, property, value, receiver);
}
};
// EXAMPLE: Why receiver matters for inherited proxied objects
class Animal {
#sound;
constructor(sound) {
this.#sound = sound;
}
get sound() {
return this.#sound;
}
set sound(value) {
this.#sound = value;
}
}
// If we proxy an Animal and ignore receiver:
// const proxy = new Proxy(animal, {
// get(target, property) {
// return target[property]; // 'this' is target, not proxy
// }
// });
// This works for public properties but can break inheritance chains
// RECEIVER WITH SETTER
const withSetter = {
_value: 0,
set value(v) {
this._value = v; // 'this' should be the receiver
},
get value() {
return this._value;
}
};
const proxyObj = new Proxy(withSetter, {
set(target, prop, value, receiver) {
// Passes receiver so setter's 'this' is the proxy
return Reflect.set(target, prop, value, receiver);
},
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});
proxyObj.value = 42;
console.log(proxyObj.value); // 42Reflect.construct and Class Patterns
// Reflect.construct enables advanced class instantiation patterns
// PATTERN 1: Creating instances of one class with another's prototype
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog {
bark() {
return `${this.name} says woof!`;
}
}
// Create an Animal instance that has Dog's prototype
const dogLikeAnimal = Reflect.construct(
Animal, // Constructor to call
["Rex"], // Arguments
Dog // newTarget (determines prototype)
);
console.log(dogLikeAnimal.name); // "Rex" (from Animal constructor)
console.log(dogLikeAnimal.bark()); // "Rex says woof!" (from Dog.prototype)
console.log(dogLikeAnimal instanceof Dog); // true
console.log(dogLikeAnimal instanceof Animal); // false
// PATTERN 2: Factory with dynamic class selection
class Button {
constructor(label) { this.label = label; this.type = "button"; }
}
class Link {
constructor(label) { this.label = label; this.type = "link"; }
}
class IconButton {
constructor(label) { this.label = label; this.type = "icon-button"; }
}
function createElement(type, ...args) {
const classes = { button: Button, link: Link, "icon-button": IconButton };
const Constructor = classes[type];
if (!Constructor) {
throw new Error(`Unknown element type: ${type}`);
}
return Reflect.construct(Constructor, args);
}
const btn = createElement("button", "Submit");
const link = createElement("link", "Home");
console.log(btn.type); // "button"
console.log(link.type); // "link"
// PATTERN 3: Mixins with Reflect.construct
function applyMixins(target, mixins) {
for (const mixin of mixins) {
const instance = Reflect.construct(mixin, []);
const descriptors = Object.getOwnPropertyDescriptors(
Reflect.getPrototypeOf(instance)
);
for (const [key, descriptor] of Object.entries(descriptors)) {
if (key !== "constructor") {
Reflect.defineProperty(target.prototype, key, descriptor);
}
}
}
return target;
}
class Serializable {
toJSON() {
return JSON.stringify(Reflect.ownKeys(this).reduce((acc, key) => {
if (typeof key === "string") acc[key] = this[key];
return acc;
}, {}));
}
}
class Validatable {
validate() {
for (const key of Reflect.ownKeys(this)) {
if (this[key] === undefined) return false;
}
return true;
}
}
class UserModel {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
applyMixins(UserModel, [Serializable, Validatable]);
const user = new UserModel("Alice", "alice@example.com");
console.log(user.validate()); // true
console.log(user.toJSON()); // '{"name":"Alice","email":"alice@example.com"}'Reflect.ownKeys for Complete Enumeration
// Reflect.ownKeys returns ALL own property keys including:
// - String keys (enumerable and non-enumerable)
// - Symbol keys
// This combines Object.getOwnPropertyNames + Object.getOwnPropertySymbols
const sym1 = Symbol("id");
const sym2 = Symbol("secret");
const obj = {
visible: true,
[sym1]: 123
};
Object.defineProperty(obj, "hidden", {
value: "not enumerable",
enumerable: false
});
Object.defineProperty(obj, sym2, {
value: "secret symbol",
enumerable: false
});
// Different enumeration methods return different subsets:
console.log(Object.keys(obj));
// ["visible"] - only enumerable string keys
console.log(Object.getOwnPropertyNames(obj));
// ["visible", "hidden"] - all string keys (enum + non-enum)
console.log(Object.getOwnPropertySymbols(obj));
// [Symbol(id), Symbol(secret)] - all symbol keys
console.log(Reflect.ownKeys(obj));
// ["visible", "hidden", Symbol(id), Symbol(secret)] - EVERYTHING
// USE CASE: Deep cloning with complete property preservation
function deepClone(source) {
if (source === null || typeof source !== "object") {
return source;
}
const clone = Array.isArray(source) ? [] : {};
// Copy prototype
Reflect.setPrototypeOf(clone, Reflect.getPrototypeOf(source));
// Copy ALL properties with their descriptors
for (const key of Reflect.ownKeys(source)) {
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
if (descriptor.value !== undefined) {
descriptor.value = deepClone(descriptor.value);
}
Reflect.defineProperty(clone, key, descriptor);
}
return clone;
}
const original = { a: 1, nested: { b: 2 } };
Object.defineProperty(original, "immutable", {
value: 42,
writable: false,
enumerable: false
});
const cloned = deepClone(original);
console.log(cloned.a); // 1
console.log(cloned.nested.b); // 2
console.log(Reflect.getOwnPropertyDescriptor(cloned, "immutable"));
// { value: 42, writable: false, enumerable: false, configurable: false }| Reflect Method | Legacy Equivalent | Key Difference |
|---|---|---|
Reflect.get | obj[prop] | Supports explicit receiver |
Reflect.set | obj[prop] = val | Returns boolean, supports receiver |
Reflect.has | prop in obj | Callable as function |
Reflect.deleteProperty | delete obj[prop] | Returns boolean reliably |
Reflect.ownKeys | getOwnPropertyNames + Symbols | All keys in one call |
Reflect.defineProperty | Object.defineProperty | Returns boolean, no throw |
Reflect.construct | new Target(...args) | Supports newTarget parameter |
Reflect.apply | fn.apply(this, args) | Not affected by prototype pollution |
Rune AI
Key Insights
- Reflect provides 13 methods mapping 1-to-1 with Proxy traps, enabling clean forwarding in trap handlers: Always use Reflect in proxy traps instead of direct object operations
- Reflect methods return booleans for success/failure instead of throwing exceptions: This eliminates try-catch boilerplate for operations like defineProperty and deleteProperty
- The receiver parameter in Reflect.get and Reflect.set controls getter/setter this-binding for correct inheritance: Omitting receiver in proxy traps breaks inherited accessors
- Reflect.ownKeys returns all own property keys (strings and symbols, enumerable and non-enumerable) in a single call: This replaces the combination of Object.getOwnPropertyNames and Object.getOwnPropertySymbols
- Reflect.construct's third parameter (newTarget) enables creating instances with one constructor's logic but another class's prototype: This supports advanced mixin and factory patterns
Frequently Asked Questions
Why was the Reflect API added to JavaScript?
When should I use Reflect instead of direct object access?
Does Reflect have any performance overhead compared to direct access?
Can Reflect methods be used without Proxy?
Conclusion
The Reflect API provides 13 methods that mirror JavaScript's internal object operations with consistent return values and proper semantics. It replaces scattered operators and Object methods with a unified interface. The receiver parameter ensures correct getter/setter behavior in proxy chains. Reflect.construct enables advanced instantiation patterns. For combining Reflect with Proxy, see Using Reflect and Proxy Together in JavaScript. For the Proxy API fundamentals, explore Advanced JavaScript Proxies Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.