Кратко
Секция статьи "Кратко"Объекты, как мы знаем, содержат свойства. У каждого из свойств объекта, кроме значения, есть ещё три флага конфигурации, которые могут принимать значения true
или false
. Эти флаги называются дескрипторами:
writable
— доступно ли свойство для записи;enumerable
— является ли свойство видимым при перечислениях (например, в циклеfor
);. . in configurable
— доступно ли свойство для переконфигурирования.
Когда мы создаём свойство объекта «обычным способом», эти три флага устанавливаются в значение true
.
Для изменения значений дескрипторов применяется статический метод Object
, а для чтения значений — Object
.
Другими словами, дескрипторы — это пары ключ-значение, которые описывают поведение свойства объекта при выполнении операций над ним (например, чтения или записи).
Пример
Секция статьи "Пример"Создадим объект и добавим в него свойство ОС для ноутбука. Сделаем это с помощью дескрипторов и статического метода Object
.
Передаём в метод:
- объект, которому добавляем свойство;
- название свойства строкой;
- объект со значениями дескрипторов и ключом
value
, содержащим значение свойства.
const laptop = {}Object.defineProperty(laptop, 'os', { value: 'MacOS', writable: false, enumerable: true, configurable: true})
const laptop = {} Object.defineProperty(laptop, 'os', { value: 'MacOS', writable: false, enumerable: true, configurable: true })
Свойство os
будет недоступно для перезаписи, но будет видно при перечислении и доступно для переконфигурирования.
Попробуем перезаписать свойство os
и выведем полученный результат:
laptop.os = 'Windows'console.log(laptop)// { 'os': 'MacOS' }
laptop.os = 'Windows' console.log(laptop) // { 'os': 'MacOS' }
Как пишется
Секция статьи "Как пишется"Object.defineProperty(объект, имяСвойства, дескрипторы)
Object.defineProperty(объект, имяСвойства, дескрипторы)
Функция принимает следующие параметры:
объект
— объект, свойство которого изменяем или добавляем;имяСвойства
— свойство, для которого нужно применить дескриптор;дескриптор
— дескриптор, описывающий поведение свойства.
Если свойство уже существует, Object
обновит флаги.
Если свойство не существует, метод создаёт новое свойство с указанным значением и флагами. Если какой-либо флаг не указан явно, ему присваивается значение false
.
Как понять
Секция статьи "Как понять"Дескрипторы, которые мы можем передать в Object
могут быть двух типов — дескриптор данных и дескриптор доступа. Каждый тип дескриптора имеет свой набор свойств.
В обоих типах можно использовать общие свойства configurable
и enumerable
.
Дескриптор, передаваемый в Object
может быть только одним типом дескриптора. Он не может быть одновременно обоими! Если передать в Object
объект, содержащий и свойства дескриптора данных, и свойства дескриптора доступа, то метод выбросит ошибку Invalid property descriptor. Cannot both specify accessors and a value or writable attribute.
Дескриптор данных
Секция статьи "Дескриптор данных"Дескриптор данных — это дескриптор, который определяет значение свойства и возможность изменить это значение.
value
— значение свойства, по умолчаниюundefined
.writable
— можно ли изменить значение с помощью оператора присваивания.
value
Секция статьи "value" Свойство value
дескриптора данных отвечает за значение свойства объекта.
Добавим ноутбуку свойство «Размер экрана»:
Object.defineProperty(laptop, 'displaySize', { value: '15'})
Object.defineProperty(laptop, 'displaySize', { value: '15' })
Выведем полученные данные:
const descriptor = Object.getOwnPropertyDescriptor(laptop, 'displaySize')console.log(descriptor)
const descriptor = Object.getOwnPropertyDescriptor(laptop, 'displaySize') console.log(descriptor)
Мы не указали остальные свойства явно, поэтому дескриптор имеет следующие значения:
{ "value": "15", "writable": false, "enumerable": false, "configurable": false}
{ "value": "15", "writable": false, "enumerable": false, "configurable": false }
writable
Секция статьи "writable" Свойство writable
дескриптора определяет, можно ли изменить значение свойства с помощью оператора присваивания. По умолчанию устанавливается в false
для свойств, созданных через Object
и в true
, если свойство добавлено через оператор
.
Изменим значение writable
:
const laptop = {}Object.defineProperty(laptop, 'displaySize', { value: '15', writable: false, // не перезаписываемо! configurable: true, enumerable: true})laptop.displaySize = '18'console.log(laptop.displaySize)// { 'displaySize': '15' }
const laptop = {} Object.defineProperty(laptop, 'displaySize', { value: '15', writable: false, // не перезаписываемо! configurable: true, enumerable: true }) laptop.displaySize = '18' console.log(laptop.displaySize) // { 'displaySize': '15' }
В строгом режиме мы получим ошибку TypeError
, которая говорит о том, что мы не можем изменить неперезаписываемое свойство.
Дескриптор доступа
Секция статьи "Дескриптор доступа"Дескриптор доступа — это дескриптор, который определяет работу свойства через функции чтения и записи свойства (геттера и сеттера).
get
— функция, используемая для получения значения свойства, возвращает значение или undefined
.
set
— функция, используемая для установки значения свойства. Принимает единственным аргументом новое значение, присваиваемое свойству.
Сравним простой объект с полем name
и объект с геттером name
, созданным через Object
:
const animal = { _hiddenName : 'Кот' }Object.defineProperty(animal, 'name', { get: function() { return this._hiddenName }})const animal2 = { name: 'И здесь тоже кот',}console.log(animal.name)// Котconsole.log(animal2.name)// И здесь тоже кот
const animal = { _hiddenName : 'Кот' } Object.defineProperty(animal, 'name', { get: function() { return this._hiddenName } }) const animal2 = { name: 'И здесь тоже кот', } console.log(animal.name) // Кот console.log(animal2.name) // И здесь тоже кот
Оба объекта имеют одинаковое поведение. Стоит только сказать, что за свойством в первом случае стоит функция, которая вызывается автоматически. Достаточно написать animal
.
Если нам понадобится изменить значение свойства name
, мы выполним animal
, ничего не произойдёт. Дело в том, что с ключом name
не связана функция-сеттер, поэтому значение этому свойству установить невозможно.
Добавим сеттер:
const animal = { _hiddenName : 'Кот' }Object.defineProperty(animal, 'name', { get: function() { return this._hiddenName }, set: function(value){ this._hiddenName = value }})animal.name = 'Собака'console.log(animal.name)// Собака
const animal = { _hiddenName : 'Кот' } Object.defineProperty(animal, 'name', { get: function() { return this._hiddenName }, set: function(value){ this._hiddenName = value } }) animal.name = 'Собака' console.log(animal.name) // Собака
По сути, мы можем регулировать возможность читать и получать значение свойства, как и в дескрипторе данных, только более тонко. Такой подход используется часто, поэтому для объявления геттеров и сеттеров придумали синтаксис без вызова Object
:
const animal = { get name() { return this._name }, set name(value) { this._name = value }}console.log(animal.name)// undefinedanimal.name = 'Кот'console.log(animal.name)// Кот
const animal = { get name() { return this._name }, set name(value) { this._name = value } } console.log(animal.name) // undefined animal.name = 'Кот' console.log(animal.name) // Кот
Сеттеры могут понадобиться, например, для модификации значения при записи свойств. В примере ниже мы модифицируем дату и записываем в нужном формате.
const updatedAt = { get date() { return this._date }, set date(value) { this._date = new Intl.DateTimeFormat('en-US').format(value) }}
const updatedAt = { get date() { return this._date }, set date(value) { this._date = new Intl.DateTimeFormat('en-US').format(value) } }
Запишем дату и время в поле date
:
updatedAt.date = new Date(2030, 11, 12)console.log(updatedAt.date)// 12/12/2030
updatedAt.date = new Date(2030, 11, 12) console.log(updatedAt.date) // 12/12/2030
И получим дату в нужном формате: 12
.
Свойства с методами доступа дают нам все возможности обработки данных с помощью функций и простоту, характерную для работы с обычными свойствами.
Общие свойства
Секция статьи "Общие свойства"Общие свойства можно указывать в обоих типах дескрипторов.
enumerable
Секция статьи "enumerable" Свойство определяет, является ли создаваемое свойство объекта видимым при перечислениях.
Создадим два свойства у объекта laptop
— одно будет перечисляемым, а другое — нет:
const laptop = {}Object.defineProperty(laptop, 'processor', // сделаем `processor` перечисляемым, как обычно { enumerable: true, value: 'Intel Core' })Object.defineProperty(laptop, 'touchID', // сделаем `touchID` НЕперечисляемым { enumerable: false, value: true })console.log(laptop.touchID)// trueconsole.log(('touchID' in laptop))// trueconsole.log(laptop.hasOwnProperty('touchID'))// truefor (let key in laptop) { console.log(key, laptop[key])}// 'processor': 'Intel Core'
const laptop = {} Object.defineProperty(laptop, 'processor', // сделаем `processor` перечисляемым, как обычно { enumerable: true, value: 'Intel Core' } ) Object.defineProperty(laptop, 'touchID', // сделаем `touchID` НЕперечисляемым { enumerable: false, value: true } ) console.log(laptop.touchID) // true console.log(('touchID' in laptop)) // true console.log(laptop.hasOwnProperty('touchID')) // true for (let key in laptop) { console.log(key, laptop[key]) } // 'processor': 'Intel Core'
Заметьте, что laptop
существует и имеет значение, но не отображается в цикле for
(при этом, оно существует, если воспользоваться оператором in
). «Перечислимое» означает: «будет учтено, если пройти перебором по свойствам объекта».
configurable
Секция статьи "configurable" Свойство configurable
определяет, доступно ли создаваемое свойство объекта для переконфигурирования.
Изменим значение configurable
:
const laptop = {}Object.defineProperty(laptop, 'processor', { value: 'Intel Core', writable: true, configurable: false, // запрещаем переконфигурирование! enumerable: true})console.log(laptop.processor)// Intel Corelaptop.processor = 'M1'console.log(laptop.processor)// 'M1'Object.defineProperty(laptop, 'processor', { value: 'M1 TOP', writable: true, configurable: true, enumerable: true})// TypeError: Cannot redefine property: processor
const laptop = {} Object.defineProperty(laptop, 'processor', { value: 'Intel Core', writable: true, configurable: false, // запрещаем переконфигурирование! enumerable: true }) console.log(laptop.processor) // Intel Core laptop.processor = 'M1' console.log(laptop.processor) // 'M1' Object.defineProperty(laptop, 'processor', { value: 'M1 TOP', writable: true, configurable: true, enumerable: true }) // TypeError: Cannot redefine property: processor
Попытка переписать дескриптор свойства processor
приводит к ошибке TypeError
, даже если вы находитесь не в строгом режиме.
Если для свойства уже задано configurable
, то writable
может быть изменено с true
на false
без ошибки, но не обратно в true
если оно уже false
.
А ещё configurable
препятствует возможности использовать оператор delete
для удаления существующего свойства. Ошибки не случится, но и свойство не удалится:
delete laptop.processorconsole.log(laptop)// { processor: 'M1' }
delete laptop.processor console.log(laptop) // { processor: 'M1' }
Периодически разработчику нужно защищать объекты от вмешательства извне. По ошибке легко изменить свойство объекта. Для защиты объектов от подобных изменений и управления их иммутабельностью предлагается использовать дескрипторы, такие как writable
и configurable
, сеттеры, а также методы Object
, Object
, и Object
для ограничения доступа к объекту целиком.
На практике
Секция статьи "На практике"🛠 В жизни проще использовать именно Object
и Object
потому что чаще всего нужно ограничить доступ ко всему объекту целиком.
Сначала скажу, что есть метод Object
, чтобы проще объяснить принцип работы Object
.
Object
запрещает добавление новых свойств объекта, но в то же время оставляет существующие свойства нетронутыми.
const laptop = { displaySize: 15}Object.preventExtensions(laptop)laptop.storage = 256console.log(laptop.storage)// undefined
const laptop = { displaySize: 15 } Object.preventExtensions(laptop) laptop.storage = 256 console.log(laptop.storage) // undefined
В нестрогом режиме, создание storage
завершится неудачей без ошибок. В строгом режиме это приведёт к ошибке TypeError
.
Метод Object
создаёт «запечатанный» объект — то есть принимает существующий объект, применяет к нему Object
, а также помечает все существующие свойства как configurable
.
Таким образом, вы не сможете не только добавлять свойства, но и переконфигурировать или удалить существующие (хотя вы всё ещё можете изменять их значения).
Object
— замораживает объект, запрещая изменение значений существующих свойств и исключает добавление новых свойств. Возвращает новый объект.
Другими словами, Object
создаёт замороженный объект, применяет к нему Object
и writable
, следовательно, их значения не могут быть изменены.
Обратим внимание, что метод поверхностный, у замороженного объекта остаётся возможность изменять вложенные объекты. На MDN есть пример глубокой заморозки, метод deepFreeze
, позволяющий сделать полностью иммутабельный объект. При этом, невозможно сделать иммутабельными Date
, Map или Set.
Этот подход даёт наивысший уровень иммутабельности, который вы можете получить для самого объекта.
🛠 Для объявления нескольких свойств, воспользуйтесь статическим методом Object
:
const laptop = {}Object.defineProperties(laptop, { os: { value: 'MacOS', enumerable: true }, age: { value: 10, enumerable: false }})const result = Object.keys(laptop)console.log(result)// ['os']
const laptop = {} Object.defineProperties(laptop, { os: { value: 'MacOS', enumerable: true }, age: { value: 10, enumerable: false } }) const result = Object.keys(laptop) console.log(result) // ['os']
🛠 Получение значений дескрипторов для конкретного свойства объекта:
const source = { name: 'DOKA', sections: ['HTML', 'CSS', 'JS', 'Tools', 'Recipes'], themes: ['light']}const nameDescriptors = Object.getOwnPropertyDescriptor(source, 'name')console.log(nameDescriptors)//{// 'value':'DOKA',// 'writable':true,// 'enumerable':true,// 'configurable':true//}
const source = { name: 'DOKA', sections: ['HTML', 'CSS', 'JS', 'Tools', 'Recipes'], themes: ['light'] } const nameDescriptors = Object.getOwnPropertyDescriptor(source, 'name') console.log(nameDescriptors) //{ // 'value':'DOKA', // 'writable':true, // 'enumerable':true, // 'configurable':true //}
🛠 Получение значений дескрипторов для всех свойств объекта:
const allPropertyDescriptors = Object.getOwnPropertyDescriptors(source)console.log(allPropertyDescriptors)
const allPropertyDescriptors = Object.getOwnPropertyDescriptors(source) console.log(allPropertyDescriptors)
Получим следующий ответ:
{ name: { value: 'DOKA', writable: true, enumerable: true, configurable: true, }, sections: { value: ['HTML', 'CSS', 'JS', 'Tools', 'Recipes'], writable: true, enumerable: true, configurable: true, }, themes: { value: ['light'], writable: true, enumerable: true, configurable: true, },}
{ name: { value: 'DOKA', writable: true, enumerable: true, configurable: true, }, sections: { value: ['HTML', 'CSS', 'JS', 'Tools', 'Recipes'], writable: true, enumerable: true, configurable: true, }, themes: { value: ['light'], writable: true, enumerable: true, configurable: true, }, }
Если передан пустой объект без свойств, то получим пустой объект:
const user = {}const userDescriptors = Object.getOwnPropertyDescriptors(user)console.log(userDescriptors)// {}
const user = {} const userDescriptors = Object.getOwnPropertyDescriptors(user) console.log(userDescriptors) // {}
🛠 Object
определяет, был ли объект заморожен. Возвращает true
, если добавление/удаление/изменение свойств запрещено, и для всех текущих свойств установлено configurable
, writable
.