Практическое руководство по Javascript Proxy

Spread the love

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

Перевод оригинальной статьи: Thomas Barrasso A practical guide to Javascript Proxy

Введение в Proxy

Хотя это новое свойство не так хорошо известно, как другие функции ES2015, у Proxy есть множество применений, включая перегрузку операторов (operator overloading), имитация объектов (object mocking), создание гибкого API, Object on-change и даже вся мощь внутренней системы реактивности в Vue.js 3.

Объект Proxy используется для создания пользовательского поведения при использование основных операций (например, поиск свойств, присваивание, перечисление, вызов функции и т. д.). – MDN

Proxy-объект – это объект-заполнитель, который «захватывает» вызовы и операции, выполняемые с его целевым объектом (target object). Он создает необнаружимый барьер вокруг целевого объекта, который перенаправляет все операции на объект-обработчик (handler object).

Proxy создается с использованием конструктора new Proxy, который принимает два обязательных аргумента: целевой объект (target object) и объект-обработчик (handler object).

Простейший пример работающего Прокси-сервера – с одним trap (ловушкой), в данном случае trap это get, который всегда возвращает «42».

let target = {
  x: 10,
  y: 20
}

let handler = {
  get: (obj, prop) => 42
}

target = new Proxy(target, handler)

target.x // 42
target.y // 42
target.x // 42

В результате получается объект, который всегда вернет «42» при любой операции доступа к свойству. Это означает target.x, target [‘x’], Reflect.get (target, ‘x’) и т. д.

Однако трапы Proxy, безусловно, не ограничиваются операциями get. Это всего лишь одна из более чем десятка различных трапов:

Примеры использования прокси

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

Source: Proxy Design Pattern

Default/ “Zero Values”

В GoLang есть концепция нулевых значений, которые являются специфическими для типов, по умолчанию заданными неявными значениями. Идея состоит в том, чтобы предоставить безопасные по умолчанию значения атрибутов.

Хотя разные порождающие шаблоны обеспечивают схожую функциональность, у Javascript не было способа обернуть объект неявными начальными значениями. Значение по умолчанию для неустановленного свойства в Javascript всегда undefined. Но при использование Proxy мы можем легко задать “Zero values”.

const withZeroValue = (target, zeroValue) => new Proxy(target, {
  get: (obj, prop) => (prop in obj) ? obj[prop] : zeroValue
})

Эта трехстрочная функция оборачивает целевой объект (target). Если свойство установлено, оно возвращает значение свойства. В противном случае он возвращает «нулевое значение» по умолчанию. Технически этот подход также не является неявным, но он мог бы быть, если бы мы расширили с помощью ZeroValue для поддержки специфичных для типа (а не параметризованных) нулевых значений для Boolean (false), Number (0) , String (“”), Object ({}), Array ([]) и т. д.

let pos = {
  x: 4,
  y: 19
}

console.log(pos.x, pos.y, pos.z) // 4, 19, undefined

pos = withZeroValue(pos, 0)

console.log(pos.x, pos.y, pos.z) // 4, 19, 0

Одним из мест, где эта функциональность может быть полезна, является система координат. Например библиотеки печати могут автоматически поддерживать 2D и 3D рендеринг в зависимости от формы данных. Вместо того, чтобы создавать две отдельные модели, имеет смысл всегда включать z по умолчанию в ноль, а не undefined.

Отрицательные индексы массивов

Обычная практика получение последнего элемента в массиве в Javascript является сложной, повторяющийся и с высокой вероятностью ошибки. Вот почему существует предложение TC39, которое определяет вспомогательное свойство Array.lastItem для получения и установки последнего элемента.

Другие языки, такие как Python и Ruby, упрощают доступ к граничным элементам массива с помощью отрицательных индексов. Например, последний элемент может быть доступен просто с помощью arr [-1] вместо arr[arr.length-1].

С Прокси отрицательные индексы могут также использоваться в Javascript.

const negativeArray = (els) => new Proxy(els, {
  get: (target, propKey, receiver) => Reflect.get(target,
    (+propKey < 0) ? String(target.length + +propKey) : propKey, receiver)
});

Одним из важных замечаний является то, что trap-сообщения, включая handler.get, преобразуют в строку (stringify) все свойства Для доступа к массиву нам нужно привести имена свойств в тип числа Numbers, что можно кратко сделать с помощью унарного оператора плюс.

Теперь [-1] обращается к последнему элементу, [-2] ко второму и т. д.

const unicorn = negativeArray(['🐴', '🎂', '🌈']);

unicorn[-1] // '🌈'

Существует даже пакет npm, negative-array, который более полно инкапсулирует эту функциональность.

