Javascript Proxy: Using Javascript Proxies like a Pro
Tags: javascript, typescript, webdev, tutorial
Published: 2022-10-22 13:56:00

Proxies are objects in Javascript which allows you to make a proxy of an object, while also defining custom behaviour for standard object operations like `get`, `set` and `has`. What that means is that, for example, you can define a set of custom behaviour should someone try to get the value of a property from an object. This turns proxies into quite a powerful tool, so lets look at how they works.

## The basics of Javascript Proxies

The above sounds quite complicated, so lets look at a simple example without any methods, to begin with. Proxies can be created using the `new Proxy()` constructor, which accepts two arguments:
- the `target`, which is the original object.
- the `handler`, which is the set of methods or properties we will add on top of our object.

The `handler` can contain a list of predefined methods. If we define a method for `get`, for example, it will customise what happens when we try to `get` an item from an object.

```javascript
let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    get: (object, prop) => {
        console.log(`Hi ${object.firstName} ${object.lastName}`)
    }
}

let proxyExample = new Proxy(target, handler);

proxyExample.age; // console logs "Hi John Doe"
```

Since we tried to `get` the value of `proxyExample.age` on our proxy, the custom `get` handler fired - so we console logged `Hi ${object.firstName} ${object.lastName}`. As you can see, this can become quite a powerful tool, as you can do all sorts of stuff when standard operations of an object are called.

Notice that when we added `get` to the `handler` above, we had some custom arguments. Each handler you can add to a proxy comes with a set of custom arguments.

For `get` the function used is `get(object, prop, receiver)`:
- `object` - the original object. In the example above, this is the object containing `firstName`, `lastName` and `age`
- `prop` - the property that someone is trying to `get`. In the example above, `age`. 
- `reciever` - the proxy itself.

!![Handler methods are called "traps"]({
    Handler methods in a proxy are typically called traps, so don't be confused if you see this term thrown around. They are called this way because they "trap" object operations and perform some kind of custom code. For example - console logging something everytime someone tries to get a property of a proxy.
})

## Updating Proxy Values
Proxies still refer to the original object, so the reference is the same for both the object values and the proxy values. As such, if you try to update the value of a proxy, it will also update the value of the original object. For example, below I try to update the proxy and as you can see, both the original object and the proxy are updated:
```javascript
let target = {
    name: "John",
    age: 152
}

let handler = {
}

let proxyExample = new Proxy(target, handler);
proxyExample.name = "Dave";

console.log(proxyExample.name); // Console logs Dave
console.log(target.name); // Console logs Dave
```

This is useful to know - don't expect that a proxy will create a separate object completely - it is **not** a way to make copies of objects.


## Custom Handlers in Javascript Proxies
Proxies have a number of custom handlers allowing us to basically "trap" any object operation and do something interesting with it. The most commonly used methods are:

- `proxy.apply(objects, thisObject, argList)` - a method to trap the function call. 
- `proxy.construct(object, argList, newTarget)` - a method to trap when a function is called with the `new` constructor keyword.
- `proxy.defineProperty(object, prop, descriptor)` - a method to trap when a new property is added to an object using `Object.defineProperty`.
- `proxy.deleteProperty(object, prop)` - a method to trap when a property is deleted from an object.
- `proxy.get(object, prop, receiver)` - as described before, a method to trap when someone tries to `get` a property from an object.
- `proxy.set(object, prop, value, receiver)` - a method to trap when a property is given a value.
- `proxy.has(object, prop)` - a method to trap the `in` operator.

The methods above are enough to do pretty much everything you ever want to do with proxies. They give you pretty good coverage of all major object operations, to modify and customise as you like.

There are a few more though - so as well as these pretty fundamental object operations, we also have access to:

- `proxy.getPrototypeOf(object)` - a method to trap the `Object.getPrototypeOf` method.
- `proxy.getOwnPropertyDescriptor(object, prop)` - a method to trap the `getOwnPropertyDescriptor`, which returns a descriptor of a specific property - for example, is it enumerable, etc.
- `proxy.isExtensible(object)` - a method to trap when `Object.isExtensible()` is fired.
- `proxy.preventExtensions(object)` - a method to trap when `Object.preventExtensions()` is fired.
- `proxy.setPrototypeOf(object, prototype)` - a method to trap when `Object.setPrototypeOf()` is fired.
- `proxy.ownKeys(object)` - a method to trap when methods like `Object.getOwnPropertyNames()`is fired.

Let's look at some of these in a bit more detail to understand how proxies work.

### Using the in operator with Proxies

We have already covered `proxy.get()`, so lets look at `has()`. This fires primarily when we use the `in` operator. For example, if we wanted to console log the fact that a property does not exist when `in` is used, we could do something like this:
```javascript
let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    has: (object, prop) => {
        if(object[prop] === undefined) {
            console.log('Property not found');
        }
        return object[prop]
    }
}

let proxyExample = new Proxy(target, handler);

console.log('address' in proxyExample); 
// console logs 
// 'Property not found' 
// false
```

