HTML / CSSJavaScriptNode jsПаттерны проектированияПрактические

Дескрипторы

Над каждым свойством любого объекта в JavaScript можно провести определённый набор манипуляций. Свойство можно записать, изменить, получить значение, а с помощью цикла for .. in или метода Object.keys перечислить все свойства объекта. Вполне стандартный набор операций для работы с объектами, к которому вы, скорее всего, уже привыкли. До релиза стандарта ES5 все эти “качества” объекта изменить было невозможно, но теперь для каждого свойства можно детально описать модель его поведения с помощью дескрипторов.

Свойства или методы?

В одной из первых статей данного цикла я уже писал, что в JavaScript, на самом деле, нет никакого разделения на свойства и методы: любая пара ключ: значение является свойством, в независимости от того, на какой тип данных ссылается ключ. Другими словами, нет никакого разделения значений на функции и всё остальное — то, что обычно называют методом, просто ссылается на функцию, в результате чего свойство можно как бы вызвать. Но, если подумать о том, что происходит “за сценой”, то сразу станет понятно, что само свойство вызвать нельзя. В этом можно легко убедиться, обратившись к методу, как к обычному свойству:

1const methods = {
2 log: function(message) {
3 console.log(message);
4 }
5};
6
7methods.log; // function log(message) { console.log(message); }

Таким образом, используя метод, вы выполняете два действия: получение функции из свойства и вызов полученной функции. Понимание того, что методы являются обычными свойствами, важно в контексте изучения дескрипторов, и именно поэтому я ещё раз обратил ваше внимание на подобное поведение.

Дескрипторы

Дескрипторы позволяют описать, как будет вести себя свойство при выполнении определённых операций над ним, например, чтения или записи. Всего у каждого свойства есть шесть дескрипторов:

  1. value — значение свойства
  2. writable — если установлен true, то значение свойства можно изменять
  3. configurable — если установлен true, то свойство можно перезаписывать с помощью новых вызовов Object.defineProperty
  4. enumerable — если установлен true, то свойство будет перечисляться в цикле for .. in и при использовании метода Object.keys
  5. get — функция, которая будет вызвана при запросе к свойству
  6. set — функция, которая будет вызвана при записе свойства

Получить доступ к изменению дескрипторов можно только используя функции Object.defineProperty, которая первым аргументом принимает объект, в который будет записано свойство, вторым — название свойства и третьим — объект содержащий необходимые дескрипторы:

1const fox = {};
2
3Object.defineProperty(fox, 'name', {
4 value: 'Oliver',
5 enumerable: true,
6 configurable: true,
7 writable: true
8});
9
10console.log(fox); // { "name": "Oliver" }

Если есть необходимость задать сразу несколько свойств за раз, то следует воспользоваться функцией Object.defineProperties:

1const fox = {};
2
3Object.defineProperties(fox, {
4'name': {
5value: 'Oliver',
6enumerable: true
7},
8
9'type': {
10value: 'fox',
11enumerable: true
12}
13});
14
15console.log(fox); // { "name": "Oliver", "type": "fox" }
❗️Важно

По умолчанию дескрипторы enumerable, configurable и writable установлены в значение false, поэтому всегда стоит указывать те дескрипторы, которые вы хотите использовать.

Writable

Writable определяет возможность перезаписи значения свойства. Если установлено значение false, то перезаписать значение нельзя.

1const fox = {};
2
3Object.defineProperty(fox, 'name', {
4 value: 'Oliver',
5 writable: false,
6 configurable: true,
7 enumerable: true
8});
9
10fox.name = 'John'; // Ошибка
11// Cannot assign to read only property 'name' of object

Configurable

Дескриптор configurable определяет, можно ли перезаписать дескрипторы свойства с помощью функции Object.defineProperty:

1const fox = {};
2
3Object.defineProperty(fox, 'name', {
4 value: 'Oliver',
5 writable: true,
6 configurable: false,
7 enumerable: true
8});
9
10Object.defineProperty(fox, 'name', {
11 value: 'John',
12 configurable: false,
13 enumerable: false
14}); // Cannot redefine property: name

Свойство будет успешно перезаписано только в том случае, если значения для дескрипторов полностью совпадают с оригинальным присваиванием:

1const fox = {};
2
3Object.defineProperty(fox, 'name', {
4 value: 'Oliver',
5 writable: true,
6 configurable: false,
7 enumerable: true
8});
9
10Object.defineProperty(fox, 'name', {
11 value: 'John',
12 writable: true,
13 configurable: false,
14 enumerable: true
15}); // Свойство было перезаписано
16
17
18console.log(fox); // { "name": "John" }

Enumerable

Каждый объект можно перебрать с помощью цикла for .. in или же получить названия всех свойств с помощью функции Object.keys. Дескриптор enumerable определяет, будет ли свойство перечисляться в данных ситуациях:Каждый объект можно перебрать с помощью цикла for .. in или же получить названия всех свойств с помощью функции Object.keys. Дескриптор enumerable определяет, будет ли свойство перечисляться в данных ситуациях:

1const fox = {};
2
3Object.defineProperties(fox, {
4 'name': {
5 value: 'Oliver',
6 enumerable: false
7 },
8 'type': {
9 value: 'fox',
10 enumerable: true
11 }
12});
13
14for (let key in fox) {
15 console.log(`${key}: ${fox[key]}`); // type: fox
16}
17
18Object.keys(fox).forEach(key => {
19 console.log(`${key}: ${fox[key]}`); // type: fox
20});

Геттеры и сеттеры

Два самых интересных дескриптора — get и set, более известные, как геттеры и сеттеры. С их помощью можно запускать обозначенные функции при запросе к получению или записи свойства соответственно.

1const fox = {
2 _name: 'Oliver',
3 _type: 'Fox'
4};
5
6Object.defineProperty(fox, 'name', {
7 enumerable: true,
8 configurable: true,
9 get: function() {
10 return `${this._type}: ${this._name}`;
11 },
12 set: function(name) {
13 if (!this.previousNames) { this.previousNames = []; }
14 this.previousNames.push(this._name);
15 this._name = name;
16 }
17});
18
19fox.name = 'John';
20fox.name = 'Doe';
21
22console.log(fox.name); // Fox: Doe
23
24console.log(`Current name: ${fox._name}. Previous names: ${fox.previousNames.join(', ')}`);
25// Current name: Doe. Previous names: Oliver, John

Таким образом, можно выполнить любой код при присваивании и получении свойства, который даже может быть абсолютно не связан с изменяемым свойством, как в примере выше, где перед тем, как записать новое имя, старое добавляется к массиву.

Hello