Bootstrap

EcmaScript 6 新特性

想要完整了学习 ECMAScipt 6,并且能够理解的比较透彻,最好还是首先了解一下 Node.js 和 AJAX 相关知识。可以参考以下博文:

Node.js「一」—— Node.js 简介 / Node.js 模块 / 包 与 NPM

Node.js「二」—— fs 模块 / async 与 await

Node.js「三」—— 创建静态 WEB 服务器

Node.js「四」—— 路由 / EJS 模板引擎 / GET 和 POST

AJAX —— 原生 AJAX / jQuery 发送 AJAX / axios 发送 AJAX / fetch 发送 AJAX

「一」ES 介绍


ES 全称 EcmaScript,是脚本语言的规范,而平时经常编写的 JavaScript,是 EcmaScript 的一种实现,所以 ES 新特性其实指的就是 JavaScript 的新特性

  • ECMA

ECMA 是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名 Ecma国际(Ecma International)。

  • ECMAScript

ECMAScript 是一种由 Ecma国际 通过 ECMA-262 标准化的脚本程序设计语言。 这种语言在万维网上应用广泛,它往往被称为 JavaScript 或 JScript,所以它可以理解为是 JavaScript 的一个标准,但实际上后两者是 ECMA-262 标准的实现和扩展。

  • 为什么要学习 ES6
  1. ES6 的版本变动内容最多,具有里程碑意义
  2. ES6 加入许多新的语法特性,编程实现更简单、高效
  3. ES6 是前端发展趋势,就业必备技能
  • ES6 兼容性

https://kangax.github.io/compat-table/es6/ 可以查看兼容性信息。
在这里插入图片描述
 

「二」let 和 const 关键字


1. let 关键字

let 关键字用来声明变量

    let a;
    let b, c, d;
    let e = 100;
    let f = 521, g = 'love', h = [];
  • 用法特点
  1. 不允许重复声明
    在这里插入图片描述

  2. 块级作用域
    在这里插入图片描述

  3. 不存在变量提升
    在这里插入图片描述

  4. 不影响作用域链
    在这里插入图片描述

  • 案例:点击切换颜色

请添加图片描述

    let items = document.getElementsByClassName('item');

    for (let i = 0; i < items.length; i++) {
        items[i].onclick = function () {
            items[i].style.background = 'skyblue';
        }
    }

需要注意,如果 for 循环中使用 var i = 0var i 没有块级作用域,在全局变量中存在,导致在点击事件未开始时,i 已经自增到 3,因此点击会将 items[3] 属性改变,此标签不存在,所以没有反应。

而此处如果使用 let i = 0let i 只在自己的作用域里面有效,互不影响,因此可以为每个 item 添加点击事件。类似于下面这样

    {
        let i = 0;
        items[i].onclick = function () {
            items[i].style.background = 'skyblue';
        }
    }
    {
        let i = 1;
        items[i].onclick = function () {
            items[i].style.background = 'skyblue';
        }
    }
    ...

当然,如果你比较调皮,非要使用 var i = 0,那么可以使用闭包。如下

    for (var i = 0; i < items.length; i++) {
        (function (i) {
            lis[i].onclick = function () {
                items[i].style.background = 'skyblue';
            }
        })(i);
    }

 
2. const 关键字


const 关键字用来声明常量,const 声明有以下特点

  1. 声明必须赋初始值
        const A;    // 报错
  1. 标识符一般为大写
  2. 不允许重复声明
        const STAR = '派大星';
        const STAR = '海绵宝宝';       // 报错
  1. 值不允许修改
        const STAR = '派大星';
        STAR = '海绵宝宝';       // 报错
  1. 块级作用域
        {
            const PLAYER = 'UZI';
        }
        console.log(PLAYER);    // 报错
  1. 对于数组和对象的元素修改,不算做对常量的修改,不会报错
        const TEAM = ['UZI', 'MLXG', 'Ming']
        TEAM.push('XiaoHu');    // 不会报错

应用场景:声明对象类型使用 const,非对象类型声明选择 let

 

「三」变量的解构赋值


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

  • 数组的解构
    const arr = ['张学友', '刘德华', '黎明', '郭富城'];
    let [zhang, liu, li, guo] = arr;
    console.log(zhang);     // 张学友
    console.log(liu);       // 刘德华
    console.log(li);        // 黎明
    console.log(guo);       // 郭富城

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

如果解构不成功,变量的值就等于 undefined

    let [bar, foo] = [1];
    console.log(bar, foo);  // 1 undefined

还一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

    let [x, y] = [1, 2, 3, 4];
    console.log(x, y);      // 1, 2

 

  • 对象的解构

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

    const lin = {
        name: '林志颖',
        tags: ['车手', '歌手', '小旋风', '演员'],
        car: function () {
            console.log('我是赛车手');
        }
    };
    let { name, tags, car } = lin;
    console.log(name);  	// 林志颖
    console.log(tags);  	// ['车手', '歌手', '小旋风', '演员']
    car();              	// 我是赛车手

 

「四」模板字符串


模板字符串(template string)是增强版的字符串,用反引号(`)标识,

    let str = `我也是字符串`;
    console.log(str);           // 我也是字符串
    console.log(typeof str);    // string
  • 特点
  1. 字符串中可以出现换行符。如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
    let str = `<ul>
                    <li>沈腾</li>
                    <li>魏翔</li>
                </ul>`;
  1. 模板字符串中嵌入变量,需要将变量名写在 ${} 之中。大括号{}内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
    let music = '遥远的她';
    let mylove = `${music}是我最喜欢的一首歌`;
    console.log(mylove);    	// 遥远的她是我最喜欢的一首歌

 

「五」简化对象写法


ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

在这里插入图片描述

 

「六」箭头函数


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

    // let fn = function () {

    // }

    let fn = (a, b) => {
        return a + b;
    }
    let result = fn(1, 2);
    console.log(result);        // 3

注意:

  1. 箭头函数没有自己的 this 对象

    对于普通函数来说,内部的 this 指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的 this 对象,内部的 this 就是定义时上层作用域中的 this。也就是说,箭头函数内部的 this 指向是固定的,相比之下,普通函数的 this 指向是可变的。

    function getName() {
        console.log(this.name);
    }
    let getName1 = () => {
        console.log(this.name);
    }

    window.name = 'window';
    const star = {
        name: 'star'
    }

    getName.call(star);     // star
    getName1.call(star);    // window
  1. 不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误。
    let Person = (name, age) => {
        this.name = name;
        this.age = age;
    }
    
    let me = new Person('andy', 18);
    // ERROR: Person is not a constructor
  1. 不可以使用 arguments 对象,该对象在函数体内不存在。
    let fn = () => {
        console.log(arguments);
    }
    fn(1, 2, 3);
    // ERROR: arguments is not defined
  1. 如果形参有且只有一个,则小括号可以省略。
     let add = n => {		 // 省略 (n) 的小括号
         return n + n;
     }
     console.log(add(1));    // 2
  1. 函数体如果只有一条语句,则花括号可以省略(此时,return 必须省略),函数的返回值为该条语句的执行结果。
    // let pow = (n) => {
    //     return n * n;
    // }
    let pow = n => n * n;

    console.log(pow(2));        // 4
  • 案例:盒子定时变色

    请添加图片描述

    let div = document.querySelector('div');
    div.addEventListener('click', function () {
        setTimeout(() => {
            this.style.backgroundColor = 'tomato';
        }, 1000);
    })

注意:这里使用箭头函数 setTimeout(() => {})。因为此箭头函数中 this 值为函数声明时所在作用域下的 this 值,也就是 div。并不是定时器的 thiswindow。因此可以直接利用 this.style.backgroundColor 改变盒子背景颜色。

  • 案例:从数组中寻找偶数
     const arr = [1, 6, 9, 10, 100, 15];
     
     // const result = arr.filter(function (item) {
     //     if (item % 2 === 0)
     //         return true;
     //     else
     //         return false;
     // });
     
     const result = arr.filter(item => item % 2 === 0);
     console.log(result);        // [6, 10, 100]

注释部分为没有利用箭头函数时的写法,可以对比一下,就会发现箭头函数的妙处了。总的来说,箭头函数适合与 this 无关的回调,比如定时器、数组方法回调等。

 

「七」参数默认值


ES6 允许给函数参数赋初始值,具有默认值的参数一般位置靠后。

    function add(a, b, c = 3) {
        return a + b + c;
    }
    console.log(add(1, 2));     // 6

此外,参数默认值可以与解构赋值结合来使用。如下

    function connect({ host = "127.0.0.1", username, password, port }) {
        console.log(host)       // baidu.com
        console.log(username)   // root
        console.log(password)   // root
        console.log(port)       // 3306
    }
    connect({
        host: 'baidu.com',
        username: 'root',
        password: 'root',
        port: 3306
    })

 

「八」rest 参数


ES6 引入 rest 参数,用于获取函数的实参,用来代替 arguments

我们先来回忆一下 ES5 获取实参的方式,如下:

    function data() {
        console.log(arguments);
    }
    data('派大星', '海绵宝宝', '章鱼哥'); 
  • 注意这里获取到的是一个对象,而不是数组
    在这里插入图片描述

下面来看 ES6 获取实参的方法,如下:

    function data(...args) {
        console.log(args);
    }
    data('派大星', '海绵宝宝', '章鱼哥');
  • 用此方式获取到的实参是数组,可以采用数组方法对实参进行处理,如 filtersomeevery
    在这里插入图片描述

注意:rest 参数必须放到参数最后,否则会报错

    function fn(a, b, ...args) {
	
    }

 

「九」spread 扩展运算符


扩展运算符 spread 也是三个点 ... 。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。

在这里插入图片描述

下面举例介绍扩展运算符的应用。

  • 案例:数组的合并
    const s1 = ['刘德华', '张学友'];
    const s2 = ['黎明', '郭富城'];
    const sdtw = [...s1, ...s2];
    console.log(sdtw);      //  ['刘德华', '张学友', '黎明', '郭富城']
  • 案例:数组的克隆
    const szh = ['E', 'G', 'M'];
    const clone = [...szh];
    console.log(clone);     // ['E', 'G', 'M']

注意:这里的拷贝是浅拷贝

  • 案例:伪数组转为真正数组
    const divs = document.querySelectorAll('div');
    console.log(divs);      // 返回对象 NodeList(5)
    const divArr = [...divs];
    console.log(divArr);    // 返回数组 Array(5)

 

「十」Symbol


1. Symbol 基本介绍

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入 Symbol 的原因。

ES6 引入了一种新的基本数据类型 Symbol,每个从 Symbol() 返回的 symbol 值都是唯一的,表示独一无二的值,可以用来解决命名冲突的问题。

它是 JavaScript 语言的第七种数据类型,前六种是:undefinednullBooleanStringNumberObject

 
2. Symbol.prototype.description


创建 Symbol 的时候,可以添加一个描述。

	Symbol([description])
  • description: 可选的,字符串类型。对 symbol 的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

但是,读取这个描述需要将 Symbol 显式转为字符串,如下:

	const sym = Symbol('foo');

	String(sym) 		// "Symbol(foo)"
	sym.toString() 		// "Symbol(foo)"

上面的用法不是很方便。ES2019 提供了一个实例属性 description,直接返回 Symbol 的描述:

    let sym = Symbol('foo');
    console.log(sym.description);    // foo

 
3. 作为属性名的 Symbol


由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

如下图所示通过方括号 对象名['属性名'] (这里的字符串'属性名'用 Symbol 值代替)结构将对象的属性名指定为一个 Symbol 值:
在这里插入图片描述
除了此写法,还有下面两种方式,其打印结果都是相同的。如下代码:

let a = {
  [mySymbol]: 'Hello!'
};
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

 
注意:Symbol 值作为对象属性名时,不能用点运算符。

因为点运算符后面总是字符串,所以不会读取 mySymbol 作为标识名所指代的那个值,导致 a.mySymbol 的被认为是新添加的属性名。如下:
在这里插入图片描述

同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。如下:
在这里插入图片描述

4. Symbol 用于定义常量


Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。

const COLOR_RED    = Symbol();
const COLOR_GREEN  = Symbol();

function getComplement(color) {
  switch (color) {
    case COLOR_RED:
      return COLOR_GREEN;
    case COLOR_GREEN:
      return COLOR_RED;
    default:
      throw new Error('Undefined color');
    }
}

常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。

 
5. 属性名的遍历


Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for...infor...of 循环中,也不会被Object.keys()Object.getOwnPropertyNames()等返回。
在这里插入图片描述

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

但是,它也 不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
在这里插入图片描述

Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。

Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法

 
6. Symbol.for()


通过前面的学习我们知道,使用 Symbol() 返回的 Symbol 值是不同的。如下:

 	let s1 = Symbol();
   	let s2 = Symbol();
   	console.log(s1 === s2);     // false

但是有的时候,我们希望重新使用同一个 Symbol 值,这时就可以使用 Symbol.for() 方法。

Symbol() 不同的是,用 Symbol.for(key) 方法创建的 Symbol 会被放入一个全局 Symbol 注册表中。注册表中的记录结构如下:

字段名字段值
[[key]]一个字符串,用来标识每个 symbol
[[symbol]]存储的 symbol 值

Symbol.for() 并不是每次都会创建一个新的 Symbol,它会首先检查给定的 key 是否已经在注册表中了。如果存在,则会直接返回上次存储的那个。否则,它会再新建一个。

    let s1 = Symbol.for('foo');
    let s2 = Symbol.for('foo');
    console.log(s1 === s2);     // true

注意:Symbol.for() 为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。

 
7. Symbol 的内置属性


除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。

  • Symbol.hasInstance

对象的 Symbol.hasInstance 属性,指向一个内部方法。当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法。利用这个方法,我们可以实现自己去控制类型检测。
在这里插入图片描述
上面代码中,MyClass 是一个类,new MyClass() 会返回一个实例。该实例的 Symbol.hasInstance 方法,会在进行 instanceof 运算时自动调用,判断左侧的运算子是否为 Array 的实例。

  • Symbol.isConcatSpreadable

对象的 Symbol.isConcatSpreadable 属性等于一个布尔值,表示该对象用于 Array.prototype.concat() 时,是否可以展开。

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    console.log(arr1.concat(arr2));     // [1, 2, 3, 4, 5, 6]

    arr2[Symbol.isConcatSpreadable] = false;
    console.log(arr1.concat(arr2));     // [1, 2, 3, Array(3)]

数组的默认行为是可以展开,Symbol.isConcatSpreadable 默认等于 undefined。该属性等于 true 时,也有展开的效果。如果将该属性设置为 false ,则表示不可以展开。

 

「十一」迭代器


遍历器(Iterator)就是一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。

  • Iterator 的作用
  1. 为各种数据结构,提供一个统一的、简便的访问接口
  2. 使得数据结构的成员能够按某种次序排列
  3. ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费
  • 工作原理
  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象
  2. 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员
  3. 不断调用指针对象的 next 方法,直到它指向数据结构的结束位置
  4. 每一次调用 next 方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 valuedone 两个属性的对象。其中,value 属性是当前成员的值,done 属性是一个布尔值,表示遍历是否结束

在这里插入图片描述

  • for...offor..in 区别

for...of 返回的是每个键值,而 for..in 返回的是键名
在这里插入图片描述

  • 应用:实现自定义遍历数据

这里展示一个利用迭代器自定义遍历数据的应用,实现对象 xcm 中 actors 数组的遍历。

    // 声明一个对象
    const obj = {
        name: '熊出没',
        actors: [
            '熊大',
            '熊二',
            '光头强',
        ],
        [Symbol.iterator]() {
            let index = 0;
            let _this = this;

            return {
                next() {
                    if (index < _this.actors.length) {
                        const result = { value: _this.actors[index], done: false };
                        index++;
                        return result;
                    } else {
                        return { value: _this.actors[index], done: true };
                    }
                }
            }
        }
    }
    
    for (let v of obj) {
        console.log(v);
    }

上面代码中,对象 obj 是可遍历的(Iterable),因为具有 Symbol.iterator 属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 valuedone 两个属性。

  • 具备此接口的数据类型

ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for…of循环遍历。原因在于,这些数据结构原生部署了 Symbol.iterator 属性。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。

原生具备 Iterator 接口的数据(可用 for...of 遍历)有:Array函数的 arguments 对象SetMapStringTypedArrayNodeList
 

「十二」Generator 函数


生成器函数(Generator)是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

  • 特点
  1. function 关键字与函数名之间有一个 *,如 function* fn(){}
  2. 函数体内部使用 yield 表达式,定义不同的内部状态

Generator 函数的调用方法与普通函数一样。不同的是,调用 Generator 函数后,该函数并不执行,而是返回一个遍历器对象,里面含有 next 方法。
在这里插入图片描述

那么如何使函数内代码执行呢,可以借助调用遍历器对象的 next 方法,使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return语句)为止。

换言之,Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
在这里插入图片描述

  • yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 表达式就是暂停标志。

遍历器对象的 next 方法的运行逻辑如下:

  1. 遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值
  2. 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式
  3. 如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值
  4. 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined

在这里插入图片描述

  • next 方法的参数

yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

在这里插入图片描述
注意console.log(a) 并没有任何打印,这是生成器最初没有产生任何结果。

这个功能有很重要的语法意义。通过它就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

  • 案例:模拟获取数据

用定时器模拟异步行为,每隔 1 秒获取数据,顺序为 用户数据 => 订单顺序 => 商品数据。
请添加图片描述
当然很容易可以想到,利用定时器套定时器可以实现这个目的,如下代码:

    setTimeout(() => {
        let data = '用户数据';
        console.log(data);
        setTimeout(() => {
            let data = '订单数据';
            console.log(data);
            setTimeout(() => {
                let data = '商品数据';
                console.log(data);
            }, 1000);
        }, 1000);
    }, 1000);

但是,当异步操作越多,这种嵌套的层级也就越复杂,代码可读性非常差,不利于代码后期维护。这种现象被称为 回调地狱

为解决回调地狱问题,可以利用 Generator 函数,将 data 作为 next 方法参数传入,利用 yield 返回值打印 。

	function getUsers() {
        setTimeout(() => {
            let data = '用户数据';
            // 第二次调用 next 方法,并将数据传入
            iterator.next(data);
        }, 1000);
    }

    function getOrders() {
        setTimeout(() => {
            let data = '订单数据';
            iterator.next(data)
        }, 1000);
    }

    function getGoods() {
        setTimeout(() => {
            let data = '商品数据';
            iterator.next(data)
        }, 1000);
    }

    function* gen() {
        let users = yield getUsers();
        console.log(users);
        let orders = yield getOrders();
        console.log(orders);
        let goods = yield getGoods();
        console.log(goods);
    }

    let iterator = gen();
    // 第一次调用 next()
    iterator.next();

 

「十三」Promise


Promise 是 ES6 引入的异步编程的新解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
 
Promise 有两个特点

  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和 从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。

    如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
 
当然,Promise 也有它的缺点

  1. 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
  3. 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

 
Promise 基本用法

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

	const promise = new Promise(function(resolve, reject) {
	  // ... some code
	
	  if (/* 异步操作成功 */){
	    resolve(value);
	  } else {
	    reject(error);
	  }
	});

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolvereject 。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

  1. resolve:将 Promise 对象的状态从 pending 变为 fullfilled,在异步操作成功时调用,并将异步操作的结果,作为参数 value 传递出去。
  2. reject:将 Promise 对象的状态从 pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数 error 传递出去。

resolved 不一定表示状态变为 fulfilled 状态;而 resolve 一定是成功 fulfilled 时执行的回调

 
Promise.prototype.then()

Promise 实例生成以后,可以用 then() 方法分别指定当 Promise 变为 fulfilled(成功)rejected(失败) 状态时的回调函数。

	promise.then(function(value) {
	  // 成功后执行的回调
	}, function(error) {
	  // 失败后执行的回调
	});

可参考 MDN 给出的关系图:
在这里插入图片描述
注意:then() 方法返回的是一个新的 Promise 实例(见下图)。因此可以采用链式写法,即 then() 方法后面再调用另一个 then() 方法(套娃)。

在这里插入图片描述
再说一下 then() 方法中回调函数的返回值:

  • 若返回一个非 Promise 类型值(如上图),那么 then() 返回的 Promise 将会成为接受状态 fulfilled,并且将返回的值作为接受状态的回调函数的参数值。

  • 若没有返回任何值,那么 then() 返回的 Promise 将会成为接受状态 fulfilled,并且该接受状态的回调函数的参数值为 undefined

  • 若抛出一个错误,那么 then() 返回的 Promise 将会成为拒绝状态 rejected,并且将抛出的错误作为拒绝状态的回调函数的参数值。
    在这里插入图片描述

  • 若返回一个已经是接受状态的 Promise,那么 then() 返回的 Promise 也会成为接受状态 fulfilled,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的 Promise 的接受状态回调函数的参数值。

  • 若返回一个已经是拒绝状态的 Promise,那么 then() 返回的 Promise 也会成为拒绝状态 rejected,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的 Promise 的拒绝状态回调函数的参数值。如下图:
    在这里插入图片描述

  • 若返回一个未定状态 pending 的 Promise,那么 then() 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。

 
举一个 Promise 对象的简单例子

    function timeout(ms) {
        return new Promise((resolve, reject) => {
            setTimeout(resolve, ms, 'done');	// 'done' 作为参数传给 resolve
        });
    }

    timeout(100).then((value) => {
        console.log(value);					// 打印 done
    });

上面代码中,timeout 函数返回一个Promise 实例,表示一段时间以后才会发生的结果。过了指定的时间 ms 以后,Promise 实例的状态变为 resolved,就会触发 then() 方法绑定的回调函数。

 
再举一个 Promise 封装 AJAX 请求的例子

	const p = new Promise((resolve, reject) => {
        // 1. 创建对象
        const xhr = new XMLHttpRequest();
        // 2. 初始化
        xhr.open('GET', 'https://api.apiopen.top/getJoke');
        // 3. 发送
        xhr.send();
        // 4. 绑定事件
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(xhr.response)
                } else {
                    reject(xhr.status);
                }
            }
        }
    })

    // 指定回调
    p.then(function (value) {
        console.log(value);
    }, function (reason) {
        console.error(reason);
    })

利用 Promise 使得代码逻辑结构更加清晰,而且不会产生回调地狱的问题。
 
注意:Promise 新建后就会立即执行。如下

	let promise = new Promise(function(resolve, reject) {
	  console.log('Promise');
	  resolve();
	});
	
	promise.then(function() {
	  console.log('resolved.');
	});
	
	console.log('Hi!');
	
	// 依次打印:
	// Promise
	// Hi!
	// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是 Promise。而 then 方法指定的 回调函数,将在当前脚本 所有同步任务执行完才会执行,所以最后输出 resolved

 
实践练习 —— 读取多个文件

需求:利用 node.js 按顺序读取文件,并将文件内容拼接后打印,实现如下效果:
在这里插入图片描述
一般方法 —— 嵌套:

fs.readFile('./resources/为学.md', (err, data1) => {
    fs.readFile('./resources/插秧诗.md', (err, data2) => {
        fs.readFile('./resources/观书有感.md', (err, data3) => {
            let result = data1 + '\n' + data2 + '\n' + data3;
            console.log(result);
        });
    });
});

这种方法的弊端很明显:会出现回调地狱的问题,而且容易重名,调式问题很不方便。

下面我们利用 Promise 来实现:

const p = new Promise((resolve, reject) => {
    fs.readFile('./resources/为学.md', (err, data) => {
        resolve(data);
    })
})

p.then(value => {
    return new Promise((resolve, reject) => {
        fs.readFile('./resources/插秧诗.md', (err, data) => {
            resolve([value, data]);
        });
    })
}).then(value => {
    return new Promise((resolve, reject) => {
        fs.readFile('./resources/观书有感.md', (err, data) => {
            // 压入,此时的 value 是上面的数组
            value.push(data);
            resolve(value);
        });
    })
}).then(value => {
    console.log(value.join('\r\n'));
});

这样我们就将异步任务串联了起来,而且不会出现回调地狱的问题。

 
Promise.prototype.catch()

catch() 方法返回一个 Promise,并且处理拒绝的情况。

	const p = new Promise((resolve, reject) => {
	     setTimeout(() => {
	         reject('出错');
	     }, 1000);
	 })
	
	 p.catch(reason => {
	     console.warn(reason);
	 })

实际上它是一个语法糖,其作用等同于下面这种写法:

	p.then(value => { }, reason => {
	    console.warn(reason);
	})

Promise 的介绍暂且结束,其实 Promise 还有很多其他的语法、API,因为文章重心和篇幅原因不再详解。

 

「十四」Set 与 Map


先介绍一下 Set 对象

Set 对象是值的集合,你可以按照插入的顺序迭代它的元素。 Set 中的元素只会出现一次,即 Set 中的元素是唯一的。

Set 对象实现了 iterator 接口,所以可以使用 扩展运算符for…of… 进行遍历。

Set 集合的属性和方法:

  1. Set.prototype.size: 返回集合的元素个数
  2. Set.prototype.add(value): 添加某个值,返回 Set 结构本身。
  3. Set.prototype.delete(value): 删除某个值,返回一个布尔值,表示删除是否成功。
  4. Set.prototype.has(value): 返回一个布尔值,表示该值是否为 Set 的成员。
  5. Set.prototype.clear(): 清除所有成员,没有返回值。
	// 声明
	let s = new Set(['玛卡巴卡', '唔西迪西', '小点点', '玛卡巴卡']);
	
	// 元素个数
	console.log(s.size);        // 3
	
	// 添加新的元素
	s.add('汤姆布利柏');        // Set(4) { '玛卡巴卡', '唔西迪西', '小点点', '汤姆布利柏' }
	
	// 删除元素
	s.delete('小点点');         // Set(3) { '玛卡巴卡', '唔西迪西', '汤姆布利柏' } 
	
	for (let v of s) {
	    console.log(v);         
	    // 依次打印:
	    // 玛卡巴卡
	    // 唔西迪西
	    // 汤姆布利柏
	}

举几个 Set 集合的应用

  • 数组去重
	let arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];
	let result = [...new Set(arr)];
	console.log(result);        // [ 1, 2, 3, 4, 5 ]
  • 交集
	let arr1 = [1, 2, 3, 4, 5, 4, 3, 2, 1];
	let arr2 = [4, 5, 6];
	
	let result = [...new Set(arr1)].filter(item => new Set(arr2).has(item));
	console.log(result);    // [ 4, 5 ]
  • 并集
	let arr1 = [1, 2, 3, 4, 5, 4, 3, 2, 1];
	let arr2 = [4, 5, 6];
	
	let union = [...new Set([...arr1, ...arr2])];
	console.log(union);     // [ 1, 2, 3, 4, 5, 6 ]
  • 差集
	let arr1 = [1, 2, 3, 4, 5, 4, 3, 2, 1];
	let arr2 = [4, 5, 6];
	
	let result = [...new Set(arr1)].filter(item => !(new Set(arr2).has(item)));
	console.log(result);    // [ 1, 2, 3 ]

 


下面介绍一下 Map

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。

Map 也实现了 iterator 接口,所以也可以使用 扩展运算符for…of… 进行遍历。

Map 的属性和方法:

  1. size: 返回 Map 结构的成员总数。
  2. Map.prototype.set(key, value): 设置键名 key 对应的键值为 value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
  3. Map.prototype.get(key): 读取 key 对应的键值,如果找不到 key,返回 undefined
  4. Map.prototype.has(key): 返回一个布尔值,表示某个键是否在当前 Map 对象之中
  5. Map.prototype.delete(key): 删除某个键,返回 true。如果删除失败,返回 false
  6. Map.prototype.clear():清除所有成员,没有返回值。
const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

 

「十五」class 类


JavaScript 语言中,生成实例对象的传统方法是通过构造函数。如下:

	function Point(x, y) {
	  this.x = x;
	  this.y = y;
	}
	
	Point.prototype.toString = function () {
	  return '(' + this.x + ', ' + this.y + ')';
	};
	
	var p = new Point(1, 2);

为了更接近传统语言的写法,像 c++ 或者 java 那样,ES6 引入了 Class 这个概念,作为对象的模板。通过 class 关键字,可以定义类。

基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。如下:

	class Point {
	  constructor(x, y) {
	    this.x = x;
	    this.y = y;
	  }
	
	  toString() {
	    return '(' + this.x + ', ' + this.y + ')';
	  }
	}

前文已经总结过 ES6 类构造、继承的相关知识点,此处不再过多解释,参考此文 面向对象基础

 

「十六」数值的扩展


  • Number.EPSILON

ES6 在 Number 对象上面,新增一个极小的常量 Number.EPSILON

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

	Number.EPSILON		// 2.220446049250313e-16

Number.EPSILON 可以用来设置 “ 能够接受的误差范围 ”,进而判断两个浮点数是否相等。如下:

	function equal(a, b) {
	    return Math.abs(a - b) < Number.EPSILON;
	}
	
	console.log(0.1 + 0.2 === 0.3);         // false
	console.log(equal(0.1 + 0.2, 0.3));     // true
  • 二进制和八进制

ES6 提供了二进制和八进制数值的新的写法,分别用前缀 0b(或 0B)和 0o(或 0O)表示。

	0b111110111 === 503 	// true
	0o767 === 503 			// true

如果要将 0b0o 前缀的字符串数值转为十进制,要使用 Number 方法。

	Number('0b111')  		// 7
	Number('0o10')  		// 8
  • 数值分隔符

欧美语言中,较长的数值允许每三位添加一个分隔符(通常是一个逗号),增加数值的可读性。比如,1000 可以写作 1,000。

ES2021,允许 JavaScript 的数值使用下划线 _ 作为分隔符。

	let budget = 1_000_000_000_000;
	budget === 10 ** 12 				// true

这个数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。

	123_00 === 12_300 			// true
	12345_00 === 123_4500 		// true
	12345_00 === 1_234_500 		// true

小数和科学计数法也可以使用数值分隔符。

	// 小数
	0.000_001
	// 科学计数法
	1e10_000

 
此外还有一些其他的 API,下表中简单列出,详细用法可查阅文档

方法描述
Number.isFinite()用来检查一个数值是否为有限的
Number.isNaN()用来检查一个值是否为NaN
Number.parseInt()字符串转为整数
Number.parseFloat()字符串转为浮点数
Number.isInteger()用来判断一个数值是否为整数
Math.trunc()用于去除一个数的小数部分,返回整数部分
Math.sign()用来判断一个数到底是正数、负数、还是零
Math.cbrt()用于计算一个数的立方根

可参考 数值的扩展 —— 阮一峰

 

「十七」对象的扩展


ES6 新增了一些 Object 对象的方法。这里主要介绍三种:

  1. Object.is(): 比较两个值是否严格相等,与 === 行为基本一致(区别在于 ±0 与 NaN)
  2. Object.assign(): 对象的合并,将源对象的所有可枚举属性,复制到目标对象(如果重名,后面属性值会覆盖前面)
  3. Object.setPrototypeOf()Object.getPrototypeOf(): 可以直接设置和获取对象的原型(不建议这么做)

 

「十八」模块化


模块化是指将一个大的程序文件,拆分成许多个小的文件,然后将小文件组合起来。
 
模块化有什么好处呢?

  1. 可以防止命名冲突。
  2. 提高代码复用。可以将功能代码封装成文件,对外只暴露接口。
  3. 维护性高。
     

模块化规范的产品有哪些?

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJSAMD 两种。前者用于服务器,后者用于浏览器。

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
 

ES6 模块化的语法是什么?

模块化功能主要由两个命令构成:export 和 import。

  • export:用于规定模块的对外接口
  • import:用于输入其他模块提供的功能

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上 "use strict";

 
export 命令几种写法

1. 分别暴露

	export var firstName = 'Michael';
	export var lastName = 'Jackson';
	export function multiply(x, y) {
	  return x * y;
	};

2. 统一暴露

	var firstName = 'Michael';
	var lastName = 'Jackson';
	function multiply(x, y) {
	  return x * y;
	};
	
	export { firstName, lastName, multiply };

3. 默认暴露

	// 文件 m3.js
	export default {
	    school: 'CSDN',
	    ad: function () {
	        console.log('VIP买一年送一年,再赠羽绒服,抽万元壕礼!');
	    }
	}

上面这段代码,向外默认暴露了一个对象。该对象里面有一个 school 属性和一个方法 ad()

注意其引入后的调用格式,如下

	import * as m3 from './m3.js';
	console.log(m3.default.school);     // CSDN
	m3.default.ad();                    // VIP买一年送一年,再赠羽绒服,抽万元壕礼!

export 命令的几个注意点

  1. 通常情况下,export 输出的变量就是本来的名字,但是可以使用 as 关键字重命名。
	function v1() { ... }
	function v2() { ... }
	
	export {
	  v1 as streamV1,
	  v2 as streamV2,
	  v2 as streamLatestVersion
	};
  1. export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
	// 报错: 没有提供对外的接口
	export 1;
	
	// 报错:同上,没有提供对外的接口
	var m = 1;
	export m;

        正确的写法是下面这样:

	// 写法一
	export var m = 1;
	
	// 写法二
	var m = 1;
	export {m};
	
	// 写法三
	var n = 1;
	export {n as m};
  1. export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
	export var foo = 'bar';
	setTimeout(() => foo = 'baz', 500);
	// 上面代码暴露变量 foo,值为 bar,500 毫秒之后变成 baz
  1. export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。
	function foo() {
	  export default 'bar' 	// SyntaxError
	}
	foo();

import 命令

1. 通用方式

例如,引入下面的 profile.js 模块

	// profile.js 文件
	export var firstName = 'Michael';
	export var lastName = 'Jackson';

通用方法引入

    import * as m1 from './profile.js';    
    console.log(m1.firstName + ' ' + m1.lastName);      // Michael Jackson

2. 解构赋值形式

对于上面的 profile.js 模块(非默认模块),也可以通过解构赋值的方式来引入。

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

对于默认模块,如下面的 m3.js:

	export default {
	    school: 'CSDN',
	    ad: function () {
	        console.log('VIP买一年送一年,再赠羽绒服,抽万元壕礼!');
	    }
	}

解构赋值引入的方式有所不同,如下:

    import { default as m3 } from './m3.js';
    console.log(m3.school);     // CSDN
    m3.ad();                    // VIP买一年送一年,再赠羽绒服,抽万元壕礼! 

3. 简便形式

这种引入方式只能针对于默认暴露。

    import m3 from './m3.js';

 


感谢您的阅读,如果还想了解 ES6 学习前必备的基础知识,可以阅读下面文章:

JavaScript 面向对象编程(一) —— 面向对象基础

JavaScript 面向对象编程(二) —— 构造函数 / 原型 / 继承 / ES5 新增方法

JavaScript 面向对象编程(三) —— 严格模式 / 高阶函数 / 闭包 / 浅拷贝和深拷贝

JavaScript 面向对象编程(四) —— 正则表达式

;