Exploring Sets and Maps in JavaScript

Exploring Sets and Maps in JavaScript

In this article, we’ve taken a look at Sets and Maps and how they handle unique items and key-value pairs respectively. These useful data structures provide easier and more efficient ways to structure and access data under certain use cases.

In this article, we’ve taken a look at Sets and Maps and how they handle unique items and key-value pairs respectively. These useful data structures provide easier and more efficient ways to structure and access data under certain use cases.

Let’s take~ ~a look into two new constructs that were introduced in the JavaScript ES6 specification:

Table of Contents

  • Sets
  • Maps
  • Conclusion
  1. *Set *- The Set object allows you to store unique values of any type.
  2. *Map *- The Map object allows you to store key-value pairs and remembers the original insertion order of the keys.

The objective of these new constructs is to provide easier and more efficient ways to structure and access data under certain use cases. In this article, we will look at how Sets and Maps work and explore some of the operations that can be performed on them.

Sets in JavaScript

The MDN docs describe the Set object as:

A collections of values. You can iterate through the elements of a set in insertion order. A value in the Set may only occur once; it is unique in the Sets collection.

The JavaScript Set object behaves similarly to the mathematical Set. It allows the addition of distinct values and provides useful methods on its prototype. These methods include - addition, removal and looping over of the items present in the Set.

Array vs Set

An array, like a set, is a data structure that allows addition, removal and looping operations on its items. However, an array differs from a set in the sense that it permits the addition of duplicate values and its operations are relatively slower.

Searching through an array has a linear time complexity of O(n), the same as inserting an element in the middle of an array. This means that the running time for searching and inserting items in an array grows as the size of the array increases.

JavaScript’s Push and Pop array methods have a run-time of O(1) which means that: these operations will have a constant time of execution regardless of the size of the array size. However, in practice, the Push operation is O(n) as copy costs are incurred when new contiguous memory locations are allocated to the newly formed array.

In contrast, all insert, delete and search operations for Sets have a running time of just O(1).

Creating a Set

Let’s create a Set:

const set = new Set();

console.log(set); // Set {}

Initializing a Set

To initialize a set, we can pass an array of values to the Set constructor, this will create a Set with those values:

const confectioneries = new Set(['oreo', 'marshmallow','oreo', 'kitkat', 'gingerbread']);

console.log(confectioneries); // result: Set { 'oreo', 'marshmallow', 'kitkat', 'gingerbread' }

In the snippet above, the duplicate value “oreo” is quietly removed from the Set and only unique values are returned.

Adding Items

We can add more items to a Set using the add() method. This method adds a new value to the Set object and returns the Set. An attempt to add a duplicate item to the Set object wouldn’t return an error, instead, the item will not be added.

Let’s go over an example:

const confectioneries = new Set(['oreo', 'marshmallow', 'kitkat', 'oreo','gingerbread']);

confectioneries.add('donut');

console.log(confectioneries); //_ log result: Set { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' } _

confectioneries.add('kitkat');

console.log(confectioneries); //_ log result: Set { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' } _

Deleting Items

With sets, we can delete items using either of these commands:

  • delete()
  • clear()

To use the delete() method, the value to be deleted is passed to the method. The method will return a Boolean value true if the deletion was successful and false if otherwise. We can delete all the elements of the Set object using the clear() method.

Let’s try out both methods in this example:

confectioneries.delete('kitkat');

console.log(confectioneries); //_ log result: Set { 'oreo', 'marshmallow', 'gingerbread', 'donut' }_

confectioneries.clear();

console.log(confectioneries); // log result: Set {}

Size of a Set

We can get the size of a Set using the size property on the Set prototype. This is similar to the length property for Arrays:

const confectioneries = new Set(['oreo', 'marshmallow', 'kitkat', 'oreo','gingerbread']);

console.log(confectioneries.size); // log result: 5

Searching for Items

We may need to know if a Set has a particular item. This can be accomplished using the has() method. The has() method returns true if the item is in the Set object, and false if it isn't:

console.log(confectioneries.has('marshmallow')); // log result: true


