Proxy and Reflect
JavaScript Proxy and Reflect (JavaScript代理和反射)
Proxies are commonly applied in various libraries and several browser frameworks. (代理通常应用于各种库和几个浏览器框架。)
A Proxy object is used for wrapping another object and intercepting operations such as writing and reading properties, dealing with them, or transparently making the object to handle them. (代理对象用于包装另一个对象并拦截操作,例如写入和读取属性、处理它们或透明地使对象处理它们。)
Proxy
Proxy (代理人)
The first thing to know is the syntax of proxy, which looks as follows:
let proxy = new Proxy(target, handler)
This syntax includes a target and a handler. The target is the object that is going to be wrapped. It can be functions, and so on. The handler is the configuration of the proxy. (此语法包括目标和处理程序。目标是要被包装的对象。可以是函数,等等。处理程序是代理的配置。)
In case there is a trap in the handler, then it runs, and the proxy can handle it. In another way, the operation will be implemented on the target. (如果处理程序中存在陷阱,则它将运行,代理可以处理它。换句话说,操作将在目标上实现。)
As a starting point, let’s consider an example of a proxy without traps:
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 7; // writing proxy (1)
console.log(target.test); // 7, property appeared in the target
console.log(proxy.test); // 7, we can read this from a proxy (2)
for (let key in proxy) console.log(key); // test, iteration works (3)
In case there isn’t any trap, the operations are passed to the target. It is necessary to add traps for activating more capabilities. (如果没有任何陷阱,操作将传递给目标。 有必要添加陷阱以激活更多功能。)
In JavaScript, there is a so-called “internal method” for most object operations, describing how it works at the lowest level. For example, the [[Get]] method is used for reading a property, [[Set]] - for writing a property. Anyway, such kinds of methods can not be called by name. (在JavaScript中,大多数对象操作都有一个所谓的“内部方法” ,用于描述它在最底层的工作原理。例如, [[Get]]方法用于读取属性[[Set]] -用于写入属性。无论如何,此类方法不能被命名。)
Further, we will cover operations with some of the methods and traps. JavaScript imposes invariants: those are conditions that must be carried out by the internal methods and traps.
For example,[[Set]] , [[Delete]] , and others return values. In the case of using [[Delete]], it should return true once the value was deleted, and, otherwise, it will return false. (例如, [[Set]]、[[Delete]]和其他返回值。 如果使用[[Delete]] ,则在删除值后应返回true ,否则将返回false。)
Other types of variants also exist. For example, when [[GetPrototypeOf]] is inserted to the proxy object, it must return the same value as [[GetPrototypeOf]], attached to the target object of the proxy. (还有其他类型的变体。例如,当[[GetPrototypeOf]]插入到代理对象时,它必须返回与附加到代理目标对象的[[GetPrototypeOf]]相同的值。)
Default Value with “get” Trap
Default Value with “get” Trap (带有“GET”陷阱的默认值)
The most frequently used traps are for writing and reading properties. For intercepting reading, the handler should include a get method. It occurs once a property is read, along with the target (the target object), property (the property name), and the receiver (if the target property is a getter, then the object is the receiver). (最常用的陷阱用于写入和读取属性。 为了拦截读取,处理程序应包含一个GET方法。 一旦读取属性以及目标(目标对象)、属性(属性名称)和接收器(如果目标属性是getter ,则对象是接收器) ,就会发生这种情况。)
For using get to perform default values for an object, it is necessary to make a numeric array, which returns 0 for nonexistent values. (要使用GET为对象执行默认值,必须创建一个数值数组,对于不存在的值返回0。)
As a rule, trying to get a non-existing array item leads to undefined. However, you can wrap a regular array into the proxy that returns 0, like here:
let numbers = [11, 22, 33];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0;
}
}
});
console.log(numbers[1]); // 22
console.log(numbers[12]); // 0, no such item
Validation with “set” Trap
Validation with “set” Trap (使用“设置”陷阱进行验证)
Imagine that you need to get an array numbers. Adding another value type will cause an error. (假设您需要获取一个数组编号。添加其他值类型将导致错误。)
The set trap occurs when a property is written. It includes target, property, value, and receiver. (写入属性时会发生set陷阱。它包括目标、属性、值和接收者。)
So, the set trap returns true when the setting is done successfully, and false, otherwise. (因此,当设置成功完成时, set trap返回true ,否则返回false。)
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, value) { // intercept property record
if (typeof value == 'number') {
target[prop] = value;
return true;
} else {
return false;
}
}
});
numbers.push(11); // added successfully
numbers.push(22); // added successfully
console.log("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ,'set' on proxy returns false
console.log("Error in the line above,this line is never reached ");
Also consider that the built-in functionality of arrays still works. With push, it is possible to add values. (还要考虑数组的内置功能仍然有效。通过推送,可以添加值。)
Value-adding methods such as push and unshift shouldn’t be overridden. (不应覆盖push和unshift等增值方法。)
Protected Properties with “deleteProperty” and Other Traps
Protected Properties with “deleteProperty” and Other Traps (带有“deleteProperty”和其他陷阱的受保护属性)
It is known that properties and methods that are prefixed by an underscore _ are considered internal. It is not possible to access them outside the object. (众所周知,以下划线_为前缀的属性和方法被视为内部属性。无法在对象外部访问它们。)
Let’s see how it is possible technically:
let user = {
name: "Jack",
_userPassword: "secret"
};
console.log(user._userPassword); // secret
So, proxies are used for preventing any access to the properties that begin with _. (因此,代理用于阻止对以_开头的属性的任何访问。)
The necessary traps are as follows:
get - for throwing an error while reading a property like that. (- get -用于在读取此类属性时抛出错误。)
set - for throwing an error while writing. (-设置-用于在写入时抛出错误。)
deleteProperty - for throwing an error while deleting. (- deleteProperty -用于在删除时抛出错误。)
ownKeys - for excluding properties that begin with _ from for..in and methods such as Object.keys. (- ownKeys -用于排除以_from for.. in开头的属性和Object.keys等方法。)
The code will look like this:
console.log(1 / 0); // Infinity
let user = {
userName: "Jack",
_userPassword: "***"
};
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, value) { // to intercept property writing
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = value;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // to intercept property list
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" doesn't allow to read _userPassword
try {
console.log(user._userPassword); // Error: Access denied
} catch (e) {
console.log(e.message);
}
// "set" doesn't allow to write _userPassword
try {
user._userPassword = "test"; // Error: Access denied
} catch (e) {
console.log(e.message);
}
// "deleteProperty" doesn't allow to delete _userPassword
try {
delete user._userPassword; // Error: Access denied
} catch (e) {
console.log(e.message);
}
// "ownKeys" filters out _userPassword
for (let key in user) console.log(key); // userName
In the line (*), there is an important detail:
get(target, prop) {
// ...
(//...)
let val = target[prop];
return (typeof val === 'function') ? val.bind(target) : val; // (*)
}
So, a function is necessary to call value.bind(target) because object methods like user.checkPassword() should be able to access _password, like this:
user = {
// ...
(//...)
checkPassword(val) {
// object method should be able to read _userPassword
(//object方法应该能够读取_userPassword)
return val === this._userPassword;
}
}
Wrapping Functions: “apply”
Wrapping Functions: “apply”
It is possible to wrap a proxy around a function, too. (也可以将代理包裹在函数周围。)
The oapply(target, thisArg, args) trap can handle calling a proxy as a function. It includes target (the target object), thisArg (the value of this), and args (a list of arguments). (Oapply (target, thisArg, args)陷阱可以作为函数处理对代理的调用。它包括target (目标对象)、thisArg ( this的值)和args (参数列表)。)
For instance, imagine recalling the delay(fn, ms) decorator. It returns a function, which forwards all the calls to fn after ms milliseconds. (例如,想象一下调用延迟(fn, ms)修饰符。它返回一个函数,该函数在ms毫秒后将所有调用转发到fn。)
The function-based performance will look like this:
function delay(fn, ms) {
// return wrapper that passes fn call after timeout
(//超时后传递fn调用的返回包装器)
return function () { // (*)
setTimeout(() => fn.apply(this, arguments), ms);
};
}
function sayWelcome(siteName) {
console.log(`Welcome to ${siteName}!`);
}
// after this wrapping, sayWelcome calls will be delayed for 2 seconds
sayWelcome = delay(sayWelcome, 2000);
sayWelcome("w3cdoc"); // Welcome to w3cdoc (after 2 seconds)
It is noticeable that the wrapper function (*) makes the call after a timeout. But the wrapper function isn’t capable of forwarding write and read operations, and so on. And, when the wrapping is implemented, the access to the properties will be lost:
function delay(f, ms) {
return function () {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayWelcome(siteName) {
console.log(`Welcome to ${siteName}!`);
}
console.log(sayWelcome.length); // 1 (function length is the count of arguments in the declaration)
sayWelcome = delay(sayWelcome, 2000);
console.log(sayWelcome.length); // 0 (wrapper declaration has zero arguments)
Luckily, the proxy can forward anything to the target object. So, it is recommended to use proxy rather than the wrapping function. Here is an example of using the proxy:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayWelcome(siteName) {
console.log(`Welcome to ${siteName}!`);
}
sayWelcome = delay(sayWelcome, 2000);
console.log(sayWelcome.length); // 1 (*) proxy forwarded "get length" operation to target
sayWelcome("w3cdoc"); // Welcome to w3cdoc (after 2 seconds)
Reflect
Reflect (思考)
Reflect is the term used for specifying a built-in object, which simplifies the proxy creation. (Reflect是用于指定内置对象的术语,它简化了代理创建。)
As it was noted above, the internal methods such as [[Set]],[[Get]] , and others can’t be called directly. Reflect can be used for making it possible. (如上所述,不能直接调用[[Set]]、[[Get]]等内部方法。反思可以用来使它成为可能。)
Let’s see an example of using reflect:
let site = {};
Reflect.set(site, 'siteName', 'w3cdoc');
console.log(site.siteName); //
For each internal method, that is trapped by proxy, there is a matching method in reflect. It has the same name and arguments as the proxy trap. (对于每个被代理捕获的内部方法,在reflect中有一个匹配方法。它与代理陷阱具有相同的名称和参数。)
So, reflect is used for forwarding an operation to the original object. (因此, reflect用于将操作转发到原始对象。)
Let’s check out another example:
let site = {
name: "w3cdoc",
};
site = new Proxy(site, {
get(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
console.log(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = site.name; // shows "GET name"
site.name = "w3cdoc"; // shows "SET name=w3cdoc"
In this example, an object property is read by Reflect.get. Also, Reflect.set writes an object property, returning true if successful, and false, otherwise. (在此示例中,对象属性由Reflect.get读取。此外, Reflect.set会写入一个对象属性,如果成功则返回true ,否则返回false。)
Proxy Limitations
Proxy Limitations (代理限制)
Of course, proxies are a unique and handy way for altering or tweaking the behavior of the existing objects at the lowest level. But, it can’t handle anything and has limitations. Below, we will look through the main proxy limitations. (当然,代理是在最低级别改变或调整现有对象行为的一种独特而方便的方式。但是,它无法处理任何事情,并且有局限性。下面,我们将了解主要的代理限制。)
Built-in objects: internal slots
Most of the built-in objects such as Set, Map, Date, and so on work with internal slots. (大多数内置对象(如Set、Map、Date等)使用内部插槽。)
Those are similar to properties but reserved for internal purposes. For instance, Map embraces items in the internal slot [[MapData]] . The built-in methods are capable of accessing them directly and not through the [[Get]]/[[Set]] methods. So, it can’t be intercepted by the proxy. (这些与属性相似,但仅供内部使用。例如, Map包含内部槽[[MapData]]中的项目。内置方法能够直接访问它们,而不是通过[[Get]]/[[Set]]方法。因此,它无法被代理拦截。)
An essential issue here is that after a built-in object gets proxied, the proxy doesn’t include the slots, so an error occurs, like this:
let map = new Map();
let proxy = new Proxy(map, {});
console.log(proxy.set('testArg', 1)); // Error
Let’s see another case. A Map encloses all the data in the [[MapData]] internal slot. Then, the Map.prototype.set method attempts to access the internal this.[[MapData]]. As a result, it will not be found in the proxy. (让我们看看另一个案例。Map包含[[MapData]]内部插槽中的所有数据。然后, Map.prototype.set方法尝试访问内部this。[[MapData]]。因此,它将不会在代理中找到。)
But there is a way of fixing it:
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', 10);
console.log(proxy.get('test')); // 10
Please, take into consideration a notable exception: the built-in Array never uses internal slots.
Private Fields
Something similar takes place with private fields. For instance, the getName() method can access the #name and break after proxying, like this:
class Site {
#name = "w3cdoc";
getName() {
return this.#name;
}
}
let site = new Site();
site = new Proxy(site, {});
console.log(site.getName()); // Error
That happens because private fields are performed by applying internal slots. While accessing them, JavaScript doesn’t use [[Get]]/[[Set]] . (之所以会发生这种情况,是因为专用字段是通过应用内部插槽来执行的。在访问它们时, JavaScript不使用[[Get]]/[[Set]]。)
Calling getName() the proxied user is the value of this . It doesn’t have the slot with private fields. (调用getName ()代理用户是this的值。它没有带有专用字段的插槽。)
Let’s have a look at the solution, which will make it work:
class Site {
#name = "w3cdoc";
getName() {
return this.#name;
}
}
let site = new Site();
site = new Proxy(site, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
console.log(site.getName()); // w3cdoc
Proxy != target
The original object and the proxy are considered different objects. (原始对象和代理被视为不同的对象。)
Having the original object as a key, proxying it, then it won’t be found, like here:
let allBooks = new Set();
class Book {
constructor(name) {
this.name = name;
allBooks.add(this);
}
}
let book = new Book("Javascript");
console.log(allBooks.has(book)); // true
book = new Proxy(book, {});
console.log(allBooks.has(book)); // false
It is noticeable that the user can’t be found in the allUsers after proxying. That’s because it’s a different object. (值得注意的是,代理后无法在所有用户中找到用户。那是因为它是一个不同的物体。)
Please, also note that proxies don’t intercept various operators like new, delete and more. All the operations and built-in classes comparing objects for equality, differentiate between the proxy and the object. (另请注意,代理不会拦截各种运算符,如new、delete等。比较对象是否相等的所有操作和内置类,区分代理和对象。)
Revocable Proxies
Revocable Proxies (可撤销代理)
A proxy that can be disabled is called a revocable proxy. (可以禁用的代理称为可撤销代理。)
Imagine having a resource and want to close access to it at any time. It is necessary to wrap it to a revocable proxy without using traps. A proxy like that will forward operations to object. It can be disabled at any moment. (想象一下,拥有一个资源并希望随时关闭对其的访问。 有必要在不使用陷阱的情况下将其包装到可撤销的代理中。 这样的代理会将操作转发给对象。 它可以随时禁用。)
The syntax is the following:
let {
proxy,
(n.代表 ,代表权,代表人,代理,代理权,代理人,委任代理人)
revoke
} = Proxy.revocable(target, handler)
Now, let’s see an example of the call returning an object with the proxy and revoke function for disabling it:
let object = {
data: "Valued data"
};
let {
proxy,
(n.代表 ,代表权,代表人,代理,代理权,代理人,委任代理人)
revoke
} = Proxy.revocable(object, {});
// pass proxy somewhere instead of an object
console.log(proxy.data); // Valued data
// later
revoke();
// the proxy doesn't work (revoked)
console.log(proxy.data); // Error
Calling revoke() deletes all the internal references to the target object. So, there is not connection between them. (调用revoke ()会删除对目标对象的所有内部引用。因此,它们之间没有联系。)
Also, revoke can be stored in WeakMap. That makes it easier to detect it using a proxy object, like this:
let revokes = new WeakMap();
let object = {
data: "Valued data"
};
let {
proxy,
(n.代表 ,代表权,代表人,代理,代理权,代理人,委任代理人)
revoke
} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// later
revoke = revokes.get(proxy);
revoke();
console.log(proxy.data); // Error (revoked)
The primary benefit here is that it’s not necessary to carry revoke anymore. To sum up, we can state that a Proxy is a wrapper, surrounding an object, which forwards operations on it to the object. Some of them can optionally by trapped. Any type of object can be wrapped, including functions and classes. (这里的主要好处是,不再需要进行撤销。 总而言之,我们可以说代理是一个包装器,围绕着一个对象,它将对它的操作转发到对象。 他们中的一些人可以选择陷入困境。 可以包装任何类型的对象,包括函数和类。)
Proxy doesn’t include own methods and properties. It will trap the operation when there is a trap. Otherwise, it will be forwarded to target the object. (代理不包括自己的方法和属性。当有陷阱时,它将陷阱操作。否则,它将被转发到目标对象。)
To complement proxy, an API, called reflect is created. For each proxy trap, you can find a corresponding reflect, using the same arguments. (为了补充代理,创建了一个名为reflect的API。对于每个代理陷阱,您可以使用相同的参数找到相应的反射。)