ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Categories:

JavaScript: объекты класса Proxy, решение задачи Observable

Прочел подраздел 14.1 «Proxy и Reflect» четырнадцатого раздела «Разное» первой части «Язык программирования JavaScript» учебника по JavaScript.

Английское слово «proxy» имеет много значений. В этом посте это слово меня интересует в значении «заместитель» или «посредник». При проектировании программ используются разные шаблоны, одним из которых является шаблон с названием «Заместитель» (по-английски «Proxy pattern»):

https://ru.wikipedia.org/wiki/Заместитель_(шаблон_проектирования)
https://en.wikipedia.org/wiki/Proxy_pattern

В этом контексте «Proxy» — это объект-посредник, который контролирует доступ к другому объекту, перехватывая все вызовы.

Как это работает? Программист обращается к объекту-посреднику (заместителю) так, будто это нужный ему реальный объект. Объект-посредник пропускает все вызовы к реальному объекту через себя и передает их реальному объекту. Ответы от реального объекта точно так же проходят обратно, возвращаясь к программисту через объект-посредник.

Зачем это нужно? Чтобы была возможность приурочить к конкретному обращению к реальному объекту (вызову) какие-либо действия. Например, с помощью объекта-посредника можно подсчитать количество чтений какого-либо свойства некоего реального объекта за время работы программы. Или можно на любую попытку чтения какого-либо свойства реального объекта выдавать на экран сообщение.

В языке JavaScript для создания объектов-посредников существует встроенная функция-конструктор Proxy.

К этому подразделу в учебнике есть три задачи. Мне понравилась последняя из них, которая называется «Observable», что по-русски значит «Наблюдаемый».

В задаче дан следующий код:
function makeObservable(target) {
    /* ваш код */
}

// Код ниже нужен для тестирования нашей функции:

let user = {};
user = makeObservable(user);

user.observe((key, value) => {
    alert(`SET ${key}=${value}`);
});

user.name = "John"; // выводит: SET name=John

Требуется написать тело функции makeObservable, которая получает в первом параметре target объект и делает его наблюдаемым.

Что автор задачи понимает под фразой «наблюдаемый объект»? Это такой объект, который при каждом записывании нового значения в любое его свойство будет выполнять некое задаваемое ему действие. В данном случае таким действием будет вывод на экран сообщения с названием изменяемого в этот момент свойства key и новым значением этого свойства value. Объект и называется наблюдаемым потому, что пользователю нашего скрипта (наблюдателю) будет видно каждое изменение каждого свойства данного объекта.

Каким образом функция makeObservable делает заданный ей объект наблюдаемым? Это происходит во второй строке тестовой части заданного в задаче кода, а именно:
user = makeObservable(user);
То есть функция makeObservable получает в первый параметр целевой объект user, после чего внутри себя трансформирует его так, чтобы он стал наблюдаемым, и возвращает ссылку на трансформированный объект. Возвращенная нашей функцией ссылка записывается в ту же переменную user.

Каким образом задается действие, которое будет выполняться при перезаписи любого из свойств целевого объекта? В тестовой части заданного в задаче кода это делается так:
user.observe((key, value) => {
    alert(`SET ${key}=${value}`);
});
То есть действие заключается в выводе названия перезаписываемого свойства key целевого объекта и нового значения этого свойства value на экран с помощью функции alert. Само действие задается стрелочной функцией и передается в качестве первого параметра методу observe целевого объекта.

Начнем писать тело функции makeObservable.

Учитывая вышеизложенное про объект-посредник, понятно, что для перехвата обращений (вызовов) на перезапись свойств целевого объекта этот самый целевой объект следует обернуть в объект-посредник, который можно создать с помощью встроенной функции-конструктора Proxy, как это было неоднократно продемонстрировано в рассматриваемом подразделе 14.1 учебника. Затем следует возвратить полученный объект-посредник в качестве результата работы функции. Меняем код:
1.
function makeObservable(target) {
    /* ваш код */

    return new Proxy(target, {
        set(target, property, value, receiver) {
            /* тут будут дополнительные действия */
            target[property] = value;
            return true;
        }
    });

}
В первый параметр встроенной функции-конструктора Proxy передаем целевой объект target. Вторым параметром передаем безымянный объект с одним методом-ловушкой set, который отлавливает все обращения на перезапись любого из свойств целевого объекта target.

В этом коде мы пока при отлове обращений на перезапись свойств целевого объекта не делаем ничего дополнительного. Просто записываем новое значение в свойство объекта и возвращаем значение true, чтобы сообщить об успешной перезаписи свойства целевого объекта. По причинам, изложенным в подразделе 14.1, это лучше сделать с помощью дополняющей функцию-конструктор Proxy функции-конструктора Reflect. Меняем код:
2.
function makeObservable(target) {
    /* ваш код */

    return new Proxy(target, {
        set(target, property, value, receiver) {
            /* тут будут дополнительные действия */
            return Reflect.set(target, property, value, receiver);
        }
    });

}

