likes
comments
collection
share

ES6的类 vs TypeScript的类:解密两种语言中的面向对象之争

作者站长头像
站长
· 阅读数 14

ES6的类 vs TypeScript的类:解密两种语言中的面向对象之争

ES6 类

ES6(ECMAScript 2015)引入了类的概念,为 JavaScript 增加了面向对象编程的能力。ES6 中的类是一种语法糖,本质上仍然是基于原型的继承。使用类可以定义构造函数、实例方法和静态方法,并且支持继承和类之间的关系。

ES6 类的常见特性

以下是一些 ES6 类的常见特性:

1. 构造函数

类中的构造函数通过 constructor 关键字定义,并且在创建实例时被调用。

当使用 ES6 类创建对象时,构造函数是在实例化过程中自动调用的方法。构造函数使用 constructor 关键字定义在类内部,它负责初始化对象的属性和状态。

以下是一个使用 ES6 的构造函数的示例:

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

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

const person1 = new Person('Alice', 28);
person1.greet(); // 输出:Hello, my name is Alice and I am 28 years old.

在上面的例子中,我们创建了一个名为 Person 的类。构造函数 constructor 接受两个参数 nameage,并将它们分别赋值给实例属性 this.namethis.agegreet 方法用于打印出个人信息。

通过使用 new 关键字,我们可以实例化该类并传入相应的参数。在实例化过程中,构造函数会被自动调用,初始化属性。然后,我们可以调用 greet 方法来打印出个人信息。

值得注意的是,ES6 类中的构造函数只能有一个,且使用 super 关键字来调用父类的构造函数(如果有继承关系)。构造函数可以执行各种操作,例如初始化数据、调用其他方法或执行其他逻辑,以确保实例在创建时处于正确的状态。

2. 实例方法

类中的实例方法不需要使用关键字 function 声明,直接在类体内声明即可。实例方法默认会绑定到实例对象。

在 ES6 类中,实例方法是定义在类的原型上的方法,可以被类的实例调用。以下是一个使用 ES6 的实例方法的示例:

class Circle {
  constructor(radius) {
    this.radius = radius;
  }

  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }

  calculateCircumference() {
    return 2 * Math.PI * this.radius;
  }
}

const circle1 = new Circle(5);
console.log(circle1.calculateArea()); // 输出:78.53981633974483
console.log(circle1.calculateCircumference()); // 输出:31.41592653589793

在上面的例子中,我们创建了一个名为 Circle 的类。该类有一个构造函数,接受一个参数 radius 并将其赋值给实例属性 this.radius

然后,我们定义了两个实例方法 calculateAreacalculateCircumferencecalculateArea 方法用于计算圆的面积,calculateCircumference 方法用于计算圆的周长。这两个方法都可以通过类的实例进行调用。

使用 new 关键字我们实例化了 Circle 类,并传入半径为 5。然后我们分别调用 calculateAreacalculateCircumference 方法,并打印出结果。

ES6 类中的实例方法是绑定到类的原型上的,这意味着每个类的实例都共享同一个实例方法的实现,从而节省了内存空间。同时,实例方法可以访问类的实例属性和其他实例方法,允许我们在方法内部处理实例的数据和状态。

3. 静态方法

类中的静态方法使用 static 关键字声明,静态方法与实例无关,可以直接通过类来调用。

在 ES6 类中,静态方法是定义在类本身上的方法,而不是类的实例。可以通过类名直接调用静态方法,而不需要创建类的实例。以下是一个使用 ES6 的静态方法的示例:

class MathUtils {
  static add(a, b) {
    return a + b;
  }

  static subtract(a, b) {
    return a - b;
  }
}

console.log(MathUtils.add(5, 3)); // 输出:8
console.log(MathUtils.subtract(10, 4)); // 输出:6

在上面的例子中,我们创建了一个名为 MathUtils 的类。该类定义了两个静态方法 addsubtract。这两个方法都可以直接通过类名进行调用,而不需要创建类的实例。

使用类名 MathUtils 来调用静态方法,并传入相应的参数。然后我们打印出结果。

ES6 类中的静态方法是绑定到类本身的,而不是绑定到类的实例。因此,它们不能访问类的实例属性或其他实例方法。静态方法通常用于执行与类相关的操作,例如辅助函数、工具函数或对象的创建和管理。

可以通过在方法前添加 static 关键字来定义静态方法。静态方法可以通过类名直接调用,而无需实例化对象。这使得静态方法具有更高的灵活性和代码组织性,可以在不创建实例的情况下执行特定的功能。

4. 继承

通过 extends 关键字可以实现类的继承,子类可以继承父类的属性和方法,并可以覆盖或扩展父类的功能。

