Web

ES6 学习

Posted by Kerwen Blog on July 16, 2020

https://es6.ruanyifeng.com/

let & const

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

1
2
3
4
for (let i = 0; i < 10; i++) {
    // ...
}
console.log(i);    // ReferenceError: i is not defined

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。let命令所声明的变量一定要在声明后使用,否则报错。

const声明一个只读的常量。一旦声明,常量的值就不能改变。

变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

1
let [a, b, c] = [1, 2, 3];

解构赋值允许指定默认值。

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

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

1
const [a, b, c, d, e] = 'hello';

变量的解构赋值用途:

  1. 交换变量的值

    1
    2
    3
    
     let x = 1;
     let y = 2;
     [x, y] = [y, x];
    
  2. 从函数返回多个值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
     // 返回一个数组
     function example() {
         return [1, 2, 3];
     }
     let [a, b, c] = example();
    
     // 返回一个对象
     function example() {
         return {
             foo: 1,
             bar: 2
         };
     }
     let { foo, bar } = example();
    
  3. 提取 JSON 数据

    1
    2
    3
    4
    5
    6
    7
    8
    
     let jsonData = {
         id: 42,
         status: "OK",
         data: [867, 5309]
     };
     let { id, status, data: number } = jsonData;
     console.log(id, status, number);
     // 42, "OK", [867, 5309]
    
  4. 遍历 Map 结构

    const map = new Map(); map.set(‘first’, ‘hello’); map.set(‘second’, ‘world’);

    for (let [key, value] of map) { console.log(key + “ is “ + value); } // first is hello // second is world

字符串

传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

1
2
3
4
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

函数的扩展

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

1
2
3
4
function log(x, y = 'World') {
    console.log(x, y);
}
log('Hello') // Hello World

箭头函数

ES6 允许使用“箭头”(=>)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

1
2
3
4
5
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
    return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
箭头函数的一个用处是简化回调函数。

1
2
3
4
5
6
7
// 正常函数写法
[1,2,3].map(function (x) {
    return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

数组

  1. find()
    数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

    1
    2
    
     [1, 4, -5, 10].find((n) => n < 0)
     // -5
    
  2. fill()
    fill方法使用给定值,填充一个数组。

    1
    2
    
     new Array(3).fill(7)
     // [7, 7, 7]
    
  3. includes()
    Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,

    1
    2
    
     [1, 2, 3].includes(2)     // true
     [1, 2, 3].includes(4)     // false  
    

set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

1
2
3
4
5
6
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
    console.log(i);
}
// 2 3 5 4

方法:
add(value), delete(value), has(value), clear()

1
2
3
4
5
6
7
s.add(1).add(2).add(2);    // 注意2被加入了两次
s.size // 2
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

Array.from方法可以将 Set 结构转为数组。

1
2
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

去除数组重复成员:

1
2
3
4
function dedupe(array) {
    return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]

Map

Map是键值对的集合,“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

1
2
3
4
5
6
7
8
const map = new Map([
    ['name', '张三'],
    ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"

Map构造函数接手数组或set作为参数

1
2
3
4
5
6
7
8
9
10
11
12
const set = new Set([
    ['foo', 1],
    ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"

Promise

ES6 将Promise写进了语言标准,统一了用法,原生提供了Promise对象。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise = new Promise(function(resolve, reject) {
    // ... some code

    if (/* 异步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }
});

promise.then(function(value) {
    // success
}, function(error) {
    // failure
});

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
下面是一个Promise对象的简单例子。

1
2
3
4
5
6
7
8
9
function timeout(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, ms, 'done');
    });
}

timeout(100).then((value) => {
    console.log(value);
});

加载图片

1
2
3
4
5
6
7
8
const preloadImage = function (path) {
    return new Promise(function (resolve, reject) {
        const image = new Image();
        image.onload  = resolve;
        image.onerror = reject;
        image.src = path;
    });
};

Generator函数

Generator 函数是 ES6 提供的一种异步编程解决方案. 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

1
2
3
4
5
6
7
8
9
10
11
function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
    return 'ending';
}

var hw = helloWorldGenerator();
console.log(hw.next()); // { value: 'hello', done: false }
console.log(hw.next()); // { value: 'world', done: false }
console.log(hw.next()); // { value: 'ending', done: true }
console.log(hw.next()); // { value: undefined, done: true }

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

1
2
3
function* gen() {
yield  123 + 456;
}

Generator的应用:

  1. 异步操作的同步化表达
    Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

    function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next()

    // 卸载UI loader.next()

第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。

异步

所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
ES6 诞生以前,异步编程的方法,大概有下面四种。

1
2
3
4
回调函数
事件监听
发布/订阅
Promise 对象

Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。

回调函数

所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

1
2
3
4
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
    if (err) throw err;
    console.log(data);
});

上面代码中,readFile函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。

Promise

回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。

1
2
3
4
5
fs.readFile(fileA, 'utf-8', function (err, data) {
    fs.readFile(fileB, 'utf-8', function (err, data) {
        // ...
    });
});

如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为”回调函数地狱”(callback hell)。
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
    console.log(data.toString());
})
.then(function () {
    return readFile(fileB);
})
.then(function (data) {
    console.log(data.toString());
})
.catch(function (err) {
    console.log(err);
});

Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

Generator函数

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fetch = require('node-fetch');
function* gen(){
    var url = 'https://api.github.com/users/github';
    var result = yield fetch(url);
    console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
    return data.json();
}).then(function(data){
    g.next(data);
});

首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async函数对 Generator 函数的改进,体现在以下四点。

  1. 内置执行器
    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

  2. 更好的语义。
    async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性。
  4. 返回值是 Promise。
    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

1
2
3
4
5
6
7
8
9
async function getStockPriceByName(name) {
    const symbol = await getStockSymbol(name);
    const stockPrice = await getStockPrice(symbol);
    return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
    console.log(result);
});

另一个例子,指定多少毫秒后输出一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function timeout(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value);
}

asyncPrint('hello world', 2000).then(function(){
    console.log("execute end");
});
console.log("Main");
// Main
// hello world
// execute end

Class

ES6 引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

1
2
3
4
5
6
7
8
9
10
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return '(' + this.x + ', ' + this.y + ')';
    }
}
var point = new Point(2, 3);

定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
}
class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
    }

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

子类必须在constructor方法中调用super方法,否则新建实例时会报错。

Module

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。

1
2
3
4
5
6
7
8
// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块是通过export命令显式指定输出的代码,再通过import命令输入。

1
2
3
4
5
6
7
8
9
10
11
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
export { firstName, lastName, year };

// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
    element.textContent = firstName + ' ' + lastName;
}

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

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

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。