Изучаю Proxy
volond
В Google Apps Script, в связи с переходом на версию V8 , появилась новая возможность, которая пока используется не особенно широко. Речь идёт о прокси-объектах.
Прокси позволяют создавать обёртки для других объектов, организовывая перехват операций доступа к их свойствам и операций вызова их методов.
Причём, это работает даже для несуществующих свойств и методов проксируемых объектов.
Аналогией может служить следующая ситуация:
Что бы попасть к шефу, надо пройти сначала его секретаря
Когда мы читаем свойства объекта, выполняется функция get.let obj = {a: 1, b:2}
// Используем синтаксис Proxy в поиске "секретаря" для объекта
let objProxy = new Proxy(obj, {
get: function(item, property, itemProxy){
console.log(
`Вы только попробовали прочитать значение свойства ${property}`
)
return item[property]
}
})
Или даже изменить значение при чтении
let obj = {a: 1, b:2}
let objProxy = new Proxy(obj, {
get: function(item, property, itemProxy){
console.log(`
Вы только изменили значение свойства ${property}
при его чтении
`)
return item[property] * 2
}
})
console.log(objProxy.b)
Вы только изменили значение свойства 'b' при его чтении
4
В дополнение к перехвату чтения свойств можно перехватывать их модификации:
let objProxy = new Proxy(obj, {
set: function(item, property, value, itemProxy){
console.log(`You are setting '${value}' to '${property}' property`)
item[property] = value
}
})
В целом Proxy перехватывает 13 операций над объектами:
- get(item, propKey, itemProxy) — чтение свойств, например obj.a и ojb['b']
- set(item, propertyKey, value, itemProxy) — установка свойств: obj.a = 1
- has(item,propKey) — операция propKey in objProxy и возврат логического значения.
- deleteProperty(item, propKey) — операция delete proxy[propKey] и возврат логического значения.
- ownKeys(item) для операций Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), for...in, для возврата массива. Метод возвращает имена всех собственных свойств целевого объекта, в то время как возвращаемый результат Object.keys() включает в себя только перечисляемые свойства целевого объекта.
- getOwnPropertyDescriptor(item, propKey): перехват операции Object.getOwnPropertyDescriptor(proxy, propKey), возврат описания свойства.
- defineProperty(item, propKey, propDesc) для операций Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDescs), возврат логического значения.
- preventExtensions(item): перехватчик операции операции Object.preventExtensions(proxy), возврат логического значения.
- getPrototypeOf(item): перехватчик операции Object.getPrototypeOf(proxy), возврат объекта.
- isExtensible(item): перехватчик операции Object.isExtensible(proxy), возврат логического значения.
- setPrototypeOf(item, proto): перехватчик операции Object.setPrototypeOf(proxy, proto), возврат логического значения.
Если целевой объект — функция, можно применять две дополнительные операции перехвата:
- apply(item, object, args) — перехват вызовов функции, таких как proxy(...args), proxy.call(object, ...args), proxy.apply(...) .
- construct(item, args): — перехват операции конструирования, вызванной экземпляром Proxy, например new proxy(...args).
Теперь посмотрим, на что прокси способен.
Отрицательные индексы массивов
Python и другие языки поддерживают доступ к элементам массива по отрицательному индексу. Отправная точка индекса — последний элемент массива, счёт идёт в обратном порядке, то есть:
- arr[-1] — последний элемент массива,
- arr[-3] — третий элемент массива с конца.
Многие считают эту функцию очень полезной, но, к сожалению, она не поддерживается в JavaScript.
function negativeArray(array) {
return new Proxy(array, {
get: function(target, propKey){
if (Number(propKey) != NaN && Number.isInteger(Number(propKey)) && Number(propKey) < 0) {
propKey = String(target.length + Number(propKey));
}
return target[propKey]
}
})
}
var newArr = negativeArray(["a", "b", "c"])
console.log(newArr[-1]);//c
Валидация данных
Как мы знаем, JavaScript — слабо типизированный язык. Обычно при создании объекта объект остаётся открытым, то есть кто угодно может его изменить. Но в большинстве случаев значение свойства объекта должно соответствовать определённым условиям. Например, объект, записывающий пользовательскую информацию в поле возраст, должен быть целым числом больше нуля и меньше 150:
let person1 = {
name: 'Jon',
age: 23
}
По умолчанию, однако, JavaScript не предоставляет механизм безопасности, и это значение при желании можно изменить:
person1.age = 9999 person1.age = 'hello world'
Чтобы сделать код безопаснее, можно обернуть объект в Proxy. Мы можем перехватить операцию set и проверить, соответствует ли новое значение поля age каким-то правилам:
let ageValidate = {
set (item, property, value) {
if (property === 'age') {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new TypeError('age should be an integer between 0 and 150');
}
}
item[property] = value
}
}
Теперь попробуем изменить значение этого
let person1 = {
name: 'Jon',
age: 23
}
var person = new Proxy(person1, ageValidate)
person.age = 33
person.age = '33'//Число как текст
person.age = 'Что то не число'
console.log(person)
Ассоциированное свойство
Свойства объекта связаны друг с другом. Например, для объекта, хранящего пользовательскую информацию, почтовый индекс и местоположение тесно связаны. Когда определён почтовый индекс пользователя, определено и его местоположение.
let postcodeValidate = {
set(item, property, value) {
if (property = 'location') {
item.postcode = location2postcode[value]
item[property]=value
}
if (property = 'postcode') {
item.location = postcode2location[value]
item[property]=value
}
}
}
const location2postcode = {
'A Street': 232200,
'B Street': 234422,
'C Street': 231142
}
const postcode2location = {
'232200': 'A Street',
'234422': 'B Street',
'231142': 'C Street'
}
let person1 = {
name: 'Jon'
}
var person = new Proxy({ name: 'Вася' }, postcodeValidate)
person.postcode = 232200
console.log(person.postcode);
console.log(person.location);
Приватные свойства
Мы знаем, что приватные свойства никогда не поддерживались в JavaScript, что не позволяет управлять правами доступа к ним. Для решения этой проблемы существует соглашение сообщества JavaScript: поля, начинающиеся с символа _, рассматриваются как приватные:
var obj = {
a: 1,
_value: 22
}
Свойство _value выше рассматривается как приватное. Важно помнить, что это просто соглашение, то есть на уровне языка такого правила не существует. Теперь с помощью Proxy можно смоделировать приватные свойства. Приватные свойства обладают такими особенностями:
- Их значение не может быть прочитано.
- Когда пользователь пытается получить доступ к ключу объекта, приватное свойство скрыто.
Теперь изучим 13 операций перехвата прокси, упоминавшиеся выше, и увидим, что нам нужно перехватить только 3 из них:
function setPrivateField(obj, prefix = "_"){
return new Proxy(obj, {
// Перехват операции`propKey in objProxy`
has: (obj, prop) => {},
// Перехват `Object.keys(proxy)`
ownKeys: obj => {},
//Перехват чтения свойств объекта
get: (obj, prop, rec) => {})
});
}
Затем добавим в код условие: если пользователь пытается получить доступ к полю, начинающемуся с символа _, доступ запрещается. [ Примеры не совсем оптимальны, их задача — демонстрация, а не эффективность]:
function setPrivateField(obj, prefix = "_"){
return new Proxy(obj, {
has: (obj, prop) => {
if(typeof prop === "string" && prop.startsWith(prefix)){
return false
}
return prop in obj
},
ownKeys: obj => {
return Reflect.ownKeys(obj).filter(
prop => typeof prop !== "string" || !prop.startsWith(prefix)
)
},
get: (obj, prop) => {
if(typeof prop === "string" && prop.startsWith(prefix)){
return undefined
}
return obj[prop]
}
});
}
SDK для API в 20-ти строках кода
Как я уже сказал, объекты Proxy позволяют перехватывать вызовы методов, которых в проксированных объектах не существует. При вызове метода проксированного объекта вызывается обработчик get, после чего можно возвратить динамически сгенерированную функцию. При этом данный объект, если в этом нет необходимости, изменять не нужно.
Вооружившись этой идеей, можно проанализировать имя и аргументы вызываемого метода и динамически, во время выполнения программы, реализовать его функциональность.
Например, может существовать прокси-объект, который, при вызове api.getUsers(), может создать путь GET/users в API. Этот подход можно развивать и дальше. Например, команда вида api.postItems({ name: ‘Item name' }) может вызывать обращение к POST /items, используя первый параметр метода в виде тела запроса.
Посмотрим на программное выражение этих рассуждений:
const { METHODS } = require('http')
const api = new Proxy({},
{
get(target, propKey) {
const method = METHODS.find(method =>
propKey.startsWith(method.toLowerCase()))
if (!method) return
const path =
'/' +
propKey
.substring(method.length)
.replace(/([a-z])([A-Z])/g, '$1/$2')
.replace(/\$/g, '/$/')
.toLowerCase()
return (...args) => {
const finalPath = path.replace(/\$/g, () => args.shift())
const queryOrBody = args.shift() || {}
// Тут можно использовать fetch
// return fetch(finalPath, { method, body: queryOrBody })
console.log(method, finalPath, queryOrBody)
}
}
}
)
// GET /
api.get()
// GET /users
api.getUsers()
// GET /users/1234/likes
api.getUsers$Likes('1234')
// GET /users/1234/likes?page=2
api.getUsers$Likes('1234', { page: 2 })
// POST /items с телом запроса
api.postItems({ name: 'Item name' })
// api.foobar не является функцией
api.foobar()
Здесь мы создаём обёртку для пустого объекта — {}, при этом все методы реализованы динамически. Прокси необязательно использовать с объектами, содержащими необходимую функциональность или её части. Значок $ используется как подстановочный символ для параметров.
Тут хотелось бы отметить, что вышеприведённый пример вполне допустимо реализовать иначе. Его, например, можно оптимизировать. Скажем, динамически генерируемые функции можно кэшировать, что избавит нас от необходимости постоянно возвращать новые функции. Всё это не имеет прямого отношения к прокси-объектам, поэтому я, чтобы не перегружать примеры, привожу их именно в таком виде.
Выполнение запросов к структурам данных с помощью удобных и понятных методов
Предположим, есть массив с информацией о неких людях и с ним надо работать примерно так:
arr.findWhereNameEquals('Lily')
arr.findWhereSkillsIncludes('javascript')
arr.findWhereSkillsIsEmpty()
arr.findWhereAgeIsGreaterThan(40)
Подобное можно реализовать с помощью прокси. А именно, массив можно обернуть прокси-объектом, который анализирует вызовы методов и выполняет поиск запрошенных данных в массиве.
Вот как это может выглядеть:
const camelcase = require('camelcase')
const prefix = 'findWhere'
const assertions = {
Equals: (object, value) => object === value,
IsNull: (object, value) => object === null,
IsUndefined: (object, value) => object === undefined,
IsEmpty: (object, value) => object.length === 0,
Includes: (object, value) => object.includes(value),
IsLowerThan: (object, value) => object === value,
IsGreaterThan: (object, value) => object === value
}
const assertionNames = Object.keys(assertions)
const wrap = arr => {
return new Proxy(arr, {
get(target, propKey) {
if (propKey in target) return target[propKey]
const assertionName = assertionNames.find(assertion =>
propKey.endsWith(assertion))
if (propKey.startsWith(prefix)) {
const field = camelcase(
propKey.substring(prefix.length,
propKey.length - assertionName.length)
)
const assertion = assertions[assertionName]
return value => {
return target.find(item => assertion(item[field], value))
}
}
}
})
}
const arr = wrap([
{ name: 'John', age: 23, skills: ['mongodb'] },
{ name: 'Lily', age: 21, skills: ['redis'] },
{ name: 'Iris', age: 43, skills: ['python', 'javascript'] }
])
console.log(arr.findWhereNameEquals('Lily')) // находит Lily
console.log(arr.findWhereSkillsIncludes('javascript')) // находит Iris
Очень похоже на то, что тут показано, может выглядеть написание с использованием прокси-объектов библиотеки для работы с утверждениями вроде expect.
А вот ещё одна идея использования прокси-объектов. Она заключается в создании библиотеки для построения запросов к базе данных со следующим API:
const id = await db.insertUserReturningId(userInfo) // Выполняет запрос INSERT INTO user ... RETURNING id
Singleton
Singleton паттерн ограничивает количество экземпляров определенного объекта только одним. Этот единственный экземпляр называется синглтоном.
Синглтоны полезны в ситуациях, когда общесистемные действия должны координироваться из единого центрального места. Примером является пул соединений с базой данных. Пул управляет созданием, уничтожением и временем жизни всех соединений с базой данных для всего приложения, гарантируя, что никакие соединения не будут «потеряны».
Синглтоны уменьшают потребность в глобальных переменных, что особенно важно в JavaScript, поскольку ограничивает загрязнение пространства имен и связанный с ним риск конфликтов имен.
Код:
function proxy(func) {
let instance;
let handler = {
construct(target, args) {
if (!instance) {
// Create an instance if there is not exist
instance = Reflect.construct(func,args)
}
return instance }
}
return new Proxy(func, handler)
}
// example
function Person(name, age) { this.name = name this.age = age}
const SingletonPerson = proxy(Person)
let person1 = new SingletonPerson('zhl', 22)
let person2 = new SingletonPerson('cyw', 22)
console.log(person1 === person2) // true