ES6常用新特性总结

前言

ECMAScript 6 简称 ES6,是 JavaScript 语言的下一代标准,已经在2015年6月正式发布了。本文对ES6标准中常用的一些特性进行介绍,需要注意的是,本文并不完整的涵盖ES6标准的规定,只是就我本人常用的特性进行总结。编写代码积极使用ES6特性能够使代码更加规范、易懂。

let和const关键字

在ES6中,推荐使用let和const关键字来代替var关键字用于声明变量。

let关键字相较于之前的var关键字更符合编程语言的常识,其具有以下特性:

  1. 具有块级作用域。
1
2
3
4
5
{
let a = 'hello';
console.log(a); // 'hello'
}
console.log(a); // Uncaught ReferenceError: a is not defined
  1. 不允许重复声明。
1
2
3
4
{
let a = 'hello';
let a = 'hello'; // Identifier 'a' has already been declared
}
  1. 不存在变量提升。
1
2
3
4
5
6
console.log(a);  // 使用var定义,输出变量默认值 undefined
console.log(b); // Uncaught ReferenceError: b is not defined

var a = 'hello';
let b = 'world';

  1. 暂时性死区

在块级作用域中,在let声明变量之前,变量均是不可用的。

1
2
3
4
5
6
7
var a = 'hello';

{
console.log(a); // Uncaught ReferenceError
let a;
}

const关键字定义的变量是不可修改的,习惯上将使用const声明的变量使用全大写字母,且声明时必须给变量赋初值,此外,const特性和let关键字基本一致。

需要注意的是,const关键字只保证「字面意义」上的常量,例如,对于对象和数组而言,只是保证变量保存的地址值不被改变,并不能保证其管理的实际对象不改变:

1
2
3
4
5
6
7
8
const ref = {
a: 'hello',
b: 'world'
}

ref.a = '你好'; // ok
ref = 123; // Uncaught TypeError: Assignment to constant variable.

尽量使用let和const关键词,避免使用var!

解构赋值

在ES6中,能够按照一定的匹配规则从数组或对象中提取值用于变量赋值。

  1. 从数组中解构赋值。
1
2
3
4
5
6
7
let list = [1, 2, 3];
let [a, b, c] = list; // 完全解构

let [e, f] = list; // 不完全解构 e=1, f=2

let [x, [y0, y1], z] = [1, [1,2], 3]; // 嵌套解构

  1. 从对象中解构赋值。
1
2
3
4
5
6
7
const person = {
name: '小明',
age: 12,
id: 1,
}

let {name: n, age: a, id: i} = person; //n='小明', a=12, i=1

简化写法:

1
2
3
4
// 当对象名和属性名一致时,可采用ES6中对象属性简洁写法:
// {name:name, age:age, id:id}简写为{name, age, id}

let {name, age, id} = person; //name='小明', age=12, id=1

不完全解构:

1
2
// 只解构出age属性
let {age} = person; // age=12

模板字符串

模板字符串用一对反引号标识,且字符串内可以使用${js表达式}的形式嵌入js表达式,模板字符串可以标识多行字符串,空格和缩进均被保留。这极大的简化了以前js字符串拼接的麻烦。

1
2
3
4
5
6
7
8
9
// 可以定义多行字符串,空格和缩进被保留
let s = `<ul>
<li></li>
<li></li>
</ul>`;

// 可以通过${}嵌入表达式或变量
let a = 12
let s1 = `a is ${a}`; // 'a is 12'

函数特性

默认参数

1
2
3
4
5
6
7
// 默认参数,一般靠后
function add(a, b=0) {
return a + b;
}

let res = add(1);
let res1 = add(1, 2);

默认参数可以和解构赋值配合使用:

1
2
3
4
5
6
7
8
9
10
11
12
const person = {
name: '小明',
age: 12,
id: 1,
}
// 默认参数与解构赋值结合
function printer({name='小红', age}) {
console.log(name);
console.log(age);
}

printer(person);

ES6还支持rest参数,该参数形式为...varible,该变量接收不定个数的参数作为一个数组(rest参数只能放在参数的最后,且只允许有一个rest参数):

1
2
3
4
5
6
function printer(...res) {
console.log(res);
}

printer(1,2,3); // [1,2,3]
printer(1,2); // [1,2]

箭头函数

