第三章 ECMAScript6 进阶


(如果没有了解前面的知识可以去我主页看看 第一至二章的内容来学习)前两章我们学习了 ES6 的基础语法和对象的扩展内容,事实上 ES6 还对一些语言的高级特性进行了支持,比如面向对象编程、异步编程、模块化编程,这章继续介绍这些高级编程特性

3.1 面向对象编程

  无论是基于Object对象方式还是使用字面量的方式创建单个对象,都有一个明显的缺点:在遇见批量创建对象时,会产生大量重复代码。

3.1.1 构造函数

  在ES6(ECMAScript 2015)中,构造函数是通过class关键字定义的。构造函数用于创建和初始化对象。以下是一个简单的示例,展示了如何定义和使用ES6中的构造函数。

// 定义一个类,名为Person
class Person {
  // 构造函数,用于初始化对象
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
 
  // 一个方法,用于打印Person的信息
  printInfo() {
    console.log(`Name: ${this.name}, Age: ${this.age}`);
  }
}
 
// 使用构造函数创建Person对象
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
 
// 调用对象的方法
person1.printInfo(); // 输出: Name: Alice, Age: 30
person2.printInfo(); // 输出: Name: Bob, Age: 25

在这个示例中:

  1. 我们定义了一个名为Person的类。
  2. constructor方法是类的构造函数,用于初始化新创建的对象。它接收两个参数:name和age,并将它们赋值给对象的属性。
  3. printInfo是一个实例方法,用于打印对象的信息。
  4. 我们使用new关键字创建了Person类的两个实例:person1和person2。
  5. 最后,我们调用了printInfo方法来打印每个实例的信息。

你也可以在类中定义静态方法,静态方法属于类本身,而不是类的实例。以下是一个包含静态方法的示例:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
 
  printInfo() {
    console.log(`Name: ${this.name}, Age: ${this.age}`);
  }
 
  // 静态方法,属于类本身
  static species() {
    return 'Homo sapiens';
  }
}
 
const person = new Person('Alice', 30);
 
// 调用实例方法
person.printInfo(); // 输出: Name: Alice, Age: 30
 
// 调用静态方法
console.log(Person.species()); // 输出: Homo sapiens

在这个示例中,species是一个静态方法,它属于Person类本身,而不是Person的实例。因此,我们通过类名Person来调用它,而不是通过实例。

3.1.2 基于原型链继承

  在ES6(ECMAScript 2015)中,虽然引入了类(class)语法,但原型链继承的机制仍然是其背后的核心原理。下面是一个基于原型链继承的简单示例,展示了如何使用ES6的类语法来实现继承。

// 定义一个父类(基类)
class Animal {
  constructor(name) {
    this.name = name;
  }
 
  // 父类的方法
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}
 
// 定义一个子类,继承自父类 Animal
class Dog extends Animal {
  constructor(name, breed) {
    // 调用父类的构造函数
    super(name);
    this.breed = breed;
  }
 
  // 子类的方法
  bark() {
    console.log(`${this.name} barks.`);
  }
 
  // 重写父类的方法
  speak() {
    console.log(`${this.name} says woof!`);
  }
}
 
// 创建一个 Dog 的实例
const myDog = new Dog('Buddy', 'Golden Retriever');
 
// 调用子类的方法
myDog.bark(); // 输出: Buddy barks.
 
// 调用重写后的父类方法
myDog.speak(); // 输出: Buddy says woof!
 
// 调用从父类继承的(但未被重写)方法(虽然这里没有直接展示,但可以通过其他方式调用)
// 例如,可以添加一个方法到子类来调用父类的原始方法
class EnhancedDog extends Dog {
  constructor(name, breed, trick) {
    super(name, breed);
    this.trick = trick;
  }
 
  // 调用父类的 speak 方法(未重写版本)
  animalSpeak() {
    super.speak = Animal.prototype.speak; // 临时恢复父类的原始 speak 方法
    super.speak(); // 调用父类的原始 speak 方法
    super.speak = this.speak; // 恢复子类重写的 speak 方法
  }
}
 
const myEnhancedDog = new EnhancedDog('Max', 'Labrador', 'Play dead');
myEnhancedDog.bark(); // 输出: Max barks.
myEnhancedDog.speak(); // 输出: Max says woof!
myEnhancedDog.animalSpeak(); // 输出: Max makes a sound.(调用父类的原始 speak 方法)

父类 Animal

  • 包含一个构造函数,接受一个 name 参数。
  • 包含一个 speak 方法,打印出动物的名字和一条通用的发声信息。

子类 Dog

  • 使用 extends 关键字继承自 Animal。
  • 在构造函数中,使用 super(name) 调用父类的构造函数,以确保 name 属性被正确初始化。
  • 添加一个 breed 属性。
  • 定义一个 bark 方法,打印出狗的叫声。
  • 重写 speak 方法,打印出狗的特定发声信息。

扩展子类 EnhancedDog

  • 继承自 Dog,并添加一个 trick 属性。
  • 定义一个 animalSpeak 方法,通过临时恢复父类的原始 speak 方法来调用它。

通过这种方式,你可以利用ES6的类语法和原型链机制来实现继承和多态。

3.1.3 基于class语法实现继承

  在ES6(ECMAScript 2015)中,class 语法提供了一种更简洁和直观的方式来定义类及其继承关系。下面是一个基于 class 语法实现继承的示例代码:

