Javascript ES6 特性概述(即ECMAScript 6和ES2015+)

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

比如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

let替换var让代码不出所料。如果if语句块没有执行到,变量x不会被重定义。

* IIFE *

在解释IIFE之前我们来看个例子。看下面的代码:

{
  var private = 1;
}
console.log(private); // 1

能够看到,private变量泄露了出去。你需要使用IIFE(立即执行函数)来包含它:

(function(){
  var private2 = 1;
})();
console.log(private2); // Uncaught ReferenceError

如果你看一下jQuery/lodash或者其它开源项目就会发现他们使用IIFE来避免污染全局命名空间,而仅定义了有限的几个全局变量,如_$或者jQuery

ES6要清爽多了,我们不再需要使用IIFE,只要用代码块和let就够了:

{
  let private3 = 1;
}
console.log(private3); // Uncaught ReferenceError

* Const *

如果根本不希望变量改变也可以使用const。
这里写图片描述

底线: 抛弃var,要用letconst
- 所有引用都用const,避免使用var
- 如果一定要重新赋值,使用let而非var

3.2 模板字面量(Literals)

有了模板字面量,我们不再需要搞一堆嵌套连接了。看这个例子:

var first = 'Adrian';
var last = 'Mejia';
console.log('Your name is ' + first + ' ' + last + '.');

现在你可以用反引号(`)和字符串插值(interpolation)${}:

const first = 'Adrian';
const last = 'Mejia';
console.log(`Your name is ${first} ${last}.`);

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);

在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);

两段代码作用完全一样。

3.4 解构赋值

ES6解构非常有用、明确。看下面这个例子:

* 从一个数组中得到元素 *

var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third); // 1 3

等同于

const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third); // 1 3

* 交换两个值 *

var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b); // 2 1

等同于

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1

* 多个返回值的解构 *

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

第3行,你也可以像这样返回一个数组(少打几个字):

return [left, right, top, bottom];

但是下来,调用者要考虑返回值的顺序。

var left = data[0];
var bottom = data[3];

有了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

注意:第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

等同于(但更简洁):

const user = {firstName: 'Adrian', lastName: 'Mejia'};
function getFullName({ firstName, lastName }) {
  return `${firstName} ${lastName}`;
}
console.log(getFullName(user)); // Adrian Mejia

* 深度匹配 *

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

等同于(但更简洁):

function settings() {
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();
console.log(displayColor, keyboardLayout); // red querty

这也叫做对象解构。

如你所见,解构非常有用,而且有利于写出更好的编码风格。

优秀实践:
- 使用数组解构来获取元素或者交换变量,省了定义临时变量的麻烦。
- 不要使用数组解构来获取多个返回值,用对象解构。

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.

在ES6中,我们有了些语法糖,能够用更少的模式化代码和新的关键字来实现同样的功能,比如classconstructor。同样,可以比较一下哪种方式更清晰: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.

我们看到,两种方式(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.

我不会每个细节都过一遍,但要注意:
- 第3行, 我们显式地带参数调用Animal的构造函数。
- 第7-8行,我们将Lion的原型设置为Animal。
- 第11行,我们调用父类Animal的speak方法。

在ES6中,我们有了新的关键字extendssuper

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.

相比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);
  });
});

我们有这样一个方法,当它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);
});

你能看到,通过使用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

你需要在函数中使用一个临时的_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}`);

3.9 For … of

我们经历过从forforeach,然后到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);
});

ES6中的for...of也是用来处理循环的。

// for...of
const array = ['a', 'b', 'c', 'd'];
for (const element of array) {
    console.log(element);
}

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

也许没错,这是一种检查变量是否赋值或者赋默认值的常见方式。然而,注意这有些问题:
- 第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

注意第5行和第6行我们得到了期望的结果。但ES5的例子就不行,我们首先必须要检查undefined,因为falsenullundefined0都是布尔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

当我们检查了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);

我们可以用剩余操作符…来做同样的事情。

function printf(format, ...params) {
  console.log('params: ', params);
  console.log('format: ', format);
}
printf('%s %d %.2f', 'adrian', 321, Math.PI);

3.12 展开操作符

我们可以用展开操作符代替apply()。又是...帮了大忙:

记住:我们用apply()来将数组转换为一个参数列表。比如说,Math.max()需要一个参数列表,但是如果我们只有一个数组,这时就可以用apply来转换。

这里写图片描述
像前面看到那样,我们用apply将数组转换为参数列表:

Math.max.apply(Math, [2,100,1,6,43]) // 100

在ES6中,你可以用展开操作符:

Math.max(...[2,100,1,6,43]) // 100

同样,我们也可以用展开操作符来替换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));

在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]);

4. 结论

Javascript经历过太多的变化。这篇文章讲到了所有Javascript开发者都应该了解的核心特性。同时,我们也讲了一些优秀实践,能让你的代码更加简洁明了。

如果你认为还有其他MUST KNOW的特性,麻烦在评论中吱一声,我来更新。

原文

译注:
貌似markdown编辑器不是太好用啊,发现了几个问题

  • 编辑器中预览和发布后的正文显示效果不一致
  • 目录中的编号显示不对
  • 强调显示的不对,旁边还有星号
  • 图标字符显示不出来
  • 代码块不显示语言名称
  • 最蛋疼的是,发表后点再修改,编辑器里面只有半拉文章了,其他的给我吃了啊
阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页