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

  • Tutorial

Пролог


Хочу представить на Ваш суд ряд мини статеек, в которых будут описаны приемы и основы метапрограммирования. В основном я буду писать об использовании тех или иных техник в 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 и как его можно использовать.


Все написанное выше не претендует быть абсолютно верным или единственно правильным.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 23

    +5
    Метопрограммирование — создание программ порождающих программы.
    В основном конечно под этим имеют ввиду больше генерацию кода или рантайму манипуляции над инстансами будь то модификация прототипа в JS HOCом или подмена функции. Хотя строго говоря можно под это дело и компиляторы отнести.
    Делится по времени исполнения, среде исполнения, и средстве описания.
    По времени: compile time, runtime. Первая группа: Препроцессоры (язык си), транспайлеры Babel, генераторы ну пусть YACC, bezon. Вторая: самомодифицирующийся код, eval, подмена виртуальных функций в c++. По среде исполнения, по простому кто выполняет изменение. По средству описания, к примеру макросы раста пишутся с использованием грамматики раста.
      0
      Ещё метапрограммирование — это не только создание программы программой, но и обработка, анализ программы программой, корректировка её поведения. См, например:

      exploringjs.com/es6/ch_proxies.html#sec_programming-vs-metaprogramming
        0
        эммм… так это и написано, правда на английском.
        взято с ВИКИ.
          +1
          Тут скользкий момент. Мой посыл наверное тоже не совсем понятен и кажется, скорей всего что я просто процетировал википедию.
          Попробую объяснить, обычно когда говорят про обработку, то уточняют — к примеру: тайп чекер, бьютифаер ну и тд
          Про анализ тоже обычно уточняют — вот мол у нас тут статический анализатор.
          А когда уже реч заходит о корректировки поведения к примеру о рантайм генерации ассеблерного кода, к примера либа asmjit. То говорят уже как о явном представителе метапрограммирования. Тоесть я к тому, что формально то конечно определение подходит, но обычно общеупотребимо никто к примеру компилятор не называет примером метапрограммирования.
          –1
          Начнем с последней строчки:
          Все написанное выше не претендует быть абсолютно верным или единственно правильным.
          .
          А теперь по делу: HOCи являются частью метапрограммирования, они же и находятся на той черте, где метапрограммирование легко может стать программированием. Подмена вункций тому пример.
          И да метапрограммирование, не является чем-то новым, корни идут от компиляторов старых языков в годах так 70-ых (если не раньше).
          image
            +1
            И теперь вы хотите чтоб на вашу в целом «статью-мнение» не было других мнений? Возможно даже таких же не абсолютно верных и не единственно правильных.
              0
              почему же нет?
              Вон уже сколько мнений…
              0
              Нет все же HOC это чистый вид метапрограммирования. Возмем к примеру mixin и HOC, казалось бы в чем разница? Ответ прост, первое не создает новый тип, а только расширяет декларацию сушествуюшего. Второе же, работает во время исполнения и создает новый тип (в плане JS где все же прототипное программирование, небольшие осечки терминологические, но суть верна).
            +1
            А имеет ли смысл использование defineProperty, когда уже давно есть и свойства и Proxy? Кажется, что это уже некое легаси, не?
            0
            По средству описания, к примеру макросы раста пишутся с использованием грамматики раста.

            Неверно. Грамматика макросов совершенно другая.
              0
              Ну хотя бы proof бы сделали, не забывайте о function like макросах. Хотя нет, да даже в общем вы не правы, декларация макросов и их описания это часть грамматики раста. Возьмем к примеру с++ препроцессор это отдельная часть (то что он сросся с компиляторами это уже исторически) или тот же moc. Ну ладно, для любителей красивых примеров предположим что я имел ввиду гомоиконность LISP. Кстати если уж говорить о еще одном примере темплейт хаскель, то там тоже среда исполнения сам компилятор но грамматика завозится расширением, тоесть не является частью языка, так что в любом случая я прав. Грамматика то раста, а то что она другая не делает утверждение неверным. И то для функшион лайк макросов она почти как для функций не учитывая маджик типы и некоторые атрибуты
                0
                Зачем мне делать пруф, если это очевидная вещь, которая обсуждалась тысячи раз? github.com/serde-rs/json/blob/master/src/macros.rs#L88 и любой другой пример макроса.

                не забывайте о function like макросах.

                Это ничего не значит. Абсолютно неважно что я там забуду — любой контр-пример рушит ваш тезис т.к. в макросе уже будет внешняя по отношению к языку грамматика. А сколько её в каждом конкретном случае — неважно.

                  0
                  в макросе уже будет внешняя по отношению к языку грамматика

                  Макросы это часть языка Rust. Я уже не знаю как вам объяснить, оставайтесь при своем
                    0
                    Ну это похоже на слив и на генерацию бреда в ответ. К чему тут «не знаю», если никаких попыток объяснить не было? Да и их не может быть, ведь изначальный тезис — полная чушь.

                    В чём заключался изначальный тезис? В том, что макросы в расте используют НЕ базовую грамматику, т.е. не грамматику языка, а грамматику расширенную, отдельную. Т.е. ту, которую НЕЛЬЗЯ использовать в самом языке.

                    Очевидно, что я опроверг этот тезис. Попытка использовать защиту от «часть языка» не работает, т.к. часть языка всё. А говорилось именно о грамматике языка, причём языка базового(на то, на котором пишется обычный код).

                    Смысл той методичке, которую вы повторяете в том, чтобы как-то возвысится над препроцессором в С/С++, ведь там это типа отдельная грамматика. Но, очевидно, что в расте это такой же препроцессор, только ещё более страшный, нелепый и убогий. А то, что формально он препроцессором не является — мало что меняет. Ведь суть проблемы С/С++-препроцессора не в том, что это какая-то отдельная программа(которой уже давно нет), а в том, что это нечто чуждое.
                  0
                  Зачем править сообщения? Отвечал я на совершенно другое сообщение в котором не было этих рассуждений, а они появились тут после и я даже их не видел.

                  По поводу самих рассуждения — я уже там ответил на подобные попытки. В макросах раста нет грамматики раста — там есть совершенно другая грамматика. Я это доказал там ниже. Попытка съехать на отдельные типы макросов — глупая попытка, т.к. ничего не меняет. К тому же даже для них там грамматика своя.

                  В конечном итоге эту попытку можно назвать лишь одним — наивной попыткой обмануть публику и сказать, что макросы в расте это не какой-то там другой язык(как в С/С++), а что это тот самый язык, с той же грамматикой. Но — это враньё. От начала и до конца.

                  И как я уже говорил — единственная возможность подобных раст-пропагандистов оправдаться — это попытка съехать на «препроцессор в С/С++ — был когда-то отдельной программой и именно это „я и имел ввиду“, но нет. Когда имеют ввиду именно это — говорят это, либо то, из чего прямо это следует.

                  Когда говорят про грамматику — говорят про грамматику. Сколько там отдельных программ разбирают эту грамматику — неважно. Пусть хоть десять — это ничего не меняет.
                +2
                Странный вывод,
                "… это позволяет программистам минимизировать количество строк кода для имплементации решения, что в свою очередь сокращает время разработки"

                Далеко не всегда и не везде.
                Скорее уж дело не в минимизации количества строк кода (писать if-else в одну строчку — не метапрограммирование и не сокращает время разработки), а в переходе на следующий уровень абстракции, когда оперируешь не только базовым лингвистическим описанием, но и различными высокоуровневыми кодогенерациями, обработчиками в рантайме и прочим.
                Я к тому, что количество строк кода здесь тоже следствие, как и время разработки.
                  0
                  Да обсолютно верно, первая параметризация она как правило примитивная, обычно пишут всякие билдеры и играют с ad hoc полиморфизмом. Потом идет уже параметрический полиморфизм, а потом уже параметризация программ программами. Ну тут я не эксперт так и не дополз до книги Пирса. Обычно, что меньше писать это следствие вообше любого повышения синтаксических абстракций поэтому таки да спорный момент преймущества
                  0
                  let descriptor = Object.getOwnPropertyDescriptor(todoObject, 'floor');


                  Минорная ошибка:
                  todoObject 
                  ->
                  apt
                    0
                    В статье не показана связь дескрипторов свойств с метапрограммированием, даже тем его определением, которое дано в статье. Хотя связь эта, без сомнения есть, но приведенные примеры демонстрируют только часть техники рефлексии и интроспекции в JavaScript. Ни одного примера, где бы эта техника давала практический результат, выражающийся в сокращении кода, повышении гибкости, повышении уровня абстракции или динамическом изменении поведения программных абстракций, не приведено.
                    Думаю, что это потому, что автор «для себя» принял очень сомнительные правила.
                    1. Метапрограммирование не занимается бизнес логикой, не меняет её и никак на нее не воздействует.
                    2. Если убрать весь код относящийся к метапрограммированию, это не должно (радикально) повлиять на программу.

                    Почему не занимается бизнес-логикой? Радикально повлиять это как? То есть, работу с дискрипторами свойств можно как добавлять в приложение, так и удалять из него ну просто так, без влияния на поведение приложения. Метапрограммирование представляется как нечто, что замкнуто в себе и ни на что ни как не воздействует.

                    Вот тут моя лекция по метапрограммированию с четкими определениями и примерами:

                      0
                      Как же разительно отличаются комментарии на русскоязычных и англоязычных ресурсах.
                      На русскоязычных, основные комментарии это поиск в чем автор не прав и максимально показать насколько комментирующий умнее автора. Жаль.

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


                        0
                        Эта мини-статейка должна называться «Дескрипторы свойств в JavaScript»
                          0
                          Эта первая (и надеюсь не последняя) статья из серии.

                          Это так себе стратегия. На Хабре любят, чтобы каждая статья была по существу, а не обещала рассказать что-нибудь в следующей статье.

                          А на счёт ресурсов — у Хабра просто относительно высокий уровень. Многие здесь сидящие метапрограммированием пользуются.
                          Только компании в своих блогах могут себе позволить статьи в стиле КО — и то только потому что их кармой не задушишь.

                      Only users with full accounts can post comments. Log in, please.