你不知道的JavaScript(四)

310 阅读16分钟

1. Proxy与Reflect

1. Proxy监听对象属性的操作

在ES6中, 新增了一个Proxy类, 用来帮助我们创建一个代理. 也就是说, 如果我们希望监听一个对象的相关操作, 那么我们可以先创建一个代理对象, 之后对该对象的所有操作, 都通过代理对象完成, 代理对象可以监听我们想要对原对象进行哪些操作.

  const obj = {
    name: "zgc",
    age: 18,
    height: 1.88,
  };

  // 需求:监听对象属性的所有操作

  // 方式一: Object.defineProperty
  const keys = Object.keys(obj);
  for (let key of keys) {
    // console.log(key);
    let value = obj[key];
    Object.defineProperty(obj, key, {
      get: function () {
        // console.log(`监听: 获取${key}的值`);
        return value;
      },
      set(newVal) {
        // console.log(`监听: 设置${key}的值`, newVal);
        value = newVal;
      },
    });
  }

  // console.log(obj.name); // zgc
  // obj.name = "wf";
  // console.log(obj.name); // wf

  // 方式二: Proxy代理对象
  // 首先, 我们需要new Proxy对象 并且传入需要侦听的对象(target)以及一个处理对象(handler)
  // const p = new Proxy(target, handler)
  // 其次, 我们之后的操作都是对Proxy的操作, 而不是原有的对象
  var info = {
    name: "wlc",
    age: 24,
    height: 1.88,
  };

  // 如果我们想要侦听某些具体的操作, 那么就可以在handler中添加对应的捕捉器(Trap):
  const infoProxy = new Proxy(info, {
    // 获取对象属性:
    get: function (target, property, receiver) {
      // target: 侦听的对象, property: 被获取的属性key, receiver: 调用的代理对象
      console.log(target, property, receiver);
      return target[property];
    },
    // 设置对象属性值/添加对象属性
    set: function (target, property, value, receiver) {
      // target: 侦听的对象, property: 被设置的属性key, value: 新属性值, receiver: 调用的代理对象
      console.log(target, property, value, receiver);
      target[property] = value;
    },
    // 删除对象属性
    deleteProperty: function (target, property) {
      // target: 侦听的对象, property: 被获取的属性key
      console.log(target, property);
      delete info.height;
    },
    // 判断属性是否在对象中
    has: function (target, property) {
      // target: 侦听的对象, property: 被获取的属性key
      console.log(target, property);
      return property in target;
    },
  });

  console.log(infoProxy.age); // 24
  infoProxy.age = 20;
  console.log(infoProxy.age); // 20

  // 添加新属性
  infoProxy.address = "北京";
  console.log(info, infoProxy);
  // {name: 'wlc', age: 20, height: 1.88, address: '北京'}  {name: 'wlc', age: 20, height: 1.88, address: '北京'}

  // 删除对象属性
  delete infoProxy.height;
  console.log(info, infoProxy);
  // {name: 'wlc', age: 20, address: '北京'} {name: 'wlc', age: 20, address: '北京'}

  // 判断属性是否在对象中
  console.log("age" in infoProxy); // true
  console.log("height" in infoProxy); // false

2. Reflect

www.cnblogs.com/windrunnerm…

Reflect也是ES6新增的API, 它是一个 对象 , 字面意思是反射. Reflect并非一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect对象作为一个函数来调用,就像Math对象一样,Reflect对象的所有属性和方法都是静态的.

实际上Reflect对象是ES6为操作对象而提供的新API,而这个API设计的目的主要有:

  • Object对象的一些属于语言内部的方法放到Reflect对象上,从Reflect上能拿到语言内部的方法,例如Object.defineProperty方法。
  • 修改某些Object方法返回的结果,例如Object.defineProperty(obj, name, desc)方法在无法定义属性的时候会抛出异常,而Reflect.defineProperty(obj, name, desc)方法在操作失败时则会返回false
  • Object的操作都变成函数行为,例如Object的命令式name in objdelete obj[name]Reflect.has(obj, name)Reflect.deleteProperty(obj, name)相等。
  • 使Reflect对象的方法与Proxy对象的方法一一对应,只要Proxy对象上能够定义的handlers方法Reflect也能找到。

Reflect和Proxy共同完成代理

  // Reflect也是ES6新增的API, 它是一个 对象 , 字面意思是反射
  // Reflect提供了很多操作JS对象的方法, 有点像Object中操作对象的方法
  // 如获取对象原型: Reflect.getPrototypeOf() 和 Object.getPrototypeOf()
  // 如设置对象属性: Object.defineProperty() 和 Reflect.defineProperty();
  const obj = {
    name: "zgc",
    age: 18,
    m: function () {
      console.log(this === objProxy, this === obj);
    },
  };

  const objProxy = new Proxy(obj, {});
  objProxy.m(); // true  false
  obj.m(); // false true

  // Reflect和Proxy共同完成代理

  var info = {
    _name: "wlc",
    set name(newVal) {
      console.log("set this", this);
      this._name = newVal;
    },
    get name() {
      console.log("get this", this);
      return this._name;
    },
    age: 18,
  };

  // 如果我们想要侦听某些具体的操作, 那么就可以在handler中添加对应的捕捉器(Trap):
  const infoProxy = new Proxy(info, {
    // 获取对象属性:
    get: function (target, property, receiver) {
      // target: 侦听的对象, property: 被获取的属性key, receiver: 调用的代理对象
      // console.log(target, property, receiver);
      // return target[property];
      return Reflect.get(target, property, receiver);
    },
    // 设置对象属性值/添加对象属性
    set: function (target, property, value, receiver) {
      // target: 侦听的对象, property: 被设置的属性key, value: 新属性值, receiver: 调用的代理对象
      // console.log(target, property, value, receiver);
      // console.log(receiver === infoProxy); // true

      // 好处一: 代理对象的目的: 不再直接操作原对象
      // 好处二: Reflect.set方法返回一个布尔值, 可以进行边界判断
      /*
        好处三:
         > receiver就是外面的Proxy对象
         > Reflect.set/get方法最后一个参数可以决定对象访问器getter,setter的this指向
         > 如果target对象中指定了getter,setter,receiver可以作为则为getter,setter调用时的this值
      */
      // target[property] = value;
      // const isSuccess = Reflect.set(target, property, value); // this 指向 info对象
      const isSuccess = Reflect.set(target, property, value, receiver); // this 指向 infoProxy对象
      if (!isSuccess) {
        throw new Error(`set ${property} failure`);
      }
    },
  });

  // infoProxy.age = 24;
  // console.log(info, infoProxy); // {_name: 'wlc', age: 24} Proxy {_name: 'wlc', age: 24}

  // info.name = 'wf' // this是info对象
  // console.log(info.name); // this是info对象
  infoProxy.name = "zgc"; // this指向看是否传入receiver属性
  console.log(infoProxy.name);// this指向看是否传入receiver属性

Reflect.apply()

Reflect.apply(target, thisArgument, argumentsList)
静态方法Reflect.apply()通过指定的参数列表发起对目标target函数的调用,和Function.prototype.apply()功能类似。

  • target: 目标函数。
  • thisArgumenttarget函数调用时绑定的this对象。
  • argumentsListtarget函数调用时传入的实参列表,该参数应该是一个类数组的对象。
  • return: 返回值是调用完带着指定参数和this值的给定的函数后返回的结果。
var obj = {name: "11"};
function target(a, b){
    console.log(this.name);
    console.log(a, b);
}
Reflect.apply(target, obj, [1, 2]); // 11 // 1 2

Reflect.construct()

Reflect.construct(target, argumentsList[, newTarget])
Reflect.construct()方法的行为像new操作符构造函数,相当于运行new target(...args)

  • target: 被运行的目标构造函数。
  • argumentsList: 类数组对象,目标构造函数调用时的参数。
  • newTarget: 可选,作为新创建对象的原型对象的constructor属性,默认值为target
  // 创建一个Student类型的对象

  // 方式一: 借用构造函数
  // function Person(name, age) {
  //   this.name = name;
  //   this.age = age;
  // }

  // function Student(name, age) {
  //   Person.apply(this, [name, age]);
  // }

  // const stu = new Student("zgc", 18);
  // console.log(stu);

  // 方式二: 反射
  function Person(name, age) {
    this.name = name;
    this.age = age;
  }

  function Student() {}

  const stu = Reflect.construct(Person, ["wf", 22], Student);
  console.log(stu); // Student {name: 'wf', age: 22}
  console.log(stu.__proto__ === Student.prototype); // true

Reflect.defineProperty()

Reflect.defineProperty(target, propertyKey, attributes)
方法Reflect.defineProperty()基本等同于Object.defineProperty()方法,唯一不同是返回Boolean值。

  • target: 目标对象。
  • propertyKey: 要定义或修改的属性的名称。
  • attributes: 要定义或修改的属性的描述。
  • return: 返回Boolean值指示了属性是否被成功定义。
var obj = {_name: 11};
var success = Reflect.defineProperty(obj, "name", {
    get:function(){
        console.log("getter");
        return obj._name;
    }
})
console.log(success); // true
console.log(obj.name); // getter // 11

Reflect.deleteProperty()

Reflect.deleteProperty(target, propertyKey)
方法Reflect.deleteProperty()允许用于删除属性,类似于delete operator,但它是一个函数。

  • target: 删除属性的目标对象。
  • propertyKey: 需要删除的属性的名称。
  • return: 返回Boolean值表明该属性是否被成功删除。
var obj = {name: 11};
var success = Reflect.deleteProperty(obj, "name");
console.log(success); // true
console.log(obj.name); // undefined

Reflect.get()

Reflect.get(target, propertyKey[, receiver])
Reflect.get()方法与从对象target[propertyKey]中读取属性类似,但它是通过一个函数执行来操作的。

  • target: 需要取值的目标对象
  • propertyKey: 需要获取的值的键值
  • receiver: 如果target对象中指定了getterreceiver则为getter调用时的this值。
  • return: 返回属性的值。
var obj = {name: 11};
var name = Reflect.get(obj, "name");
console.log(name); // 11

Reflect.getOwnPropertyDescriptor()

Reflect.getOwnPropertyDescriptor(target, propertyKey)
方法Reflect.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor()方法相似,如果在对象中存在,则返回给定的属性的属性描述符,否则返回undefined

  • target: 需要寻找属性的目标对象。
  • propertyKey: 获取自己的属性描述符的属性的名称。
var obj = {name: 11};
var des = Reflect.getOwnPropertyDescriptor(obj, "name");
console.log(des); // {value: 11, writable: true, enumerable: true, configurable: true}

Reflect.getPrototypeOf()

Reflect.getPrototypeOf(target)
方法Reflect.getPrototypeOf()Object.getPrototypeOf()方法几乎相同,都是返回指定对象的原型,即内部的[[Prototype]]属性的值。

  • target: 获取原型的目标对象。
  • return: 给定对象的原型,如果给定对象没有继承的属性,则返回null
var obj = {name: 11};
var proto = Reflect.getPrototypeOf(obj);
console.log(proto === Object.prototype); // true

Reflect.has()

Reflect.has(target, propertyKey)
方法Reflect.has()作用与in操作符类似,但它是通过一个函数执行来操作的。

  • target: 目标对象.
  • propertyKey: 属性名,需要检查目标对象是否存在此属性。
  • return: 返回一个Boolean类型的对象指示是否存在此属性。
var obj = {name: 11};
var exist = Reflect.has(obj, "name");
console.log(exist); // true

Reflect.isExtensible()

Reflect.isExtensible(target)
方法Reflect.isExtensible()判断一个对象是否可扩展,即是否能够添加新的属性,与Object.isExtensible()方法相似。

  • target: 检查是否可扩展的目标对象。
  • return: 返回一个Boolean值表明该对象是否可扩展。
var obj = {name: 11};
var extensible = Reflect.isExtensible(obj);
console.log(extensible); // true

Reflect.ownKeys()

Reflect.ownKeys(target)
方法Reflect.ownKeys()返回一个由目标对象自身的属性键组成的数组。

  • target: 获取自身属性键的目标对象。
  • return: 返回由目标对象的自身属性键组成的Array
var obj = {name: 11};
var keys = Reflect.ownKeys(obj);
console.log(keys); // ["name"]

Reflect.preventExtensions()

Reflect.preventExtensions(target)
方法Reflect.preventExtensions()方法阻止新属性添加到对象,防止将来对对象的扩展被添加到对象中,该方法与Object.preventExtensions()相似。

  • target: 阻止扩展的目标对象。
  • return: 返回一个Boolean值表明目标对象是否成功被设置为不可扩展。
var obj = {name: 11};
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // false

Reflect.set()

Reflect.set(target, propertyKey, value[, receiver])
方法Reflect.set()是在一个对象上设置一个属性。

  • target: 设置属性的目标对象。
  • propertyKey: 设置的属性的名称。
  • value: 设置的值。
  • receiver: 如果遇到setterreceiver则为setter调用时的this值。
  • return: 返回一个Boolean值表明是否成功设置属性。
var obj = {name: 1};
Reflect.set(obj, "name", 11);
console.log(Reflect.get(obj, "name")); // 11

Reflect.setPrototypeOf()

Reflect.setPrototypeOf(target, prototype)
除了返回类型以外,方法Reflect.setPrototypeOf()Object.setPrototypeOf()方法是一样的,它可设置对象的原型即内部的[[Prototype]]属性,为另一个对象或null,如果操作成功返回true,否则返回false

  • target: 设置原型的目标对象。
  • prototype: 对象的新原型,为一个对象或null
  • return: 返回一个Boolean值表明是否原型已经成功设置。
var obj = {};
var proto = {name: 11};
Reflect.setPrototypeOf(obj, proto);
console.log(proto === Reflect.getPrototypeOf(obj)); // true

对比

Method NameObjectReflect
defineProperty()Object.defineProperty()返回传递给函数的对象,如果未在对象上成功定义属性,则返回TypeError如果在对象上定义了属性,则Reflect.defineProperty()返回true,否则返回false
defineProperties()Object.defineProperties() 返回传递给函数的对象。如果未在对象上成功定义属性,则返回TypeErrorN/A
set()N/A如果在对象上成功设置了属性,则Reflect.set()返回true,否则返回false。如果目标不是Object,则抛出TypeError
get()N/AReflect.get()返回属性的值。如果目标不是Object,则抛出TypeError
deleteProperty()N/A如果属性从对象中删除,则Reflect.deleteProperty()返回true,否则返回false
getOwnPropertyDescriptor()如果传入的对象参数上存在Object.getOwnPropertyDescriptor() ,则会返回给定属性的属性描述符,如果不存在,则返回undefined如果给定属性存在于对象上,则Reflect.getOwnPropertyDescriptor()返回给定属性的属性描述符。如果不存在则返回undefined,如果传入除对象(原始值)以外的任何东西作为第一个参数,则返回TypeError
getOwnPropertyDescriptors()Object.getOwnPropertyDescriptors()返回一个对象,其中包含每个传入对象的属性描述符。如果传入的对象没有拥有的属性描述符,则返回一个空对象。N/A
getPrototypeOf()Object.getPrototypeOf()返回给定对象的原型。如果没有继承的原型,则返回null。在ES5中为非对象抛出TypeErrorReflect.getPrototypeOf()返回给定对象的原型。如果没有继承的原型,则返回null,并为非对象抛出TypeError
setPrototypeOf()如果对象的原型设置成功,则Object.setPrototypeOf()返回对象本身。如果设置的原型不是Objectnull,或者被修改的对象的原型不可扩展,则抛出TypeError如果在对象上成功设置了原型,则Reflect.setPrototypeOf()返回true,否则返回false(包括原型是否不可扩展)。如果传入的目标不是Object,或者设置的原型不是Objectnull,则抛出TypeError
isExtensible()如果对象是可扩展的,则Object.isExtensible()返回true,否则返回false,如果第一个参数不是对象,则在ES5中抛出TypeError,在ES2015中,它将被强制为不可扩展的普通对象并返回false如果对象是可扩展的,则Reflect.isExtensible()返回true,否则返回false。如果第一个参数不是对象,则抛出TypeError
preventExtensions()Object.preventExtensions()返回被设为不可扩展的对象,如果参数不是对象,则在ES5中抛出TypeError,在ES2015中,参数如为不可扩展的普通对象,然后返回对象本身。如果对象已变得不可扩展,则Reflect.preventExtensions() 返回true,否则返回false。如果参数不是对象,则抛出TypeError
keys()Object.keys()返回一个字符串数组,该字符串映射到目标对象自己的(可枚举)属性键。如果目标不是对象,则在ES5中抛出TypeError,但将非对象目标强制为ES2015中的对象N/A
ownKeys()N/AReflect.ownKeys()返回一个属性名称数组,该属性名称映射到目标对象自己的属性键,如果目标不是Object,则抛出TypeError

2. Promise

1. Promise的基本使用

  // 创建 promise 对象(pending 状态), 指定执行器(executor)函数
  const promise = new Promise((resolve, reject) => {
    // 接受executor(执行器)函数作为new Promise()的参数, 这个执行器函数会被立即执行
    console.log("立即执行");
    setTimeout(() => {
      const time = Date.now();
      // 如果成功了, 调用 resolve(), 执行then传递过来的回调函数, 指定成功的 value值, 且promise变为 resolved 状态
      if (time % 2 === 1) {
        //  promise的状态一旦确定下来, 就不会再更改, 即多次调用resolve/reject无效, 只执行第一个
        resolve("成功的值 " + time);
      } else {
        // 如果失败了, 调用 reject(), 执行then传递过来的回调函数, 指定失败的 reason值, 且promise变为rejected 状态
        reject("失败的值" + time);
      }
    }, 2000);
  });

  promise.then(
    (value) => { // 传入的回调函数, 当Promise的状态为resolved/fullfilled的时候调用resolve方法, 执行这个回调
      console.log("成功的回调:", value);
    },
    (reason) => { // 传入的回调函数, 当Promise的状态为rejected的时候调用reject方法, 执行这个回调
      console.log("失败的回调:", reason);
    }
  );
  console.log(".then方法是立即执行的, 但then方法内的回调是异步执行的");
  // 立即执行
  // .then方法是立即执行的, 但then方法内的回调是异步执行的
  // 失败的回调: 失败的值1677679498530

2. Promise的resolve值

 var obj = {
    name: "zgc",
    then: function (resolve) {
      resolve("哈哈哈");
    },
  };
  const promise = new Promise((resolve, reject) => {
    // 1. 如果传入的参数为非Promise类型的值, 返回的结果为成功promise对象(掌握)
    // resolve传入的普通值,就是then函数中res参数接收的数据
    resolve([
      { name: "zgc", age: 18 },
      { name: "wf", age: 24 },
    ]);

    // 2. 如果传入的参数为 Promise 对象, 则返回的 promise 对象的状态由传入的 Promise 决定(理解)
    // new Promise中resolve/reject传入的普通值,就是then/catch函数中参数接收的数据
    // resolve(
    //   new Promise((resolve, reject) => {
    //     // resolve("yes");
    //     reject("no");
    //   })
    // );

    // 3. 如果传入的是一个对象, 且对象内部有实现then方法, 那么就会执行该then方法, 并且根据then方法的结果来决定Promise的状态(了解)
    // resolve(obj)
  });

  console.log("promise", promise);

  promise
    .then((res) => {
      console.log("res:", res);
    })
    .catch((err) => {
      console.log("err:", err);
    });

image.png

