Загляни внутрь себя: рефлексия и интроспекция в JavaScript

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое рефлексия и интроспекция в JavaScript и зачем это вам?

Представьте, что вы — программист-детектив. У вас есть объект, но вы не знаете, что у него внутри. Какие у него свойства? Какой он вообще природы — массив, функция или просто набор данных? В обычной жизни мы просто смотрим на код, но в динамическом мире JavaScript ваша программа должна уметь делать это сама. Это и есть рефлексия и интроспекция.

Эти термины звучат как философия, но на деле это мощные инструменты. Интроспекция — это когда код смотрит сам на себя: проверяет тип переменной, узнаёт имя функции или перечисляет свойства объекта. Рефлексия идёт дальше — это способность кода изменять себя во время выполнения: добавлять новые методы, менять прототипы или даже создавать прокси-объекты.

На первый взгляд может показаться, что это нужно только авторам сложных фреймворков. Но на самом деле каждый день вы используете интроспекцию, даже не замечая этого. Когда вы пишете typeof x === 'string' — вы уже занимаетесь интроспекцией. А когда используете Object.assign для копирования свойств — вы применяете рефлексию.

В этой статье мы разберём все ключевые методы и приёмы. Вы узнаете, как «заглянуть внутрь» любого объекта, как безопасно работать с неизвестными данными и как писать более гибкий и адаптивный код. Поехали!

Основы интроспекции: смотрим, что у нас в руках

Интроспекция начинается с простого вопроса: «Что это такое?». В JavaScript есть несколько встроенных операторов и методов, которые помогают ответить на этот вопрос.

Оператор typeof: быстрая проверка типа

Это ваш первый помощник. typeof возвращает строку, указывающую тип операнда. Он прост, но у него есть пара подводных камней.

console.log(typeof 42);        // "number"
console.log(typeof 'hello');   // "string"
console.log(typeof true);      // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null);      // "object" — вот он, знаменитый баг!
console.log(typeof []);        // "object" — а это уже не баг, а фича
console.log(typeof function(){}); // "function"

Обратите внимание: typeof null возвращает "object". Это историческая ошибка JavaScript, которую уже не исправят. typeof [] тоже возвращает "object", что логично, ведь массив — это подвид объекта. Но как тогда отличить массив от обычного объекта? Для этого есть другие инструменты.

Полезный лайфхак: typeof идеально подходит для проверки, определена ли переменная, без риска выбросить исключение ReferenceError.

if (typeof someGlobalVar !== 'undefined') {
  // Безопасно используем someGlobalVar
}

instanceof: проверка цепочки прототипов

Если typeof отвечает на вопрос «какой примитивный тип?», то instanceof отвечает на «является ли объектом определённого класса?». Он проверяет, присутствует ли прототип конструктора в цепочке прототипов объекта.

const arr = [1, 2, 3];
console.log(arr instanceof Array);  // true
console.log(arr instanceof Object); // true — потому что Array наследует от Object

const date = new Date();
console.log(date instanceof Date);   // true
console.log(date instanceof Object); // true

// А вот с примитивами не работает
console.log('hello' instanceof String); // false — примитив не объект

Важный нюанс: instanceof может дать сбой при работе с iframe или разными контекстами выполнения (например, в Node.js). В каждом контексте свой глобальный объект, и [] instanceof window.Array может вернуть false, если массив создан в другом фрейме. Для таких случаев лучше использовать Array.isArray().

Array.isArray() и Object.prototype.toString

Самый надёжный способ проверить, является ли значение массивом — это Array.isArray(). Он работает даже в разных контекстах.

console.log(Array.isArray([1,2,3])); // true
console.log(Array.isArray({}));      // false

А если вам нужно узнать точный внутренний тип любого объекта, используйте Object.prototype.toString.call(). Это универсальный «детектор лжи».

function getType(value) {
  return Object.prototype.toString.call(value).slice(8, -1);
}

console.log(getType([]));        // "Array"
console.log(getType({}));        // "Object"
console.log(getType(null));      // "Null"
console.log(getType(new Date())); // "Date"
console.log(getType(/regex/));   // "RegExp"

Этот метод возвращает строки вида "[object Array]", а мы просто отрезаем лишнее. Работает безотказно.

Глубокая интроспекция: изучаем структуру объектов

Теперь, когда мы знаем, с чем имеем дело, пора заглянуть внутрь объекта. Какие у него есть ключи? Наследуются ли они? Являются ли свойства перечисляемыми?

Object.keys(), Object.values(), Object.entries()

Это трио — ваши лучшие друзья для обхода собственных (не наследуемых) свойств объекта.

const user = {
  name: 'Алиса',
  age: 30,
  isAdmin: false
};

console.log(Object.keys(user));    // ["name", "age", "isAdmin"]
console.log(Object.values(user));  // ["Алиса", 30, false]
console.log(Object.entries(user)); // [["name", "Алиса"], ["age", 30], ["isAdmin", false]]

Все три метода возвращают только собственные перечисляемые свойства. Унаследованные методы (например, toString) в них не попадают. Это делает их идеальными для сериализации данных или простого копирования.

for...in: обход с наследством

Цикл for...in проходит по всем перечисляемым свойствам, включая унаследованные. Обычно это не то, что нужно, поэтому его часто комбинируют с hasOwnProperty().

const parent = { inherited: true };
const child = Object.create(parent);
child.own = 'my own';

for (let key in child) {
  if (child.hasOwnProperty(key)) {
    console.log('Собственное:', key); // "own"
  } else {
    console.log('Унаследованное:', key); // "inherited"
  }
}

hasOwnProperty() и getOwnPropertyNames()

Метод hasOwnProperty() проверяет, принадлежит ли свойство самому объекту, а не его прототипу. А Object.getOwnPropertyNames() возвращает массив всех собственных свойств, включая неперечисляемые.

const obj = {};
Object.defineProperty(obj, 'hidden', {
  value: 42,
  enumerable: false
});

console.log(Object.keys(obj));               // [] — скрыто
console.log(Object.getOwnPropertyNames(obj)); // ["hidden"] — найдено!

Это особенно полезно, когда вы работаете с классами и хотите узнать все методы экземпляра, включая приватные (до появления настоящих приватных полей).

Блоги

Рекомендаций по книгам