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:
- let / const
- for…of
- Iterable
- Generator
- Symbol
- Default Parameter
- Destructuring Syntax
- Rest / Spread
- Arrow Function
- Object Literal Extensions
- Class
- Map / Set / WeakMap / WeakSet
- Promise
- Useful Methods
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
andu
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.