在 ES6 中,我们可以使用 extends 关键字实现类的继承。通过继承,一个子类可以继承父类的属性和方法,同时可以添加新的属性和方法。以下是一个使用 ES6 实现继承的代码示例:

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} barks loudly.`);
  }
}

const myDog = new Dog('Max', 'Labrador');
myDog.speak(); // 输出:Max makes a sound.
myDog.bark(); // 输出:Max barks loudly.
console.log(myDog.breed); // 输出:Labrador

在上面的例子中,我们创建了一个名为 Animal 的父类。该类有一个构造函数,接受一个参数 name 并将其赋值给实例属性 this.name。还定义了一个方法 speak,用于输出动物发出声音。

然后,我们创建了一个名为 Dog 的子类,使用 extends 关键字来继承父类 Animal。子类 Dog 有一个构造函数,接受两个参数 namebreed。在子类的构造函数中,我们使用 super 关键字调用父类的构造函数,并传递 name 参数。然后,我们将 breed 参数赋值给子类特有的实例属性 this.breed

子类 Dog 还定义了一个新的方法 bark,用于输出狗的吠声。

通过创建 Dog 类的实例 myDog,我们可以调用继承自父类的方法 speak 和子类自己的方法 bark。还可以访问子类特有的属性 breed

继承允许我们在子类中复用父类的代码和功能,并在子类中添加更多特定的属性和方法。这提高了代码的可重用性和可维护性,并支持面向对象编程的概念,如封装、继承和多态。

TypeScript 类

TypeScript 是一个由 Microsoft 开发的开源编程语言,它是 JavaScript 的一个超集,增加了静态类型检查和面向对象编程的特性。TypeScript 中的类和 ES6 类有相似之处,但又具有更强的类型系统和其他功能。

TypeScript 类的特性

以下是一些 TypeScript 类的特性:

1. 类型注解

TypeScript 强调类型的静态检查,可以在类的属性、方法和参数上添加类型注解,以约束数据类型。

TypeScript 是 JavaScript 的一个超集,它提供了类型注解的功能来静态检查代码。通过类型注解,我们可以为变量、函数参数、函数返回值等添加类型信息,从而使编译器能够检查代码中的类型错误,并在开发过程中提供更好的代码提示和自动补全。

以下是一个使用 TypeScript 的类型注解的代码示例:

function greet(name: string): void {
  console.log(`Hello, ${name}!`);
}

let username: string = "John";
greet(username); // 输出:Hello, John!

在上面的例子中,我们定义了一个名为 greet 的函数。函数接受一个参数 name,并且我们使用 : string 进行类型注解来指定该参数的类型为字符串。

同时,我们还为函数的返回值 void 添加了类型注解,表示该函数没有返回值。

在函数调用时,我们创建一个名为 username 的变量,并将其类型注解为字符串。然后,我们将 username 作为参数传递给 greet 函数。

TypeScript 编译器将根据类型注解进行代码检查,以确保传递给函数的参数类型正确,并且函数的返回值与类型注解一致。

类型注解提供了一种强大的工具,可以增强代码的可读性和可维护性,并提供更好的开发体验。它可以帮助我们在编译阶段发现潜在的类型错误,并减少在运行时出现的错误。此外,类型注解还提供了更好的代码提示和自动补全功能,以提高开发效率。

2. 访问修饰符

TypeScript 提供了 publicprotectedprivate 等访问修饰符,用于控制类成员的访问权限。

TypeScript 提供了访问修饰符来控制类的属性和方法的访问权限。有三种主要的访问修饰符:publicprotectedprivate

下面是一个使用 TypeScript 访问修饰符的示例代码:

class Person {
  public name: string;
  protected age: number;
  private phoneNumber: string;

  constructor(name: string, age: number, phoneNumber: string) {
    this.name = name;
    this.age = age;
    this.phoneNumber = phoneNumber;
  }

  public introduce(): void {
    console.log(`My name is ${this.name}. I am ${this.age} years old.`);
    this.privateMethod();
  }

  protected protectedMethod(): void {
    console.log("This is a protected method.");
  }

  private privateMethod(): void {
    console.log("This is a private method.");
  }
}

class Employee extends Person {
  public position: string;

  constructor(name: string, age: number, phoneNumber: string, position: string) {
    super(name, age, phoneNumber);
    this.position = position;
  }

  public getPhoneNumber(): void {
    console.log(`${this.name}'s phone number is ${this.phoneNumber}.`);
    this.protectedMethod();
  }
}

const john = new Person("John", 30, "123456789");
console.log(john.name); // 公有属性,可以访问
// console.log(john.age); // 受保护属性,不能在类外部访问
// console.log(john.phoneNumber); // 私有属性,不能在类外部访问
john.introduce(); // 公有方法,可以访问

const employee = new Employee("Bob", 25, "987654321", "Manager");
// console.log(employee.name); // 公有属性,可以访问
// console.log(employee.age); // 受保护属性,不能在类外部访问
// console.log(employee.phoneNumber); // 私有属性,不能在类外部访问
employee.introduce(); // 公有方法,可以访问
employee.getPhoneNumber(); // 子类可以访问受保护方法

