typescript 属性
Recently, while developing my pet project I encountered a problem where I needed to “virtualize” a property in a TypeScript class and I thought it would be interesting to share my solution.
最近,在开发我的宠物项目时,我遇到一个问题,需要“虚拟化” TypeScript类中的属性,我认为分享我的解决方案会很有趣。
问题 (The Problem)
I have a base class and a derived class. The base class defines a property — let’s call it first
— and assigns a value to it (assume that it is of an object type and not of a primitive type — we will talk about primitive types later). The base class also defines another property — let’s call it second
— which makes use of the first property. The derived class overrides the first
property and gives it a different value. Then the instance of the derived class is created and the value of the second
property is read. Here is the code snippet illustrating the situation:
我有一个基类和一个派生类。 该基类定义的属性-我们称之为first
-和赋值给它(假设它是一个对象类型,而不是一个原始类型的-我们将谈论基本类型后)。 基类还定义了另一个属性(我们称之为second
属性),它利用了第一个属性。 派生类将覆盖第first
属性,并为其赋予一个不同的值。 然后,创建派生类的实例,并读取second
属性的值。 这是说明情况的代码片段:
When I first wrote this code I was expecting the number 2 in the console — after all it is the derived object that I have created and by the time I am doing console.log
, the first
property has been assigned the value of {v:2}
. To my surprise the console showed 1 and not 2.
当我第一次编写此代码时,我期望控制台中的数字为2 —毕竟这是我创建的派生对象,并且在我执行console.log
,第first
属性的值已分配为{v:2}
。 令我惊讶的是,控制台显示1而不是2。
After a second, it became clear that the result was obviously correct and it was my thinking that was not. Since the properties are initialized as they are declared, the code is actually part of the constructor. During the construction of the Base class the value of this.first
on line 4 is {v:1}
, which is remembered in the second
property and remains unchanged when it’s printed on line 13.
一秒钟之后,很明显结果显然是正确的,这是我的想法。 由于属性是在声明时初始化的,因此代码实际上是构造函数的一部分。 在构造基类的this.first
,第4行上的this.first
的值为{v:1}
,该值在second
属性中被记住,并且在第13行上打印时保持不变。
So it works as it should (or as a colleague of mine once put it “works as implemented”), but that left me unsatisfied because I really liked the clear structure of the code with property initializers but I also really wanted the overridden value of the first
property to show up when I access the second
property.
因此它可以正常工作(或者作为我的同事曾经说过“按实现工作”),但是这让我不满意,因为我非常喜欢带有属性初始化程序的代码的清晰结构,但是我也确实想要重写的值。的first
属性显示,当我访问second
属性。
解决方案 (The Solution)
I realized that I needed to postpone access to the first
property during the assignment of its value to the second
property until the moment the second
property is read. The simplest way to postpone access to a value is to have a function that returns this value. So I wrote the following code:
我意识到我需要推迟访问first
它的价值分配到在财产second
,直到此刻属性second
属性被读取。 推迟访问值的最简单方法是拥有一个返回该值的函数。 所以我写了下面的代码:
This works but the obvious — and major — problem is that the type of the second
property ceases to be the same as the type of the first
property — it is now a function.
这是可行的,但明显的(也是主要的)问题是second
属性的类型不再与first
属性的类型相同,它现在是一个函数。
If only JavaScript had an object that would present itself as a target object, but allow custom code to be executed when accessed! Of course, JavaScript does have such an object and it is called Proxy. You create a proxy instance, giving it a target object and a handler object. The latter has methods that are invoked when the proxy instance is accessed.
如果只有JavaScript有一个对象,该对象将自己显示为目标对象,但是允许在访问时执行自定义代码! 当然,JavaScript确实有这样的对象,它称为Proxy 。 您创建一个代理实例,为其提供一个目标对象和一个处理程序对象。 后者具有在访问代理实例时调用的方法。
Using the Proxy object, the idea is that whenever the first
property is assigned a value, we will create a proxy object for this value. We will use the same instance of the handler for all these proxy objects and it will keep the latest value that was assigned to our property. When the handler is invoked in order to read the value, it will return the value it holds at the time of access and not the one that the proxy was created with.
使用Proxy对象的想法是,每当为first
属性分配一个值时,我们都将为此值创建一个代理对象。 我们将对所有这些代理对象使用同一处理程序实例,并将保留分配给我们属性的最新值。 调用处理程序以读取值时,它将返回访问时持有的值,而不是创建代理时使用的值。
So the next attempt went like this:
所以下一次尝试是这样的:
This works but the code is quite unwieldy. What if I needed another property to be “virtualized”? I would have to create another instance of the VirtHandler
class and keep it in another property. Fortunately, TypeScript has decorators that can perform exactly this kind of work behind the scenes.
这行得通,但是代码非常笨拙。 如果我需要另一个资产进行“虚拟化”怎么办? 我将不得不创建VirtHandler
类的另一个实例,并将其保留在另一个属性中。 幸运的是,TypeScript具有装饰器,这些装饰器可以在后台准确执行此类工作。
I created the @virtual
decorator and put it, along with the proxy handler, into a separate file — to show just how simple it is to make the property “virtual” once you have the infrastructure in place.
我创建了@virtual
装饰器,并将其与代理处理程序一起放入一个单独的文件中-展示了在拥有适当的基础结构之后,使属性“虚拟”多么简单。
Now, whenever we need a “virtualized” property, we need only to apply the @virtual
decorator to it — the rest of the code and the way the property is accessed remain exactly the same.
现在,只要我们需要“虚拟化”属性,就只需对其应用@virtual
装饰器-其余的代码和访问该属性的方式将完全相同。
基本类型 (Primitive Types)
I used object types in the previous discussion for two simple reasons: first, that was my initial need, and second, the above solution wouldn’t work for the primitive types: strings, numbers and booleans. In fact, if you were to try assigning a number to the first
property as in the code below, you would get the following error: TypeError: Cannot create proxy with a non-object as target or handler.
我在前面的讨论中使用对象类型有两个简单的原因:首先,这是我的最初需求,其次,上述解决方案不适用于基本类型:字符串,数字和布尔值。 实际上,如果您尝试像下面的代码中那样为第first
属性分配一个数字,则会出现以下错误: TypeError: Cannot create proxy with a non-object as target or handler.
What we can do in order to make it work is to box the primitive values before passing them to the proxy. Boxing means creating instances of the built-in classes corresponding to the primitive values: Number
for numbers, String
for strings and Boolean
for booleans. The proxy handler’s get
method will be invoked when the unboxing occurs; that is, when the object needs to be converted back to the primitive value. For example, for the Number
object, the get
method will be asked to return the valueOf
method.
为了使它起作用,我们可以做的是在将原始值传递给代理之前将它们装箱 。 装箱意味着创建与原始值相对应的内置类的实例: Number
表示数字, String
表示字符串, Boolean
表示布尔值。 拆箱发生时,将调用代理处理程序的get
方法。 也就是说,当需要将对象转换回原始值时。 例如,对于Number
对象,将要求get
方法返回valueOf
方法。
There is a caveat here, however: within the proxy handler’s get
method it is not enough to just use the expression this.v[p]
. The retrieved method should be bound to the target object first. A good explanation of why this has to be done can be found here.
但是,这里有一个警告:在代理处理程序的get
方法中,仅使用表达式this.v[p]
是不够的。 检索到的方法应首先绑定到目标对象。 关于为什么必须这样做的一个很好的解释可以在这里找到。
An extra complication arises when the virtualized property is assigned a null
or undefined
value. We will keep this value in our handler, but what would happen when the property is used? In most cases, the behavior would be correct — for example, if we try to get property value (e.g. obj.b.v
) the correct exception will be thrown.
当为虚拟化属性分配null
或undefined
值时,会产生额外的麻烦。 我们将这个值保留在处理程序中,但是使用该属性会发生什么? 在大多数情况下,行为是正确的-例如,如果我们尝试获取属性值(例如obj.bv
),则会抛出正确的异常。
There is one particular case that deserves a special attention: when our virtualized property is used in an arithmetic expression or is appended to a string. JavaScript will try to get the value of the Symbol.toPrimitive
well-known symbol and we must return a proper function so that the behavior would be the same as expected from a regular null
or undefined
value.
有一种特殊情况值得特别注意:当我们的虚拟化属性在算术表达式中使用或附加到字符串时。 JavaScript将尝试获取众所周知的Symbol.toPrimitive
符号的值,并且我们必须返回适当的函数,以使行为与常规的null
或undefined
值所期望的相同。
其他作业 (Other Operations)
So far, we were focused on a single operation on our virtual property — getting values of its properties. The handler’s get
method took care of that. But what if our virtual property is a function? What if we want to find out whether it is an instance of a particular class? If we want our property to support all other possible operations, we need to implement all methods specified in the ProxyHandler
interface. Fortunately, it is very easy to do so. JavaScript provides the Reflect
object that has methods matching those of a proxy handler, and we just need to call the corresponding methods of the Reflect
object, passing our latest value as the first parameter.
到目前为止,我们只专注于对虚拟属性的单个操作-获取其属性的值。 处理程序的get
方法解决了这一问题。 但是如果我们的虚拟属性是一个函数呢? 如果我们想确定它是否是特定类的实例怎么办? 如果希望我们的属性支持所有其他可能的操作,则需要实现ProxyHandler
接口中指定的所有方法。 幸运的是,这样做非常容易。 JavaScript提供的Reflect
对象具有与代理处理程序匹配的方法,我们只需要调用Reflect
对象的相应方法,并将我们的最新值作为第一个参数即可。
最终密码 (Final Code)
With all of the above in mind, below is the implementation of the proxy handler and the decorator that both support object types, primitive types and all proxy operations.
考虑到以上所有内容,下面是代理处理程序和装饰器的实现,它们都支持对象类型,原始类型和所有代理操作。
Note that we only create a proxy object once. When a new value is assigned to the property it is kept in the handler and since all proxy methods are intercepted, they will always work with the latest remembered value.
请注意,我们只创建一次代理对象。 将新值分配给属性后,该值将保留在处理程序中,并且由于所有代理方法均被拦截,因此它们将始终使用最新记住的值工作。
Also note that in the decorator’s get
method we check whether we have already created our handler and proxy objects and create them if not. This is necessary to handle the situation when the property is initially undefined
or null
.
还要注意,在装饰器的get
方法中,我们检查是否已经创建了处理程序和代理对象,如果没有,则创建它们。 这是处理属性最初undefined
或为null
时的必要条件。
结论 (Conclusion)
The problem I described here might not be a widespread one and it only manifests itself if you have a hierarchy of classes with property initializers. If, however, you do encounter such a structure, the described solution is one more tool in your arsenal.
我在这里描述的问题可能不是一个广泛存在的问题,它仅在您具有带有属性初始化程序的类的层次结构时才显现出来。 但是,如果确实遇到这样的结构,则所描述的解决方案是您军械库中的又一工具。
typescript 属性