Введение в JavaScript Proxy
Что такое Proxy
Объект Proxy
позволяет обнаружить, когда кто-то взаимодействует со свойством массива или объекта, и выполнить в ответ код.
Для создания нового объекта Proxy
можно использовать конструктор new Proxy()
. В качестве аргумента передайте массив ([]
) или объект ({}
), из которого необходимо создать Proxy
, а также объект обработчик, определяющий способ обработки взаимодействий (подробнее об этом вкратце).
В этом примере из объекта wizards
создаётся Proxy
, а в качестве обработчика передаётся пустой объект.
let wizard = {
name: 'Merlin',
tool: 'Wand'
};
// Создание Proxy объекта
let wizardProxy = new Proxy(wizard, {});
Объект обработчик и ловушки
Объект обработчик указывает Proxy
, как реагировать на взаимодействие с объектом или свойствами массива. Можно назначить несколько функций, называемых ловушками, которые запускаются как обратные вызовы для различных типов взаимодействий.
Существует более десятка различных методов ловушек, но три наиболее распространённых — это методы get()
, set()
и deleteProperty()
. Они выполняются каждый раз, когда кто-то получает, устанавливает или удаляет свойство массива или объекта, соответственно.
В этом примере получение, установка и удаление свойств объекта происходит в обычном режиме. Но при этом выведем некоторую информацию в консоль, чтобы можно было увидеть, что происходит, когда вносятся изменения в массив или объект. Для простоты используем сокращённый синтаксис свойств объекта.
// Создание Proxy объекта
let wizardProxy = new Proxy(wizard, {
/**
* Выполняется при получении значения свойства
* @param {Object|Array} obj Объект или массив, который обрабатывает Proxy
* @param {String|Integer} key Ключ или индекс свойства
*/
get (obj, key) {
console.log('get', obj, key, obj[key]);
return obj[key];
},
/**
* Выполняется при определении или обновлении свойства
* @param {Object|Array} obj Объект или массив, который обрабатывает Proxy
* @param {String|Integer} key Ключ или индекс свойства
* @param {*} value Значение, присваиваемое свойству
*/
set (obj, key, value) {
console.log('set', obj, key, value);
// Обновление свойства
obj[key] = value;
// Индикация успеха
return true;
},
/**
* Выполняется при удалении свойства
* @param {Object|Array} obj Объект или массив, который обрабатывает Proxy
* @param {String|Integer} key Ключ или индекс свойства
*/
deleteProperty (obj, key) {
console.log('delete', obj, key, obj[key]);
// Удаление свойства
delete obj[key];
// Индикация успеха
return true;
}
});
Теперь можно изменять объект Proxy
так же, как и обычный объект, а методы ловушки обработчика Proxy
в ответ будут выполнять код.
// Запускает ловушку get() и выводит в лог...
// "get" {name: 'Merlin', tool: 'Wand'} "name" "Merlin"
let name = wizardProxy.name;
// Запускает ловушку deleteProperty() и выводит в лог...
// "set" {name: 'Merlin', tool: 'Wand'} "age" 172
wizardProxy.age = 172;
// Запускает ловушку get() и выводит в лог...
// "delete" {name: 'Merlin', tool: 'Wand'} "tool" "Wand"
delete wizardProxy.tool;
Proxy и вложенность
Одна из проблем с прокси заключается в том, что они обнаруживают изменения только в свойствах первого уровня объекта или массива. Свойства, являющиеся вложенными объектами и массивами объекта Proxy
, сами по себе не являются Proxy
и не детектируются.
В данном случае есть объект wizard
с вложенным массивом spells
. Также есть объект handler
с методами get()
и set()
. Они оба выводят сообщение в консоль, а в остальном сохраняют поведение по умолчанию.
Создадим объект Proxy
с объектами wizard
и handler
и присвоим его переменной wizardProxy
.
// Объект с вложенным массивом
let wizard = {
name: 'Merlin',
tool: 'wand',
spells: ['Abbracadabra', 'Disappear']
};
// Объект handler
let handler = {
get (obj, key) {
console.log('get', key);
return obj[key];
},
set (obj, key, value) {
console.log('set', key);
obj[key] = value;
return true;
}
};
// Создаём Proxy
let wizardProxy = new Proxy(wizard, handler);
Если в wizardProxy
добавить свойство или получить значение свойства из него, в консоль будет выведено сообщение, как и ожидалось.
// Выводит "get" "name" и "set" "age" соответственно
let name = wizardProxy.name;
wizardProxy.age = 172;
Но если получить свойство из массива wizardProxy.spells
, то метод handler.get()
выполнится при получении массива spells
, но не конкретных свойств из него.
Но если использовать метод Array.prototype.push()
для добавления свойства в массив wizardProxy.spells
, то метод handler.get()
выполняется, при получении массива spells
, но метод handler.set()
никогда не выполняется.
// Обновление вложенного массива
// выводит "get" "spells"
wizardProxy.spells.push('Heal');
Как обрабатывать вложенные массивы и объекты в объекте Proxy
Чтобы обнаружить вложенные массивы и объекты внутри объекта Proxy
, сначала нужно переместить объект handler
в функцию, возвращающую объект.
function handler () {
return {
get (obj, key) {
console.log('get', key);
return obj[key];
},
set (obj, key, value) {
console.log('set', key);
obj[key] = value;
return true;
}
};
}
// Создаём Proxy
let wizardProxy = new Proxy(wizard, handler());
Внутри метода handler.get()
необходимо проверить, является ли значение свойства (obj[key]
) массивом или объектом.
Если да, то мы передаём его в конструктор new Proxy()
и возвращаем обратно, рекурсивно передавая функцию handler()
. Если нет, то возвращаем его как есть.
Оператор typeof
возвращает object
для всех видов элементов, не являющихся простыми объектами ({}
), поэтому используем другой подход, чтобы выяснить это. Можно использовать метод call()
метода Object.prototype.toString()
и передать в него элемент, который хотим проверить. Он вернёт имя прототипа.
// возвращает [object Array]
Object.prototype.toString.call([]);
// [object Object]
Object.prototype.toString.call({});
Создадим массив с [object Object]
и [object Array]
, затем используем метод Array.prototype.includes()
, чтобы проверить, является ли строка, возвращённая Object.prototype.toString.call(obj[key])
, одним из этих значений.
Если да, то вернём new Proxy()
, передав в качестве аргументов obj[key]
и handler()
.
function handler () {
return {
get (obj, key) {
console.log('get', key);
// Если элемент является объектом или массивом, возвращается proxy
let nested = ['[object Object]', '[object Array]'];
let type = Object.prototype.toString.call(obj[key]);
if (nested.includes(type)) {
return new Proxy(obj[key], handler());
}
return obj[key];
},
set (obj, key, value) {
console.log('set', key);
obj[key] = value;
return true;
}
};
}
Теперь, когда Array.prototype.push()
элемент в массив wizardProxy.spells
, метод handler.set()
действительно выполняется.
wizardProxy.spells.push('Heal');
Как избежать создания Proxy из Proxy
Прокси непрозрачны. Нет никакого нативного свойства, по которому можно определить, является ли объект уже Proxy
или нет.
В текущем коде можно создать Proxy
из Proxy
, в результате чего функция handler()
будет выполняться для одного и того же массива или объекта несколько раз. Если это произойдёт несколько раз, браузер может зависнуть или даже упасть.
let data = new Proxy({
wizards: {
list: ['Gandalf', 'Radagast', 'Merlin']
},
witches: {
list: ['Ursula', 'Wicked Witch Of The West', 'Malificent']
}
}, handler());
/**
* Реверс witches и wizards
* После нескольких десятков замен браузер начнёт тормозить или упадёт.
*/
function swapMagic () {
let tempCache = data.wizards.list;
data.wizards.list = data.witches.list;
data.witches.list = tempCache;
}
Хотя в браузере нет способа проверить, является ли массив или объект Proxy
, его можно добавить с помощью объекта handler
.
В методе handler.get()
сначала проверим, является ли извлекаемый ключ _isProxy
. Если да, то возвращаем true
.
Это не реальное свойство объекта. Это внутреннее фиктивное свойство, возвращающее true
только в том случае, если выполняется метод handler.get()
. Если это происходит, мы знаем, что свойство уже является объектом Proxy
.
// Объект handler
function handler () {
return {
get (obj, key) {
// Если ключ "_isProxy", возвращаем true
// Это произойдёт только в случае, если свойство уже является Proxy
if (key === '_isProxy') return true;
// ...
},
// ...
};
}
Если свойство является массивом или объектом, мы проверяем, возвращает ли свойство _isProxy
значение true
.
Если это так, то массив или объект уже управляется объектом handler
и уже стал Proxy
, поэтому можно вернуть его как есть. Если нет, то это обычный массив или объект, и можно смело возвращать new Proxy()
.
// Объект handler
function handler () {
return {
get (obj, key) {
// Если ключ "_isProxy", возвращаем true
// Это произойдёт только в случае, если свойство уже является Proxy
if (key === '_isProxy') return true;
// Если элемент является объектом или массивом и ещё не является Proxy, возвращаем new Proxy
let nested = ['[object Object]', '[object Array]'];
let type = Object.prototype.toString.call(obj[key]);
if (nested.includes(type) && !obj[key]._isProxy) {
return new Proxy(obj[key], handler());
}
// Иначе возвращаем свойство
return obj[key];
},
// ...
};
}
Эти два небольших дополнения позволяют избежать вложенности массивов и объектов в несколько обработчиков Proxy
, а также связанных с этим проблем с производительностью.