// 定义一个父类(基类)
class Parent {
  constructor(name) {
    this.name = name;
  }
 
  // 父类的方法
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}
 
// 定义一个子类,继承自父类 Parent
class Child extends Parent {
  constructor(name, age) {
    // 调用父类的构造函数
    super(name);
    this.age = age;
  }
 
  // 子类的方法
  describe() {
    console.log(`I am ${this.age} years old and my name is ${this.name}.`);
  }
 
  // 可以重写父类的方法(如果需要)
  // greet() {
  //   console.log(`Hi, I'm ${this.name} and I'm a child.`);
  // }
}
 
// 创建一个 Child 的实例
const myChild = new Child('Alice', 10);
 
// 调用子类的方法
myChild.describe(); // 输出: I am 10 years old and my name is Alice.
 
// 调用从父类继承的方法
myChild.greet(); // 输出: Hello, my name is Alice.

父类 Parent

  • 包含一个构造函数,接受一个 name 参数,并将其赋值给实例的 name 属性。
  • 包含一个 greet 方法,用于打印出问候信息。

子类 Child

  • 使用 extends 关键字继承自 Parent。
  • 在构造函数中,使用 super(name) 调用父类的构造函数,以确保 name 属性被正确初始化。这是必须的,为子类构造函数中的 this 在调用 super() 之前是不可用的。
  • 添加一个 age 属性,用于存储子类的特有信息。
  • 定义一个 describe 方法,用于打印出子类的描述信息。
  • 子类可以选择性地重写父类的方法。在上面的代码中,greet 方法没有被重写,所以子类实例会继承并使用父类中的 greet 方法。

通过这种方式,你可以利用ES6的 class 语法轻松地实现类的继承,并在子类中扩展或重写父类的方法。

3.1.3.1 constructor()方法

  在JavaScript的ES6(ECMAScript 2015)中,constructor() 方法是一个特殊的方法,用于初始化新创建的对象。当你定义一个类时,可以选择性地包含一个 constructor() 方法来设置对象的初始状态。如果你没有在类中显式定义 constructor(),JavaScript会为你提供一个默认的构造函数。

下面是一个包含 constructor() 方法的类的示例代码:

// 定义一个类
class Person {
  // 构造函数,用于初始化新对象
  constructor(name, age) {
    this.name = name; // 设置对象的name属性
    this.age = age;   // 设置对象的age属性
  }
 
  // 类的方法
  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}
 
// 使用类创建新对象
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);
 
// 调用对象的方法
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.

类定义

  • 使用 class 关键字定义了一个名为 Person 的类。

构造函数

  • constructor(name, age) 是 Person 类的构造函数。
  • 它接受两个参数:name 和 age。
  • 在构造函数内部,使用 this 关键字将传入的参数值分别赋给新对象的 name 和 age 属性。

类的方法

  • greet() 是一个类方法,它使用 this 关键字访问对象的 name 和 age 属性,并打印一条问候信息。

创建对象

  • 使用 new 关键字和 Person 类创建了两个新对象:person1 和 person2。
  • 在创建对象时,向 constructor() 方法传递了相应的参数来初始化对象的属性。

调用方法

  • 通过对象实例调用 greet() 方法,打印出对象的问候信息。

如果你没有在类中定义 constructor() 方法,JavaScript 会为你提供一个默认的构造函数,该构造函数什么也不做(即它是一个空函数)。但是,通常你会希望自定义构造函数来初始化对象的属性。

3.1.3.2 super()调用父类构造方法

  在JavaScript的ES6(ECMAScript 2015)中,当你定义一个子类并希望在其构造函数中调用父类的构造函数时,你可以使用super()关键字。super()是一个引用父类构造函数的表达式,它允许你在子类的构造函数中初始化从父类继承的属性。

下面是一个包含super()调用父类构造方法的类的示例代码:

// 定义一个父类
class Parent {
  constructor(name) {
    this.name = name;
  }
 
  speak() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}
 
// 定义一个子类,继承自父类 Parent
class Child extends Parent {
  constructor(name, age) {
    // 调用父类的构造函数,传递 name 参数
    super(name);
    
    // 子类特有的属性
    this.age = age;
  }
 
  describe() {
    console.log(`I am ${this.age} years old and my name is ${this.name}.`);
  }
}
 
// 使用子类创建新对象
const child1 = new Child('Alice', 10);
 
// 调用子类的方法
child1.describe(); // 输出: I am 10 years old and my name is Alice.
 
// 调用从父类继承的方法
child1.speak(); // 输出: Hello, my name is Alice.

父类 Parent

  • 包含一个构造函数,接受一个 name 参数,并将其赋值给实例的 name 属性。
  • 包含一个 speak 方法,用于打印出问候信息。

子类 Child

  • 使用 extends 关键字继承自 Parent。
  • 在子类的构造函数中,使用 super(name) 调用父类的构造函数,并传递 name 参数。这是必须的,因为子类构造函数中的 this 在调用 super() 之前是不可用的,并且你需要确保从父类继承的属性被正确初始化。
  • 添加一个 age 属性,这是子类特有的属性。
  • 定义一个 describe 方法,用于打印出子类的描述信息。

