DevGang
Авторизоваться

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

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

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

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

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

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

Простейший пример работающего Прокси - один с одной ловушкой, в данном случае ловушка 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. Это всего лишь одна из более чем десятка различных ловушек:

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

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

По умолчанию / «Нулевые значения»

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

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

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

Эта трехстрочная функция оборачивает целевой объект. Если свойство установлено, оно возвращает значение свойства (сквозное). В противном случае он возвращает «нулевое значение» по умолчанию. Технически этот подход также не является неявным, но он мог бы быть, если бы мы расширили функцию withZeroValue добавив поддержку нулевых значений, специфичных для типа (а не параметризованных), для 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, преобразуют все свойства, для доступа к массиву нам нужно привести имена свойств в числа, что можно сделать кратко с помощью унарного оператора плюс.

Теперь [-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.

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

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

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

Они, однако, видны в консоли разработки. Только замыкания освобождаются от этой участи.

Кэширование

В компьютерных науках есть две серьезные проблемы: аннулирование кэша, присвоение имен и ошибки «один на один».

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

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

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 секунд. Более подробные примеры использования в реальных условиях представлены в нескольких статьях, посвященных кэшированию и ведению журнала и кэшированию на стороне клиента с использованием прокси и sessionStorage.

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

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

Просмотр только для чтения

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 View

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. Это облегчает поиск и решение проблем на ранней стадии.

Наш пример перечисления также является первым примером прокси в прокси, подтверждая, что прокси является допустимым целевым объектом для другого прокси. Это облегчает повторное использование кода посредством композиции функций 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 которые возвращают имя свойства с заданным значением перечисления, имитируя поведение в таких языках, как Javascript.

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

Перегрузка оператора

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

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

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.cookie. Это необычный API, в котором API - это строка, которая считывает все файлы cookie, разделенные точкой с запятой, но вы используете оператор присваивания для инициализации или перезаписи одного файла cookie.

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

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

Короче говоря, работа с document.cookie разочаровывает и подвержена ошибкам. Одним из подходов является простая структура 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) => document.cookie = `${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))
    })
}

Эта функция возвращает объект, который действует как любой другой объект.

let docCookies = getCookieObject()

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

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

Хорошая печать

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

Polyfill

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

Это прокси?

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

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

Нет возможности изменить target?

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

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

Примитивы

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

// TypeError: Cannot create proxy with a non-object as target or handler

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

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

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

Почему прокси?

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

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

// document.cookie = "_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.

Перевод статьи: A practical guide to Javascript Proxy

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

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

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить