Pull to refresh

ES5 Harmony Proxy — меняем семантику JavaScript внутри самого JavaScript

Reading time 10 min
Views 11K
Прокси — это новые объекты 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)

Как с ними работать — 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.

image

Прокси трейсер/Прокси зонд

Трейсер просто печатает описание всех операций, который он обрабатывал. Это очень полезно для дебага или для изучения работы прокси.
Живой пример

Зонд имеет схожую с трейсером логику он логирует все мета-уровневые операции, примененные к нему.
Живой пример

Когда это можно будет использовать?


Сейчас только 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)

Если вам что-то не понятно, пожалуйста, задавайте свои вопросы или посмотрите слайды. Предложения, пожелания, критика приветствуется!
Tags:
Hubs:
+44
Comments 39
Comments Comments 39

Articles