创建对象

  • 使用 new 关键字和 Child 类创建了一个新对象 child1。
  • 在创建对象时,向 Child 的构造函数传递了 name 和 age 参数。这些参数被用于初始化对象的属性。

调用方法

  • 通过对象实例 child1 调用了 describe() 方法,打印出子类的描述信息。
  • 通过对象实例 child1 调用了从父类继承的 speak() 方法,打印出问候信息。

在子类的构造函数中,super() 调用是必需的,除非你的子类不需要从父类继承任何属性或方法(这在实际应用中很少见)。调用 super() 后,你可以安全地使用 this 来访问和初始化子类的属性。

3.1.3.3 私有属性和私有方法

  在JavaScript的ES6(ECMAScript 2015)及更早版本中,并没有直接支持私有属性和私有方法的概念。然而,从ES2019(ECMAScript 2019)开始,JavaScript引入了私有字段的提案,并在后续的标准中被正式采纳。私有字段以#字符开头,并且只能在类的内部被访问。

下面是一个包含私有属性和私有方法的类的示例代码:

class Person {
  #name; // 私有属性
  #age;  // 私有属性
 
  // 构造函数,用于初始化私有属性
  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }
 
  // 私有方法,只能在类的内部被调用
  #greet() {
    console.log(`Hello, my name is ${this.#name}.`);
  }
 
  // 公共方法,可以访问私有属性和调用私有方法
  introduce() {
    this.#greet(); // 调用私有方法
    console.log(`I am ${this.#age} years old.`);
  }
 
  // 公共方法,用于设置年龄(可以间接修改私有属性)
  setAge(newAge) {
    if (newAge > 0) {
      this.#age = newAge;
    } else {
      console.error('Age must be a positive number.');
    }
  }
}
 
// 创建Person类的实例
const person = new Person('Alice', 30);
 
// 调用公共方法
person.introduce(); // 输出: Hello, my name is Alice. I am 30 years old.
 
// 尝试直接访问私有属性(会报错)
// console.log(person.#name); // SyntaxError: Private field '#name' must be declared in the same class in which it is accessed
 
// 尝试调用私有方法(会报错)
// person.#greet(); // SyntaxError: Private field '#greet' must be declared in the same class in which it is accessed
 
// 使用公共方法来修改年龄
person.setAge(31);
person.introduce(); // 输出: Hello, my name is Alice. I am 31 years old.

私有属性

  • #name 和 #age 是私有属性,它们以 # 字符开头。
  • 私有属性只能在类的内部被访问和修改。

构造函数

  • 构造函数用于初始化私有属性 #name 和 #age。

私有方法

  • #greet 是一个私有方法,它只能在 Person 类的内部被调用。
  • 私有方法不能从类的外部直接访问。

公共方法

  • introduce 是一个公共方法,它可以访问私有属性 #name 和 #age,并且可以调用私有方法 #greet。
  • setAge 是另一个公共方法,用于设置私有属性 #age 的值。

创建对象

  • 使用 new 关键字和 Person 类创建了一个新对象 person。

调用方法

  • 通过对象实例 person 调用了公共方法 introduce 和 setAge。
  • 尝试直接访问私有属性或调用私有方法会导致语法错误(SyntaxError)。

请注意:私有字段是ES2019及更高版本中的特性,并且可能不是所有JavaScript环境都支持。如果你在不支持私有字段的环境中编写代码,你可能需要使用其他技术(如闭包或WeakMap)来模拟私有属性和方法的行为。

3.1.3.4 取值函数(getter)和存值函数(setter)

  在JavaScript中,取值函数(getter)和存值函数(setter)允许你为对象的属性定义自定义的访问和修改行为。这些函数是通过在对象属性前加上get和set关键字,并在类的定义中实现的。以下是一个简单的示例,展示了如何在JavaScript类中使用取值函数和存值函数:

class Person {
  constructor(name, age) {
    this._name = name; // 使用下划线前缀表示内部属性
    this._age = age;   // 使用下划线前缀表示内部属性
  }
 
  // 取值函数,用于获取 name 属性的值
  get name() {
    console.log('Getting the name...');
    return this._name;
  }
 
  // 存值函数,用于设置 name 属性的值
  set name(value) {
    if (typeof value === 'string' && value.trim() !== '') {
      console.log('Setting the name...');
      this._name = value;
    } else {
      console.error('Name must be a non-empty string.');
    }
  }
 
  // 类似的,为 age 属性定义取值和存值函数
  get age() {
    console.log('Getting the age...');
    return this._age;
  }
 
  set age(value) {
    if (Number.isInteger(value) && value > 0) {
      console.log('Setting the age...');
      this._age = value;
    } else {
      console.error('Age must be a positive integer.');
    }
  }
}
 
// 创建 Person 类的实例
const person = new Person('Alice', 30);
 
// 访问 name 和 age 属性(会调用取值函数)
console.log(person.name); // 输出: Getting the name... Alice
console.log(person.age);  // 输出: Getting the age... 30
 
// 修改 name 和 age 属性(会调用存值函数)
person.name = 'Bob'; // 输出: Setting the name...
person.age = 25;     // 输出: Setting the age...
 
// 再次访问以验证更改
console.log(person.name); // 输出: Getting the name... Bob
console.log(person.age);  // 输出: Getting the age... 25
 
// 尝试设置无效的值(会触发错误消息)
person.name = '';       // 输出: Name must be a non-empty string.
person.age = -5;        // 输出: Age must be a positive integer.

