Представляем прокси ES2015

Адди Османи
Addy Osmani

Прокси-серверы ES2015Chrome 49 и более поздних версиях) предоставляют JavaScript API-интерфейс заступничества, позволяющий нам перехватывать или перехватывать все операции с целевым объектом и изменять способ работы этого целевого объекта.

Прокси имеют множество применений, в том числе:

  • Перехват
  • Виртуализация объектов
  • Управление ресурсами
  • Профилирование или ведение журнала для отладки
  • Безопасность и контроль доступа
  • Контракты на пользование объектом

Proxy API содержит конструктор Proxy , который принимает назначенный целевой объект и объект-обработчик.

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

Поведение прокси контролируется обработчиком , который может модифицировать исходное поведение целевого объекта множеством полезных способов. Обработчик содержит дополнительные методы-ловушки (например .get() , .set() , .apply() ), вызываемые при выполнении соответствующей операции на прокси-сервере.

Перехват

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

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

Запустив приведенный выше код в Chrome 49, мы получим следующее:

get was called for: power  
"Flight"

Как мы видим на практике, правильное выполнение нашего свойства get или свойства прокси-объекта привело к вызову на метауровне соответствующей ловушки в обработчике. Операции обработчика включают в себя чтение свойств, назначение свойств и применение функций, и все они перенаправляются в соответствующую ловушку.

Функция-ловушка может, если захочет, реализовать операцию произвольно (например, перенаправить операцию целевому объекту). Это действительно то, что происходит по умолчанию, если ловушка не указана. Например, вот бездействующий прокси-сервер пересылки, который делает именно это:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

Мы только что рассмотрели проксирование простых объектов, но мы можем так же легко проксировать объект-функцию, где функция является нашей целью. На этот раз мы воспользуемся ловушкой handler.apply() :

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

Идентификация прокси

Идентичность прокси можно наблюдать с помощью операторов равенства JavaScript ( == и === ). Как мы знаем, при применении к двум объектам эти операторы сравнивают идентификаторы объектов. Следующий пример демонстрирует такое поведение. Сравнение двух разных прокси возвращает false, несмотря на то, что базовые цели одинаковы. Аналогично, целевой объект отличается от любого из своих прокси:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

В идеале вы не должны иметь возможность отличить прокси-сервер от объекта, не являющегося прокси-сервером, чтобы установка прокси-сервера не влияла на результаты работы вашего приложения. Это одна из причин, по которой Proxy API не позволяет проверить, является ли объект прокси-сервером, и не предоставляет ловушек для всех операций с объектами.

Случаи использования

Как уже упоминалось, прокси имеют широкий спектр вариантов использования. Многие из вышеперечисленных, такие как контроль доступа и профилирование, подпадают под общие оболочки : прокси, которые оборачивают другие объекты в одно и то же адресное «пространство». Также была упомянута виртуализация. Виртуальные объекты — это прокси, которые эмулируют другие объекты, при этом эти объекты не должны находиться в том же адресном пространстве. Примеры включают удаленные объекты (которые имитируют объекты в других пространствах) и прозрачные фьючерсы (эмулирующие результаты, которые еще не вычислены).

Прокси как обработчики

Довольно распространенный вариант использования обработчиков прокси-серверов — выполнение проверки или проверки контроля доступа перед выполнением операции над обернутым объектом. Только если проверка прошла успешно, операция пересылается. Приведенный ниже пример проверки демонстрирует это:

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

Более сложные примеры этого шаблона могут учитывать все различные операции, которые могут перехватывать прокси-обработчики. Можно представить, что реализация должна дублировать шаблон проверки доступа и пересылки операции в каждой ловушке.

Это может быть сложно абстрагировать, поскольку каждую операцию, возможно, придется пересылать по-разному. В идеальном сценарии, если бы все операции можно было единообразно провести через одну ловушку, обработчику нужно было бы выполнить проверку достоверности только один раз в одной ловушке. Вы можете сделать это, реализовав сам обработчик прокси-сервера в качестве прокси. К сожалению, это выходит за рамки данной статьи.

Расширение объекта

Другой распространенный вариант использования прокси — расширение или переопределение семантики операций над объектами. Например, вы можете захотеть, чтобы обработчик регистрировал операции, уведомлял наблюдателей, генерировал исключения вместо возврата неопределенного значения или перенаправлял операции на разные цели для хранения. В этих случаях использование прокси может привести к совершенно иному результату, чем использование целевого объекта.

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

Контроль доступа

Контроль доступа — еще один хороший вариант использования прокси. Вместо того, чтобы передавать целевой объект фрагменту ненадежного кода, можно передать его прокси, завернутый в своего рода защитную мембрану. Как только приложение посчитает, что ненадежный код выполнил определенную задачу, оно может отозвать ссылку, которая отделяет прокси-сервер от его цели. Мембрана будет рекурсивно распространять это отделение на все объекты, достижимые из исходной заданной цели.

Использование отражения с прокси

Reflect — это новый встроенный объект, который предоставляет методы для перехватываемых операций JavaScript, что очень полезно для работы с прокси. По сути, методы Reflect такие же, как и у обработчиков прокси .

Статически типизированные языки, такие как Python или C#, уже давно предлагают API отражения, но JavaScript на самом деле не нуждается в нем, поскольку он является динамическим языком. Можно утверждать, что в ES5 уже есть немало функций отражения, таких как Array.isArray() или Object.getOwnPropertyDescriptor() , которые в других языках будут считаться отражением. ES2015 представляет API Reflection, в котором будут размещены будущие методы для этой категории, что упрощает их анализ. Это имеет смысл, поскольку Object задуман как базовый прототип, а не как набор методов отражения.

Используя Reflect, мы можем улучшить наш предыдущий пример Superhero для правильного перехвата полей при получении и установке ловушек следующим образом:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

Какие выходы:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

Другой пример: кто-то может захотеть:

  • Оберните определение прокси внутри специального конструктора, чтобы избежать создания нового прокси вручную каждый раз, когда мы хотим работать с определенной логикой.

  • Добавьте возможность «сохранять» изменения, но только в том случае, если данные действительно были изменены (гипотетически из-за того, что операция сохранения очень дорогая).

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

Дополнительные примеры API Reflect см. в разделе Прокси-серверы ES6 от Tagtree.

Полизаполнение Object.observe()

Хотя мы прощаемся с Object.observe() , теперь их можно заполнять с помощью прокси ES2015. Саймон Блэквелл недавно написал прошивку Object.observe() на основе прокси, которую стоит проверить. Эрик Арвидссон также написал довольно полную версию еще в 2012 году.

Поддержка браузера

Прокси-серверы ES2015 поддерживаются в Chrome 49, Opera, Microsoft Edge и Firefox. Общественность Safari неоднозначно восприняла эту функцию, но мы сохраняем оптимизм. Reflect доступен в Chrome, Opera и Firefox и находится в разработке для Microsoft Edge.

Google выпустил ограниченный полифилл для Proxy . Это можно использовать только для общих оболочек , поскольку оно может проксировать только свойства, известные на момент создания прокси.

дальнейшее чтение