ES6允许使用箭头=>定义函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 无参数箭头函数: 
let f = () => {
...;
console.log('你好!');
}

// 单参数箭头函数(可省略参数适号):
let f1 = a => {
...;
console.log(a);
}

// 多参数箭头函数:
let f2 = (a, b, c) => {
...;
console.log('a');
}

特别的,当函数体只有一条语句,且需要返回值时,可以省略函数体的花括号:

1
2
// 省略代码块适号: 
let f = (a, b) => a + b;

对象特性

方法和属性简写

ES6中,可以对对象的属性和方法采用简写形式:

1
2
3
4
5
6
7
8
9
10
11
12
let name = '小明';
let age = 12;

const person = {
// 属性名简写,等同于name: name
name,
age,
// 方法简写,等同于:laugh: function(){console.log('哈哈');}
laugh() {
console.log('哈哈');
}
}

属性名表达式

ES6允许使用表达式作为属性名:

1
2
3
4
5
6
7
8
let obj = {
foo : 12,
// 表达式属性
['b' + 'ar'] : 123,
}

let expr = 'valid';
obj[expr] = true; // obj.valid = true; ES5

扩展运算符应用

扩展运算符将对象中所有可遍历的属性拷贝至新的对象上:

1
2
3
4
5
6
7
8
9
10
11
12
13
const p1 = {
name: '小明',
age: 12,
}

const p2 = {
sex: 'male',
}

// 扩展运算符将对象合并
const p3 = {...p1, ...p2};
// 扩展运算符用于给对象添加属性,注意属性名相同时后面的属性覆盖前面的属性
const p4 = {...p1, sex: 'male'};

此外,扩展运算符也可用于数组对象上:

1
2
3
4
5
6
7
8
9
10
11
const arr1 = ['小明', '小红'];
const arr2 = ['小黑'];

// 扩展运算符合并数组
const arr = [...arr1, ...arr2];

// 用于复制数组
const arr1Cpy = [...arr1];

// 用于将字符串变为数组
const arr3 = [...'HELLO'];

Symbol

ES6提供一种新的数据类型Symbol,表示独一无二的值,常用于解决命名冲突问题。

1
2
3
4
5
// 以字符串为参数构造
let sym = Symbol('hello');
let sym1 = Symbol('hello');

sym === sym1; //false

以Symbol类型的值作为对象属性可以保证对象属性不重名:

1
2
3
4
5
6
7
8
9
10
// 以字符串为参数构造
let sym = Symbol('hello');

const obj = {
[sym] : 123,
}

// 或 obj[sym] = 123;

console.log(obj); // {Symbol(hello) : 123}

迭代器(iterator)

这里的迭代器和其它编程语言中的迭代器概念类似,也是给不同的数据结构提供了统一的接口。且ES6提供了一种新的遍历方式:for…of方式。

本质上,iterator是一个对象,这个对象提供了next()方法,每次调用next()方法返回一个形如{value: val, done : boolean}的对象,分别表示当前返回的数据结构成员的值和是否遍历结束。

ES6规定,默认的iterator接口部署在数据结构的Symbol.iterator属性上,该属性的值是一个函数,即迭代器生成函数,调用该函数会生成一个数据结构的迭代器。Symbol.iterator属性名本身是一个表达式,是一个预定义好的类型为Symbol的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1, 2];

// 数组为原生可迭代对象,取其Symbol.iterator属性,
// 由于Symbol.iterator为表达式,故用[]取,取出的属性值为函数,故用()调用,最终得到迭代器对象
let it = arr[Symbol.iterator]();
// 调用it的next()方法
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: undefined, done: true}

// 可以使用for...of循环遍历,对具有迭代器接口的对象进行遍历
for (let val of arr) {
console.log(val);
}

在ES6中,原生具有iterator接口的数据结构有:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • NodeList对象
  • arguments对象

上述对象均可使用for…of直接遍历。

Promise对象

基础介绍

Promise对象是ES6提供的一种解决异步编程问题的方案。Promise保存着某个事件(通常为异步事件)执行的结果,该对象一共有三种状态:pending(未决,或进行中),fulfilled(已成功),rejected(已失败)

ES6规定,Promise对象是一个构造函数,用于生成实例。构造函数的参数为一个函数,这个函数有resolvereject两个参数,且这两个参数均为函数,由js引擎提供。