在这个示例中,Person类有两个内部属性_name和_age,它们通过取值函数和存值函数与外部世界进行交互。当你尝试访问person.name或person.age时,会调用相应的取值函数,并返回内部属性的值。当你尝试设置person.name或person.age时,会调用相应的存值函数,并在设置值之前进行验证。

取值函数和存值函数提供了一种强大的机制来封装和验证对象的属性,使得你可以控制对属性的访问和修改,并在必要时添加额外的逻辑(如日志记录、数据验证或触发事件)。

3.1.3.5 静态属性和静态方法

  在ES6(ECMAScript 2015)中,类(class)可以包含静态属性和静态方法。静态属性和方法属于类本身,而不是类的实例。你可以通过类名直接访问静态属性和方法,而不需要创建类的实例。

以下是如何在ES6中定义和使用静态属性和静态方法的示例代码:

// 定义一个类
class MyClass {
  // 静态属性(需要借助静态方法或者类属性语法糖来实现,因为直接定义静态属性在ES6标准中不支持)
  static staticProperty = 'I am a static property';
 
  // 构造函数
  constructor(name) {
    this.name = name;
  }
 
  // 实例方法
  instanceMethod() {
    console.log(`Instance method called. Name: ${this.name}`);
  }
 
  // 静态方法
  static staticMethod() {
    console.log('Static method called');
  }
 
  // 通过静态方法访问静态属性(因为直接定义静态属性在ES6标准中不支持,所以这里展示如何通过静态方法返回属性)
  static getStaticProperty() {
    return MyClass.staticProperty;
  }
}
 
// 访问静态属性和静态方法(不需要实例化类)
console.log(MyClass.staticProperty); // 如果使用静态属性语法糖,则直接输出 'I am a static property'
console.log(MyClass.getStaticProperty()); // 如果不使用静态属性语法糖,则通过静态方法返回 'I am a static property'
MyClass.staticMethod(); // 输出 'Static method called'
 
// 创建类的实例
const myInstance = new MyClass('John Doe');
 
// 访问实例方法(需要实例化类)
myInstance.instanceMethod(); // 输出 'Instance method called. Name: John Doe'
 
// 尝试通过实例访问静态属性和方法(会报错或返回undefined,具体取决于JavaScript引擎的实现)
// console.log(myInstance.staticProperty); // 可能会报错或返回undefined
// myInstance.staticMethod(); // 可能会报错或无效

注意:

  1. 静态属性:在ES6的原始规范中,并不直接支持静态属性的定义。上面的代码示例中使用了静态属性语法糖(这是一个提案,并且可能在某些JavaScript环境中得到支持,比如通过Babel等转译器)。如果不使用语法糖,你可以通过静态方法来模拟静态属性的访问,如上面的getStaticProperty方法所示。
  2. 静态方法:静态方法是直接定义在类上的方法,它们不能被类的实例调用,只能通过类名来调用。
  3. 实例方法和属性:实例方法和属性是定义在类的原型上的,它们需要通过类的实例来访问。

确保你的JavaScript环境支持这些特性,特别是在使用静态属性语法糖时。如果你在一个不支持静态属性语法糖的环境中工作,你可能需要继续使用静态方法来返回静态属性的值。

3.2 异步编程

  在前面我们学过了Ajax的使用,在实际工作中,我们通常会遇到多次异步操作且层层依赖回调结果的需求。

3.2.1 Promise对象

  romise对象是JavaScript中用于异步编程的一个重要特性,它代表了一个可能现在还不可用,但将来某个时刻会变得可用的值。Promise对象可以处于以下三种状态之一:

  1. Pending(等待):初始状态,既不是成功,也不是失败状态。
  2. Fulfilled(已成功):意味着操作成功完成。
  3. Rejected(已失败):意味着操作失败。

Promise对象通过new Promise(executor)构造函数创建,其中executor是一个执行器函数,它接收两个函数作为参数:resolve和reject。这两个函数分别用于将Promise对象的状态从Pending变为Fulfilled或Rejected。

以下是一个简单的Promise对象示例代码:

// 创建一个Promise对象
const myPromise = new Promise((resolve, reject) => {
  // 模拟一个异步操作,例如使用setTimeout
  setTimeout(() => {
    const success = true; // 这里可以是一个条件,用于决定是resolve还是reject
    if (success) {
      resolve('操作成功完成!'); // 将Promise状态变为Fulfilled,并传递一个值
    } else {
      reject('操作失败!'); // 将Promise状态变为Rejected,并传递一个错误原因
    }
  }, 1000); // 1秒后执行
});
 
// 使用.then()方法处理Fulfilled状态
// 使用.catch()方法处理Rejected状态
myPromise.then(
  (value) => {
    console.log('成功回调:', value); // 如果Promise状态变为Fulfilled,则执行此函数
  }
).catch(
  (error) => {
    console.error('失败回调:', error); // 如果Promise状态变为Rejected,则执行此函数
  }
);

在这个示例中,myPromise是一个Promise对象,它在1秒后模拟完成一个异步操作。根据success变量的值,它会调用resolve或reject函数来改变Promise的状态,并传递相应的值或错误原因。

then方法用于指定当Promise对象的状态变为Fulfilled时应该执行的回调函数,该函数接收resolve传递的值作为参数。

