Текст длиной почти 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";