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

Dev IT JavaScript 1157

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

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

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

Апгрейд 2025:

Вік живи, вік учись і дурнем помреш, тому що можна модифікувати код з використанням суто Object.defineProperty() і код з кількома Object.defineProperty() буде працювати. А саме:

const work2025 = {}

Object.defineProperty(
  work2025,
  'name',
  {
    value: 'Якийсь ноунейм',
    configurable: true
  }
)

Object.defineProperty(
  work2025,
  'named',
  {
    get(){
      return this.name
    },
    set(value){
        Object.defineProperty(work2025, 'name',  { value } )
    }
  }
)

work2025.named = 'Перейменували ноунейма'
console.log(work2025.named) // Перейменували ноунейма

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

Апгрейд 2025: у фаєрфоксі теж працює!

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()!

Апгрейд 2025: якщо властивість була створена безпосередньо в літералі об'єкта, то вона стає true, її примусово потрібно робити false.

const human = {
    age: 42, // початкове значення
    name: 'John'
}
Object.defineProperty(human, 'age', {
    value: 44, // змінили на 44
    enumerable: false,
    writable: false, 
    configurable: false
})
human.age = 43 // не спрацює, значення 44