Skip to content

JS 重点知识复习

1.class 类

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>1_类的基本知识</title>
  </head>
  <body>
    <script type="text/javascript">
      //创建一个Person类
      class Person {
        //构造器方法:可以不写
        constructor(name, age) {
          //构造器要有属性的形参
          //构造器中的this是谁?—— 类的实例对象
          this.name = name;
          this.age = age;
        }
        //一般方法:不用写 function 关键字
        speak() {
          //speak方法放在了哪里?—— 类的原型对象上,供实例使用
          //通过Person实例调用speak时,speak中的this就是Person实例(在不主动绑定 this 的情况下,谁调用的this 就是谁)
          console.log(`我叫${this.name},我年龄是${this.age}`);
        }
      }

      //创建一个Student类,继承于Person类
      class Student extends Person {
        constructor(name, age, grade) {
          //这里如果没有 grade 这个单独的属性,那么可以不写构造器,子类会默认调用父类的构造器
          super(name, age); //如果子类写了构造器,那么就必须写 super,super 就是调用父类的构造器用的
          this.grade = grade;
          this.school = "尚硅谷"; //固定的属性,可以直接赋值
        }
        //重写从父类继承过来的方法
        speak() {
          console.log(
            `我叫${this.name},我年龄是${this.age},我读的是${this.grade}年级`
          );
          this.study();
        }
        study() {
          //study方法放在了哪里?——类的原型对象上,供实例使用
          //通过Student实例调用study时,study中的this就是Student实例(在不主动绑定 this 的情况下,谁调用的this 就是谁)
          console.log("我很努力的学习");
        }
      }
      const s1 = new Student("张三", 18, 3);
      console.log(s1);
      class Car {
        constructor(name, price) {
          this.name = name;
          this.price = price;
          // this.wheel = 4 //这个属性不需要外部传进来,就没必要写在构造器里面
        }
        //类中可以直接写赋值语句:意思就是添加一个 a 属性,值为 1
        //如下代码的含义是:给Car的实例对象添加一个属性,名为a,值为1,也可以写在上面的构造器中(但是这些属性是固定的,不用外部传进来的,那么就可以直接写在类里面)——> 这也就产生了React 中的 state 的简写方式!
        a = 1;
        wheel = 4; //轮子
        static demo = 100; //给 Car 类自身加一个属性 demo(静态属性,不是给实例对象的) ——> props 的简写方式
      }
      //Car.demo = 100; 也可以
      const c1 = new Car("奔驰c63", 199);
      console.log(c1);
      console.log(Car.demo);
    </script>
  </body>
</html>

函数调用的五个方法:自身直接运行调用,对象调用,call 调用,apply 调用,bind 绑定(call 和 apply 都是直接调用,而 bind 是绑定对象并返回一个新的函数,没有进行调用)

s1 对象的输出效果:可以看到 speak 和 study 方法在 Stundent 类(Student 构造函数)的 prototype 原型对象上面(speak 方法是从 Person 类继承并重写的)

满足等式:s1.__proto__ = Student.prototype

s1 中没有 speak 和 student 方法,会去 s1.__proto__中查找,也就是说对象的隐式原型指向类的构造函数的显示原型对象

第一行的意思:这个是一个 Student 类的对象

image-20230618003606098

本质上全部的层次结构(原型链)应该是这样的:

image-20230618004018469

s1.__proto__.__proto__ = Person.prototype

因为 Student 构造函数的原型对象本质上就是 Person 构造函数 new 出来的,所以形成了这种原型链

总结:

1.类中的构造器不是必须要写的,要对实例进行一些初始化的操作,如添加指定属性时才写。

2.如果 A 类继承了 B 类,且 A 类中写了构造器,那么 A 类构造器中的 super 是必须要调用的。

3.类中所定义的方法,都放在了类的原型对象上,供实例和子类的实例去使用。

注意点:类字段的使用(注意和类的方法区分,类字段并不是放在原型上的)

在 constructor()外面声明变量,这种语法(直接在类中声明属性并赋值)是在 JavaScript 中的实验性特性,称为类字段(Class Fields)

