Тайная жизнь свойств: Как флаги и дескрипторы управляют поведением объектов
Вы думаете, что свойство объекта - это просто пара "ключ-значение"? Что ж, это только верхушка айсберга. Под поверхностью скрывается целый мир: флаги, которые определяют, можно ли свойство изменять, перечислять или удалять. Дескрипторы, которые позволяют создавать вычисляемые свойства с геттерами и сеттерами.
В JavaScript каждое свойство объекта - это не просто значение. Это структура с тремя скрытыми флагами и, возможно, функциями доступа. Понимание этой системы открывает двери к продвинутому метапрограммированию, созданию неизменяемых объектов и тонкому контролю над API.
Сегодня мы разберём, как устроены свойства изнутри, как управлять их поведением и как использовать эту мощь для создания надёжных, защищённых и элегантных объектов.
Часть 1. Три тайных флага
У каждого свойства объекта есть три скрытых флага:
javascript
const user = { name: "Анна" };
// Получаем дескриптор свойства
const descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// { value: "Анна", writable: true, enumerable: true, configurable: true }
Часть 2. Чтение и запись дескрипторов
2.1 Получение дескриптора (getOwnPropertyDescriptor)
javascript
const obj = { id: 42 };
const desc = Object.getOwnPropertyDescriptor(obj, "id");
console.log(desc.value); // 42
console.log(desc.writable); // true
console.log(desc.enumerable); // true
console.log(desc.configurable);// true
2.2 Установка дескриптора (defineProperty)
javascript
const user = {};
Object.defineProperty(user, "name", {
value: "Анна",
writable: false, // только для чтения
enumerable: true,
configurable: true
});
console.log(user.name); // "Анна"
user.name = "Борис"; // Ошибка в строгом режиме (тихо игнорируется в нестрогом)
console.log(user.name); // "Анна" (не изменилось!)
Часть 3. Флаг writable - защита от изменений
javascript
const obj = {};
Object.defineProperty(obj, "constant", {
value: 42,
writable: false // свойство только для чтения
});
obj.constant = 100; // игнорируется (или ошибка в strict mode)
console.log(obj.constant); // 42
// Проверка
const desc = Object.getOwnPropertyDescriptor(obj, "constant");
console.log(desc.writable); // false
Часть 4. Флаг enumerable - скрытие от итераций
javascript
const obj = { visible: 1 };
Object.defineProperty(obj, "secret", {
value: "тайна",
enumerable: false // не показывается в циклах
});
console.log(Object.keys(obj)); // ["visible"] (secret не виден)
console.log(Object.values(obj)); // [1]
console.log(JSON.stringify(obj)); // {"visible":1}
// Но свойство существует и доступно
console.log(obj.secret); // "тайна"
// for...in тоже не видит secret
for (const key in obj) {
console.log(key); // "visible"
}
// Специальный метод показывает все свойства
console.log(Object.getOwnPropertyNames(obj)); // ["visible", "secret"]
Где применяется?
- Внутренние свойства, которые не должны мешать итерации
- Приватные данные (до появления реальных приватных полей)
- Мета-информация об объекте
Часть 5. Флаг configurable - защита от удаления и переопределения
configurable: false запрещает:
- Удаление свойства (delete)
- Изменение дескриптора (кроме writable с true на false)
- Изменение флагов (кроме writable)
javascript
const obj = {};
Object.defineProperty(obj, "locked", {
value: 42,
configurable: false,
writable: true
});
// Нельзя удалить
delete obj.locked; // false (или ошибка в strict mode)
console.log(obj.locked); // 42
// Нельзя переопределить дескриптор
try {
Object.defineProperty(obj, "locked", {
configurable: true
});
} catch (e) {
console.log("Ошибка! Нельзя изменить configurable");
}
// Но можно изменить writable (только с true на false)
Object.defineProperty(obj, "locked", {
writable: false
}); // работает
// А обратно уже нельзя
try {
Object.defineProperty(obj, "locked", {
writable: true
});
} catch (e) {
console.log("Ошибка! Нельзя вернуть writable");
}
Часть 6. Геттеры и сеттеры (accessor properties)
Вместо простого значения свойство может определять функции, которые вызываются при чтении и записи.
javascript
const user = {
firstName: "Анна",
lastName: "Иванова",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(user.fullName); // "Анна Иванова" (вызвался геттер)
user.fullName = "Борис Петров"; // вызвался сеттер
console.log(user.firstName); // "Борис"
console.log(user.lastName); // "Петров"
6.1 Геттеры и сеттеры через defineProperty
javascript
const obj = {
_value: 0
};
Object.defineProperty(obj, "value", {
get() {
console.log("Чтение value");
return this._value;
},
set(newValue) {
console.log(`Запись value: ${newValue}`);
if (newValue < 0) {
throw new Error("Значение не может быть отрицательным");
}
this._value = newValue;
},
enumerable: true,
configurable: true
});
obj.value = 10; // "Запись value: 10"
console.log(obj.value); // "Чтение value" → 10
// obj.value = -5; // Ошибка!
6.2 Дескриптор доступа vs дескриптор данных
Важно: Нельзя смешивать. Либо value и writable, либо get и set.
javascript
// ❌ Ошибка!
Object.defineProperty(obj, "bad", {
value: 42,
get() { return 42; } // конфликт
});
Часть 7. Методы управления объектами
7.1 Object.preventExtensions() - запрет добавления новых свойств
javascript
const obj = { a: 1 };
Object.preventExtensions(obj);
obj.b = 2; // игнорируется (или ошибка в strict mode)
console.log(obj.b); // undefined
console.log(Object.isExtensible(obj)); // false
7.2 Object.seal() - запрет добавления/удаления, делает configurable: false
javascript
const obj = { a: 1, b: 2 };
Object.seal(obj);
obj.c = 3; // игнорируется
delete obj.a; // игнорируется
obj.b = 100; // ✅ работает (writable остаётся true)
console.log(Object.isSealed(obj)); // true
console.log(Object.getOwnPropertyDescriptor(obj, "a").configurable); // false
7.3 Object.freeze() - полная заморозка (ничего нельзя изменить)
javascript
const obj = { a: 1, b: { c: 2 } };
Object.freeze(obj);
obj.a = 100; // игнорируется
obj.c = 3; // игнорируется
delete obj.a; // игнорируется
console.log(obj.a); // 1
// Но freeze поверхностный!
obj.b.c = 3; // ✅ работает! (вложенный объект не заморожен)
console.log(obj.b.c); // 3
// Для глубокой заморозки нужна рекурсия
function deepFreeze(obj) {
Object.freeze(obj);
for (const key in obj) {
if (obj.hasOwnProperty(key) && typeof obj[key] === "object") {
deepFreeze(obj[key]);
}
}
}
Часть 8. Сравнение методов
Часть 9. Реальные кейсы
9.1 Создание констант (неизменяемых объектов)
javascript
const Constants = {};
Object.defineProperties(Constants, {
API_URL: {
value: "https://api.example.com",
writable: false,
enumerable: true,
configurable: false
},
MAX_RETRIES: {
value: 3,
writable: false,
enumerable: true,
configurable: false
},
TIMEOUT_MS: {
value: 5000,
writable: false,
enumerable: true,
configurable: false
}
});
// Constants.API_URL = "другое"; // Ошибка!
console.log(Constants.API_URL); // "https://api.example.com"
9.2 Валидация при установке
javascript
class Temperature {
constructor() {
this._celsius = 0;
}
get celsius() {
return this._celsius;
}
set celsius(value) {
if (typeof value !== "number") {
throw new TypeError("Температура должна быть числом");
}
if (value < -273.15) {
throw new RangeError("Температура не может быть ниже абсолютного нуля");
}
this._celsius = value;
}
get fahrenheit() {
return (this._celsius * 9/5) + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // Ошибка!
9.3 Ленивые свойства (вычисляются при первом доступе)
javascript
function defineLazyProperty(obj, propName, getter) {
let defined = false;
let value;
Object.defineProperty(obj, propName, {
get() {
if (!defined) {
value = getter();
defined = true;
}
return value;
},
enumerable: true,
configurable: true
});
}
const data = {};
defineLazyProperty(data, "expenses", () => {
console.log("Вычисляем расходы... (тяжёлая операция)");
return [100, 200, 300, 400, 500];
});
console.log(data.expenses); // "Вычисляем расходы..." → [100, 200, 300, 400, 500]
console.log(data.expenses); // [100, 200, 300, 400, 500] (из кэша)
9.4 Скрытие внутренних свойств (enumerable: false)
javascript
function createBankAccount(initialBalance) {
const account = {
deposit(amount) {
if (amount > 0) {
this._balance += amount;
}
return this;
},
withdraw(amount) {
if (amount > 0 && amount <= this._balance) {
this._balance -= amount;
}
return this;
},
getBalance() {
return this._balance;
}
};
// Скрытое свойство
Object.defineProperty(account, "_balance", {
value: initialBalance,
writable: true,
enumerable: false, // не видно в циклах
configurable: false
});
return account;
}
const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(Object.keys(account)); // ["deposit", "withdraw", "getBalance"] (_balance не видно)
Часть 10. Подводные камни
Камень #1: configurable: false нельзя отменить
javascript
const obj = {};
Object.defineProperty(obj, "prop", {
value: 42,
configurable: false
});
// Нельзя сделать configurable: true
// Нельзя удалить свойство
// Нельзя изменить дескриптор (кроме writable с true на false)
Камень #2: freeze и seal поверхностные
javascript
const obj = { a: { b: 1 } };
Object.freeze(obj);
obj.a.b = 2; // работает!
console.log(obj.a.b); // 2
Камень #3: Геттер не должен иметь побочных эффектов (или должен, но осторожно)
javascript
let callCount = 0;
const obj = {
get count() {
callCount++;
return callCount;
}
};
console.log(obj.count); // 1
console.log(obj.count); // 2
console.log(obj.count); // 3 (каждый вызов меняет состояние!)
Часть 11. Продвинутая техника: Object.defineProperties
javascript
const library = {};
Object.defineProperties(library, {
// Свойство с геттером и сеттером
version: {
get() { return this._version; },
set(v) { this._version = v; },
enumerable: true
},
// Только для чтения
author: {
value: "Анна Иванова",
writable: false,
enumerable: true
},
// Скрытое свойство
_version: {
value: "1.0.0",
enumerable: false,
writable: true
}
});
library.version = "2.0.0";
console.log(library.version); // "2.0.0"
console.log(library.author); // "Анна Иванова"
console.log(Object.keys(library)); // ["version", "author"]
Итог: Манифест флагов и дескрипторов
- Три флага - writable, enumerable, configurable - управляют поведением свойства.
- writable: false - свойство только для чтения.
- enumerable: false - свойство скрыто от итераций (Object.keys, for...in).
- configurable: false - свойство нельзя удалить или изменить дескриптор.
- Геттеры/сеттеры - позволяют вычислять значения при доступе и валидировать при записи.
- Object.freeze() - полная заморозка (поверхностная).
- Object.seal() - запрет добавления/удаления.
- Object.preventExtensions() - запрет добавления новых свойств.
- Дескрипторы данных и доступа - взаимоисключающие.
Финальный тест (что выведет?):
javascript
const obj = {};
Object.defineProperty(obj, "a", {
value: 1,
writable: false,
enumerable: true,
configurable: true
});
Object.defineProperty(obj, "b", {
value: 2,
writable: true,
enumerable: false,
configurable: false
});
obj.a = 10;
obj.b = 20;
delete obj.b;
console.log(obj.a);
console.log(obj.b);
console.log(Object.keys(obj));
Ответы:
- 1 (writable: false)
- 20 (b остался, delete не сработал из-за configurable: false)
- ["a"] (b не enumerable)
Флаги и дескрипторы - это инструмент для тех, кто хочет контролировать каждое свойство своего объекта. Они позволяют создавать неизменяемые константы, скрывать внутренние детали, валидировать данные и создавать вычисляемые свойства. Это не повседневный инструмент, но когда он нужен - он незаменим. Используйте эту мощь с умом, и ваши объекты станут не только хранилищами данных, но и умными, защищёнными структурами.