文章中很多内容借鉴(copy)了阮神的著作 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 全称 EcmaScript,是脚本语言的规范,而平时经常编写的 JavaScript,是 EcmaScript 的一种实现,所以 ES 新特性其实指的就是 JavaScript 的新特性。
ECMA 是一家国际性会员制度的信息和电信标准组织。1994年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名 Ecma国际(Ecma International)。
ECMAScript 是一种由 Ecma国际 通过 ECMA-262 标准化的脚本程序设计语言。 这种语言在万维网上应用广泛,它往往被称为 JavaScript 或 JScript,所以它可以理解为是 JavaScript 的一个标准,但实际上后两者是 ECMA-262 标准的实现和扩展。
https://kangax.github.io/compat-table/es6/ 可以查看兼容性信息。
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 = `
- 沈腾
- 魏翔
`;
${}
之中。大括号{}
内部可以放入任意的 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
})
ES6 引入 rest
参数,用于获取函数的实参,用来代替 arguments
。
我们先来回忆一下 ES5 获取实参的方式,如下:
function data() {
console.log(arguments);
}
data('派大星', '海绵宝宝', '章鱼哥');
下面来看 ES6 获取实参的方法,如下:
function data(...args) {
console.log(args);
}
data('派大星', '海绵宝宝', '章鱼哥');
注意:rest 参数必须放到参数最后,否则会报错
function fn(a, b, ...args) {
}
扩展运算符 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)
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 属性,指向一个内部方法。当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法。利用这个方法,我们可以实现自己去控制类型检测。
上面代码中,MyClass 是一个类,new MyClass()
会返回一个实例。该实例的 Symbol.hasInstance
方法,会在进行 instanceof 运算时自动调用,判断左侧的运算子是否为 Array 的实例。
对象的 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 接口,就可以完成遍历操作。
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)是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
function
关键字与函数名之间有一个 *
,如 function* fn(){}
yield
表达式,定义不同的内部状态Generator 函数的调用方法与普通函数一样。不同的是,调用 Generator 函数后,该函数并不执行,而是返回一个遍历器对象,里面含有 next
方法。
那么如何使函数内代码执行呢,可以借助调用遍历器对象的 next
方法,使得指针移向下一个状态。也就是说,每次调用 next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield
表达式(或 return
语句)为止。
换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而 next
方法可以恢复执行。
由于 Generator 函数返回的遍历器对象,只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的 next
方法的运行逻辑如下:
yield
表达式,就暂停执行后面的操作,并将紧跟在 yield
后面的那个表达式的值,作为返回的对象的 value
属性值next
方法时,再继续往下执行,直到遇到下一个 yield
表达式yield
表达式,就一直运行到函数结束,直到 return
语句为止,并将 return
语句后面的表达式的值,作为返回的对象的 value
属性值return
语句,则返回的对象的 value
属性值为 undefined
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 是 ES6 引入的异步编程的新解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。
所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 有两个特点
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 pending
变为 fulfilled
和 从 pending
变为 rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved
(已定型)。
如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了 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()
方法(套娃)。
若返回一个非 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 对象
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
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 类构造、继承的相关知识点,此处不再过多解释,参考此文 面向对象基础
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 新增方法
JavaScript 面向对象编程(三) —— 严格模式 / 高阶函数 / 闭包 / 浅拷贝和深拷贝
JavaScript 面向对象编程(四) —— 正则表达式