Что такое прокси?

Прокси, в широком смысле, это некая доверенная сущность, выступающая от имени другой сущности. Прокси - это заменитель реального объекта, у которого есть право выступать от имени и в интересах этого объекта. "Объектом" в данном случае может быть практически всё что угодно. Если же ограничиться рассмотрением прокси в применении к JavaScript, то можно сказать, что это - особые объекты, которые позволяют перехватывать и изменять действия, выполняемые над другими объектами. В частности, речь идёт о вызове функций, об операциях присваивания, о работе со свойствами, о создании новых объектов, и так далее. Эту технологию используют для блокирования прямого доступа к целевому объекту или целевой функции и организации взаимодействия с объектом или функцией через прокси-объект.

Прежде чем мы продолжим разговор о прокси-объектах и перейдём к примерам работы с ними, рассмотрим три важных термина, имеющих к ним непосредственное отношение.

  • Обработчик (Handler) - объект-заменитель, содержащий перехватчики.
  • Перехватчики (Traps) - методы, которые предоставляют доступ к свойствам.
  • Целевой объект (Target) - объект, который виртуализируется прокси.

Синтаксис

Вот как выглядит объявление простого прокси-объекта, которому передаётся целевой объект и обработчик.

let proxy = new Proxy(target, handler)

Проверка поддержки прокси-объектов браузером

Начнём с проверки поддержки прокси-объектов JavaScript браузером.

let proxyTest = new Proxy({}, {})

if (proxyTest instanceof Object) {
	document.write("Proxy supported!")
}

Стандартное поведение объектов

Здесь мы объявим объект, а затем попробуем обратиться к несуществующему свойству этого объекта.

let obj = {
	c: "car",
	b: "bike"
}

document.write(obj.b)  //Результат -> "bike"
document.write(obj.c)  //Результат -> "car"
document.write(obj.l)  //Результат -> "undefined"

Использование прокси для объекта

В следующем примере мы используем обработчик с перехватчиком get. Обработчик передаст целевой объект и запрошенный ключ перехватчику.

let handler = {
	get: function(target, name) {
		return name in target ? target[name] : "Key does not exist"
	}
}

let obj = {
	c: "car",
	b: "bike"
}

let proxyObj = new Proxy(obj, handler)

document.write(proxyObj.b)  //Результат -> "bike"
document.write(proxyObj.c)  //Результат -> "car"
document.write(proxyObj.l)  //Результат -> "Key does not exist"

Перехватчик get

Перехватчик get выполняется при попытке доступа к свойству объекта с использованием прокси. Метод get принимает параметр target (объект, с которым нужно работать через прокси) и property (свойство, к которому мы пытаемся получить доступ).

Синтаксис

var p = new Proxy(target, {
	get: function(target, property, receiver) {}
})
  • target: целевой объект
  • property: имя свойства, с которым нужно работать
  • receiver: прокси-объект или объект, унаследованный от прокси-объекта

Пример

В следующем примере мы попытаемся воздействовать на значение с помощью перехватчика get до вывода его на экран.

let handler = {
	get: function(target, name) {
		return name in target ? target[name]*10 : "Key does not exist"
	}
}
 
let obj = {
	a: 1,
	b: 2
}
 
let proxyObj = new Proxy(obj, handler)

document.write(proxyObj.a)  //Результат -> 10
document.write(proxyObj.b)  //Результат -> 20
document.write(proxyObj.c)  //Результат -> "Key does not exist"

Перехватчик set

Перехватчик set выполняется при попытке установки свойства объекта с использованием прокси. Метод set принимает параметр target (объект, доступ к которому мы собираемся получить), property (свойство, которое мы собираемся устанавливать), и value (значение свойства, которое мы собираемся установить).

Синтаксис

var p = new Proxy(target, {
	set: function(target, property, value, receiver) {}
})
  • target: целевой объект
  • property: имя свойства, которое нужно устанавливать
  • value: новое значение интересующего нас свойства
  • receiver: объект, которому изначально была направлена операция присваивания. Обычно - это сам прокси. Однако обработчик set может быть вызван не напрямую, а через цепочку прототипов или каким-то другим способом

Пример

В следующем примере мы добавим к объекту некоторые свойства, назначим им значения, при этом сделаем это до объявления прокси-объекта (в коде он носит имя proxyObj).

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

let handler = {
	set: function(target, name, value) {
		target[name] = value*10
	}
}
 
let obj = {
	a: 1,
	b: 2
}

let proxyObj = new Proxy(obj, handler)

proxyObj.c = 3

document.write(proxyObj.a)  //Результат -> 1
document.write(proxyObj.b)  //Результат -> 2
document.write(proxyObj.c)  //Результат -> 30

Перехватчик has

Перехватчик has вызывается при выполнении оператора in. Метод has принимает параметры target (целевой объект) и property (свойство, доступ к которому нужно контролировать с помощью прокси-объекта).

Синтаксис

var p = new Proxy(target, {
	has: function(target, property) {}
})
  • target: целевой объект
  • property: имя свойства, существование которого будет проверяться