3. then&catch方法的返回值

  const promise = new Promise((resolve, reject) => {
    resolve([
      { name: "zgc", age: 18 },
      { name: "wf", age: 24 },
    ]);
  });

  console.log("promise", promise);

  // then方法返回的也是一个promise对象
  // 所以我们可以定义promise对象进行链式调用
  /*
    > 当then方法的回调函数本身在执行时, 那么返回的promise对象处在pending状态
    > 当then方法的回调函数return一个结果时, 那么它处在fulfilled/resolved状态, 并且会将结果作为新Promise的resolve的参数(默认是undefined)
      > 正常数据
      > promise对象
    > 当then方法抛出一个异常时, 那么它处于reject状态
  */
  const result = promise
    .then((res) => {
      console.log("res1:", res);
      return res[0];
    })
    .then((res) => {
      console.log("res2:", res);
      return res.name;
    })
    .then((res) => {
      console.log("res3:", res);
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("success");
        }, 1000);
      });
    })
    .then((res) => {
      console.log("res4:", res);
      return new Promise((resolve, reject) => {
        reject("fail");
      });
    })
    .catch((err) => {
      console.log("err1:", err);
      return "catch";
    })
    .then((res) => {
      console.log("res5:", res);
      // 抛出异常
      throw new Error("Error");
    })
    .then((res) => {
      console.log("res6:", res);
    })
    .catch((err) => {
      // Promise 异常穿透(传透)
      // 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调
      // 前面任何操作出了异常,都会传到最后失败的回调中处理
      console.log("err2:", err);
      // 中断Promise链
      return new Promise(() => {});
    })
    .then((res) => {
      console.log("res7:", res);
    })
    .then((res) => {
      console.log("res8:", res);
    });

  console.log("result", result);

image.png

4. finally方法

 // finally表示Promise对象无论状态变为fulfilled/resolved还是rejected, 都会被执行

  const promise = new Promise((resolve, reject) => {
    resolve([
      { name: "zgc", age: 18 },
      { name: "wf", age: 24 },
    ]);
  });

  promise
    .then((res) => {
      console.log("res", res);
    })
    .catch((err) => {
      console.log("err", err);
    })
    .finally(() => {
      console.log('finall在哪种状态都会被调用');
    });

image.png

5. Promise类方法

 // 1. Promise.resolve
  // Promise.resolve("Hello World");

  // 相当于

  // new Promise((resolve, reject) => {
  //   resolve("Hello World");
  // });

  // 2. Promise.reject
  //  Promise.reject("reject err")

  // 相当于

  // new Promise((resolve, reject) => {
  //   reject("reject err");
  // });

  // 3. Promise.all: 返回一个新的 promise,只有所有的 promise 都成功才成功,只要有一个失败了就直接失败
  /*
    当所有的Promise状态变成fulfilled时, 新Promise的状态为fulfilled, 并且会将所有Promise的返回值组成一个数组, 
    数组结果的顺序与Promise.all([数组])的顺序一致。,当有一个Promise的状态为reject时, 
    新Promise的状态为reject, 并且会将第一个reject的返回值作为结果
  */
  const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "zgc", age: 18 });
      // reject({ name: "zgc", age: 18 });
    }, 1000);
  });

  const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: "wf", age: 24 });
      // reject({ name: "wf", age: 24 });
    }, 2000);
  });
  const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      // resolve("success");
      reject("fail");
    }, 3000);
  });

  const promise = Promise.all([p1, p2, p3]);
  console.log("promise", promise);

  promise
    .then((res) => {
      console.log("res1", res);
      // [{ name: "zgc", age: 18 }, { name: "wf", age: 24 }, success ];
    })
    .catch((err) => {
      console.log("err", err); // fail
    });

  // 4. Promise.race: 返回一个新的 promise,第一个完成的 promise 的结果状态就是最终的结果状态
  // Promise.race()只会得到(打印)第一个返回的结果,但其余结果也会返回(不打印)。 谁先完成就输出谁(不管是成功还是失败)

  const result = Promise.race([p1, p2, p3]);
  console.log("result", result);
  result
    .then((res) => {
      console.log("res2", res); // { name: "zgc", age: 18 }
    })
    .catch((err) => {
      console.log("err", err);
    });

  // 5. Promise.allSettled: 解决all方法的缺陷
  /*
      all方法有一个缺陷, 当有一个Promise状态变为reject时, 新Promise就会立即变成对应的reject状态,
      那么对于resolve的, 以及依然处于pending状态的promise, 我们是获取不到对应结果的;

      allSettled方法会在所有的promise都有结果(无论是fulfilled还是rejected)才会有最终的状态,
      并且这个状态一定是fulfilled的;
    */

  const allSettled = Promise.allSettled([p1, p2, p3]);
  console.log("allSettled", allSettled);

  allSettled.then((res) => {
    console.log("res3", res);
  });

  // 6. any: 和race方法类似, 但是会等到第一个成功的结果
  // 会等到一个fulfilled状态, 才会决定新Promise的状态
  // 如果所有的Promise都是reject的, 那么也会等到所有的Promise都变成rejected状态

  const any = Promise.any([p1, p2, p3]);
  console.log("any", any);

  any
    .then((res) => {
      console.log("res4", res);
    })
    .catch((err) => {
      // 当所有的Promise都是reject时, 返回的结果不是任何一个reject的参数, 而是一条错误信息
      console.log("err", err); // err AggregateError: All promises were rejected
    });

image.png

4. 迭代器(Iterator)与生成器(Generator)

JavaScript提供了四种数据集合,分别是array、object、map和set。这四种数据集合的数据结构各不相同,但是都可以被循环遍历,这一切的背后都离不开iteration(迭代器)的支撑。迭代器(Iterator)是一种机制,也可以说是一种接口,它为各种不同的数据结构提供了统一的访问机制。任何数据结构只要配置了 Iterator 接口(可以称之为迭代器对象),就可以完成遍历操作(配置了迭代器的数据结构称之为可迭代对象).

1. 迭代器对象

  • 在 Javascript 中,迭代器是一个对象(也可称作为迭代器对象),它提供了一个 next() 方法,用来返回迭代序列中的下一项;
  • next 方法的定义,next 方法是一个无参数或者一个参数的函数,执行后返回一个对象, 返回的包含两个属性:{ done: [boolean], value: [any] }, 当donetruevalue可省略;
  // Iterator: 迭代器
  const names = ["zgc", "wf", "cx", "wlc"];

  // 给数组names模拟一个迭代器对象
  function mockIterator(arr) {
    let index = 0;
    return {
      // next方法: 一个无参数或者一个参数的函数, 返回一个带有done/value的对象
      next: function () {
        // done: boolean值, 为false时表示尚未迭代完成, value: 具体值/undefined, 当done为turue时可以省略
        return index < arr.length
          ? { done: false, value: arr[index++] }
          : { done: true };
      },
    };
  }
  const NIterator = mockIterator(names)
  console.log(NIterator.next()); // {done: false, value: 'zgc'}
  console.log(NIterator.next()); // {done: false, value: 'wf'}
  console.log(NIterator.next()); // {done: false, value: 'cx'}
  console.log(NIterator.next()); // {done: false, value: 'wlc'}
  console.log(NIterator.next()); // {done: true}
  
 for (let i = 0; i < names.length; i++) {
    console.log(NIterator.next());
  }

2. 可迭代对象

ES6中为迭代器提供了统一的访问机制for..of..,ES6中原生的迭代器有Array、Set、Map、 String、argumrntsfor..of能够遍历它们是因为它们具有Symbol.iterator属性,该属性指向该数据结构的默认迭代器方法,当使用for...of..迭代该数据结构时,js引擎就会调用其Symbol.iterator方法,从而返回相应的默认迭代器。

  // 1. 数组/字符串/Set/Map内部有默认的迭代器, 本身就是可迭代的
  // 可迭代对象必然有一个名为[Symbol.iterator]的方法
  var arr = [10, 2, 3, 4, 5]; //数组是一个可迭代对象
  console.log(arr);

  // 使用for..of..来遍历迭代器
  for (var v of arr) {
    console.log(v); // 10  2  3  4  5
  }

  //也可以使用ES6提供的next()方法手工遍历
  var it = arr[Symbol.iterator](); // 调用可迭代对象的Symbol.iterator方法可以获取默认迭代器,将迭代器引用赋给it变量
  console.log(it.next().value); // 10
  console.log(it.next().value); // 2
  console.log(it.next().value); // 3

  // 2. 将info变成一个可迭代对象(迭代info中的friends)
  /*
    > 1. 必须实现一个特定的函数: [Symbol.iterator]
    > 2. 这个函数必须返回一个迭代器对象(用于迭代当前的对象)
  */
  const info = {
    friends: ["wf", "cx", "wlc"],
    // 计算属性名: []
    [Symbol.iterator]() {
      let index = 0;
      return {
        next: () => {
          return index < this.friends.length
            ? { done: false, value: this.friends[index++] }
            : { done: true };
        },
      };
    },
  };

  const FIterator = info[Symbol.iterator]();

  console.log(FIterator.next()); // {done: false, value: 'wf'}
  console.log(FIterator.next()); // {done: false, value: 'wf'}
  console.log(FIterator.next()); // {done: false, value: 'WLC'}
  console.log(FIterator.next()); // {done: true}
  console.log(FIterator.next()); // {done: true}

  // 3. 将infos变成一个可迭代对象(迭代infos中的key/value)
  const infos = {
    name: "zgc",
    age: 18,
    friends: ["wf", "cx", "wlc"],
    [Symbol.iterator]: function () {
      // const keys = Object.keys(this); // 得到key
      const values = Object.values(this); // 得到value
      // const entries = Object.entries(this) // 得到key/value数组
      let index = 0;
      const iterator = {
        next: function () {
          return index < values.length
            ? { done: false, value: values[index++] }
            : { done: true };
        },
      };
      return iterator;
    },
  };

  // 4. 改造的对象支持for...of遍历(对象原本是不支持的)

  for (const item of infos) {
    console.log(item); // zgc 18  ["wf", "cx", "wlc"]
  }

  const obj = {
    friends: ["wf", "cx", "wlc"],
  };

  // for (const item of obj) {
  //   console.log(item); // obj is not iterable
  // }

