Dario Venneri

Javascript Proxy Object, some examples

Oct 8th 2023

In javascript there is a very fancy object called the Proxy object which is used to intercept operations made on selected objects. This means that if you do something like

user.name = 'David'

you can have something else to happen instead of just the assignment of the string, or maybe you can make it so nothing happens at all.

So, let's try some examples. First thing we need an object to work with, we will use this very simple object:

let user = {
    name: 'David',
    age: 17
}

then we create a Proxy and wraps user around it

let userProxy = new Proxy(user, {});

the first argument of the Proxy constructor is the target object, the second argument is the handler which is the object where we will list traps. Traps are functions that are invoked in place of the default operations on the target object.

We then can try a few things in the console:

user.name // returns 'David'

userProxy.name // also returns 'David'

userProxy.name = 'Jack'

user.name // now returns 'Jack'

as you can see the operations on the proxy called userProxy happened on the target object user. Nothing really new happened, so let's try to change the proxy object a little by adding a trap into the handler:

let userProxy = new Proxy(user, {
    get(target, prop) {
        return 'I am ' + target[prop]
    }
});

with get()we added a trap to intercept the internal method [[Get]] and we changed its behavior. Now when we try

user.name // returns 'David'

userProxy.name // returns 'I am David!'

the behavior is different on the proxy than if we were to use the object.

Reassigning the same variable

You may not like the idea of having two different object so closely connected with each other and different behaviors. You could reassign user so it contains the proxy directly

let user = {
    name: 'David',
    age: 17
}

user = new Proxy(user, {});

So now instead of having userProxy and user, we just have user. This tecnique has nothing to do with proxies themselves, but it's useful to remember.


We want to create an object called post that follows the following specification:

  1. when other programmers use this object, they can only set title and slug (this-is-a-url-slug) as properties
  2. only a properly formatted slug is accepted as a slug

To make sure the behavior matches the specification we can set trap on a proxy object. First let's see the most basic example of a set trap:

let post = new Proxy({}, {
    set(target, property, value) {
        return target[property] = value
    }
})

post.title = 'The origin of Ancient Rome'

Right now the behavior is very similar to that of a normal object. The string The origin of Ancient Rome is the value and is assigned to title. Now let's try to develop a solution that matches our specification

let post = new Proxy({}, {
    set(target, property, value) {
        if(property == 'title') {
            return target[property] = value
        } else if(property == 'slug') {
            return target[property] = value
        } else {
            throw new Error('Only "title" and "slug" are allowed as property');
        }
    }
})

post.title = 'The origin of Ancient Rome'
post.author = 'David Smith' // error

This code will throw an error when we try to set the author property. A programmer using this object will only be allowed to set title and slug. Now let's add a regular expression to make sure the slug is properly formatted.

let post = new Proxy({}, {
    set(target, property, value) {
        if(property == 'title') {
            return target[property] = value
        } else if(property == 'slug') {
            const regex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/g;
            if(!regex.test(value)) {
                throw new Error('The slug is not properly formatted');
            }
            return target[property] = value
        } else {
            throw new Error('Only "title" and "slug" are allowed as property');
        }
    }
})

post.title = 'The origin of Ancient Rome'
post.slug = 'Origin of Rome' // error

The object now follows the specification and will throw an error when the slug is not properly formatted. If we assigned origin-of-rome as a slug, we wouldn't have had any error.

As you can see proxies can be really powerful (which also means dangerous). There are many traps that can be set matching the internal method of objects and functions. A list can be found on MDN and you can be refer to it when the need arises.

Reflect

The Reflect object can be used anywhere but it goes hand in hand with proxies. Reflect will match the default behavior of a target object.

let post = {
    title: 'Geography of Japan'
}

Reflect.get(post, 'title'); // 'Geography of Japan'
post.title; // 'Geography of Japan'

Getting the property title using Reflect.get or by writing post.title has the same result, and this is useful in proxies because we overwrite the original behavior of the target object but we may still want to refer to it and Reflect let us do so in a safe way.

let post = {}

post = new Proxy(post, {
    set(target, property, value) {
        console.log('Setting the property...')
        return Reflect.set(target, property, value);
    }
});

post.title = 'Geography of Japan';

instead of assigning the property on the target by writing something like target[property] = value we have used Reflect.set().

Internal methods that are trappable by Proxy have the same method on Reflect;

Receiver

In your code editor you may have noticed that a get trap has a third argument called receiver that we ignored up until now. The receiver is the original object when the property lookup is performed.

let content = {
    _title: '7 cookies recipes',
    get title() {
        return this._title;
    }
}

console.log(content.title); // '7 cookies recipes'

content = new Proxy(content, {
    get(target, property) {
        return target[property];
    }
})

console.log(content.title); // '7 cookies recipes'

let recipe = {
    __proto__: content,
    _title: 'Spaghetti',
}

console.log(recipe.title); // '7 cookies recipes'

In this example when we try to access recipe.title we are moved to the get title() on the content object, which right now is a proxy. The this now refers to content and we get the _title in content. Instead by using Reflect.get and receiver we can get the _title in recipe.

let content = {
    _title: '7 cookies recipes',
    get title() {
        return this._title;
    }
}

console.log(content.title); // '7 cookies recipes'

content = new Proxy(content, {
    get(target, property, receiver) {
        return Reflect.get(target, property, receiver);
    }
})

let recipe = {
    __proto__ : content,
    _title: 'Spaghetti'
};

console.log(recipe.title); // 'Spaghetti'

By using the receive the original object we were trying to get the property from is remember.

Make some syntactic sugar

We have an array of animals and we want the programmers that use this array to be able to write something like 'dog' in animals and get true or false. This syntactic sugar can be accomplished with proxies and the has trap

let animals = ['lion', 'zebra', 'dog'];

animals = new Proxy(animals, {
    has(target, valueToSearch) {
        for(let element of target) {
            if(element == valueToSearch) {
                return true;
            }
        }
    }
});

console.log('lion' in animals); // true
console.log('dog' in animals); // true
console.log('cat' in animals); //false

Attention! By default the in operator behaves much differently, and what we are using as valueToSearch would normally be the key of the object. See this comparison:

let animals = ['lion', 'zebra', 'dog'];

console.log(0 in animals); // true, result of default behavior

animals = new Proxy(animals, {
    has(target, valueToSearch) {
        for(let element of target) {
            if(element == valueToSearch) {
                return true;
            }
        }
    }
});

console.log(0 in animals); // false, result of modified behavior

Be careful when using this tecnique.

Make an Observable

function makeObservable(observed) {

    let fns = [];

    observed.observe = (fn) => {
        fns.push(fn);
    }

    return new Proxy(observed, {
        set(target, property, value, receiver) {
            if(property == 'length') {
                return Reflect.set(target, property, value, receiver);
            }
            let success = Reflect.set(target, property, value, receiver);
            if(success) {
                fns.forEach(fn => {
                    fn(property, value);
                })
            }
            return success;
        }
    })
}

let animals = ['dog', 'cat', 'lion'];

animals = makeObservable(animals);

animals.observe((index, animal) => {console.log(`New animal spotted: ${animal}`)})