Пример

В следующем примере мы проверяем, входит ли в ключ подстрока ar. Для начала проверим, существует ли ключ, и, если это так, проверим, содержит ли он интересующую нас подстроку. Если будут выполнены оба условия, мы вернём логическое значение true, в противном случае - вернём false.

const handler = {
	has: function(target, key) {
		if (key in target && key.includes("ar")) {
			return true
		}
		
		return false
	}
}

const user = {
	car: 'Bentley',
	bar: 4,
	hotel: "no",
}

const proxy = new Proxy(user, handler)

document.write('car' in proxy)      // Результат -> true
document.write('car' in user)       // Результат -> true
document.write('hotel' in proxy)    // Результат -> false
document.write('hotel' in user)     // Результат -> true
document.write('spacebar' in proxy) // Результат -> false
document.write('spacebar' in user)  // Результат -> false

Перехватчик apply

Перехватчик apply позволяет вызывать прокси с параметрами. Он позволяет переопределять функции. Метод apply принимает параметры target (целевой объект или целевая функция), thisArg (аргумент this для использования при вызове) и argumentsList (список всех аргументов в виде массива).

Синтаксис

var p = new Proxy(target, {
	apply: function(target, thisArg, argumentsList) {}
})
  • target: целевой объект
  • thisArg: аргумент this для вызова
  • argumentsList: список аргументов для вызова

Пример

В следующем примере мы, с помощью прокси, переопределим функцию, которая умножает два числа. Прокси меняет поведение функции, прибавляя 1 к результату умножения.

function multiply(a, b) {
	return a * b
}

const handler = {
	apply: function(target, thisArg, argumentsList) {
		return target(argumentsList[0], argumentsList[1]) + 1
	}
}

var proxy = new Proxy(multiply, handler)

document.write(multiply(2, 5))  // Результат -> 10
document.write(proxy(2, 5))     // Результат -> 11

Перехватчик construct

Перехватчик construct выполняется при вызове оператора new. Для того чтобы этот перехватчик мог нормально функционировать, нужно, чтобы целевой объект, для работы с которым его планируется вызывать, можно было бы создать командой вида new Target().

Синтаксис

var p = new Proxy(target, {
	construct: function(target, argumentsList, newTarget) {}
})
  • target: целевой объект
  • argumentList: список аргументов для конструктора
  • newTarget: исходный конструктор

Пример

В следующем примере мы передадим через прокси новое значение, к которому добавлен символ валюты, функции-конструктору.

function formatCurrency(format) {
	this.format = format
}

const handler = {
	construct: function(target, args) {
		return new target("$" + args[0])
	}
}

const proxy = new Proxy(formatCurrency, handler)

document.write(new proxy('100').format)  // Результат -> $100

Перехватчик deleteProperty

Перехватчик deleteProperty вызывается при обращении к методу delete. Он принимает параметры target (целевой объект или целевая функция), и property (свойство, команду удаления которого мы хотим обрабатывать).

Синтаксис

var p = new Proxy(target, {
	deleteProperty: function(target, property) {}
})
  • target: целевой объект
  • property: имя свойства, в операцию удаления которого нужно вмешаться

Пример

Следующий пример демонстрирует вызов нужной нам функции и выполнение определённых действий при попытке удаления свойства объекта.

const cars = {
	merc: 's320',
	buggati: 'veyron',
}

const handler = {
	deleteProperty: function(target, prop) {
		if (prop in target) {
			document.write(`${prop} has been removed`)
			delete target[prop]
		}
	}
}

document.write(cars.merc)  // Результат -> "s320"

const proxy = new Proxy(cars, handler)

delete proxy.merc          // Результат -> merc has been removed

document.write(cars.merc)  // Результат -> undefined

Варианты использования прокси

Вот несколько областей практического применения прокси-объектов в JavaScript.

  • Безопасность. Прокси позволяют организовать проверку аргументов функций и значений свойств объектов.
  • Хранение данных. Прокси-функция может вызываться при попытках модификации свойств объектов и создавать резервные копии данных.
  • Статистика. Прокси-объекты позволяют собирать статистические сведения об объектах, с которыми взаимодействуют пользователи или некие программные механизмы приложений.
  • Учёт контекста. Различные прокси можно использовать для различных контекстов выполнения программ. Скажем, это может быть контекст отладки или контекст продакшн-среды. В случае с отладкой прокси могут, например, представлять значения переменных и давать возможности прямого воздействия на содержимое объектов.
  • Шаблон проектирования "посредник". Прокси являются посредниками, используемыми для работы с представляемыми ими объектами, позволяющими объектам взаимодействовать не напрямую, а через них. В результате, при использовании прокси, не нужно описывать механизмы непосредственного взаимодействия объектов друг с другом.
  • Управление доступом. При организации доступа к свойствам объектов через прокси можно управлять происходящим, например, запрещать пользователю или группе пользователей доступ к объекту. Эти ограничения можно вводить либо на основании внутренней логики прокси-объектов, либо отдавая этим объектам явные команды, касающиеся ограничения доступа к целевым объектам.