在上面的示例中,我们创建了一个名为 Person 的基类,并定义了三个属性:nameagephoneNumbername 是公有属性,可以在类外部访问;age 是受保护属性,只能在类内部及其子类中访问;phoneNumber 是私有属性,只能在类内部访问。

我们还定义了两个方法:introduceprotectedMethodprivateMethodintroduce 是公有方法,可以在类外部调用;protectedMethod 是受保护方法,只能在类内部及其子类中调用;privateMethod 是私有方法,只能在类内部调用。

然后,我们创建了一个名为 Employee 的子类,继承自 Person。子类拥有 position 属性,并在构造函数中通过 super 关键字调用父类的构造函数来初始化继承的属性。

通过创建 PersonEmployee 的实例,我们可以看到不同访问修饰符的效果。只有公有属性和方法可以从类外部访问,受保护属性和方法只能在类内部及其子类中访问,而私有属性和方法只能在类内部访问。

访问修饰符允许我们控制类的成员的可见性和可访问性,并提供了一种封装数据和行为的方式,增强了代码的安全性和可维护性。

3. 类型推断

TypeScript 可以根据赋值表达式的右侧推断出变量的类型,减少重复的类型注解。

TypeScript 提供了类型推断的功能,能够根据赋值表达式的右侧值自动推断变量的类型。下面是一个使用 TypeScript 类型推断的示例代码:

let name = "Alice"; // 类型推断为 string
let age = 30; // 类型推断为 number
let isStudent = true; // 类型推断为 boolean

let fruits = ["apple", "banana", "orange"]; // 类型推断为 string[]
let numbers = [1, 2, 3]; // 类型推断为 number[]
let matrix = [[1, 2], [3, 4]]; // 类型推断为 number[][]

let person = {
  name: "Bob",
  age: 25,
}; // 类型推断为 { name: string, age: number }

function add(a: number, b: number) {
  return a + b;
} // 函数参数和返回值的类型推断为 number

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
} // 类的属性和构造函数参数的类型推断为 string

let animal = new Animal("Dog"); // 类的实例类型推断为 Animal

在上述示例中,我们声明了一些变量并初始化它们,TypeScript 根据赋值的值来推断变量的类型。例如,通过字符串赋值给变量 name,TypeScript 推断出 name 的类型为 string

类似地,数组的元素类型也可以通过初始化的值进行推断。在示例中,fruits 的类型被推断为 string[],即字符串数组。

在函数定义中,参数 ab 的类型被推断为 number,因为我们在函数体内使用了加法操作,而返回值的类型也被推断为 number

对于类,属性和构造函数参数的类型也可以通过初始化来推断。在示例中,Animal 类的属性 name 和构造函数参数 name 都被推断为 string 类型。

最后,当我们创建 Animal 类的实例 animal 时,TypeScript 推断出它的类型为 Animal

类型推断是 TypeScript 的一个强大功能,能够减少显式类型注解的冗余,并帮助开发人员更轻松地编写类型安全的代码。

4. 接口实现

TypeScript 支持类实现接口,通过 implements 关键字来强制类遵循接口的契约。

在 TypeScript 中,接口(Interfaces)用于定义对象的结构和类型。一个类可以通过实现(implements)一个接口来强制遵循该接口所定义的结构。下面是一个使用 TypeScript 接口实现的示例代码:

interface Shape {
  getArea(): number;
}

class Circle implements Shape {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle implements Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

const circle = new Circle(5);
console.log(circle.getArea()); // 输出: 78.53981633974483

const rectangle = new Rectangle(4, 6);
console.log(rectangle.getArea()); // 输出: 24

在上述示例中,我们定义了一个名为 Shape 的接口,它有一个名为 getArea 的方法,返回类型为 number

然后我们创建了两个类 CircleRectangle,它们分别实现了 Shape 接口。这意味着这两个类必须实现 Shape 接口中定义的方法 getArea

Circle 类有一个属性 radius,并在构造函数中初始化。它实现了 getArea 方法来计算圆的面积。

Rectangle 类有两个属性 widthheight,并在构造函数中初始化。它同样实现了 getArea 方法来计算矩形的面积。

通过创建 CircleRectangle 类的实例,并调用它们的 getArea 方法,我们可以得到圆和矩形的面积。

接口的使用使得我们能够在 TypeScript 中定义和强制对象的结构,并确保类遵循指定的接口约束。这提高了代码的可读性和可维护性,并加强了类型检查和类型安全性。

总结来说,ES6 类是 JavaScript 中基于原型的面向对象编程的语法糖,而 TypeScript 类在此基础上增加了强类型检查和其他面向对象编程的特性,使得代码更具可读性、可维护性和可靠性。