Мета-программирование
С приходом ECMAScript 2015, в JavaScript введены объекты Proxy и Reflect, позволяющие перехватить и переопределить поведение фундаментальных процессов языка (таких как поиск свойств, присвоение, итерирование, вызов функций и так далее). С помощью этих двух объектов вы можете программировать на мета уровне JavaScript.
Объекты Proxy
Введённый в ECMAScript 6, объект Proxy позволяет перехватить и определить пользовательское поведение для определённых операций. Например, получение свойства объекта:
var handler = {
get: function (target, name) {
return name in target ? target[name] : 42;
},
};
var p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, 42
Объект Proxy определяет target (в данном случае новый пустой объект) и handler - объект в котором реализована особая функция-ловушка get. "Проксированный" таким образом объект, при доступе к его несуществующему свойству вернёт не undefined, а числовое значение 42.
Дополнительные примеры доступны в справочнике Proxy.
Терминология
В разговоре о функциях объекта Proxy применимы следующие термины:
- handler (обработчик)
-
Объект - обёртка, содержащий в себе функции-ловушки.
- ловушки (traps)
-
Методы, реализующие доступ к свойствам. В своей концепции они аналогичны методам перехвата(hooking) в операционных системах.
- цель (target)
-
Объект, который оборачивается в Proxy. Часто используется лишь как внутреннее хранилище для Proxy объекта. Проверка на нарушение ограничений (invariants), связанных с нерасширяемостью объекта или неконфигурируемыми свойствами объекта производится для конкретной цели.
- неизменяемые ограничения (дословно Invariants - те что остаются неизменными)
-
Некоторые особенности поведения объекта, которые должны быть сохранены при реализации пользовательского поведения названы invariants. Если в обработчике нарушены такие ограничения, будет выброшена ошибка
TypeError.
Обработчики и ловушки
В следующей таблице перечислены ловушки, доступные для использования в объекте Proxy. Смотрите подробные объяснения и примеры в документации.
| Обработчик / ловушка | Перехватываемые методы | Неизменяемые ограничения |
|---|---|---|
handler.getPrototypeOf()
|
Object.getPrototypeOf()Reflect.getPrototypeOf()__proto__Object.prototype.isPrototypeOf()instanceof
|
|
handler.setPrototypeOf()
|
Object.setPrototypeOf()Reflect.setPrototypeOf()
|
если целевой объект target нерасширяем, значение параметра
prototype должно быть равным значению возвращаемому методом
Object.getPrototypeOf(target).
|
handler.isExtensible()
|
Object.isExtensible()Reflect.isExtensible()
|
Object.isExtensible(proxy) должно возвращать тоже значение,
что и Object.isExtensible(target).
|
handler.preventExtensions()
|
Object.preventExtensions()Reflect.preventExtensions()
|
Object.preventExtensions(proxy) возвращает
true только в том случае, если
Object.isExtensible(proxy) равно false.
|
handler.getOwnPropertyDescriptor()
|
Object.getOwnPropertyDescriptor()Reflect.getOwnPropertyDescriptor()
|
|
handler.defineProperty()
|
Object.defineProperty()Reflect.defineProperty()
|
|
handler.has()
|
Property query: foo in proxyInherited property query: foo in Object.create(proxy)Reflect.has()
|
|
handler.get()
|
Property access: proxy[foo]and proxy.barInherited property access: Object.create(proxy)[foo]Reflect.get()
|
|
handler.set()
|
Property assignment: proxy[foo] = bar and
proxy.foo = barInherited property assignment: Object.create(proxy)[foo] = barReflect.set()
|
|
handler.deleteProperty()
|
Property deletion: delete proxy[foo] and
delete proxy.fooReflect.deleteProperty()
|
Свойство не может быть удалено, если оно существует в целевом объекте как собственное, неконфигурируемое свойство. |
handler.enumerate()
|
Property enumeration / for...in:
for (var name in proxy) {...}Reflect.enumerate()
|
Метод enumerate
должен возвращать объект.
|
handler.ownKeys()
|
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()Reflect.ownKeys()
|
|
handler.apply()
|
proxy(..args)Function.prototype.apply() and
Function.prototype.call()Reflect.apply()
|
Ограничений нет. |
handler.construct()
|
new proxy(...args)Reflect.construct()
|
Обработчик должен возвращать Object. |
Отзываемый Proxy
Метод Proxy.revocable() создаёт отзываемый объект Proxy. Такой прокси объект может быть отозван функцией revoke, которая отключает все ловушки-обработчики. После этого любые операции над прокси объектом вызовут ошибку TypeError.
var revocable = Proxy.revocable(
{},
{
get: function (target, name) {
return "[[" + name + "]]";
},
},
);
var proxy = revocable.proxy;
console.log(proxy.foo); // "[[foo]]"
revocable.revoke();
console.log(proxy.foo); // ошибка TypeError
proxy.foo = 1; // снова ошибка TypeError
delete proxy.foo; // опять TypeError
typeof proxy; // "object", для метода typeof нет ловушек
Рефлексия
Reflect это встроенный объект, предоставляющий методы для перехватываемых операций JavaScript. Это те же самые методы, что имеются в обработчиках Proxy. Объект Reflect не является функцией.
Reflect помогает при пересылке стандартных операций из обработчика к целевому объекту.
Например, метод Reflect.has() это тот же оператор in но в виде функции:
Reflect.has(Object, "assign"); // true
Улучшенная функция apply
В ES5 обычно используется метод Function.prototype.apply() для вызова функции в определённом контексте (с определённым this) и с параметрами, заданными в виде массива (или массива-подобного объекта).
Function.prototype.apply.call(Math.floor, undefined, [1.75]);
С методом Reflect.apply эта операция менее громоздка и более понятна:
Reflect.apply(Math.floor, undefined, [1.75]);
// 1;
Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]);
// "hello"
Reflect.apply(RegExp.prototype.exec, /ab/, ["confabulation"]).index;
// 4
Reflect.apply("".charAt, "ponies", [3]);
// "i"
Проверка успешности определения нового свойства
Метод Object.defineProperty, в случае успеха операции, возвращает объект, а при неудаче вызывает ошибку TypeError. Из-за этого определение свойств требует обработки блоком try...catch для перехвата возможных ошибок. Метод Reflect.defineProperty, в свою очередь, возвращает успешность операции в виде булева значения, благодаря чему возможно использование простого if...else условия:
if (Reflect.defineProperty(target, property, attributes)) {
// успех
} else {
// что-то пошло не так
}