The Power of Proxy Pattern in JavaScript
Tags: javascript, webdev, node, programming
Published: 2022-04-05 14:24:37


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/grg56paaqj6erd78glfa.jpg)

One of the more interesting patterns I learned on a later stage of my career is the `Proxy`.

When you look for examples of the Proxy pattern you might often see different variations of implementations. That's because the Proxy is not limited to one use case. One proxy may act as a validator while the other may be more interested in improving performance, etc.

The idea is that by utilizing a proxy we wrap existing objects that functions the same as the original where its methods (or even properties) are exactly identical until we add additional logic inside the wrapped methods _before the wrapped function is called_. This is is a process completely hidden to the outside world, and this call will always appear the same to the caller.

In other words the proxy sits right in between the client of an object and the actual object itself. This is where it can choose to act as a "protector" or add custom logic such as _caching_ without the caller ever knowing this. Because of this it can sometimes be referred to as the Mediator. Some may also categorize it as another form of a Decorator pattern, but there are some differences.

In this post we will be going over the power of the Proxy Design Pattern in JavaScript and go over several examples of how beneficial it can become for your next application.

Since JavaScript natively added a `Proxy` class that implements the pattern, we will be directly using the `Proxy` class instead to demonstrate the pattern after a couple vanilla implementations.

## Difference between Decorator vs Proxy

In the decorator pattern, the decorator's main responsibility is to enhance the object it is wrapping (or "decorating"), whereas the proxy has more accessibility and controls the object.

The proxy may choose to enhance the object it is wrapping or control it in other ways such as restricting access from the outside world, but a decorator instead informs and applies enhancements.

The difference responsibility-wise is clear. Engineers commonly use decorators to add new behavior or as a form of an adapter for old or legacy classes where they return an enhanced interface _that the client may know about but doesn't care about at the same time_. The proxy is usually intended to be returning _the same interface where the client may assume it is working with the same object untouched_.

## Validator/Helper

The first implementation of a Proxy pattern I will be showing here will be a validator.

This example shows the pattern being implemented as a way to help validate input and protect properties from being set the wrong data types. Remember that the caller must always assume that it is working with the original object so the Proxy must not change the signature or interface of the object it is wrapping:

```js
class Pop {
  constructor(...items) {
    this.id = 1
  }
}

const withValidator = (obj, field, validate) => {
  let value = obj[field]

  Object.defineProperty(obj, field, {
    get() {
      return value
    },
    set(newValue) {
      const errMsg = validate(newValue)
      if (errMsg) throw new Error(errMsg)
      value = newValue
    },
  })

  return obj
}

let mello = new Pop(1, 2, 3)

mello = withValidator(mello, 'id', (newId) => {
  if (typeof newId !== 'number') {
    return `The id ${newId} is not a number. Received ${typeof newId} instead`
  }
})

mello.id = '3'
```

This example shows a simple helper that validates fields of an object, throwing a `TypeError` exception when the validation fails.

The Proxy takes ownership of the `getter` and `setter` of the `id` property and chooses to allow or reject values that are attempted to be set.

In the `Proxy` class it can be implemented with something like this:

```js
const withValidator = (obj, field, validate) => {
  return new Proxy(obj, {
    set(target, prop, newValue) {
      if (prop === field) {
        const errMsg = validate(newValue)
        if (errMsg) throw new TypeError(errMsg)
        target[prop] = newValue
      }
    },
  })
}

let mello = new Pop(1, 2, 3)

mello = withValidator(mello, 'id', (newId) => {
  if (typeof newId !== 'number') {
    return `The id ${newId} is not a number. Received ${typeof newId} instead`
  }
})

mello.id = '3'
```

The validator works perfectly:

```js
TypeError: The id 3 is not a number. Received string instead
```

## Clipboard Polyfill

This section will go over using the Proxy as a way to support older browsers when copying selections of text into the users clipboard by ensuring that the browser supports the `Navigator.clipboard` API. If it doesn't, then it will fall back to using `execCommand` to copy the selection.

Again, the client will always assume that the object it is calling methods on is the original object and only knows that it's calling the said method:

```js
const withClipboardPolyfill = (obj, prop, cond, copyFnIfCond) => {
  const copyToClipboard = (str) => {
    if (cond()) {
      copyFnIfCond()
    } else {
      const textarea = document.createElement('textarea')
      textarea.value = str
      textarea.style.visibility = 'hidden'
      document.body.appendChild(textarea)
      textarea.select()
      document.execCommand('copy')
      document.body.removeChild(textarea)
    }
  }
  obj[prop] = copyToClipboard
  return obj
}

const api = (function () {
  const o = {
    copyToClipboard(str) {
      return navigator.clipboard.writeText(str)
    },
  }
  return o
})()

let copyBtn = document.createElement('button')
copyBtn.id = 'copy-to-clipboard'
document.body.appendChild(copyBtn)

copyBtn.onclick = api.copyToClipboard

copyBtn = withClipboardPolyfill(
  copyBtn,
  'onclick',
  () => 'clipboard' in navigator,
  api.copyToClipboard,
)

copyBtn.click()
```

