Skip to main content
โšก Calmops

Reflect API: Metaprogramming Fundamentals in JavaScript

Reflect API: Metaprogramming Fundamentals in JavaScript

The Reflect API provides methods for interceptable JavaScript operations. This article covers reflection, introspection, and practical metaprogramming patterns.

Introduction

The Reflect API enables:

  • Introspection of objects
  • Dynamic property access
  • Method invocation
  • Object construction
  • Metaprogramming patterns

Understanding Reflect helps you:

  • Build advanced abstractions
  • Implement frameworks
  • Create dynamic systems
  • Understand language internals

Reflect API Basics

Reflect Methods

// Reflect provides static methods for common operations
// Each method corresponds to a language operation

// Reflect.get() - Get property value
const obj = { name: 'John', age: 30 };
console.log(Reflect.get(obj, 'name')); // 'John'

// Reflect.set() - Set property value
Reflect.set(obj, 'age', 31);
console.log(obj.age); // 31

// Reflect.has() - Check if property exists
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'email')); // false

// Reflect.deleteProperty() - Delete property
Reflect.deleteProperty(obj, 'age');
console.log(Reflect.has(obj, 'age')); // false

Reflect vs Traditional Operations

// Traditional way
const obj = { x: 1 };
obj.x = 2;
delete obj.x;

// Reflect way
const obj = { x: 1 };
Reflect.set(obj, 'x', 2);
Reflect.deleteProperty(obj, 'x');

// Reflect returns boolean indicating success
const result = Reflect.set(obj, 'x', 2);
console.log(result); // true

Property Descriptors

Getting Property Descriptors

// โœ… Good: Get property descriptor
const obj = { name: 'John' };

const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
//   value: 'John',
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Defining Property Descriptors

// โœ… Good: Define property with descriptor
const obj = {};

Reflect.defineProperty(obj, 'name', {
  value: 'John',
  writable: false,
  enumerable: true,
  configurable: false
});

console.log(obj.name); // 'John'
obj.name = 'Jane'; // Fails silently in non-strict mode
console.log(obj.name); // 'John'

Practical Property Descriptor Example

// โœ… Good: Create read-only properties
class User {
  constructor(name, email) {
    Reflect.defineProperty(this, 'name', {
      value: name,
      writable: false,
      enumerable: true,
      configurable: false
    });

    Reflect.defineProperty(this, 'email', {
      value: email,
      writable: false,
      enumerable: true,
      configurable: false
    });
  }
}

const user = new User('John', '[email protected]');
console.log(user.name); // 'John'
user.name = 'Jane'; // Fails silently
console.log(user.name); // 'John'

Object Introspection

Getting Object Information

// โœ… Good: Introspect object properties
const obj = { a: 1, b: 2, c: 3 };

// Get all own property names
const keys = Reflect.ownKeys(obj);
console.log(keys); // ['a', 'b', 'c']

// Get property names (enumerable only)
const enumerableKeys = Object.keys(obj);
console.log(enumerableKeys); // ['a', 'b', 'c']

// Get all property names including non-enumerable
const allKeys = Reflect.ownKeys(obj);
console.log(allKeys); // ['a', 'b', 'c']

Checking Object Characteristics

// โœ… Good: Check object characteristics
const obj = { x: 1 };

// Check if extensible (can add properties)
console.log(Reflect.isExtensible(obj)); // true

// Prevent extensions
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // false

// Try to add property
const result = Reflect.set(obj, 'y', 2);
console.log(result); // false (failed)
console.log(obj.y); // undefined

Function and Constructor Reflection

Calling Functions

// โœ… Good: Call function with Reflect
function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}

// Traditional way
const result1 = greet('Hello', 'John');

// Reflect way
const result2 = Reflect.apply(greet, null, ['Hello', 'John']);

console.log(result1); // 'Hello, John!'
console.log(result2); // 'Hello, John!'

Constructing Objects

// โœ… Good: Construct objects with Reflect
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

// Traditional way
const user1 = new User('John', '[email protected]');

// Reflect way
const user2 = Reflect.construct(User, ['John', '[email protected]']);

console.log(user1); // User { name: 'John', email: '[email protected]' }
console.log(user2); // User { name: 'John', email: '[email protected]' }

Practical Constructor Example

// โœ… Good: Dynamic object construction
function createInstance(Constructor, args) {
  if (!Reflect.isExtensible(Constructor.prototype)) {
    throw new Error('Constructor prototype is not extensible');
  }

  return Reflect.construct(Constructor, args);
}

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
}

const product = createInstance(Product, ['Laptop', 999]);
console.log(product); // Product { name: 'Laptop', price: 999 }

Prototype Chain Introspection

Getting and Setting Prototype

// โœ… Good: Introspect prototype chain
const obj = { x: 1 };

// Get prototype
const proto = Reflect.getPrototypeOf(obj);
console.log(proto); // Object.prototype

// Set prototype
const newProto = { y: 2 };
Reflect.setPrototypeOf(obj, newProto);

console.log(obj.y); // 2 (inherited from new prototype)

Practical Prototype Example

// โœ… Good: Create object with specific prototype
const animalProto = {
  speak() {
    return `${this.name} makes a sound`;
  }
};

