Понятие дескриптора свойства объекта
Объект — это набор свойств и их значений в памяти, на которые можно сослаться с помощью идентификатора.
1 2 3 4 5 6 7 8 9 |
// Объект user const user = { firstName: "Alex", lastName: "NAV", "user-adress": { country: "Russia", sity: "Moscow" }, }; |
Свойство объекта – это пара «ключ: значение» (или key: value):
- ключ — это идентификатор (или имя) свойства (тип String или Symbol),
- значения могут быть любыми (любой тип, включая другие объекты).
Свойства объекта также иногда называют полями объекта.
Значения свойств могут иметь любой тип, включая другие объекты, что позволяет строить сложные, разветвлённые иерархии данных.
Дескриптор (флаг) - это объект, позволяющий описать поведение свойства при выполнении определённых операций над ним (например, чтения или записи).
Типы дескрипторов свойств объектов:
- дескрипторы данных (для свойства, имеющего значение, которое может (или не может) быть записываемым);
- дескрипторы доступа (свойство, описываемое парой функций — геттером (get) и сеттером (set).
Дескриптор может быть только одним из этих двух типов; он не может быть одновременно обоими.
Дескрипторы свойств (как объекты) могут содержать следующие ключи:
- value - значение, ассоциированное со свойством; может быть любым допустимым значением JavaScript (числом, объектом, функцией и т.д.);
- writable - если true , то значение, ассоциированное со свойством, может быть изменено с помощью оператора присваивания;
- enumerable - если true, то это свойство доступно через перечисление свойств содержащего его объекта (при false свойство будет отсутствовать в перечне перечисляемых свойств объекта);
- configurable - если true, то тип свойства может быть изменен и если свойство может быть удалено из содержащего его объекта;
- get - функция, используемая как геттер свойства (undefined, если свойство не имеет геттера); возвращаемое значение функции будет использоваться как значение свойства;
- set - функция, используемая как сеттер свойства (undefined, если свойство не имеет сеттера); функция принимает единственным аргументом новое значение, присваиваемое свойству.
Дескриптор | Значение по умолчанию | |
Value | undefined | |
Get | undefined | |
Set | undefined | |
Writable | false | Возможность изменения значения [[Value]] |
Enumerable | false | Если true, свойство будет перечислено в циклах for ... in. |
Configurable | false | Если false, свойство не может быть удалено, не может быть изменено на свойство-аксессор (get/set), а атрибуты, отличные от [[Value]] и [[Writable]], не могут быть изменены. |
ВАЖНО! Чтобы избежать конфликтов запрещено одновременно указывать значение value и функции get/set (указывают либо значение (value), либо функции для его чтения-записи (get или set)). Также запрещено и не имеет смысла указывать writable при наличии get/set-функций.
Значения дескрипторов по умолчанию зависят от способа добавления свойств:
1 2 3 4 5 6 7 8 9 10 |
// создадим объект с помощью литерала и добавим свойство с помощью метода Object const car = {}; Object.defineProperty(car, "brand", { value: "BMW", }); Object.getOwnPropertyDescriptor(car,'brand') // {value: "BMW", writable: false, enumerable: false, configurable: false} // создадим объект с помощью литерала, сразу добавив в него свойство (значения дескрипторов по умолчанию не false, а true) const airplane = { brand: "Su" }; Object.getOwnPropertyDescriptor(airplane,'brand') // {value: "Su", writable: true, enumerable: true, configurable: true} |
Дескриптор Writable:
1 2 3 4 5 6 7 |
const car = {}; Object.defineProperty(car, "brand", { value: "Nissan", writable: false // только для чтения, перезаписать (изменить) значение свойства нельзя }); car.brand = "Opel"; // TypeError: "brand" is read-only (ошибка) |
Дескриптор Configurable:
Дескриптор configurable определяет:
- можно ли изменить тип свойства enumerable или configurable с помощью функции Object.defineProperty();
- может ли свойство быть удалено из содержащего его объекта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const car = {}; Object.defineProperty(car, "brand", { value: "Nissan", writable: true, configurable: false // запрет на изменение и удаление свойства объекта }); // Пытаемся изменить значения дескрипторов enumerable (или configurable), // ошибка TypeError: can't redefine non-configurable property "brand" Object.defineProperty(car, "brand", { value: "BMW", writable: false, configurable: false, enumerable: true }); // Пытаемся удалить свойство "brand", // ошибка TypeError: property "brand" is non-configurable and can't be deleted delete car.brand; |
Значение дескриптора value будет перезаписано только в том случае, если значения для остальных дескрипторов полностью совпадают с предыдущим присваиванием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const car = {}; Object.defineProperty(car, "brand", { value: "Nissan", writable: true, configurable: false }); Object.defineProperty(car, "brand", { value: "BMW", writable: false, configurable: false, enumerable: false, }); res = car.brand; console.log(res); // BMW (ошибки не будет, значение value изменится) |
Дескриптор Enumerable:
Дескриптор enumerable определяет:
- будет ли свойство перебираться с помощью цикла for .. in;
- можно ли получить название свойства с помощью функции Object.keys.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const car = {}; Object.defineProperties(car, { brand: { value: "BMW", enumerable: true }, age: { value: 10, enumerable: false } }); res = Object.keys(car); console.log(res); // Array [ "brand" ] (свойство age отсутствует) |
Использование дескрипторов при работе со свойствами объекта
Получение значений дескрипторов для свойств объекта:
Метод Object.getOwnPropertyDescriptor() возвращает дескриптор собственного свойства объекта, переданного в метод (собственное свойство объекта находится непосредственно в объекте, а не получено через цепочку прототипов.
1 |
Object.getOwnPropertyDescriptor( obj, prop ) |
где obj - объект, в котором ищется свойство; prop - имя свойства, чьё описание (дескрипторы) будет возвращено.
Метод Object.getOwnPropertyDescriptor() возвращает дескриптор переданного свойства, если оно присутствует в объекте, либо undefined, если его там нет.
Метод Object.getOwnPropertyDescriptors() возвращает все собственные дескрипторы свойств переданного объекта.
1 |
Object.getOwnPropertyDescriptors( obj ) |
где obj - объект, для которого нужно получить все собственные дескрипторы свойств.
Метод Object.getOwnPropertyDescriptors() возвращает объект, содержащий все собственные дескрипторы свойств объекта. Если у переданного объекта нет свойств, то возвращается пустой объект.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
let user = { name: "Alex", surname: "NAV", age: 40, skills: ["teacher", "JavaScript"] }; // Получение значений дескрипторов для конкретного свойства объекта let res = Object.getOwnPropertyDescriptor(user, 'name'); console.log(res); // Object { value: "Alex", writable: true, enumerable: true, configurable: true } // Получение значений дескрипторов для всех свойств объекта res = Object.getOwnPropertyDescriptors(user); console.log(res); // {…} // age: Object { value: 40, writable: true, enumerable: true, … } // name: Object { value: "Alex", writable: true, enumerable: true, … } // skills: Object { writable: true, enumerable: true, configurable: true, … } // surname: Object { value: "NAV", writable: true, enumerable: true, … } // Если передан пустой объект (без свойств) user = {}; res = Object.getOwnPropertyDescriptors(user); console.log(res); // Object { } |
Добавление (изменение) свойств объекта:
Метод Object.defineProperties() определяет (добавляет) новые или изменяет существующие свойства, непосредственно в объекте, возвращая этот объект.
При вызове метода учитывается значение дескриптора configurable!
1 |
Object.defineProperties( obj, props ) |
где obj - объект, в котором определяются новые или изменяются существующие свойства; props - объект, чьи собственные перечисляемые свойства представляют собой дескрипторы для создаваемых или изменяемых свойств.
Метод возвращает измененный объект.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const user = {}; Object.defineProperty(user, 'name', { value: "Olga", enumerable: true }); console.log(user); // Object { name: "Olga" } res = Object.getOwnPropertyDescriptors(user); console.log(res); // name: Object // { configurable: false // enumerable: true // value: "Olga" // writable: false } |
Обратите внимание на значения по умолчанию дескрипторов configurable: false и writable: false.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const user = {}; Object.defineProperties(user, { name: { value: "Olga", enumerable: true }, age: { value: 25, configurable: true, enumerable: true, writable: true } }); console.log(user); // Object { name: "Olga", age: 25 } |
Метод Object.defineProperties() позволяет определять все свойства переданного объекта.
При вызове метода учитывается значение дескриптора configurable!
Геттеры (get) и сеттеры (set)
Дескрипторы get и set (более известные как геттеры и сеттеры) позволяют задавать функции в качестве свойств объекта, а также запускать их при запросе для получения или записи значения свойства (соответственно):
- get – функция, которая возвращает значение свойства (по умолчанию undefined);
set – функция, которая записывает значение свойства (по умолчанию undefined).
1 2 3 4 5 6 |
const myObj = { get getter() { // геттер, срабатывает при чтении свойства myObj.getter }, set setter(value) { // сеттер, срабатывает при записи свойства myObj.setter = value } }; |
При этом мы не вызываем геттер (например, myObj.getter) или сеттер как функцию, а просто читаем его значение (геттер или сеттер выполняются скрыто). Снаружи свойство-аксессор выглядит как обычное свойство объекта.
Указание get и set в качестве свойства в литерале объекта
Если мы создаём объект при помощи литерального синтаксиса { ... }, то задать свойства-функции можно прямо в его определении.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let myCar = { brand: "Opel", year: 2014, set age(value) { this.year = 2020 - value; }, get allInfo() { return `Car: ${this.brand}, year: ${this.year}`; } }; myCar.age = 2010; // вызываем сеттер и передаем значение console.log(myCar.allInfo); // Car: Opel, year: 10 (вызов геттера) |
Геттер срабатывает, когда myCar.allInfo вызывается (читается), сеттер – когда значение myCar.age присваивается.
Добавление get или set в существующий объект
С помощью методов Object.defineProperty() и Object.defineProperties() можно добавить get или set в уже существующий объект.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Исходный объект let myCar = { brand: "Opel", year: 0 }; Object.defineProperty(myCar, "age", { set: function(value) { this.year = 2020 - value; }, get: function() { return `Car: ${this.brand}, year: ${this.year}`; } }); myCar.age = 2010; // вызываем сеттер и передаем значение console.log(myCar.age); // Car: Opel, year: 10 (вызов геттера) console.log(myCar); // Object { brand: "Opel", year: 10, … } |
Ещё раз отметим, что свойство объекта может быть либо свойством-данным (со значением value), либо свойством-аксессором (с методами get/set).
ВАЖНО! Попытка указать и get/set, и value в одном дескрипторе вызовет ошибку:
1 2 3 4 5 6 |
Object.defineProperty(myCar, "age", { value: 0, // TypeError: property descriptors must not specify a value or be writable when a getter or setter has been specified set: function(value) { this.year = 2020 - value; }, }); |
Использование get или set в качестве обёртки
Геттеры/сеттеры можно использовать как обёртки над «реальными» значениями свойств, чтобы получить больше контроля над операциями с ними.
Например, если мы хотим запретить устанавливать короткое имя для login, мы можем использовать сеттер login для проверки, а само значение хранить в отдельном внутреннем свойстве _login:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let userLogin = { get login() { return this._login; }, set login(value) { if (value.length < 5) { console.log("Login is short"); } else { return (this._login = value); } } }; userLogin.login = "Adam"; // Login is short console.log(userLogin.login) // undefined console.log(userLogin) // Object { login: Getter & Setter } userLogin.login = "AdamSmith"; console.log(userLogin.login) // AdamSmith console.log(userLogin) // Object { login: Getter & Setter, _login: "AdamSmith" } |
Таким образом, login хранится в свойстве _login, доступ к которому производится через геттер и сеттер.
Замечание: технически можно получить доступ к login напрямую с помощью userLogin._login, но существует соглашение о том что свойства, которые начинаются с символа "_", являются внутренними, и к ним не следует обращаться из-за пределов объекта.
Использование get или set для совместимости кода
Аксессоры позволяют в любой момент взять «обычное» свойство и изменить его поведение, поменяв на геттер и сеттер.
Пример отсюда
Например, представим, что мы начали реализовывать объект user, используя свойства-данные имя name и возраст age:
1 2 3 4 5 6 7 |
function User(name, age) { this.name = name; this.age = age; } let john = new User("John", 25); console.log(`Возраст: ${john.age}`); // Возраст: 25 |
Но вместо возраста age мы можем решить хранить дату рождения birthday, потому что так более точно и удобно:
1 2 3 4 5 6 7 |
function User(name, birthday) { this.name = name; this.birthday = birthday; } let john = new User("John", new Date(1992, 6, 1)); console.log(`Дата рождения: ${john.birthday}`); // Дата рождения: Wed Jul 01 1992 00:00:00 GMT+0600 (Екатеринбург, летнее время) |
Что делать со старым кодом, который использует свойство age?
Мы можем попытаться найти все такие места и изменить их, но это отнимает время и может быть невыполнимо, если код используется другими людьми. И кроме того, age может использоваться и в других местах кода. Давайте его сохраним.
Добавление геттера для age решит проблему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function User(name, birthday) { this.name = name; this.birthday = birthday; // Возраст рассчитывается из текущей даты и дня рождения Object.defineProperty(this, "age", { get() { let todayYear = new Date().getFullYear(); return todayYear - this.birthday.getFullYear(); } }); } let john = new User("John", new Date(1992, 6, 1)); console.log(john.birthday); // доступен как день рождения console.log(john.age); // ...так и возраст |
Теперь старый код тоже работает, и у нас есть дополнительное свойство age.