一、兼容性
目前各大浏览器基本上都能够支持 ES6 的新特性,其中 Chrome 和 Firefox 对 ES6 最为友好,但 IE7~11 基本不支持。
以下是各大浏览器支持情况以及开始时间:
二、let 与 const
let声明变量,const声明常量:两个都有块级作用域,ES5没有块级作用域。
①变量提升(变量可在声明前使用,值为undefined): let,const不行,var可以,并将申明的变量挂载在window上。
②块级作用域: let和const只在块级作用域中有效,存在暂存死区,var不是。
③重复声明: 同一作用域下let和const命令都不允许重复声明同一个变量,var可以但不建议这么做。
④初始值: const一旦声明必须赋初始值,且声明后不许改变,如果声明的是复合类型数据,可以修改其属性,因为const只是指向对象的地址,地址不变,地址指向的内容是可以改变的。
for (var j = 0; j < 10; j++) {
setTimeout(function(){
console.log(j);
})
}
// var: 输出十个10
// 因为所有j都使用同一个引用,而j最终会被更新为10
for (let j = 0; j < 10; j++) {
setTimeout(function(){
console.log(j);
})
}
// 输出: 0123456789
// let不会在栈内存里预分配内存空间,在栈内存分配变量时检查是否存在相同变量名,有就报错,也就是暂时性死区
// 块级作用域中let命令所声明的变量不受外部影响
for (const j = 0; j < 10; j++) {
setTimeout(function(){
console.log(j);
})
}
// 输出 0
// const声明后不许改变,所以为初始赋值0
三、解构赋值
针对数组或对象进行匹配,然后对其中的变量进行赋值。常用于交换变量值,取函数返回值,或设置默认值。
3.1 数组的解构赋值(Array)
// 基本
let [a, b, c] = [1, 2, 3]; // a = 1 b = 2 c = 3
// 可嵌套
let [a, [[b], c]] = [1, [[2], 3]]; // a = 1 b = 2 c = 3
// 可忽略
let [a, , b] = [1, 2, 3]; // a = 1 b = 3
// 不完全解构
let [a = 1, b] = []; // a = 1, b = undefined
// 剩余运算符
let [a, ...b] = [1, 2, 3]; //a = 1 b = [2, 3]
// 字符串等
// 在数组的解构中,解构的目标若为可遍历对象,皆可进行解构赋值。可遍历对象即实现 Iterator 接口的数据。
let [a, b, c, d, e] = 'hello'; // a = 'h' b = 'e' c = 'l' d = 'l' e = 'o'
// 解构默认值
let [a = 2] = [undefined]; // a = 2
// 当解构模式有匹配结果,且匹配结果是 undefined 时,会触发默认值作为返回结果。
let [a = 3, b = a] = []; // a = 3, b = 3
let [a = 3, b = a] = [1]; // a = 1, b = 1
let [a = 3, b = a] = [1, 2]; // a = 1, b = 2
// a 与 b 匹配结果为 undefined ,触发默认值:a = 3; b = a =3
// a 正常解构赋值,匹配结果:a = 1,b 匹配结果 undefined ,触发默认值:b = a = 1
// a 与 b 正常解构赋值,匹配结果:a = 1,b = 2
3.2 对象的解构赋值(Object)
基本
let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; // foo = 'aaa' // bar = 'bbb' let { baz : foo } = { baz : 'ddd' }; // foo = 'ddd'
可嵌套可忽略
let obj = {p: ['hello', {y: 'world'}] }; let {p: [x, { y }] } = obj; // x = 'hello' // y = 'world' let obj = {p: ['hello', {y: 'world'}] }; let {p: [x, { }] } = obj; // x = 'hello'
不完全解构
let obj = {p: [{y: 'world'}] }; let {p: [{ y }, x ] } = obj; // x = undefined // y = 'world'
剩余运算符
let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40}; // a = 10 // b = 20 // rest = {c: 30, d: 40}
解构默认值
let {a = 10, b = 5} = {a: 3}; // a = 3; b = 5; let {a: aa = 10, b: bb = 5} = {a: 3}; // aa = 3; bb = 5;
四、Symbol
ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。
作为属性名:
let sy = Symbol("key1");
// 写法1
let syObject = {};
syObject[sy] = "kk";
console.log(syObject); // {Symbol(key1): "kk"}
// 写法2
let syObject = { [sy]: "kk" };
console.log(syObject); // {Symbol(key1): "kk"}
// 写法3
let syObject = {};
Object.defineProperty(syObject, sy, {value: "kk"});
console.log(syObject); // {Symbol(key1): "kk"}
五、Map 与 Set
应用场景:Set用于数据重组,Map用于数据储存
Set:
(1)成员不能重复
(2)只有键值没有键名,类似数组
(3)可以遍历,方法有add,delete,has
Map:
(1)本质上是健值对的集合,类似集合
(2)可以遍历,可以跟各种数据格式转换
5.1 Map 对象
Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。
Maps 和 Objects 的区别:
- 一个 Object 的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值。
- Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
- Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。
- Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
5.2 Map 中的 key
key 是字符串
var myMap = new Map();
var keyString = "a string";
myMap.set(keyString, "和键'a string'关联的值");
myMap.get(keyString); // "和键'a string'关联的值"
myMap.get("a string"); // "和键'a string'关联的值" 因为 keyString === 'a string'
key 是对象
var myMap = new Map();
var keyObj = {},
myMap.set(keyObj, "和键 keyObj 关联的值");
myMap.get(keyObj); // "和键 keyObj 关联的值"
myMap.get({}); // undefined, 因为 keyObj !== {}
key 是函数
var myMap = new Map();
var keyFunc = function () {}, // 函数
myMap.set(keyFunc, "和键 keyFunc 关联的值");
myMap.get(keyFunc); // "和键 keyFunc 关联的值"
myMap.get(function() {}) // undefined, 因为 keyFunc !== function () {}
key 是 NaN
var myMap = new Map();
myMap.set(NaN, "not a number");
myMap.get(NaN); // "not a number"
var otherNaN = Number("foo");
myMap.get(otherNaN); // "not a number"
虽然 NaN 和任何值甚至和自己都不相等(NaN !== NaN 返回true),NaN作为Map的键来说是没有区别的。
5.3 Map 的迭代
对 Map 进行遍历,以下两个最高级。
5.3.1 for…of
可以遍历数组,Set和Map结构,某些类数组对象,以及字符串。
var myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");
// 将会显示两个 log。 一个是 "0 = zero" 另一个是 "1 = one"
for (var [key, value] of myMap) {
console.log(key + " = " + value);
}
for (var [key, value] of myMap.entries()) {
console.log(key + " = " + value);
}
/* 这个 entries 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组。 */
// 将会显示两个log。 一个是 "0" 另一个是 "1"
for (var key of myMap.keys()) {
console.log(key);
}
/* 这个 keys 方法返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键。 */
// 将会显示两个log。 一个是 "zero" 另一个是 "one"
for (var value of myMap.values()) {
console.log(value);
}
/* 这个 values 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值。*/
5.3.2 forEach()
var myMap = new Map();
myMap.set(0, "zero"); myMap.set(1, "one");
// 将会显示两个 logs。 一个是 "0 = zero" 另一个是 "1 = one"
myMap.forEach(function(value, key) {
console.log(key + " = " + value);
}, myMap)
5.4 Map 对象的操作
Map 与 Array的转换
var kvArray = [["key1", "value1"], ["key2", "value2"]];
// Map 构造函数可以将一个二维键值对数组转换成一个 Map 对象
var myMap = new Map(kvArray);
// 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组
var outArray = Array.from(myMap);
Map 的克隆
var myMap1 = new Map([["key1", "value1"], ["key2", "value2"]]);
var myMap2 = new Map(myMap1); console.log(original === clone);
// 打印 false。 Map 对象构造函数生成实例,迭代出新的对象。
Map 的合并
var first = new Map([[1, 'one'], [2, 'two'], [3, 'three'],]);
var second = new Map([[1, 'uno'], [2, 'dos']]);
// 合并两个 Map 对象时,如果有重复的键值,则后面的会覆盖前面的,对应值即 uno,dos, three
var merged = new Map([...first, ...second]);
5.5 Set 对象
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
5.6 类型转换
Array
// Array 转 Set
var mySet = new Set(["value1", "value2", "value3"]);
// 用...操作符,将 Set 转 Array
var myArray = [...mySet];
String
// String 转 Set
var mySet = new Set('hello'); // Set(4) {"h", "e", "l", "o"}
// 注:Set 中 toString 方法是不能将 Set 转换成 String
5.7 数组去重
var mySet = new Set([1, 2, 3, 4, 4]);
[...mySet]; // [1, 2, 3, 4]
5.8 并集
var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var union = new Set([...a, ...b]); // {1, 2, 3, 4}
5.9 交集
var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var intersect = new Set([...a].filter(x => b.has(x))); // {2, 3}
5.10 差集
var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var difference = new Set([...a].filter(x => !b.has(x))); // {1}
六、Reflect 与 Proxy
Proxy 与 Reflect 是 ES6 为了操作对象引入的 API 。
Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。它不直接操作对象,而是像代理模式,通过对象的代理对象进行操作,在进行这些操作时,可以添加一些需要的额外操作。
Reflect 可以用于获取目标对象的行为,它与 Object 类似,但是更易读,为操作对象提供了一种更优雅的方式。它的方法与 Proxy 是对应的。
6.1 Proxy
一个 Proxy 对象由两个部分组成: target 、 handler 。在通过 Proxy 构造函数生成实例对象时,需要提供这两个参数。 target 即目标对象, handler 是一个对象,声明了代理 target 的指定行为。
let target = {
name: 'Tom',
age: 24
}
let handler = {
get: function(target, key) {
console.log('getting '+key);
return target[key]; // 不是target.key
},
set: function(target, key, value) {
console.log('setting '+key);
target[key] = value;
}
}
let proxy = new Proxy(target, handler)
proxy.name // 实际执行 handler.get
proxy.age = 25 // 实际执行 handler.set
// getting name
// setting age
// 25
// target 可以为空对象
let targetEpt = {}
let proxyEpt = new Proxy(targetEpt, handler)
// 调用 get 方法,此时目标对象为空,没有 name 属性
proxyEpt.name // getting name
// 调用 set 方法,向目标对象中添加了 name 属性
proxyEpt.name = 'Tom'
// setting name
// "Tom"
// 再次调用 get ,此时已经存在 name 属性
proxyEpt.name
// getting name
// "Tom"
// 通过构造函数新建实例时其实是对目标对象进行了浅拷贝,因此目标对象与代理对象会互相
// 影响
targetEpt)
// {name: "Tom"}
// handler 对象也可以为空,相当于不设置拦截操作,直接访问目标对象
let targetEmpty = {}
let proxyEmpty = new Proxy(targetEmpty,{})
proxyEmpty.name = "Tom"
targetEmpty) // {name: "Tom"}
6.2 Reflect
ES6 中将 Object 的一些明显属于语言内部的方法移植到了 Reflect 对象上(当前某些方法会同时存在于 Object 和 Reflect 对象上),未来的新方法会只部署在 Reflect 对象上。
Reflect 对象对某些方法的返回结果进行了修改,使其更合理。
Reflect 对象使用函数的方式实现了 Object 的命令式操作。
静态方法
Reflect.get(target, name, receiver)
查找并返回 target 对象的 name 属性。
let exam = {
name: "Tom",
age: 24,
get info(){
return this.name + this.age;
}
}
Reflect.get(exam, 'name'); // "Tom"
// 当 target 对象中存在 name 属性的 getter 方法, getter 方法的 this 会绑定 // receiver
let receiver = {
name: "Jerry",
age: 20
}
Reflect.get(exam, 'info', receiver); // Jerry20
// 当 name 为不存在于 target 对象的属性时,返回 undefined
Reflect.get(exam, 'birth'); // undefined
// 当 target 不是对象时,会报错
Reflect.get(1, 'name'); // TypeError
七、字符串
ES6 之前判断字符串是否包含子串,用 indexOf 方法,ES6 新增了子串的识别方法。
- **includes()**:返回布尔值,判断是否找到参数字符串。
- **startsWith()**:返回布尔值,判断参数字符串是否在原字符串的头部。
- **endsWith()**:返回布尔值,判断参数字符串是否在原字符串的尾部。
以上三个方法都可以接受两个参数,需要搜索的字符串,和可选的搜索起始位置索引。
let string = "apple,banana,orange";
string.includes("banana"); // true
string.startsWith("apple"); // true
string.endsWith("apple"); // false
string.startsWith("banana",6) // true
注意点:
- 这三个方法只返回布尔值,如果需要知道子串的位置,还是得用 indexOf 和 lastIndexOf 。
- 这三个方法如果传入了正则表达式而不是字符串,会抛出错误。而 indexOf 和 lastIndexOf 这两个方法,它们会将正则表达式转换为字符串并搜索它。
7.1 字符串重复
repeat():返回新的字符串,表示将字符串重复指定次数返回。
console.log("Hello,".repeat(2)); // "Hello,Hello,"
如果参数是小数,向下取整
console.log("Hello,".repeat(3.2)); // "Hello,Hello,Hello,"
如果参数是 0 至 -1 之间的小数,会进行取整运算,0 至 -1 之间的小数取整得到 -0 ,等同于 repeat 零次
console.log("Hello,".repeat(-0.5)); // ""
如果参数是 NaN,等同于 repeat 零次
console.log("Hello,".repeat(NaN)); // ""
如果参数是负数或者 Infinity ,会报错:
console.log("Hello,".repeat(-1));
// RangeError: Invalid count value
console.log("Hello,".repeat(Infinity));
// RangeError: Invalid count value
如果传入的参数是字符串,则会先将字符串转化为数字
console.log("Hello,".repeat("hh")); // ""
console.log("Hello,".repeat("2")); // "Hello,Hello,"
7.2 字符串补全
- padStart:返回新的字符串,表示用参数字符串从头部(左侧)补全原字符串。
- padEnd:返回新的字符串,表示用参数字符串从尾部(右侧)补全原字符串。
以上两个方法接受两个参数,第一个参数是指定生成的字符串的最小长度,第二个参数是用来补全的字符串。如果没有指定第二个参数,默认用空格填充。
console.log("h".padStart(5,"o")); // "ooooh"
console.log("h".padEnd(5,"o")); // "hoooo"
console.log("h".padStart(5)); // " h"
如果指定的长度小于或者等于原字符串的长度,则返回原字符串:
console.log("hello".padStart(5,"A")); // "hello"
如果原字符串加上补全字符串长度大于指定长度,则截去超出位数的补全字符串:
console.log("hello".padEnd(10,",world!")); // "hello,worl"
常用于补全位数:
console.log("123".padStart(10,"0")); // "0000000123"
7.3 模板字符串
模板字符串相当于加强版的字符串,${}
拼接参数,可以在字符串中加入变量和表达式。
基本用法
普通字符串
let string = `Hello'\n'world`;
console.log(string);
// "Hello'
// 'world"
多行字符串:
let string1 = `Hey, can you stop angry now?`;
console.log(string1);
// Hey,
// can you stop angry now?
字符串插入变量和表达式。
变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。
let name = "Mike";
let age = 27; let info = `My Name is ${name},I am ${age+1} years old next year.` console.log(info);
// My Name is Mike,I am 28 years old next year.
字符串中调用函数:
function f(){ return "have fun!"; }
let string2= `Game start,${f()}`;
console.log(string2);
// Game start,have fun!
注意要点
模板字符串中的换行和空格都是会被保留的
应用
过滤 HTML 字符串,防止用户输入恶意内容。
function f(stringArr,...values){
let result = "";
for(let i=0;i<stringArr.length;i++){
result += stringArr[i];
if(values[i]){
result += String(values[i]).replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
}
return result;
}
name = '<Amy&MIke>';
f`<p>Hi, ${name}.I would like send you some message.</p>`;
// <p>Hi, <Amy&MIke>.I would like send you some message.</p>
国际化处理(转化多国语言)
i18n`Hello ${name}, you are visitor number ${visitorNumber}.`;
// 你好**,你是第**位访问者
八、箭头函数
箭头函数:省去function关键字,用 () = > 来定义,函数的参数放在箭头前的括号,函数体在箭头后的花括号。
它的 this 继承了外层执行环境的 this,不能被 call() apply() bind() 改变指向。
没有自己的arguments,用rest参数代替arguments对象,来访问箭头函数的参数列表。
没有原型prototype,打印显示undefined。
不能用作Generator函数,所以不能使用yeild关键字。
不能用作构造函数,也就是说不能使用new命令,否则报错
适合使用的场景:
ES6 之前,JavaScript 的 this 对象一直很令人头大,回调函数,经常看到 var self = this 这样的代码,为了将外部 this 传递到回调函数中,当我们需要维护一个 this 上下文的时候,就可以使用箭头函数。
不适合使用的场景:
定义函数的方法,且该方法中包含 this 或需要动态 this 的时候。
九、… 拓展运算符
将数组或对象里面的值展开,或将多个值收集为一个变量,代替argument对象,对象未嵌套可用来浅拷贝。
十、Iterator 迭代器
Iterator 是 ES6 引入的一种新的遍历机制,迭代器有两个核心概念:
- 迭代器是一个统一的接口,它的作用是使各种数据结构可被便捷的访问,它是通过一个键为Symbol.iterator 的方法来实现。
- 迭代器是用于遍历数据结构元素的指针(如数据库中的游标)。
迭代的过程如下:
- 通过 Symbol.iterator 创建一个迭代器,指向当前数据结构的起始位置
- 随后通过 next 方法进行向下迭代指向下一个位置, next 方法会返回当前位置的对象,对象包含了 value 和 done 两个属性, value 是当前属性的值, done 用于判断是否遍历结束
- 当 done 为 true 时则遍历结束
```javascript
const items = [“zero”, “one”, “two”];
const it = itemsSymbol.iterator;
it.next();
{value: “zero”, done: false}
it.next();
{value: “one”, done: false}
it.next();
{value: “two”, done: false}
it.next();
{value: undefined, done: true}
上面的例子,首先创建一个数组,然后通过 Symbol.iterator 方法创建一个迭代器,之后不断的调用 next 方法对数组内部项进行访问,当属性 done 为 true 时访问结束。
迭代器是协议(使用它们的规则)的一部分,用于迭代。该协议的一个关键特性就是它是顺序的:迭代器一次返回一个值。这意味着如果可迭代数据结构是非线性的(例如树),迭代将会使其线性化。
**可迭代的数据结构:**
以下是可迭代的值:
- Array
- String
- Map
- Set
- Dom元素(正在进行中)
## 十一、Class 类
在ES6中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类。class 的本质是 function。它可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。定义类更简便地实现类的继承。
**类定义**
类表达式可以为匿名或命名。
// 匿名类
let Example = class {
constructor(a) {
this.a = a;
}
}
// 命名类
let Example = class Example {
constructor(a) {
this.a = a;
}
}
**类声明**
class Example {
constructor(a) {
this.a = a;
}
}
**注意要点:**
不可重复声明。
方法间不能加分号。
类中方法不需要 function 关键字。
类定义不会被提升,这意味着,必须在访问前对类进行定义,否则就会报错。
修饰器@: decorator是一个函数,用来修改类甚至于是方法的行为,修饰器本质就是编译时执行的函数。
**属性**
prototype,ES6 中,prototype 仍旧存在,虽然可以直接自类中定义方法,但是其实方法还是定义在 prototype 上的。 覆盖方法 / 初始化时添加方法。
Example.prototype={
//methods
}
添加方法
Object.assign(Example.prototype,{
//methods
})
静态属性:class 本身的属性,即直接定义在类内部的属性( Class.propname ),不需要实例化。 ES6 中规定,Class 内部只有静态方法,没有静态属性。
class Example {
// 新提案
static a = 2;
}
// 目前可行写法
Example.b = 2;
公共属性
class Example{}
Example.prototype.a = 2;
实例属性:定义在实例对象( this )上的属性。
class Example {
a = 2;
constructor () {
console.log(this.a);
}
}
name 属性,返回跟在 class 后的类名(存在时)。
let Example=class Exam {
constructor(a) {
this.a = a;
}
}
console.log(Example.name); // Exam
let Example=class {
constructor(a) {
this.a = a;
}
}
console.log(Example.name); // Example
constructor 方法是类的默认方法,创建类的实例化对象时被调用。
class Example{
constructor(){
console.log(‘我是constructor’);
}
}
new Example(); // 我是constructor
返回对象
class Test {
constructor(){
// 默认返回实例对象 this
}
}
console.log(new Test() instanceof Test); // true
class Example {
constructor(){
// 指定返回对象
return new Test();
}
}
console.log(new Example() instanceof Example); // false
静态方法
class Example{
static sum(a, b) {
console.log(a+b);
}
}
Example.sum(1, 2); // 3
原型方法
class Example {
sum(a, b) {
console.log(a + b);
}
}
let exam = new Example();
exam.sum(1, 2); // 3
实例方法
class Example {
constructor() {
this.sum = (a, b) => {
console.log(a + b);
}
}
}
## 十二、模块
ES6 模块自动开启严格模式,即使没加 `use strict;`。
模块中可以导入和导出各类型变量,如函数,对象,字符串,数字,布尔值,类等。
每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。
每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。
模块导入导出各种类型的变量,如字符串,数值,函数,类。
- 导出的函数声明与类声明必须要有名称(export default 命令另外考虑)。
- 不仅能导出声明还能导出引用(例如函数)。
- export 命令可以出现在模块的任何位置,但必需处于模块顶层。
- import 命令会提升到整个模块的头部,首先执行。
/—–export [test.js]—–/
let myName = “Tom”;
let myAge = 20;
let myfn = function(){
return “My name is” + myName + “! I’m ‘“ + myAge + “years old.”
}
let myClass = class myClass {
static a = “yeah!”;
}
export { myName, myAge, myfn, myClass }
/—–import [xxx.js]—–/
import { myName, myAge, myfn, myClass } from “./test.js”;
console.log(myfn());// My name is Tom! I’m 20 years old.
console.log(myAge);// 20
console.log(myName);// Tom
console.log(myClass.a );// yeah!
建议使用大括号指定所要输出的一组变量写在文档尾部,明确导出的接口。
函数与类都需要有对应的名称,导出文档尾部也避免了无对应名称。
不同模块导出接口名称命名重复, 使用 as 重新定义变量名。
**export default 命令:**
- 在一个文件或模块中,export、import 可以有多个,export default 仅有一个。
- export default 中的 default 是对应的导出接口变量。
- 通过 export 方式导出,在导入时要加{ },export default 则不需要。
- export default 向外暴露的成员,可以使用任意变量来接收。
## 十三、Promise 对象
是异步编程的一种解决方案。比传统的解决方案(回调函数和事件)更强大合理。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
Promise 异步操作有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。除了异步操作的结果,任何其他操作都无法改变这个状态。
Promise 对象只有:从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ,状态就不会再变了即 resolved(已定型)。
**状态的缺点**
无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。
如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
**then 方法**
then 方法接收两个函数作为参数,第一个参数是 Promise 执行成功时的回调,第二个参数是 Promise 执行失败时的回调,两个函数只会有一个被调用。
**then 方法的特点**
在 JavaScript 事件队列的当前运行完成之前,回调函数永远不会被调用。
then 方法将返回一个 resolved 或 rejected 状态的 Promise 对象用于链式调用,且 Promise 对象的值就是这个返回值。
简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise。注意总是返回或终止 Promise 链。
const p1 = new Promise(function(resolve,reject){
resolve(1);
}).then(function(result) {
p2(result).then(newResult => p3(newResult));
}).then(() => p4());
创建新 Promise 但忘记返回它时,对应链条被打破,导致 p4 会与 p2 和 p3 同时进行。
大多数浏览器中不能终止的 Promise 链里的 rejection,建议后面都跟上 `catch(error => console.log(error));`
reject是用来抛出异常,catch是用来处理异常;
reject是Promise的方法,而catch是Promise实例的方法;
reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch;
网络异常(比如断网),会直接进入catch而不会进入then的第二个回调。
**手写 promise:**
> promise 的三种状态:pending、fulfilled、rejected。
```javascript
class Promise {
constructor(executor) {
// Promise接一个方法 (resolve,reject)=>{}
// 开始的状态
this.state = 'pending'
// 成功的状态
this.value = undefined
// 失败的状态
this.result = undefined
// 状态执行的时候,要立即改变,且不可逆转
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.result = reason
}
}
executor(resolve, reject) //这个方法要立即执行
}
// then接受两个参数,都是方法
then(onFulfilled, onRejected) {
// 如果这样写是同步的,this.state一直是padding的状态
console.log(this.state)
// 如果成功,传入成功的参数
if (this.state === 'fulfilled') {
onFulfilled(this.value)
}
// 如果失败,传入失败的参数
if (this.state === 'rejected') {
onRejected(this.result)
}
}
}
上面是同步的,执行 then 方法后,this.state 一直是 pending 的状态。
发布订阅者模式。
module.exports = class Promise {
constructor(executor) {
// Promise接一个方法 (resolve,reject)=>{}
// 开始的状态
this.state = 'pending'
// 成功的状态
this.value = undefined
// 失败的状态
this.result = undefined
// 成功的订阅者数组
this.onResolvedCallbacks = []
// 失败的订阅者数组
this.onRejectedCallbacks = []
// 状态执行的时候,要立即改变,且不可逆转
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
// 执行resolve的时候,把 this.onResolvedCallbacks里面的方法全部执行一遍
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.result = reason
// 执行reject的时候,把 this.onRejectedCallbacks里面的方法全部执行一遍
this.onRejectedCallbacks.forEach(fn => fn())
}
}
executor(resolve, reject) //这个方法要立即执行
}
// then接受两个参数,都是方法
then(onFulfilled, onRejected) {
// 如果这样写是同步的,this.state一直是padding的状态
// 如果成功,传入成功的参数
if (this.state === 'fulfilled') {
onFulfilled(this.value)
}
// 如果失败,传入失败的参数
if (this.state === 'rejected') {
onRejected(this.result)
}
// 发布订阅者模式
// 如果状态是 pending
// 就把要做的方法传入 待发布状态中去
if (this.state === 'padding') {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.result)
})
}
}
}
但是上面没有链式调用。then() 方法后面不能接 then()。
解决 then() 方法不返回 promise 的问题。
class Promise {
constructor(executor) {
// Promise接一个方法 (resolve,reject)=>{}
// 开始的状态
this.state = 'pending'
// 成功的状态
this.value = undefined
// 失败的状态
this.result = undefined
// 成功的订阅者数组
this.onResolvedCallbacks = []
// 失败的订阅者数组
this.onRejectedCallbacks = []
// 状态执行的时候,要立即改变,且不可逆转
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
// 执行resolve的时候,把 this.onResolvedCallbacks里面的方法全部执行一遍
this.onResolvedCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.result = reason
// 执行reject的时候,把 this.onRejectedCallbacks里面的方法全部执行一遍
this.onRejectedCallbacks.forEach(fn => fn())
}
}
executor(resolve, reject) //这个方法要立即执行
}
// then接受两个参数,都是方法
then(onFulfilled, onRejected) {
// 如果这样写是同步的,this.state一直是padding的状态
// 如果成功,传入成功的参数
const promise2 = new Promise((resolve, reject) => {
// 解决不能链式调用的问题。需要再返回一个promise
if (this.state === 'fulfilled') {
setTimeout(() => {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
}, 0)
}
// 如果失败,传入失败的参数
if (this.state === 'rejected') {
setTimeout(() => {
const x = onRejected(this.result)
resolvePromise(promise2, x, resolve, reject)
}, 0)
}
// 发布订阅者模式
// 如果状态是 padding
// 就把要做的方法传入 待发布状态中去
if (this.state === 'padding') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
const x = onRejected(this.result)
resolvePromise(promise2, x, resolve, reject)
}, 0)
})
}
})
return promise2
}
}
// 定义resolvePromise 方法
const resolvePromise = (promise2, x, resolve, reject) => {
// console.log(promise2, x, resolve, reject)
if (promise2 === x) {
console.log(11111)
return reject(new TypeError('循环引用!'))
}
// 判断 x 的类型
if (typeof x === 'function' || (typeof x === 'object' && x !== null)) {
// 复杂类型
try {
const then = x.then
if (typeof then === 'function') {
// 认为这个确实是一个promise
then.call(x, y => {
// 只能解决一次,如果resolve里面嵌套n个promise
// resolve(y)
// 递归解析当前的x
resolvePromise(promise2, y, resolve, reject)
}, r => {
reject(r)
})
}
} catch (error) {
reject(error)
}
} else {
// 基本类型
resolve(x)
}
}
十四、Generator 函数
ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。 基本用法:
Generator 有两个区分于普通函数的部分:
- 一是在 function 后面,函数名之前有个 * 。
- 函数内部有 yield 表达式。
其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。
十五、async 函数
async 是 ES7 才有的与异步操作有关的关键字,和 Promise , Generator 有很大关联。通过编写类似同步的代码来处理异步流程,提高代码的简洁性和可读性。
async function name([param[, param[, ... param]]]) { statements }
- name: 函数名称。
- param: 要传递给函数的参数的名称。
- statements: 函数体语句。
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。
async function helloAsync(){
return "helloAsync";
}
console.log(helloAsync()) // Promise {<resolved>: "helloAsync"}
helloAsync().then(v=>{
console.log(v); // helloAsync
})
async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。
await 关键字仅在 async function 中有效。如果在 async function 函数体外使用 await ,你只会得到一个语法错误。
function testAwait(){
return new Promise((resolve) => {
setTimeout(function(){
console.log("testAwait");
resolve();
}, 1000);
});
}
async function helloAsync(){
await testAwait();
console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync
await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。
[return_value] = await expression;
expression: 一个 Promise 对象或者任何要等待的值。
返回 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回该值本身。
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。
function testAwait (x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function helloAsync() {
var x = await testAwait ("hello world");
console.log(x);
}
helloAsync ();
// hello world
正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。
function testAwait(){
console.log("testAwait");
}
async function helloAsync(){
await testAwait();
console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync
await针对所跟不同表达式的处理方式:
Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值。
非 Promise 对象:直接返回对应的值。
async较Generator的优势:
(1)内置执行器: Generator函数的执行必须依靠执行器,而 Aysnc函数自带执行器,调用方式跟普通函数的调用一样
(2)更好的语义: async和await相较于*和yield更加语义化
(3)更广的适用性: yield命令后面只能是Thunk函数或Promise对象,async函数的await后面可以是Promise也可以是原始类型的值
(4)返回值是 Promise: async函数返回的是Promise对象,比Generator函数返回的Iterator对象方便,可以直接使用then()方法进行调用