想要完整了学习 ECMAScipt 6,并且能够理解的比较透彻,最好还是首先了解一下 Node.js 和 AJAX 相关知识。可以参考以下博文:
Node.js「一」—— Node.js 简介 / Node.js 模块 / 包 与 NPM
Node.js「二」—— fs 模块 / async 与 await
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
- ES6 的版本变动内容最多,具有里程碑意义
- ES6 加入许多新的语法特性,编程实现更简单、高效
- 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 = [];
- 用法特点
-
不允许重复声明
-
块级作用域
-
不存在变量提升
-
不影响作用域链
- 案例:点击切换颜色
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 = 0
,var i
没有块级作用域,在全局变量中存在,导致在点击事件未开始时,i 已经自增到 3,因此点击会将 items[3]
属性改变,此标签不存在,所以没有反应。
而此处如果使用 let i = 0
,let 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 声明有以下特点
- 声明必须赋初始值
const A; // 报错
- 标识符一般为大写
- 不允许重复声明
const STAR = '派大星';
const STAR = '海绵宝宝'; // 报错
- 值不允许修改
const STAR = '派大星';
STAR = '海绵宝宝'; // 报错
- 块级作用域
{
const PLAYER = 'UZI';
}
console.log(PLAYER); // 报错
- 对于数组和对象的元素修改,不算做对常量的修改,不会报错
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
- 特点
- 字符串中可以出现换行符。如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
let str = `<ul>
<li>沈腾</li>
<li>魏翔</li>
</ul>`;
- 模板字符串中嵌入变量,需要将变量名写在
${}
之中。大括号{}
内部可以放入任意的 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
注意:
-
箭头函数没有自己的
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
- 不可以当作构造函数,也就是说,不可以对箭头函数使用
new
命令,否则会抛出一个错误。
let Person = (name, age) => {
this.name = name;
this.age = age;
}
let me = new Person('andy', 18);
// ERROR: Person is not a constructor
- 不可以使用
arguments
对象,该对象在函数体内不存在。
let fn = () => {
console.log(arguments);
}
fn(1, 2, 3);
// ERROR: arguments is not defined
- 如果形参有且只有一个,则小括号可以省略。
let add = n => { // 省略 (n) 的小括号
return n + n;
}
console.log(add(1)); // 2
- 函数体如果只有一条语句,则花括号可以省略(此时,
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
。并不是定时器的 this
值 window
。因此可以直接利用 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('派大星', '海绵宝宝', '章鱼哥');
- 用此方式获取到的实参是数组,可以采用数组方法对实参进行处理,如
filter
、some
、every
等
注意: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 语言的第七种数据类型,前六种是:undefined
、null
、Boolean
、String
、Number
、Object
。
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...in
、for...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 的作用
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据结构的成员能够按某种次序排列
- ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费
- 工作原理
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象
- 第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员 - 不断调用指针对象的
next
方法,直到它指向数据结构的结束位置 - 每一次调用
next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束
for...of
和for..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
方法,都会返回一个代表当前成员的信息对象,具有 value
和 done
两个属性。
- 具备此接口的数据类型
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for…of循环遍历。原因在于,这些数据结构原生部署了 Symbol.iterator
属性。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
原生具备 Iterator 接口的数据(可用 for...of
遍历)有:Array
、函数的 arguments 对象
、Set
、Map
、String
、TypedArray
、NodeList
。
「十二」Generator 函数
生成器函数(Generator)是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
- 特点
function
关键字与函数名之间有一个*
,如function* fn(){}
- 函数体内部使用
yield
表达式,定义不同的内部状态
Generator 函数的调用方法与普通函数一样。不同的是,调用 Generator 函数后,该函数并不执行,而是返回一个遍历器对象,里面含有 next
方法。
那么如何使函数内代码执行呢,可以借助调用遍历器对象的 next
方法,使得指针移向下一个状态。也就是说,每次调用 next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield
表达式(或 return
语句)为止。
换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而 next
方法可以恢复执行。
- yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的 next
方法的运行逻辑如下:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
- next 方法的参数
yield
表达式本身没有返回值,或者说总是返回 undefined
。next
方法可以带一个参数,该参数就会被当作上一个 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 有两个特点
-
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。 -
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从
pending
变为fulfilled
和 从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved
(已定型)。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
当然,Promise 也有它的缺点
- 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise 基本用法
ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve
和 reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve
:将 Promise 对象的状态从pending
变为fullfilled
,在异步操作成功时调用,并将异步操作的结果,作为参数value
传递出去。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 集合的属性和方法:
Set.prototype.size
: 返回集合的元素个数Set.prototype.add(value)
: 添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
: 删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
: 返回一个布尔值,表示该值是否为 Set 的成员。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 的属性和方法:
size
: 返回 Map 结构的成员总数。Map.prototype.set(key, value)
: 设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。Map.prototype.get(key)
: 读取key
对应的键值,如果找不到key
,返回undefined
。Map.prototype.has(key)
: 返回一个布尔值,表示某个键是否在当前 Map 对象之中Map.prototype.delete(key)
: 删除某个键,返回true
。如果删除失败,返回false
。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
如果要将 0b
和 0o
前缀的字符串数值转为十进制,要使用 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 对象的方法。这里主要介绍三种:
Object.is()
: 比较两个值是否严格相等,与===
行为基本一致(区别在于 ±0 与 NaN)Object.assign()
: 对象的合并,将源对象的所有可枚举属性,复制到目标对象(如果重名,后面属性值会覆盖前面)Object.setPrototypeOf()
、Object.getPrototypeOf()
: 可以直接设置和获取对象的原型(不建议这么做)
「十八」模块化
模块化是指将一个大的程序文件,拆分成许多个小的文件,然后将小文件组合起来。
模块化有什么好处呢?
- 可以防止命名冲突。
- 提高代码复用。可以将功能代码封装成文件,对外只暴露接口。
- 维护性高。
模块化规范的产品有哪些?
在 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.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 命令的几个注意点
- 通常情况下,
export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错: 没有提供对外的接口
export 1;
// 报错:同上,没有提供对外的接口
var m = 1;
export m;
正确的写法是下面这样:
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// 上面代码暴露变量 foo,值为 bar,500 毫秒之后变成 baz
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 新增方法