3. 自定义类的迭代

  class Person {
    constructor(name, age, height) {
      this.name = name;
      this.age = age;
      this.height = height;
    }

    // 实例方法是放在Person.prototype上面的
    [Symbol.iterator]() {
      const values = Object.values(this);
      let index = 0;
      return {
        next: function () {
          return index < values.length
            ? { done: false, value: values[index++] }
            : { done: true };
        },
        // 监听迭代器是否进行了中断的方法, 当迭代进行中断的情况下会触发此方法
        return: function () {
          console.log("监听到迭代器函数进行中断了");
          return { done: true };
        },
      };
    }
  }

  const p1 = new Person("zgc", "18", "1.88");
  const p2 = new Person("wf", "20", "2.88");
  console.log(p1);
  // 默认对象是不可以进行迭代的
  // 要求: 使通过自定义类Person创建出来的对象都是可迭代的

  /*
    实现方法:
    > 1. 必须实现一个特定的实例方法: [Symbol.iterator]
    > 2. 这个函数必须返回一个迭代器对象(用于迭代当前的对象)
  */

  for (const value of p1) {
    console.log(value); // zgc 18 1.88
  }

  // 迭代器某些情况下会在没有迭代完全都是时候进行中断
  for (const value of p2) {
    if (value === "20") break; // break return throw
    console.log(value); // wf
  }

4. 生成器函数的基本使用

生成器是ES6中新增的一种函数控制, 使用的方案, 它可以让我们更加灵活的控制函数什么时候继续执行或者暂停执行.

  • 生成器函数需要在function的后面加一个符号: *
  • 生成器函数可以通过yield关键字来控制函数的执行流程
  • 生成器函数的返回值是一个Generaor(生成器), 不会执行函数内部的代码
    • 生成器事实上是一种特殊的迭代器
    • 要想执行函数内部的代码, 需要返回的生成器对象(Generaor)调用它的next方法
    • 当遇到yield时中断执行, 想要继续执行需要再次调用next方法
  // 1. 定义生成器函数
  function* foo() {
    console.log(1);
    console.log(2);
    yield;
    console.log(3);
    console.log(4);
    yield;
    console.log(5);
  }

  // 2. 调用生成器函数, 得到生成器对象
  const generator = foo();

  // 3. 调用生成器对象的next方法执行函数内部代码
  generator.next(); // 1 2
  generator.next(); // 3 4
  generator.next(); // 5

5. 生成器函数的参数和返回值

  // 1. 定义生成器函数
  function* foo(text) {
    const age = 18;
    console.log("执行内部代码1", text);
    const next2 = yield;
    console.log("执行内部代码2", next2);
    const next3 = yield "zgc";
    // return "zgc";
    console.log("执行内部代码3", next3);
    yield age;
    console.log("执行内部代码4");
  }

  // 2. 调用生成器函数, 得到生成器对象
  const generator = foo("第一次执行yield");

  // 3. 生成器函数的内部next是有返回值的
  // 当yield全部执行完成之后的下一次调用next, done值变为true, value为undefined
  // console.log(generator.next()); // {value: undefined, done: false}
  // console.log(generator.next()); // {value: 'zgc', done: false}
  // console.log(generator.next()); // {value: 18, done: false}
  // console.log(generator.next()); // {value: undefined, done: true}

  // 4. 在中间直接进行return, 表示迭代已经完成, 后续代码无任何意义
  // 返回结果后done值变为true, 后续所有的done值也都变为true且value为undefined
  // console.log(generator.next()); // {value: undefined, done: false}
  // console.log(generator.next()); // {value: 'zgc', done: true}
  // console.log(generator.next()); // {value: undefined, done: true}

  // 5. 给函数每次执行传入参数
  //       // 1. 定义生成器函数
      function* foo(text) {
        const age = 18;
        console.log("执行内部代码1", text);
        const next2 = yield;
        console.log("执行内部代码2", next2);
        const next3 = yield "zgc";
        // return "zgc";
        console.log("执行内部代码3", next3);
        yield age;
        console.log("执行内部代码4");
      }

      // 2. 调用生成器函数, 得到生成器对象
      const generator = foo("第一次执行yield");

      // 3. 生成器函数的内部next是有返回值的
      // 当yield全部执行完成之后的下一次调用next, done值变为true, value为undefined
      // console.log(generator.next()); // {value: undefined, done: false}
      // console.log(generator.next()); // {value: 'zgc', done: false}
      // console.log(generator.next()); // {value: 18, done: false}
      // console.log(generator.next()); // {value: undefined, done: true}

      // 4. 在中间直接进行return, 表示迭代已经完成, 后续代码无任何意义
      // 返回结果后done值变为true, 后续所有的done值也都变为true且value为undefined
      // console.log(generator.next()); // {value: undefined, done: false}
      // console.log(generator.next()); // {value: 'zgc', done: true}
      // console.log(generator.next()); // {value: undefined, done: true}

      // 5. 给函数每次执行传入参数
      // 每个next传入的参数可以再上一个yield拿变量进行接收
      // 但第一个yield比较特殊, 无法从next传入参数, 因为第一个yield没有上一个yield接收它的参数
      // 所有第一个yield想要传入的参数往往在函数调用时传入
      console.log(generator.next());
      console.log(generator.next("第二次执行yield"));
      console.log(generator.next("第三次执行yield"));
      console.log(generator.next());
  // 但第一个yield比较特殊, 无法从next传入参数, 因为第一个yield没有上一个yield接收它的参数
  // 所有第一个yield想要传入的参数往往在函数调用时传入
  console.log(generator.next());
  console.log(generator.next("第二次执行yield"));
  console.log(generator.next("第三次执行yield"));
  console.log(generator.next());

6. 使用生成器来代替迭代器

  const names = ["zgc", "wf", "cx", "wlc"];

  // 1. 数组自带的迭代器
  const defaultIterator = names[Symbol.iterator]();
  // console.log(defaultIterator.next());
  // console.log(defaultIterator.next());
  // console.log(defaultIterator.next());
  // console.log(defaultIterator.next());
  // console.log(defaultIterator.next());

  // 2. 给数组names模拟一个迭代器对象
  function mockIterator(arr) {
    let index = 0;
    return {
      // next方法: 一个无参数或者一个参数的函数, 返回一个带有done/value的对象
      next: function () {
        // done: boolean值, 为false时表示尚未迭代完成, value: 具体值/undefined, 当done为turue时可以省略
        return index < arr.length
          ? { done: false, value: arr[index++] }
          : { done: true };
      },
    };
  }
  const NIterator = mockIterator(names);
  // console.log(NIterator.next());
  // console.log(NIterator.next());
  // console.log(NIterator.next());
  // console.log(NIterator.next());
  // console.log(NIterator.next());

  // 3. 使用生成器来代替迭代器

  // 生成器函数的返回值是一个特殊的迭代器
  function* Gfoo(arr) {
    for (let i = 0; i < arr.length; i++) {
      yield arr[i];
    }
  }

  const GIterator = Gfoo(names);
  console.log(GIterator.next()); // {value: 'zgc', done: false}
  console.log(GIterator.next()); // {value: 'wf', done: false}
  console.log(GIterator.next()); // {value: 'cx', done: false}
  console.log(GIterator.next()); // {value: 'wlc', done: false}
  console.log(GIterator.next()); // {value: undefined, done: false}

  // 4. 我们可以通过 yield*简化上面写法
  // yield*是一种yield的语法糖, 只不过会依次迭代一个可迭代对象, 每次迭代其中的一个值
  function* Gbar(arr) {
    yield* arr;
  }

  const GIterator2 = Gbar(names);
  console.log(GIterator2.next());
  console.log(GIterator2.next());
  console.log(GIterator2.next());
  console.log(GIterator2.next());
  console.log(GIterator2.next());

7. 生成器自定义类的迭代

  class Person {
    constructor(name, age, height) {
      this.name = name;
      this.age = age;
      this.height = height;
    }

    *[Symbol.iterator]() {
      const values = Object.values(this);
      // const values = Object.keys(this);
      yield* values;
      // yield* values 是下方循环的语法糖
      // for (let i = 0; i < values.length; i++) {
      //   yield values[i];
      // }
    }

    // 迭代器写法
    // [Symbol.iterator]() {
    //   const values = Object.values(this);
    //   let index = 0;
    //   return {
    //     next: function () {
    //       return index < values.length
    //         ? { done: false, value: values[index++] }
    //         : { done: true };
    //     },
    //     // 监听迭代器是否进行了中断的方法, 当迭代进行中断的情况下会触发此方法
    //     return: function () {
    //       console.log("监听到迭代器函数进行中断了");
    //       return { done: true };
    //     },
    //   };
    // }
  }

  const p1 = new Person("zgc", "18", "1.88");

  // 默认对象是不可以进行迭代的
  // 要求: 使通过自定义类Person创建出来的对象都是可迭代的
  // 使用生成器来简化迭代器写法

  for (const value of p1) {
    console.log(value); // zgc 18 1.88
  }

5. async & await

1. 回调函数与Promise处理异步请求

  // 1. 回调函数写法:
  // 当面临回调函数嵌套调用(外部回调函数异步执行的结果是嵌套的回调执行的条件)时,容易形成回调地狱
  function request1(url, callback) {
    setTimeout(() => {
      callback(url);
    }, 2000);
  }

  function getData1() {
    request1("zgc", (res1) => {
      console.log("res1", res1);
      request1(res1 + "wf", (res2) => {
        console.log("res2", res2);
        request1(res2 + "cx", (res3) => {
          console.log("res3", res3);
        });
      });
    });
  }

  getData1();

  // 2. promise 链式调用
  // 解决回调地狱的问题
  function request2(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(url);
      }, 2000);
    });
  }

  function getData2() {
    request2("zgc")
      .then((res1) => {
        console.log("res1", res1);
        return request2(res1 + "wf");
      })
      .then((res2) => {
        console.log("res2", res2);
        return request2(res2 + "wlc");
      })
      .then((res3) => {
        console.log("res3", res3);
      });
  }

  getData2();

image.png

2. 生成器处理异步请求

 function request(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(url);
      }, 2000);
    });
  }

  function* getData() {
    const res1 = yield request("zgc");
    console.log("res1", res1);

    const res2 = yield request(res1 + "wf");
    console.log("res2", res2);

    const res3 = yield request(res2 + "wlc");
    console.log("res3", res3);
  }

  // const Generator = getData();
  // console.log(Generator.next().value);

  // 1. 初始写法
  // Generator.next().value.then((res1) => {
  //   Generator.next(res1).value.then((res2) => {
  //     Generator.next(res2).value.then((res3) => {
  //       Generator.next(res3);
  //     });
  //   });
  // });

  // 2. 封装优化
  function execGenFn(fn) {
    // 调用传入的生成器函数, 得到生成器
    const generator = fn();
    recursiveCall(generator);
  }

  function recursiveCall(generator, data) {
    // 拿到生成器返回的结果
    const result = generator.next(data);
    if (result.done) return;


    result.value.then((res) => {
      recursiveCall(generator, res);
    });
  }

  execGenFn(getData);