这些变量和 constructor 定义的一样都是实例属性(可以通过对象访问),并且可以在类的各个方法中用 this 调用来共享使用(可以说相当于全局变量)。

下面是基本的用法和测试:

js
class MyClass {
  name = "xiaolan"; //1.可以当作构造函数中属性的一个提前声明和默认赋值
  constructor(name) {
    if (name) this.name = name;
    // this.myProperty = 10; //同样可以访问,并修改成功
  }

  myProperty = 0; //2.可以当作全局变量,在任意方法中访问
  /**
   * 注意:本质上类字段和constructor定义的属性都是实例属性,每个对象自己维护,不共享,并且可以在类的任意方法中通过this进行访问!
   */

  getMyProperty() {
    //类里面的方法会被放到原型上面,所有对象进行共享!
    this.myProperty += 1;
    return this.myProperty + this.name;
  }
}

const instance1 = new MyClass("hao");
const instance2 = new MyClass(); //也可以不传递,因为我们的name有默认声明

console.log(instance1.getMyProperty()); // Output: '1hao'
console.log(instance2.getMyProperty()); // Output: '1xiaolan'

myProperty 将成为 MyClass 类的每个实例的属性,并且每个实例都将有自己的 myProperty!

2.原生事件绑定

三种方法:

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <button id="btn1">按钮1</button>
    <button id="btn2">按钮2</button>
    <button onclick="demo()">按钮3</button>

    <script type="text/javascript">
      const btn1 = document.getElementById("btn1");
      btn1.addEventListener("click", () => {
        //用事件监听器
        alert("按钮1被点击了");
      });

      const btn2 = document.getElementById("btn2");
      btn2.onclick = () => {
        //直接写事件名也可以
        alert("按钮2被点击了");
      };

      function demo() {
        //直接在 dom元素上用事件绑定 js 中的回调函数 ——> 最简洁方便
        alert("按钮3被点击了");
      }
    </script>
  </body>
</html>

3.类中方法的 this 指向

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <script type="text/javascript">
      class Person {
        constructor(name, age) {
          this.name = name;
          this.age = age;
        }
        study() {
          //study方法放在了哪里?——类的原型对象上,供实例使用
          //this隐式绑定:通过Person实例调用study时,study中的this就是Person实例对象,这是默认的规则!
          console.log(this);
        }
      }

      const p1 = new Person("tom", 18);
      p1.study(); //通过实例调用study方法
      const x = p1.study; //把方法赋值给变量 x
      x(); //这里就相当于是函数直接自调用,所以一定是 undefined
      //为什么是 undefined 不是 window 呢,这里也没用 babel 呀?
      //因为类中定义的方法,是在局部默认开启严格模式的,所以它的 this 默认就是 undefined
      //相当于:
      /*
      	study(){
					'use strict'
					console.log(this);
				}
      */
      //注:局部也是可以开启严格模式的
    </script>
  </body>
</html>

内存分析:左边是栈,右边是堆

函数在堆里面,函数的 study 属性和 x 变量都对其进行了引用

image-20230618163442416

为什么通过对象实例调用方法,方法里面的 this 就是那个实例呢?

在 JavaScript 中,当你通过对象实例调用一个方法时,方法内部的this关键字会指向调用该方法的对象实例。这是 JavaScript 中的一种常用规则,通常称为"this 绑定"或"this 机制"。

这也是 this 绑定规则中的隐式绑定当一个函数作为对象的属性被调用时,this会绑定到该对象

这种行为有助于让方法能够访问和操作特定实例的属性和方法。当你调用对象实例上的方法时,JavaScript 会自动将this绑定到该实例,以便在方法内部可以访问该实例的属性和方法。

示例:

js
const person = {
  firstName: "John",
  lastName: "Doe",
  getFullName: function () {
    return this.firstName + " " + this.lastName;
  },
};

console.log(person.getFullName()); // Output: 'John Doe'

在这个示例中,当调用person.getFullName()时,this指向了person对象,因此方法内部可以访问firstNamelastName属性。

