Javascript在过去的一年里变化了很多,从现在开始,有12个新的特性可以开始用了!
1. 历史
ECMAScript 6是对Javascript语言的增强,有时候也被称为ES6或者ES2015+。
Javascript诞生在1995年,从那以后一直在缓慢地演变,每隔几年会有一些新的增强特性出现。ECMAScript出现在1997年,目的是指导Javascript的演化路径,它已经发布了好几个版本,如ES3、ES5、ES6,等等。
可以看到,在ES3、ES5和ES6之间分别有10年和6年的空当。最新的模式是每年有一些小的增强,而不是像ES6一样突然出现巨大的变化。
2. 浏览器支持
所有现代浏览器和环境都已经支持ES6了!
Chrome, MS Edge, Firefox, Safari, Node等已经默认支持了Javascript ES6的大多数特性。 所以,在接下来的这篇概述中你们看到的所有东西都可以马上用上了。
那就进入正题。
3. ES6 核心特性
你可以在浏览器console窗口中测试接下来的所有代码片段!
不要贸然相信我的话,自己测试一下ES5和ES6的每个例子。我们开始干吧��
3.1 块变量
在ES6中,声明变量不再用var
而用let
/const
。
var
到底做错了什么?
var
的问题在于它会泄露变量到其他代码块中,比如for
循环或者if
语句:
var x = 'outer';
function test(inner) {
if (inner) {
var x = 'inner'; // scope whole function
return x;
}
return x; // gets redefined on line 4
}
test(false); // undefined ��
test(true); // inner
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
比如test(false)
,本来希望返回outer
,结果却不是,你得到undefined
。
为什么?
因为即使if语句块没有执行到,代码第4行仍然重新定义了var x
,值为undefined
。
ES6解决了这个问题:
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
用let
替换var
让代码不出所料。如果if
语句块没有执行到,变量x不会被重定义。
* IIFE *
在解释IIFE之前我们来看个例子。看下面的代码:
{
var private = 1;
}
console.log(private); // 1
- 1
- 2
- 3
- 4
能够看到,private
变量泄露了出去。你需要使用IIFE(立即执行函数)来包含它:
(function(){
var private2 = 1;
})();
console.log(private2); // Uncaught ReferenceError
- 1
- 2
- 3
- 4
如果你看一下jQuery/lodash或者其它开源项目就会发现他们使用IIFE来避免污染全局命名空间,而仅定义了有限的几个全局变量,如_
,$
或者jQuery
。
ES6要清爽多了,我们不再需要使用IIFE,只要用代码块和let
就够了:
{
let private3 = 1;
}
console.log(private3); // Uncaught ReferenceError
- 1
- 2
- 3
- 4
* Const *
如果根本不希望变量改变也可以使用const。
底线: 抛弃
var
,要用let
和const
。
- 所有引用都用const
,避免使用var
。
- 如果一定要重新赋值,使用let
而非var
。
3.2 模板字面量(Literals)
有了模板字面量,我们不再需要搞一堆嵌套连接了。看这个例子:
var first = 'Adrian';
var last = 'Mejia';
console.log('Your name is ' + first + ' ' + last + '.');
- 1
- 2
- 3
现在你可以用反引号(`)和字符串插值(interpolation)${}
:
const first = 'Adrian';
const last = 'Mejia';
console.log(`Your name is ${first} ${last}.`);
- 1
- 2
- 3
3.3 多行字符串
我们不再需要这样拼字符串了:string + \n
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在ES6中,我们也可以用反引号来搞定它:
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
两段代码作用完全一样。
3.4 解构赋值
ES6解构非常有用、明确。看下面这个例子:
* 从一个数组中得到元素 *
var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third); // 1 3
- 1
- 2
- 3
- 4
等同于
const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third); // 1 3
- 1
- 2
- 3
* 交换两个值 *
var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b); // 2 1
- 1
- 2
- 3
- 4
- 5
- 6
等同于
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
- 1
- 2
- 3
- 4
* 多个返回值的解构 *
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
第3行,你也可以像这样返回一个数组(少打几个字):
return [left, right, top, bottom];
- 1
但是下来,调用者要考虑返回值的顺序。
var left = data[0];
var bottom = data[3];
- 1
- 2
有了ES6,调用者只选择他们需要的数据(第6行):
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
- 1
- 2
- 3
- 4
- 5
- 6
注意:第3行, 这里出现了另外的ES6特性。我们可以将{left: left}
压缩为{left}
。 看看跟ES5相比有多简洁。够酷吧?
* 参数匹配的解构 *
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
- 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
- 1
- 2
- 3
- 4
- 5
* 深度匹配 *
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
- 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
- 1
- 2
- 3
- 4
- 5
这也叫做对象解构。
如你所见,解构非常有用,而且有利于写出更好的编码风格。
优秀实践:
- 使用数组解构来获取元素或者交换变量,省了定义临时变量的麻烦。
- 不要使用数组解构来获取多个返回值,用对象解构。
3.5 类和对象
有了ES6,我们可以用“类”��来替换“构造函数”��。
Javascriptscript中,每个对象都有一个原型,那是另外的一个对象。所有Javascript对象从它们的原型对象中继承方法和属性。
在ES5中,我们进行面向对象编程时使用构造函数来创建对象,比如:
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.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在ES6中,我们有了些语法糖,能够用更少的模式化代码和新的关键字来实现同样的功能,比如class
和constructor
。同样,可以比较一下哪种方式更清晰:constructor.prototype.speak = function()
vs speak()
:
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.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我们看到,两种方式(ES6/6)在幕后输出了同样的结果,用起来没有差别。
最佳实践:
- 尽量使用class
语法而避免直接操作prototype
。这能够令代码更简洁易懂。
- 避免使用空的构造函数。如果没有指定,class
有默认实现。
3.6 继承
在前面Animal类的基础上,假设我们想扩展它,定义一个Lion类。
在ES5中,这需要用到一些原型继承的东西。
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.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
我不会每个细节都过一遍,但要注意:
- 第3行, 我们显式地带参数调用Animal的构造函数。
- 第7-8行,我们将Lion的原型设置为Animal。
- 第11行,我们调用父类Animal的speak方法。
在ES6中,我们有了新的关键字extends
和super
。
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.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
相比ES5,同样的事情用ES6有多么清晰易读。简直妙不可言!
最佳实践:
- 使用内置的extends来实现继承。
3.7 Native Promises
我们经历过回调黑洞�� ,终于等来了promises ��
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);
});
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
我们有这样一个方法,当它done
后需要调用一个回调函数。它要执行两次,在执行一次之后还要再执行一次,这就是为什么在回调函数printfAfterTimeout
中我们又一次调用它。
假设我们需要第3个或第4个回调函数,这会很快变得混乱不堪。来看一下如果用promises可以怎么做:
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);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
你能看到,通过使用promise,我们可以在完成一件事情后使用then
来做另一件事。不再需要嵌套的回调方法了。
3.8 箭头函数
ES6没有删除函数表达式,而是增加了一种新方式,叫做箭头函数。
在ES5中,this
有一些要注意的地方:
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
你需要在函数中使用一个临时的_this
来引用外面的this
,或者使用bind
。在ES6中,你可以使用箭头函数!
// 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}`);
- 1
- 2
- 3
- 4
- 5
3.9 For … of
我们经历过从for
到foreach
,然后到for...of
:
// 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);
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
ES6中的for...of
也是用来处理循环的。
// for...of
const array = ['a', 'b', 'c', 'd'];
for (const element of array) {
console.log(element);
}
- 1
- 2
- 3
- 4
- 5
3.10 默认参数
以前我们经常要检查一个变量是否已定义,没定义就赋一个默认值。你做过类似下面的事情吗?
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
也许没错,这是一种检查变量是否赋值或者赋默认值的常见方式。然而,注意这有些问题:
- 第8行,我们传了0,0
,但是得到0,-1
- 第9行,我们传了false
,但得到true
。
如果你有一个boolean类型的默认参数或者设置值为0,这种情况下就有问题。你知道为啥吗???我会在ES6的例子之后告诉你
在ES6中,你可以用更少的代码做得更好!
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
注意第5行和第6行我们得到了期望的结果。但ES5的例子就不行,我们首先必须要检查undefined
,因为false
、null
、undefined
和0
都是布尔false的值。只有这样我们才能勉强处理好参数是number的情况。
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
当我们检查了undefined
后,工作符合预期。
3.11 剩余参数
我们见识过剩余参数和展开操作符.
在ES5中,获取剩余参数的方式有点笨拙:
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);
- 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);
- 1
- 2
- 3
- 4
- 5
3.12 展开操作符
我们可以用展开操作符代替apply()
。又是...
帮了大忙:
记住:我们用
apply()
来将数组转换为一个参数列表。比如说,Math.max()
需要一个参数列表,但是如果我们只有一个数组,这时就可以用apply
来转换。
像前面看到那样,我们用apply将数组转换为参数列表:
Math.max.apply(Math, [2,100,1,6,43]) // 100
- 1
在ES6中,你可以用展开操作符:
Math.max(...[2,100,1,6,43]) // 100
- 1
同样,我们也可以用展开操作符来替换concat
数组:
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));
- 1
- 2
- 3
- 4
在ES6中,你可以用展开操作符展平(flatten)内嵌的数组:
const array1 = [2,100,1,6,43];
const array2 = ['a', 'b', 'c', 'd'];
const array3 = [false, true, null, undefined];
console.log([...array1, ...array2, ...array3]);