catch方法用于指定当Promise对象的状态变为Rejected时应该执行的回调函数,该函数接收reject传递的错误原因作为参数。

注意:Promise对象的链式调用非常常见,因为then和catch方法都会返回一个新的Promise对象,这使得你可以继续链式调用其他then或catch方法。

此外,Promise.all()、Promise.race()和Promise.allSettled()等静态方法也提供了处理多个Promise对象的便捷方式。

3.2.1.1 基本用法

  当然,以下是一个关于JavaScript中Promise对象基本用法的代码示例。这个示例将展示如何创建一个Promise,以及如何使用.then()和.catch()方法来处理成功和失败的情况。

// 定义一个返回Promise的函数
function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟一个异步操作,比如从服务器获取数据
    setTimeout(() => {
      // 这里我们可以根据某些条件来决定是调用resolve还是reject
      const data = "这是从服务器获取的数据";
      // 假设操作成功
      resolve(data); // 将Promise状态设置为Fulfilled,并传递数据
      
      // 如果操作失败,则应该调用reject
      // reject("获取数据失败!"); // 将Promise状态设置为Rejected,并传递错误信息
    }, 2000); // 模拟2秒的延迟
  });
}
 
// 使用fetchData函数,并通过.then()和.catch()处理结果
fetchData()
  .then((data) => {
    // 当Promise状态为Fulfilled时执行
    console.log("成功获取数据:", data);
    // 可以在这里进行进一步的数据处理
  })
  .catch((error) => {
    // 当Promise状态为Rejected时执行
    console.error("获取数据时发生错误:", error);
    // 可以在这里进行错误处理
  });
 
// 注意:由于fetchData函数内部使用了setTimeout来模拟异步操作,
// 因此上面的.then()和.catch()回调将在2秒后执行。

在这个示例中:

  • fetchData函数返回一个新的Promise对象。
  • 在Promise的执行器函数中,我们使用setTimeout来模拟一个异步操作,这个操作在2秒后完成。
  • 根据模拟操作的结果,我们调用resolve函数将Promise状态设置为Fulfilled,并传递一个数据字符串。如果我们希望模拟操作失败,则可以调用reject函数。
  • 我们调用fetchData函数,并使用.then()方法来处理成功的情况,即当Promise状态为Fulfilled时执行的回调。
  • 我们还使用.catch()方法来处理失败的情况,即当Promise状态为Rejected时执行的回调。

这个示例展示了Promise对象的基本用法,包括如何创建Promise、如何处理成功和失败的情况,以及如何使用.then()和.catch()方法进行链式调用。

3.2.1.2 Promise对象的方法

  Promise对象是JavaScript中处理异步操作的核心机制之一。除了构造函数Promise(executor)之外,Promise原型上还有一些非常有用的方法,如.then()、.catch()、.finally(),以及静态方法如Promise.all()、Promise.race()、Promise.resolve()和Promise.reject()。下面是一些关于这些方法的代码示例:

Promise 原型方法

.then(onFulfilled, onRejected)
处理Promise成功或失败的情况。

let promise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => resolve("成功!"), 1000);
});
 
promise.then(
  (value) => console.log(value), // 成功回调
  (error) => console.error(error) // 失败回调(可选)
);

.catch(onRejected)
处理Promise失败的情况,它是.then()方法的第二个参数的语法糖。

let promise = new Promise((resolve, reject) => {
  // 模拟异步操作失败
  setTimeout(() => reject("失败!"), 1000);
});
 
promise
  .then((value) => console.log(value)) // 不会被调用,因为Promise会失败
  .catch((error) => console.error(error)); // 输出:失败!

.finally(onFinally)
无论Promise成功还是失败,都会执行onFinally回调。

let promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("成功!"), 1000);
});
 
promise
  .then((value) => console.log(value))
  .catch((error) => console.error(error))
  .finally(() => console.log("完成,无论成功还是失败都会执行这里。"));

Promise 静态方法
Promise.all(iterable)
  等待所有给定的Promise对象都成功,并返回一个新的Promise对象,该对象在所有给定的Promise都成功时被解析,其解析值是一个数组,包含所有给定Promise的解析值。

let promise1 = Promise.resolve(1);
let promise2 = Promise.resolve(2);
let promise3 = Promise.resolve(3);
 
Promise.all([promise1, promise2, promise3])
  .then((values) => console.log(values)); // 输出:[1, 2, 3]

Promise.race(iterable)
返回一个新的Promise对象,该对象的状态由第一个完成的Promise对象决定(无论是成功还是失败)。

let promise1 = new Promise((resolve, reject) => setTimeout(resolve, 500, "one"));
let promise2 = new Promise((resolve, reject) => setTimeout(resolve, 100, "two"));
 
Promise.race([promise1, promise2])
  .then((value) => console.log(value)); // 输出:two,因为promise2更快完成

Promise.resolve(value)
返回一个以给定值解析的Promise对象。

Promise.resolve("Hello, world!")
  .then((value) => console.log(value)); // 输出:Hello, world!

Promise.reject(reason)
返回一个以给定原因拒绝的Promise对象。

Promise.reject("出错了!")
  .catch((error) => console.error(error)); // 输出:出错了!

这些方法使得Promise对象在异步编程中非常强大和灵活,允许你以链式的方式处理多个异步操作,以及组合和转换Promise。

