神刀安全网

ECMAScript Proposal for JavaScript Decorators(and protocols)

Decorator Fundamentals

JavaScript decorators apply to classes and any statically-defined properties, such as those found on an object literal declaration or in a class declaration – even if they were static , or accessors like get and set . The proposal for JavaScript decorators defines a decorator as:

  • an expression (that’s preceded by an @ sign)
  • that evaluates to a function
  • that takes the target , name , and decorator descriptor as arguments
  • and optionally returns a decorator descriptor to install on the target object

To better understand decorators, it may be better to look at an example using a plain JavaScript object first.

const dog = {   name: 'Doug',   legs: 4 }; 

Typically, we would think of the dog object literal declaration above as an atom: when the assignment statement is executed, the object literal is assigned to dog . In order to better understand decorators, though, it may be more useful to think of the object literal as follows, using Object.defineProperties instead. The following piece of code is functionally equivalent to the previous one, albeit more verbose.

const literal = {}; const dog = Object.defineProperties(literal, {   name: {     value: 'Doug',     writable: true,     enumerable: true,     configurable: true   },   legs: {     value: 4,     writable: true,     enumerable: true,     configurable: true   } }); 

Properties are writable, enumerable, and configurable by default.

Great. Let’s now imagine that – for some reason – we want to add a decorator to the amount of legs so that it’s no longer writable . Our decorator is declared as @readonly . This must precede the property declaration for legs , as that’s the property declaration we want to modify. The @ is mandatory, and readonly should be an arbitrary JavaScript expression that evaluates to a function.

const dog = {   name: 'Doug',   @readonly   legs: 4 }; 

Let’s start by looking at how our could would look like using Object.defineProperties , now that we have a decorator. We moved the legs property descriptor into a variable, we’re calling the readonly decorator by passing in the literal variable that will be eventually assigned to dog , the 'legs' property name, and the original legsDescriptor property descriptor. The legs property will be defined using either the return value from the readonly decorator, or a reference to the original property descriptor.

const literal = {}; const legsDescriptor = {   value: 4,   writable: true,   enumerable: true,   configurable: true }; const dog = Object.defineProperties(literal, {   name: {     value: 'Doug',     writable: true,     enumerable: true,     configurable: true   },   legs: readonly(literal, 'legs', legsDescriptor) || legsDescriptor }); 

Understanding how to write the readonly decorator so that the legs property becomes read-only should be trivial, and is demonstrated below: we just modify the original descriptor so that the property isn’t writable .

function readonly (target, prop, descriptor) {   descriptor.writable = false; } 

Given that the original descriptor is used if nothing is returned, we didn’t necessarily need to return a value.

Stacking Decorators and a Warning about Immutability

With all the fluff around immutability you may be tempted to return a new property descriptor, as to not modify the original descriptor. While well-intentioned, this may have an undesired effect . It is possible to decorate the same property or class several times.

If any of the decorators in the following piece of code returned an entirely new descriptor without taking into account the descriptor supplied to the decorator function, you’d effectively lose all the decoration that took place before returning a different descriptor.

const dog = {   @readonly   @nonenumerable   @doubledValue   legs: 4 }; 

Thus, we should be careful to write decorators that take into account the supplied descriptor : modify it directly or create one that’s based on the descriptor that’s provided to you.

A Little Back Story: RunUO and Attributes in C#

A long time ago, in a galaxy far far away, I was getting acquainted with C# – without even knowing – by way of a Ultima Online server emulator written in open-source C# code: RunUO . RunUO was one of the most beautiful codebases I’ve ever worked with, and it was written in C# to boot.

They distributed the server software as an executable and a series of .cs files. The runuo executable would compile those .cs scripts at runtime and dynamically mix them into the application. The result was that you didn’t need the Visual Studio IDE (nor msbuild ) , or really anything other than knowing just enough programming to edit one of the “scripts” written in .cs files. All of the above made RunUO a perfect learning environment for me.

I didn’t know C# at the time. I didn’t even know C# was a heavily used enterprise language. I just thought it was a beautiful language and a beautifully written codebase for the server-side of an MMORPG game I loved, so saying I was eager to learn more would be an understatement.

RunUO relied heavily in reflection. They made a significant effort to be customizable by players who were interested in changing a few details of the game (such as how much damage a Dragon’s fire breath inflicts) , but not necessarily invested in programming itself. Good UX was a big part of their philosophy, and so you could create a new kind of Dragon just by copying one of the monster files, changing it to inherit from the Dragon class, and modifying a few properties to change its color, its damage output, etc.

