Lesson overview · Free interview practice
Reflection
Explore advanced meta-programming concepts.
Topics in the full lesson
- Introduction to Reflection and Metaprogramming
- The Reflect API
- Using Reflect Methods
- Dynamic Property Access
- Metaprogramming with Proxies
- Best Practices
- Exercises
Practice Problems
Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.
Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.
Theory questions
The Immutability Ladder: `preventExtensions` / `seal` / `freeze`
JavaScript has three increasingly strict immutability operations on an object. What does each do, and what are the corresponding is* queries?
Take a moment to think about this before revealing the answer
Explain your reasoning or try an implementation before comparing answers.
Property Descriptors: Data vs Accessor
Every property in JavaScript has a descriptor. What are the two kinds, and how do you read and define them?
Take a moment to think about this before revealing the answer
Explain your reasoning or try an implementation before comparing answers.
`Reflect` API for Introspection
How does the Reflect API differ from the Object.* methods and the matching operators (in, delete)? When do you prefer Reflect?
Take a moment to think about this before revealing the answer
Explain your reasoning or try an implementation before comparing answers.
Object Introspection: `keys` / `values` / `entries` / Descriptors / `Reflect.ownKeys`
JavaScript has a small constellation of methods for introspecting an object's properties at runtime. What does each one cover, and when do you reach for which?
Take a moment to think about this before revealing the answer
Explain your reasoning or try an implementation before comparing answers.
Coding practice
Class Introspection and Prototype Chain Walking
Implement and check your solution in the stated environment before revealing the answer.
Implement describe(cls) to collect class member metadata across the prototype and constructor chains. Include the actual object that declares each member without invoking getters or constructing an instance.
Input and traversal contract
- Use ordinary user-defined classes or constructor functions with an own, object-valued data property named
prototype. Their inheritance chains are ordinary and stable while inspected. Proxies, built-in subclasses, and manually rewired chains are outside this exercise. - Throw
TypeErrorwhen the input is not a function or lacks that object-valued own dataprototype. This is a structural guard, not proof that a value came from class syntax. - First walk from
cls.prototypetoward its ancestors. Stop beforeObject.prototypeor atnull. - Then walk from
clstoward its constructor ancestors. Stop beforeFunction.prototypeor atnull. - At each level, use
Reflect.ownKeysorder. Include non-enumerable keys and preserve symbol identity. - Keep every declaration, including overridden names at different levels. Do not collapse two symbols that happen to have the same description.
- On the prototype side, skip only the string key
'constructor'. On the static side, skip only'name','length', and'prototype', even if one was deliberately redefined. Other string and symbol keys remain eligible. - Include methods, accessors, and ordinary data properties. Do not inspect private fields or create an instance to discover instance fields.
Output contract
Return an array of rows with exactly these fields:
| Field | Meaning |
|---|---|
key | Original string or symbol key |
placement | 'prototype' or 'static' |
kind | 'method', 'accessor', or 'data' |
owner | The declaring object itself, such as Base.prototype or Base |
depth | Zero at the starting object of that walk, then one per ancestor |
descriptor | The full own property descriptor, including flags and function/value references |
A data descriptor with a function value is a method. Any accessor descriptor is an accessor, even if both its getter and setter are undefined. Other data descriptors are data.
Do not read a member through owner[key]. Metadata must not execute a getter, setter, constructor, or method. The output order is all prototype rows from nearest to farthest, followed by all static rows from nearest to farthest.
Review the host's descriptor and hierarchy bridge before this advanced extension. You need property descriptors, non-enumerable properties, symbol identity, and prototype traversal.
Local setup
Use a new local folder with Node.js 24.21.0. Save starter.mjs and checks.mjs.
node --version
cp starter.mjs solution.mjs
node checks.mjs
The starter intentionally fails. Implement describe in solution.mjs, then rerun the checks. There are no packages, browser APIs, or network calls.
For TypeScript, save starter.mts and this tsconfig.json. Use TypeScript 5.9.3 with strict checking.
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": [],
"noEmitOnError": true,
"outDir": "build"
},
"files": ["solution.mts"]
}
tsc --version
cp starter.mts solution.mts
tsc --project tsconfig.json
node checks.mjs ./build/solution.mjs
The .mts files compile to .mjs without a package manifest. Consult the course's environment setup guidance if a tool is unavailable.
Starters
JavaScript starter: starter.mjs
export function describe(cls) {
throw new Error('Implement describe')
}
TypeScript starter: starter.mts
type MemberRow = {
key: string | symbol
placement: 'prototype' | 'static'
kind: 'method' | 'accessor' | 'data'
owner: object
depth: number
descriptor: PropertyDescriptor
}
export function describe(cls: unknown): MemberRow[] {
throw new Error('Implement describe')
}
Visible checks
Save these checks as checks.mjs.
import assert from 'node:assert/strict'
import test from 'node:test'
const { describe } = await import(process.argv[2] ?? './solution.mjs')
function fixture() {
const first = Symbol('same')
const second = Symbol('same')
let effects = 0
class Base {
ownOnly = 'instance field'
#hidden = 1
constructor() { throw new Error('Do not construct') }
shared() {}
[first]() {}
get size() { effects += 1; return this.#hidden }
set size(value) { effects += 1 }
static from() {}
static get status() { effects += 1; throw new Error('Do not read') }
static category = 'base'
}
class Child extends Base {
shared() {}
[second]() {}
static from() {}
}
Object.defineProperty(Base.prototype, 'label', {
value: 'prototype data', enumerable: false, writable: false, configurable: true,
})
Object.defineProperty(Base.prototype, 'emptyAccessor', {
get: undefined, set: undefined, configurable: true,
})
return { Base, Child, first, second, effects: () => effects }
}
test('normal: metadata includes exact declaring owners, depths, and descriptors', () => {
const { Base, Child, effects } = fixture()
const rows = describe(Child)
const declarations = rows.filter(row => row.key === 'shared')
assert.equal(declarations.length, 2)
assert.equal(declarations[0].owner, Child.prototype)
assert.equal(declarations[0].depth, 0)
assert.equal(declarations[1].owner, Base.prototype)
assert.equal(declarations[1].depth, 1)
for (const row of rows) {
assert.deepEqual(Object.keys(row).sort(),
['depth', 'descriptor', 'key', 'kind', 'owner', 'placement'])
assert.deepEqual(row.descriptor, Object.getOwnPropertyDescriptor(row.owner, row.key))
}
assert.equal(declarations[0].kind, 'method')
assert.equal(declarations[0].placement, 'prototype')
assert.equal(declarations[0].descriptor.enumerable, false)
const from = rows.filter(row => row.key === 'from')
assert.deepEqual(from.map(row => [row.owner, row.depth, row.placement]),
[[Child, 0, 'static'], [Base, 1, 'static']])
assert.equal(effects(), 0)
})
test('edge: symbols with equal descriptions stay distinct and accessors never run', () => {
const { Base, Child, first, second, effects } = fixture()
const rows = describe(Child)
assert.equal(rows.find(row => row.key === first).owner, Base.prototype)
assert.equal(rows.find(row => row.key === second).owner, Child.prototype)
for (const key of ['size', 'status', 'emptyAccessor']) {
assert.equal(rows.find(row => row.key === key).kind, 'accessor')
}
const size = rows.find(row => row.key === 'size')
assert.equal(size.descriptor.get, Object.getOwnPropertyDescriptor(Base.prototype, 'size').get)
assert.equal(size.descriptor.set, Object.getOwnPropertyDescriptor(Base.prototype, 'size').set)
const label = rows.find(row => row.key === 'label')
assert.equal(label.kind, 'data')
assert.equal(label.descriptor.value, 'prototype data')
assert.equal(label.descriptor.writable, false)
assert.equal(rows.find(row => row.key === 'category').kind, 'data')
assert.equal(effects(), 0)
})
test('edge: row order follows both walks and stops before built-in roots', () => {
const { Base, Child } = fixture()
const rows = describe(Child)
const expected = [
...Reflect.ownKeys(Child.prototype).filter(key => key !== 'constructor')
.map(key => [Child.prototype, key]),
...Reflect.ownKeys(Base.prototype).filter(key => key !== 'constructor')
.map(key => [Base.prototype, key]),
...Reflect.ownKeys(Child).filter(key => !['name', 'length', 'prototype'].includes(key))
.map(key => [Child, key]),
...Reflect.ownKeys(Base).filter(key => !['name', 'length', 'prototype'].includes(key))
.map(key => [Base, key]),
]
assert.deepEqual(rows.map(row => [row.owner, row.key]), expected)
assert.equal(rows.some(row => row.owner === Object.prototype), false)
assert.equal(rows.some(row => row.owner === Function.prototype), false)
assert.equal(rows.some(row => row.key === 'ownOnly' || row.key === '#hidden'), false)
})
test('edge: exclusions depend on placement, not a global reserved-name set', () => {
class Named {
name() {}
length() {}
prototype() {}
static constructor() {}
}
const rows = describe(Named)
assert.deepEqual(rows.map(row => [row.key, row.placement]), [
['name', 'prototype'], ['length', 'prototype'], ['prototype', 'prototype'],
['constructor', 'static'],
])
})
test('edge: empty classes and a null-root prototype finish without construction', () => {
class Empty {}
class NullRoot extends null {
method() {}
}
assert.deepEqual(describe(Empty), [])
const rows = describe(NullRoot)
assert.equal(rows.length, 1)
assert.equal(rows[0].key, 'method')
assert.equal(rows[0].owner, NullRoot.prototype)
assert.equal(rows[0].depth, 0)
})
test('error: non-functions and functions without own object prototypes are rejected', () => {
class Sample {}
for (const invalid of [null, undefined, 1, 'Sample', {}, () => {}, Sample.bind(null)]) {
assert.throws(() => describe(invalid), TypeError)
}
})
Take a moment to think about this before revealing the answer
Explain your reasoning or try an implementation before comparing answers.
Explore the full Reflection material
Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.