Приватные свойства

Общеизвестно что в Javascript не хватает механики приватных свойств. Первоначально для этого был введен Symbol, но позже он был дополнен рефлексивными методами, такими как Object.getOwnPropertySymbols, которые сделали приватные методы общедоступными для обнаружения.

Давнее соглашение заключалось в том, чтобы именовать частные свойства с подчеркиванием. Прокси предлагает немного лучший подход к маскировке таких свойств.

const hide = (target, prefix = '_') => new Proxy(target, {
  has: (obj, prop) => (!prop.startsWith(prefix) && prop in obj),
  ownKeys: (obj) => Reflect.ownKeys(obj)
    .filter(prop => (typeof prop !== "string" || !prop.startsWith(prefix))),
  get: (obj, prop, rec) => (prop in rec) ? obj[prop] : undefined
})

Функция hide оборачивает целевой объект и делает свойства с префиксом подчеркивания недоступными для оператора in и таких методов, как Object.getOwnPropertyNames.

et userData = hide({
  firstName: 'Tom',
  mediumHandle: '@tbarrasso',
  _favoriteRapper: 'Drake'
})

userData._favoriteRapper        // undefined
('_favoriteRapper' in userData) // false
Object.keys(userData)           // ['firstName', 'mediumHandle']

Более полная реализация также будет включать такие трапы как deleteProperty и defineProperty. Помимо замыканий, этот подход, вероятно, наиболее близок к действительно частным свойствам, поскольку они недоступны из-за перечисления, клонирования, доступа или изменения.

Однако они, видны в консоли разработки.

Кэширование

В компьютерных науках есть три серьезные проблемы: инвалидация кеша (cache invalidation), присвоение имен и ошибка завышения или занижения на единицу (off-by-one errors).

Нередко возникают трудности с синхронизацией состояния между клиентом и сервером. Данные могут со временем меняться, и становиться трудно точно определить их актуальность. Возникает вопрос где размещать логику того, когда выполнять повторную синхронизацию.

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

const ephemeral = (target, ttl = 60) => {
  const CREATED_AT = Date.now()
  const isExpired = () => (Date.now() - CREATED_AT) > (ttl * 1000)
  
  return new Proxy(target, {
    get: (obj, prop) => isExpired() ? undefined : Reflect.get(obj, prop)
  })
}

Эта функция упрощена: она делает все свойства объекта недоступными через определенный промежуток времени. Однако не составит труда расширить этот подход, чтобы установить время жизни (TTL) для каждого свойства и обновлять его после определенной продолжительности или количества обращений.

let bankAccount = ephemeral({
  balance: 14.93
}, 10)

console.log(bankAccount.balance)    // 14.93

setTimeout(() => {
  console.log(bankAccount.balance)  // undefined
}, 10 * 1000)

Этот пример просто делает баланс банковского счета недоступным через 10 секунд. Более подробные примеры использования в реальных условиях представлены в нескольких статьях, Caching & Logging и кэшированию на стороне клиента с использованием Proxy и sessionStorage.

Enums и представления только для чтения

Следующие примеры взяты из статьи Чабы Хеллингер Proxy Use Cases и Mozilla Hacks. Подход заключается в том, чтобы обернуть объект, чтобы предотвратить расширение или его модификацию. Хотя Object.freeze теперь предоставляет функциональные возможности для создания объекта только для чтения, этот подход можно расширить для улучшения enum объектов, которые выдают ошибки при доступе к несуществующим свойствам.

const NOPE = () => {
  throw new Error("Can't modify read-only view");
}

const NOPE_HANDLER = {
  set: NOPE,
  defineProperty: NOPE,
  deleteProperty: NOPE,
  preventExtensions: NOPE,
  setPrototypeOf: NOPE
}

const readOnlyView = target =>
  new Proxy(target, NOPE_HANDLER)

Представление Enum

const createEnum = (target) => readOnlyView(new Proxy(target, {
  get: (obj, prop) => {
    if (prop in obj) {
      return Reflect.get(obj, prop)
    }
    throw new ReferenceError(`Unknown prop "${prop}"`)
  }
}))

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

Наш пример перечисления enum также является первым примером прокси на прокси, подтверждая, что прокси является допустимым целевым объектом для другого прокси. Это облегчает повторное использование кода посредством композиции функций Proxy.

let SHIRT_SIZES = createEnum({
  S: 10,
  M: 15,
  L: 20
})

SHIRT_SIZES.S // 10
SHIRT_SIZES.S = 15

// Uncaught Error: Can't modify read-only view

SHIRT_SIZES.XL

