# 🌵 ES6
# 001: var、 let 及 const 区别?
(1) var 在全局作用域下声明的变量会挂载在 window 对象上,let 和 const 则不会。
const constValue = 1;
let letValue = 2;
var varValue = 3;
console.log(window.constValue, window.letValue, window.varValue);
// undefined undefined 3
(2)var 声明的变量会被提升到作用域的顶部,而 let 和 const 则不会。
var 变量提升,在定义前使用会是undefined;let const不会变量提升,定义前使用会报 ReferenceError。
因为存在暂时性死区(Temporal Dead Zone,简写为 TDZ)和块级作用域,所以不能在声明前就使用 let 和 const 使用声明的变量,let 和 const 只在声明所在块级作用域内有效。
暂时性死区:只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码运行之后,才可以获取和使用该变量。
块级作用域:es5 只有全局/函数局部作用域,没有块级作用域会导致内层变量覆盖外层变量(变量冲突),循环记数的变量成了全局变量。let/const 存在块级作用域,不会相互覆盖/重复(代码块分离、外层读取不到内层)。
暂时性死区和块级作用域的意义在于:为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。
const condition = false;
if (condition) {
var foo = "foo";
const baz = "baz";
let bom = "bom";
}
console.log(foo); // undefined
console.log(bar); // Uncaught ReferenceError: bar is not defined
console.log(bom); // Uncaught ReferenceError: bom is not defined
(3)var 声明的变量名字可重复,类型和值可修改;let 和 const 不能重复声明;
let 声明的变量值和类型都可以改变;
const 在声明的时候必须赋予初始值,一旦声明值就不能改变(值是原始数据类型的不可改变,值是引用类型绑定的地址,地址不可变,但属性值能改变)。
var value = 1;
let value = 2; // Uncaught SyntaxError: Identifier 'value' has already been declared
const value = 3; // Uncaught SyntaxError: Identifier 'value' has already been declared
const obj; // Uncaught SyntaxError: Missing initializer in const declaration
const obj = {name: 'obj'};
const obj = {age: 20}; //Uncaught SyntaxError: Identifier 'obj' has already been declared
obj.name = 'chieminchan';
console.log(obj); // {name: "chieminchan"}
⚠️ 循环中的块级作用域问题:
一个老生常谈的 for 循环打印 i 的问题:
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, 0); }
ES6 中的 let 提供了解决这个问题的方法,let 不提升不能重复声明,也不能绑定全局作用域等等特性,可是为什么在循环中可以利用 let 多次进行对 i 声明,并且这里就能正确打印出 i 值呢?
console.log("-----iterator with let-------"); for (let i = 0; i < 3; i++) { let i = "abc"; console.log(i); } // -----iterator with let------- // abc // abc // abc console.log("-----iterator with var-------"); for (var i = 0; i < 3; i++) { var i = "abc"; console.log(i); } // -----iterator with var------- // abc
输出结果不一样的主要原因是因为,在 for 循环中使用 let 和 var,底层会使用不同的处理方式。
当在
for (let i = 0; i < 3; i++)
中,即圆括号之内建立一个隐藏的作用域,然后在每次迭代循环时都创建一个新变量,并且会以之前迭代中同名变量的值将其初始化。这样子,老生常谈使用 let 打印 i 的循环过程便可以理解为:// 伪代码 (let i = 0) { setTimeout(function timer() { console.log(i); }, 0); } (let i = 1) { setTimeout(function timer() { console.log(i); }, 0); } (let i = 2) { setTimeout(function timer() { console.log(i); }, 0); };
若利用 const 代替 let 来进行 for 遍历却会产生这样子的情况:
for (const i = 0; i < 3; i++) {
setTimeout(function timer() {
console.log(i);
}, 0);
}
// Uncaught TypeError: Assignment to constant variable.
结果会报错,,因为虽然我们每次都创建了一个新的变量,但在圆括号之内建立的隐藏的作用域中尝试修改了 const 的值,所以最终会报错。
# 002: 箭头函数和普通函数的区别?
- 箭头函数是匿名函数,不能作为构造函数,不能使用 new 去调用,箭头函数没有原型属性;
- 箭头函数不绑定 arguments,取而代之可以用扩展运算符...解决
- 箭头函数没有 this,函数里的 this 会根据词法作用域决定,继承外层函数调用的 this 绑定,不能通过 bind、call、apply 显式改变 this 指向。
# 003: Set 和 WeakSet、Map 和 WeakMap 区别?
Set 和 Map 主要的应用场景在于 数据重组 和 数据储存。 Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构。
Set:
类似于数组,但成员是唯一且无序的,没有重复的值。
Set 内部判断两个值是否不同,使用的算法叫做Same-value-zero equality
,它类似于精确相等运算符(===),主要的区别是NaN 等于自身,而精确相等运算符认为 NaN 不等于自身。
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set; // Set {NaN}
let set1 = new Set();
set1.add(5);
set1.add("5");
console.log([...set1]); // [5, "5"]
属性:size、constructor
方法:add、delete、has、
Map:类似于对象,但键类型不限制于字符串,可多种类型。
# Map 和 Object 的对比
JS 项目中究竟应该使用 Object 还是 Map? (opens new window)
# 004: Class 和普通构造函数的区别是什么?
提升:
- Class 不存在变量提升,内部采用严格模式,必须先定义后使用
- 普通构造函数存在变量提升,可在定义前调用
调用方式:
- Class 构造实例必须通过 new 调用执行
- 普通构造函数不用 new 也可以执行
方法差异:
方法定义:
- Class 中的方法直接定义在类的定义块内,更简洁(会自动添加到 prototype 上)
- 普通构造函数需手动通过 prototype 属性添加方法
原型方法可枚举性:
- Class 原型链上的属性和方法不可枚举
- 普通构造函数原型链上的属性和方法可以枚举
function Foo(color) { this.color = color; } Foo.prototype.like = function() { console.log(`like${this.color}`); }; console.log(Object.keys(Foo.prototype)) // [like] //es6 class Foo { color: '', constructor(color) { this.color = color; } like() { console.log(`like ${this.color}`); } } console.log(Object.keys(Foo.prototype)) // []
继承实现差异: Class 结构简洁清晰,普通构造函数继承繁琐易错
// 普通构造函数继承(繁琐易错): // 通过构造函数和原型链实现,需要手动设置原型(如Child.prototype = Object.create(Parent.prototype)) // 和修复构造函数(Child.prototype.constructor = Child),以及调用父构造函数(Parent.call(this, ...)) function Father (firstName, age) {} function Son (firstName,age) { Father.call(this,firstName); this.age = age; } Son.prototype = Object.create(Father.prototype); Son.prototype.constructor = Son; Son.prototype.talk = function() {} // Class 继承(简洁清晰): // 使用`extends`关键字和`super`方法,语法更清晰。 class Son extends Father { constructor(firstName, age) { super(firstName); this.title = age; } talk(){ } }
特殊方法支持:
Class 支持静态方法(static)、私有字段、getter/setter等特性
静态属性方法:
class MathUtils { static sumNum = 0; static sum(a, b) { return a + b; } } MathUtils.sum(1,1) // ✅ 合法 const mathUtils = new MathUtils() mathUtils.sum(); // ❌ TypeError: mathUtils.sum is not a function class Utils extends MathUtils {} console.log(Utils.sum()); // 自动继承静态方法 console.log(MathUtils.sumNum); // 自动继承静态属性 //es5 function MathUtils() {} MathUtils.sum = (a, b) => a + b;
可以直接通过
类本身.static属性/方法
访问静态属性和方法,静态属性方法可以继承,但类的实例不可以直接访问。私有属性方法(ES2022+)
class Person { #age = 0; // 私有字段 birthday() { this.#age++; console.log(`Age: ${this.#age}`); } static getSecret() { return this.#age; // ❌ SyntaxError: Private field '#age' must be declared in an enclosing class } static getSecretFromInstance(instance) { return instance.#age; // ✅ 合法,只要在类作用域内 } } const p = new Person(); p.birthday(); // Age: 1 p.#age; // ❌ 语法错误:Private field must be declared in an enclosing class console.log(Person.getSecretFromInstance(p)); // ✅ 输出 1
#secret 是实例私有字段,必须通过某个实例访问。
super/extends 简化继承
class Dog extends Animal { speak() { super.speak(); // 直接调用父类方法 console.log('And then barks!'); } } // ES5 等价实现 Dog.prototype.speak = function() { Animal.prototype.speak.call(this); // 繁琐的调用方式 console.log('And then barks!'); };
普通构造函数需要通过其他方式模拟这些特性
# 005: Class 中的 this 指向
new 调用后的返回,会根据 constructor 构造器里面返回来决定(没返回则返回一个正常实例对象。有则按 constructor 返回的来)。
Class 中的 this 指向:
在构造函数中:this 指向新创建的实例对象
class Animal { constructor(name) { this.name = name; // this = 新实例 console.log(this) // Animal {name: 'dog'} } } const animal = new Animal('dog');
在普通方法中:this 默认指向调用该方法的实例对象
class Animal { name = ''; sparkTime = 0; constructor(name) { this.name = name; } spark() { this.sparkTime++; // this = 调用此方法的实例 console.log(this.name, ',sparkTime:', this.sparkTime) } } const dog = new Animal('dog'); dog.spark(); // dog,sparkTime: 1 const cat = new Animal('cat'); cat.spark(); // cat,sparkTime: 1 const bird = new Animal('bird'); bird.spark(); // bird,sparkTime: 1
在静态方法中:this 指的是类(构造函数本身)(如果在一个方法前,加上 static 关键字,就表示该方法可被子类继承,不可被实例继承,也可以直接通过类来调用,这称为“静态方法”。静态方法也是可以从 super 对象上调用的,静态属性类同)
class MathUtils { static sum(a, b) { console.log(this === MathUtils); // true return a + b; } } MathUtils.sum(2, 3);
如果被单独拎出来,方法中的 this 会指向该方法运行时所在的环境。(想要 this 指向实例,可以在 constructor 中使用 this,或者使用箭头函数,或者 proxy)
class DataFetcher { data = null; // 将类方法作为回调传递,丢失this fetchData() { fetch('/api').then(function(response) { this.data = response; // ❌ this 丢失 }); } } // 方案1: 箭头函数 // 箭头函数类字段:定义时绑定到实例 fetchData() { fetch('/api').then(response => { this.data = response; // ✅ }); } // 方案2: 绑定this fetchData() { fetch('/api').then(function(response) { this.data = response; }.bind(this)); }
最佳实践:
- 静态方法中访问类属性或方法时,使用
this
指向类本身。 - 如果希望方法中的
this
始终绑定实例,使用类字段箭头函数(箭头函数this 指向定义时所在上下文)。 - 如果不需要绑定,使用普通方法,但在作为回调传递时注意使用箭头函数包裹或绑定。
- 静态方法中访问类属性或方法时,使用
# 006: Class 中的 super 用法
super 关键字,一般表示父类的构造函数,用来新建父类的 this 对象。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。因为子类自己的 this 对象必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。
super 只能访问父类原型上的方法和静态方法,不能直接访问实例属性。
在构造函数中调用
super()
作为函数调用时,super()只能用在子类的构造函数之中,相当于 父类.prototype.constructor.call(this),this 指向各个子类环境。// constructor 和 super 都默认被创建 class Point { constructor(...args) { // new.target.name指向当前执行环境 console.log(new.target.name, ...args); } } // 只有使用 extends 才能使用 super,否则会报错。 class ColorPoint extends Point { constructor(...args) { // Point.prototype.constructor.call(this, ...args) super(...args); // 必须写在 this 之前,否则会抛出 ReferenceError。 // this. props = ...args } } new Point(); // Point new ColorPoint(12, 13); // ColorPoint, 12, 13
在普通方法中调用
super.method()
(访问父类方法) 指向父类的原型对象(super.p() = 父类.prototype.p()),可在子类方法中覆盖父类方法时使用。 在子类普通方法中通过 super 调用父类的方法时,父类中方法内部的 this 指向当前的子类实例。class A { constructor() { this.x = "a"; } // A类原型上的方法中的this会指向子类的实例 print() { console.log("print method is A:",this.x); } } class B extends A { constructor() { super(); this.x = "b"; } m() { // super.print() 调用的是父类原型对象上的方法,A.prototype.print() // A.prototype.print()内部的this指向子类B的实例 // 通过 super 调用父类方法时,this 仍指向当前子类实例 super.print(); } } let b = new B(); b.m(); // "print method is A: b"
super 当作对象访问属性,访问父类实例属性/getter(少见但合法)
class Parent { get info() { return 'parent info'; } } class Child extends Parent { get info() { return super.info + ' + child info'; } } console.log(new Child().info); // 输出 parent info + child info
在静态方法中调用
super.method()
(访问父类静态方法) 只能在子类静态方法中使用,super 指向父类本身,而不是实例。class Parent { static myMethod(msg) { console.log("static", msg); } myMethod(msg) { console.log("instance", msg); } } class Child extends Parent { constructor() { super(); super.myMethod(msg); // Parent.prototype.myMethod(msg); } static myMethod(msg) { super.myMethod(msg); // Parent.myMethod(msg); } myMethod(msg) { super.myMethod(msg); // Parent.prototype.myMethod(msg); } } // 调用子类中的静态方法,即调用了父类中的静态方法 Child.myMethod(1); // static 1 // 普通方法被继承,调用了子类原型上的普通方法,从而调用到了父类原型对象上的方法 var child = new Child(); // instance 2 child.myMethod(2); // instance 2
class Parent { value = 42; } class Child extends Parent { getParentValue() { // super只能访问父类原型上的方法和静态方法,不能直接访问实例属性 return super.value; // ❌ 返回 undefined } } class Child extends Parent { getParentValue() { return this.value; // ✅ 通过实例访问 } }
class A { constructor() { // 实例属性,不影响静态属性 this.x = 1; } // 在于静态方法中 this 的指向和静态属性的作用域 // 3. 父类的静态方法print中的this指向这个类,在B中调用因此指向B类 static print() { // 关键点:静态方法中的 this 指向当前类 console.log(this.x); } } class B extends A { constructor() { super(); // 实例属性,不影响静态属性 this.x = 2; } // 2. 直接访问B中的静态方法,此时super指向父类,即调用了父类中的静态方法print static m() { // super 只指定方法所在的父类,但方法内部的 this 仍由调用者决定;实际执行:A.print.call(B) super.print(); } } // 1. 直接修改B对象上的x值为3 // 等价于 B 类上添加静态属性:class B { static x = 3 } B.x = 3; B.m(); // 3 // 如果去掉 B.x=3 会输出什么? // 会输出 undefined,因为A和B都没有静态属性,A只有实例属性
# 007: ES6 的 Class 继承和 ES5 的继承区别
区别 1,this 创建顺序不一样:
ES5 中,子类先创建实例对象 this(new操作),然后在子类构造函数中,将父类的实例属性方法添加到子类实例对象 this 上面(Parent.apply(this))
ES6 中,先创建父类的实例对象 this(即父类的构造函数内部的this指向子类的实例,所以必须先调用 super 方法),再用子类的构造函数修饰 this。
// es5 function Child(name) { Parent.call(this); // 子类 this 上添加父类属性 this.childProp = name; } // 比如自定义CustomArray继承 Array,CustomArray无法正确继承Array内部的length属性 function MyArray() { Array.apply(this, arguments); } // 以下等同:Child.prototype = new Array(); // 也可以等同:Object.setPrototypeOf(MyArray.prototype, Array.prototype); MyArray.prototype = Object.create(Array.prototype, { constructor: { value: MyArray, writable: true, configurable: true, enumerable: true } }); // 子类实例化时父类内部属性无法被正确继承。 var colors = new MyArray(); colors[0] = "red"; colors.length // 0 colors.length = 0; colors[0] // "red"
// es6 // constructor 和 super 都默认被创建 class Child extends Parent {} // 等同于 class Child extends Parent { constructor(...args) { // 先创建父类 this,在子类构造函数中先调用 super()方法,之后再能使用 this // 是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例; // super虽然代表了父类构造函数,但是返回的是子类Son的实例,即super内部的this指向的是Child, // super指向父类,super()中的this指向Child // super的作用就是 创建父类的实例对象this(即父类的构造函数内部的this指向子类的实例),然后再用子类的构造函数去修饰这个this。 // 因此super()在这里相当于 Parents.constructor.call(this, ...args); super(...args); // 再修改 this this.childProp = ...; } } //extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。 //优势:子类可直接继承父类内部属性(如原生构造函数 Array 的 length 机制) class VersionedArray extends Array { constructor() { super(); this.history = [[]]; } }
区别 2,继承链差异:
ES5 继承原型链
ES5 实现之中,每一个对象都有
__proto__
属性,指向对应的构造函数的 prototype 属性。son ——_proto__——> Son.prototype ——_proto__——> Father.prototype ——_proto__——> Object.prototype Son.__proto__ -> Function.prototype
ES6 继承原型链:
son -> Son.prototype -> Father.prototype -> Function.prototype -> Object.prototype Son.__proto__ -> Father
Class 底层实际有两条继承链:
子类 prototype 属性的
__proto__
属性,表示方法的继承,指向父类的 prototype 属性(跟 es5 原型链继承一样,Child.prototype = new Parent()
)子类
__proto__
属性,表示构造函数的继承,总是指向父类,子类可以通过__proto__
属性找到父类(但es5子类通过__proto__
只能找到Function.prototype)// es5 function Father() {} function Child() {} Child.prototype = new Father(); // 原型链继承,子类原型指向父类实例 Child.prototype.constructor = Child; console.log(Child.prototype.__proto__ === Father.prototype); // true,继承关系 console.log(Child.__proto__ === Function.prototype); // true,非继承关系 // es6 class Father {} class Child extends Father {} console.log(Child.__proto__ === Father, Child.prototype.__proto__ === Father.prototype); // true // 等价于(简化版): function inherit(Child, Parent) { Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; // 关键区别在这里 Object.setPrototypeOf(Child, Parent); }
之所以会有两条链,因为类的继承实现模式如下:
class A {} class B {} // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); const b = new B(); // 其中Object.setPrototypeOf过程 Object.setPrototypeOf = function(obj, proto) { obj.__proto__ = proto; return obj; }; // 所以上面式子又等价于 B.prototype.__proto__ = A.prototype; B.__proto__ = A;
# 008: Symbol 用法和应用场景
# 009: Promise.all 的用法、错误处理、以及手写原理实现
# Promise.all 用法
Promise.all 接受一个由 promise 异步任务组成的数组,并支持同时处理多个 promise 异步任务。如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象。
当且仅当所有异步任务都执行完成被 resolve 以后,Promise.all 才会被 resolve,并返回所有异步任务结果组成的数组;如果其中有某个异步任务失败了,则会返回失败的信息,即使其他 promise 异步任务执行成功,也会返回失败。
// 被resolve
const p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function(results) {
console.log(results); // [1, 2, 3]
});
// 被reject
const p1 = Promise.resolve(1),
p2 = Promise.reject(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
.then(function(results) {
//then方法不会被执行
console.log(results);
})
.catch(function(e) {
//catch方法将会被执行,输出结果为:2
console.log(2);
});
# Promise.all 错误处理
有时候,我们使用 Promise.all()执行多个网络请求,可能有一个请求出错,但我们并不希望其他的网络请求也返回 reject,要错都错,这样显然是不合理的。
如何做才能做到 promise.all 中即使一个 promise 程序 reject,promise.all 依然能把其他数据正确返回呢?
方案一:在处理单个异步任务时,通过try..catch
对异常情况也进行捕获,异常原因也作为 resolve 结果。
const p1 = new Promise((resolve) => {
try {
setTimeout(() => {
resolve(1);
}, 1000);
} catch (error) {
resolve(error);
}
});
const p2 = new Promise((resolve, reject) => {
try {
console.log(xxx.bb);
} catch (error) {
resolve(error);
}
});
const p3 = new Promise((resolve) => {
try {
setTimeout(() => {
resolve(3);
}, 2000);
} catch (error) {
resolve(error);
}
});
Promise.all([p1, p2, p3])
.then((res) => {
console.log(res); // [ 1, ReferenceError: xxx is not defined at file, 3]
})
.catch((error) => {
console.log("err", error);
});
方案二:通过 promise 的 catch 捕获,并且 return 错误信息
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
}).catch((error) => error);
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("error");
}, 1000);
}).catch((error) => error);
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 1000);
}).catch((error) => error);
Promise.all([p1, p2, p3])
.then((res) => {
console.log(res); // [1, "error", 3]
})
.catch((error) => {
console.log("err", error);
});
# Promise.all 手写原理实现
function PromiseAll(promises) {
// promises也不一定为数组,但必须为可迭代对象
if (!Array.isArray(promises)) {
return reject("argument must be an array");
}
const len = promises.length;
const result = new Array(len);
let count = 0;
return new Promise((resolve, reject) => {
// 也可以通过for..of遍历
for (let i = 0; i < len; i++) {
Promise.resolve(promises[i])
.then((res) => {
result[i] = res;
count++;
if (count === len) {
resolve(result);
}
})
.catch((error) => {
reject(error);
});
}
});
}
// 测试例子
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
}).catch((error) => error);
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("error");
}, 1000);
}).catch((error) => error);
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 1000);
}).catch((error) => error);
PromiseAll([p1, p2, p3])
.then((res) => {
console.log(res); // [1, "error", 3]
})
.catch((error) => {
console.log("err", error);
});
# 010: Promise.allSettled 的用法以及手写原理实现
Promise.allSettled 用法类似于 Promise.all,接受一个由 promise 异步任务组成的数组,并支持同时处理多个 promise 异步任务。
和 all 的区别就是,不管其中的 promise 元素失败还是成功,都会被 resolve,触发 then 的第一个函数,结果由包含每个 promise 的 status 和 result/reason 对象组成。
用法:
const promise1 = new Promise(function(resolve, reject) {
setTimeout(function() {
reject("promise1");
}, 2000);
});
const promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("promise2");
}, 3000);
});
const promise3 = Promise.resolve("promise3");
const promise4 = Promise.reject("promise4");
Promise.allSettled([promise1, promise2, promise3, promise4]).then((args) => {
console.log(args);
});
/*
3000ms后输出:
result:
[
{status: "rejected", reason: "promise1"},
{status: "fulfilled", value: "promise2"},
{status: "fulfilled", value: "promise3"},
{status: "rejected", reason: "promise4"}
]*/
手写实现:
function PromiseAllSettled(promises) {
const len = promises.length;
const result = new Array(len);
let count = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < len; i++) {
Promise.resolve(promises[i])
.then((res) => {
const resultItem = {
status: "fulfilled",
result: res,
};
result[i] = resultItem;
count++;
if (count === len) {
resolve(result);
}
})
.catch((reason) => {
const resultItem = {
status: "rejected",
reason,
};
result[i] = resultItem;
count++;
if (count === len) {
resolve(result);
}
});
}
});
}
// 测试例子
const promise1 = new Promise(function(resolve, reject) {
setTimeout(function() {
reject("promise1");
}, 2000);
});
const promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("promise2");
}, 3000);
});
const promise3 = Promise.resolve("promise3");
const promise4 = Promise.reject("promise4");
PromiseAllSettled([promise1, promise2, promise3, promise4]).then(function(
args
) {
console.log(args);
});
/*
result:
[
{status: "rejected", reason: "promise1"},
{status: "fulfilled", value: "promise2"},
{status: "fulfilled", value: "promise3"},
{status: "rejected", reason: "promise4"}
]*/
# 011: Promise 串行和并行的实现
# 串行方案
// 测试用例
// 当上一个 Promise 开始执行,当其执行完毕后再调用下一个 Promise,并作为一个新 Promise 返回,下次迭代就会继续这个循环。
const createPromise = (time, id) => () =>
new Promise((resolve) =>
setTimeout(() => {
console.log("promise", id);
resolve();
}, time)
);
runPromises([
createPromise(3000, 1),
createPromise(2000, 2),
createPromise(1000, 3),
]);
// 输出顺序
// promise 1
// promise 2
// promise 3
// Array.prototype.reduce + promise.then
const runPromises = (promiseArr) => {
promiseArr.reduce((prevPromise, curPromise) => {
return prevPromise.then(() => curPromise());
}, Promise.resolve());
};
// for+async/await版本
const runPromises = async (promiseArr) => {
for (let promise of promiseArr) {
await promise();
}
};
# 并行方案
// 测试用例
const createPromise = (time, id) => () =>
new Promise((resolve) =>
setTimeout(() => {
console.log("promise", id);
resolve();
}, time)
);
runPromises([
createPromise(3000, 1),
createPromise(2000, 2),
createPromise(1000, 3),
]);
// 输出顺序
// promise 3
// promise 2
// promise 1
// 模拟promise.allSettled实现
const runPromises = (promiseArr) => {
return new Promise((resolve, reject) => {
const len = promiseArr.length;
const result = new Array(len);
let count = 0;
for (let i = 0; i < len; i++) {
Promise.resolve(promiseArr[i]()).then(
(res) => {
result[i] = { result: res, status: "fulfilled" };
count++;
if (count === len) return result;
},
(error) => {
result[i] = { reason: res, status: "rejected" };
count++;
if (count === len) return result;
}
);
}
});
};
# 012: Promise 限制并行调度器
题目:现有 8 个图片资源的 url,已经存储在数组 urls 中,且已有一个函数 function loading,输入一个 url 链接,返回一个 Promise。该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。 要求:任何时刻同时下载的链接数量不可以超过 3 个。 请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。
var urls = [
"https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg",
"https://www.kkkk1000.com/images/getImgData/gray.gif",
"https://www.kkkk1000.com/images/getImgData/Particle.gif",
"https://www.kkkk1000.com/images/getImgData/arithmetic.png",
"https://www.kkkk1000.com/images/getImgData/arithmetic2.gif",
"https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg",
"https://www.kkkk1000.com/images/getImgData/arithmetic.gif",
"https://www.kkkk1000.com/images/wxQrCode2.png",
];
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
console.log("一张图片加载完成");
resolve();
};
img.onerror = reject;
img.src = url;
});
}
// 题目的意思是需要先并发请求3张图片,当一张图片加载完成后,又会继续发起一张图片的请求,让并发数保持在3个,直到需要加载的图片都全部发起请求。
function fetchImgWithLimit(urls = [], limit = 3, handlerFn = loadImg) {
const allPromises = []; // 用来存储所有的Promise任务
const executing = new Array();
const run = () => {
// 边界处理
if (!urls.length) {
return Promise.resolve();
}
// 处理一个 移除一个
const url = urls.shift();
// 被作为一个任务存储
const promiseItem = Promise.resolve()
.then(() => handlerFn(url))
.then(() => {
// 请求图片完毕后从executing队列中移除
executing.splice(executing.indexOf(promiseItem), 1);
});
allPromises.push(promiseItem);
executing.push(promiseItem);
let cur =
executing.length >= limit
? Promise.race(executing)
: Promise.resolve();
return cur.then(() => run());
};
return run().then(() => Promise.all(allPromises));
}
fetchImgWithLimit(urls, 3, loadImg)
.then((res) => {
console.log("all loaded success", res);
})
.catch((error) => {
console.log("loaded error", error);
});
# 013: Promise.all 捕获错误
const getData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const ok = Math.random() > 0.5; // 模拟成功或失败
if (ok) resolve("get data");
else {
reject("error"); // 正常的reject
}
}, 2000);
});
};
// 方法1
const promise1 = getData().catch((err) => err);
const promise2 = getData().catch((err) => err);
Promise.all([promise1, promise2])
.then((response) => {
// 业务逻辑...
})
.catch((response) => {
console.log(response);
});
// 方法2-方法1的封装
const getPromiseResult = (arr) => {
const promiseArr = arr.map((promise) => promise.catch((err) => err));
return Promise.all(promiseArr)
.then((response) => {
// 业务逻辑...
})
.catch((response) => {
console.log(response);
});
};
// allSettled
Promise.allSettled([promise1, promise2, promise3, promise4]).then(function(
args
) {
console.log(args);
/*
result:
[
{"status":"rejected","reason":"promise1"},
{"status":"fulfilled","value":"promise2"},
{"status":"fulfilled","value":"promise3"},
{"status":"rejected","reason":"promise4"}
]*/
});
# 014: async/await 捕获错误
function getData(data) {
return new Promise((resolve, reject) => {
if (data === 1) {
setTimeout(() => {
resolve("getdata success");
}, 1000);
} else {
setTimeout(() => {
reject("getdata error");
}, 1000);
}
});
}
// 方法1
// 缺点:如果有多个testAwait,需要多个try..catch,代码冗余!
const testAwait1 = async () => {
try {
let res = await getData(3);
console.log(res);
} catch (error) {
console.log(res); //getdata error
}
};
// 方法2
// 缺点:res把错误的结果和正确的结果都混在一起了,需要额外判断
const testAwait2 = async () => {
let res = await getData(3)
.then((r) => r)
.catch((err) => err);
console.log(res); //getdata error
};
// 方法3,对异步任务错误捕获封装
const awaitWraper = (promise) => {
return [data, error] = await promise.then((res) => [res, null]).catch((error) => [null, error]);
};
const testAwait3 = async () => {
const [res, error] = await awaitWraper(getData(3));
console.log(res); //null
console.log(error); //getdata error
};
参考:https://cloud.tencent.com/developer/article/1470715