ES6: ловушки прокси изнутри

Перевод статьи 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, чтобы познакомиться с темой.

Обработчики ловушек прокси

Особенно интересно в прокси то, как именно их можно использовать для перехвата практически любых взаимодействий с объектом target, а не только операций get или set. Ниже представлен полный список тех ловушек, которые можно настроить.

  • has – ловушка для оператора in
  • deleteProperty – ловушка для оператора delete
  • defineProperty – ловушка для Object.defineProperty и декларативных альтернатив
  • enumerate – ловушка для циклов for..in
  • ownKeys – ловушка для Object.keys и связанных с ним методов
  • apply – ловушка для вызовов функции

Мы пропустим get и set, так как уже рассматривали их вчера; и также здесь не хватает ещё нескольких ловушек, которые мы обсудим в завтрашней статье. Оставайтесь на связи!

has

Чтобы «спрятать» любое свойство, можно использовать handler.has. Это ловушка для оператора in. В примере с ловушкой set мы не позволили менять и вообще обращаться к свойствам с префиксом _, но посторонние скрипты могли по-прежнему «прозвонить» наш прокси, чтобы выяснить, есть там эти свойства или нет. Как в сказке «Три медведя», у нас есть три варианта:

  • Можно позволить key in proxy провалиться до key in target
  • Можно вернуть false (или true) – независимо от того, есть key или нет.
  • Можно выбросить ошибку и изначально считать вопрос недопустимым.

Последний вариант крайне суров, и я думаю, что в некоторых ситуациях он оправдан – но нужно будет осознавать, что свойство (или «пространство свойств») на самом деле является защищенным. Зачастую выгоднее просто тихо указать, что свойства в объекте нет (in дает false). А вариант с «проваливанием», когда вы просто возвращаете результат выражения key in target, обычно хорош в качестве варианта по умолчанию.

В нашем примере мы, наверное, хотим вернуть false для свойств в «пространстве свойств» с префиксом в виде подчёркивания и по умолчанию key in target для всех остальных свойств. Это надежно скроет недоступные свойства от нежелательных посетителей.

var handler = {
  get (target, key) {
    invariant(key, 'прочитать')
    return target[key]
  },
  set (target, key, value) {
    invariant(key, 'установить')
    return true
  },
  has (target, key) {
    if (key[0] === '_') {
      return false
    }
    return key in target
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Недопустимая попытка ${action} приватное свойство "${key}"`)
  }
}

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

var target = { _prop: 'foo', pony: 'foo' }
var proxy = new Proxy(target, handler)
console.log('pony' in proxy)
// <- true
console.log('_prop' in proxy)
// <- false
console.log('_prop' in target)
// <- true

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

Это и правда зависит от задачи!

deleteproperty

Я пользуюсь оператором delete довольно часто. Установка свойству undefined очищает его значение, но это свойство по-прежнему остаётся частью объекта. Использование оператора delete в свойстве с кодом вроде delete foo.bar означает, что свойство bar будет навсегда удалено из объекта foo.

var foo = { bar: 'baz' }
foo.bar = 'baz'
console.log('bar' in foo)
// <- true
delete foo.bar
console.log('bar' in foo)
// <- false

Помните наш пример с ловушкой set, где мы предотвратили доступ к свойствам с префиксом «_»? В этом коде была проблема. Даже если вы не могли изменить значение _prop, вы могли удалить свойство целиком с помощью оператора delete. Даже через объект прокси!

var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
console.log('_prop' in proxy)
// <- true
delete proxy._prop
console.log('_prop' in proxy)
// <- false

Можно использовать handler.deleteProperty чтобы не дать операции delete сработать. Как и с ловушками get и set, выбрасывания в ловушке deleteProperty вполне хватит для предотвращения удаления свойства.

var handler = {
  get (target, key) {
    invariant(key, 'прочитать')
    return target[key]
  },
  set (target, key, value) {
    invariant(key, 'установить')
    return true
  },
  deleteProperty (target, key) {
    invariant(key, 'удалить')
    return true
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Недопустимая попытка ${action} приватное свойство "${key}"`)
  }
}

Если запустить тот же кусок кода, что и раньше, то при попытке удалить _prop из прокси мы столкнёмся с исключением.

var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
console.log('_prop' in proxy)
// <- true
delete proxy._prop
// <- Error: недопустимая попытка удалить приватное свойство "_prop"

И клиенты, взаимодействующие с target через proxy, больше не смогут ничего удалить в нашем пространстве приватных свойств с «_»

defineProperty

