loading...

Javascript: Let's create a proxy

dipsaus9 profile image Dennis Spierenburg ・7 min read

ES6 Proxy

Two weeks ago I was attending the Vue.js and the Frontend love conference with Matise. I would highly recommend everyone to attend this conference because I’ve learned a lot in this week. One of the talks that inspired me to write this article is the talk of John Lindquist. Lindquist, co-founder of Egghead.io, talked on the first day about the power of Proxy in JavaScript with the focus on Vue.js.

I have heard about a Proxy object in JavaScript but I never knew what you could achieve with it. Lindquist created some examples that he called: “John Lindquist has bad ideas”. But while he was presenting the bad ideas you could clearly see the benefits and possibilities of the Proxy object. For more information check out his repo: https://github.com/johnlindquist/has-bad-ideas

Getters and setters with lots of swag

According to the article A quick intro to JavaScript Proxies by Chuks El-Gran Opia a proxy is, in simple terms, getters and setters with lots of swag. The Proxy object is used to define custom behavior for fundamental operations. In simpler terms a Proxy behaves like the original object but now you can interfere the original behaviour with some new functions. With the Proxy object you can for example:

  • Extend constructors
  • Manipulate DOM nodes
  • Value check and extra prop check
  • Tracing property accesses
  • Trapping function calls
  • And many more!

The Proxy object can contain the following three properties.

Target
The methods that provide property access. This is analogous to the concept of traps in operating systems

Handler
Placeholder object which contains traps.

Traps
Object which the proxy virtualizes. It is often used as storage backend for the proxy. Invariants (semantics that remain unchanged) regarding object non-extensibility or non-configurable properties are verified against the target.

source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

You can use the Proxy object like any other object.

const proxyEl = new Proxy(target, handler);

The best way to learn more about these properties is to use them.

Back to the future

The first time I heard about the Proxy object I compared it with the defineProperty function on the Object instance. The static defineProperty function defines a new property directly on an object, or modifies an existing property on an object, and returns the object. In this function you can also manipulate the getters and setters of an object on a low level key of JavaScript. For example:

const silObject = Object.defineProperty({}, 'condition', {
   get() {
      return 'is crazy';
   },
   set() {
       throw 'You may not change the condition of Sil, Sil is way too crazy and will kill you';
   }
});

Here I defined an object called the silObject. In this example I start with an empty object and add the property condition, cause we want to know if Sil is crazy or not. The weird thing about this object is if we log the silObject to the console we can’t see any properties in this object, the object is empty.

console.log(silObject) // {}

But if we want to check the condition of the silObject we can call the condition property.

console.log(silObject.condition) // is crazy

Sil complained to me about this example because he wanted to change his condition.

silObject.condition = 'Sil is not crazy!' //error: You may not change the condition of Sil, Sil is way too crazy and will kill you

This example shows the power of manipulation in JavaScript on a low key level. The worst part about this example is that we have to define these functions for all properties in an object. Opia wrote a perfect example for this in his article.

class Staff {
  constructor(name, age) {
    this._name = name;
    this._age = 25;
  }
  get name() {
    console.log(this._name);
  }
  get age() {
    console.log(this._age);
  }
  set age(newAge) {
    this._age = newAge;
    console.log(this._age)
  }
};

const staff = new Staff("Jane Doe", 25);

staff.name; // "Jane Doe"
staff.age; // 25
staff.age = 30; // 30

This is only possible in the new Class methods with getters and setters. But I think this is still way too abstract, so let’s write a function for this on object level.

const staff = {
  name: "Jane Doe",
  age: 25
};

Object.keys(staff).forEach(key => {
  let internalValue = staff[key];

  Object.defineProperty(staff, key, {
    get() {
      console.log(internalValue);
    },
    set(newVal) {
      internalValue = newVal;
      console.log(internalValue);
    }
  });
});

staff.name; // “Jane Doe”
staff.age; // 25
staff.age = 30; // 30

We now have get and set functions in the class instance of ES6 so the Object.defineProperty won’t be used as much anymore. The only difference with this function is that you can change some deeper level properties. For example with the defineProperty function you can change the enumerable properties of an object. If you want to know more about that check out the documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties

Let's build some proxies

The Proxy object can achieve something similar but on a more abstract level.

const sil = {
  condition: 'Super normal'
}

