想要完整了学习 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 介绍
  • 「二」let 和 const 关键字
  • 「三」变量的解构赋值
  • 「四」模板字符串
  • 「五」简化对象写法
  • 「六」箭头函数
  • 「七」参数默认值
  • 「八」rest 参数
  • 「九」spread 扩展运算符
  • 「十」Symbol
  • 「十一」迭代器
  • 「十二」Generator 函数
  • 「十三」Promise
  • 「十四」Set 与 Map
  • 「十五」class 类
  • 「十六」数值的扩展
  • 「十七」对象的扩展
  • 「十八」模块化

「一」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);     // stargetName1.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.comconsole.log(username)   // rootconsole.log(password)   // rootconsole.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 codeif (/* 异步操作成功 */){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);         // falseconsole.log(equal(0.1 + 0.2, 0.3));     // true
  • 二进制和八进制

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

 0b111110111 === 503  // true0o767 === 503             // true

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

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

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

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

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

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

 123_00 === 12_300            // true12345_00 === 123_4500         // true12345_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 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。

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.jsexport default {school: 'CSDN',ad: function () {console.log('VIP买一年送一年,再赠羽绒服,抽万元壕礼!');}}

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

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

 import * as m3 from './m3.js';console.log(m3.default.school);     // CSDNm3.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);     // CSDNm3.ad();                    // VIP买一年送一年,再赠羽绒服,抽万元壕礼!

3. 简便形式

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

    import m3 from './m3.js';

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

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

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

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

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

EcmaScript 6 新特性相关推荐

  1. ECMAScript 6新特性简介

    文章目录 简介 ECMAScript和JavaScript的关系 let和const 解构赋值 数组的扩展 函数的扩展 简介 ECMAScript 6.0(以下简称 ES6)是 JavaScript ...

  2. 即将到来的 ECMAScript 2022 新特性

    大家好,我是若川.持续组织了5个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列& ...

  3. ECMAScript 6-11新特性(笔记)

    目  录 一.ES6 新特性 1.1 let 关键字 1.2 const 关键字 1.3 变量的解构赋值 1.4 模板字符串 1.5 简化对象写法 1.6 箭头函数 1.7 rest 参数 1.8 s ...

  4. ECMAScript 6新特性介绍

    箭头函数 箭头函数使用=>语法来简化函数,在语句结构上和C#.Java 8 和 CoffeeScript类似,支持表达式和函数体..=>`操作符左边为输入的参数,而右边则是进行的操作以及返 ...

  5. JavaScript高级之ECMAScript 6 新特性

    2.1. let关键字 let关键字用来声明变量,使用 let声明的变量有几个特点: 不允许重复声明 块儿级作用域 不存在变量提升 不影响作用域链 应用场景:以后声明变量使用let就对了 案例:点击切 ...

  6. ECMAScript 2021(ES12)新特性简介

    简介 ES12是ECMA协会在2021年6月发行的一个版本,因为是ECMAScript的第十二个版本,所以也称为ES12. ES12发行到现在已经有一个月了,那么ES12有些什么新特性和不一样的地方呢 ...

  7. ECMAScript 2019(ES10)新特性简介

    简介 ES10是ECMA协会在2019年6月发行的一个版本,因为是ECMAScript的第十个版本,所以也称为ES10. 今天我们讲解一下ES10的新特性. ES10引入了2大特性和4个小的特性,我们 ...

  8. ECMAScript 2016(ES7)新特性简介

    简介 自从ES6(ECMAScript 2015)在2015年发布以来,ECMAScript以每年一个版本的速度持续向前发展.到现在已经是ECMAScript 2020了. 每个版本都有一些新的特性, ...

  9. ES6、ES7、ES8、ES9、ES10 新特性ECMAScript版本简介

    ES全称ECMAScript,ECMAScript是ECMA制定的标准化脚本语言 ES6新特性(2015) ES6的特性比较多,在 ES5 发布近 6 年(2009-11 至 2015-6)之后才将其 ...

最新文章

  1. P3119 [USACO15JAN]草鉴定Grass Cownoisseur
  2. Chrome谷歌浏览器新功能 删除主题更方便
  3. 复合文档(Compound Document)读写栗子
  4. [Robot Framework] SikuliLibrary的关键字执行依赖java进程,但是上次的java进程如果没有杀掉,robot framework控制台的日志出不来,怎么办?...
  5. 未来5年中国企业信息化格局
  6. IE8的模式修改优化Windows7
  7. C/C++图书管理系统
  8. 非极大值抑制算法详解
  9. 《动手学深度学习》网页版
  10. 2021年下半年软件设计师下午真题试题(案例分析)及答案
  11. Android之制作Nine-Patch图片
  12. 铲雪车 骑马修栅栏 (欧拉路径和欧拉回路)
  13. 计算机考完试后感想,期中考试后的感想(精选10篇)
  14. Eclipse中如何调出Servers,这里教你一遍成功。
  15. 跌宕起伏的区块链行业2022年如何发展?10大行业趋势
  16. 分享工作上的一些体会
  17. 做母婴微商怎么线上引流?做母婴产品如何线上引流?
  18. IP SAN 实验(小白教程,超级具体)
  19. 深圳及周边适合小朋友(3岁以下)玩的地方总结及交流[转载]
  20. 土壤湿度检测仪c语言代码,单片机测土壤湿度可自动浇水并报警 带C#上位机源码...

热门文章

  1. C-------------使用scanf输入字符串的故事;
  2. android字体安装失败,字体管家安装字体失败插件
  3. 动易BizIdea和SpaceBuilder实现单点登录
  4. DX2355装xp的驱动
  5. Android直播实现(一)Android端推流、播放
  6. 操作系统-先进先出置换算法
  7. 数据分析04-朴素贝叶斯
  8. Unity3d办公场景灯光布设与光影烘焙及后处理【2020】
  9. 漏洞复现-OpenSSL
  10. Hive---外部表和内部表