ECMAScript 8 都发布了,你还没有用上 ECMAScript 6?

ECMAScript 8 都发布了,你还没有用上 ECMAScript 6? ES8已经与17年6月底发布,而很多的前端开发者还没有开始用上ES6。本文聊一聊怎么快速入门ES6,并将ES6的语法应用到实战项目中。 阅读全文大约需要15分钟。 文中以 ES 表示 ECMAScript。 今年六月底,TC39发布新一版的ES 8(ES 2017),自从ES6在15年发布之后,每一年TC…

ES8已经与17年6月底发布,而很多的前端开发者还没有开始用上ES6。本文聊一聊怎么快速入门ES6,并将ES6的语法应用到实战项目中。

阅读全文大约需要15分钟。

文中以 ES 表示 ECMAScript。

今年六月底,TC39发布新一版的ES 8(ES 2017),自从ES6在15年发布之后,每一年TC39都会发布新一版的ES语言标准。

我了解的前端开发者中,还有很多人没有用上ES6,有的人是觉得ES5用的挺好的,懒得去学ES6,有的人是有想学ES6的决心,但是苦于没有合适的机会(项目)去实战练习。

如果你用过React,Vue或Nodejs等,那你多多少少都会使用到一些ES6语法的。

ES8中的新特性,浏览器厂商和语法转换器还需要一段来实现,不如我们还是先聊聊怎么在你的项目中用上ES6吧。

什么是ES6?它和ES5有什么区别?

我们常说的JavaScript是指ES3和ES5,ES6是ECMAScript 6 的缩写。
对于经常写原生JavaScript的前端开发者来说,对ES5中的语法肯定比较熟悉,比如数组中的一些方法forEach,map,filter,some,every,indexOf,lastIndexOf,reduce,reduceRight ……,以及对象(Object)和函数(Function)都拓展了很多方法,这里不多赘叙。

ES6给前端开发者带来了很多的新的特性,可以更简单的实现更复杂的操作,很大的提高开发效率,提高代码的整洁性。

ES6中的新特性有很多,列一些比较常用的特性:

  • Block-Scoped Constructs Let and Const(块作用域构造Let and Const)

  • Default Parameters(默认参数)

  • Template Literals (模板字符串)

  • Multi-line Strings (多行字符串)

  • Arrow Functions (箭头函数)

  • Enhanced Object Literals (增强的对象文本)

  • Promises

  • Classes(类)

  • Modules(模块)

  • Destructuring Assignment (解构赋值)

下面介绍下这些常用的ES6特性。

Block-Scoped Constructs Let and Const(块作用域构造Let and Const)

ES6提供了两个新的声明变量的关键字:let和const。而let和const要和块级作用域结合才能发挥其优势。

什么是块级作用域?

块级作用域的表示一对大括号{}包围的区域是一个独立的作用域,在这个作用域内用let声明的变量a,只能在这个作用域内被访问到,在这对大括号外面是访问不到a的。

当然,在块级作用域中还可以声明函数,该函数也是只能作用域内部才能被访问到。所以,在if、else、for甚至是一对单独的{},都是一个块级作用域。

在ES6之前,是没有块级作用域的概念的,只有全局作用域和函数作用域两种,并且,用var声明的变量和用function声明的函数会被提前到作用域的顶部,这也就是我们常说的声明提前。

let

用let声明的变量是存在于距离声明语句最近的一个作用域(全局作用域、函数作用域或块级作用域)内的,在声明的时候,可选的将其初始化成一个值。

语法如下:

let var1 [= value1] [, var2 [= value2 ] ] [, ..., varN [= valueN]] ;

这一点与var的声明不同,用var声明的变量是属于离他最近的一个全局作用域或函数作用域中,且声明会被提前。在块级作用域中,var的声明与在全局作用域和函数作用域中是一样的。

块级作用域和let声明变量,解决了使用var一些痛点,相当于用let声明的变量不会被提前到作用域顶部。

有一点需要注意,let也是声明提前,但是let声明变量的语句必须在使用该变量语句之前,在声明之前引用会报错该变量未被声明,且let不允许重复声明相同名称的变量,否则会报错。我们看下例子:

