Текст длиной почти 10 000 символов, не распыляйте~~
ОдинProxyОбъект оборачивает другой объект и перехватывает такие операции, как чтение/запись свойств и другие операции, при необходимости обрабатывая их самостоятельно или прозрачно позволяя объекту обрабатывать их.
Прокси используется во многих библиотеках и некоторых браузерных фреймворках. В этой главе мы увидим много практических приложений.
грамматика:
let proxy = new Proxy(target, handler)-
target-- объект для переноса, который может быть чем угодно, включая функции. -
handler- Конфигурация прокси: объект с "хуками" ("ловушками", методами, перехватывающими операции). Напримерgetкрючок для чтенияtargetАтрибуты,setкрючок написатьtargetсвойства и т. д.
правильноproxyоперации, если вhandlerЕсли в нем есть соответствующий хук, он сработает и у прокси есть шанс его обработать, в противном случае он будет напрямую обрабатывать цель.
Для начала создадим прокси без всяких хуков:
Поскольку крюков нет, все парыproxyоперации направляются непосредственноtarget.
- операция записи
proxy.test=будет писать значениеtarget. - операция чтения
proxy.testбудет отtargetВернуть соответствующее значение. - повторять
proxyбудет отtargetВернуть соответствующее значение.
Как видим, без всяких крючков,proxyЯвляетсяtargetпрозрачная упаковка.
Proxyпредставляет собой особый вид «странного объекта». У него нет собственных свойств. еслиhandlerпусто, операция прозрачно перенаправляется наtarget.
Чтобы активировать больше функций, давайте добавим хуки.
Что мы можем перехватить с их помощью?
Для большинства операций над объектами в спецификации JavaScript существует так называемый «внутренний метод», который описывает, как работает самый нижний уровень. Например[[Get]], внутренний метод чтения свойств,[[Set]], внутренние методы записи свойств и т. д. Эти методы используются только в спецификации, мы не можем вызывать их напрямую по имени метода.
Прокси-хуки будут перехватывать вызовы этих методов. они вспецификация проксии перечислены в таблице ниже.
Для каждого внутреннего метода в этой таблице есть хук: можно использовать для добавления вnew ProxyвремяhandlerИмя метода для перехвата операции в параметре:
| внутренний метод | Метод обработчика | когда запускать |
|---|---|---|
[[Get]] |
get |
читать свойство |
[[Set]] |
set |
написать свойство |
[[HasProperty]] |
has |
inоператор |
[[Delete]] |
deleteProperty |
deleteдействовать |
[[Call]] |
apply |
Прокси-объект вызывается как функция |
[[Construct]] |
construct |
newдействовать |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
|
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object/keys/values/entries
|
JavaScript применяет определенные инварианты — когда операции должны выполняться внутренними методами и ловушками.
Большинство из них предназначены для возвращаемых значений:
-
[[Set]]Должен возвращаться, если значение было успешно записаноtrue, иначе возвратfalse. -
[[Delete]]Если значение было успешно удалено, оно должно вернутьсяtrue, иначе возвратfalse. - ...и так далее, мы увидим больше в примерах ниже.
Есть некоторые другие инварианты, такие как:
-
[[GetPrototypeOf]], применяемый к прокси-объекту, должен возвращать тот же[[GetPrototypeOf]]То же значение применяется к проксируемому объекту. Другими словами, чтение прототипа прокси-объекта всегда должно возвращать прототип прокси-объекта.
Хуки могут перехватывать эти операции, но эти правила необходимо соблюдать.
Инварианты обеспечивают правильное и согласованное поведение языковых функций. Полный список инвариантов находится наТехнические характеристики. Если вы не будете делать странных вещей, вы не будете их нарушать.
Давайте посмотрим, как это работает на практическом примере.
Значение по умолчанию с хуком "get"
Наиболее распространенные хуки предназначены для чтения/записи свойств.
Чтобы перехватить операции чтения,handlerдолжен иметьget(target, property, receiver)метод.
Этот метод запускается при чтении свойства, и его параметры следующие:
-
target-- целевой объект, который передается в качестве первого аргументаnew Proxy, -
property— имя целевого атрибута, -
receiver-- если целевое свойство является свойством доступа геттера, тоreceiverЗдесь находится атрибут чтенияthisобъект. Обычно этоproxyСам объект (или, если мы наследуем от прокси, объект, который наследуется от этого прокси). Сейчас нам не нужен этот параметр, поэтому мы объясним его подробно позже.
давайте использоватьgetРеализует значение объекта по умолчанию.
Мы создадим массив, который возвращает 0 для элементов массива, которые не существуют.
Обычно, когда люди пытаются получить несуществующий элемент массива, они получаютundefined, но мы бы обернули обычный массив в прокси, чтобы поймать операцию чтения и вернуться без такого свойства0:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默认值
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (没有这样的元素)Как мы видим, используйтеgetКрючки очень легкие.
мы можем использоватьProxyдля реализации любой логики, которая считывает значение по умолчанию.
Представьте, что у нас есть словарь с фразами и их переводами:
Теперь, если нет фразы, отdictionaryчтение вернетсяundefined. Однако на практике возврат непереведенной фразы обычно происходит быстрее, чемundefinedБудь хорошим. Так что давайте в этом случае вернем непереведенную фразу вместоundefined.
Для этого мы обернемdictionaryВведите прокси, который перехватывает операции чтения:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // 拦截读取属性操作
if (phrase in target) { //如果字典包含该短语
return target[phrase]; // 返回译文
} else {
// 否则返回未翻译的短语
return phrase;
}
}
});
// 在字典中查找任意短语!
// 最坏的情况也只是它们没有被翻译。
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to ProxyОбратите внимание, как прокси перезаписывает переменную:
dictionary = new Proxy(dictionary, ...);Прокси должен полностью заменить целевой объект везде. После проксирования целевого объекта никто больше не должен ссылаться на целевой объект. Иначе легко испортить.
Подтвердить с помощью хука "set"
Предположим, нам нужен массив, посвященный числам. Ошибка должна быть выброшена, если добавляются другие типы значений.
При написании свойствsetКрючковые триггеры.
set(target, property, value, receiver):
-
target-- целевой объект, который передается в качестве первого аргументаnew Proxy, -
property— имя целевого атрибута, -
value- значение, которое будет установлено для целевого свойства, -
receiver-- а такжеgetХуки похожи, только связаны с аксессорами сеттера.
Если операция записи прошла успешно,setкрючок должен вернутьсяtrue, иначе возвратfalse(вызыватьTypeError).
Давайте используем его для проверки нового значения:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 拦截写入操作
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError (proxy 的 `set` 操作返回 false)
alert("This line is never reached (error in the line above)");Примечание. Встроенные методы Array по-прежнему работают! использование значенияpushметод добавления в массив. При добавлении значенийlengthАтрибуты автоматически увеличиваются. Наш прокси-объект Proxy ничего не ломает.
Нам не нужно переписывать такие вещи, какpushа такжеunshiftМетоды массива типа добавления элементов, в них можно добавлять проверки, т.к. внутренне они используют перехваченный прокси[[Set]]работать.
Поэтому код лаконичный и понятный.
true
Как упоминалось выше, сохраняйте инварианты.
дляsetоперация, она должна вернуться при успешной записиtrue.
Если мы забудем это сделать или вернем ложное значение, действие сработает.TypeError.
Итерация с «ownKeys» и «getOwnPropertyDescriptor»
Object.keys,for..inЦиклы и большинство других методов перебора свойств объекта используют[[OwnPropertyKeys]]внутренний метод (поownKeysперехват хука), чтобы получить список свойств.
Эти методы различаются в деталях:
-
Object.getOwnPropertyNames(obj)Возвращает несимвольные ключи. -
Object.getOwnPropertySymbols(obj)Символ ключа возврата. -
Object.keys/values()возвращается сenumerableПара ключ-значение тега, не являющаяся символом (теги свойств подробно описаны в главе «Флаги атрибутов и дескрипторы свойств»). -
for..inперебрать всеenumerableНесимвольные ключи для тегов и ключи для объектов-прототипов.
...но все начинается с этого списка.
В приведенном ниже примере мы используемownKeysперехват крюкаfor..inправильноuserобход, также используяObject.keysа такжеObject.valuesчтобы пропустить подчеркивание_Свойства в начале:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 过滤掉 _password
for(let key in user) alert(key); // name,然后是 age
// 对这些方法同样有效:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30Пока еще работает.
Хотя, если мы вернем ключ, которого нет в объекте,Object.keysне перечисляет ключ:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>Почему? причина проста:Object.keysвозвращается только сenumerableатрибут тега. Для проверки метод вызывается для каждого свойства[[GetOwnProperty]]чтобы получить дескриптор атрибута. Здесь, поскольку атрибута нет, его дескриптор пустой, нетenumerableотметьте, поэтому он будет пропущен.
чтобыObject.keysВозвращая свойство, нам нужно либо добавить свойство, либоenumerableТег хранится в объекте, либо мы можем перехватывать обращения к нему[[GetOwnProperty]](крюкgetOwnPropertyDescriptorсделает это) и возвращает дескриптор enumerable: true.
Вот пример:
Еще раз отметим: если свойство не существует в объекте, нам просто нужно перехватить[[GetOwnProperty]].
Защищенные свойства с помощью «deleteProperty» и других хуков
Существует общее соглашение, которое подчеркивает_Префиксные свойства и методы являются внутренними. Доступ к ним не должен осуществляться извне объекта.
Технически это возможно:
Давайте использовать прокси, чтобы предотвратить_Любой доступ к свойствам, начинающимся с .
Нам понадобятся следующие крючки:
-
getПри чтении таких свойств выдается ошибка, -
setПри записи свойства выдается ошибка, -
deletePropertyВыдается ошибка при удалении свойства, -
ownKeysВ использованииfor..inи подобныеObject.keysметод исключения_свойства в начале.
код показывает, как показано ниже:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // 拦截写入操作
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // 拦截属性删除
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // 拦截读取属性列表
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// “get” 不允许读取 _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// “set” 不允许写入 _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// “deleteProperty” 不允许删除 _password 属性
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// “ownKeys” 过滤排除 _password
for(let key in user) alert(key); // nameПожалуйста, обратите внимание на строку(*)серединаgetВажные детали крючков:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}Зачем нужен вызов функцииvalue.bind(target)?
Причина в том, что методы объекта (например,user.checkPassword()) должен иметь доступ_password:
user = {
// ...
checkPassword(value) {
//对象方法必须能读取 _password
return value === this._password;
}
}правильноuser.checkPassword()Вызов прокси-объекта будет вызванuserтак какthis(Объект перед оператором точки становитсяthis), поэтому, когда он пытается получить доступthis._passwordВремяgetХук активируется (срабатывает при чтении любого свойства) и выдает ошибку.
Поэтому мы делаем(*)для привязки контекста метода объекта к исходному объекту,target. Затем их будущие вызовы будут использоватьtargetтак какthis, не запускает никаких хуков.
Это решение обычно работает, но оно не идеально, потому что один метод может передать непроксированный объект куда-то еще, и тогда мы застрянем: где исходный объект, а где проксированный объект?
Кроме того, объект может быть подтвержден несколько раз (несколько прокси могут добавлять разные «твики» на объект), и если мы передаем развернутый объект к способу, могут быть непреднамеренные последствия.
Поэтому такой прокси нигде не должен использоваться.
Современные движки Javascript изначально поддерживают частные свойства, которые начинаются с#как префикс. Это подробно описано в главе Частные и защищенные свойства и методы. Прокси не требуется.
Однако у таких свойств есть свои проблемы. В частности, они не передаются по наследству.
Крючки «в пределах досягаемости» и «имеет»
Давайте рассмотрим еще несколько примеров.
У нас есть объект диапазона:
let range = {
start: 1,
end: 10
};мы хотим использоватьinоператора, чтобы проверить, находится ли номер вrangeв пределах диапазона.
Долженhasперехват крюкаinпередача.
has(target, property)
-
target-- целевой объект, переданный в качестве первого параметраnew Proxy -
property-- имя атрибута
Примеры следующие
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end
}
});
alert(5 in range); // true
alert(50 in range); // falseДовольно синтаксический сахар, не так ли? И это очень просто реализовать.
Функция-обертка: «применить»
Мы также можем обернуть прокси вокруг функции.
apply(target, thisArg, args)Хуки позволяют вызывать прокси как функции:
-
targetявляется целевым объектом (функции являются объектами в JavaScript) -
thisArgдаthisзначение -
argsэто список параметров
Например, давайте вспомнимdelay(f, ms)декоратор, это то, что у нас есть вВыкройка декоратора, звоните/заявказавершена в одной главе.
В этой главе мы не реализовали его с помощью прокси. передачаdelay(f, ms)Возвращает функцию, которая будетmsпереадресовывать все звонки наf.
Вот предыдущая реализация на основе функций:
Как мы уже видели, в большинстве случаев это работает. функция-оболочка(*)Выполнить вызов после тайм-аута.
Но функция-оболочка не пересылает операции чтения/записи свойств или что-то еще. После переноса свойства исходной функции недоступны, напримерname,lengthи другие:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (函数的 length 是其声明中的参数个数)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (在包装器声明中,参数个数为0)ProxyГораздо мощнее, потому что он перенаправляет все на целевой объект.
давайте использоватьProxyвместо функции-оболочки:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy 转发“获取 length” 操作到目标对象
sayHi("John"); // Hello, John! (3秒后)Результат тот же, но теперь не только вызов, но и все операции на прокси переадресовываются на исходную функцию. Так сказать Hi.length в(*)Результат (*) возвращается правильно после переноса строк.
У нас есть более «богатая» обертка.
Существуют и другие хуки: полный список находится в начале этой главы. Схема их использования аналогична приведенной выше.
Reflect
Reflectвстроенный объект, упрощающий созданиеProxy.
Предыдущие внутренние методы, такие как[[Get]],[[Set]]и т. д. являются просто спецификациями и не могут быть вызваны напрямую.
ReflectОбъекты позволяют вызывать эти внутренние методы. Его методы являются минимальными оболочками для внутренних методов.
ЭтоReflectПример того же и вызова:
| действовать |
Reflectпередача |
внутренний метод |
|---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
| … | … | … |
Например:
особенно,Reflectпозволяет нам использовать функцию (Reflect.construct,Reflect.deleteProperty, ...) выполнить операцию (new,delete, ...). Это интересная особенность, но здесь есть еще один важный момент.
для каждого может бытьProxyзахваченные внутренние методы,ReflectУ каждого есть соответствующий метод Reflect с тем же именем и параметрами, что иProxyКрючки одинаковые.
Поэтому мы можем использоватьReflectчтобы перенаправить операцию исходному объекту.
В этом примере крючокgetа такжеsetПрозрачно (как будто ни одного из них не существует) пересылать операции чтения/записи объекту с сообщением:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"здесь:
-
Reflect.getпрочитать свойство объекта -
Reflect.setЗаписать свойства объекта, успешно вернутьсяtrue, иначе возвратfalse
При этом все просто: если хук хочет перенаправить вызов на объект, он просто вызывает с теми же параметрамиReflect.<method>Будет достаточно.
В большинстве случаев мы не можем использоватьReflectсделать то же самое, например, используяReflect.get(target, prop, receiver)Свойство read можно заменить наtarget[prop]. Хотя есть и тонкие отличия.
прокси геттер
Давайте посмотрим на примере, почемуReflect.getлучше. мы также увидим, почемуget/setимеет четвертый параметрreceiver, который мы раньше не использовали.
у нас есть с_nameсвойство и объект с геттеромuser.
Вот прокси:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // GuestДолженgetХук здесь «прозрачен», он возвращает исходное свойство и больше ничего не делает. Для нашего примера этого достаточно.
Кажется, все в порядке. Но усложним пример.
другой объектadminотuserПосле наследования мы можем наблюдать неправильное поведение:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // 输出:Guest (?!?)читатьadmin.nameдолжен вернуться"Admin", вместо"Guest"!
Что случилось? Может мы что-то не так делаем с наследованием?
Но если убрать прокси то все работает как положено.
Проблема собственно в прокси, в(*)Ряд.
когда мы читаем
admin.name,из-заadminСам объект не имеет соответствующего свойства, поиск пойдет по его прототипу.Прототип
userProxy.-
читать с прокси
nameимущество,getХук срабатывает и возвращается из исходного объектаtarget[prop]свойства, в(*)Рядпри звонке
target[prop]когда, еслиpropгеттер, который будетthis=targetзапустить его код в контексте. Таким образом, результат исходного объектаtargetизthis._nameто есть изuser.
Для разрешения этой ситуации нам нужноgetТретий параметр хукаreceiver. Он гарантирует правильностьthisк геттеру. В нашем случае этоadmin.
Как передать контекст как геттер? Для рутинных функций мы можем использоватьcall/apply, но это геттер, он не "вызывается", просто к нему обращаются.
Reflect.getЭто может быть сделано. Если мы его используем, все работает нормально.
Вот исправленный вариант:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // AdminСейчасreceiver, сохраняя правильнуюthisцитаты (т.admin) ссылка, которая будет в(*)использовать в очередиReflect.getпередается геттеру.
Мы можем переписать хук, чтобы он был короче:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}ReflectВызовы именуются точно так же, как хуки, и принимают те же параметры. Они специально разработаны таким образом.
следовательно,return Reflect...Для переадресации действий предусмотрена безопасная процедура напоминания, чтобы мы не забыли ничего, связанного с этим.
Ограничения прокси
Прокси предоставляют уникальный способ изменить или настроить поведение существующих объектов на самом низком уровне. Однако это не идеально. Есть ограничения.
Встроенные объекты: внутренние слоты
Многие встроенные объекты, такие какMap, Set, Date, Promiseи т. д. все используют так называемые «внутренние слоты».
Они аналогичны свойствам, но только для внутреннего использования, только для целей спецификации. Например,MapХраните предмет в[[MapData]]середина. встроенные методы для доступа к ним напрямую, без прохождения[[Get]]/[[Set]]внутренний метод. такProxyне может быть перехвачен.
Почему вас это должно волновать? Они внутренние!
Вот в чем проблема. После проксирования встроенного объекта, такого как этот, прокси-объект не имеет этих внутренних слотов, поэтому встроенный метод не работает.
Например:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // ErrorВнутренне, аMapхранить все данные в[[MapData]]во внутреннем слоте. Прокси-объекты не имеют такого слота.встроенный методMap.prototype.setметод пытается получить доступ к внутреннему свойствуthis.[[MapData]], но из-заthis=proxyсуществуетproxyне могу найти его в , он может только потерпеть неудачу.
К счастью, есть обходной путь:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)Теперь это работает нормально, потому чтоgetХуки будут работать со свойствами (например,map.set) привязан к целевому объекту (map)сам.
В отличие от предыдущего примера,proxy.set(...)внутреннийthisЗначение неproxy, но исходный объектmap. Следовательно, когдаsetВнутренняя реализация хука пытается получить доступthis.[[MapData]]Когда внутри слота, это удается.
Arrayнет внутренних слотовОдно заметное исключение: встроенныйArrayВнутренние слоты не используются. Это по историческим причинам, потому что он появился очень давно.
Поэтому при прокси-массиве такой проблемы нет.
частное поле
То же самое происходит и с приватными полями класса.
Например,getName()доступ к методу частный#nameсвойство и перерыв после прокси:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // ErrorПричина в том, что приватные поля реализованы с использованием внутренних слотов. JavaScript обращается к ним без использования[[Get]]/[[Set]].
вызовgetName()ВремяthisЗначение проксируетсяuser, у него нет слота с закрытыми полями.
Опять же, решение для метода привязки возвращает его в нормальное состояние:
У этого решения есть недостатки, как упоминалось ранее: открытие исходного объекта для метода, что может привести к его дальнейшему прохождению и нарушению других функций прокси.
Proxy != target
Прокси и исходный объект - разные объекты. Это естественно, верно?
Итак, если мы используем исходный объект в качестве ключа, а затем проксируем его, прокси не будет найден:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // falseКак мы видим, после проксирования мы находимся вallUsersне найдено вuser, потому что прокси — это другой объект.
===
Прокси может перехватывать многие операторы, такие как new (используяconstruct), в (используяhas), удалить (используяdeleteProperty)Ждать.
Но нет возможности перехватить строгие проверки на равенство для объектов. Объект строго равен самому себе и не имеет никакой другой ценности.
Поэтому все операции и встроенные классы, которые сравнивают объекты на равенство, различают цель и прокси. Здесь нет прозрачной замены.
Отменяемый прокси
Один
Прокси — это прокси, который можно отключить.
Предположим, у нас есть ресурс, и мы хотим закрыть доступ к этому ресурсу в любое время.
Что мы можем сделать, так это обернуть его как отзываемый прокси без каких-либо хуков. Такой прокси будет перенаправлять действие на объект, который мы можем отключить в любой момент.
Синтаксис:
let {proxy, revoke} = Proxy.revocable(target, handler)Вызов возвращаетproxyа такжеrevokeобъект функции, чтобы отключить его.
Вот пример:
передачаrevoke()Все внутренние ссылки на целевые объекты удаляются из прокси, поэтому они больше не связаны. Затем целевой объект может быть удален сборщиком мусора.
Мы также можемrevokeсохранить вWeakMap, чтобы иметь возможность легко найти его через прокси-объект:
Преимущество этого подхода заключается в том, что нам не нужно нести с нами отзыва. Мы можем получить его с карты при необходимостиproxyчтобы получить это.
Здесь мы используемWeakMapвместоMap, так как это не мешает сборке мусора. Если прокси-объект становится «недоступным» (например, никакие переменные больше не ссылаются на него), тоWeakMapпозволяют сочетать его сrevokeОбъект стирается из памяти целиком, потому что он нам больше не нужен.
использованная литература
Суммировать
Proxyпредставляет собой оболочку вокруг объекта, которая перенаправляет операции с прокси-сервером на объект и, при необходимости, захватывает некоторые из этих операций.
Он может обертывать любой тип объекта, включая классы и функции.
Синтаксис:
let proxy = new Proxy(target, {
/* traps */
});... тогда мы должны использовать вездеproxyвместоtarget. Прокси не имеют собственных свойств или методов. Если указан хук, он зафиксирует действие, в противном случае оно будет перенаправлено наtargetобъект.
Мы можем захватить:
- читать(
get), записывать (set),удалять(deleteProperty) свойства (даже несуществующие). - вызов функции (
applyкрюк). -
newработать(constructкрюк). - Многие другие операции (полный список в начале статьи иdocsсередина).
Это позволяет нам создавать «виртуальные» свойства и методы, реализовывать значения по умолчанию, наблюдаемые объекты, декораторы функций и многое другое.
Мы также можем обернуть объект несколько раз в разные прокси и украсить его несколькими функциями.
ДолженReflectAPI предназначен для дополненияProxy. для любогоProxyкрючки, оба имеютReflectпередача. Мы должны использовать их для переадресации вызовов целевому объекту.
Прокси имеет некоторые ограничения:
- Встроенные объекты имеют «внутренние слоты», и доступ к этим объектам не может быть проксирован. См. обходной путь выше.
- То же самое касается полей закрытого класса, поскольку они реализуются внутри с помощью слотов. Таким образом, вызов прокси-метода должен иметь целевой объект
thisчтобы получить к ним доступ. - Тест на равенство объектов
===не может быть заблокирован. - Производительность: тесты зависят от движка, но обычно доступ к свойствам с простейшими прокси занимает в несколько раз больше времени. На практике это имеет значение только для некоторых «узких мест» объектов.
Несколько небольших примеров задач
Как правило, попытка прочитать свойство, которое не существует, возвращаетundefined.
Создайте прокси, который выдает ошибку при попытке прочитать несуществующее свойство.
Это может помочь выявить ошибки программирования на ранней стадии.
написать согласиеtargetобъект и возвращает прокси, который добавляет функциональность этого аспектаwrap(target)функция.
Должны быть удовлетворены следующие результаты:
let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
/* 你的代码 */
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // 错误:属性不存在let user = {
name: "John"
};
function wrap(target) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
} else {
throw new ReferenceError(`Property doesn't exist: "${prop}"`)
}
}
});
}
user = wrap(user);
alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't existВ некоторых языках программирования мы можем получить доступ к элементам массива, используя отрицательные индексы с конца.
так:
let array = [1, 2, 3];
array[-1]; // 3,最后一个元素
array[-2]; // 2,从末尾开始向前移动一步
array[-3]; // 1,从末尾开始向前移动两步другими словами,array[-N]а такжеarray[array.length - N]такой же.
Создайте прокси для реализации этого поведения.
Это должно выглядеть так:
let array = [1, 2, 3];
array = new Proxy(array, {
/* your code */
});
alert( array[-1] ); // 3
alert( array[-2] ); // 2
// 其他数组也应该适用于这个功能let array = [1, 2, 3];
array = new Proxy(array, {
get(target, prop, receiver) {
if (prop < 0) {
// even if we access it like arr[1]
// prop is a string, so need to convert it to number
prop = +prop + target.length;
}
return Reflect.get(target, prop, receiver);
}
});
alert(array[-1]); // 3
alert(array[-2]); // 2Создайте объект, который «делает объект наблюдаемым», возвращая проксиmakeObservable(target)функция.
Это работает следующим образом:
другими словами,makeObservableВозвращаемый объект аналогичен исходному объекту, но также имеетhandlerФункция настроена на метод, который вызывается при изменении любого свойства.observe(handler).
Всякий раз, когда свойство изменяется, оно вызывается с именем и значением свойства.handler(key, value).
P.S. В этом задании обратите внимание только на атрибут записи. Аналогичным образом могут быть реализованы и другие операции.
Решение состоит из двух частей:
- несмотря ни на что
.observe(handler)Когда он вызывается, нам всем нужно где-то запомнить обработчик, чтобы его можно было вызвать позже. Мы можем хранить обработчик непосредственно в объекте, используя Symbol в качестве ключа свойства. - нам нужен ремень
setПрокси хука для вызова обработчика при возникновении каких-либо изменений.
let handlers = Symbol('handlers');
function makeObservable(target) {
// 1. 初始化 handler 存储数组
target[handlers] = [];
// 存储 handler 函数到数组中以便于未来调用
target.observe = function(handler) {
this[handlers].push(handler);
};
// 2. 创建代理以处理更改
return new Proxy(target, {
set(target, property, value, receiver) {
let success = Reflect.set(...arguments); // 转发写入操作到目标对象
if (success) { // 如果设置属性的时候没有报错
// 调用所有 handler
target[handlers].forEach(handler => handler(property, value));
}
return success;
}
});
}
let user = {};
user = makeObservable(user);
user.observe((key, value) => {
alert(`SET ${key}=${value}`);
});
user.name = "John";