Обычно мы используем Object.defineProperty(obj, key, descriptor) в двух случаях.

  1. Когда нужна кроссбраузерная поддержка геттеров и сеттеров.
  2. Каждый раз, когда хотим определить метода доступа для пользовательского свойства.

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

  • configurable: false отключает большинство изменений в дескрипторе свойства и делает свойство неудаляемым
  • enumerable: false скрывает свойство от циклов for..in и Object.keys
  • value: undefined — начальное значение для свойства
  • writable: false делает значение свойства неизменяемым
  • get: undefined — этот метод ведёт себя, как геттер для свойства
  • set: undefined — этот метод получает новое значение и обновляет значение свойства.

Заметьте, что при определении свойства придётся выбрать между использованием value и writable или get и set. Выбрав первое, вы настраиваете дескриптор данных — такое получается при объявлении свойств вроде foo.bar = 'baz', у него есть значение value и он бывает перезаписываемым (writeable). При выборе последнего вы создаёте дескриптор метода доступа, полностью определяемый с помощью методов, которые можно использовать, чтобы прочитать (get()) или задать (set()) значение свойства.

В примере кода ниже показано, как дескрипторы свойства полностью различаются в зависимости от того, выбрали ли вы декларативный вариант или программный API.

var target = {}
target.foo = 'bar'
console.log(Object.getOwnPropertyDescriptor(target, 'foo'))
// <- { value: 'bar', writable: true, enumerable: true, configurable: true }
Object.defineProperty(target, 'baz', { value: 'ponyfoo' })
console.log(Object.getOwnPropertyDescriptor(target, 'baz'))
// <- { value: 'ponyfoo', writable: false, enumerable: false, configurable: false }

Теперь, когда мы немного рассмотрели Object.defineProperty, настало время ловушки.

Ловушка

Ловушка handler.defineProperty нужна для перехвата вызовов Object.defineProperty. Используются key и descriptor. Пример ниже полностью запрещает добавлять свойства через прокси. Ну не здорово ли, что это перехватывает и декларативный вариант объявления свойства foo.bar = 'baz'? Еще как здорово!