一句话总结:普通函数就是,谁调用我,我的 this 就指向谁!!!

4.展开运算符…

... 这个符号恐怕都不陌生,这个是一个 ES6 的展开运算符,主要用来展开数组

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <script type="text/javascript">
      let arr1 = [1, 3, 5, 7, 9];
      let arr2 = [2, 4, 6, 8, 10];
      console.log(...arr1); //展开一个数组
      let arr3 = [...arr1, ...arr2]; //连接数组

      //在函数中使用:不确定参数个数(默认会将传进来的所有参数合成一个列表),将得到的所有参数组成的数组进行展开
      function sum(...numbers) {
        return numbers.reduce((preValue, currentValue) => {
          return preValue + currentValue;
        });
      }
      console.log(sum(1, 2, 3, 4));

      //构造字面量对象时使用展开语法:对象复制
      let person = { name: "tom", age: 18 };
      //console.log(...person); //报错,展开运算符不能直接展开一个对象
      let person2 = { ...person }; //如果外侧包裹了一个花括号,里面用三个点,这种写法可以,将 person 对象进行复制,成为 person2 ——> 这是 ES9 的对象扩展运算符,但是这样只能拷贝一层(深拷贝)
      person.name = "jerry";
      console.log(person2);
      console.log(person);

      //对象合并:复制对象并修改一部分属性(这里写了 name,就会把原来的name 给覆盖掉),并增加新的属性
      let person3 = { ...person, name: "jack", address: "地球" };
      console.log(person3);
    </script>
  </body>
</html>

5.对象相关的知识

js
let a = "name";

let obj = {}; // 想让 obj 变为{name:'tom'}
obj[a] = "tom"; //通过读取 a 变量作为属性名
console.log(obj);

6.函数的柯里化

构建一个函数,多次接收参数,最后再统一处理参数

js
/* function sum(a,b,c){
			return a+b+c
	 }
	 const result = sum(1,2,3)
*/

function sum(a) {
  return (b) => {
    return (c) => {
      return a + b + c;
    };
  };
}
const result = sum(1)(2)(3);
console.log(result);

7.对象解构赋值

js
let obj = { a: { b: { c: 1 } } };
//一般写法:const value = obj.a.b.c;
//console.log(value);
//解构取值的写法:只要这个属性是一个对象,就可以连续地写下去
const {
  a: {
    b: { c },
  },
} = obj;
console.log(c);

注意:解构赋值可以进行重命名

js
const {
  a: {
    b: { c: cValue },
  },
} = obj;

解构赋值取出属性 c,并重命名为 cValue!

8.async 和 await

async

该关键字是放在函数之前的,使得函数成为一个异步函数,他最大的特点就是将函数封装成 Promise,也就是被他修饰的函数的返回值都是 Promise 对象。而这个 Promise 对象的状态则是由函数执行的返回值决定的。

如果返回的是一个非 promise 对象(同样会被包裹为 promise),该函数将返回一个成功的 Promise,成功的值则是返回的值;

如果返回的是一个 promise 对象,则该函数返回的就是该 promise 对应的状态。

——> 这个特点其实和 await 没什么关系,和.then的返回值包装规则比较类似。

await

await 右边是一个表达式,如果该表达式(异步函数)返回的是一个 Promise 对象,则左边接收的结果就是该 Promise 对象成功的结果,如果该 Promise 对象失败了,就必须使用 try..catch 来捕获。如果该表达式返回的不是一个 promise 对象,则左边接收的就是该表达式的返回值。

当 await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面(async 关键字包裹的函数里面)才起作用。它可以放在任何异步的、基于 promise 的函数之前。它会暂停异步函数的代码在该行上,直到 promise 完成,然后返回结果值(将异步执行变为了同步)。

在暂停的同时,在外层异步函数所处在的外层同步代码中其他正在等待执行的代码就有机会执行了。

也就是下面这个说法:

当使用 await 表达式时,异步函数会在遇到异步操作时暂停执行,并允许其他代码继续执行