Since `address` is not defined in `target` (and thus in `proxyExample`), trying to console log `'address' in proxyExample` will return false - but it will also console log `'Property not found'`, as we defined that in our proxy.

### Setting values with proxies
A similarly useful method you may want to modify is `set()`. Below, I use the custom `set` handler to modify what happens when we try to change a user's age. For every set operation, if the property is a number, then we'll console log the difference when the number is updated.
```javascript
let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    set: (object, prop, value) => {
        if(typeof object[prop] === "number" && typeof value === "number") {
            console.log(`Change in number was ${value - object[prop]}`);
        }
        return object[prop]
    }
}

let proxyExample = new Proxy(target, handler);

proxyExample['age'] = 204;
// Console logs 
// Change in number was 52
```

Since both `proxyExample.age` and the updated value `204` are numbers, not only do we update our value to `204`, but we also get a useful console log telling us what the difference between the two numbers is. Pretty cool, right?

While `set` will fire for any set operation, including adding new items to an object, you can also achieve similar behaviour with `defineProperty`. For example, this will also work:
```javascript
let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    defineProperty: (object, prop, descriptor) => {
        console.log(`A property was set - ${prop}`);
    },
}

let proxyExample = new Proxy(target, handler);

proxyExample['age'] = "123 Fake Street";
// Console logs
// A property was set - address
```

However please note that should you add `set` and `defineProperty` both as handlers, `set` will override `defineProperty` in situations where we set properties using square bracket `[]` or `.` notation. `defineProperty` will still fire if you use `Object.defineProperty` explicitly, though, as shown below:
```javascript
let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    defineProperty: (object, prop, descriptor) => {
        console.log(`A property was set with defineProperty - ${prop}`);
        return true;
    },
    set: (object, prop, descriptor) => {
        console.log(`A property was set - ${prop}`);
        return true;
    },
}

let proxyExample = new Proxy(target, handler);

Object.defineProperty(proxyExample, 'socialMedia', {
    value: 'twitter',
    writable: false
});
proxyExample['age'] = "123 Fake Street";
// Console logs
// A property was set with defineProperty - socialMedia
// A property was set - address
```
### Deleting values with proxies

As well as these useful methods, we can also use `deleteProperty` to handle what happens if the user uses the `delete` keyword to remove something. For example, we could console log to let someone know that properties are being deleted:
```javascript
let target = {
    firstName: "John",
    lastName: "Doe",
    age: 152
}

let handler = {
    deleteProperty: (object, prop) => {
        console.log(`Poof! The ${prop} property was deleted`);
    },
}

let proxyExample = new Proxy(target, handler);

delete proxyExample['age'];
// Console logs
// Poof! The age property was deleted
```

### Customising function calls with proxies

Proxies also allow us to run custom code when we want to call a function. This is because of the Javascript quirk of functions being objects. There are two ways to do this:
- with the `apply()` handler, which traps standard function calls.
- with the `construct()` handler, which traps `new` constructor calls.

Here's a quick example where we trap a function call, and modify it by appending something to the end of its output.

```javascript
let target = (firstName, lastName) => {
    return `Hello ${firstName} ${lastName}`
}

let handler = {
    apply: (object, thisObject, argsList) => {
        let functionCall = object(...argsList);
        return `${functionCall}. I hope you are having a nice day!`
    },
}

let proxyExample = new Proxy(target, handler);

proxyExample("John", "Doe");
// Returns
// Hello John Doe. I hope you are having a nice day!
```

`apply` accepts three arguments:
- `object` - the original object.
- `thisObject` - the `this` value for the function/object.
- `argsList` - the arguments passed to the function.

Above, we called our function using the `object` argument, which contains the original `target` function. Then we added some text onto the end of it to change the output of the function. Again, pretty cool, right?

We can also do the same using `construct`, which also has three arguments:
- `object` - the original object.
- `argsList` - the arguments for the function/object.
- `newTarget` - the constructor that was originally called - i.e. the proxy.

Here's an example where a function returns an object, and we add a few more properties onto it using the `construct` method on our proxy:
```javascript
function target(a, b, c) {
    return { 
        a: a,
        b: b,
        c: c
    }
}

let handler = {
    construct: (object, argsList, newTarget) => {
        let functionCall = object(...argsList);
        return { ...functionCall, d: 105, e: 45 }
    },
}

let proxyExample = new Proxy(target, handler);

new proxyExample(15, 24, 45);
// Returns
// {a: 15, b: 24, c: 45, d: 105, e: 45}
```

## Conclusion
Proxies are an amazing tool in your Javascript arsenal which let you modify the basic operations of objects. There are a tonne of methods here to play around with and they can greatly simplify your code if you use them correctly. I hope you've enjoyed this article - you can read more of my [Javascript content here](https://fjolt.com/series/learn-javascript).