image.png

3. async&await的基本使用

  1. await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
  2. 如果表达式是 promise 对象, await 返回的是 promise 成功的值
  3. 如果表达式是其它值, 直接将此值作为 await 的返回值
  4. await 必须写在 async 函数中, 但 async 函数中可以没有 await
  5. 如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try...catch 捕获处理
  6. await会等待Promise得到结果之后才会继续执行后续代码
  // 1. 声明异步函数: async
  async function foo() {
    console.log(1);
  }
  foo();

  // 2. 异步函数的内部代码执行顺序与普通函数是一致的 默认情况下也会同步执行

  // 3. 异步函数的返回值:
  // 函数的返回值为 promise 对象
  // promise 对象的结果由 async 函数return的返回值决定
  const bar = async function () {
    // 1. 如果return的是非promise的任意值, 那么得到一个新的状态为fulfilled的promise, value为返回值
    //  return 520;

    // 2. 如果return是一个Promise对象,那么return的Promise对象的结果状态决定了新的Promise的结果状态
    return new Promise((resolve, reject) => {
      resolve("YES");
    });

    // 3. 如果抛出异常, 新 promise 变为 rejected
    //  在异步函数里面如果抛出了异常(产生了错误), 这个异常不会如普通函数一样立即被浏览器处理 ,
    //  而是会在异步函数中进行处理: Promise.reject(err)

    // "abc".filter();
    // throw new Error("asycn fun err");
  };

  bar()
    .then((res) => {
      console.log("res", res);
    })
    .catch((err) => {
      console.log("异步函数捕获异常:", err);
    });

  // 4. await关键字

  const baz = async () => {
    let p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("ok");
      }, 2000);
    });

    let p2 = new Promise((resolve, reject) => {
      reject("throw error");
    });
    //1. 右侧为promise的情况
    let baz1 = await p1;
    console.log(baz1);

    //2. 右侧为其他类型的数据
    let baz2 = await 20;
    console.log(baz2);

    //3. 如果promise是失败的状态
    try {
      let baz3 = await p2;
    } catch (err) {
      console.log(err);
    }
  };
  baz();

  // 5. async & await处理异步请求
  function request(url) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(url);
      }, 2000);
    });
  }

 async function getData() {
    const res1 = await request("zgc");
    console.log("res1", res1);

    const res2 = await request(res1 + "wf");
    console.log("res2", res2);

    const res3 = await request(res2 + "wlc");
    console.log("res3", res3);
  }
  getData()

image.png

4. 异常处理与捕获

  // 1. 函数遇到throw之后, 后面的代码都不会执行
  // 2. throw 抛出一个具体的错误信息
  function foo() {
    console.log("foo");
    // 1. 字符串
    // throw 'error'

    // 2. 对象
    // throw {
    //   errMessage: "我是错误信息",
    //   errCode: 101101,
    // };

    // 3. 系统提供的类
    // 一般只传入第一个参数就可以了
    throw new Error("我是错误信息");

    console.log("foo结束");
  }

  // 当函数出现异常时, 会将异常抛给自己的调用者
  // 如果调用者没有进行处理, 那么异常会继续向外抛
  // 如果抛到全局还是没有进行处理, 异常就会抛给浏览器, 控制台报错, 后续代码终止执行
  // 捕获异常: try{ //正常代码 } catch(err){ //处理异常 }
  function bar() {
    try {
      foo();
      console.log('try执行');
    } catch (err) { // ES10中, 如果不打算使用参数err, 则小括号可以省略
      // new Error()抛出的异常, 有三个属性
      console.log('catch执行');
      console.log("err", err);
      console.log(err.message, err.name);
      console.log(err.stack);
    } finally {
      console.log('不论有没有异常都会执行');
    }
    console.log("bar");
  }

  function test() {
    bar();
    console.log("test");
  }

  test();
  console.log("-------");

image.png

6. 宏任务与微任务

1. 进程与线程

  • 进程: 进程可以简单的理解为一个可以独立运行的程序单位,进程就是由一个或多个线程构成的,简单来说你的电脑每运行一个程序就是一个进程,是 CPU 资源分配的最小单位;
  • 线程: 而线程是进程中的实际运行单位,是操作系统进行运算调度的最小单位。可理解为线程是进程中的一个最小运行单元,线程是 CPU 调度的最小单位;
  • 一个程序只至少有一个进程,一个进程至少有一个线程
  • 操作系统如何做到同时让多个进程(便听歌, 边写代码, 边查阅资料)同时工作呢?
    • 因为CPU(可直接运行操作系统)的运算速度非常快, 它可以迅速的在多个进程之间相互切换
    • 当进程中的线程获取到时间片时, 就可以快速编写我们的代码
    • 对于用户来说是感受不到这种快速的切换的

2. 单线程与多线程

  • JS是单线程的, 但JS的线程有自己的进程容器:浏览器或者Node;
  • 多数浏览器多进程的, 每开启一个tab页就会开启一个新的进程(防止一个页面卡死而造成所有页面无法响应), 每个进程中有很多的线程, 其中包括执JS代码的线程;
  • JS的代码执行是在一个单独的线程中执行的, 这就意味着JS的代码在同一时刻只能干一件事, 如果一件事非常耗时, 那么当前的线程就会被阻塞;
  • 但真正耗时的操作, 实际上并不是由JS线程在执行的
    • 浏览器的每个进程都是多线程的, 那么其他线程可以完成这个耗时的操作
    • 比如网络请求, 定时器等

3. 任务队列与事件轮询(Event Loop)

JS是一门单线程的非阻塞脚本语言,Event Loop就是JS异步编程的一种解决方案。 代码执行开启一个全局调用栈(主栈)提供代码运行的环境,在执行过程中同步任务的代码立即执行,遇到异步任务将异步的回调注册到任务队列中,等待同步代码执行完毕查看异步是否完成,如果完成将当前异步任务的回调拿到主栈中执行,得到其执行的结果。

  • 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。 js引擎执行异步代码而不用等待,是因有为有任务队列和事件轮询
    • 任务队列:任务队列是一个先进先出的队列,它里面存放着各种异步任务回调。
    • 事件轮询:事件轮询是指主线程重复从任务队列中取任务、执行任务的过程。
  • JS 主线程不断的循环往复的从任务队列中读取任务,执行任务,这种运行机制称为事件循环(event loop)。

4. 宏任务与微任务

  • 浏览器的事件循环中分成宏任务和微任务, JS 中任务分成同步任务和异步任务。
  • 定时器,事件绑定,ajax,回调函数等就是宏任务
  • promise中的then回调, async/await, process.nextTick等就是微任务。
  • 执行顺序: 主栈全局任务(宏任务) > 宏任务中的微任务 > 下一个宏任务。

5. 简述同步和异步的区别

同步是阻塞模式,异步是非阻塞模式。众所周知,javascript是单线程的、同步的语言;

  • 同步就是所有的任务都处在同一队列中,一个任务执行完接着开始执行下一个,相对于浏览器而言,同步的效率过低,一些耗费时间比较长的任务应该用异步来执行。
  • 异步任务就是将异步的回调注册到任务队列中,等待同步代码执行完毕查看异步是否完成,如果完成将当前异步任务的回调拿到主栈中执行,得到其执行的结果。

7. webStorage

  1. 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
  2. 浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。

1. 相关API

1. ```xxxxxStorage.setItem('key', 'value');```

    该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
    
    值为字符串,如果存入的值为对象,则将其转换为字符串后存入

2. ```xxxxxStorage.getItem('person');```

   该方法接受一个键名作为参数,返回键名对应的值。对象转字符串后存入,取出后需将其重新转化为对象

3. ```xxxxxStorage.removeItem('key');```

   该方法接受一个键名作为参数,并把该键名从存储中删除。

4. ``` xxxxxStorage.clear()```

   该方法会清空存储中的所有数据。

备注:

  • SessionStorage(会话存储): 存储的内容会随着浏览器/浏览器窗口关闭而消失。
  • LocalStorage(本地存储): 存储的内容,需要手动清除才会消失, 即关闭浏览器后仍然存在。
  • xxxxxStorage.getItem(xxx)如果xxx对应的value获取不到,那么getItem的返回值是null。
  • JSON.parse(null)的结果依然是null。
  • 这两者的API用法一致

2. localStorage的基本使用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>localStorage</h2>
    <button onclick="saveData()">点我保存一个数据</button>
    <button onclick="readData()">点我读取一个数据</button>
    <button onclick="deleteData()">点我删除一个数据</button>
    <button onclick="deleteAllData()">点我清空一个数据</button>

    <script type="text/javascript">
      let p = { name: "张三", age: 18 };

      function saveData() {
        localStorage.setItem("msg", "hello!!!");
        localStorage.setItem("msg2", 666);
        localStorage.setItem("person", JSON.stringify(p));
      }

      function readData() {
        console.log(localStorage.getItem("msg"));
        console.log(localStorage.getItem("msg2"));
        const result = localStorage.getItem("person");
        JSON.parse(result);
        // console.log(localStorage.getItem('msg3'))
      }

      function deleteData() {
        localStorage.removeItem("msg2");
      }

      function deleteAllData() {
        localStorage.clear();
      }
    </script>
  </body>
</html>

3. sessionStorage的基本使用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h2>sessionStorage</h2>
    <button onclick="saveData()">点我保存一个数据</button>
    <button onclick="readData()">点我读取一个数据</button>
    <button onclick="deleteData()">点我删除一个数据</button>
    <button onclick="deleteAllData()">点我清空一个数据</button>

    <script type="text/javascript">
      let p = { name: "张三", age: 18 };

      function saveData() {
        sessionStorage.setItem("msg", "hello!!!");
        sessionStorage.setItem("msg2", 666);
        sessionStorage.setItem("person", JSON.stringify(p));
      }
      function readData() {
        console.log(sessionStorage.getItem("msg"));
        console.log(sessionStorage.getItem("msg2"));

        const result = sessionStorage.getItem("person");
        console.log(JSON.parse(result));

        // console.log(sessionStorage.getItem('msg3'))
      }
      function deleteData() {
        sessionStorage.removeItem("msg2");
      }
      function deleteAllData() {
        sessionStorage.clear();
      }
    </script>
  </body>