{
  var hello = 'Hello';
  let world = 'World';
}
console.log(hello);
console.log(world);

在Chrome浏览器的控制台(最新版本的Chrome已支持一部分ES6语法)执行一下,会发现有报错,见下图。constconst声明与let基本相同,它也是存在于块级作用域内。
有一点区别就是const声明的是常量,即不可被重新赋值改变原值。需要注意,const在声明常量的时候,必须同时给常量初始化赋值。如果只声明,不初始化值的话,会报错。见下面代码。

const MAX;
// Uncaught SyntaxError: Missing initializer in const declaration

声明变量的方法在ES5中,可以通过var和function这两种方法来声明变量。

而在ES6中,除了增加了let和const两种声明方式,还有接下来要介绍的import和class的声明方式。

Default Parameters(默认参数)

默认参数是ES6中对函数拓展的一个特性,可以直接为函数的参数设置默认值,当某一参数设置了默认值时,如果调用函数的时候没有传该参数,则该参数的值为默认值,如果传了该参数,则该参数的值为传递的参数值。

在ES6之前,我们可以通过手动的方式,为函数的参数设置默认值,代码如下:

function sign (x) {
  if (typeof x === 'undefined') {
    x = 'default'
  }
  console.log(x)
}
sign('new sign')    // new sign
sign()              // default

将上述代码换成ES6模式,可以这样写:

function sign (x = 'default') {
  console.log(x)    
}
sign('new sign')    // new sign
sign()              // default

Template Literals (模板字符串)

