Proxies are a really cool JavaScript feature. If you like meta programming you probably are already familiar with them. In this article we are not going to get in to programming design patterns or get meta or even understand how proxies work.
Usually articles about traps always have the same examples to set private properties with proxies. It is a great example. However, here we are going to look at all the traps you can use. These examples are not meant to be real world use cases, the goal is to help you understand how Proxy
traps work.
I don’t really like the word trap. I’ve read everywhere that the word comes from the domain of operating systems (even Brendan Eich mentions it at JSConfEU 2010). However I am not exactly sure why. Maybe it’s because traps in the context of operating systems are synchronous and can interrupt the normal execution of the program.
Traps are internal method detection tools. Whenever you interact with an object, you are calling an essential internal method. Proxies allow you to intercept the execution of a given internal method.
So when you run:
You are telling your JavaScript engine to call the [[SET]] internal method. So the set
trap will call a function to execute before profile.firstName
is set to 'Jack'
.
Here our set
trap will reject any program which tries to create a profile with the first name Jack
.
Anything that satisfies:
This means arrays, functions, object and even…
PROXIES! You just can’t proxy anything if your browser doesn’t support it since there are no fully functional polyfills or transpiling options (more on that in another post).
There are 13 traps in JavaScript! I chose not to classify them, I’ll present them from what I think are the most useful to less useful (sort of). It’s not an official classification and feel free to disagree. I am not even convinced by my own ranking.
Before we get started, here is a little cheat sheet taken from the ECMAScript specification:
Internal Method | Handler Method |
---|---|
[[Get]] | get |
[[Delete]] | deleteProperty |
[[OwnPropertyKeys]] | ownKeys |
[[HasProperty]] | has |
[[Call]] | apply |
[[DefineOwnProperty]] | defineProperty |
[[GetPrototypeOf]] | getPrototypeOf |
[[SetPrototypeOf]] | setPrototypeOf |
[[IsExtensible]] | isExtensible |
[[PreventExtensions]] | preventExtensions |
[[GetOwnProperty]] | getOwnPropertyDescriptor |
[[Enumerate]] | enumerate |
[[Construct]] | construct |
We already saw set
, let’s take a look at get
and delete
. Side note: when you use set
or delete
you have to return true
or false
to tell the JavaScript engine if the key should be modified.
Let’s say we have a web server that gets some application data to our route. We want to keep that data in our controller. But maybe we want to make sure it doesn’t get misused. The ownKeys
trap will activate once when we try to access the object’s keys.
In a real application you should NOT clean your parameters like this. However, you can build a more complex system based on proxies.
Have you always dreamt of using the in
operator with arrays, but have always been too shy to ask how?
The has
trap intercepts methods which attempts to check if a property exists in an object using the in
operator.
apply
is used to intercept function calls. Here we’re going to look at a very simple caching proxy.
The createCachedFunction
takes a func
argument. The ‘cachedFunction’ has an apply
(aka [[Call]]
) trap which is called every time we run cachedFunction(arg)
. Our handler also has a cache
property which stores the arguments used to call the function and the result of the function. In the [[Call]]
/ apply
trap we check if the function was already called with that argument. If so, we return the cached result. If not we create a new entry in our cache with the cached result.
This is not a complete solution. There are a lot of pitfalls. I tried to keep it short to make it easier to understand. Our assumption is that the function input and output are a single number or string and that the proxied function always returns the same output for a given input.
This is a bit tougher to chew than the other code snippets. If you don’t understand the code try copy/pasting it in your developer console and add some console.log()
or try your own delayed functions.
defineProperty
is really similar to set
, it’s called whenever Object.defineProperty
is called, but also when you try to set a property using =
. You get some extra granularity with an additional descriptor
argument. Here we use defineProperty
like a validator. We check that new properties are not writeable or enumerable. Also we modify the defined property age
to check that the age is a number.
apply
and call are the two function traps. construct
intercepts the new
operator. I find MDN’s example on function constructor extension really cool. So I will share my simplified version of it.
Object.isExtensible
checks if we can add property to an object and Object.preventExtensions
allows us to prevent properties from being added. In this code snippet we create a trick or treat transaction. Imagine a kid going to a door, asking for treats but he doesn’t know what’s the maximum amount of candy he can get. If he asks how much he can get, the allowance will drop.
Wanna see something weird?
This is because we set enumerable as false. If you set enumerable to true
then candiesObject
will be equal to ['reeses', 'nerds', 'sour patch']
.
Not sure when this will come in handy. Not even sure when setPrototypeOf comes handy but here it goes. Here we use the setPrototype trap to check if the prototype of our object has been tampered with.
Enumerate allowed us to intercept the for...in
, but unfortunately we can’t use it since ECMAScript 2016. You can find more about that decision in this TC39 meeting note.
I tested a script on Firefox 40 just so that you don’t say I lied to you when I promised 13 traps.
You might have noticed that we don’t use `Reflect` to make things simpler. We will cover reflect
in another post. It the meantime I hope you had fun. We will also build a practical software to get a bit more hands-on next time.
table { width: 100%; } table.color-names tr th, table.color-names tr td { font-size: 1.2rem; } <p> table { border-collapse: collapse; border-spacing: 0; background: var(–bg); border: 1px solid var(–gs0); table-layout: auto; margin: 0 auto } table thead { background: var(–bg3) } table thead tr th { padding: .5rem .625rem .625rem; font-size: 1.625rem; font-weight: 700; color: var(–text-color) } table tr td, table tr th { padding: .5625rem .625rem; font-size: 1.5rem; color: var(–text-color); text-align: center } table tr:nth-of-type(even) { background: var(–bg3) } table tbody tr td, table tbody tr th, table thead tr th, table tr td { display: table-cell; line-height: 2.8125rem }
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!