——> 这句话的解释?

其实也就是我们常说的,async+await 的组合让异步代码看起来像同步代码!但是实际上本来就是要用同步套异步,这里我们变成了异步套异步,但是外层的异步让我们用 await 变成同步了,所以本质上异步同步没有区别,只是写法会更加优雅易懂!

作用:这样可以实现非阻塞的异步操作,让程序能够同时处理多个任务,提高效率和响应性。

以下是一个示例,展示了使用 await 实现非阻塞异步操作的情况:

js
async function fetchData() {
  console.log("Start fetching data...");
  const response = await fetch("https://api.example.com/data"); // 等待异步网络请求完成
  const data = await response.json(); // 等待将响应转换为 JSON
  console.log("Data fetched:", data);
}

console.log("Before calling fetchData");
fetchData(); //因为fetchData这个异步函数里面有 await 关键字,才让这个异步函数可以不被等待就向下执行
console.log("After calling fetchData");

在上面的示例中,我们定义了一个异步函数 fetchData,它使用 await 表达式来等待异步网络请求的完成和响应的解析。

在调用 fetchData 函数之前和之后,我们分别输出了两行日志。这样可以观察到异步函数的执行是非阻塞的,它在遇到 await 表达式时会暂停执行,让其他代码继续执行。

当我们运行上述代码时,输出的日志顺序如下:

kotlin
Before calling fetchData
Start fetching data...
After calling fetchData //继续执行了同步代码
Data fetched: [data] //异步网络请求完成

可以看到,调用 fetchData 函数时,它会立即执行到第一个 await 表达式处,并暂停执行。然后,控制权返回给调用者,继续执行后续的代码,即输出 "After calling fetchData"。

在后台,异步网络请求在执行,直到完成。一旦网络请求完成,异步函数会恢复执行,继续执行后续的代码,并输出 "Data fetched: [data]"。

关于 await 的总结:

如果在异步函数中没有使用 await 表达式,调用者会等待异步函数执行完毕,但它不需要等待具体的异步操作完成,而是等待整个异步函数执行完毕后获取最终的返回值。

作用:使用了await表达式,那么调用者就不需要等待异步函数执行完毕(让外层的异步变为不会阻塞的函数),而异步函数内部需要等待具体的异步操作完成(把内部的异步变为了同步)。

其实只要使用了 await,就至少有两层异步函数,被 async 包裹的函数本身就是一个大的异步函数(async 对于 await 来说的主要价值就是会把函数变为异步的),而 await 也是加在一个返回 promise 对象的异步函数上的(promise 本身就是用于将异步操作包装成一个对象,异步操作可以更加可控和易于处理),而 await 的主要作用就是把异步变为同步(以同步的方式编写异步代码,不用再.then 了),但是要发挥 await 的这个作用的代价就是要把 await 放在一个 async 异步函数里面,也就是 async 异步套 await 异步

异步变同步的主要作用是简化 promise 状态流转(.then)的编写复杂度和样式;让异步代码看起来是同步的(实际上也变成同步的了),更易于逻辑的构建与编写(有些操作需要特定的先天条件,如 echarts 渲染需要数据)

总之,通过使用 "async" 和 "await",我们可以以同步的方式编写异步代码(直接把异步函数的 promise 当做同步的来操作),使其更易于阅读和理解。

举个 async+await 的应用例子:

jsx
f1 = () => {
  return new Promise((resolve, reject) => {
    // resolve(1);
    reject("错误");
  });
};

async function test() {
  try {
    const p = await f1();
    console.log(p);
  } catch (error) {
    console.error(error);
  }
}
test();

promise 的注意事项:

Promise 是 JavaScript 提供的一种用于处理异步操作的机制。它可以用于将异步操作包装成一个对象,使得异步操作可以更加可控和易于处理。

Promise 对象表示一个异步操作的最终完成或失败,并可以返回异步操作的结果或错误信息

但是new Promise((resolve, reject) => { ... }) 中的代码不一定是异步函数,但通常用于处理异步操作。也就是说 Promise 也不是一定用来处理异步操作的!

