Метапрограммирование в JavaScript и TypeScript +11


Пролог


Хочу представить на Ваш суд ряд мини статеек, в которых будут описаны приемы и основы метапрограммирования. В основном я буду писать об использовании тех или иных техник в JavaScript либо в TypeScript
Эта первая (и надеюсь не последняя) статья из серии.


Так что же такое метапрограммирование


Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running. In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time.

Довольно запутанное описание, но основная польза от метапрограммирования вполне понятна:


… это позволяет программистам минимизировать количество строк кода для имплементации решения, что в свою очередь сокращает время разработки


На самом деле у метапрограммирования есть много лик и обличий. И можно долго дискутировать о том “где заканчивается метапрограммирование и начинается, непосредственно само программирование”


Для себя я принял следующие правила:


  1. Метапрограммирование не занимается бизнес логикой, не меняет её и никак на нее не воздействует.
  2. Если убрать весь код относящийся к метапрограммированию, это не должно (радикально) повлиять на программу.

В JavaScript метапрограммирование, — относительно новое веяние, базовым кирпичиком которого является descriptor.


Descriptor в JavaScript


Descriptor — это своего рода описание (мета информация) некоего свойства или метода в объекте.


Понимание и правильная манипуляция этим объектом (descriptor) позволяет намного больше чем просто создавать и изменять методы или свойства в объектах.
Также descriptor поможет в понимании работы с декораторами (но об этом в следующей статье).


Для наглядности представим, что наш объект это описание квартиры.
Опишем объект нашей квартиры:


let apt = {
    floor: 12,
    number: '12B',
    size: 3400,
    bedRooms: 3.4,
    bathRooms: 2,
    price: 400000,
    amenities: {...}
};

Давайте определим какие из свойств поддаются изменению, а какие нет.


К примеру этаж или общий размер у квартиры изменить невозможно, а вот количество комнат или ванных комнат вполне возможно.
И так у нас есть следующее требование: в объектах apt сделать невозможным изменение свойств: floor и size.


Для решения данной задачи нам, как раз, и понадобятся descriptor-ы каждого из этих свойств. Чтобы получить descriptor, воспользуемся статическим методом getOwnPropertyDescriptor, который принадлежит классу Object.


let descriptor = Object.getOwnPropertyDescriptor(todoObject, 'floor');
console.log(descriptor);
// Output
{
  value: 12,
  writable: true,
  enumerable: true,
  configurable: true
}

Разберем по порядку:
value:any — собственно то самое значение которое в определенный момент было присвоено свойству floor
writable:boolean — определяет возможно или нет изменить значение value
enumerable:boolean — определяет если свойство floor может или нет быть перечисленным — (об этом чуть позже).
configurable: boolean — определяет возможность вносить изменения в объект descriptor.


Для того, чтобы предотвратить возможность изменения свойства floor, после инициализации, необходимо изменить значение writable на false.
Для изменения свойств descriptor-а существует статичный метод defineProperty, который принимает сам объект, имя свойства и descriptor.


Object.defineProperty(apt, 'floor', {writable: false});

В данном примере мы передаем не весь объект descriptor, а только одно свойство writable со значением false.
Теперь попробуем изменить значение в свойстве floor:


apt.floor = 44;
console.log(apt.floor);
// output
12

Значение не изменилось, а при использовании ‘use strict’ получим сообщение об ошибке:


Cannot assign to read only property 'floor' of object...

И вот мы уже не можем менять значение. Однако мы все еще можем вернуть значение writable -> true и затем изменить свойство floor. Что-бы избежать этого необходимо в descriptor-е изменить значение свойства configurable на false.


Object.defineProperty(apt, 'floor', {writable: false, configurable: false});

Eсли мы теперь попробуем изменить значение любого из свойств нашего descriptor-а…


Object.defineProperty(apt, 'floor', {writable: true, configurable: true});

В ответ получим:


TypeError: Cannot redefine property: floor
Другими словами, мы больше ни как не можем изменить ни значение floor, ни его descriptor.

Суммируем


Чтобы сделать значение свойства в объекте неизменным необходимо прописать конфигурацию этого свойства: {writable: false, configurable: false}.


Это можно сделать как во-время инициализации свойства:


Object.defineProperty(apt, 'floor', {value: 12, writable: false, configurable: false});

Либо уже после.


Object.defineProperty(apt, 'floor', {writable: false, configurable: false});

Под конец, рассмотрим пример с классом:


class Apartment {
    constructor(apt) {
        this.apt = apt;
    }

    getFloor() {
        return this.apt.floor
    }
}

let apt = {
    floor: 12,
    number: '12B',
    size: 3400,
    bedRooms: 3.4,
    bathRooms: 2,
    price: 400000,
    amenities: {...}
};

Изменим метод getFloor:


Apartment.prototype.getFloor = () => {
    return 44
};

let myApt = new Apartment(apt);
console.log(myApt);
// output will be changed.
44

Теперь изменим descriptor метода getFloor() :


Object.defineProperty(Apartment.prototype, 'getFloor', {writable: false, configurable: false});
Apartment.prototype.getFloor = () => {
    return 44
};

let myApt = new Apartment(apt);
console.log(myApt);
// output will be original.
12

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


Все написанное выше не претендует быть абсолютно верным или единственно правильным.




К сожалению, не доступен сервер mySQL