Прокси — это новые объекты JavaScript для которых программист должен определить своё поведение. Стандартное поведение всех объектов определено в движке JavaScript, который чаще всего написан на C++. Прокси позволяют программисту определить практически любое поведение объекта JavaScript, они полезны для написания базовых объектов или оберток функций или для создания абстракций виртуальных объектов и предоставляют API для мета-программирования. Сейчас Прокси не входит в стандарт, но его стандартизация запланирована в ECMAScript Harmony. Чтобы избежать путаницы уточню, что эти Прокси не имеют ничего общего с прокси серверами.
1. Общие промежуточные абстракции
2. Создание виртуальных объектов: обертки существующих объектов, удаленные(от слова далекий) объекты, ленивое создание объектов (Пример ORM — Ruby ActiveRecord, Groovy GORM)
3. Прозрачное ведение логов, трассировки, профилирования
4. Внедрение предметно-ориентированных языков
5. Динамический перехват несуществующих методов, создание отсутствующих методов (__noSuchMethod__)
6. База для специфичных итераторов
1. Эта особенность языка называется «всеобъемлющий механизм»(ориг. catch-all mechanism) — это имя используется для описания этой особенности. В тексте будет использоваться оригинальное название.
2. Другое название понятию «всеобъемлющий механизм» — посредник (ориг. intercession API)
3. Объект, который обрабатывает свойство называется обработчик — handler
4. Объект, свойства которого заменяются называется прокси — proxy
5. Объект/метод, который создает прокси объекты называется фабрика прокси — proxy factory
6. Методы, входящие в обработчик, которые обрабатывают какое-либо поведение называются ловушки/перехватчики traps (по аналогии с операционными системами)
7. Прокси может быть захватывающим/активным (ориг. trapping) или быть распущенным (ориг. fixed)
Существует два вида фабрик прокси. Одна для для объектов другая для функций.
Конструктор прокси-объекта:
Конструктор прокси-функции:
proto — не обязательный параметр, определяющий прототип прокси
callTrap — функция, которая будет замещать оригинальную функцию при прямом вызове прокси-функции (пример ниже все объяснит). Важно: this замещающей функции (callTrap) совпадает с this замещаемой.
constructTrap — не обязательный параметр — функция, которая будет заменять оригинальный конструктор функции при вызове через new. Важно: this замещающей функции (constructTrap) всегда undefined. Если constructTrap не передан, то используется callTrap в котором this делегирует от proxy.prototype (обычное поведение конструкторов ES5 Глава 13.2.2)
handler — объект, которые определяет поведение прокси. Этот объект должен всегда содержать Базовые ловушки (traps)
Как это читать:
Эти перехватчики не обязательны, если они не будут определены, то будет использоваться логика по умолчанию.
По умолчанию будет выполняться следующая логика
В виде таблицы можно посмотреть тут
Производными ловушки называются «производными» потому, что они могут быть определены базовыми ловушками. Например, ловушка has может быть определена, используя ловушку getPropertyDescriptor (проверить возвращает undefined или нет). С помощью производных перехватчиков можно с меньшими затратами эмулировать свойства, поэтому они и были определены. Ловушка fix была введена для того, чтобы позволить прокси взаимодействовать с
Обработчик fix имеет 2 варианта:
1. отклонить запрос (fix должен возвратить undefined), тогда будет вызвано исключение TypeError.
2. выполнить запрос и возвратить описание объекта в виде
Следующий кусок кода создает прокси, который прерывает доступ к свойствам и возвращает для каждого свойства «p» значение «Hello, p»:
Живой пример
Создадим простую обертку, которая считает сколько раз какое свойство было получено:
Функция makeSimpleProfiler получает в качестве аргумента объект, который мы хотим мониторить. Она возвращает объект, имеющий 2 свойства: сам прокси и stats — количество вызовов.
Живой пример
Функция ForwardingHandler в строке два функции makeSimpleProfiler создает простой переадресующий проки, который прозрачно делегирует все операции, выполняемые над прокси, целевому объекту. Вот как она выглядит:
С полной версией этой функции можно ознакомиться тут. Этот переаресующий обработчик (forwarding handler) скорее всего станет частью стандарта.
Прокси позволяют вам создавать виртуальные объекты, которые могут эмулировать удаленные объекты или существующие объекты. Для демонстрации давайте создадим обертку вокруг существующей библиотеки для удаленной коммуникации в JavaScript. Библиотека web_send Tyler Close'а может быть использована для создания удаленных связей с объектами, расположенными на сервере. Теперь мы можем вызывать методы по HTTP POST запросам, используя эту удаленную связь. К сожалению, удаленные связи не могут быть использованы как объекты.
Сравним. Для вызова удаленной функции изначально нужно было вызывать:
Используя прокси мы можем сделать этот вызов более естественным, напишем нашу обертку:
Теперь мы можем делать вот так
Используя Прокси возможно эмулировать хук __noSuchMethod__ в тех браузерах, которые его не поддерживают (но сейчас это не актуально).
Этот объект использует NoSuchMethodTrap-прокси в котором ловушка get заменяет оригинальный __noSuchMethod__.
Живой пример
Сообщения высшего порядка это сообщения, получающие другие сообщения в качестве аргумента, как описано тут.
Сообщения высшего порядка похожи на функции высшего порядка, но они более емкие в коде. Используя прокси очень просто создавать сообщения высшего порядка. Рассмотрим специальный объект "_", который переделывает сообщения в функции:
msg это функция, которая использует один аргумент, как если бы она была определена как
Вот код объекта "_":
Живой пример
Прокси дают Javascript программистам возможность эмулировать странности базовых объектов, таких как DOM. Это позволяет авторам библиотек оборачивать базовые объекты для того, чтобы их «приручить» (прим. Песочницы) или исправить их для сокращения кросс-браузерной несовместимости.
Предыдущие примеры использовали объекты. Вот простой пример прокси-функции:
Живой пример
Прокси-функции открывают возможности для написания особых идиом, которые были нам не доступны в чистом JavaScript. В первую очередь функции-прокси могут создать из любого объекта функцию (callable object):
Второе, функции-прокси можно использовать для создания псевдо-классов, сущности которых функции(instances are callable).
Для любителей языка: Прокси могут быть использованы для создания «стандартных» объектов и функций JavaScript в самом JavaScript. Возможность менять семантику объектов JavaScript внутри JavaScript значительно упрощает механизм внесения мелких изменений в семантику для проведения экспериментов. Частичную реализацию семантик внутри JavaScript можно наблюдать в значениях ловушек по умолчанию. Другой пример: Массивы JavaScript внутри JavaScript, используя Прокси.
Избегайте неявных вызовов toString внутри ловушек. Будьте аккуратны с аргументом receiver ловушек get и set. receiver представляет ссылку на прокси, поэтому неявно вызвав get и set приведет к бесконечной рекурси. Например вызов console.log(receiver) для дебага внутри сеттера вызовет метод toString, который приведет к бесконечной рекурсии.
Если
Прокси обработчики сами могут быть прокси. Прокси трейсер ниже доменстрирует этот паттерн, он используется для создания «туннелирования» всех операций для обработчику через одну ловушку get.
Трейсер просто печатает описание всех операций, который он обрабатывал. Это очень полезно для дебага или для изучения работы прокси.
Живой пример
Зонд имеет схожую с трейсером логику он логирует все мета-уровневые операции, примененные к нему.
Живой пример
Сейчас только Firefox 4.0 поддерживает прокси. Есть реализация прокси для Node.js в виде расширения: node-overload (частичная поддержка) node-proxy (практически полная поддержка). В любом случае Прокси будут внесены в стандарт так, что скоро он появится и в вашем браузере!
1. ECMAScript Harmony
2. Документация на The Mozilla Developer Network
3. Разработка стандарта: первая часть этого Google Tech Talk и вот эта бумага, показанная на DLS 2010.
4. Brendan Eich, в своем блоге кратко объясняет основы Прокси.
5. Частичный список открытых проблем Прокси в Firefox 4.
6. Слайды Brendan Eich из его оклада на jsconf
1. MDC Proxy (DRAFT)
2. ES5 Catch-all Proxies
3. Proxy Inception (Brendan Eich)
4. Tutorial: Harmony Proxies (Tom Van Cutsem)
Если вам что-то не понятно, пожалуйста, задавайте свои вопросы или посмотрите слайды. Предложения, пожелания, критика приветствуется!
Где их можно использовать
1. Общие промежуточные абстракции
2. Создание виртуальных объектов: обертки существующих объектов, удаленные(от слова далекий) объекты, ленивое создание объектов (Пример ORM — Ruby ActiveRecord, Groovy GORM)
3. Прозрачное ведение логов, трассировки, профилирования
4. Внедрение предметно-ориентированных языков
5. Динамический перехват несуществующих методов, создание отсутствующих методов (__noSuchMethod__)
6. База для специфичных итераторов
Понятия
1. Эта особенность языка называется «всеобъемлющий механизм»(ориг. catch-all mechanism) — это имя используется для описания этой особенности. В тексте будет использоваться оригинальное название.
2. Другое название понятию «всеобъемлющий механизм» — посредник (ориг. intercession API)
3. Объект, который обрабатывает свойство называется обработчик — handler
4. Объект, свойства которого заменяются называется прокси — proxy
5. Объект/метод, который создает прокси объекты называется фабрика прокси — proxy factory
6. Методы, входящие в обработчик, которые обрабатывают какое-либо поведение называются ловушки/перехватчики traps (по аналогии с операционными системами)
7. Прокси может быть захватывающим/активным (ориг. trapping) или быть распущенным (ориг. fixed)
Как с ними работать — API
Существует два вида фабрик прокси. Одна для для объектов другая для функций.
Конструктор прокси-объекта:
var proxy = Proxy.create(handler, proto);
Конструктор прокси-функции:
var proxy = Proxy.createFunction(handler, callTrap, constructTrap);
proto — не обязательный параметр, определяющий прототип прокси
callTrap — функция, которая будет замещать оригинальную функцию при прямом вызове прокси-функции (пример ниже все объяснит). Важно: this замещающей функции (callTrap) совпадает с this замещаемой.
constructTrap — не обязательный параметр — функция, которая будет заменять оригинальный конструктор функции при вызове через new. Важно: this замещающей функции (constructTrap) всегда undefined. Если constructTrap не передан, то используется callTrap в котором this делегирует от proxy.prototype (обычное поведение конструкторов ES5 Глава 13.2.2)
handler — объект, которые определяет поведение прокси. Этот объект должен всегда содержать Базовые ловушки (traps)
Базовые ловушки/перехватчики (Fundamental traps)
Как это читать:
имяПерехватчика: function(переменные, которые_передаются_перехватчику) -> {Тип возвращаемых данных} // Что заменяет
{
getOwnPropertyDescriptor: function(name) -> {PropertyDescriptor | undefined}
// Object.getOwnPropertyDescriptor(proxy, name)
getPropertyDescriptor: function(name) -> {PropertyDescriptor | undefined}
// Object.getPropertyDescriptor(proxy, name) (not in ES5)
getOwnPropertyNames: function() -> {String[]}
// Object.getOwnPropertyNames(proxy)
getPropertyNames: function() -> {String[]}
// Object.getPropertyNames(proxy) (not in ES5)
defineProperty: function(name, propertyDescriptor) -> {Mixed}
// Object.defineProperty(proxy,name,pd)
delete: function(name) -> {Boolean}
// delete proxy.name
fix: function() -> {{String:PropertyDescriptor}[]|undefined}
// Object.{freeze|seal|preventExtensions}(proxy)
}
Производные ловушки/перехватчики (Derived traps)
Эти перехватчики не обязательны, если они не будут определены, то будет использоваться логика по умолчанию.
{
has: function(name) -> {Boolean}
// if (name in proxy) ...
hasOwn: function(name) -> {Boolean}
// ({}).hasOwnProperty.call(proxy, name)
get: function(receiver, name) -> {Mixed}
// receiver.name;
set: function(receiver, name, val) -> {Boolean}
// receiver.name = val;
enumerate: function() -> {String[]}
// for (name in proxy) Возвращает массив имен своих и унаследованные перечисляемых объектов
keys: function() -> {String[]}
// Object.keys(proxy) Возвращает массив своих перечисляемых объектов
}
По умолчанию будет выполняться следующая логика
В виде таблицы можно посмотреть тут
Производными ловушки называются «производными» потому, что они могут быть определены базовыми ловушками. Например, ловушка has может быть определена, используя ловушку getPropertyDescriptor (проверить возвращает undefined или нет). С помощью производных перехватчиков можно с меньшими затратами эмулировать свойства, поэтому они и были определены. Ловушка fix была введена для того, чтобы позволить прокси взаимодействовать с
Object.preventExtensions
, Object.seal
и Object.freeze
. Нерасширеямый, запечатанный или замороженный объект (non-extensible, sealed, frozen) должен каким-либо образом ограничить свободу обработчика (handler) т.е. то, что он должен возвратить в будущих вызовах set, get
итп. Например, если предыдущий вызов handler.get(p, “foo”)
возвратил не undefined, то будущие вызовы handler.get(p, "foo")
обязаны возвращать такое же значение, если объект заморожен (frozen). Каждый раз, когда прокси пытаются заморозить, запечатать, заблокировать (non-extensible, sealed or frozen) вызывается ловушка "fix"
обработчика.Обработчик fix имеет 2 варианта:
1. отклонить запрос (fix должен возвратить undefined), тогда будет вызвано исключение TypeError.
2. выполнить запрос и возвратить описание объекта в виде
{String:PropertyDescriptor}[]
в этом случае «всеобъемлющий механизм» создаст новый объект, основываясь на описании объекта. В этот момент удаляются все ссылки на обработчик (он может быть удален сборщиком мусора). В этом случае прокси называется распущенным.Примеры
Hello, Proxy!
Следующий кусок кода создает прокси, который прерывает доступ к свойствам и возвращает для каждого свойства «p» значение «Hello, p»:
var p = Proxy.create({
get: function(proxy, name) {
return 'Hello, '+ name;
}
});
document.write(p.World); // Напечатает 'Hello, World'
Живой пример
Простой профайлер
Создадим простую обертку, которая считает сколько раз какое свойство было получено:
function makeSimpleProfiler(target) {
var forwarder = new ForwardingHandler(target);
var count = Object.create(null);
forwarder.get = function(rcvr, name) {
count[name] = (count[name] || 0) + 1;
return this.target[name];
};
return {
proxy: Proxy.create(forwarder,
Object.getPrototypeOf(target)),
get stats() { return count; }
};
}
Функция makeSimpleProfiler получает в качестве аргумента объект, который мы хотим мониторить. Она возвращает объект, имеющий 2 свойства: сам прокси и stats — количество вызовов.
Живой пример
Функция ForwardingHandler в строке два функции makeSimpleProfiler создает простой переадресующий проки, который прозрачно делегирует все операции, выполняемые над прокси, целевому объекту. Вот как она выглядит:
function ForwardingHandler(obj) {
this.target = obj;
}
ForwardingHandler.prototype = {
has: function(name) { return name in this.target; },
get: function(rcvr,name) { return this.target[name]; },
set: function(rcvr,name,val) { this.target[name]=val;return true; },
delete: function(name) { return delete this.target[name]; }
enumerate: function() {
var props = [];
for (name in this.target) { props.push(name); };
return props;
},
iterate: function() {
var props = this.enumerate(), i = 0;
return {
next: function() {
if (i === props.length) throw StopIteration;
return props[i++];
}
};
},
keys: function() { return Object.keys(this.target); },
...
};
Proxy.wrap = function(obj) {
return Proxy.create(new ForwardingHandler(obj),
Object.getPrototypeOf(obj));
}
С полной версией этой функции можно ознакомиться тут. Этот переаресующий обработчик (forwarding handler) скорее всего станет частью стандарта.
Удаленные объекты
Прокси позволяют вам создавать виртуальные объекты, которые могут эмулировать удаленные объекты или существующие объекты. Для демонстрации давайте создадим обертку вокруг существующей библиотеки для удаленной коммуникации в JavaScript. Библиотека web_send Tyler Close'а может быть использована для создания удаленных связей с объектами, расположенными на сервере. Теперь мы можем вызывать методы по HTTP POST запросам, используя эту удаленную связь. К сожалению, удаленные связи не могут быть использованы как объекты.
Сравним. Для вызова удаленной функции изначально нужно было вызывать:
Q.post(ref, 'foo', [a,b,c]);
Используя прокси мы можем сделать этот вызов более естественным, напишем нашу обертку:
function Obj(ref) {
return Proxy.create({
get: function(rcvr, name) {
return function() {
var args = Array.prototype.slice.call(arguments);
return Q.post(ref, name, args);
};
}
});
}
Теперь мы можем делать вот так
Obj(ref).foo(a,b,c)
.Эмуляция __noSuchMethod__
Используя Прокси возможно эмулировать хук __noSuchMethod__ в тех браузерах, которые его не поддерживают (но сейчас это не актуально).
function MyObject() {};
MyObject.prototype = Object.create(NoSuchMethodTrap);
MyObject.prototype.__noSuchMethod__ = function(methodName, args) {
return 'Hello, '+ methodName;
};
new MyObject().foo() // returns 'Hello, foo'
Этот объект использует NoSuchMethodTrap-прокси в котором ловушка get заменяет оригинальный __noSuchMethod__.
var NoSuchMethodTrap = Proxy.create({
get: function(rcvr, name) {
if (name === '__noSuchMethod__') {
throw new Error("receiver does not implement __noSuchMethod__ hook");
} else {
return function() {
var args = Array.prototype.slice.call(arguments);
return this.__noSuchMethod__(name, args);
}
}
}
});
Живой пример
Сообщения высшего порядка
Сообщения высшего порядка это сообщения, получающие другие сообщения в качестве аргумента, как описано тут.
Сообщения высшего порядка похожи на функции высшего порядка, но они более емкие в коде. Используя прокси очень просто создавать сообщения высшего порядка. Рассмотрим специальный объект "_", который переделывает сообщения в функции:
var msg = _.foo(1,2)
msg.selector; // "foo"
msg.args; // [1,2]
msg(x); // x.foo(1,2)
msg это функция, которая использует один аргумент, как если бы она была определена как
function(z) { return z.foo(1,2); }
. Следующий пример это прямая интерпретация СВП из вышеупомянутых документов, но написанная более емким кодом:var words = "higher order messages are fun and short".split(" ");
String.prototype.longerThan = function(i) { return this.length > i; };
// используем СВП для обработки сообщения как функции
document.write(words.filter(_.longerThan(4)).map(_.toUpperCase()));
// Без СВП этот код был бы таким:
// words.filter(function (s) { return s.longerThan(4) })
// .map(function (s) { return s.toUpperCase() })
Вот код объекта "_":
// превращает сообщение в функцию
var _ = Proxy.create({
get: function(_, name) {
return function() {
var args = Array.prototype.slice.call(arguments);
var f = function(rcvr) {
return rcvr[name].apply(rcvr, args);
};
f.selector = name;
f.args = args;
return f;
}
}
});
Живой пример
Эмуляция базовых объектов
Прокси дают Javascript программистам возможность эмулировать странности базовых объектов, таких как DOM. Это позволяет авторам библиотек оборачивать базовые объекты для того, чтобы их «приручить» (прим. Песочницы) или исправить их для сокращения кросс-браузерной несовместимости.
Прокси-функции
Предыдущие примеры использовали объекты. Вот простой пример прокси-функции:
var simpleHandler = {
get: function(proxy, name) {
// can intercept access to the 'prototype' of the function
if (name === 'prototype') return Object.prototype;
return 'Hello, '+ name;
}
};
var fproxy = Proxy.createFunction(
simpleHandler,
function() { return arguments[0]; }, // call trap
function() { return arguments[1]; }); // construct trap
fproxy(1,2); // 1
new fproxy(1,2); // 2
fproxy.prototype; // Object.prototype
fproxy.foo; // 'Hello, foo'
Живой пример
Прокси-функции открывают возможности для написания особых идиом, которые были нам не доступны в чистом JavaScript. В первую очередь функции-прокси могут создать из любого объекта функцию (callable object):
function makeCallable(target, call, construct) {
return Proxy.createFunction(
new ForwardingHandler(target),
call,
construct || call);
}
Второе, функции-прокси можно использовать для создания псевдо-классов, сущности которых функции(instances are callable).
function Thing() {
/* initialize state, etc */
return makeCallable(this, function() {
/* actions to perform when instance
is called like a function */
});
}
Эксперименты с новой семантикой
Для любителей языка: Прокси могут быть использованы для создания «стандартных» объектов и функций JavaScript в самом JavaScript. Возможность менять семантику объектов JavaScript внутри JavaScript значительно упрощает механизм внесения мелких изменений в семантику для проведения экспериментов. Частичную реализацию семантик внутри JavaScript можно наблюдать в значениях ловушек по умолчанию. Другой пример: Массивы JavaScript внутри JavaScript, используя Прокси.
Советы по использованию прокси
Избегайте рекурсий
Избегайте неявных вызовов toString внутри ловушек. Будьте аккуратны с аргументом receiver ловушек get и set. receiver представляет ссылку на прокси, поэтому неявно вызвав get и set приведет к бесконечной рекурси. Например вызов console.log(receiver) для дебага внутри сеттера вызовет метод toString, который приведет к бесконечной рекурсии.
get: function(receiver, name) {
print(receiver);
return target[name];
}
Если
p
это прокси, который использует ловушку выше, тогда вызов p.foo
приведет к бесконечному циклу: Сперва ловушка get
будет вызвана с name="foo"
, которая печатает receiver
(т.е. p
). Это приводит к вызову p.toString()
, который приведет к вызову ловушки ещё раз в этот раз с name="toString"
. И так далее.Прокси как обработчики
Прокси обработчики сами могут быть прокси. Прокси трейсер ниже доменстрирует этот паттерн, он используется для создания «туннелирования» всех операций для обработчику через одну ловушку get.
Прокси трейсер/Прокси зонд
Трейсер просто печатает описание всех операций, который он обрабатывал. Это очень полезно для дебага или для изучения работы прокси.
Живой пример
Зонд имеет схожую с трейсером логику он логирует все мета-уровневые операции, примененные к нему.
Живой пример
Когда это можно будет использовать?
Сейчас только Firefox 4.0 поддерживает прокси. Есть реализация прокси для Node.js в виде расширения: node-overload (частичная поддержка) node-proxy (практически полная поддержка). В любом случае Прокси будут внесены в стандарт так, что скоро он появится и в вашем браузере!
Дополнительные ресурсы
1. ECMAScript Harmony
2. Документация на The Mozilla Developer Network
3. Разработка стандарта: первая часть этого Google Tech Talk и вот эта бумага, показанная на DLS 2010.
4. Brendan Eich, в своем блоге кратко объясняет основы Прокси.
5. Частичный список открытых проблем Прокси в Firefox 4.
6. Слайды Brendan Eich из его оклада на jsconf
Использованные в статье ресурсы
1. MDC Proxy (DRAFT)
2. ES5 Catch-all Proxies
3. Proxy Inception (Brendan Eich)
4. Tutorial: Harmony Proxies (Tom Van Cutsem)
Если вам что-то не понятно, пожалуйста, задавайте свои вопросы или посмотрите слайды. Предложения, пожелания, критика приветствуется!