CSS-live.ru

ES6: ещё о ловушках прокси изнутри

Перевод статьи More ES6 Proxy Traps in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.

Привет! Это  «ES6 — Ловушки? Снова? — изнутри». Интересуетесь и другими вкусняшками ES6? Тогда загляните в краткую историю инструментария ES6. Затем изучите деструктирование,литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, let, const и«Временную мёртвую зону», а также итераторы, генераторы, символы и объекты Map, WeakMaps, Sets и WeakSets,  прокси и ловушки прокси. Ну а сегодня поговорим об остальных ловушках прокси в ES6.

Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.

Заметьте, что Proxy труднее пощупать, поскольку в Babel он заработает лишь при условии, что его поддерживает браузер, в котором его запускают. Можно ознакомиться с таблицей совместимости ES6 для поддерживающих браузеров. Чтобы опробовать Proxy сегодня, воспользуйтесь Microsoft Edge или Mozilla Firefox. Что до меня, то я проверяю свои примеры в Firefox.

Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.

Спасибо, что выслушали, а теперь перейдём к остальным ловушкам Proxy! Если не понимаете, о чём речь, советую прочитать предыдущие статьи про встроенный объект Proxy и про первые несколько ловушек прокси, о которых я рассказывал вчера.

Постойте! Есть ещё… (Обработчики ловушек прокси)

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

  • construct – ловушка для использования оператора new
  • getPrototypeOf – ловушка для внутренних вызовов [[GetPrototypeOf]]
  • setPrototypeOf – ловушка для вызовов Object.setPrototypeOf
  • isExtensible – ловушка для вызовов Object.isExtensible
  • preventExtensions – ловушка для вызовов Object.preventExtensions
  • getOwnPropertyDescriptor – ловушка для вызовов Object.getOwnPropertyDescriptor

construct

Метод handler.construct можно использовать, чтобы поймать использование оператора new. Вот быстрая «реализация по умолчанию», нисколько не меняющая поведение new. Не забыли, что у нас есть новый приятель — оператор расширения?

var handler = {
  construct (target, args) {
    return new target(...args)
  }
}

Если написать handler, как выше, то поведение new, к которому вы уже привыкли, не изменится. Это здорово, поскольку что бы вы ни пытались получить, всегда можно вернуться к поведению по умолчанию — что всегда важно.

function target (a, b, c) {
  this.a = a
  this.b = b
  this.c = c
}
var proxy = new Proxy(target, handler)
console.log(new proxy(1,2,3))
// <- { a: 1, b: 2, c: 3 }

Очевидные применения для ловушек construct: подготовка данных в аргументах, действия, обязательные при каждом вызове new proxy(), регистрация и отслеживание создания объекта и замена реализаций целиком. Представьте себе прокси в тех ситуациях, где есть разные «ветки» наследования.

class Automobile {}
class Car extends Automobile {}
class SurveillanceVan extends Automobile {}
class SUV extends Automobile {}
class SportsCar extends Car {}
function target () {}
var handler = {
  construct (target, args) {
    var [status] = args
    if (status === 'nsa') {
      return new SurveillanceVan(...args)
    }
    if (status === 'single') {
      return new SportsCar(...args)
    }
    return new SUV(...args) // семья
  }
}

Конечно, само ветвление можно сделать и обычными методами, но оператор new также разумно использовать, если в конечном итоге вы всё равно создаёте новый объект во всех ветках кода.

console.log(new proxy('nsa').constructor.name)
// <- `SurveillanceVan`

Но всё же чаще всего ловушки construct понадобятся для чего-то попроще: для расширения объекта target после создания — и прежде любых других действий — так, чтобы он поддерживал прокси-фильтр. Возможно, к объекту target придётся добавить флаг proxied или нечто похожее.

getPrototypeOf

Вот для чего может пригодиться метод handler.getPrototypeOf в качестве ловушки:

Эту ловушку можно использовать, чтобы заставить объект притворяться, что он Array при обращении через прокси. Но учитывайте, что в этом случае сам proxy не становится настоящим Array

var handler = {
  getPrototypeOf: target => Array.prototype
}
var target = {}
var proxy = new Proxy(target, handler)
console.log(proxy instanceof Array)
// <- true
console.log(proxy.push)
// <- undefined

Конечно, можно продолжать патчить прокси до нужного результата. В этом случае можно воспользоваться ловушкой get, чтобы «смешать» Array.prototype с фактическим конечным target. Всякий раз, когда свойство в target не найдено, нам потребуются reflection-методы, чтобы найти его в Array.prototype. Оказывается этого вполне достаточно для большинства операций.