new Promise 的构造函数中,我们可以执行任意代码逻辑,包括同步操作和异步操作。一般情况下,我们在 Promise 构造函数中执行的是异步操作,例如进行网络请求、读取文件、定时器等等。

使用 Promise 的主要目的是为了处理异步操作的结果,并将其封装成一个可处理的对象,以便后续进行链式操作或使用 .then.catch 方法处理结果和错误。

9. .then 的包装规则以及链式调用法则

在 Promise 的链式调用中,.then() 方法的返回值会影响后续的 Promise 状态和值。下面是一些关于 .then() 返回值的规则:

  1. 如果 .then() 方法中的回调函数返回一个值(非 Promise 对象),则返回的 Promise 对象将会处于已解决(fulfilled)状态,并且该值将作为解决值传递给下一个 .then() 方法的回调函数。例如:

    js
    someAsyncFunction()
      .then((result) => {
        return result * 2; // 返回一个值
      })
      .then((finalResult) => {
        console.log(finalResult); // 使用上一个回调函数的返回值
      });
  2. 如果 .then() 方法中的回调函数返回一个 Promise 对象,则返回的 Promise 对象的状态和值将取决于该返回的 Promise 对象。如果返回的 Promise 对象被解决(fulfilled),则后续的 .then() 方法将接收到解决值;如果返回的 Promise 对象被拒绝(rejected),则后续的 .catch() 或带有拒绝回调的 .then() 方法将被触发。例如:

    js
    someAsyncFunction()
      .then((result) => {
        return new Promise((resolve, reject) => {
          resolve(result * 2); // 返回一个 Promise 对象
        });
      })
      .then((finalResult) => {
        console.log(finalResult); // 使用上一个回调函数返回的 Promise 的解决值
      });

    注意:使用 resolve 方法将会返回一个已解决(fulfilled)状态的 Promise 对象,而使用 reject 方法将会返回一个已拒绝(rejected)状态的 Promise 对象。

    • resolve("Promise resolved");

    • reject(new Error("Promise rejected"));

  3. 如果 .then() 方法中的回调函数抛出一个异常,则返回的 Promise 对象将会处于被拒绝(rejected)状态,并且异常将作为拒绝理由传递给后续的 .catch() 或带有拒绝回调的 .then() 方法。例如:

    js
    someAsyncFunction()
      .then((result) => {
        throw new Error("Something went wrong"); // 抛出一个异常
      })
      .catch((error) => {
        console.log(error); // 捕获上一个回调函数抛出的异常
      });

需要注意的是,.then() 方法返回的 Promise 对象是一个全新的 Promise 对象,它与前一个 Promise 对象是不同的。因此,在 Promise 链中可以进行多个 .then() 方法的串联调用,每个 .then() 方法都可以进行值的转换、异步操作或错误处理。

如果没有返回值呢,也没有使用 resolve 和 reject?还会继续链式调用吗?

如果前一个.then()没有返回值(即没有明确的 return 语句),后续的.then() 仍然会被调用,但它们会接收到前一个.then()中的 undefined 作为参数

考虑以下示例:

js
promiseFunction()
  .then((result) => {
    console.log("First .then():", result); // 输出结果:First .then(): undefined
  })
  .then((result) => {
    console.log("Second .then():", result); // 输出结果:Second .then(): undefined
  });

在这个示例中,如果 promiseFunction() 返回一个 Promise,并且在第一个.then()中没有明确的返回值,那么第二个.then() 会被调用,但它会收到 undefined 作为参数。如果在第一个.then()中有返回值,那么第二个.then() 将会收到这个返回值作为参数。

那么 promise 的状态会是什么?

如果在 .then() 中没有返回值(或者显式地返回了 undefined),Promise 的状态仍然会保持为已解决(fulfilled)状态,但是 Promise 内部的值会变成 undefined

这是因为在 JavaScript 的 Promise 链中,如果 .then().catch() 中没有返回任何值,那么会隐式地返回一个 Promise,该 Promise 的状态为已解决(fulfilled)状态,并且值为 undefined