Just as they made it easy to create new monsters, – or “non-player characters” (NPC) in gaming slang – they also relied in reflection to provide functionality to in-game administrators (“Game Masters”) . Game masters could run an in-game command and click on an item or a moster to visualize or change properties without ever leaving the game. Again, great UX.

ECMAScript Proposal for JavaScript Decorators(and protocols)

I used to go by “Kenko” and participate on the RunUO community. I guess this was one of my first adventures into the world of open-source, back around 2006 .

Not every property in a class is meant to be accessible in-game, though. The consequences of doing something like that could be catastrophic if unforeseen. RunUO had a CommandPropertyAttribute decorator where you could specify the access level required to read and write properties. This decorator was used extensively throughout the RunUO codebase .

The PlayerMobile class, which governed how a player’s character works, is a great place to look at these attributes. PlayerMobile have several properties that are accessible in-game to administrators and moderators. Here are a couple of getters and setters, but only the first one has the CommandProperty attribute – making it in-game accessible to Game Masters.

[CommandProperty( AccessLevel.GameMaster )] public int Profession {   get{ return m_Profession; }   set{ m_Profession = value; } }  public int StepsTaken {   get{ return m_StepsTaken; }   set{ m_StepsTaken = value; } } 

One interesting difference between C# attributes and JavaScript decorators is that reflection in C# allows us to pull all custom attributes from an object using MemberInfo#getCustomAttributes . RunUO leverages that method to pull up information about each property that should be accessible in-game when displaying the dialog that lets an administrator view or modify an in-game object’s properties.

Marking “special” properties in JavaScript – or, defining protocols

In JavaScript, there’s no such thing – in this proposal draft, at least – to get the custom attributes on a property. That said, JavaScript is a highly dynamic language, and creating this sort of “labels” wouldn’t be much of a hassle. Decorating a dog with a “command property” wouldn’t be all that different from RunUO and C#.

var dog = {   @commandProperty('gm')   legs: 4 }; 

The commandProperty function would have to be a little more sophisticated. Given that there is no reflection around decorators yet, we could use a runtime-wide symbol to define a Map of special properties found on target . In the example shown above, target would be the dog object. Note that we aren’t even touching the descriptor (nor returning a different one) . This is okay because we’re more concerned withthe protocol we’ve established than with implementation details for this specific property.

function commandProperty (read, write) {   return (target, prop, descriptor) => {     const commandProperties = Symbol.for('commandProperties');     if (!target[commandProperties]) {       target[commandProperties] = new Map();     }     target[commandProperties].set(prop, {       readLevel: read,       writeLevel: write || read     });   }; } 

The dog object could have as many command properties as neccessary, and each would be properly mapped behind a symbol property. To find out which special command properties any given object has, all we’d have to do is use the following one-liner, spreading all special properties as [key, value] pairs into an array.

[...target[Symbol.for('commandProperties')] || []] // <- [['legs', { readLevel: 'gm', writeLevel: 'gm' }]] 

You could then iterate over these special properties that are known to be changeable by a user who may not be as computer savvy. They may just want to change the amount of legs one given dog has. Instead of maintaining long lists of properties that can be modified, relying on some sort of heuristics bound to break from time to time, or using some sort of restrictive naming convention, decorators are the cleanliest way to implement a protocol such as this where we mark properties as special for some particular use case.

Granted, the pattern doesn’t translate perfectly into JavaScript. Being a dynamic language, properties defined outside of an object literal would have to rely on a different methodology to mark them as special. I’ll leave you to do the thinking for that one!

ECMAScript Decorators Moving Forward

There are still moving parts in this proposal. For that reason, an “official” transpiler for decorators isn’t available in Babel 6. You can learn more about the thought process and current state of decorators in Babel’s core modules on Phabricator – Babel’s issue tracker.

Instead, you’ll have to rely on babel-plugin-transform-decorators-legacy , which mostly replicates the behavior in Babel 5 – back when Babel shipped with a decorator-transpiling feature.

I spoke briefly with Brian Terlson – editor for the ECMAScript standard – about what may be changing in the decorator specification in the future. He mentioned the decorator API may change in the future , and that there may be changes in the execution model. In other words, what may change boils down to: the parameters you receive and order of operations.

The specification is, however, unlikely to change fundamentally. The essence of ECMAScript decorators will, in all likelihood, not change from what we’ve discussed in the article.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » ECMAScript Proposal for JavaScript Decorators(and protocols)

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