1
2
3
4
5
6
7
8
9
const p = new Promise(function(resolve, reject) {
//...执行异步操作

if (/*异步操作成功*/) {
resolve(value); //调用js引擎提供的resolve函数
} else {
reject(error); //异步操作失败时调用reject
}
})

Promise实例生成之后,可以使用then()方法指定resolved状态和rejected状态的回调函数:

1
2
3
4
5
6
p.then(function(value){
console.log(value);
}, function(error){
console.log(error);
})
// 注意:第二个参数不是必须的

Promise对象封装Ajax请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const promise = new Promise(function(resolve, reject){
// 创建请求对象
const xhr = new XMLHttpRequest();
// 初始化请求
xhr.open("GET", "https://api.apiopen.top/getJoke");
// 发送请求
xhr.send();
// 绑定事件,处理结果
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status <= 299) {
// 请求成功
// resolve和reject函数的参数均被传给then中的回调函数
resolve(xhr.response);
} else {
reject(xhr.status);
}
}
}
})

promise.then(function(value){
console.log(value);
}, function(error){
console.log(error);
})

Promise.prototype.catch()

Promise.prototype.catch()方法用于指定发生错误时的回调函数。

1
2
3
4
5
promise.then(value => {
console.log("成功时的回调!");
}).catch(error => {
console.log("发生错误了!");
})

async和await

asyncawait是用于简化异步操作的语法糖,使异步操作和同步操作一样简单。

async函数返回一个Promise对象,可以使用then方法添加回调函数,且当函数执行时遇到内部的await会先返回,等到异步操作完成后再执行函数体后面的语句。这个返回的Promise的值为内部的return语句的值。当async内部抛出错误时,返回的Promise对象变为reject状态,可以用catch方法接收。

一般来说,await命令后面是一个Promise对象,其返回值是后面Promise对象成功时的值,若后面Promise对象失败了会抛出异常,需要在外面使用catch捕获。注:await命令必须放在async函数内部,这很容易理解,毕竟有await一般就表示存在异步操作。

下面是一个通过商品id来获取商品价格的例子,函数内部存在两个异步操作,分别是getGoodsName和获取getPrice,它们返回值均是Promise对象,故可以通过await修饰来简化操作,且外部的函数需声明为async

1
2
3
4
5
6
7
8
9
async getGoodsPrice(id) {
const name = await getGoodsName(id);
const price = await getPrice(name);
return goodsPrice;
}

getGoodsPrice(12).then( price => {
console.log(price);
})

模块化(export和import)

ES6允许编写多个js文件,并通过export将模块导出,通过import将模块导入。export本质上是对外暴露了一个变量或函数的访问接口,必须和模块内部的变量建立一一对应关系,通过这些暴露的接口,别人能够访问到模块内部的值。

  1. 分别导出

文件e.js中导出需要暴露的变量或函数:

1
2
3
4
export var name = 'hello';
export function hello(){
console.log('hello');
}

在文件i.js导入上述模块:

1
2
3
4
5
// 注意变量名需保持一致
import {name, hello} from './e.js';

console.log(name);
hello();
  1. 统一导出

文件e.js中导出需要暴露的变量或函数:

1
2
3
4
5
6
var name = 'hello';
function hello(){
console.log('hello');
}

export{name, hello}

在文件i.js导入上述模块:

1
2
3
4
5
// 注意变量名需保持一致
import {name, hello} from './e.js';

console.log(name);
hello();
  1. 默认导出

文件e.js中导出需要暴露的变量或函数:

1
2
3
4
5
6
7
// 默认导出一个对象
export default {
name: 'hello';
hello(){
console.log('hello');
}
}

在文件i.js导入上述模块:

1
2
3
4
5
// 针对默认导出模块,导入时可任意取名
import mod from './e.js';

console.log(mod.name);
mod.hello();

此外,导入和导出时均可以使用as对变量重新取名:

导出:

1
2
3
4
5
// 导出时需按照新变量名来匹配
export {
name as name1,
hello as hello1
}

导入:

1
2
// 导入重新命名变量
import * as mod from './e.js';

总结

本篇文章对部分常用的ES6标准特性进行了介绍,并不包含该标准的全部特性。

参考文献

  1. https://www.w3cschool.cn/escript6/escript6-m42v37eq.html
  2. https://www.runoob.com/w3cnote/es6-tutorial.html