var handler = {
  defineProperty (target, key, descriptor) {
    return false
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.foo = 'bar'
// <- TypeError: обработчик defineProperty прокси вернул false для свойства '"foo"'

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

var handler = {
  defineProperty (target, key, descriptor) {
    invariant(key, 'определить')
    return true
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Недопустимая попытка ${action} приватное свойство "${key}"`)
  }
}

Затем можно проверить его на объекте target, установка свойства с префиксом _ теперь выбросит ошибку. Можно сделать, чтобы это тихо проигнорировалось, вернув falseзависит от вашей задачи.

var target = {}
var proxy = new Proxy(target, handler)
proxy._foo = 'bar'
// <- Error: недопустимая попытка определить свойство "_foo"

Теперь прокси надёжно скрывает приватные свойства с префиксом _ за ловушкой, которая не дает установить их ни через proxy[key] = value, ни через Object.defineProperty(proxy, key, { value })потрясающе!

Метод enumerate

Метод handler.enumerate можно использовть для перехвата оператора for..in. C has можно было не дать key in proxy вернуть true для всех наших приватных свойств с подчеркиванием, но что насчёт цикла for..in? Даже при том, что ловушка has скрывает свойство от проверки key in proxy, клиент случайно наткнётся на свойство при использовании цикла for..in!

var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false
    }
    return key in target
  }
}
var target = { _prop: 'foo' }
var proxy = new Proxy(target, handler)
for (let key in proxy) {
  console.log(key)
  // <- '_prop'
}

С помощью ловушки enumerate можно вернуть итератор, который заменит перечисляемые свойства, найденные в прокси в ходе цикла for..in. Полученный итератор должен соответствовать протоколу «Итератор», например, итераторы, полученные из любого метода Symbol.iterator. Вот возможная реализация такого прокси, который возвращает вывод Object.keys за вычетом свойств из приватной области.

var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false
    }
    return key in target
  },
  enumerate (target) {
    return Object.keys(target).filter(key => key[0] !== '_')[Symbol.iterator]()
  }
}
var target = { pony: 'foo', _bar: 'baz', _prop: 'foo' }
var proxy = new Proxy(target, handler)
for (let key in proxy) {
  console.log(key)
  // <- 'pony'
}

Теперь даже пытливый for...in ни за что не увидит наших приватных свойств!

ownKeys

С помощью метода handler.ownKeys можно вернуть массив Array со свойствами, который станет результатом для Reflect.ownKeys()он должен включать все свойства target (перечисляемые или нет, а также символы). Реализация по умолчанию, как показано ниже, может просто вызвать метод Reflect.ownKeys у проксированного объекта target. Не волнуйтесь, через пару статей мы доберемся до встроенного объекта Reflect.

var handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target)
  }
}

В этом случае перехват не повлиял бы на вывод Object.keys.

var target = {
  _bar: 'foo',
  _prop: 'bar',
  [Symbol('secret')]: 'baz',
  pony: 'ponyfoo'
}
var proxy = new Proxy(target, handler)
for (let key of Object.keys(proxy)) {
  console.log(key)
  // <- '_bar'
  // <- '_prop'
  // <- 'pony'
}

Заметьте, что перехватчик ownKeys используется во время всех следующих операций.

  • Object.getOwnPropertyNames() — возвращает только не-символьные свойства
  • Object.getOwnPropertySymbols() – возвращает только символьные свойства
  • Object.keys() – возвращает только не-символьные перечисляемые свойства
  • Reflect.ownKeys() — мы вернёмся к Reflect через пару статей!

В случае, когда нужно отключить доступ к пространству свойств с префиксом _, можно взять вывод Reflect.ownKeys(target) и отфильтровать его.

var handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_')
  }
}

Если теперь воспользоваться обработчиком в коде выше, чтобы вытащить ключи объекта, мы обнаружим лишь свойства в публичном пространстве, а не в пространстве с префиксом _.

var target = {
  _bar: 'foo',
  _prop: 'bar',
  [Symbol('secret')]: 'baz',
  pony: 'ponyfoo'
}
var proxy = new Proxy(target, handler)
for (let key of Object.keys(proxy)) {
  console.log(key)
  // <- 'pony'
}

Это не повлияло бы на перебор Symbol, поскольку sym[0] возвращает undefinedи во всяком случае уж точно не '_'..

var target = {
  _bar: 'foo',
  _prop: 'bar',
  [Symbol('secret')]: 'baz',
  pony: 'ponyfoo'
}
var proxy = new Proxy(target, handler)
for (let key of Object.getOwnPropertySymbols(proxy)) {
  console.log(key)
  // <- Symbol(secret)
}

Нам удалось скрыть свойства с префиксом _ от перечисления ключей, оставив символы и другие свойства без изменений.

apply

Метод handler.apply весьма интересен. Его можно использовать в качестве ловушки для любого вызова proxy. Каждый пример в коде ниже пройдёт через ловушку apply для вашего прокси.

proxy(1, 2)
proxy(...args)
proxy.call(null, 1, 2)
proxy.apply(null, [1, 2])

Метод apply принимает три аргумента.

  • target — функция, скрываемая за прокси
  • ctx — контекст, переданный как this в target при применении вызова
  • args — аргументы, переданные как this в target при применении вызова

Можно попытаться реализовать это «в лоб» в виде target.apply(ctx, args), но ниже мы будем использовать Reflect.apply(...arguments). Мы познакомимся со встроенным объектом Reflect через пару статей. А пока думайте о них, как об эквивалентных, и учитывайте, что значение, которое вернула ловушка apply, также будет использоваться в качестве результата вызова функции через proxy.

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments)
  }
}

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

В примере ниже проксируется метод sum с помощью ловушки twice, удваивающей результат sum, не меняя ничего в коде, разве что вместо вызова метода sum напрямую теперь используется proxy.

var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2
  }
}
function sum (left, right) {
  return left + right
}
var proxy = new Proxy(sum, twice)
console.log(proxy(1, 2))
// <- 6
console.log(proxy(...[3, 4]))
// <- 14
console.log(proxy.call(null, 5, 6))
// <- 22
console.log(proxy.apply(null, [7, 8]))
// <- 30

Конечно, ловушка apply также поймает и вызов Reflect.apply для прокси.

Reflect.apply(proxy, null, [9, 10])
// <- 38

А где ещё может пригодиться handler.apply?

Завтра я опубликую последнюю статью про Proxyобещаю!. В ней я расскажу про оставшиеся ловушечные обработчики, к примеру, construct и getPrototypeOf. Чтобы не пропустить их, подписывайтесь ниже.

Прим. перев.: со времени публикации метод handler.enumerate устарел, см. подробности на MDN.

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

2 Комментарии

  1. Олег Торбасов

    handler.enumerate() больше нет, его убрали.

    1. Максим Усачев (Автор записи)

      Спасибо за замечание. Дописал об этом в конце статьи!

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

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

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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