总结:如果在 Promise 链的 .then() 中没有明确的返回值,Promise 的状态仍然是已解决(fulfilled),但内部的值会变成 undefined

为什么非要隐式地返回一个 fulfilled 状态的 undefined 呢?

这是因为,只有当 Promise 对象被解决(fulfilled)时,才会触发该 Promise 对象对应的 .then() 方法中的回调函数,Promise 的每个状态对应着不同的处理方法(.then、.catch),每个状态是通过特定的状态流转方法(resolve、reject)得来的,有着严格的对应关系!

如果连续的链式.then 时,其中有一个.then 在执行过程中发生了错误,那么后面的.then 还会 执行吗?

如果在连续的链式 .then() 方法中的某个回调函数发生了错误(抛出异常),则后续的 .then() 方法会被跳过,直接进入最近的 .catch() 方法(如果有的话)来处理错误。

这是因为在 Promise 链式调用中,每个 .then() 方法返回的是一个新的 Promise 对象,用于处理前一个 最近的状态符合的 Promise 对象的解决值。如果前一个 .then() 方法中的回调函数发生错误,它会导致该 Promise 对象变为已拒绝状态,而后续的 .then() 方法会被跳过。

直接 return 和使用resolve 方法产生的效果是基本一样的

在某种程度上可以说返回一个值和使用 resolve 方法的效果是类似的,因为它们都会将一个值作为 Promise 对象的解决值进行传递,并且都是 fulfilled 状态的。

在大多数情况下,使用隐式的 return 语句是更常见和推荐的方式,因为它更简洁和直观。而手动创建 Promise 对象并使用 resolve 方法则更适用于需要进行更复杂操作或需要手动处理异步操作的情况。

注意:即便没有任何 return,也会隐式地返回一个 fulfilled 状态的值为 undefined 的 Promise 对象!

一个使用 redux-promise 中间件解决异步 action 时的例子:

js
export const incrementAsyncAction = (data, delay) => {
  return axios({ url: xxx, method: "get", headers: {} }).then((res) => {
    return incrementAction(res.data); //内部返回一个同步 action 对象,会被封装为一个 Promise 对象,并且状态为fulfilled(已解决),还可以继续.then
    //如果使用 resolve方法 那么同样返回的是一个fulfilled的Promise对象
  });
};

注意:上述 incrementAsyncAction 方法返回的是一个 fulfilled 的 Promise 对象

Promise 链式调用是基于 Promise 对象的状态和值,而不是依赖于回调函数的返回值

可以这样理解:在 Promise 链式调用中,每个 .then() 方法接收一个回调函数,而返回的是一个新的 Promise 对象,该对象的状态和值取决于前一个 Promise 对象的状态和前一个回调函数的返回值。

回调函数的返回值在 Promise 链中的作用是影响下一个 .then() 方法的输入值(即解决值)以及 Promise 的状态。

注意,当 Promise 对象被解决(fulfilled)时,会触发该 Promise 对象对应的 .then() 方法中的回调函数。而回调函数的返回值(无论有无)并不会直接影响 Promise 对象的状态,无非是不会走不会执行.then()方法罢了!

要正确理解 Promise 链式调用,需要将重点放在 Promise 对象的状态和值的变化上,而回调函数的返回值仅仅是影响下一个 Promise 对象的输入值(解决值)而已。

总结:resolve 和 reject 方法用于状态的流转,then 和 catch 方法用于状态的接收!

.then 的第二个参数,以及.catch 的深入理解

.catch 后面如果还有.then 也是可以被执行的

js
// 函数A只返回一个reject异常
function A() {
  return Promise.reject(new Error(Math.random()));
}

// 这样会先执行catch,然后执行后面的then
A()
  .then(() => console.log("第一个then"))
  .catch((e) => console.error(e))
  .then(() => console.log("第二个then"));

原因:catch 也会隐式地返回一个值为 undefined 的 promise(fulfilled 状态的)所以可以继续 then。 从语义上也很好理解,都被捕获处理过了就不是 error 了,所以可以继续!

