JavaScript在过去几年发生了很大的变化。 这篇文章整理了 JS 的12个新特性,你今天就开始使用!ES6是JavaScript语言的下一个版本,也称作 ECMAScript 6 和 ES2015+。ES6中包含了许多新的语言特性,它们将使JS变得更加强大,更富表现力。
JavaScript 历史
The new additions to the language are called ECMAScript 6. It is also referred as ES6 or ES2015+.
Since JavaScript conception on 1995, it has been evolving slowly. New additions happened every few years. ECMAScript came to be in 1997 to guide the path of JavaScript. It has been releasing versions such as ES3, ES5, ES6 and so on.
As you can see, there are gaps of 10 and 6 years between the ES3, ES5, and ES6. The new model is to make small incremental changes every year. Instead of doing massive changes at once like happened with ES6.
浏览器兼容
All modern browser and environments support ES6 already!
source: https://kangax.github.io/compat-table/es6/
Chrome, MS Edge, Firefox, Safari, Node and many others have already built-in support for most of the features of JavaScript ES6. So, everything that you are going to learn in this tutorial you can start using it right now.
Let’s get started with ECMAScript 6!
ES6 核心特性
You can test all these code snippets on your browser console!
So don’t take my word and test every ES5 and ES6 example. Let’s dig in
Block scope variables
With ES6, we went from declaring variables with var
to use let
/const
.
What was wrong with var
?
The issue with var
is the variable leaks into other code block such as for
loops or if
blocks.
1
2
3
4
5
6
7
8
9
10
11
|
var x = ‘outer’;
function test(inner) {
if (inner) {
var x = ‘inner’; // scope whole function
return x;
}
return x; // gets redefined because line 4 declaration is hoisted
}
test(false); // undefined
test(true); // inner
|
For test(false)
you would expect to return outer
, BUT NO, you get undefined
.
Why?
Because even though the if-block is not executed, the expression var x
in line 4 is hoisted.
var hoisting:
var
is function scoped. It is availble in the whole function even before being declared.- Declarations are Hoisted. So you can use a variable before it has been declared.
- Initializations are NOT hoisted. If you are using
var
ALWAYS declare your variables at the top.- After applying the rules of hoisting we can understand better what’s happening:
ES5
123456789 var x = ‘outer’;function test(inner) {var x; // HOISTED DECLARATIONif (inner) {x = ‘inner’; // INITIALIZATION NOT HOISTEDreturn x;}return x;}
ECMAScript 2015 comes to the rescue:
1
2
3
4
5
6
7
8
9
10
11
|
let x = ‘outer’;
function test(inner) {
if (inner) {
let x = ‘inner’;
return x;
}
return x; // gets result from line 1 as expected
}
test(false); // outer
test(true); // inner
|
Changing var
for let
makes things work as expected. If the if
block is not called the variable x
doesn’t get hoisted out of the block.
Let hoisting and “temporal dead zone”
- In ES6,
let
will hoist the variable to the top of the block (NOT at the top of function like ES5).- However, referencing the variable in the block before the variable declaration results in a
ReferenceError
.let
is blocked scoped. You cannot use it before it is declared.- “Temporal dead zone” is the zone from the start of the block until the variable is declared.
IIFE
Let’s show an example before explaining IIFE. Take a look here:
1
2
3
4
5
|
{
var private = 1;
}
console.log(private); // 1
|
As you can see, private
leaks out. You need to use IIFE (immediately-invoked function expression) to contain it:
1
2
3
4
5
|
(function(){
var private2 = 1;
})();
console.log(private2); // Uncaught ReferenceError
|
If you take a look at jQuery/lodash or other open source projects you will notice they have IIFEto avoid polluting the global environment and just defining on global such as _
, $
orjQuery
.
On ES6 is much cleaner, We also don’t need to use IIFE anymore when we can just use blocks and let
:
1
2
3
4
5
|
{
let private3 = 1;
}
console.log(private3); // Uncaught ReferenceError
|
Const
You can also use const
if you don’t want a variable to change at all.
Bottom line: ditch
var
forlet
andconst
.
- Use
const
for all your references; avoid usingvar
.- If you must reassign references, use
let
instead ofconst
.
Template Literals
We don’t have to do more nesting concatenations when we have template literals. Take a look:
1
2
3
|
var first = ‘Adrian’;
var last = ‘Mejia’;
console.log(‘Your name is ‘ + first + ‘ ‘ + last + ‘.’);
|
Now you can use backtick (`) and string interpolation ${}
:
1
2
3
|
const first = ‘Adrian’;
const last = ‘Mejia’;
console.log(`Your name is ${first} ${last}.`);
|
Multi-line strings
We don’t have to concatenate strings + \n
anymore like this:
1
2
3
4
5
6
7
8
9
|
var template = ‘<li *ngFor=”let todo of todos” [ngClass]=”{completed: todo.isDone}” >\n’ +
‘ <div class=”view”>\n’ +
‘ <input class=”toggle” type=”checkbox” [checked]=”todo.isDone”>\n’ +
‘ <label></label>\n’ +
‘ <button class=”destroy”></button>\n’ +
‘ </div>\n’ +
‘ <input class=”edit” value=””>\n’ +
‘</li>’;
console.log(template);
|
On ES6 we can use the backtick again to solve this:
1
2
3
4
5
6
7
8
9
|
const template = `<li *ngFor=”let todo of todos” [ngClass]=”{completed: todo.isDone}” >
<div class=”view”>
<input class=”toggle” type=”checkbox” [checked]=”todo.isDone”>
<label></label>
<button class=”destroy”></button>
</div>
<input class=”edit” value=””>
</li>`;
console.log(template);
|
Both pieces of code will have exactly the same result.
Destructuring Assignment
ES6 desctructing is very useful and consise. Follow this examples:
Getting elements from an arrays
1
2
3
4
5
6
|
var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third); // 1 3
|
Same as:
1
2
3
4
5
|
const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third); // 1 3
|
Swapping values
1
2
3
4
5
6
7
8
|
var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b); // 2 1
|
same as
1
2
3
4
5
6
|
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
|
Destructuring for multiple return values
1
2
3
4
5
6
7
8
9
10
|
function margin() {
var left=1, right=2, top=3, bottom=4;
return { left: left, right: right, top: top, bottom: bottom };
}
var data = margin();
var left = data.left;
var bottom = data.bottom;
console.log(left, bottom); // 1 4
|
In line 3, you could also return it in an array like this (and save some typing):
1
|
return [left, right, top, bottom];
|
but then, the caller needs to think about the order of return data.
1
2
|
var left = data[0];
var bottom = data[3];
|
With ES6, the caller selects only the data they need (line 6):
1
2
3
4
5
6
7
8
|
function margin() {
const left=1, right=2, top=3, bottom=4;
return { left, right, top, bottom };
}
const { left, bottom } = margin();
console.log(left, bottom); // 1 4
|
Notice: Line 3, we have some other ES6 features going on. We can compact { left: left }
to just { left }
. Look how much concise it is compare to the ES5 version. Isn’t that cool?
Destructuring for parameters matching
1
2
3
4
5
6
7
8
9
10
|
var user = {firstName: ‘Adrian’, lastName: ‘Mejia’};
function getFullName(user) {
var firstName = user.firstName;
var lastName = user.lastName;
return firstName + ‘ ‘ + lastName;
}
console.log(getFullName(user)); // Adrian Mejia
|
Same as (but more concise):
1
2
3
4
5
6
7
|
const user = {firstName: ‘Adrian’, lastName: ‘Mejia’};
function getFullName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}
console.log(getFullName(user)); // Adrian Mejia
|
Deep Matching
1
2
3
4
5
6
7
8
9
|
function settings() {
return { display: { color: ‘red’ }, keyboard: { layout: ‘querty’} };
}
var tmp = settings();
var displayColor = tmp.display.color;
var keyboardLayout = tmp.keyboard.layout;
console.log(displayColor, keyboardLayout); // red querty
|
Same as (but more concise):
1
2
3
4
5
6
7
|
function settings() {
return { display: { color: ‘red’ }, keyboard: { layout: ‘querty’} };
}
const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();
console.log(displayColor, keyboardLayout); // red querty
|
This is also called object destructing.
As you can see, destructing is very useful and encourages good coding styles.
Best practices:
- Use array destructing to get elements out or swap variables. It saves you from creating temporary references.
- Don’t use array destructuring for multiple return values, instead use object destructuring
Classes and Objects
With ECMAScript 6, We went from “constructor functions” to “classes” .
In JavaScript every single object has a prototype, which is another object. All JavaScript objects inherit their methods and properties from their prototype.
In ES5, we did Object Oriented programming (OOP) using constructor functions to create objects as follows:
1
2
3
4
5
6
7
8
9
10
11
12
|
var Animal = (function () {
function MyConstructor(name) {
this.name = name;
}
MyConstructor.prototype.speak = function speak() {
console.log(this.name + ‘ makes a noise.’);
};
return MyConstructor;
})();
var animal = new Animal(‘animal’);
animal.speak(); // animal makes a noise.
|
In ES6, we have some syntax sugar. We can do the same with less boiler plate and new keywords such as class
and constructor
. Also, notice how clearly we define methodsconstructor.prototype.speak = function ()
vs speak()
:
1
2
3
4
5
6
7
8
9
10
11
|
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + ‘ makes a noise.’);
}
}
const animal = new Animal(‘animal’);
animal.speak(); // animal makes a noise.
|
As we saw, both styles (ES5/6) produces the same results behind the scenes and are used in the same way.
Best practices:
- Always use
class
syntax and avoid manipulating theprototype
directly. Why? because it makes the code more concise and easier to understand.- Avoid having an empty constructor. Classes have a default constructor if one is not specified.
Inheritance
Building on the previous Animal
class. Let’s say we want to extend it and define a Lion
class
In ES5, It’s a little more involved with prototypal inheritance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
var Lion = (function () {
function MyConstructor(name){
Animal.call(this, name);
}
// prototypal inheritance
MyConstructor.prototype = Object.create(Animal.prototype);
MyConstructor.prototype.constructor = Animal;
MyConstructor.prototype.speak = function speak() {
Animal.prototype.speak.call(this);
console.log(this.name + ‘ roars ‘);
};
return MyConstructor;
})();
var lion = new Lion(‘Simba’);
lion.speak(); // Simba makes a noise.
// Simba roars.
|
I won’t go over all details but notice:
- Line 3, we explicitly call
Animal
constructor with the parameters. - Line 7-8, we assigned the
Lion
prototype toAnimal
‘s prototype. - Line 11, we call the
speak
method from the parent classAnimal
.
In ES6, we have a new keywords extends
and super
.
1
2
3
4
5
6
7
8
9
10
|
class Lion extends Animal {
speak() {
super.speak();
console.log(this.name + ‘ roars ‘);
}
}
const lion = new Lion(‘Simba’);
lion.speak(); // Simba makes a noise.
// Simba roars.
|
Looks how legible this ES6 code looks compared with ES5 and they do exactly the same. Win!
Best practices:
- Use the built-in way for inherintance with
extends
.
Native Promises
We went from callback hell to promises
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function printAfterTimeout(string, timeout, done){
setTimeout(function(){
done(string);
}, timeout);
}
printAfterTimeout(‘Hello ‘, 2e3, function(result){
console.log(result);
// nested callback
printAfterTimeout(result + ‘Reader’, 2e3, function(result){
console.log(result);
});
});
|
We have one function that receives a callback to execute when is done
. We have to execute it twice one after another. That’s why we called the 2nd time printAfterTimeout
in the callback.
This can get messy pretty quickly if you need a 3rd or 4th callback. Let’s see how we can do it with promises:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
function printAfterTimeout(string, timeout){
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve(string);
}, timeout);
});
}
printAfterTimeout(‘Hello ‘, 2e3).then((result) => {
console.log(result);
return printAfterTimeout(result + ‘Reader’, 2e3);
}).then((result) => {
console.log(result);
});
|
As you can see, with promises we can use then
to do something after another function is done. No more need to keep nesting functions.
Arrow functions
ES6 didn’t remove the function expressions but it added a new one called arrow functions.
In ES5, we have some issues with this
:
1
2
3
4
5
6
7
8
9
|
var _this = this; // need to hold a reference
$(‘.btn’).click(function(event){
_this.sendData(); // reference outer this
});
$(‘.input’).on(‘change’,function(event){
this.sendData(); // reference outer this
}.bind(this)); // bind to outer this
|
You need to use a temporary this
to reference inside a function or use bind
. In ES6, you can use the arrow function!
1
2
3
4
5
6
|
// this will reference the outer one
$(‘.btn’).click((event) => this.sendData());
// implicit returns
const ids = [291, 288, 984];
const messages = ids.map(value => `ID is ${value}`);
|
For…of
We went from for
to forEach
and then to for...of
:
1
2
3
4
5
6
7
8
9
10
11
|
// for
var array = [‘a’, ‘b’, ‘c’, ‘d’];
for (var i = 0; i < array.length; i++) {
var element = array[i];
console.log(element);
}
// forEach
array.forEach(function (element) {
console.log(element);
});
|
The ES6 for…of also allow us to do iterations.
1
2
3
4
5
|
// for …of
const array = [‘a’, ‘b’, ‘c’, ‘d’];
for (const element of array) {
console.log(element);
}
|
Default parameters
We went from checking if the variable was defined to assign a value to default parameters
. Have you done something like this before?
1
2
3
4
5
6
7
8
9
10
11
|
function point(x, y, isFlag){
x = x || 0;
y = y || -1;
isFlag = isFlag || true;
console.log(x,y, isFlag);
}
point(0, 0) // 0 -1 true
point(0, 0, false) // 0 -1 true
point(1) // 1 -1 true
point() // 0 -1 true
|
Probably yes, it’s a common pattern to check is the variable has a value or assign a default. Yet, notice there are some issues:
- Line 8, we pass
0, 0
and get0, -1
- Line 9, we pass
false
but gettrue
.
If you have a boolean as a default parameter or set the value to zero, it doesn’t work. Do you know why??? I’ll tell you after the ES6 example 😉
With ES6, Now you can do better with less code!
1
2
3
4
5
6
7
8
|
function point(x = 0, y = -1, isFlag = true){
console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true
|
Notice line 5 and 6 we get the expected results. The ES5 example didn’t work. We have to check for undefined
first since false
, null
, undefined
and 0
are falsy values. We can get away with numbers:
1
2
3
4
5
6
7
8
9
10
11
|
function point(x, y, isFlag){
x = x || 0;
y = typeof(y) === ‘undefined’ ? -1 : y;
isFlag = typeof(isFlag) === ‘undefined’ ? true : isFlag;
console.log(x,y, isFlag);
}
point(0, 0) // 0 0 true
point(0, 0, false) // 0 0 false
point(1) // 1 -1 true
point() // 0 -1 true
|
Now it works as expected when we check for undefined
.
Rest parameters
We went from arguments to rest parameters and spread operator.
On ES5, it’s clumpsy to get an arbitrary number of arguments:
1
2
3
4
5
6
7
|
function printf(format) {
var params = [].slice.call(arguments, 1);
console.log(‘params: ‘, params);
console.log(‘format: ‘, format);
}
printf(‘%s %d %.2f’, ‘adrian’, 321, Math.PI);
|
We can do the same using the rest operator ...
.
1
2
3
4
5
6
|
function printf(format, …params) {
console.log(‘params: ‘, params);
console.log(‘format: ‘, format);
}
printf(‘%s %d %.2f’, ‘adrian’, 321, Math.PI);
|
Spread operator
We went from apply()
to the spread operator. Again we have ...
to the rescue:
Reminder: we use
apply()
to convert an array into a list of arguments. For instance,Math.max()
takes a list of parameters, but if we have an array we can useapply
to make it work.
As we saw in earlier, we can use apply
to pass arrays as list of arguments:
1
|
Math.max.apply(Math, [2,100,1,6,43]) // 100
|
In ES6, you can use the spread operator:
1
|
Math.max(…[2,100,1,6,43]) // 100
|
Also, we went from concat
arrays to use spread operator:
1
2
3
4
5
|
var array1 = [2,100,1,6,43];
var array2 = [‘a’, ‘b’, ‘c’, ‘d’];
var array3 = [false, true, null, undefined];
console.log(array1.concat(array2, array3));
|
In ES6, you can flatten nested arrays using the spread operator:
1
2
3
4
5
|
const array1 = [2,100,1,6,43];
const array2 = [‘a’, ‘b’, ‘c’, ‘d’];
const array3 = [false, true, null, undefined];
console.log([…array1, …array2, …array3]);
|