3.2.2 async与await

  async 和 await 是 JavaScript 中用于处理异步操作的关键字,它们使得异步代码看起来和同步代码非常相似,从而提高了可读性和可维护性。这两个关键字通常与 Promise 对象一起使用。

下面是一个使用 async 和 await 的代码示例:

// 定义一个返回 Promise 的函数,模拟异步操作
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true; // 模拟操作是否成功的标志
      if (success) {
        resolve("这是从服务器获取的数据");
      } else {
        reject("获取数据失败!");
      }
    }, 2000); // 模拟2秒的延迟
  });
}
 
// 定义一个 async 函数,用于处理异步操作
async function getData() {
  try {
    // 使用 await 等待 fetchData() 返回的 Promise 完成
    const data = await fetchData();
    console.log("成功获取数据:", data);
  } catch (error) {
    // 如果 fetchData() 的 Promise 被拒绝,则捕获错误
    console.error("获取数据时发生错误:", error);
  }
}
 
// 调用 async 函数
getData();

在这个示例中:

  1. fetchData 函数返回一个 Promise 对象,该对象在 2 秒后解析或拒绝,取决于 success 变量的值。
  2. getData 函数被声明为 async,这意味着它可以包含 await 表达式。async 函数总是返回一个 Promise,即使你没有显式地返回它(如果没有返回值,则隐式地返回 Promise.resolve(undefined))。
  3. 在 getData 函数内部,我们使用 await 关键字来等待 fetchData 函数返回的 Promise。await 表达式会暂停 async 函数的执行,直到 Promise 被解析或拒绝,然后继续执行 async 函数并返回解析的值(或在捕获块中处理错误)。
  4. try…catch 语句用于捕获 fetchData 函数可能抛出的任何错误(即,如果 Promise 被拒绝)。
    5
    最后,我们调用 getData 函数,它会按照预期运行,并在控制台中输出成功或错误消息。

使用 async 和 await 可以使异步代码更加直观和易于理解,特别是当你有多个异步操作需要按顺序执行时。它们也允许你使用传统的 try…catch 错误处理机制,而不是将错误处理逻辑分散在多个 .catch() 回调中。

3.2.2.1 基本用法

  async 和 await 是 JavaScript 中用于处理异步操作的关键字,它们通常与 Promise 对象一起使用,使得异步代码看起来更像是同步代码。以下是一个简单的例子,展示了 async 和 await 的基本用法:

// 定义一个返回 Promise 的函数,模拟异步操作
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true; // 模拟操作是否成功的标志
      if (success) {
        resolve("数据加载成功!");
      } else {
        reject("数据加载失败!");
      }
    }, 2000); // 模拟2秒的延迟
  });
}
 
// 定义一个 async 函数,用于处理异步操作
async function loadData() {
  try {
    // 使用 await 等待 fetchData() 返回的 Promise 完成
    const message = await fetchData();
    console.log(message); // 输出 "数据加载成功!"
  } catch (error) {
    // 如果 fetchData() 的 Promise 被拒绝,则捕获错误
    console.error(error); // 输出 "数据加载失败!"(如果模拟失败)
  }
}
 
// 调用 async 函数
loadData();

在这个例子中:

  1. fetchData 函数返回一个 Promise 对象,该对象在 2 秒后根据 success 变量的值被解析或拒绝。
  2. loadData 函数被声明为 async,这意味着它内部可以包含 await 表达式。async 函数总是返回一个 Promise,即使你没有显式地返回它(如果没有返回值,则隐式地返回 Promise.resolve(undefined))。
  3. 在 loadData 函数内部,我们使用 await 关键字来等待 fetchData 函数返回的 Promise。await 表达式会暂停 async 函数的执行,直到 Promise 被解析或拒绝,然后继续执行 async 函数并返回解析的值(或在捕获块中处理错误)。
  4. try…catch 语句用于捕获 fetchData 函数可能抛出的任何错误(即,如果 Promise 被拒绝)。
  5. 最后,我们调用 loadData 函数,它会等待 fetchData 函数完成,并在控制台中输出成功或错误消息。

使用 async 和 await 可以使异步代码更加直观和易于理解,特别是当你有多个异步操作需要按顺序执行时。它们也允许你使用传统的 try…catch 错误处理机制,而不是将错误处理逻辑分散在多个 .catch() 回调中。

3.2.2.2 async和await特点

  async 和 await 是 JavaScript 中处理异步操作的强大工具,它们让异步代码看起来更像是同步代码,从而提高了代码的可读性和可维护性。以下是 async 和 await 的一些主要特点,以及相应的代码示例:

特点

简洁性

  • async 和 await 使得异步代码更加直观和简洁。
  • 不需要像 .then() 和 .catch() 那样链式调用。

错误处理

  • 可以使用 try…catch 语句来捕获异步操作中可能抛出的错误。

串行执行

  • await 会暂停 async 函数的执行,直到等待的 Promise 被解析或拒绝。
  • 这允许你按顺序执行多个异步操作。

返回值

  • async 函数总是返回一个 Promise。
  • 如果 async 函数中有返回值,则该值会被 Promise.resolve() 包装后返回。
  • 如果 async 函数抛出错误,则该错误会被 Promise.reject() 包装后返回。

代码示例
以下是一个包含上述特点的 async 和 await 代码示例:

