Дескрипторы
Над каждым свойством любого объекта в JavaScript можно провести определённый набор манипуляций. Свойство можно записать, изменить, получить значение, а с помощью цикла for .. in или метода Object.keys перечислить все свойства объекта. Вполне стандартный набор операций для работы с объектами, к которому вы, скорее всего, уже привыкли. До релиза стандарта ES5 все эти “качества” объекта изменить было невозможно, но теперь для каждого свойства можно детально описать модель его поведения с помощью дескрипторов.
Свойства или методы?
В одной из первых статей данного цикла я уже писал, что в JavaScript, на самом деле, нет никакого разделения на свойства и методы: любая пара ключ: значение является свойством, в независимости от того, на какой тип данных ссылается ключ. Другими словами, нет никакого разделения значений на функции и всё остальное — то, что обычно называют методом, просто ссылается на функцию, в результате чего свойство можно как бы вызвать. Но, если подумать о том, что происходит “за сценой”, то сразу станет понятно, что само свойство вызвать нельзя. В этом можно легко убедиться, обратившись к методу, как к обычному свойству:
1const methods = {2 log: function(message) {3 console.log(message);4 }5};67methods.log; // function log(message) { console.log(message); }
Таким образом, используя метод, вы выполняете два действия: получение функции из свойства и вызов полученной функции. Понимание того, что методы являются обычными свойствами, важно в контексте изучения дескрипторов, и именно поэтому я ещё раз обратил ваше внимание на подобное поведение.
Дескрипторы
Дескрипторы позволяют описать, как будет вести себя свойство при выполнении определённых операций над ним, например, чтения или записи. Всего у каждого свойства есть шесть дескрипторов:
value— значение свойстваwritable— если установленtrue, то значение свойства можно изменятьconfigurable— если установленtrue, то свойство можно перезаписывать с помощью новых вызововObject.definePropertyenumerable— если установленtrue, то свойство будет перечисляться в циклеfor .. inи при использовании методаObject.keysget— функция, которая будет вызвана при запросе к свойствуset— функция, которая будет вызвана при записе свойства
Получить доступ к изменению дескрипторов можно только используя функции Object.defineProperty, которая первым аргументом принимает объект, в который будет записано свойство, вторым — название свойства и третьим — объект содержащий необходимые дескрипторы:
1const fox = {};23Object.defineProperty(fox, 'name', {4 value: 'Oliver',5 enumerable: true,6 configurable: true,7 writable: true8});910console.log(fox); // { "name": "Oliver" }
Если есть необходимость задать сразу несколько свойств за раз, то следует воспользоваться функцией Object.defineProperties:
1const fox = {};23Object.defineProperties(fox, {4'name': {5value: 'Oliver',6enumerable: true7},89'type': {10value: 'fox',11enumerable: true12}13});1415console.log(fox); // { "name": "Oliver", "type": "fox" }
Важно
По умолчанию дескрипторы enumerable, configurable и writable установлены в значение false, поэтому всегда стоит указывать те дескрипторы, которые вы хотите использовать.
Writable
Writable определяет возможность перезаписи значения свойства. Если установлено значение false, то перезаписать значение нельзя.
1const fox = {};23Object.defineProperty(fox, 'name', {4 value: 'Oliver',5 writable: false,6 configurable: true,7 enumerable: true8});910fox.name = 'John'; // Ошибка11// Cannot assign to read only property 'name' of object
Configurable
Дескриптор configurable определяет, можно ли перезаписать дескрипторы свойства с помощью функции Object.defineProperty:
1const fox = {};23Object.defineProperty(fox, 'name', {4 value: 'Oliver',5 writable: true,6 configurable: false,7 enumerable: true8});910Object.defineProperty(fox, 'name', {11 value: 'John',12 configurable: false,13 enumerable: false14}); // Cannot redefine property: name
Свойство будет успешно перезаписано только в том случае, если значения для дескрипторов полностью совпадают с оригинальным присваиванием:
1const fox = {};23Object.defineProperty(fox, 'name', {4 value: 'Oliver',5 writable: true,6 configurable: false,7 enumerable: true8});910Object.defineProperty(fox, 'name', {11 value: 'John',12 writable: true,13 configurable: false,14 enumerable: true15}); // Свойство было перезаписано161718console.log(fox); // { "name": "John" }
Enumerable
Каждый объект можно перебрать с помощью цикла for .. in или же получить названия всех свойств с помощью функции Object.keys. Дескриптор enumerable определяет, будет ли свойство перечисляться в данных ситуациях:Каждый объект можно перебрать с помощью цикла for .. in или же получить названия всех свойств с помощью функции Object.keys. Дескриптор enumerable определяет, будет ли свойство перечисляться в данных ситуациях:
1const fox = {};23Object.defineProperties(fox, {4 'name': {5 value: 'Oliver',6 enumerable: false7 },8 'type': {9 value: 'fox',10 enumerable: true11 }12});1314for (let key in fox) {15 console.log(`${key}: ${fox[key]}`); // type: fox16}1718Object.keys(fox).forEach(key => {19 console.log(`${key}: ${fox[key]}`); // type: fox20});
Геттеры и сеттеры
Два самых интересных дескриптора — get и set, более известные, как геттеры и сеттеры. С их помощью можно запускать обозначенные функции при запросе к получению или записи свойства соответственно.
1const fox = {2 _name: 'Oliver',3 _type: 'Fox'4};56Object.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});1819fox.name = 'John';20fox.name = 'Doe';2122console.log(fox.name); // Fox: Doe2324console.log(`Current name: ${fox._name}. Previous names: ${fox.previousNames.join(', ')}`);25// Current name: Doe. Previous names: Oliver, John
Таким образом, можно выполнить любой код при присваивании и получении свойства, который даже может быть абсолютно не связан с изменяемым свойством, как в примере выше, где перед тем, как записать новое имя, старое добавляется к массиву.