</html>

4. local与session的区别

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <a href="./test.html">页面内跳转</a>
    <a href="./test.html" target="_blank">页面外跳转</a>

    <script>
      // 验证一: 关闭网页(复制当前网页地址, 注释掉 1.1, 1.2的代码, 在新的网页打开)
      // 1.1 localStorage的存储保持
      // localStorage.setItem("name", "localStorage");

      // 1.2 sessionStorage的存储会消失
      // sessionStorage.setItem("name", "sessionStorage");

      // 结论一: 关闭当前网页后重写打开, localStorage的存储保持, sessionStorage的存储会消失

      // 验证二: 在页面内实现跳转
      // 1.1 localStorage的存储保持
      // localStorage.setItem("info", "localStorage");

      // 1.2 sessionStorage的存储也会保持
      // sessionStorage.setItem("info", "sessionStorage");

      // 结论二: 在页面内实现跳转, localStorage的存储保持, sessionStorage的存储也会保持

      // 验证三: 在页面外跳转
      // 1.1 localStorage的存储保持
      localStorage.setItem("key", "localStorage");

      // 1.2 sessionStorage的存储不会保留
      sessionStorage.setItem("key", "sessionStorage");

      // 结论三: 在页面外实现跳转, localStorage的存储保持, sessionStorage的存储不会保留
    </script>
  </body>
</html>

test.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>我是测试网页</h1>
    <script>
      console.log(sessionStorage.getItem("info"));
    </script>
  </body>
</html>

5. catch工具封装

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 若不动态地加载或是显式地添加“defer”或“async”属性,script标签将按照在页面上的出现顺序进行加载, 无论是外部脚本还是内联脚本都将这样进行处理。 -->
    <script src="./cache.js"></script>
    <script>
      const user = {
        name: "zgc",
        age: 18,
      };

      const info = "My name is wf";

      localCache.setCache("user", user);
      console.log(localCache.getCache("user"));

      sessionCache.setCache("info", info);
      console.log(sessionCache.getCache("info"));

    </script>
  </body>
</html>

cache.js

class Cache {
  constructor(storage) {
    if (storage === "local") {
      this.storage = localStorage;
    } else if (storage === "session") {
      this.storage = sessionStorage;
    } else {
      confirm("请输入 'local' 或 'session' 作为参数");
    }
  }

  setCache(key, value) {
    if (!value) return confirm("请检查要缓存的数据");
    if (key) this.storage.setItem(key, JSON.stringify(value));
  }

  getCache(key) {
    if (key) return JSON.parse(this.storage.getItem(key));
  }

  removeCache(key) {
    if (key) this.storage.removeItem(key);
  }

  clearCache() {
    this.storage.clear();
  }
}

const localCache = new Cache("local");
const sessionCache = new Cache("session");

8. 正则表达式

正则匹配规则工具大全: c.runoob.com/front-end/8…

1. 正则表达式初体验

  // 正则匹配规则工具大全: https://c.runoob.com/front-end/854/

  // 1. 正则表达式是一种字符串匹配机制, 而已用来搜索, 获取, 代替字符串

  // 2. 在JS中, 正则表达式使用RegExp类来创建, 也可以使用字面量创建
  // 2.1 RegExp类: new RegExp(匹配规则, 修饰符)
  const re1 = new RegExp("hello", "i");

  // 2.2 字面量: /匹配规则/修饰符
  const re2 = /hello/i;

  // 3. 正则表达式的初体验
  // 需求1: 将所有的abc替换成cba(忽略大小写)
  const msg = "abcd00 1Abcd ABc23d ABC456d 789ABCD";

  // 传统字符串方法
  const msg1 = msg.replaceAll("abc", "cba");
  console.log("msg1", msg1); // 失败, 只能替换第一个, 无法忽视大小写

  // 正则表达式
  const msg2 = msg.replaceAll(/abc/gi, "cba");
  console.log("msg2", msg2); // 成功, /abc/:查找abc字符串, i: 忽略大小写, g: 就是匹配全部可匹配结果

  // 需求2: 将所有的数字删除
  // 正则表达式
  const msg3 = msg.replace(/\d+/g, "");
  console.log("msg3", msg3); // 成功, \d 匹配数字类型, +: 一个或者多个, g: 就是匹配全部可匹配结果

image.png

2. 正则表达式的基本使用

  // JS中的正则表达式被用于 RegExp 的 exec, test方法
  // 也包括 字符串 的 match, matchAll, replace, replaceAll, search, split方法
  // i: 忽略大小写, g: 就是匹配全部可匹配结果
  // 如果你不带g,在正则过程中,字符串是从左至右匹配的,如果匹配成功就不再继续向右匹配了,如果你带g,它会重头到尾的把正确匹配的字符串挑选出来

  // 1. 使用正则对象上的实例方法
  // 1.1 test: 检测某一个字符串是否符合正则的规则, 返回值是布尔类型
  /*
    如果正则表达式设置了 g,test() 的执行会改变正则表达式 lastIndex属性。连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串
    即在相同的全局正则表达式实例上多次调用test将会越过之前的匹配
    但如果只执行一次, 加上g与不加g是一样的
 */
  const re1 = /abc/gi;
  const msg1 = "abcd00 1Abcd ABc23d ABC456d 789ABCD";

  const re2 = /^abxc$/gi;
  const msg2 = "aBxc";
  if (re1.test(msg1)) {
    console.log("msg字符串中包含abc, 符合规则");
  }

  if (re2.test(msg2)) {
    console.log("msg字符串以a开头, 以c结尾, 中间包含bx, 符合规则");
  }

  // 1.2 exec: 使用正则执行字符串, 匹配成功返回一个数组, 没有匹配成功则返回null
  // 不加入g,则只返回第一个匹配,无论执行多少次均是如此,如果加入g,则第一次执行也返回第一个匹配,再执行返回第二个匹配,依次类推。
  const res1 = re1.exec(msg1);
  console.log("res1", res1);

  // 2. 使用字符串方法, 传入一个正则表达式
  // 2.1 match: 匹配字符串中符合正则条件的子字符串, 并以数组形式返回, 没有匹配成功则返回null
  // 不加入g,也只是返回第一个匹配,一直执行match方法也总是返回第一个匹配,加入g,则一次返回所有的匹配
  const res2 = msg1.match(re1);
  console.log("res2", res2);

  // 2.2 matchAll: 得到一个迭代器, 里面包含匹配到的字符串的详情
  // 传入的正则修饰符必须加g, 否则报错
  const res3 = msg1.matchAll(re1);
  console.log("res3", res3);
  for (const item of res3) {
    console.log("item", item);
  }

  // 2.3 replace: 替换
  // 表达式不加入g,则只替换第一个匹配,如果加入g,则替换所有匹配
  const res4 = msg1.replace(/abc/gi, "cba");
  console.log("res4", res4);

  // 2.4 replaceAll: 全部替换
  // 传入的正则修饰符必须加g, 否则报错
  const res5 = msg1.replaceAll(/\d+/g, "");
  console.log("res5", res5);

  // 2.5 split: 以正则为分隔符切割字符串, 返回一个数组
  // 加上g与不加g是一样的
  const res6 = msg1.split(re1);
  console.log("res6", res6, msg1);

  // 2.6 search 搜索字符串, 返回匹配到的位置索引, 失败时返回-1
  // 加不加g也是一样的
  const res7 = msg1.search(re1);
  console.log("res7", res7);

image.png

3. 字符类和反向类

  // 1. 字符类: 是一种特殊的符号, 匹配特定集中的任何符号
  const msg = "abc 1234dfg567u io890MM";

  // 1.1  \d(digit) 匹配数字类型(匹配一个从 0 到 9 的字符)
  const re1 = /\d\d/gi;
  const res1 = msg.match(re1);
  console.log("res1", res1);

  // 1.2 \s(space) 匹配空格字符, 包括 空格, 制表符\t, 换行符\n和其他少数稀有字符\v, \f, \r等
  const re2 = /\s/gi;
  const res2 = msg.match(re2);
  console.log("res2", res2);

  // 1.3 \w(word) 匹配单字字符: 数字或下划线_或拉丁字母(a-z,A-Z),
  const re3 = /\w/gi;
  const res3 = msg.match(re3);
  console.log("res3", res3);

  // 1.4 .字符: 与除换行符之外的任何 **一个** 字符匹配
  const re4 = /./gi;
  const res4 = msg.match(re4);
  console.log("res4", res4);

  // 2. 反向类
  // 2.1 \D: 匹配除了 \d 之外的任意字符
  // 2.2 \S  匹配除了 \s 之外的任意字符
  // 2.3 \W  匹配除了 \w 之外的任意字符

image.png

4. 锚点和词边界

  // 1. ^ 和 $ 在正则表达式中被称为锚点
  // ^: 匹配文本开头, $: 匹配文本末尾

  const msg = "My Name Is zgc.";

  // 字符串方法(区分大小写)
  if (msg.startsWith("my")) {
    console.log("以my开头"); // false
  }

  if (msg.endsWith("zgc.")) {
    console.log("以zgc.结尾"); // true
  }

  // 正则匹配
  if (/^my/gi.test(msg)) {
    console.log("以my开头"); // true
  }

  if (/zgc\.$/gi.test(msg)) {
    // \. 使用了转义
    console.log("以zgc.结尾"); // true
  }

  const re1 = /^coder$/; // 必须一个不差
  const re2 = /^cod.*er$/; // 想要在中间插入其他字符

  const info1 = "coder";
  const info2 = "codxxer";

  console.log(re1.test(info1)); // true
  console.log(re1.test(info2)); //false
  console.log(re2.test(info2)); // true

  // 2. 词边界 \b: 检查某一字符串是否处于词边界
  // 即检测某一字符串的一侧是否匹配 \w

  const txt = "My Username Is zgc";

  console.log(/name/gi.test(txt)); // true
  console.log(/\bname\b/gi.test(txt)); // false, txt字符串的name左边是User, 匹配 \w, 不处于词边界
  console.log(/\bname\b/gi.test(msg)); // true, msg字符串的name左右是空格, 不匹配 \w 处于词边界

  // 3. 词边界应用: 匹配所有的时间
  const time = "now time is 11:56, 12:00 eat food, number is 123:456";
  const timeRe = /\b\d\d:\d\d\b/gi;
  console.log(time.match(timeRe));