// 定义一个返回 Promise 的函数,模拟异步操作
function fetchData(url) {
  return new Promise((resolve, reject) => {
    // 这里只是模拟一个异步操作,比如从网络获取数据
    setTimeout(() => {
      if (url === 'https://example.com/data') {
        resolve('数据加载成功!');
      } else {
        reject('无效的 URL!');
      }
    }, 2000); // 模拟2秒的延迟
  });
}
 
// 定义一个 async 函数,用于处理多个异步操作
async function loadAndProcessData(urls) {
  try {
    // 按顺序处理多个 URL
    for (const url of urls) {
      const message = await fetchData(url);
      console.log(message); // 输出加载成功的信息或错误信息(如果 URL 无效)
    }
    // 所有异步操作完成后,可以执行其他逻辑
    console.log('所有数据已加载并处理完毕!');
  } catch (error) {
    // 捕获并处理任何错误
    console.error('加载数据时发生错误:', error);
  }
}
 
// 调用 async 函数,并传入要处理的 URL 列表
const urls = [
  'https://example.com/data', // 有效的 URL
  'https://invalid-url.com'   // 无效的 URL,将触发拒绝
];
loadAndProcessData(urls);

在这个例子中:

  • fetchData 函数模拟了一个异步操作,它根据传入的 URL 返回解析或拒绝的 Promise。
  • loadAndProcessData 函数是一个 async 函数,它按顺序处理传入的 URL 列表。对于每个 URL,它使用 await 等待 fetchData 函数返回的 Promise。
  • 如果 fetchData 函数解析成功,则输出成功消息;如果拒绝,则抛出错误(但由于我们在 for 循环的外部使用了 try…catch,所以错误会被捕获并处理,而不会中断循环)。
  • 最后,当所有异步操作完成后,输出一条消息表示所有数据已加载并处理完毕。

这个示例展示了 async 和 await 如何使异步代码更加简洁、易于理解和维护,同时提供了强大的错误处理机制和按顺序执行异步操作的能力。

3.3 模块化编程

  历史上,JavaScript一直没有模块(module)体系,因此无法将一个大程序拆分成互相依赖的小文件,再用最简单的方法拼接装起来,对JavaScript开发大型的、复杂的项目形成了障碍。

3.3.1 export 和import 命令

  在 JavaScript 中,export 和 import 命令用于模块系统,它们允许你将代码分割成可重用的模块,并在其他文件中导入这些模块。以下是一些基本的 export 和 import 用法示例:

导出(export)
你可以导出变量、函数、类等。在 ES6 模块中,有两种主要的导出方式:命名导出(named exports)和默认导出(default export)。

命名导出

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
 
// 或者使用 const, function, class 等声明后,再导出
const multiply = (a, b) => a * b;
function divide(a, b) {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
}
export { multiply, divide };

默认导出

// greeting.js
export default function greet() {
  console.log('Hello, world!');
}

注意,每个模块只能有一个默认导出,但可以有多个命名导出。

导入(import)
要导入模块中导出的内容,你使用 import 命令。

导入命名导出

// main.js
import { add, subtract } from './math.js';
 
console.log(add(2, 3)); // 输出 5
console.log(subtract(5, 3)); // 输出 2

如果你只想导入部分命名导出,可以使用花括号 {} 指定要导入的项。

导入默认导出

// main.js
import greet from './greeting.js';
 
greet(); // 输出 'Hello, world!'

注意,当导入默认导出时,你可以使用任何名称来接收它,而不仅仅是原始导出时使用的名称。

导入所有命名导出(命名空间导入)

// main.js
import * as math from './math.js';
 
console.log(math.add(4, 5)); // 输出 9
console.log(math.subtract(9, 4)); // 输出 5

使用 * as 语法,你可以将所有命名导出作为一个对象导入,该对象的属性与原始模块中的导出相对应。

注意事项

  • 导入和导出时使用的路径是相对于当前文件的路径。如果文件在同一目录下,你可以使用 ./ 前缀(表示当前目录)。如果文件在父目录或子目录中,你需要相应地调整路径。
  • 在浏览器环境中,你可能需要配置一个模块打包器(如 Webpack)或使用支持 ES6 模块的浏览器来运行这些代码。
  • 在 Node.js 环境中,你需要确保你的文件使用 .mjs 扩展名或在 package.json 中设置 “type”: “module” 来启用 ES6 模块支持。

3.3.1.1 export 命令

  在 JavaScript 的 ES6 模块系统中,export 命令用于从模块中导出变量、函数、类等,以便它们可以在其他模块中被导入和使用。以下是 export 命令的一些代码示例:

命名导出(Named Exports)
命名导出允许你导出多个值,每个值都有一个名称。当你导入这些值时,你需要使用相同的名称(或者将它们解构到一个不同的名称上)。

// mathUtils.js
// 导出两个函数作为命名导出
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
 
// 你也可以先声明,然后再导出
function multiply(a, b) {
  return a * b;
}
 
const divide = (a, b) => {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
};
 
export { multiply, divide };

默认导出(Default Export)
默认导出是模块中“主要”的导出项。每个模块只能有一个默认导出。当你导入默认导出时,你可以使用任何名称来接收它。

// greeting.js
// 导出一个函数作为默认导出
export default function() {
  console.log('Hello, world!');
}
 