You might ask what is the point of applying the proxy in situations like this instead of directly hardcoding the implementation inside the actual `copyToClipboard` function. If we utilize a proxy we can reuse it as a standalone and freely change the implementation via [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control#:~:text=In%20software%20engineering%2C%20inversion%20of,control%20from%20a%20generic%20framework.).

Another benefit from using this strategy is that we don't modify the original function.

## Cacher (Enhancing performance)

Caching can take in many different forms in many different scenarios. For example there is a [Stale While Revalidate](https://datatracker.ietf.org/doc/html/rfc5861) for http requests, [nginx content caching](https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/), [cpu caching](https://en.wikipedia.org/wiki/CPU_cache), [lazy loading caching](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/Strategies.html), memoization. etc.

In JavaScript we can also achieve caching with the help of a Proxy.

To implement the proxy pattern without directly using the `Proxy` class we can do something like this:

```js
const simpleHash = (str) =>
  str.split('').reduce((acc, str) => (acc += str.charCodeAt(0)), '')

const withMemoization = (obj, prop) => {
  const origFn = obj[prop]
  const cache = {}

  const fn = (...args) => {
    const hash = simpleHash(args.map((arg) => String(arg)).join(''))
    if (!cache[hash]) cache[hash] = origFn(...args)
    return cache[hash]
  }

  Object.defineProperty(obj, prop, {
    get() {
      return fn
    },
  })

  return obj
}

const sayHelloFns = {
  prefixWithHello(str) {
    return `[hello] ${str}`
  },
}

const enhancedApi = withMemoization(sayHelloFns, 'prefixWithHello')
enhancedApi.prefixWithHello('mike')
enhancedApi.prefixWithHello('sally')
enhancedApi.prefixWithHello('mike the giant')
enhancedApi.prefixWithHello('sally the little')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
```

Cache:

```json
{
  "109105107101": "[hello] mike",
  "11597108108121": "[hello] sally",
  "109105107101321161041013210310597110116": "[hello] mike the giant",
  "115971081081213211610410132108105116116108101": "[hello] sally the little",
  "108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
```

Implementing this directly in a `Proxy` class is straight forward:

```js
const withMemoization = (obj, prop) => {
  const origFn = obj[prop]
  const cache = {}

  const fn = (...args) => {
    const hash = simpleHash(args.map((arg) => String(arg)).join(''))
    if (!cache[hash]) cache[hash] = origFn(...args)
    return cache[hash]
  }

  return new Proxy(obj, {
    get(target, key) {
      if (key === prop) {
        return fn
      }
      return target[key]
    },
  })
}
```

## The `Proxy` class

We've seen a persistent pattern in a couple of barebones Proxy pattern implementation vs directly using the `Proxy` class. Since JavaScript directly provides `Proxy` as an object into the language, the rest of this post will be using this as a convenience.

All remaining examples can be achieved without the `Proxy`, but we will be focusing on the class syntax instead because it is more concise and easier to work with especially for the sake of this post.

## Proxy to Singleton

If you've never heard of a Singleton, it's another design pattern that ensures that an object of interest will be returned and reused if it's already instantiated throughout the lifetime of an application. In practice you will most likely see this being used as some global variable.

For example, if we were coding an MMORPG game and we had three classes `Equipment`, `Person`, and `Warrior` where there can only be _one_ `Warrior` in existence, we can use the `construct` handler method inside the second argument when instantiating a `Proxy` on the `Warrior` _class_:

```js
class Equipment {
  constructor(equipmentName, type, props) {
    this.id = `_${Math.random().toString(36).substring(2, 16)}`
    this.name = equipmentName
    this.type = type
    this.props = props
  }
}

class Person {
  constructor(name) {
    this.hp = 100
    this.name = name
    this.equipments = {
      defense: {},
      offense: {},
    }
  }

  attack(target) {
    target.hp -= 5
    const weapons = Object.values(this.equipments.offense)
    if (weapons.length) {
      for (const weapon of weapons) {
        console.log({ weapon })
        target.hp -= weapon.props.damage
      }
    }
  }

  equip(equipment) {
    this.equipments[equipment.type][equipment.id] = equipment
  }
}

class Warrior extends Person {
  constructor() {
    super(...arguments)
  }

  bash(target) {
    target.hp -= 15
  }
}

function useSingleton(_Constructor) {
  let _warrior

  return new Proxy(_Constructor, {
    construct(target, args, newTarget) {
      if (!_warrior) _warrior = new Warrior(...args)
      return _warrior
    },
  })
}

const WarriorSingleton = useSingleton(Warrior)
```

If we try to create multiple instances of `Warrior` we are ensured that only the first one created is used every time:

```js
const mike = new WarriorSingleton('mike')
const bob = new WarriorSingleton('bob')
const sally = new WarriorSingleton('sally')

console.log(mike)
console.log(bob)
console.log(sally)
```

Result:

```js
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
```

## Cookie Stealer

In this section we will demonstrate an example using a `Proxy` to prevent mutations from a list of cookies. This will prevent the original object from being mutated and the mutator (the `CookieStealer`) will assume that their evil operation was a success.

Lets take a look at this example:

```js
class Food {
  constructor(name, points) {
    this.name = name
    this.points = points
  }
}

class Cookie extends Food {
  constructor() {
    super(...arguments)
  }

  setFlavor(flavor) {
    this.flavor = flavor
  }
}

class Human {
  constructor() {
    this.foods = []
  }

  saveFood(food) {
    this.foods.push(food)
  }

  eat(food) {
    if (this.foods.includes(food)) {
      const foodToEat = this.foods.splice(this.foods.indexOf(food), 1)[0]
      this.hp += foodToEat.points
    }
  }
}

const apple = new Food('apple', 2)
const banana = new Food('banana', 2)

const chocolateChipCookie = new Cookie('cookie', 2)
const sugarCookie = new Cookie('cookie', 2)
const butterCookie = new Cookie('cookie', 3)
const bakingSodaCookie = new Cookie('cookie', 3)
const fruityCookie = new Cookie('cookie', 5)

chocolateChipCookie.setFlavor('chocolateChip')
sugarCookie.setFlavor('sugar')
butterCookie.setFlavor('butter')
bakingSodaCookie.setFlavor('bakingSoda')
fruityCookie.setFlavor('fruity')

const george = new Human()

george.saveFood(apple)
george.saveFood(banana)
george.saveFood(chocolateChipCookie)
george.saveFood(sugarCookie)
george.saveFood(butterCookie)
george.saveFood(bakingSodaCookie)
george.saveFood(fruityCookie)

console.log(george)
```

George's food:

```json
 {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 },
    Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
    Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
    Cookie { name: 'cookie', points: 3, flavor: 'butter' },
    Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
    Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
  ]
}
```

We instantiated `george` using the `Human` class and we added 7 items of food to its storage. George is happy he is about to eat his fruits and cookies. He is especially excited about his cookies because he's gotten his favorite flavors all at the same time, soon to be gobbling on them to satisfy his cravings for cookies.

However, there is an issue:

```js
const CookieStealer = (function () {
  const myCookiesMuahahaha = []

  return {
    get cookies() {
      return myCookiesMuahahaha
    },
    isCookie(obj) {
      return obj instanceof Cookie
    },
    stealCookies(person) {
      let indexOfCookie = person.foods.findIndex(this.isCookie)
      while (indexOfCookie !== -1) {
        const food = person.foods[indexOfCookie]
        if (this.isCookie(food)) {
          const stolenCookie = person.foods.splice(indexOfCookie, 1)[0]
          myCookiesMuahahaha.push(stolenCookie)
        }
        indexOfCookie = person.foods.findIndex(this.isCookie)
      }
    },
  }
})()

CookieStealer.stealCookies(george)
```

The `CookieStealer` comes out of the blue to steal his cookies. The `CookieStealer` now has the 5 cookies in his storage:

```js
[
  Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
  Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
  Cookie { name: 'cookie', points: 3, flavor: 'butter' },
  Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
  Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
```

George:

```js
Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 }
  ]
}
```

If we were to rewind back and introduce our savior `Superman` to apply one of his methods that implement the `Proxy` pattern to prevent the `CookieStealer` from his evil acts it would solve our issue:

```js
class Superman {
  protectFromCookieStealers(obj, key) {
    let realFoods = obj[key]
    let fakeFoods = [...realFoods]

    return new Proxy(obj, {
      get(target, prop) {
        if (key === prop) {
          fakeFoods = [...fakeFoods]

          Object.defineProperty(fakeFoods, 'splice', {
            get() {
              return function fakeSplice(...[index, removeCount]) {
                fakeFoods = [...fakeFoods]
                return fakeFoods.splice(index, removeCount)
              }
            },
          })

          return fakeFoods
        }
        return target[prop]
      },
    })
  }
}

const superman = new Superman()
const slickGeorge = superman.protectFromCookieStealers(george, 'foods')
```

Our friend `superman` luckily happens to have a `protectFromCookieStealers` using the power of the `Proxy` to _fake_ a list of cookies! He keeps the _real_ collection of foods that contain george's cookies hidden away from the `CookieStealer`. `CookieStealer` proceeds with his evil plans and is seemingly _tricked_ into thinking he got away with the cookies:

```js
CookieStealer.stealCookies(slickGeorge)

console.log(CookieStealer.cookies)
```

The `CookieStealer` walks away with cookies in his storage still and _thinks_ he got away with it:

```json
[
  Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
  Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
  Cookie { name: 'cookie', points: 3, flavor: 'butter' },
  Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
  Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
```

Little does he know that he was tricked by superman and those were _fake cookies_! `george` still has his cookies untouched thanks to the power of `Proxy` saving him from the darkness of evil:

```js
console.log(slickGeorge)
```

```json
Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 },
    Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
    Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
    Cookie { name: 'cookie', points: 3, flavor: 'butter' },
    Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
    Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
  ]
}
```

## Conclusion

I hope this helped shed some light on the Proxy pattern and how to take advantage of this concept using the now built-in `Proxy` class in JavaScript.

That concludes the end of this post :) I hope you found this article helpful to you, and make sure to follow me on medium for future posts!

Find me on [medium](https://medium.com/@jsmanifest)