image.png

5. 转义字符

  // 如果要把特殊的字符作为常规的字符使用, 需要对其进行转义: 在前面加上反斜杠 \
  const re1 = /./gi; // .字符: 与除换行符之外的任何字符匹配
  const re2 = /\./gi; // 进行转义: 匹配 字符串中的 .
  const msg = "abc.def";
  console.log(msg.match(re1)); // ['a', 'b', 'c', '.', 'd', 'e', 'f']
  console.log(msg.match(re2)); // ['.']
  console.log(re1.test(msg)); // true
  console.log(re2.test(msg)); // false

  // 常见需要转义的字符: [],\, ^, $, ., /, |, ?, *, +, ()

  // 应用: 获取所有的js/jsx后缀的文件名
  const fileNames = ["abc.html", "abc,css", "abc.js", "xyz.jsx", "def.js"];

  const re = /\.jsx?$/gi; // 使用变量承载时不要轻易使用 g, 同一实例多次调用lastIndex会改变 

  const arr1 = fileNames.filter((item) => re.test(item));

  const arr2 = fileNames.filter((item) => /\.jsx?$/i.test(item));
  console.log(arr1, arr2);

image.png

6. 集合与范围

  // 1. 集合
  // 有时我们只需要选择多个匹配字符其中之一就可以:
  // 在方括号[...]中的几个字符或者字符类 意味着'搜索给定的字符中的任意一个'

  const str = "13166781999";
  const re = /^1[34567]\d/; // 以 1 开头, 第二位在数字 34567 中选择, 第三位是数字
  console.log(re.test(str)); // true


  // 应用: 手机号匹配正则
  const phoneRe = /^1[3456789]\d{9}$/; // 以 1 开头, 第二位在数字 3456780 中选择, 后面9位都是数字
  // 或者: /^1(3|4|5|6|7|8|9)\d{9}$/
  console.log(phoneRe.test(str)); // true
  /*
  注意:
   1. 正则里面的中括号[]只能匹配其中一个,如果要匹配特定几组字符串的话,那就必须使用小括号 () 与 |
   2. [3456]匹配3或者4或者5或者6,而(3456)只匹配3456,若要跟前面一样可以加或(3|4|5|6)。
   3. [34|56]匹配3或者4或者|或者5或者6.而(34|56)能匹配34或者56。
  */

  // 2. 范围: []也可以包含字符范围 
  // [0-5] 匹配从0到5的数字, [a-z]匹配从a到z的拉丁字母
  // [0-9A-F]: 搜索一个字符, 妈妈在数字0-9或者字母A-F
  // \d: 表示所有数字
  // \w: 与[a-zA-Z0-9]相同

  // 3. 排除范围 [^...]
  // \d : [0-9] 匹配任意一个数字
  // \D: [^0-9] 匹配除了 \d 之外的任意字符

7. 量词

  // 量词: 描述字符串匹配的数量 {n}
  // 确切的数量: {5}, 匹配5个字符
  // 描述范围:{3,7}, 匹配3到7个字符
  // 注意: 这里的,后面千万不要加空格

  // 匹配字符串中3到5个a
  const msg = "abdaajsjaaasaaaahdfaaaaajkgdfaaaaaa";
  console.log(msg.match(/a{3,5}/gi));

  // +: 代表 1个 或者 多个, 相当于 {1,}
  // ?: 代表 0个 或者 1个, 相当于{0,1}, 它使得符号变得可选
  // *: 代表 0个 或者 多个, 相当于{0,}, 这个字符可以不出现或者多次出现
  console.log(msg.match(/a+/gi))
  // 案例: 匹配开始结束标签
  const el = "<div><i>哈哈哈</i></div><h1>呵呵呵</h1>";
  const tagRe = /<\/?[a-z][a-z0-9]*>/gi;

  const res = el.match(tagRe);
  console.log(res);

image.png

8. 贪婪和惰性模式

  const msg = "我最喜欢的三个游戏: <仁王2> 与 <文明VI> 以及 <泰拉瑞亚>";

  // 1. 贪婪:
  /*
   ^ 正则表达式通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符, 这被称为贪婪匹配。
   ^ 比如这个表达式:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。
  */

  // 正常使用量词默认是贪婪模式:

  // *	重复任意次,但尽可能少重复
  // +	重复1次或更多次,但尽可能少重复
  // ?	重复0次或1次,但尽可能少重复
  // {n,m} 重复n到m次,但尽可能少重复
  // {n,}	重复n次以上,但尽可能少重复

  const re1 = /<.+>/gi;
  const res1 = msg.match(re1);
  console.log("res1", res1);

  // 2. 惰性:
  /*
   ^ 懒惰匹配,也就是匹配尽可能少的字符, 在能使整个匹配成功的前提下使用最少的重复;
   ^ a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符);
   ^ 为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?
   ^ 简单地说,因为正则表达式有一条比懒惰/贪婪规则优先级更高的规则,就是:最先开始的匹配拥有最高的优先权;
  */

  // 在量词后加?启用惰性:

  //*?	重复任意次,但尽可能少重复
  // +?	重复1次或更多次,但尽可能少重复
  // ??	重复0次或1次,但尽可能少重复
  // {n,m}?	重复n到m次,但尽可能少重复
  // {n,}?	重复n次以上,但尽可能少重复
  const re2 = /<.+?>/gi;
  const res2 = msg.match(re2);
  console.log("res2", res2);

image.png

9. 捕获组与或操作

// 1. 捕获组: 模式的一部分可以用 () 括起来, 这称为捕获组

  // 1.1 它允许将匹配的一部分作为结果数组中的单独项
  const msg = "我最喜欢的三个游戏: <仁王2> 与 <文明VI> 以及 <泰拉瑞亚>";
  const re1 = /(<)(.+?)>/gi;

  const iterator1 = msg.matchAll(re1);
  for (const item of iterator1) console.log(item);

  // 1.2 如果我们将量词放在括号后,则它将括号视为一个整体
  // 不带括号,模式 go+ 表示 g 字符,其后 o 重复一次或多次。例如 goooo 或 gooooooooo。
  // 括号将字符组合,所以 (go)+ 匹配 go,gogo,gogogo等。
  console.log("Gogoogooo now!".match(/go+/gi));
  console.log("Gogoogooo now!".match(/(go)+/gi));

  // 1.3 捕获组命名(?<name>规则): groups
  const re2 = /(<)(?<zgc>.+?)>/gi;
  const iterator2 = msg.matchAll(re2);
  for (const item of iterator2) console.log(item);

  // 1.4 非捕获组: 使用 ?: 排除组
  // 有时我们需要括号才能正确应用量词, 但我们不希望它们的内容出现在结果中
  const re3 = /(?:<)(.+?)>/gi;
  const iterator3 = msg.matchAll(re3);
  for (const item of iterator3) console.log(item);

  // 2. 或(or) :在正则表达式中使用 | 表示
  // 通常会和捕获组一起使用, 在其中表示多个值
  const info = "abcabcdefcbanhbcaddwewwe";

  const re4 = /(abc|cba|bca){1,}/gi;

  console.log(info.match(re4));

image.png

10. 歌词解析

  // 进行歌词格式转化
  // line: [00:31.160]如果场景里出现一架钢琴 ==>{time: 16280, content: 'あんなに愛した君がいない'}
  const lyricString = "[00:00.000] 作词 : 许嵩\n[00:01.001] 作曲 : 许嵩\n[00:03.010]天青色等烟雨\n[00:05.100]而我在等你\n[00:07.11]这其实不是许嵩的歌\n";

  // 用正则表达式匹配前面的时间[00:31.160], 毫秒可能是两位 00:00:12 = 00:00:120
  const parseExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/;

  function parseLyric(lyricStr) {
    const lyricArr = lyricStr.split("\n");
    console.log("将字符串切割成数组:", lyricArr);
    const lyrics = [];
    for (const line of lyricArr) {
      if (line) {
        const result = parseExp.exec(line);
        console.log("result", result);
        // 如果这一次没有匹配到,则跳过这一次进行下一次匹配
        if (!result) continue;
        const minuteTime = result[1] * 60 * 1000;
        const secondTime = result[2] * 1000;
        const msTime = result[3].length === 3 ? result[3] * 1 : result[3] * 10;
        const time = minuteTime + secondTime + msTime;
        // replace方法:用后面的值取代前面的值,trim:去掉空格
        const content = line.replace(parseExp, "").trim();
        lyrics.push({ time: time, content: content });
      }
    }
    return lyrics;
  }

  const lyricInfo = parseLyric(lyricString)
  console.log(lyricInfo);

image.png

11. 时间格式化

  // 得到时间戳
  const time = new Date().getTime();
  console.log("time", time);

  // 商品信息
  const productInfo = {
    name: "iPhone",
    newPrice: 7999,
    oldPrice: 8999,
    endTime: time,
  };

  // 进行时间格式化
  // yyyy/MM/dd hh:mm:ss
  // yyyy-MM-dd hh:mm:ss
  // ...
  function formatDate(time, formatStr) {
    let date = new Date(time);
    // console.log("date", date);

    const obj = {
      "y+": date.getFullYear(),
      "M+": date.getMonth() + 1,
      "d+": date.getDate(),
      "h+": date.getHours(),
      "m+": date.getMinutes(),
      "s+": date.getSeconds(),
    };

    for (const key in obj) {
      if (new RegExp(key).test(formatStr)) {
        const value = (obj[key] + "").padStart(2, "0");
        formatStr = formatStr.replace(new RegExp(key), value);
      }
    }

    return formatStr;
  }

  console.log(formatDate(productInfo.endTime, "yyyy-MM-dd hh:mm:ss"));
  console.log(formatDate(productInfo.endTime, "yyyy年-MM月-dd日 hh:mm:ss"));
  console.log(formatDate(productInfo.endTime, "hh:mm:ss yyyy/MM/dd "));