const dogProto = Reflect.create(animalProto, {
  bark: {
    value() {
      return `${this.name} barks`;
    }
  }
});

const dog = Reflect.create(dogProto);
dog.name = 'Rex';

console.log(dog.speak()); // 'Rex makes a sound'
console.log(dog.bark()); // 'Rex barks'

Practical Metaprogramming Patterns

Object Validation

// โœ… Good: Validate object structure
function validateObject(obj, schema) {
  for (const [key, type] of Object.entries(schema)) {
    if (!Reflect.has(obj, key)) {
      throw new Error(`Missing property: ${key}`);
    }

    const value = Reflect.get(obj, key);
    if (typeof value !== type) {
      throw new Error(`Invalid type for ${key}: expected ${type}, got ${typeof value}`);
    }
  }

  return true;
}

// Usage
const schema = {
  name: 'string',
  age: 'number',
  email: 'string'
};

const user = { name: 'John', age: 30, email: '[email protected]' };
validateObject(user, schema); // OK

const invalid = { name: 'John', age: '30' };
validateObject(invalid, schema); // Error: Invalid type for age

Object Cloning

// โœ… Good: Deep clone object using Reflect
function deepClone(obj, visited = new WeakSet()) {
  if (visited.has(obj)) return obj; // Circular reference
  visited.add(obj);

  const cloned = Reflect.construct(Object, []);

  for (const key of Reflect.ownKeys(obj)) {
    const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);
    const value = Reflect.get(obj, key);

    if (typeof value === 'object' && value !== null) {
      descriptor.value = deepClone(value, visited);
    }

    Reflect.defineProperty(cloned, key, descriptor);
  }

  return cloned;
}

// Usage
const original = { a: 1, b: { c: 2 } };
const clone = deepClone(original);

clone.b.c = 3;
console.log(original.b.c); // 2 (unchanged)
console.log(clone.b.c); // 3

Object Serialization

// โœ… Good: Serialize object using Reflect
function serialize(obj) {
  const result = {};

  for (const key of Reflect.ownKeys(obj)) {
    const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);

    if (descriptor.enumerable) {
      const value = Reflect.get(obj, key);

      if (typeof value === 'object' && value !== null) {
        result[key] = serialize(value);
      } else if (typeof value !== 'function') {
        result[key] = value;
      }
    }
  }

  return result;
}

// Usage
const user = {
  name: 'John',
  age: 30,
  email: '[email protected]',
  greet() {
    return 'Hello';
  }
};

const serialized = serialize(user);
console.log(JSON.stringify(serialized));
// {"name":"John","age":30,"email":"[email protected]"}

Mixin Pattern

// โœ… Good: Apply mixins using Reflect
function applyMixin(target, mixin) {
  for (const key of Reflect.ownKeys(mixin)) {
    if (key !== 'constructor') {
      const descriptor = Reflect.getOwnPropertyDescriptor(mixin, key);
      Reflect.defineProperty(target, key, descriptor);
    }
  }

  return target;
}

// Usage
const canEat = {
  eat() {
    return `${this.name} is eating`;
  }
};

const canWalk = {
  walk() {
    return `${this.name} is walking`;
  }
};

class Animal {
  constructor(name) {
    this.name = name;
  }
}

applyMixin(Animal.prototype, canEat);
applyMixin(Animal.prototype, canWalk);

const dog = new Animal('Rex');
console.log(dog.eat()); // 'Rex is eating'
console.log(dog.walk()); // 'Rex is walking'

Best Practices

  1. Use Reflect for consistency:

    // โœ… Good
    Reflect.set(obj, 'x', 1);
    Reflect.get(obj, 'x');
    
  2. Check return values:

    // โœ… Good
    const success = Reflect.set(obj, 'x', 1);
    if (!success) {
      console.error('Failed to set property');
    }
    
  3. Use for metaprogramming:

    // โœ… Good
    const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'x');
    
  4. Combine with Proxy:

    // โœ… Good
    const handler = {
      get(target, prop) {
        return Reflect.get(target, prop);
      }
    };
    

Common Mistakes

  1. Ignoring return values:

    // โŒ Bad
    Reflect.set(obj, 'x', 1);
    
    // โœ… Good
    const success = Reflect.set(obj, 'x', 1);
    
  2. Not handling errors:

    // โŒ Bad
    Reflect.construct(Constructor, args);
    
    // โœ… Good
    try {
      Reflect.construct(Constructor, args);
    } catch (error) {
      console.error('Construction failed:', error);
    }
    
  3. Mixing Reflect and traditional operations:

    // โŒ Bad - inconsistent
    obj.x = 1;
    Reflect.get(obj, 'y');
    
    // โœ… Good - consistent
    Reflect.set(obj, 'x', 1);
    Reflect.get(obj, 'y');
    

Summary

The Reflect API is powerful for metaprogramming. Key takeaways:

  • Reflect provides methods for language operations
  • Use for introspection and dynamic access
  • Check return values for success/failure
  • Combine with Proxy for advanced patterns
  • Enables metaprogramming and frameworks
  • Consistent API for object manipulation

Next Steps

Comments