Returning the Items in a Set

We can return the items in a Set object in the same insertion order using the values()method. This method returns a new setIterator object . A similar method for returning the items of a set is the keys() method:

console.log(confectioneries.values()); // _log result: _[_Set Iterator] { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' }_

console.log(confectioneries.keys()); //_ log result: _[_Set Iterator] { 'oreo', 'marshmallow', 'kitkat', 'gingerbread', 'donut' }_

The setIterator object is an Iterator object because it implements the Iteratable and Iterator protocols. The Iterable protocol specifies a way to iterate through a set of values using loop constructs. It also makes it possible for the values to be iterated using the next() method. When we call next() on a setIterator object, we get the next value in the iteration and a false until all values of the Set have been iterated over:

let iterator = confectioneries.values();

console.log( iterator.next()); // _{ value: 'oreo', done: false } 
_
console.log( iterator.next()); // _{ value: 'marshmallow', done: false }
_
console.log( iterator.next()); //_ { value: 'kitkat', done: false }
_
console.log( iterator.next()); //_ { value: 'gingerbread', done: false }
_
console.log( iterator.next()); //_ { value: 'donut', done: false }
_
console.log( iterator.next()); // _{ value: undefined, done: true }_

Since Sets implement the Iterable protocol, loop constructs such as for ...ofcan be used as shown below:

for (let confectionery of confectioneries) {
  console.log(confectionery);
}

/_ _console.log() result 
oreo
marshmallow
kitkat
gingerbread
donut 
__/

WeakSets

WeakSets provide extra flexibility when working with the Set data structure. They are different from regular Sets in that they only accept objects and are not iterable; they can’t be looped over, and do not have a clear() method. How then do they provide extra flexibility? We’ll see in a bit.

We can create a WeakSetusing the WeakSet constructor:

let user1 = {name: 'user 1', email: '[email protected]'};
let user2 = {name: 'user 2', email: '[email protected]'};
let user3 = {name: 'user 3', email: '[email protected]'};

const users = new WeakSet([user1, user2, user3]);

The code above creates a new WeakSet object, adding items other than objects returns a TypeError:

users.add('user 4');

console.log(users); // TypeError: Invalid value used in weak set

Since WeakSets do not have a clear() method, objects can only be deleted by setting them to null. This works because the JavaScript Engine’s garbage collection algorithms will automatically free up memory allocated to the null object, hence deleting it from the WeakSet.

This is wonderful because the WeakSets objects set to null are garbage-collected while the program is still running, hence, reducing memory consumption and preventing memory leakage, especially when dealing with huge amounts of data that are generated asynchronously.

Within this feature lies a chance for you to write light-weight solutions to programming problems without having to bother with the details of memory management.

Maps In JavaScript

JavaScript Maps are objects designed to efficiently store and retrieve items based on a unique key for each item. A Map stores key-value pairs where both keys and values could be either primitive values or objects, or both.

The MDN docs describe the Map object as:

The Map object holds key-value pairs and remembers the original insertion order of the keys. Any value (both objects and primitive values) may be used as either a key or a value. A Map object iterates its elements in insertion order — a for...of loop returns an array of [key, value] for each iteration.

Creating a Map

Akin to Sets, Maps are easy to create. Let’s create a Map using the Map constructor:

const users = new Map();

console.log(users); // Map {}

Adding Items

Key-value pairs are added to a Map using the set() method. This method takes in two arguments, the first being the key and the second, the value, which is referenced by the key:

users.set('John Doe', {
  email: '[email protected]',
});

users.set('Jane Doe', {
  email: '[email protected]',
});

console.log(users);

/__ console.log result 
Map {
  'John Doe' => { email: '[email protected]'},
  'Jane Doe' => { email: '[email protected]'} }
__/

Unlike Sets which discard duplicate keys, Maps will update the value attached to that key:

users.set('John Doe', {
  email: '[email protected]',
});

console.log(users);

/__ console.log result 
Map {
  'John Doe' => {email: '[email protected]'},
  'Jane Doe' => { email: '[email protected]'} }
__/

When you run the example above, John Doe's email will neatly be replaced. Smooth.

Deleting Items

As with Sets, key-value pairs can be deleted using the delete() method. The key to be deleted is passed to the delete() method as shown below:

users.delete('Jane Doe');


Maps also have a clear() method, this removes all key-value pairs from the Map object:

users.clear();

console.log(users); // Map {}

Searching for Items

Maps also have a has() method which checks if a key exists in a Map. This method will return true if the key is in the Map and false if it is not:

let users = new Map();

users.set('John Doe', {
  email: '[email protected]',
});

users.set('Jane Doe', {
  email: '[email protected]',
});

console.log(users.has('John Doe')); // true

Returning the Value of a Map item

The value of a key in a Map object can be gotten using the get method on the Map prototype:

console.log(users.get('Jane Doe'); // { email: '[email protected]' }


It is possible to get all the keys and values of a Map object using the keys() and values() methods respectively. These methods both return a new MapIterator object which has a next() method that can be used to loop through the items of the Map:

let userKeys = users.keys();

console.log(userKeys.next()); // { value: 'John Doe', done: false }

let userValues = users.values();

console.log(userValues.next()); // _{ value: { email: '[email protected]' }, done: false }_

As with Sets, loop constructs such as for...of and forEach() can be used to loop through Map items:

for (let user of users) {
  console.log('[for...of]: ', user);
}

/_ Log result
  _[_for...of]:  _[_ 'John Doe', { email: '[email protected]' } ]
  _[_for...of]:  _[___ 'Jane Doe', { email: '[email protected]' } ]
_/

users.forEach((value, key) => console.log('[__forEach()]:  ', key, value));

/*_ Log result
  [__forEach()]:   John Doe { email: '[email protected]' }
  _[_forEach()]:   Jane Doe { email: '[email protected]' }
*_/

WeakMaps

As with WeakSets, WeakMaps differ from regular Map objects. WeakMaps only accept objects as keys, are not iterable and do not have a clear() method.

A WeakMap constructor is used to create a WeakMap.

Let’s look at an example:

let users = new WeakMap();

const user1 = {
  name: 'John Doe',
};
const user2 = {
  name: 'Jane Doe',
};

users.set(user1, {
  email: '[email protected]',
});

users.set(user2, {
  email: '[email protected]',
});

As with WeakSets, setting the key of a WeakMap object to null will implicitly garbage collect that item:

user1 = null;


This has the same advantages as with WeakSets in providing easier memory management.

Conclusion

In this article, we’ve taken a look at Sets and Maps and how they handle unique items and key-value pairs respectively. These useful data structures provide easier and more efficient ways to structure and access data under certain use cases.

Special modifications such as WeakSets and WeakMaps provide more options for the developer and are handy for memory management.

An Introduction to JavaScript ES6 Proxies

An Introduction to JavaScript ES6 Proxies

Proxy is one of the most overlooked concepts introduced in ES6 version of JavaScript, but ES6 proxies bound to come in handy at some point in your future.

Proxy is one of the most overlooked concepts introduced in the ES6 version of JavaScript.

Admittedly, it isn’t particularly useful on a day-to-day basis, but it is bound to come in handy at some point in your future.

The basics

The Proxy object is used to define a custom behavior for fundamental operations such as property lookup, assignment, and function invocation.

The most basic example of a proxy would be:

const obj = {
 a: 1,
 b: 2,
};

const proxiedObj = new Proxy(obj, {
 get: (target, propertyName) => {
   // get the value from the "original" object
   const value = target[propertyName];

   if (!value && value !== 0) {
     console.warn('Trying to get non-existing property!');

     return 0;
   }

   // return the incremented value
   return value + 1;
 },
 set: (target, key, value) => {
   // decrement each value before saving
   target[key] = value - 1;

   // return true to indicate successful operation
   return true;
 },
});

proxiedObj.a = 5;

console.log(proxiedObj.a); // -> incremented obj.a (5)
console.log(obj.a); // -> 4

console.log(proxiedObj.c); // -> 0, logs the warning (the c property doesn't exist)

We have intercepted the default behavior of both get and set operations by defining the handlers with their respective names in the object provided to the proxy constructor. Now each get operation will return the incremented value of the property, while set will decrement the value before saving it in the target object.

What’s important to remember with proxies is that once a proxy is created, it should be the only way to interact with the object.

Different kinds of traps

There are many traps (handlers that intercept the object’s default behavior) aside from get and set, but we won’t be using any of them in this article. With that being said, if you are interested in reading more about them, here’s the documentation.

Having fun

Now that we know how proxies work, let’s have some fun with them.

Observing object’s state

As it has been stated before it is very easy to intercept operations with proxies. To observe an object’s state is to be notified every time there’s an assignment operation.

const observe = (object, callback) => {
 return new Proxy(object, {
   set(target, propKey, value) {
     const oldValue = target[propKey];
   
     target[propKey] = value;

     callback({
       property: propKey,
       newValue: value,
       oldValue,
     });

     return true;
   }
 });
};

const a = observe({ b: 1 }, arg => {
 console.log(arg);
});

a.b = 5; // -> logs from the provided callback: {property: "b", oldValue: 1, newValue: 5}

And that’s all we have to do — invoke the provided callback every time the set handler is fired.

As an argument to the callback, we provide an object with three properties: the name of the changed property, the old value, and the new value.

Prior to executing the callback, we assign the new value in the target object so the assignment actually takes place. We have to return true to indicate that the operation has been successful; otherwise, it would throw a TypeError.

Here’s a live example.

Validating properties on set

If you think about it, proxies are a good place to implement validation — they are not tightly coupled with the data itself. Let’s implement a simple validation proxy.

As in the previous example, we have to intercept the set operation. We would like to end up with the following way of declaring data validation:

const personWithValidation = withValidation(person, {
 firstName: [validators.string.isString(), validators.string.longerThan(3)],
 lastName: [validators.string.isString(), validators.string.longerThan(7)],
 age: [validators.number.isNumber(), validators.number.greaterThan(0)]
});

In order to achieve this, we define the withValidation function like so:

const withValidation = (object, schema) => {
 return new Proxy(object, {
   set: (target, key, value) => {
     const validators = schema[key];

     if (!validators || !validators.length) {
       target[key] = value;

       return true;
     }

     const shouldSet = validators.every(validator => validator(value));

     if (!shouldSet) {
       // or get some custom error
       return false;
     }

     target[key] = value;
     return true;
   }
 });
};

First we check whether or not there are validators in the provided schema for the property that is currently being assigned — if there aren’t, there is nothing to validate and we simply assign the value.

If there are indeed validators defined for the property, we assert that all of them return true before assigning. Should one of the validators return false, the whole set operation returns false, causing the proxy to throw an error.

The last thing to do is to create the validators object.

const validators = {
 number: {
   greaterThan: expectedValue => {
     return value => {
       return value > expectedValue;
     };
   },
   isNumber: () => {
     return value => {
       return Number(value) === value;
     };
   }
 },
 string: {
   longerThan: expectedLength => {
     return value => {
       return value.length > expectedLength;
     };
   },
   isString: () => {
     return value => {
       return String(value) === value;
     };
   }
 }
};

The validators object contains validation functions grouped by the type they should validate. Each validator on invocation takes the necessary arguments, like validators.number.greaterThan(0), and returns a function. The validation happens in the returned function.

We could extend the validation with all kinds of amazing features, such as virtual fields or throwing errors from inside the validator to indicate what went wrong, but that would make the code less readable and is outside the scope of this article.

Here’s a live example.

Making code lazy

For the final — and hopefully most interesting — example, let’s create a proxy that makes all the operations lazy.

Here’s a very simple class called Calculator, which contains a few basic arithmetic operations.

class Calculator {
 add(a, b) {
   return a + b;
 }

 subtract(a, b) {
   return a - b;
 }

 multiply(a, b) {
   return a * b;
 }

 divide(a, b) {
   return a / b;
 }
}

Now normally, if we ran the following line:

new Calculator().add(1, 5) // -> 6

The result would be 6.

The code is executed on the spot. What we would like is to have the code wait for the signal to be run, like a run method. This way the operation will be postponed until it is needed — or not executed at all if there is never a need.

So the following code, instead of 6, would return the instance of the Calculator class itself:

lazyCalculator.add(1, 5) // -> Calculator {}

Which would give us another nice feature: method chaining.

lazyCalculator.add(1, 5).divide(10, 10).run() // -> 1

The problem with that approach is that in divide, we have no clue of what the result of add is, which makes it kind of useless. Since we control the arguments, we can easily provide a way to make the result available through a previously defined variable — $, for example.

lazyCalculator.add(5, 10).subtract($, 5).multiply($, 10).run(); // -> 100

$ here is just a constant Symbol. During execution, we dynamically replace it with the result returned from the previous method.

const $ = Symbol('RESULT_ARGUMENT');

Now that we have a fair understanding of what do we want to implement, let’s get right to it.

Let’s create a function called lazify. The function creates a proxy that intercepts the get operation.

function lazify(instance) {
 const operations = [];

 const proxy = new Proxy(instance, {
   get(target, propKey) {
     const propertyOrMethod = target[propKey];

     if (!propertyOrMethod) {
       throw new Error('No property found.');
     }

     // is not a function
     if (typeof propertyOrMethod !== 'function') {
       return target[propKey];
     }

     return (...args) => {
       operations.push(internalResult => {
         return propertyOrMethod.apply(
           target,
           [...args].map(arg => (arg === $ ? internalResult : arg))
         );
       });

       return proxy;
     };
   }
 });

 return proxy;
}

Inside the get trap, we check whether or not the requested property exists; if it doesn’t, we throw an error. If the property is not a function, we return it without doing anything.

Proxies don’t have a way of intercepting method calls. Instead, they are treating them as two operations: the get operation and a function invocation. Our get handler has to act accordingly.

Now that we are sure the property is a function, we return our own function, which acts as a wrapper. When the wrapper function is executed, it adds yet another new function to the operations array. The wrapper function has to return the proxy to make it possible to chain methods.

Inside the function provided to the operations array, we execute the method with the arguments provided to the wrapper. The function is going to be called with the result argument, allowing us to replace all the $ with the result returned from the previous method.

This way we delay the execution until requested.

Now that we have built the underlying mechanism to store the operations, we need to add a way to run the functions — the .run() method.

This is fairly easy to do. All we have to do is check whether the requested property name equals run. If it does, we return a wrapper function (since run acts as a method). Inside the wrapper, we execute all the functions from the operations array.

The final code looks like this:

const executeOperations = (operations, args) => {
 return operations.reduce((args, method) => {
   return [method(...args)];
 }, args);
};

const $ = Symbol('RESULT_ARGUMENT');

function lazify(instance) {
 const operations = [];

 const proxy = new Proxy(instance, {
   get(target, propKey) {
     const propertyOrMethod = target[propKey];

     if (propKey === 'run') {
       return (...args) => {
         return executeOperations(operations, args)[0];
       };
     }

     if (!propertyOrMethod) {
       throw new Error('No property found.');
     }

     // is not a function
     if (typeof propertyOrMethod !== 'function') {
       return target[propKey];
     }

     return (...args) => {
       operations.push(internalResult => {
         return propertyOrMethod.apply(
           target,
           [...args].map(arg => (arg === $ ? internalResult : arg))
         );
       });

       return proxy;
     };
   }
 });

 return proxy;
}

The executeOperations function takes an array of functions and executes them one by one, passing the result of the previous one to the invocation of the next one.

And now for the final example:

const lazyCalculator = lazify(new Calculator());

const a = lazyCalculator
 .add(5, 10)
 .subtract($, 5)
 .multiply($, 10);

console.log(a.run()); // -> 100

If you are interested in adding more functionality I have added a few more features to the lazify function — asynchronous execution, custom method names, and a possibility to add custom functions through the .chain() method. Both versions of the lazify function are available in the live example.

Summary

Now that you have seen proxies in action, I hope that you could find a good use for them in your own codebase.

Proxies have many more interesting uses than those covered here, such as implementing negative indices and catching all the nonexistent properties in an object. Be careful, though: proxies are a bad choice when performance is an important factor.

Metaprogramming: An Introduction to JavaScript(ES6) Proxy

Metaprogramming: An Introduction to JavaScript(ES6) Proxy

Metaprogramming: An Introduction to JavaScript(ES6) Proxy - The concept of Metaprogramming is not new. There are many programming languages like, Lisp, Scala, Clojure, Rust, Haskell, etc already got the use of it. JavaScript is not really behind either!

Originally published by Tapas Adhikary at blog.greenroots.info

Before we go any further, let us understand, What is Metaprogramming?

Metaprogramming

Metaprogramming is nothing less than a Magic! Truly, how about writing a program to Read, Modify, Analyze and even to Generate a Program? Doesn't it sound Wizardry and Powerful?

Wikipedia defines Metaprogramming as,

Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data.

So basically, it is the Program that deals with the Meta Data of another program and able to do lot of useful things.

Meet Proxy

Proxy wraps objects and intercepts their behavior through traps

Among several ways we can do Metaprogramming in JavaScript, usage of Proxyobject is one of the important one. Proxy object is an ES6 concept used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

Here are few useful terms you need to remember and use:

  • target: an Object which the proxy virtualizes.
  • handler: a Placeholder Object which contains traps.
  • trap: the Methods that provide property access of the target object.

It is perfectly fine, if you haven't got much from the description above. We will understand it very easily through code and examples.

Code Time

Here is the syntax for creating a Proxy Object:

let p = new Proxy(target, handler); 

Now let us take an example of an employee object and try to print some of the properties of it:

const employee = {
    firstName: 'Tapas',
    lastName: 'Adhikary'
};
 
console.group('employee');
    console.log(employee.firstName);
    console.log(employee.lastName);
    console.log(employee.org);
    console.log(employee.fullName);
console.groupEnd()

Well, we know the expected output would be,

employee
  Tapas
  Adhikary
  undefined
  undefined

Now let us use the Proxy object to alter this program of employee handling and provide some behavior to it:

  • Step 1: Create a Handler that uses a Trap

We will be using a trap called get which is a trap for getting a property value. Here is our Handler:

let handler = {
    get: function(target, fieldName) {       
 
        if(fieldName === 'fullName' ) {
            return `${target.firstName} ${target.lastName}`;
        }
 
        return fieldName in target ?
            target[fieldName] :
                `No such property as, '${fieldName}'!`
 
    }
};

The above handler helps to create the value for fullName property. It also adds a better error message in case, we are dealing with a missing property.

  • Step 2: Create a Proxy Object

As we have the target as employee object and the handler, we will be able to create a Proxy object as:

let p = new Proxy(employee, handler);
  • Step 3: Access the properties on the Proxy object


console.group('proxy');
    console.log(p.firstName);
    console.log(p.lastName);
    console.log(p.org);
    console.log(p.fullName);
console.groupEnd()

You should be seeing the output as,

proxy
  Tapas
  Adhikary
  No such property as, 'org'!
  Tapas Adhikary

Notice how we have magically changed things for the employee object.

In Previous example, we used a trap called get. Here are the list of available traps:

  • apply
  • construct
  • defineProperty
  • deleteProperty
  • get
  • getOwnPropertyDescriptor
  • getPrototypeOf
  • has
  • isExtensible
  • ownKeys
  • preventExtensions
  • set
  • setPrototypeOf

More on these can be found here, Proxy - JavaScript | MDN

Proxy for Validation of Values

Let's create a handler(we can name it as, validator):

const validator = {
    set: function(obj, prop, value) {
        if (prop === 'age') {
            if(!Number.isInteger(value)) {
                throw new TypeError('Age is always an Integer, Please Correct it!');
            }
            if(value < 0) {
                throw new TypeError('This is insane, a negative age?');
            }
        }
    }
};

Again, we can create a Proxy object as:

let p = new Proxy(employee, validator); 

If you do,

p.age = 'I am testing the blunder'; 

The output would be a TypeError as,

TypeError: Age is always an Integer, Please Correct it!
    at Object.set (E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:28:23)
    at Object.<anonymous> (E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:40:7)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)
    at startup (bootstrap_node.js:188:16)
    at bootstrap_node.js:609:3

Similarly, try doing this!

p.age = -1;

Use-cases

Proxy Object is a very powerful concept. There are several use-cases where this concept can be used. Here are few:

  • Protect ID field from deletion from an Object(trap: deleteProperty)
  • Tracing Property Accesses(trap: get, set)
  • Data Binding(trap: set)
  • Revocable references
  • Manipulate the in operator behavior

... and many many more.

Last Note

Hope you liked the concept of Proxy Object. Try it out, it is Fun! Feel free to access the examples from My Github Repo.

'Proxy' is not the only concept for JavaScript based Metaprogramming, there are are others like, Reflect. That is coming soon.

Originally published by Tapas Adhikary at blog.greenroots.info

===========================================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Learn More

☞ Svelte.js - The Complete Guide

☞ The Complete JavaScript Course 2019: Build Real Projects!

☞ Become a JavaScript developer - Learn (React, Node,Angular)

☞ JavaScript: Understanding the Weird Parts

☞ JavaScript: Coding Challenges Bootcamp - 2019

☞ The Complete Node.js Developer Course (3rd Edition)

☞ Angular & NodeJS - The MEAN Stack Guide

☞ NodeJS - The Complete Guide (incl. MVC, REST APIs, GraphQL)

☞ Node.js Absolute Beginners Guide - Learn Node From Scratch

Understanding Generators in ES6 Javascript

Understanding Generators in ES6 Javascript

Maybe you have heard about this feature in ES6 or you just didn’t have time to play with it.

Maybe you have heard about this feature in ES6 or you just didn’t have time to play with it.

It is something, that I have learned a few days ago and it's super cool.

Let me explain it to you in a few steps.

So Generators looks like a similar function, but it allows us to pause the execution of the function and continue it later.

So below you can see an example of the generator and we will break it down to see how it works:

function* avengersGenerator() { // Declaring the generator
  yield "Hulk"; // Pausing the execution
  yield "Thor";
  yield "Iron man";
  return "Ultron"; // Example of finishing the generator
  yeild "Spider man";
}

const iterator = avengersGenerator(); // Creating iterator

iterator.next(); // Iterating on the generator


Declaring the generator

Generators look similar to a normal function, the only difference is we have to define an * (asterisk) after the word function.

function* avengersGenerator() {
  ...
}

Yield it!

We can yield the function, which basically would stop the execution of the function when it gets to the first yield.

function* avengersGenerator() {
  yield "Hulk" // The execution would pause here.
  yield "Iron man" // When we resume we would start here.
}


Creating the iterator

On iterator, we can call. So by this, we will prepare our generator for action.

const iterator = avengersGenerator();

Next method

This enables us to continue the execution of the function. Also, this method provides us with the object with the yielded value and whether the generator has yielded its last value, as a boolean.

iterator.next(); // [1] Object {value: "Hulk", done: false}
iterator.next(); // [2] Object {value: "Thor", done: false}
iterator.next(); // [3] Object {value: "Iron man", done: false}
iterator.next(); // [4] Object {value: "undefined", done: true}

Return / Exiting

Once a return is being called, it would finish the generator. It basically sets the done property to true.

function* avengersGenerator() {
  yield "Hulk";
  return "Ultron"; // Example of finishing the generator
  yield "Thor"; //  Sad Thor and Spiderman wouldn't be called
  yield "Spiderman";
}

iterator.next(); // [1] Object {value: "Hulk", done: false}
iterator.next(); // [2] Object {value: "Utron", done: false}
iterator.next(); // [3] Object {value: "undefined", done: true}

In my opinion, generators are quite a cool thing to play with or at least to know what it does.

In the next post, I will explain, how generators helped me to solve one case at my work thanks to the possibility to cancel the promise when needed with generators in ES6.