Что такое рефлексия и интроспекция в 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"] — найдено!
Это особенно полезно, когда вы работаете с классами и хотите узнать все методы экземпляра, включая приватные (до появления настоящих приватных полей).