const silObject = new Proxy(sil, {
   get() {
     console.log('Sil is crazy);
   },
   set() {
       throw 'You may not change the condition of Sil, Sil is way too crazy and will kill you';
   }
});

silObject.condition; // Sil is crazy
silObject.condition = 'Super awesome'; // You may not change the condition of Sil, Sil is way too crazy and will kill you

It is almost the same example as before but instead of manipulating the original object we are defining a new silObject that is our proxy based on Sil. Also we are creating get and set properties for the complete object instead of a single property at the time. This means we can create some validation on an object.

const validator = {
  set(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  }
};

let person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 300; // Throws an exception

Here we can see a example of validation using the Proxy object.

So now we have seen plenty of examples, when are we going to use the proxy for something useful? Well, you can use the proxy for many problems. This strictness can be really useful when you’re writing an extendable plugin or even framework. Evan You wrote a perfect example of how to use proxies by writing some watch functions on Vuemastery. This code is not mine but written by Vuemastery. https://www.vuemastery.com/courses/advanced-components/evan-you-on-proxies/

First we start with our data set.

let target = null;
let data = { price: 5, quantity: 2 };

From here we write a dependency class where we can store all values that can be used for a watch function later on. We check if the property is defined and isn’t already included. This way we can create a dependency for each property in our data object. The Dep class is something Vuemastery has created in an earlier lesson and I won’t explain it in this article (https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system). On request I’ll write another article on how to create a dependency class for reactivity in JavaScript.

// Our simple Dep class

class Dep {
  constructor() {
    this.subscribers = [];
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      // Only if there is a target & it's not already subscribed
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach(sub => sub());
  } 
}

After this we can create a dependency for all attributes by creating a Map.

let deps = new Map(); // Let's store all of our data's deps in a map

Object.keys(data).forEach(key => {
  // Each property gets a dependency instance
  deps.set(key, new Dep());
});

If you want to know more about set and Map check out my other blog!

We now created a a map with two dependencies, one for each property. From here we can write our proxy!

let data_without_proxy = data // Save old data object

data = new Proxy(data_without_proxy, {  // Override data to have a proxy in the middle
  get(obj, key) {
    deps.get(key).depend(); // <-- Remember the target we're running
    return obj[key]; // call original data
  },

  set(obj, key, newVal) {
    obj[key] = newVal; // Set original data to new value
    deps.get(key).notify(); // <-- Re-run stored functions
    return true;
  }
});

So now we have a new data object. The most important thing to remember here is that we called some hooks based on our dependencies created earlier on. If we want to call a data property it will check if the property has a dependency.

Now we only have to write a logic to our dependency.

// The code to watch to listen for reactive properties
function watcher(myFunc) {
  target = myFunc;
  target();
  target = null;
}

let total = 0

watcher(() => {
  total = data.price * data.quantity;
});

and tadaa, we have a total property that is dependent on our data. If we now change the price or the quantity the total will change as well.

console.log(total); // 10
data.price = 20;
console.log(total); // 40
data.quantity = 10;
console.log(total); // 200

After this we can easily create more watchers!

deps.set('discount', new Dep())
data['discount'] = 5;

let salePrice = 0;

watcher(() => {
  salePrice = data.price - data.discount;
});

console.log(salePrice); // 15
data.discount = 7.5
console.log(salePrice); // 12.5

To see the full working code checkout https://codepen.io/dipsaus9/pen/EMmevB

The Proxy object can also return a function. On GitHub Lindquist has an example called createApi.

const createApi = url =>
  new Proxy(
    {},
    {
      get(target, key) {
        return async function(id = "") {
          const response = await fetch(`${url}/${key}/${id}`);
          if (response.ok) {
            return response.json();
          }

          return Promise.resolve({ error: "Malformed Request" });
        }
      }
    }
  );

let api = createApi("https://swapi.co/api");

api is now our Proxy object with a base URL of ‘https://swapi.co/api’ because who doesn’t love Star Wars. Now let’s find some star wars people.

(async () => {
   //'get' request to https://swapi.co/api/people
   let people = await api.people();

   //'get' request to https://swapi.co/api/people/1
   let person = await api.people(1);
})();

Here we saw some examples of the Proxy object and how you can use them for your own good. Be creative with it and remember the use case. I want to thank John Lindquist, Evan You and Vuemastery for their awesome examples and talks. They really helped me to understand the power of the Proxy.

Sources:

Posted on by:

dipsaus9 profile

Dennis Spierenburg

@dipsaus9

Frontend Developer who loves beer (who doesn't)

Discussion

pic
Editor guide
 

As the actual referenced Sil in this article, I endorse this post.