# 🌵 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: 箭头函数和普通函数的区别?

  1. 箭头函数是匿名函数,不能作为构造函数,不能使用 new 去调用,箭头函数没有原型属性;
  2. 箭头函数不绑定 arguments,取而代之可以用扩展运算符...解决
  3. 箭头函数没有 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 和普通构造函数的区别是什么?

  1. 提升:

    • Class 不存在变量提升,内部采用严格模式,必须先定义后使用
    • 普通构造函数存在变量提升,可在定义前调用
  2. 调用方式:

    • Class 构造实例必须通过 new 调用执行
    • 普通构造函数不用 new 也可以执行
  3. 方法差异:

    • 方法定义:

      • 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)) // []
    
  4. 继承实现差异: 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(){
    
        }
    }
    
  5. 特殊方法支持:

    • 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 指向

  1. new 调用后的返回,会根据 constructor 构造器里面返回来决定(没返回则返回一个正常实例对象。有则按 constructor 返回的来)。

  2. 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));
        }
    

    最佳实践:

    1. 静态方法中访问类属性或方法时,使用 this 指向类本身。
    2. 如果希望方法中的 this 始终绑定实例,使用类字段箭头函数(箭头函数this 指向定义时所在上下文)。
    3. 如果不需要绑定,使用普通方法,但在作为回调传递时注意使用箭头函数包裹或绑定。

# 006: Class 中的 super 用法

super 关键字,一般表示父类的构造函数,用来新建父类的 this 对象。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。因为子类自己的 this 对象必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。

super 只能访问父类原型上的方法和静态方法,不能直接访问实例属性。

  1. 在构造函数中调用 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
    
  2. 在普通方法中调用 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
    
  3. 在静态方法中调用 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 底层实际有两条继承链:

      1. 子类 prototype 属性的__proto__属性,表示方法的继承,指向父类的 prototype 属性(跟 es5 原型链继承一样,Child.prototype = new Parent()

      2. 子类 __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

# 015: ES6 Class 实现原理(Babel 是如何编译 class 的)