Intro
Metaprogramming is the process of writing code that manipulates, generates, or analyses your application’s code. The goal is to alter the default language behavior and make your code more expressive and flexible.
In this article, we will explore metaprogramming in JavaScript and how we can use it to our advantage to write clear, efficient code.
Introspection
Let’s start with simple metaprogramming features JavaScript provides, which will probably seem familiar.
One of the most common metaprogramming tasks is introspection. Introspection is when we need to find out the type of the variable in order to apply appropriate operations to it. In JavaScript, the three most common tools for introspection are toString(),
instanceof,
and typeof.
toString()
When you call toString()
for a given object, it will return that object’s string representation. The reason for this method is to allow you to override it in your derived objects to provide a custom type conversion logic. Under the hood, JavaScript calls this method whenever it needs to convert an object to a primitive:
|
|
instanceof
instanceof
checks whether an object has a prototype of the constructor function it is comparing it to, anywhere in its prototype chain.
|
|
typeof
typeof
is a unary operator that returns the type of entity it receives.
|
|
These are some of the basic metaprogramming features JavaScript provides.
Monkey patching
Monkey patching is a type of metaprogramming. It is a technique to add, modify, or suppress the default behavior of a piece of code at runtime without changing its original source code. Let’s take a look at an example.
JavaScript’s built-in String
object provides a lot of functionality out of the box. But some niceties are not available from the String
interface alone. For example, say we want to be able to remove white spaces from strings. We can use monkey patching to augment the String
constructor:
|
|
And voila, now we can remove spaces from our strings!
|
|
A word of caution: while the example above might seem neat(we’re adding another useful function to our strings, right?), monkey patching is generally considered a dangerous practice, as it can make your code unpredictable. For the example above, a better approach would be to turn removeSpaces
into a separate utility method that you can call when needed.
With that said, just like any other programming technique, monkey patching does have its uses. Monkey patching can help fix a specific, known problem in a 3rd-party library rather than waiting for a formal release of a patch that may take too long to come around or might not come around at all if the library is no longer supported.
Function names
Another area of metaprogramming in JavaScript is inspecting function names. There are various cases when you might want to check the function name before deciding what to do with it.
The lexical name of a function is set as the name
property by default. That name
property is not writable by default, so you’ll need to use the Object.defineProperty()
method to overwrite it.
A function’s name is used in error stack traces. When a function doesn’t have a name, the browser will display a generic placeholder such as “(anonymous function)”. Stack traces become less useful when this happens, so naming functions is a good idea, especially if you’re debugging.
Well-known Symbols
Symbols are primitives created using the Symbol
constructor and are used as property keys in objects. The main advantage of using symbols is that they are guaranteed to be unique throughout the lifetime of the program, so they will never collide with other keys.
In addition to symbols that you can define in your program, the Symbol
constructor has several static fields which are symbols themselves. These particular symbols are called “well-known symbols” and are exposed to let you modify the default behavior of the JavaScript language.
Let’s cover a couple of well-known symbols in more detail.
Symbol.hasInstance
Remember how we used the instanceof
operator earlier? Well, actually, you can customize the behavior of that operator using Symbol.hasInstance
:
|
|
In the above code, we are overwriting the default behavior of the instanceof
operator for our custom MyArray
class.
Symbol.toPrimitive
Symbol.toPrimitive
is another well-known symbol in JavaScript. It specifies a method that accepts the type of object you need and returns its primitive representation. JavaScript uses this when converting objects to primitives.
|
|
Here we covered only a couple of well-known symbols. Follow this link to check out the complete list of well-known symbols available.
Proxies
A proxy is a specific type of object that wraps target objects. And instead of directly interacting with the target object, your client interacts with the proxy.
When creating a proxy, you can define specific handlers, called traps, that are triggered when various operations are performed against the target. These traps can be used to intersect a wide variety of basic operations.
Proxies provide powerful metaprogramming benefits. They let you intercept and override behavior of objects without altering them. Let’s take a look at a simple example:
|
|
In the code above, we used a proxy to change the values you get when accessing fields of the pet
object. We did this by adding a trap that overwrites the object’s get
handler.
Here’s the complete list of handlers available with proxies .
Reflect
A keen eye might notice that the behavior of our proxy above is flawed. No matter which field we try to access, we always get the hardcoded dog
string as a value. We can fix this with Reflect
.
Reflect
is a plain JavaScript object that contains static functions which match various metaprogramming tasks you can control. These functions correspond one-to-one with the handler methods (traps) that proxies can define. The primary use of Reflect
API is to forward actions called on proxy traps, and for that reason, Reflect
typically goes hand in hand with proxies.
Going back to our proxy example, we can use Reflect to make it so that we return the dog
string only when the client is trying to access the species
field:
|
|
Check out this page to get the
comprehensive list of the static fields
available via the Reflect
object.
Conclusion
In this article, we covered the main tools for metaprogramming in JavaScript.
Metaprogramming is a powerful technique for extending the capabilities of a language. However, as with any programming tool, use good judgment. Overusing metaprogramming to overwrite the default behavior of JavaScript language can result in over-engineered code that’s difficult to understand.
If you’d like to get more web development, React and TypeScript tips consider
following me on Twitter,
where I share things as I learn them.
Happy coding!