// Uncaught ReferenceError: Unknown prop "XL"

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

В то время как другие фреймворки и языковые надмножества, такие как TypeScript, предлагают enum type, это решение является уникальным в том смысле, что оно работает с ванильным Javascript без специальных инструментов сборки или транспиляторов.

Перегрузка операторов (Overload)

Возможно, наиболее интересным синтаксическим сценарием использования Proxy является возможность перегрузки операторов, таких как оператор in, использующий handler.has.

Оператор in предназначен для проверки того, находится ли «указанное свойство в указанном объекте или в его цепочке прототипов». Но это также самый синтаксически элегантный оператор для перегрузки. В этом примере определяется функция range для сравнения Numbers.

const range = (min, max) => new Proxy(Object.create(null), {
  has: (_, prop) => (+prop >= min && +prop <= max)
})

В отличие от Python, который использует генераторы для сравнения с конечной последовательностью целых чисел, этот подход поддерживает десятичное сравнение и может быть расширен для поддержки других числовых диапазонов – естественных, рациональных, мнимых и т.п..

const X = 10.5
const nums = [1, 5, X, 50, 100]

if (X in range(1, 100)) { // true
  // ...
}

nums.filter(n => n in range(1, 10)) // [1, 5]

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

В дополнение к оператору in мы также можем перегрузить delete и new.

Объект Cookies

Если вам когда-либо приходилось взаимодействовать с файлами cookie в Javascript, вам приходилось иметь дело с document.cookies. Это необычный API, в котором API – это строка, которая считывает все файлы cookie, разделенные точкой с запятой, но вы используете оператор присваивания для инициализации или перезаписи одного файла cookie.

document.cookiesэто строка, которая выглядит примерно так:

_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1

Короче говоря, работа с document.cookies сложна и подвержена ошибкам. Одним из подходов является простая структура cookie, которая может быть адаптирована для использования Proxy.

const getCookieObject = () => {
    const cookies = document.cookie.split(';').reduce((cks, ck) => 
	({[ck.substr(0, ck.indexOf('=')).trim()]: ck.substr(ck.indexOf('=') + 1), ...cks}), {});
    const setCookie = (name, val) => document.cookie = `${name}=${val}`;
    const deleteCookie = (name) => `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;

    return new Proxy(cookies, {
	set: (obj, prop, val) => (setCookie(prop, val), Reflect.set(obj, prop, val)),
        deleteProperty: (obj, prop) => (deleteCookie(prop), Reflect.deleteProperty(obj, prop))
     })
}

Эта функция возвращает объект, который действует как любой другой объект ключ-значение, но проксирует все изменения в document.cookies для сохранения.

let docCookies = getCookieObject()

docCookies.has_recent_activity              // "1"
docCookies.has_recent_activity = "2"        // "2"
delete docCookies2["has_recent_activity"]   // true

Недостатки

Дьявол кроется в деталях, и Прокси не исключение.

Полифил

На момент написания этой статьи (май 2019 года) для Proxy не было полноценного полифила. Тем не менее, существует частичный полифил для Proxy, написанный Google, который поддерживает get, set, apply и construct traps и работает для IE9+.

Это прокси?

Невозможно определить, является ли объект прокси или нет. 2ality

Согласно спецификациям языка Javascript, нет способа определить, является ли Объект Прокси. Однако в Node 10+ это возможно с помощью метода util.types.isProxy.

Какой объект является целевым ?

Для данного объекта Proxy невозможно получить или изменить целевой объект (target). Также невозможно получить или изменить объект-обработчик (handler).

В статье Бена Наделя Using Proxy to Dynamically Change THIS Binding, описан близкий пример, в которой пустой объект используется в качестве цели target прокси и замыкания для переназначения объекта, над которым выполняются действия прокси.

Proxy примитивы

new Proxy("To be, or not to be...", { })

К сожалению, одним из ограничений Прокси является то, что целевым объектом (target) может быть только Объект. Это означает, что мы не можем напрямую использовать примитивы типа String. 😞

Производительность

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

Почему Proxy?

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

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

// document.cookies = "_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1"

let docCookies = withZeroValue(hide(readOnlyView(getCookieObject())), "Cookie not found")

docCookies.has_recent_activity  // "1"
docCookies.nonExistentCookie    // "Cookie not found"
docCookies._ga                  // "Cookie not found"
docCookies.newCookie = "1"      // Uncaught Error: Can't modify read-only view

Я надеюсь, что эти примеры показали, что Proxy – это больше, чем просто эзотерическая функция для нишевого метапрограммирования в Javascript.

Была ли вам полезна эта статья?
[2 / 5]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments