ES6 features you can use with Vue now

ECMAScript 6, the so-called modern JavaScript, is packed with powerful features such as block scope, class, arrow function, generator, and many other useful things.

You might be thinking that you can’t use them because there is a lack of support for IE 11. The good news is: you can actually use most of these features with the help of Babel and core-js, which are already available to you in a Vue CLI-generated app.

Here are three categories of these ES6 features (categorized by browser compatibility):

  • Features you can use and never have to worry about compatibility (when Babel and core-js are used)
  • Features you can use but you would need to drop supports for IE 11 (and lower) because Babel and core-js won’t transpile/polyfill them for you, mainly proxy and subclassing of native types.
  • Features that even Chrome and Firefox don’t currently support, namely Tail Call Optimization.

In this article, we’ll be focusing on the first category: all the essential features that you can use in your Vue apps to improve your programming experience.

Here’s the list of ES6 features we’ll go through:


IE 11

Before we begin, let’s shed some light at how Vue is supporting Internet Explorer 11.

Let’s create a new Vue app for demonstration:

vue create es6-app

Choose the Default Vue 2 option from the prompt. (There’s no IE 11 support for Vue 3 because the Composition API is using Proxy, which isn’t supported by babel/core-js.)

If you search inside the package.json file, you’ll find a field called browserslist:

"browserslist": [
  "> 1%",
  "last 2 versions",
  "not dead"
]

There’s a lot of magic going on under the hood, and we’ll get into the details of transpilation and polyfill in another article. For now, just be aware that this is the config that tells babel what browsers to support. This default config covers many outdated browsers, including IE 11.

If we empty out the "browserslist" array, there will be no support for IE 11 if we intend to use any of the ES6 features we’ll get into in this article. Actually, that’s not entirely true, IE 11 does support a few ES6 features, such as the let and const keyword.


let / const

Let’s start with the most ubiquitous features of ES6: let and const. They’re so ubiquitous that even IE 11 supports them.

let is like var but the variables declared with let are scoped within the block where they’re declared. (“Block” refers to conditional block, for loop block, etc.)

For example, using let in a conditional block will scope the variable within the block, and it will not be available outside of it.

if(true){
  let foo = 'word'
}

console.log(foo) // error

Error is a good thing here, since it prevents potential bugs happening during production.

If you use var (like in traditional JavaScript code) instead of let in the above example, there will not be an error.

const is another ES6 keyword for declaring variables. The difference is that a variable created by const can not be changed after declaration.

For example:

const a = 1
a = 2 // error

With several ways of creating variables, which one should we use?

The best practice is to use const whenever you can. Use let only when you need to have a variable that needs to be changed later on, such as in a for loop.

And avoid using var altogether.


for…of

Speaking of loop, there’s an easier way to write for loops in ES6 syntax without even using let.

For example, a traditional for loop like this:

const arr = [1, 2, 3]

for(let i=0; i < arr.length; i++){
  const item = arr[i]
  console.log(item)
}

In ES6, we can simply do:

const arr = [1, 2, 3]

for(const item of arr){
  console.log(item)
}

We’re using the for..of syntax here.

Not to be confused with the for..in syntax; they are very different things. for..in will get you the properties in an array/object, but for..of will get the data that you’re actually intending to iterate.

We can use for..of in many different kinds of objects. They just need to be iterable.


Iterable

An iterable object is any object that implements the iterable protocol. (protocol just means requirement that you need to satisfy by having a certain kind of method with a certain name inside an object.)

For example, here’s an object that implements the iterable protocol:

const twice = {
  [Symbol.iterator]() {
    let i = 0;
    const iterator = {
      next() {
        if(i < 2){
          return { value: i++, done: false }
        }
        else{
          return { value: undefined, done: true };
        }
      }
    }
    return iterator
  }
}

I’ll explain the details in a bit, but now we can use this twice object in a for..of loop:

for(const x of twice){
  console.log(x)
}

This should loop through the twice object twice, giving you 0 and 1 respectively.

Now let’s break down the code… To create an iterable object, we actually implemented two protocols, the iterable protocol and the iterator protocol.

To satisfy the requirements of being an iterable object, we need a method with the name [Symbol.iterator].

const twice = {
  [Symbol.iterator]() {
    ...
  }
}

There are two new ES6 tricks applied in the method name.

First, Symbol.iterator is a built-in Symbol value, and Symbol is a primitive type in ES6 for creating unique labels/identifiers. (I’ll talk more about the Symbol type in a dedicated section below.)

Secondly, the square brackets wrapping the property key makes it a dynamically computed key. Here the key is whatever the expression Symbol.iterator will be evaluated to, and we usually don’t care what the actual evaluated value is. This unimportant detail is abstracted away.


So that was the iterable protocol. Now we still need to cope with the iterator protocol to create an iterable object, because we have to return an iterator from the [Symbol.iterator] function.

The iterator protocol is simpler. We just need an object to have a next method that returns an object with two keys: value and done. When you want to stop iterating, simply return the object { value: undefined, done: true }.

Here’s the iterator from our example:

const iterator = {
  next() {
    if(i < 2){
      return { value: i++, done: false }
    }
    else{
      return { value: undefined, done: true };
    }
  }
}

Together, we have an object that satisfies both the iterable protocol and iterator protocol.

Here’s the code again:

const twice = {
  [Symbol.iterator]() {
    let i = 0;
    const iterator = {
      next() {
        if(i < 2){
          return { value: i++, done: false }
        }
        else{
          return { value: undefined, done: true };
        }
      }
    }
    return iterator
  }
}

On a side note, arrays and strings can be iterated with for..of. That means these built-in types contain a [Symbol.iterator] method like the one above.


Generator

Another feature related to iteration is generator.

Our iterable code above relies on closure to memorize the i variable. With generator, we don’t have to worry about constructing the closure ourselves:

function* twiceGen(){
  let i = 0
  while(i < 2){
    yield i
    i++
  }
}

const twice = twiceGen()

This code implements the same behavior that we have with the iterable example, but much simpler.

We can use it in the exact same way with for..of:

for(const item of twice){
  console.log(item)
}

Let’s rewind a little and talk about what a generator is.

As you can see, it’s a function declared with an asterisk (*). It’s using the yield keyword to pump values one by one like what an iterator’s next method does.

Generator is a versatile tool, basically, it’s a mechanism that allows you to pause/resume a function. We don’t have to use the twice object above with for..of. We can just call its next method.

function* twiceGen(){
  let i = 0
  while(i < 2){
    yield i
  }
}

const twice = twiceGen()

twice.next().value // 0 

At this point, the twiceGen function is paused after the first run of the while loop. And if we run the same operation again, it will resume and play the second run of the loop.

twice.next().value // 1

The cool thing about generator is that it’s also creating an iterable and iterator object. That’s why we were able to iterate twice with for..of (an iterable perk) and call its next method directly (an iterator perk). And we got the iterable and iterator protocols implemented for free without messing around with [Symbol.iterator].

As I said, generator is a versatile tool. You can use it as a pause/resume mechanism, you can use it as an alternative to closure, and you can use it as a shortcut for creating iterable objects.


Symbol

Now let’s circle back and talk about the Symbol type.

To create a Symbol typed value, we just need call Symbol():

const name = Symbol()
const version = Symbol()

The primary use case of Symbol values is for object property keys:

const language = {
  [name]: 'ES',
  [version]: '6',
}

Now, to retrieve a property, we just have to access it with the right Symbol value:

language[name]

So what’s the benefit of using Symbol values as keys over using plain strings?

If we have a long descriptive key name like theMostPopularImplementationOfThisLanguage, we can just use it once for creating the property:

const theMostPopularImplementationOfThisLanguage = Symbol()

const language = {
  ...
  [theMostPopularImplementationOfThisLanguage]: 'JavaScript'
}

And from this point on, we can just assign it to a shorter variable name:

const impl = theMostPopularImplementationOfThisLanguage

And use the shorter name instead for accessing the property:

language[impl]

As an alternative, we can also put down the long name as an argument when creating the Symbol value:

const impl = Symbol('theMostPopularImplementationOfThisLanguage')

const language = {
  ...
  [impl]: 'JavaScript'
}

(Note that you can forgo the long name altogether, but the code wouldn’t be as readable. Someone else reading the code wouldn’t know what impl is supposed to be.)


Default Parameter

You might not be creating your own iterators, generators, or symbols rightaway, so let’s check out some other ES6 ingenuities that can instantly make your life easier.

Just like in many other programming languages, we can now assign default values to function parameters.

Instead of doing this:

function addOne(num){
  if(num === undefined){ 
    num = 0
  }
  return num + 1
}

addOne()

Now we can just do this:

function addOne(num = 0){
  return num + 1
}

addOne()

Destructuring Syntax

If you are passing an object to a function, you can easily pick out the object’s properties and put them in separate variables with the ES6 destructuring syntax:

function foo({ a, b }){
  console.log(a, b) // 1, 2
}

foo({ a: 1, b: 2 })

The benefit of this destructuring syntax is to avoid the need to create variables with additional lines of code.

So no need to do this anymore:

function foo(obj){
  const a = obj.a
  const b = obj.b
  console.log(a, b) // 1, 2
}

You can also set default values within the destructuring syntax:

function foo({ a = 0, b }){
  console.log(a, b) // 0, 2
}

foo({ b: 2 })

The destructuring syntax works on assignments too:

function foo(obj){
  const { a, b } = obj
  console.log(a, b) // 1, 2
}

This is also useful when you’re getting the object from places other than the parameter.

function getObj(){
  return { a: 1, b: 2 }
}

function foo(){
  const { a, b } = getObj()
  console.log(a, b) // 1, 2
}

These destructuring tricks work on arrays too, not just objects.

Destructuring parameter:

function foo([ a, b ]){
  console.log(a, b) // 1, 2
}

foo([1, 2, 3])

Destructuring assignment:

function foo(arr){
  const [ a, b ] = arr
  console.log(a, b) // 1, 2
}

Rest / Spread

When destructuring an array, we can use the three-dot syntax to get all the rest of the items in the array.

function foo([ a, b, ...c ]){
  console.log(c) // [3, 4, 5]
}

foo([1, 2, 3, 4, 5])

c is now an array of its own that contains the rest of the items: 3, 4, 5.

This three-dot syntax is called the rest operator.

This works with assignment as well:

function foo(arr){
  const [ a, b, ...c ] = arr
  console.log(c) // [3, 4, 5]
}

foo([1, 2, 3, 4, 5])

The rest operator can be used alone without destructuring, too:

function foo(...nums){
  console.log(nums) // [1, 2, 3, 4, 5]
}

foo(1, 2, 3, 4, 5)

Here, we’re passing the numbers as standalone arguments, not as a single array. But inside the function, we’re using the rest operator to gather of the numbers as a single array. This is useful when we want to loop through these arguments.

The rest syntax (the three-dot thing) looks exactly the same as another ES6 feature operator spread.

For example, if we want to combine two arrays into one:

const a = [ 1, 2 ]
const b = [ 3, 4 ]
const c = [ ...a, ...b ]
console.log(c) // [1, 2, 3, 4]

The spread operator is for spreading out all the items and put them into a different array.

Spread works with object as well:

const obj = { a: 1, b: 2 }
const obj2 = { ...obj, c: 3 }
console.log(obj2) // { a: 1, b: 2, c: 3 }

Now the second object should contain everything from the first object in addition to its own property.

(Technically all these spread and rest tricks for objects are ES9 features. ES6 only allows spread and rest to be used with array.)


Everything so far

We’ve talked about:

  • using const whenever you can
  • using for..of with iterable objects
  • using generator to create iterable and iterator
  • using default values on parameters
  • using the destructuring syntax in various ways.
  • and using rest and spread

As two seemingly unrelated concepts, iterable objects and the destructuring syntax are actually compatible with each other:

function* twiceGen(){
  let i = 0
  while(i < 2){
    yield i
    i++
  }
}

const twice = twiceGen() // an iterable

const [ a, b ] = twice // destructuring

Now a will be 0, and b will be 1.


Arrow Function

ES6 offers simpler ways to create functions, objects, and classes.

We can use the arrow syntax to create more concise functions:

const addOne = (num) => {
  return num + 1
}

This arrow syntax is most useful for creating a one-line function:

const addOne = (num) => num + 1

This function will automatically return the evaluated value of the expression num + 1 as the return value. No explicit return keyword needed.

We can even omit the parentheses if the function only accepts a single parameter:

const addOne = num => num + 1

We would still need a pair of empty parentheses if there aren’t any parameters:

const getNum = () => 1

However, there is a caveat with this syntax. If we’re returning an object literal, this wouldn’t work:

const getObj = () => { a: 1, b: 2 } // error

This will produce a syntax error because the parser would assume the curly brackets are meant for the function block, not the object literal.

To fix this, we have to wrap the object literal in a pair of parentheses:

const getObj = () => ({ a: 1, b: 2 })

The added parentheses are basically an explicit sign to the parser that we’re intending to use the one-line function syntax.

Another thing to keep in mind is that the this keyword wouldn’t work inside an arrow function. It would not give you an error; instead, it would just give you the same this reference from the surrounding scope.

function x() {
  const that = this
  const y = () => {
    console.log(that === this) // true
  }
  y()
}

x()

So each this here are the same reference.


Object literal extensions

ES6 offers a simpler way to create an object literal as well.

If you want to put two items into an object, with the same property keys as the variables, you would do something like this with traditional JavaScript:

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

But in ES6, the syntax can be much simpler:

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

And if you want to put methods in an object literal, you can just do this:

const a = 1
const b = 2
const obj = { a, b, 
  getA() {
    return this.a
  },
  getB() {
    return this.b
  }
}

(Basically, without the function keyword and the colon.)


Class

ES6 offers a class construct similar to that of other object-oriented languages. Now we don’t have to rely on messing with constructors and prototypes.

class Person {
  constructor(name, hobby){
    this.name = name
    this.hobby = hobby
  }

  introduce(){
    console.log(`Hi, my name is ${this.name}, and I like ${this.hobby}.`)
  }
}

const andy = new Person('Andy', 'coding')
andy.introduce()

As a side note, the string in the introduce method is called a template string, and it’s created using backticks instead of quotes. As you can see, we can inject expressions into the string using the dollar sign and curly brackets.

Another benefit of template string over regular strings is that it can span multiple lines:

const str = `line 1
line 2
line 3
`

It’s called template string because it’s useful for implementing a template.

function p(text){
  return `<p>${text}</p>`
}

p("Hello world")

Let’s get back to talking about class.

A class can inherit from another class (reusing the code from an existing class):

class Person {
  ...
}

class ProfessionalPerson extends Person {
  constructor(name, hobby, profession){
    super(name, hobby) // class parent's constructor()
    this.profession = profession
  }

  introduce(){
    super.introduce() // call parent's introduce()
    console.log(`And my profession is ${this.profession}.`)
  }
}

const andy = new ProfessionalPerson('Andy', 'coding', 'coding')

We’re using the extends keyword to create an inheritance relationship between the two classes, with Person as the parent class.

We’re using the super keyword here twice. The first time by itself in the constructor, that was for calling the parent class’s constructor. The second time, we’re using it like an object to invoke the parent class’s introduce method. It’s a keyword that behaves differently depending on where you’re using it.


Map / Set / WeakMap / WeakSet

ES6 comes with two novel data structures: Map and Set.

Map is a collection of key-val pairs:

const m = new Map()
m.set('first', 1)
m.set('second', 2)
m.get('first') // 1

Map objects can use any object types as the keys.


A Set object is like an array, but only contains unique items:

const s = new Set()
s.add(1)
s.add(1)

Although we inserted twice, the set still contains only one item because we inserted the same thing twice.


Let’s talk about something more complex, WeakMap and WeakSet. They are weakly-referenced versions of Map and Set. We can only use objects as keys for WeakMap, and we can only add objects to WeakSet.

A WeakMap’s items will be garbage-collected (removed from memory by the JavaScript runtime) once their keys are no longer being referenced.

For example:

let key1 = {}
let key2 = {}
const m = new WeakMap()
m.set(key1, 1)
m.set(key2, 2)
key1 = null // de-referenced

After key1’s de-referenced, its corresponding value will be scheduled for garbage collection, which means it will be gone at some point in the future.

By the same token, if we add an object to a WeakSet, and later de-reference it, it will get garbage-collected, too.

let item1 = {}
let item2 = {}
const s = new WeakSet()
s.add(item1)
s.add(item2)
item1 = null // de-referenced

Although we added two items, the set should only contain one item after garbage collection because the original item1 object is no longer referenced by a variable.


Promise

Last but not least, Promise is another commonly-used ES6 feature. It serves as an improvement over the traditional function callback pattern.

For example, here’s a traditional way of using callback:

setTimeout(function(){
  const currentTime = new Date()
  console.log(currentTime)
}, 1000)

It’s a timer that shows the time after one second.

Here’s a promise object using the same setTimeout logic:

const afterOneSecond = new Promise(function(resolve, reject) {
  setTimeout(function(){
    const currentTime = new Date()
    resolve(currentTime)
  }, 1000)
})

It accepts a function with two parameters: resolve and reject. Both of these are functions that we can call when we have something to return. We call the resolve function to return a value, and we can call the reject function to return an error.

Then we can attach a callback to this afterOneSecond Promise object using the then syntax:

afterOneSecond.then(t => console.log(t))

(The one-line arrow function syntax is just conventional, it’s not required.)

The benefit of promise over traditional callback is that a promise object can be passed around. So after setting up the promise, we have the freedom of sending it somewhere else for handling what to do after the timer is resolved.

const afterOneSecond = new Promise(function(resolve, reject) {
  setTimeout(function(){
    const currentTime = new Date()
    resolve(currentTime)
  }, 1000)
})

doSomethingAfterTheTimerResolved(afterOneSecond)

Another cool thing is that promise can be chained with multiple then clauses:

afterOneSecond
  .then(t => t.getTime())
  .then(time => console.log(time))

Each then clause will return its value to the next then clause as the parameter.


Useful Methods

Here’s a selected list of useful ES6 methods added to the existing types.

Object.assign (static method)

This method offers a simple way to shallowly clone an existing object:

const obj1 = { a: 1 }
const obj2 = Object.assign({}, obj1)

String.prototype.repeat (instance method)

Returns a repeated string:

'Hello'.repeat(3) // "HelloHelloHello"

String.prototype.startsWith (instance method)

'Hello'.startsWith('H') // true

String.prototype.endsWith (instance method)

'Hello'.endsWith('o') // true

String.prototype.includes (instance method)

'Hello'.includes('e') // true

Array.prototype.find (instance method)

Returns the first item where the callback function returns true

[1, 2, 3].find(x => {
  return x > 1
})

// 2

Function.name (property)

This one is not a method, but a property. Each function now has a name property that gives you the name of the function as a string.

setTimeout.name // "setTimeout"

Including the functions you create yourself:

function foo(){}
foo.name // "foo"

More ES6

There are some ES6 features that I left out either because they’re not essential to everyday Vue development or because they can’t be transpiled/polyfilled by Babel/core-js.

  • Reflect and Proxy (Proxy can’t be transpiled/polyfilled)
  • Subclassing of native types (can’t be transpiled/polyfilled)
  • Tail call optimization (can’t be transpiled/polyfilled)
  • The y and u flags for RegExp
  • Octal/binary literals
  • Typed array
  • Block-level function declarations

Conclusion


ES6 is cool and it’s ready, so you should be using it in your code now. With the Vue CLI’s Babel/core-js integration, you can use all of these features we’ve covered here even if your app has to support IE 11.

In this article:

Dive Deeper into Vue today

Access our entire course library with a special discount.

Get Deal

Download the cheatsheets

Save time and energy with our cheat sheets.