如果我想如果报错就捕获 catch,并且不执行后面的 then 的话要怎么做呢?

1.改变顺序使用.then.catch,then 里面要是报错也会被 catch 捕获,把 catch 放到最后

2.使用 try-catch,任意 Promise 中的 catch 异常,都会阻断后面代码的执行,并跳转到 try 中的 catch,被捕获

js
try {
  A()
    .then(() => console.log("第一个then"))
    .then(() => console.log("第二个then"));
} catch (e) {
  console.error(e);
}

如果 catch 后面有 catch,会执行吗?

可能会执行,是有条件的:需要显示地 reject 才可以,因为默认返回的是 fulfilled 状态的对象,只能被 then 捕获!

js
function B() {
  return Promise.resolve("success!");
}
B()
  .then((result) => {
    console.log(result); // 打印前一个回调函数的返回值
  })
  .catch((error) => {
    return "catch Error"; //返回的是值为undefined的fulfilled状态的Promise对象,只能被then捕获!
  })
  .catch((error) => console.log(error.message)); //不会被执行!
image-20230813010532331

10.闭包

闭包(Closure)是一个非常重要且常见的编程概念,它指的是一个函数以及它所能访问的外部变量的组合。

在 JavaScript 中,每当一个函数被创建时,它都会创建一个作用域(Scope),在这个作用域内部,函数可以访问自己的局部变量以及在外部作用域中定义的变量。

当一个函数内部定义了另一个函数,并且这个内部函数引用了外部函数的变量时,就形成了闭包。闭包使得内部函数可以继续访问外部函数的变量,即使外部函数已经执行结束,其作用域也不会被销毁,因为内部函数仍然保持对这些变量的引用

闭包的典型应用场景包括:

  1. 保留变量状态: 当一个函数返回另一个函数时,返回的函数形成了闭包,它可以记住外部函数的局部变量的状态。——> 防止外部变量数据已经被篡改了!
jsx
function counter() {
  let count = 0; //这里的 count 变量会一直为里面的闭包函数保留

  return function () {
    //返回一个函数,操作外部的 count 变量
    return ++count;
  };
}

const increment = counter();
console.log(increment()); // 1
console.log(increment()); // 2
  1. 对象的模块化封装: 通过闭包可以实现模块化封装,将对象的一些私有变量和方法隐藏在闭包的作用域中,不暴露在全局作用域下。
js
const myModule = (function () {
  let privateVar = 10;

  function privateFunc() {
    console.log("Private function");
  }

  return {
    publicVar: 20,
    publicFunc: function () {
      // 可以使用自己的私有变量方法
      console.log("Public function");
    },
  };
})(); //myModule是一个自执行函数,构建出一个对象

console.log(myModule.publicVar); // 20
myModule.publicFunc(); // "Public function"
console.log(myModule.privateVar); // undefined //访问不到
myModule.privateFunc(); // TypeError: myModule.privateFunc is not a function //访问不到
  1. 定时器和事件监听: 在使用定时器或事件监听时,如果回调函数形成了闭包,那么在回调函数中可以访问到外部作用域中的变量。
js
function doSomethingLater() {
  let message = "Hello";

  setTimeout(function () {
    console.log(message); // "Hello",调用了外部的 message 变量
  }, 1000);
}

需要注意的是,由于闭包会引用外部函数的变量,可能导致一些内存泄漏问题,特别是在长时间运行的情况下。在使用闭包时,需要注意管理内存,确保不再需要的引用能够被正确释放,以避免造成资源浪费和性能问题。

  1. **高阶函数+函数柯里化的时候:**根据 type 类型给 state 中的数据设置值,值为 dom 对象的 value 属性
js
saveFormData = (dataType) => {
  return (event) => {
    //返回一个函数 ——> 高阶函数
    this.setState({ [dataType]: event.target.value }); //参数是分批次获取的,最后统一处理 ——> 函数柯里化
    //里面用了外面的 dataType 变量参数 ——> 闭包
  };
};