// 或者,你可以先将函数赋值给一个变量,然后再默认导出
const sayHello = () => {
  console.log('Hello again, world!');
};
 
export default sayHello;

重新导出(Re-exports)
你还可以从一个模块中导入值,然后再将它们导出,这通常用于创建模块的聚合或为了重新命名导出项。

// allMath.js
// 从另一个模块导入命名导出
import { add, subtract } from './mathUtils.js';
 
// 重新导出这些命名导出
export { add, subtract };
 
// 也可以导入默认导出并重新导出它
import greeting from './greeting.js';
export { greeting as defaultGreeting }; // 注意:这里使用了别名
 
// 或者直接默认导出导入的默认导出
import defaultGreeting from './greeting.js';
export default defaultGreeting; // 但通常不需要别名,直接 export default 即可

在重新导出时,如果你想要给导出项一个不同的名称,可以使用 as 关键字来指定别名。

注意事项

  • export 命令必须出现在模块的顶层作用域中,不能出现在函数或块级作用域内。
  • 当你导入一个命名导出时,你需要知道它的确切名称,除非你使用了命名空间导入(import * as)。
  • 默认导出提供了一种方便的方式来导出模块的主要功能,但每个模块只能有一个默认导出,因此如果你需要导出多个主要功能,你应该使用命名导出。

3.3.1.2 impact 命令

  在编程和软件开发中,并没有一个广泛认可的“impact”命令。impact 这个词通常用于描述某个改变、事件或功能对项目、系统或业务的影响。在代码和命令行工具的上下文中,impact 不是一个标准的命令或函数。

然而,根据你所使用的工具、框架或编程语言,可能会有一些与“impact”相关的库、插件或脚本,它们用于分析、测量或管理代码变更的影响。这些工具可能会有自己的命令行接口(CLI),但它们的命令通常不会是简单的 impact。

例如,在代码质量、安全性或性能分析方面,有一些工具可能会提供关于代码变更影响的报告:

  • 静态代码分析工具:如 ESLint、SonarQube 等,它们可以分析代码并报告潜在的问题,包括变更可能引入的新问题。
  • 性能分析工具:如 Chrome DevTools 的性能分析器、Webpack Bundle Analyzer 等,它们可以帮助你了解代码变更对应用性能的影响。
  • 依赖管理工具:如 npm、yarn 等,它们可以显示项目依赖的变更,并可能提供关于这些变更对项目影响的信息。
  • 版本控制系统:如 Git,它允许你查看代码的历史记录,比较不同版本之间的差异,并理解这些差异对项目的影响。

如果你是在寻找一个特定的工具或库来评估代码变更的影响,你可能需要查看该工具或库的文档来了解其提供的命令和选项。

此外,如果你在开发过程中想要手动跟踪和管理变更的影响,你可以考虑实施以下做法:

  • 代码审查:通过代码审查过程来识别和讨论变更的影响。
  • 自动化测试:编写单元测试、集成测试和功能测试来确保变更没有破坏现有的功能。
  • 文档:更新项目的文档来反映变更的影响和任何新的行为或依赖。
  • 版本控制:使用 Git 等版本控制系统来跟踪变更的历史记录,并允许回滚到先前的状态(如果需要)。

总之,impact 不是一个标准的编程命令,但你可以通过各种工具、实践和文档来评估和管理代码变更的影响。

3.3.2 export default 命令

  在 JavaScript 的 ES6 模块系统中,export default 命令用于从模块中导出一个“默认”值。这个值可以是变量、函数、类、对象等。当你使用 import 命令导入一个默认导出时,你可以使用任何名称来接收它,因为默认导出在导入时是不需要通过名称来匹配的。

以下是一些使用 export default 命令的代码示例:

导出函数
/

/ myFunction.js
export default function myDefaultFunction() {
  console.log('This is the default function.');
}

在另一个文件中导入这个函数:

// main.js
import anyNameIChoose from './myFunction.js';
 
anyNameIChoose(); // 输出: This is the default function.

导出变量

// myVariable.js
const myDefaultValue = 42;
export default myDefaultValue;

在另一个文件中导入这个变量:

// main.js
import anyNumber from './myVariable.js';
 
console.log(anyNumber); // 输出: 42

导出类

// MyClass.js
class MyClass {
  constructor() {
    this.message = 'Hello from MyClass!';
  }
 
  sayHello() {
    console.log(this.message);
  }
}
 
export default MyClass;

在另一个文件中导入这个类:

// main.js
import AnyClassName from './MyClass.js';
 
const myInstance = new AnyClassName();
myInstance.sayHello(); // 输出: Hello from MyClass!

注意事项

  • 每个模块只能有一个默认导出。
  • 当导入默认导出时,你可以使用任何名称来接收它,因为默认导出在导入时是不与原始名称绑定的。
  • 如果模块中既有默认导出又有命名导出,你可以同时导入它们,但需要分别指定名称:
// myModule.js
export const namedExport = 'I am a named export.';
export default function defaultFunction() {
  console.log('I am the default function.');
}
 
// main.js
import defaultFunction, { namedExport } from './myModule.js';
 
defaultFunction(); // 输出: I am the default function.
console.log(namedExport); // 输出: I am a named export.

使用 export default 是一种方便的方式来导出模块的主要功能或值,但请记住,每个模块只能有一个默认导出。如果你需要导出多个主要功能或值,你应该使用命名导出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值