Откуда у объекта user появляется метод observe, если в части кода для тестирования нашей функции этот объект изначально был объявлен пустым let user = {};? Очевидно, что этот метод должен быть добавлен к целевому объекту нашей функцией. Меняем код:
3.
function makeObservable(target) {
    /* ваш код */

    target.observe = function(action) {
        
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            /* тут будут дополнительные действия */
            return Reflect.set(target, property, value, receiver);
        }
    });

}
Первым параметром методу target.observe передается действие (функция) action, которое будет выполняться дополнительно при перезаписи любого из свойств целевого объекта.

В этом месте у меня возник вопрос. Как передать функцию action из метода target.observe в объект-посредник? Для этого я решил создать в целевом объекте отдельное свойство с таким же именем. Меняем код:
4.
function makeObservable(target) {
    /* ваш код */

    target.observe = function(action) {
        this.action = action;
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            /* тут будут дополнительные действия */
            return Reflect.set(target, property, value, receiver);
        }
    });

}

Теперь можно полученное дополнительное действие (функцию) action выполнить в теле метода-ловушки set объекта-посредника. Меняем код:
5.
function makeObservable(target) {
    /* ваш код */

    target.observe = function(action) {
        this.action = action;
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            target.action(property, value);
            return Reflect.set(target, property, value, receiver);
        }
    });

}

В принципе, решение готово, но оно еще пока не работает. Почему? Тут две связанных между собой причины. Во-первых, когда мы в методе target.observe создаем новое свойство целевого объекта this.action = action;, это тоже является перезаписью свойства целевого объекта и поэтому отлавливается методом-ловушкой set объекта-посредника. При этом (во-вторых) метод-ловушка set обращается к свойству target.action, которого еще не существует. В результате получаем ошибку.

Я решил переписать метод-ловушку set так, чтобы он сначала делал запись нового значения в свойство целевого объекта, а уж после этого выполнял дополнительное действие action. Меняем код:
6.
function makeObservable(target) {
    /* ваш код */

    target.observe = function(action) {
        this.action = action;
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            let res = Reflect.set(target, property, value, receiver);
            target.action(property, value);
            return res;
        }
    });

}

Этот вариант уже работает на вышеизложенном тестовом коде, заданном в задаче. Наблюдаемый объект дважды сообщает о записи новых значений в свои свойства: 1) при создании свойства action, 2) при создании свойства name.

Возможно, мы не хотим, чтобы объект-посредник сообщал об изменении свойства action целевого объекта, ведь это свойство вспомогательное. Тогда можно, к примеру, поставить соответствующее условие в методе-ловушке set:
7.
function makeObservable(target) {
    /* ваш код */

    target.observe = function(action) {
        this.action = action;
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            let res = Reflect.set(target, property, value, receiver);
            if(property != "action") {
                target.action(property, value);
            }
            return res;
        }
    });

}

Авторы учебника реализовали это не с помощью условия в методе-ловушке set, а с помощью массива. Можно показать этот способ так:
8.
function makeObservable(target) {
    /* ваш код */

    target.action = [];

    target.observe = function(action) {
        this.action.push(action);
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            let res = Reflect.set(target, property, value, receiver);
            target.action[0](property, value);
            return res;
        }
    });

}
В этом случае запись в свойство action целевого объекта происходит только один раз, инструкцией target.action = [];. В этой инструкции в свойство action записывается ссылка на пустой массив. На момент выполнения этой инструкции объект-посредник еще не создан, поэтому перезапись свойств целевого объекта еще не отслеживается, следовательно сообщение об изменениии свойства целевого объекта на экран выдано не будет.

Когда же происходит запуск метода target.observe, объект-посредник уже существует, однако, перезаписи свойства action не происходит, потому что ссылка в памяти на массив не меняется. Меняется лишь содержимое массива, но не содержимое свойства action! Поэтому сообщение об изменении свойства action опять же не будет выдано на экран.

Ну и напоследок следует, видимо, прописать в методе-ловушке set для дополнительного действия условие для того, чтобы это дополнительное действие выполнялось лишь в том случае, когда запись в свойство целевого объекта выполнена успешно. Ведь, если перезапись свойства в итоге не выполнена, то зачем об этом сообщать? Меняем код (окончательный вариант):
9.
function makeObservable(target) {
    /* ваш код */

    target.action = [];

    target.observe = function(action) {
        this.action.push(action);
    };

    return new Proxy(target, {
        set(target, property, value, receiver) {
            let res = Reflect.set(target, property, value, receiver);
            if(res) {
                target.action[0](property, value);
            }
            return res;
        }
    });

}
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments