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:
const profile = {};
profile.firstName = 'Jack';
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'
.
const kickOutJacksHandler = {
set: function (target, prop, val) {
if (prop === 'firstName' && val === 'Jack') {
return false;
}
target[prop] = val;
return true;
}
}
Here our set
trap will reject any program which tries to create a profile with the first name Jack
.
const noJackProfile = new Proxy ({}, kickOutJacksHandler);
noJackProfile.firstName = 'Charles';
// console will show {} 'firstName' 'Charles'
// noJackProfile.firstName === 'Charles'
//This won't work because we don't allow firstName to equal Jack
newProfileProxy.firstName = 'Jack';
// console will show {firstName: 'Charles'} 'firstName' 'Charles'
// noJackProfile.firstName === 'Charles'
Anything that satisfies:
typeof MyThing === 'object'
This means arrays, functions, object and even…
console.log(typeof new Proxy({},{}) === 'object')
// logs 'TRUE' well actually just true... I got a bit excited...
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.
const logger = []
const loggerHandler = {
get: function (target, prop) {
logger.push(`Someone accessed '${prop}' on object ${target.name} at ${new Date()}`);
return target[prop] || target.getItem(prop) || undefined;
},
}
const secretProtectorHandler = {
deleteProperty: function (target, prop) {
// If the key we try to delete contains to substring 'secret' we don't allow the user to delete it
if (prop.includes('secret')){
return false;
}
return true;
}
};
const sensitiveDataProxy = new Proxy (
{name:'Secret JS Object', secretOne: 'I like weird JavaScript Patterns'},
{...loggerHandler, ...secretProtectorHandler}
);
const {secretOne} = sensitiveDataProxy;
//logger = ['Someone tried to accessed 'secretOne' on object Secret JS Object at Mon Dec 09 2019 23:18:54 GMT+0900 (Japan Standard Time)']
delete sensitiveDataProxy.secretOne;
// returns false it can't be deleted!
// sensitiveDataProxy equals {name: 'Secret JS Object', secretOne: 'I like weird JavaScript Patterns'}
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.
const createProxiedParameters = (reqBody, allowed) => {
return new Proxy (reqBody, {
ownKeys: function (target) {
return Object.keys(target).filter(key => allowed.includes(key))
}
});
};
const allowedKeys = ['firstName', 'lastName', 'password'];
const reqBody = {lastName:'Misteli', firstName:'Jack', password:'pwd', nefariousCode:'MWUHAHAHAHA'};
const proxiedParameters = createProxiedParameters(reqBody, allowedKeys);
const parametersKeys = Object.keys(proxiedParameters)
// parametersKeys equals ["lastName", "firstName", "password"]
const parametersValues = parametersKeys.map(key => reqBody[key]);
// parameterValues equals ['Misteli', 'Jack', 'pwd']
for (let key in proxiedParameters) {
console.log(key, proxiedParameters[key]);
}
// logs:
// lastName Misteli
// firstName Jack
// password pwd
// The trap will also work with these functions
Object.getOwnPropertyNames(proxiedParameters);
// returns ['lastName', 'firstName', 'password']
Object.getOwnPropertySymbols(proxiedParameters);
// returns []
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?
function createInArray(arr) {
return new Proxy(arr, {
has: function (target, prop) {
return target.includes(prop);
}
});
};
const myCoolArray = createInArray(['cool', 'stuff']);
console.log('cool' in myCoolArray);
// logs true
console.log('not cool' in myCoolArray);
// logs false
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.
const createCachedFunction = (func) => {
const handler = {
// cache where we store the arguments we already called and their result
cache : {},
// applu is the [[Call]] trap
apply: function (target, that, args) {
// we are assuming the function only takes one argument
const argument = args[0];
// we check if the function was already called with this argument
if (this.cache.hasOwnProperty(argument)) {
console.log('function already called with this argument!');
return this.cache[argument];
}
// if the function was never called we call it and store the result in our cache
this.cache[argument] = target(...args);
return this.cache[argument];
}
}
return new Proxy(func, handler);
};
// awesomeSlowFunction returns an awesome version of your argument
// awesomeSlowFunction resolves after 3 seconds
const awesomeSlowFunction = (arg) => {
const promise = new Promise(function(resolve, reject) {
window.setTimeout(()=>{
console.log('Slow function called');
resolve('awesome ' + arg);
}, 3000);
});
return promise;
};
const cachedFunction = createCachedFunction(awesomeSlowFunction);
const main = async () => {
const awesomeCode = await cachedFunction('code');
console.log('awesomeCode value is: ' + awesomeCode);
// After 3 seconds (the time for setTimeOut to resolve) the output will be :
// Slow function called
// awesomeCode value is: awesome code
const awesomeYou = await cachedFunction('you');
console.log('awesomeYou value is: ' + awesomeYou);
// After 6 seconds (the time for setTimeOut to resolve) the output will be :
// Slow function called
// awesomeYou value is: awesome you
// We are calling cached function with the same argument
const awesomeCode2 = await cachedFunction('code');
console.log('awesomeCode2 value is: ' + awesomeCode2);
// IMMEDIATELY after awesomeYou resolves the output will be:
// function already called with this argument!
// awesomeCode2 value is: awesome code
}
main()
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.
const handler = {
defineProperty: function (target, prop, descriptor) {
// For some reason we don't accept enumerable or writeable properties
console.log(typeof descriptor.value)
const {enumerable, writable} = descriptor
if (enumerable === true || writable === true)
return false;
// Checking if age is a number
if (prop === 'age' && typeof descriptor.value != 'number') {
return false
}
return Object.defineProperty(target, prop, descriptor);
}
};
const profile = {name: 'bob', friends:['Al']};
const profileProxied = new Proxy(profile, handler);
profileProxied.age = 30;
// Age is enumerable so profileProxied still equals {name: 'bob', friends:['Al']};
Object.defineProperty(profileProxied, 'age', {value: 23, enumerable: false, writable: false})
//We set enumerable to false so profile.age === 23
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.
const extend = (superClass, subClass) => {
const handler = {
construct: function (target, args) {
const newObject = {}
// we populate the new object with the arguments from
superClass.call(newObject, ...args);
subClass.call(newObject, ...args);
return newObject;
},
}
return new Proxy(subClass, handler);
}
const Person = function(name) {
this.name = name;
};
const Boy = extend(Person, function(name, age) {
this.age = age;
this.gender = 'M'
});
const Peter = new Boy('Peter', 13);
console.log(Peter.gender); // 'M'
console.log(Peter.name); // 'Peter'
console.log(Peter.age); // 13
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.
function createTrickOrTreatTransaction(limit) {
const extensibilityHandler = {
preventExtensions: function (target) {
target.full = true;
// this will prevent the user from even changing the existing values
return Object.freeze(target);
},
set: function (target, prop, val) {
target[prop] = val;
const candyTotal = Object.values(target).reduce((a,b) => a + b, 0) - target.limit;
if (target.limit - candyTotal <= 0) {
// if you try to cheat the system and get more that your candy allowance, we clear your bag
if (target.limit - candyTotal < 0 )
target[prop] = 0;
// Target is frozen so we can't add any more properties
this.preventExtensions(target);
}
},
isExtensible: function (target) {
// Kids can check their candy limit
console.log( Object.values(target).reduce((a,b) => a + b, 0) - target.limit);
// But it will drop their allowance by one
target.limit -= 1;
// This will return the sum of all our keys
return Reflect.isExtensible(target);
}
}
return new Proxy ({limit}, extensibilityHandler);
};
const candyTransaction = createTrickOrTreatTransaction(10);
Object.isExtensible(candyTransaction);
// console will log 10
// Now candyTransaction.limit = 9
candyTransaction.chocolate = 6;
// The candy provider got tired and decided to interrupt the negotiations early
Object.preventExtensions(candyTransaction);
// now candyTransaction equals to {limit: 9, chocolate: 6, full: true}
candyTransaction.chocolate = 20;
// candyBag equals to {limit: 9, chocolate: 6, full: true}
// Chocolates did not go change to 20 because we called freeze in the preventExtensions trap
const secondCandyTransaction = createTrickOrTreatTransaction(10);
secondCandyTransaction.reeses = 8;
secondCandyTransaction.nerds = 30;
// secondCandyTransaction equals to {limit: 10, reeses: 8, nerds: 0, full: true}
// This is because we called preventExtensions inside the set function if a kid tries to shove in extra candies
secondCandyTransaction.sourPatch = 30;
// secondCandyTransaction equals to {limit: 10, reeses: 8, nerds: 0, full: true}
Wanna see something weird?
let candies = new Proxy({}, {
// as seen above ownKeys is called once before we iterate
ownKeys(target) {
console.log('in own keys', target);
return ['reeses', 'nerds', 'sour patch'];
},
// on the other end getOwnPropertyDescriptor at every iteration
getOwnPropertyDescriptor(target, prop) {
console.log('in getOwnPropertyDescriptor', target, prop);
return {
enumerable: false,
configurable: true
};
}
});
const candiesObject = Object.keys(candies);
// console will log:
// in own keys {}
// in getOwnPropertyDescriptor {} reeses
// in getOwnPropertyDescriptor {} nerds
// in getOwnPropertyDescriptor {} sour patch
// BUT ! candies == {} and candiesObject == []
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.
const createSolidPrototype = (proto) => {
const handler = {
setPrototypeOf: function (target, props) {
target.hasBeenTampered = true;
return false;
},
getPrototypeOf: function () {
console.log('getting prototype')
},
getOwnProperty: function() {
console.log('called: ' + prop);
return { configurable: true, enumerable: true, value: 10 };
}
};
};
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.
const alphabeticalOrderer = {
enumerate: function (target) {
console.log(target, 'enumerating');
// We are filtering out any key that has a number or capital letter in it and sorting them
return Object.keys(target).filter(key=> !/\d|[A-Z]/.test(key)).sort()[Symbol.iterator]();
}
};
const languages = {
france: 'French',
Japan: 'Japanese',
'43J': '32jll',
alaska: 'American'
};
const languagesProxy = new Proxy (languages, alphabeticalOrderer);
for (var lang in languagesProxy){
console.log(lang);
}
// console outputs:
// Object { france: 'French', japan: 'Japanese', 43J: '32jll', alaska: 'American' } enumerating
// alaska
// france
// Usually it would output
// france
// Japan
// 43J
// alaska
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!