var handler = {
  getPrototypeOf: target => Array.prototype,
  get (target, key) {
    return Reflect.get(target, key) || Reflect.get(Array.prototype, key)
  }
}
var target = {}
var proxy = new Proxy(target, handler)
console.log(proxy.push)
// <- function push () { [нативный код] }
proxy.push('a', 'b')
console.log(proxy)
// <- { 0: 'a', 1: 'b', длина: 2 }

Я определённо вижу продвинутые применения для ловушек getPrototypeOf в будущем, но пока слишком рано говорить, какие паттерны могут из этого выйти.

setPrototypeOf

Название метода Object.setPrototypeOf говорит само за себя: он присваивает прототипу объекта ссылку на другой объект. Считается правильным устанавливать прототипы именно так, а не использовать __proto__, который уже устарел — что теперь закреплено и в стандарте.

Метод handler.setPrototypeOf можно использовать, чтобы установить ловушку для Object.setPrototypeOf. Код ниже не меняет поведению по умолчанию изменения прототипа на значение proto.

var handler = {
  setPrototypeOf (target, proto) {
    Object.setPrototypeOf(target, proto)
  }
}
var proto = {}
var target = function () {}
var proxy = new Proxy(target, handler)
proxy.setPrototypeOf(proxy, proto)
console.log(proxy.prototype === proto)
// <- true

Поле действия для setPrototypeOf широкое. Можно просто не вызывать Object.setPrototypeOf и ловушка превратила бы вызов в холостой. А можно выбросить исключение, делая отказ более явным — например, если полагаете, что новый прототип будет недопустимым, или не хотите, чтобы клиенты неожиданно подложили вам свинью.

Эта ловушка полезна, если нужно ограничить прокси возможности что-то делать с объектом target. Уж я бы точно подстраховал на всякий случай прокси, отдаваемый стороннему коду, ловушкой наподобие такой:

var handler = {
  setPrototypeOf (target, proto) {
    throw new Error('Изменение прототипа запрещено')
  }
}
var proto = {}
var target = function () {}
var proxy = new Proxy(target, handler)
proxy.setPrototypeOf(proxy, proto)
// <- Ошибка: Изменение прототипа запрещено

С другой стороны, при желании запутать клиентов, можно вообще превратить вызов в холостой — и это, возможно, заставит их уйти.

isExtensible

Метод handler.isExtensible в основном нужен для регистрации или проверки вызовов Object.isExtensible. На эту ловушку наложено суровое непременное условие, жестко ограничивающее возможности что-то с ней делать.

Если Object.isExtensible(proxy) !== Object.isExtensible(target), то будет выброшен TypeError.

Если не хотите сообщать клиенту, расширяемый ли оригинальный объект или нет, можно выбросить ошибку в ловушке handler.isExtensible, но кажется, есть редкие случаи, когда такой кошмарный ужас оправдан. Для наглядности, ловушка isExtensible в коде ниже иногда выбрасывает ошибки, но в остальных случаях ведёт себя как ожидалось.

var handler = {
  isExtensible (target) {
    if (Math.random() > 0.1) {
      throw new Error('вам придётся полюбить случайные непонятные ошибки!')
    }
    return Object.isExtensible(target)
  }
}
var target = {}
var proxy = new Proxy(target, handler)
console.log(Object.isExtensible(proxy))
// <- true
console.log(Object.isExtensible(proxy))
// <- true
console.log(Object.isExtensible(proxy))
// <- true
console.log(Object.isExtensible(proxy))
// <- Ошибка: вам придётся полюбить случайные непонятные ошибки!

Хотя от этой ловушки мало толку, кроме как для проверки и перестраховки, в надёжном ограничении есть смысл, поскольку есть ещё ловушка preventExtensions. Она немного полезнее!

preventExtensions

Можно использовать handler.preventExtensions, чтобы поймать метод Object.preventExtensions. Если запретить расширения в объекте, то будет нельзя добавлять новые свойства — объект не сможет расширяться.

Представьте сценарий, в которых нужна возможность запретить расширение с помощью preventExtensions на некоторых объектах — но не на всех. В таком сценарии можно использовать WeakSet, чтобы следить за объектами, предназначенными для расширения. Если объект есть в наборе, тогда ловушка должна уметь захватывать эти запросы и аннулировать их. В коде ниже именно это и происходит. Заметьте, что ловушка всегда возвращает противоположность Object.isExtensible(target), поскольку должна сообщить, сделан ли объект расширяемым или нет.

