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.

JavaScriptadvanced
17 min read

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

javascriptjavascript
// 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

javascriptjavascript
// 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

javascriptjavascript
// 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); // 42

Reflect.construct and Class Patterns

javascriptjavascript
// 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

javascriptjavascript
// 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 MethodLegacy EquivalentKey Difference
Reflect.getobj[prop]Supports explicit receiver
Reflect.setobj[prop] = valReturns boolean, supports receiver
Reflect.hasprop in objCallable as function
Reflect.deletePropertydelete obj[prop]Returns boolean reliably
Reflect.ownKeysgetOwnPropertyNames + SymbolsAll keys in one call
Reflect.definePropertyObject.definePropertyReturns boolean, no throw
Reflect.constructnew Target(...args)Supports newTarget parameter
Reflect.applyfn.apply(this, args)Not affected by prototype pollution
Rune AI

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
RunePowered by Rune AI

Frequently Asked Questions

Why was the Reflect API added to JavaScript?

Reflect was introduced in ES2015 alongside Proxy to provide a clean, consistent API for object operations. Before Reflect, these operations were scattered across operators (`in`, `delete`), Object methods (`Object.defineProperty`), and Function methods (`Function.prototype.apply`). Reflect unifies them as static methods with consistent error handling (returning booleans instead of throwing) and maps 1-to-1 with Proxy traps for clean forwarding semantics.

When should I use Reflect instead of direct object access?

Use Reflect in Proxy trap handlers (always), when you need to check if an operation succeeded without try-catch, when you need the receiver parameter for correct getter/setter behavior in inheritance chains, and when you want protection against prototype pollution (Reflect.apply is safer than fn.apply). For general application code where you are not writing proxies or meta-programming utilities, direct object access is simpler and equally performant.

Does Reflect have any performance overhead compared to direct access?

Reflect methods have minimal overhead compared to direct property access in most engines. V8 optimizes common Reflect calls to be nearly equivalent to their direct counterparts. The main case where you might notice a difference is in extremely tight loops performing millions of property accesses. In proxy trap handlers, Reflect is the recommended approach and the engine expects it, so there is no practical penalty.

Can Reflect methods be used without Proxy?

Yes. Reflect is a standalone API that works independently of Proxy. You can use `Reflect.ownKeys()` for complete property enumeration, `Reflect.defineProperty()` for safe property definition (returns boolean instead of throwing), `Reflect.construct()` for advanced class instantiation, and `Reflect.apply()` for safe function invocation. Many utility libraries use Reflect methods without any Proxy involvement.

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.