ES6提供了模板字符串的特性,模板字符串是使用反引号(`)和${}实现字符串的拼接,字符串中可以嵌入变量。

在ES6之前,我们一般这样输出模板:

var name = 'Henry';
var welcome = 'Hello, ' + name + '!';
console.log(welcome);   // Hello, Henry!

在ES6中,模板字符串可以这样拼接字符串:

let name = 'Henry';
let welcome = `Hello, ${ name }!`;
console.log(welcome);   // Hello, Henry

模板字符串的计算规则是在两个反引号之间将字符串拼接到一起,如果反引号之间含有${},则会计算这对大括号内的值,大括号里面可以是任意的JavaScript表达式,可以进行运算和引用对象属性。

let a = 3;
let number = `$ {a + 2 }`;
console.log(`${ number }`);    // 5

let b = { c: 2, d: 4 };
console.log(`${ b.c * b.d }`) ;     // 8

Multi-line Strings (多行字符串)

多行字符串是模板字符串的拓展,它跟模板字符串是同样的解析方式,不同的是,它可以拼接多行的字符串,且拼接的字符串中会保留所有的空格和缩进。

如果需要用字符串来拼接DOM树结构时,可以这样写:

let titleValue = 'This is a title';

let htmlStr = `
  <div>
      <h2>${ titleValue }</h2>
      <p>This is a paragraph.</p>
  </div>
`;

上述代码中,能看到JavaScript代码和伪html代码的结合,完全可以将模板字符串的多行字符串封装成一个页面模板工具,绝对是轻量高效的。

还有,这种书写方式是不是很眼熟,跟React的JSX是不是很像双胞胎啊。

Arrow Functions (箭头函数)

在ES6中,可以使用箭头(=>)来声明一个函数,称作箭头函数。

ES5中声明一个函数,可以这样写:

var func = function (a) {
  return a + 2;
}

将这个函数换成 箭头函数

let func = a => a + 2;

如果函数有多个参数,需要用括号包含所有参数,只有一个参数的时候,可以省略括号,如果没有设置参数,也必须有括号。示例如下:

let func1 = (arg1, arg2, arg3) => {
  return arg1 + arg2 + arg3;
}

let func2 = arg => {
  console.log(arg)
}

let func3 = () => {
  console.log(`This is an arrow function.`)
}

需要注意的是,箭头函数没有自己的this,如果在箭头函数内部使用this,那样这个this是箭头函数外部的this,也是因为箭头函数没有this,所以,箭头函数不能用作构造函数。如果用箭头函数来写回调函数时,就不用再将外部this保存起来了。

// ES5
function foo() {
  var _this = this;
    
  setTimeout(function() {
    console.log('id:', _this.id);        
  }, 200)
}

// ES6
function foo() {
  setTimeout(() => {
    console.log(`id:${ this.id }`)        
  }, 200)
}

Enhanced Object Literals (增强的对象文本)

在ES6,对象字面值扩展支持在创建时设置原型,简写foo:foo分配,定义方法,加工父函数(super calls),计算属性名(动态)。总之,这些也带来了对象字面值和类声明紧密联系起来,让基于对象的设计得益于一些同样的便利。

var obj = {
  // __proto__ 原型
  __proto__: theProtoObj,
  // Shorthand for ‘handler: handler’  简写
  handler,
  // Methods
  toString() {
    // Super calls     继承
    return "d " + super.toString();
  },
  // Computed (dynamic) property names 计算属性名
  ['prop_' + (() => 42)()]: 'name'
};

Promises

Promise是异步编程的一种解决方案,它是一个对象,且只要开始就会一直进行下去,直到成功或者失败。就像它的字面意思诺言一样,一个诺言,只要被许下,就只有两种解决:成功或失败。

Promise的结果是由异步操作的结果决定的,且一旦结果形成,便不可再被改变,任何时候都得到同样的结果。

需要注意的是:Promise被新建后,便无法被取消,会执行下去,直到出现结果;如果不设置回调,Promise内部抛出的异常,不会反应到外部。

语法

new Promise(
  /* executor */
  function(resolve, reject) {
    // ...
  }
);

参数executorexecutor是一个带有resolve和reject两个参数的函数 。
executor 函数在Promise构造函数执行时同步执行,被传递resolve和reject函数(executor 函数在Promise构造函数返回新建对象前被调用)。
resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。
如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

使用Promise异步加载图片的例子:

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    var image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

Promise.prototype.thenPromise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function funcA(comments) {
  console.log("Resolved: ", comments);
}, function funcB(err){
  console.log("Rejected: ", err);
});

// 如果用箭头函数来写,会更加简洁
getJSON("/post/1.json").then(
  post => getJSON(post.commentURL))
.then(
  comments => console.log("Resolved: ", comments),
  err => console.log("Rejected: ", err)
);

Promise.prototype.catchPromise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

需要注意的是,catch的调用是按顺序来的,如果catch后还有then函数处理时抛出的异常,不会再触发此catch函数,如要捕获异常,需要在此then后面再定义一个catch函数来捕获异常。

关于Promise,还有很多的方法,篇幅限制,本处不再赘述,想要获取更多关于Promise的内容,请到MDN查看。(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)

Classes(类)

ES6中实现了class的语法糖,用于简化ES5中使用原型继承实现类定义的方式。

虽然,ES6中的class并没有其他语言(如JAVA)中class该有的特性,但是相比原型继承,语义更清晰、明确,语法更加简洁,易理解。

class语法如下:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    console.log(`x: ${ x }, y: ${ y }`)
  }
}

在之前,用ES5语法来写,是这样:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  constructor: Point,
  toString: function() {
    console.log('x: ' + this.x + ', y: ' + this.y)
  }
}

class中的constructor是构造函数,可以通过它对class的属性进行初始化设置。

class是可以通过extends关键字实现继承的,继承的概念,是子class可以通过继承将父class的属性和方法。需要注意的是,在实现继承的时候,需要在constructor中调用super方法。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

Modules(模块)

ES6之前,JavaScript是没有模块概念的,无法将一个大的系统拆分成很多小的模块,再组合封装的。JavaScript社区中比较好的模块加载方案有 CommonJS 和 AMD 两种,分别用于服务器和浏览器,代表框架有RequireJS和Nodejs。

ES6中的module语法,非常简单的实现了JavaScript代码的模块化,且完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

语法:ES6中的模块,是通过export命令显式指定该模块输入的代码,在其他模块中,可以用个import命令输入该模块。

// 定义一个模块  a.js
const value = 2;
export value;

// 定义一个模块 b.js
import { value } from 'a';
console.log(value); // 2

exportexport可以导出很多的数据类型,除了变量,还可以导出函数和类。且export输出的是它本来的名字,如果不想用原来的名字,可以使用as关键字重命名。

function a() { ... }
function b() { ... }

export {
  a as funcA,
  b as funcB
}

import

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

// main.js
import { 
  firstName, 
  lastName, 
  year 
} from './profile';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

跟export一样,import也可以用as关键字来为导入的变量重命名,且引用路径时可以省略 .js 后缀。

import { 
  lastName as surname 
} from './profile';

Destructuring Assignment (解构赋值)

解构赋值绝对是ES6中最大的亮点,解构赋值使得在获取变量的值或为变量赋值有了更快速和简介的方式。

ES6中,模板赋值可以应用到数组、对象、字符串、布尔值和函数参数等结构中。

数组

数组的解构赋值赋值可以这样写:

// ES5
let a = 1;
let b = 2;
let c = 3;

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

数组的解构赋值取值可以这样写:

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

需要注意的是:如果解构不成功,变量的值就等于undefined,且数组的解构赋值是按照固定顺序的。

允许设置默认值:

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

对象

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

同数组解构赋值,对象解构赋值也可以嵌套使用,且如果解构不成功,变量的值就等于undefined。

函数参数函数的参数值也可以使用解构赋值:

function add([x, y]){
  return x + y;
}
add([1, 2]); // 3

也可以使用默认参数值:

function move({x = 0, y = 0} = {}) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

其他ES6特性

ES6中有很多新的特性,上述只是介绍一些常用的特性,除了这些,还有:

  • 添加了一个新的基础数据类型:Symbol

  • 数组类型增加了更多的方法

  • 增加了新的数据结构:Set 和 Map

  • 增加了异步解决方案:Generator

  • ......

想要了解更多的ES6特性,请查找相关资料。

怎么配置ES6的开发环境?

由于浏览器厂商还没有完全支持ES6语法,所以不能在生成环境中直接使用ES6语法。

借助于语法转换器babel,我们在本地开发环境中自由使用ES6语法,只需要在项目打包之前,用babel转换语法即可。

前端工程化项目中,可以使用打包编译工具在打包代码前使用babel等工具转化ES6语法成ES5语法。打包编译工具有很多选择,gulp、browserify、webpack和rollup等。

下面以webpack为例,讲述如何配置ES6开发环境。

1.初始化项目在本地新建一个项目

$ mkdir myAPP && cd myApp

2.安装webpack、babel

$ npm install --save-dev webpack
$ npm install --save-dev babel-core babel-preset-es2015  
$ npm install --save-dev babel-loader 

3.编写配置文件 .babelrc

在项目根目录新建 babel 的配置文件:

// .babelrc
{
  "presets": [  "es2015"  ]
}

4.配置webpack

在项目根目录下新建 webpack 配置文件 webpack.config.js。

// webpack.config.js
module.exports = {  
  entry: './app.js', // 项目入口文件
  output: {  
    path: './dist', // 打包文件目录
    filename: 'app.bundle.js' // 打包文件名
  },  
  module: {  
    // 配置所有的JS文件都使用 babel 进行语法转换
    loaders: [{
      test: /.js$/,  
      exclude: /node_modules/,  
      loader: 'babel-loader'  
    }]  
  }  
}  

5.执行语法转换

在项目根目录下创建文件 app.js,并书写语法如下:

class Welcome {
  constructor(name) {
    this.name = name;
  }
  
  hello() {
    console.log(`Hello, ${this.name}`)
  }
}

在命令行中执行webpack:

$ webpack

执行完之后,可以在项目的目录 dist/app.bundle.js 文件中看到转换之后的内容:

"use strict";

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Welcome = function () {
  function Welcome(name) {
    _classCallCheck(this, Welcome);

    this.name = name;
  }
  _createClass(Welcome, [{
    key: "hello",
    value: function hello() {
      console.log("Hello, " + this.name);
    }
  }]);
  
  return Welcome;
}();

自此,你已经可以在项目中使用ES6来构建项目了,去发掘ES6的更多特性吧,尽情享受ES6带给你的便利和舒爽吧。

你也可以在babel官网查看ES6实时转换的语法。地址:http://babeljs.io/repl/

参考

ECMAScript 6入门http://es6.ruanyifeng.com/MDN https://developer.mozilla.org/BABEL http://babeljs.io/docs/setup/Webpack http://webpack.github.io/docs/