Сеттер, геттер і дескриптори доступу

Dev IT JavaScript 1028

Є думка, що в джаваскрипт немає приватних властивостей. Але це не зовсім так, тому що у 2020 році є механізми закриття доступу до змінної в об'єкті.

Варіант старий, умовно-декларативний, коли ми усно домовляємося не використовувати прямий доступ до змінної, для цього позначаємо її підкресленням:

const obj1 = {
  _name: '', // оголошуємо (суто для розробників) властивість приватною
  set name(value) {
    this._name = value;
  },
  get name() {
    return this._name;
  }
};
obj1.name = 'Vasia Pupkin';
console.log(obj1.name); // Vasia Pupkin
obj1._name = 'я рукожоп'; // рукожопство, бо ми попереджали, що властивість є приватною і так не тре робити
console.log(obj1.name); // я рукожоп

Варіант з дискриптором даних, коли ми прописуємо змінній якесь значення і забороняємо цю змінну міняти. Але такий варіант не має сенсу, бо для цього є ключове слово const:

const obj2 = {};
Object.defineProperty(
  obj2,
  'name',
  {
    value: 'Вася Пупкін',
  }
);
console.log(obj2.name) // Вася Пупкін
obj2.name = 'Іван Петров';
console.log(obj2.name) // все одно буде Вася Пупкін

Є сенс у такій конструкції, коли ми використовуємо сеттер і геттер без створення окремої властивості, назва сеттера/геттера і буде цією властивістю. На перший погляд може здатися, що це просто властивість name, але це не так, оскільки у сеттері та геттері можна писати умови, або ж навіть не використовувати чи сеттер чи геттер:

const obj3 = {};
Object.defineProperty(
  obj3,
  'name',
  {
    get() {
      return name;
    },
    set(value) {
      name = value;
    }
  }
);
obj3.name = 'cat';
console.log('obj3: ', obj3.name)

Недолік Object.defineProperty() у тому, що неможливо створити властивість, а потім додати до неї сеттер і геттер. Здавалося б логічним є такий код але він працювати не буде:

const notWork = {};
Object.defineProperty(
  notWork,
  'name',
  {
    value: 'Якийсь ноунейм',
    configurable: true // ugrade 2021: якщо поставити writable, то працюватиме, але це не зовсім те, оскільки прямий перезапис
  }
);
Object.defineProperty(
  notWork,
  'named',
  {
    get(){
      return this.name;
    },
    set(value){
      this.name = value;
    }
  }
);
notWork.named = 'Перейменували ноунейма?';
console.log(notWork.named) // Якийсь ноунейм

І саме для цього існує метод Object.defineProperties():

const obj4 = {};
Object.defineProperties(
  obj4,
  {
    'name':
    {
      value: 'Якийсь Ноунейм',
      configurable: true // можливість модифікувати (але не міняти безпосередньо)
    },
    'named':
    {
      get() {
        return this.name;
      },
      set(value) {
        Object.defineProperty(
          this, // звертаємося до поточного об'єкта
          'name', // властивість 'name'
          {value} // встановлюємо значення {value: value}
        );
      }
    }
  }
);
console.log(obj4.named) // Якийсь Ноунейм
obj4.named = 'Нове імя для Ноунейма';
console.log(obj4.named) // Нове імя для Ноунейма
obj4.name = 'Тестова заміна імені';
console.log(obj4.named) // Нове імя для Ноунейма

У проєкті є ще спеціальний синтаксис для private властивостей, але на разі працює тільки на webkit'ах:

const obj5 = class {
  #private = 'якесь значення';
  getPrivate() {
    return this.#private;
  }
  setPrivate(value) {
    this.#private = value;
  }
}
const privateTest = new obj5();
console.log(privateTest.getPrivate()) // якесь значення
privateTest.setPrivate('встановлюємо нове значення');
console.log(privateTest.getPrivate()) // встановлюємо нове значення

І кілька слів про дескриптори. Їх є шість і по дефолту вони всі false. Себто якщо якийсь дескриптор нам потрібен, необхідно задати йому значення true.

  1. value: 'якесь значення'
  2. writable: true, // чи можна міняти
  3. enumerable: true, // чи можна юзати у циклі for-in та Object.key()
  4. configurable: true, // чи можна модифікувати/видаляти
  5. set(), // сеттер
  6. get() // геттер

Примітка: не можна використовувати value i writable разом з set() i get()!