Интроспекция и рефлексия в JavaScript
Рефлексия — программа исследует и модифицирует свою структуру в runtime (создаёт новые классы, типы, методы). Интроспекция — её подмножество, программа только читает структуру (узнаёт типы полей, наследование, контексты).
«При помощи рефлексии — строим новые структуры данных, классы, типы, функции. При помощи интроспекции — проходимся по уже созданным, узнаём, какие методы у класса, какие свойства у объекта», yvW1PjUVeM0.
Где живут инструменты
Всё для рефлексии и интроспекции сосредоточено в двух namespace:
| Namespace | Назначение |
|---|---|
Reflect |
новый стандартный API (ES6+), 1:1 с internal methods |
Object |
древний namespace, многие методы дублируются в Reflect |
Интроспекция: что читаем
Тип значения
typeof 5; // 'number' (примитив)
typeof new Number(5); // 'object' (боксированный)
typeof 'x'; // 'string'
typeof null; // 'object' (исторический баг)
typeof undefined; // 'undefined'
typeof function{}; // 'function'
instanceof и цепочка прототипов
class Animal {}
class Dog extends Animal {}
const r = new Dog();
r instanceof Dog; // true
r instanceof Animal; // true
r instanceof Object; // true
Object.getPrototypeOf(r) === Dog.prototype; // true
Dog.prototype.isPrototypeOf(r); // true
Боксированные vs литералы
typeof 5; // 'number'
typeof new Number(5); // 'object'
new Number(5) === 5; // false — сравниваются по ссылке!
new Number(5) === new Number(5); // false — разные объекты
(new Number(5)).valueOf(); // 5 — достаём примитив
Собственные ключи vs все ключи
const proto = { inherited: 1 };
const obj = Object.create(proto);
obj.own = 2;
Object.keys(obj); // ['own'] — только собственные enumerable
Object.getOwnPropertyNames(obj); // ['own'] — собственные включая non-enumerable
Object.getOwnPropertySymbols(obj); // — символьные ключи
Reflect.ownKeys(obj); // ['own'] — все собственные (строки + символы)
'inherited' in obj; // true — включая цепочку прототипов
obj.hasOwnProperty('inherited'); // false
Property descriptors
Object.getOwnPropertyDescriptor({ x: 1 }, 'x');
// { value: 1, writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptors(obj);
// все дескрипторы разом
Три способа задать класс
class Abstraction {} // new ES6
class Extended extends Abstraction {} // с предком
function Prototype() {} // ES5 функция-конструктор
Object.getPrototypeOf(Extended.prototype) === Abstraction.prototype; // true
Object.getPrototypeOf(Abstraction.prototype) === Object.prototype; // true
Object.getPrototypeOf(Prototype.prototype) === Object.prototype; // true
Рефлексия: что меняем
Создание класса в runtime
const className = 'DynamicUser';
const Cls = {
[className]: class {
constructor(name) { this.name = name; }
greet { return `Hi, ${this.name}`; }
}
}[className];
new Cls('Ivan').greet; // 'Hi, Ivan'
Cls.name; // 'DynamicUser'
Создание метода в runtime
class User { constructor(name) { this.name = name; } }
const methodName = 'shout';
User.prototype[methodName] = function {
return this.name.toUpperCase() + '!';
};
new User('Ivan').shout; // 'IVAN!'
Reflect API: операции 1:1 с internal methods
const obj = { x: 1 };
Reflect.get(obj, 'x'); // 1
Reflect.set(obj, 'y', 2); // true
Reflect.has(obj, 'x'); // true (как in)
Reflect.deleteProperty(obj, 'x'); // true
Reflect.ownKeys(obj); // ['y']
Reflect.defineProperty(obj, 'z', { value: 3, writable: false });
Reflect.getPrototypeOf(obj); // {} (Object.prototype)
Reflect.setPrototypeOf(obj, null); // true
Reflect.construct(class { constructor(x) { this.x = x; } }, [42]); // { x: 42 }
Reflect.apply(fn, thisArg, args);
Создание объекта через Reflect.construct
class A { constructor(x) { this.x = x; } }
class B { constructor(y) { this.y = y; } }
// Создать объект с конструктором A, но прототипом B
const obj = Reflect.construct(A, [1], B);
obj.x; // 1 (A заполнил)
obj instanceof B; // true (прототип от B)
obj instanceof A; // false
Reflect vs Object
| Reflect | Object | |
|---|---|---|
| Возврат при ошибке | false/исключение по семантике |
throw |
Reflect.get(o, k) |
работает в Proxy handler | нет аналога |
Reflect.ownKeys |
строки + символы вместе | надо два вызова |
| Назначение | для метапрограммирования | для бытовых задач |
Зачем рефлексия
- Сериализация/десериализация — обход полей объекта по дескрипторам.
- DI-контейнеры — собирают зависимости по интроспекции конструктора.
- ORM/Validator — читают метаданные классов через decorators + Reflect.
- Proxy и метапрограммирование — Reflect handler-методы 1:1 с internal methods.
- Тестирование/моки — Reflect.set/get обходят private (в TS).
Минусы
- Замедляет JIT — V8 не может оптимизировать рефлексивный код
- Ломает скрытые классы при динамическом добавлении полей
- Усложняет рефакторинг (имена в строках)
- Не находится статическим анализом (типы, IDE)
Источники
- Timur · Интроспекция и рефлексия в JavaScript (2019-05-28, 63 мин)
- Timur · Proxy и Symbol в JavaScript (2018-11-26)
- AsForJS · Что такое Object в JavaScript согласно официальной спецификации (2025-02-03)