DEV Community

jsmanifest
jsmanifest

Posted on • Updated on

The Power of Proxy Pattern in JavaScript

Image description

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:

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'
Enter fullscreen mode Exit fullscreen mode

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:

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'
Enter fullscreen mode Exit fullscreen mode

The validator works perfectly:

TypeError: The id 3 is not a number. Received string instead
Enter fullscreen mode Exit fullscreen mode

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:

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()
Enter fullscreen mode Exit fullscreen mode

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.

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 for http requests, nginx content caching, cpu caching, lazy loading caching, 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:

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')
Enter fullscreen mode Exit fullscreen mode

Cache:

{
  "109105107101": "[hello] mike",
  "11597108108121": "[hello] sally",
  "109105107101321161041013210310597110116": "[hello] mike the giant",
  "115971081081213211610410132108105116116108101": "[hello] sally the little",
  "108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
Enter fullscreen mode Exit fullscreen mode

Implementing this directly in a Proxy class is straight forward:

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]
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

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:

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)
Enter fullscreen mode Exit fullscreen mode

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

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

console.log(mike)
console.log(bob)
console.log(sally)
Enter fullscreen mode Exit fullscreen mode

Result:

Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Enter fullscreen mode Exit fullscreen mode

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:

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)
Enter fullscreen mode Exit fullscreen mode

George's food:

 {
  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' }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

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)
Enter fullscreen mode Exit fullscreen mode

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

[
  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' }
]
Enter fullscreen mode Exit fullscreen mode

George:

Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

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')
Enter fullscreen mode Exit fullscreen mode

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:

CookieStealer.stealCookies(slickGeorge)

console.log(CookieStealer.cookies)
Enter fullscreen mode Exit fullscreen mode

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

[
  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' }
]
Enter fullscreen mode Exit fullscreen mode

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:

console.log(slickGeorge)
Enter fullscreen mode Exit fullscreen mode
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' }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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

Oldest comments (8)

Collapse
 
rajajaganathan profile image
Raja Jaganathan

Thanks for sharing this!. Interest to see many use case for proxy pattern :)

Collapse
 
tracygjg profile image
Tracy Gilmore

Is the title correct? JavScript

Collapse
 
jsmanifest profile image
jsmanifest

Thank you!

Collapse
 
nijojoseph profile image
nikijoe

for clipboard this is working

    const str = "Text"

    const withClipboardPolyfill = (obj, prop, cond, copyFnIfCond)=>{
        const copyToClipboard = () => {
            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(){
                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()
Enter fullscreen mode Exit fullscreen mode

The example shown here, I am not sure.

Collapse
 
fmgordillo profile image
Facundo Martin Gordillo

The cookie stealer really got me, thank you for this article! I will definitely take a look closer to Proxy

Collapse
 
jsmanifest profile image
jsmanifest

Awesome!

Collapse
 
harithzainudin profile image
Muhammad Harith Zainudin • Edited

Nice! This is a new things to me. Thank you for sharing!

Collapse
 
jsmanifest profile image
jsmanifest

If you need any help you can email me!