var mustExtend = new WeakSet()
var handler = {
  preventExtensions (target) {
    if (!mustExtend.has(target)) {
      Object.preventExtensions(target)
    }
    return !Object.isExtensible(target)
  }
}

Теперь, когда handler и WeakSet у нас готовы, пора создать конечный объект, прокси, добавив конечный в наш набор. Затем воспользуемся Object.preventExtensions и заметим, что он не отменяет расширения в объекте. Так задумано, поскольку target можно найти в _наборе_ mustExtend.

var target = {}
var proxy = new Proxy(target, handler)
mustExtend.add(target)
Object.preventExtensions(proxy)
// <- TypeError: обработчик preventExtensions прокси вернул false

Если удалить target из набора mustExtend до вызова Object.preventExtensions, то target будет сделан нерасширяемым, как и планировалось.

var target = {}
var proxy = new Proxy(target, handler)
mustExtend.add(target)
mustExtend.delete(target)
Object.preventExtensions(proxy)
console.log(Object.isExtensible(proxy))
// <- false

Конечно, можно воспользоваться этим различием и не дать клиентам прокси сделать его нерасширяемым в случаях, где это может привести к нежелательному поведению. Хотя в большинстве случаях, эта ловушка вам вряд ли пригодится. Потому что обычно вы работаете с конечным target, а не с самим объектом прокси.

getOwnPropertyDescriptor

Метод handler.getOwnPropertyDescriptor можно использовать в качестве ловушки для Object.getOwnPropertyDescriptor. Он может вернуть дескриптор свойства, например, результат Object.getOwnPropertyDescriptor(target, key); или undefined, сообщая, что свойство не существует. И как обычно, есть также третий вариант: выбросить исключение, полностью прервав операцию.

Возвращаясь к нашему стандартному примеру с «приватным пространством свойств», можно реализовать ловушку ниже, чтобы не дать клиентам узнать о дескрипторах свойств приватных свойств.

var handler = {
  getOwnPropertyDescriptor (target, key) {
    invariant(key, 'получить дескриптор свойства для')
    return Object.getOwnPropertyDescriptor(target, key)
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Недопустимое обращение к ${action} приватного свойства "${key}"`)
  }
}
var target = {}
var proxy = new Proxy(target, handler)
Object.getOwnPropertyDescriptor(proxy, '_foo')
// <- Ошибка: недопустимая попытка получить дескриптор свойства для приватного свойства "_foo"

Проблема такого подхода в том, что вы фактически сообщаете клиентам, что свойства с префиксом _ так или иначе запрещены. Возможно, было бы правильнее скрыть их полностью, вернув undefined. Таким образом, поведение приватных свойств будет таким же, как и у свойств, которые на самом деле отсутствуют в объекте target.

var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return
    }
    return Object.getOwnPropertyDescriptor(target, key)
  }
}
var target = { _foo: 'bar', baz: 'tar' }
var proxy = new Proxy(target, handler)
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'))
// <- undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'))
// <- undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'))
// <- { value: 'tar', writable: true, enumerable: true, configurable: true }

Обычно, при попытке скрыть что-то, лучше заставить его вести себя так, как будто это что-то из совсем другой категории. Но если выскочит ошибка, это послужит сообщением, что «здесь есть что-то сомнительное, но мы не знаем, что именно…» — и в итоге клиент выяснит, почему это так.

Учтите, что если отладочные соображения перевешивают соображения безопасности, вероятно, лучше выбрать оператор throw.

Заключение

Это определённо было весело. Теперь я гораздо лучше понимаю прокси, и считаю, что они моментально станут популярными, как только ES6 начнёт набирать обороты. И я безумно рад, что скоро они будут поддерживаться в большинстве браузеров. Но я бы не стал сильно надеяться на прокси для Babel, поскольку многие из ловушек до нелепого трудно (а то и вообще невозможно) реализовать в ES5.

Насколько мы поняли за последние дни, для прокси есть множество применений. Навскидку:

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

Не сомневаюсь, что для прокси есть ещё сотни применений. И уверен, что многие библиотеки примут паттерн, который мы обсуждали в этой серии, когда «конечный» объект target создаётся и используется с целью хранения, но клиенту предоставляется только «передний край» в виде объекта прокси с ограниченным и проверяемым взаимодействием с конечным.

А для чего бы использовали Proxy вы?

Встретимся завтра… скажем, в это же время? Можно будет поговорить про встроенный объект Reflect.

P.S. Это тоже может быть интересно:

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.