image.png

9. HTTP与网络请求

1. 客户端渲染与服务器端渲染

cloud.tencent.com/developer/a…

初识:

截图.png

服务器端渲染:

Snipaste_2023-03-20_12-05-26.png

客户端渲染(前后端分离):

Snipaste_2023-03-20_12-04-31.png

2. XHR发送网络请求

模拟在线api接口

1. AJAX的理解

Ajax 即异步 JavaScript 和 XML; 通俗的将讲:在网页中利用 XMLHttpRequest 对象和服务器进行数据交互的方式,就是Ajax。

2. XHR请求的过程

  1. 创建XMLHttpRequest对象;
  2. 监听onreadystatechange事件,当状态发生改变时浏览器触发事件, 回调绑定的方法调用
  3. 调用open方法传入三个参数 请求方式(GET/POST)、url、同步异步(true/false);
  4. 调用send方法传递参数。

3. XHR的状态

XMLHttpRequest的状态(state): xhr.readyState

事实上,我们在一次网络请求中看到状态发生了很多次变化,这是因为对于一次请求来说包括如下的状态:

标值状态描述
0UNSENT代理已被创建, 但尚未调用open方法, 一般不进行监听
1OPENEDopen方法已经被调用
2HEADERS_RECEIVEDsend方法已被调用, 并且头部和状态已经可获得
3LOADING数据接收中, responseText已包含部分数据
4DONEAJAX请求完成

tips:
这个状态并非是HTTP的响应状态 ,而是记录的XMLHttpRequest对象的状态变化, http响应状态依旧需要通过响应行的status进行获取;

4. XHR的其他事件监听与响应数据类型

事件描述
onloadstart请求开始, 当调用 send() 时,触发单个 loadstart 事件
onprogress监听进度, xhr对象会发生 progress 事件,通常每隔50ms左右触发一次,所以可以使用这个事件给用户反馈请求的进度。如果请求快速完成,他可能不会触发 progress 事件。注意这里的 progress 是下载的进度,xhr2 额外的定义了上传 upload 属性,用来定义上传的相关事件。
onload当事件完成时,触发 load 事件
ontimeout如果请求超时,会触发 timeout 事件(在设置了timeout的情况下)
onerror发生错误, 这些错误(如重定向--域名错误)发生时会触发 error 事件
onabort如果请求中止,会触发 abort 事件
onloadend对于任何具体的请求,浏览器将只会触发 load/abort/timeout/error 事件中的一个, 一旦这些事件触发以后,浏览器将会触发 loadend 事件

5. 响应数据与响应类型的设置

  • 在发送了请求之后, 我们需要获取对应的结果: response属性
    • xhrresponse属性内部有响应的正文内容
    • 返回的类型取决于xhr.responseType的设置
  • 我们可以使用 xhr.responseType 属性来设置响应格式
    • "": 与默认类型 "text" 相同, 响应格式为字符串
    • "text": 响应格式为字符串, responseDOMString对象中的文本
    • "json": response 是一个 JavaScript 对象

6. HTTP响应状态status

  • xhr.readyState 是用于记录xhr对象本身的状态变化, 而非HTTP的网络请求状态
  • 如果希望获取HTTP响应的网络状态, 可以通过xhr.statusxhr.statusText来获取
  //使用Promise封装一个AJAX
  function sendAJAX(url) {
    return new Promise((resolve, reject) => {
      // 1. 创建XMLHttpRequest对象;
      const xhr = new XMLHttpRequest();

      // responseType: 用于告诉浏览器,如何解析服务端返回的数据
      // 将值设置为 json, 则response 是一个 JavaScript 对象。这个对象是通过将接收到的数据类型视为 JSON 解析得到的。
      xhr.responseType = "json";

      // 2. 监听状态的改变(浏览器将回调加入到宏任务之中)
      xhr.onreadystatechange = function () {
        // XMLHttpRequest的状态: console.log("xhr.readyState", xhr);

        if (xhr.readyState === 4) {
          // 标值 0 - 4 对应 XMLHttpRequest的状态 UNSENT - DONE
          // if (xhr.readyState === XMLHttpRequest.DONE) {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(xhr.response);
          } else {
            reject(xhr.status);
          }
        }
      };

      // 3. 配置请求open, xhr.open(method, url, async); 设置请求的方式,请求的路径,同步(false)/异步(true:默认);
      // get请求参数(query)紧跟url: http://jsonplaceholder.typicode.com/posts?name=zgc&age=18
      xhr.open("GET", url, true);

      // 4. 发送请求(浏览器帮助我们发送请求)
      xhr.send();
      // post请求参数放到send内:
      // xhr.setRequestHeader("Content-type", "application/json");
      // xhr.send(JSON.stringify({ name: "zgc", age: 18 }));

      // 同步必须xhr.send有结果后才能继续执行下方代码, 异步则不必等待
      // 所以实际开发中我们使用异步请求(默认)
      // console.log("++++++++++++++++++");
    });
  }

  sendAJAX("http://jsonplaceholder.typicode.com/posts").then(
    (value) => {
      console.log("value", typeof value, value);
    },
    (reason) => {
      console.warn("reason", reason);
    }
  );

3. AJAX网络请求封装

  function ajax({
    url,
    method = "GET",
    data = {},
    params,
    timeout = 10000,
    success,
    failure,
  } = {}) {
    const xhr = new XMLHttpRequest();

    const promise = new Promise((resolve, reject) => {
      xhr.responseType = "json";
      xhr.timeout = timeout;
      // xhr.onabort = function () {
      //   console.log("abort");
      // };

      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          if (xhr.status >= 200 && xhr.status < 300) {
            // success && success(xhr.response); // 回调函数方法得到结果
            resolve(xhr.response);
          } else {
            // failure && failure({ status: xhr.status, message: xhr.statusText }); // 回调函数方法得到结果
            reject({ status: xhr.status, message: xhr.statusText });
          }
        }
      };

      if (method.toUpperCase() === "GET") {
        if (params) {
          // 当传入params类型参数时, 将其拼接成query参数
          const queryArr = [];
          for (const key in params) {
            queryArr.push(`${key}=${params[key]}`);
          }
          url = url + "?" + queryArr.join("&");
        }
        // console.log("url", url);
        xhr.open(method, url);
        xhr.send();
      }

      if (method.toUpperCase() === "POST") {
        xhr.open(method, url);
        xhr.setRequestHeader("Content-type", "application/json");
        xhr.send(JSON.stringify(data));
      }
    });

    // 可以将xhr对象返回给外界进行操作, 如终止(abort)请求等
    promise.xhr = xhr;
    return promise;
  }

  const result = ajax({
    // get请求:
    method: "GET",
    // url: "http://jsonplaceholder.typicode.com/posts?userId=5", // query参数
    url: "http://jsonplaceholder.typicode.com/posts", // params参数
    params: {
      userId: 5,
    },
    timeout: 10000,
    // post请求:
    // method: "POST",
    // url: "http://jsonplaceholder.typicode.com/posts", // 得到一个id
    // data: {
    //   name: "wf",
    //   age: 18,
    // },

    // 成功或者失败的回调, 在不使用Promise的情况下得到结果
    // success: function (data) {
    //   console.log("data", data);
    // },
    // failure: function (err) {
    //   console.log("err", err);
    // },
  });

  result
    .then((res) => {
      console.log("res", res);
    })
    .catch((err) => {
      console.log("err", err); // {status: 404, message: 'Not Found'}
    });

  // 终止请求执行
  // result.xhr.abort();

4. fetch函数的使用

 // 1. fetch的基本使用
  fetch("http://jsonplaceholder.typicode.com/posts?userId=5")
    .then((res) => {
      console.log(res); // 返回的响应信息
      // text/json()方法属于fetchApi的一部分,它返回一个Promise实例对象 ,再用.then()才可以获取需要数据
      return res.json();
    })
    .then((data) => {
      // 注意这里data得到的才是最终数据
      console.log("data", data);
    })
    .catch((err) => {});

  // 2. POST请求 & 搭配await
  const params = {
    name: "zgc",
    age: 18,
  };

  async function getData() {
    const res = await fetch("http://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(params),
    });
    const data = await res.json();
    console.log("data", data);
  }

  // async function getData() {
  //   const res = await fetch("http://jsonplaceholder.typicode.com/posts", {
  //     method: "POST",
  //     headers: {
  //       "Content-Type": "application/x-www-form-urlencoded",
  //     },
  //     body: "name=zgc&age=18",
  //   });
  //   const data = await res.json();
  //   console.log("data", data);
  // }
  getData();

5. XHR上传文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="file" class="file" />
    <button class="upload">上传文件</button>
    <script>
      const uploadBtn = document.querySelector(".upload");

      uploadBtn.onclick = function (e) {
        // console.log("e", e);
        const xhr = new XMLHttpRequest();
        xhr.responseType = "json";

        xhr.onload = function () {
          if (xhr.status >= 200 && xhr.status < 300) {
            console.log("xhr", xhr.response);
          } else {
            console.log({ status: xhr.status, message: xhr.statusText });
          }
        };

        xhr.open("post", "http://123.207.32.32:1888/02_param/upload");

        const fileEL = document.querySelector(".file");
        const file = fileEL.files[0];

        const formData = new FormData();
        formData.append("avatar", file);

        xhr.send(formData);
      };
    </script>
  </body>
</html>

6. fetch上传文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <input type="file" class="file" />
    <button class="upload">上传文件</button>
    <script>
      const uploadBtn = document.querySelector(".upload");

      uploadBtn.onclick = async function (e) {
        const fileEL = document.querySelector(".file");
        const file = fileEL.files[0];

        const formData = new FormData();
        formData.append("avatar", file);

        const res = await fetch("http://123.207.32.32:1888/02_param/upload", {
          method: "post",
          body: formData,
        });

        const data = await res.json();
        console.log("data", data);
      };
    </script>
  </body>
</html>