Интроспекция и рефлексия в 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 строки + символы вместе надо два вызова
Назначение для метапрограммирования для бытовых задач

Зачем рефлексия

  1. Сериализация/десериализация — обход полей объекта по дескрипторам.
  2. DI-контейнеры — собирают зависимости по интроспекции конструктора.
  3. ORM/Validator — читают метаданные классов через decorators + Reflect.
  4. Proxy и метапрограммирование — Reflect handler-методы 1:1 с internal methods.
  5. Тестирование/моки — Reflect.set/get обходят private (в TS).

Минусы

  • Замедляет JIT — V8 не может оптимизировать рефлексивный код
  • Ломает скрытые классы при динамическом добавлении полей
  • Усложняет рефакторинг (имена в строках)
  • Не находится статическим анализом (типы, IDE)

Источники

Связанные темы