4.6js面向对象

js原型继承

JavaScript 的原型链继承是其核心特性之一,理解原型链对于掌握 JavaScript 的面向对象编程至关重要。


1. ​原型(Prototype)基础

在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](可以通过 __proto__Object.getPrototypeOf() 访问),它指向该对象的原型。原型本身也是一个对象,因此它也有自己的原型,这样就形成了一个链式结构,称为原型链

关键点:
  • 每个函数都有一个 prototype 属性,这个属性是一个对象,用于实现基于原型的继承。
  • 当访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。

2. ​原型链的基本原理

示例:

复制代码
function Animal(name) {
  this.name = name;
}

// 在 Person 的原型上添加方法
Animal.prototype.sayHello = function () {
  console.log(`Hello, 我是动物 ${this.name}`);
};

const cat = new Animal("小猫");
cat.sayHello(); // 输出: Hello, 我是动物 小猫
解析:
  1. Animal是一个构造函数,Animal.prototype 是它的原型对象。
  2. 当调用 new Animal("小猫") 时,会创建一个新对象 cat,并将 cat.__proto__ 指向 Animal.prototype
  3. 当调用 cat.sayHello() 时,cat自身没有 sayHello 方法,因此 JavaScript 会沿着原型链查找,最终在 Animal.prototype 上找到该方法。

3. ​原型链继承的实现

原型链继承的核心思想是通过将子类的原型对象指向父类的实例,从而实现继承。

示例:

复制代码
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function () {
  console.log(`My name is ${this.name}`);
};

function Child(age) {
  this.age = age;
}

// 实现原型链继承
Child.prototype = new Parent(); // 将 Child 的原型指向 Parent 的实例

const child1 = new Child();
child1.name = "Alice"; // 给子类实例添加属性
child1.sayName(); // 输出: My name is Alice
解析:
  1. Child.prototype = new Parent():将 Child 的原型对象指向 Parent 的一个实例,这样 Child 的实例就可以访问 Parent.prototype 上的方法。
  2. child1.name = "Alice":由于 Child 的构造函数没有 name 属性,因此需要手动为实例添加 name 属性。

缺点:

  • 所有子类实例共享同一个父类实例的属性(如果属性是引用类型,如数组或对象,可能会导致意外的修改)。
  • 无法在子类构造函数中向父类构造函数传递参数。

4. ​改进的原型链继承(借用构造函数)​

为了解决上述问题,可以使用借用构造函数的方式,在子类构造函数中调用父类构造函数。

示例:

复制代码
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayName = function () {
  console.log(`My name is ${this.name}`);
};

function Child(name, age) {
  Parent.call(this, name); // 借用父类构造函数
  this.age = age;
}

// 仍然需要手动设置原型链
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const child1 = new Child("Alice", 10);
child1.sayName(); // 输出: My name is Alice
解析:
  1. Parent.call(this, name):在子类构造函数中调用父类构造函数,确保每个子类实例都有自己的属性副本。
  2. Child.prototype = Object.create(Parent.prototype):通过 Object.create 创建一个新的对象,该对象的原型指向 Parent.prototype,从而避免直接修改 Child.prototype
  3. Child.prototype.constructor = Child:修复构造函数指向问题。

5. ​原型式继承

原型式继承通过创建一个临时构造函数,并将其原型指向目标对象,从而实现继承。

示例:

复制代码
const parent = {
  name: "Alice",
  sayName() {
    console.log(`My name is ${this.name}`);
  },
};

// 使用 Object.create 实现原型式继承
const child = Object.create(parent);
child.name = "Bob";
child.sayName(); // 输出: My name is Bob
解析:
  • Object.create(parent) 创建了一个新对象,该对象的原型指向 parent
  • 这种方式适合简单的对象继承,但不适合需要构造函数的场景。

6. ​ES6 的 classextends

ES6 引入了 classextends 关键字,使得继承的语法更加简洁和直观。

示例:

复制代码
class Parent {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(`My name is ${this.name}`);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类构造函数
    this.age = age;
  }
}

const child1 = new Child("Alice", 10);
child1.sayName(); // 输出: My name is Alice
解析:
  • classextends 是语法糖,底层仍然是基于原型的继承。
  • super() 用于调用父类构造函数。

  • 原型链继承:通过将子类的原型指向父类的实例实现继承,但存在共享属性的问题。
  • 借用构造函数:在子类构造函数中调用父类构造函数,解决属性共享问题,是现代 JavaScript 中最推荐的继承方式。
  • ES6 classextends:语法糖,底层仍然是基于原型的继承,推荐使用。

es6面向对象详解

1. ​类(Class)​

ES6 引入了 class 关键字,提供了一种更简洁的语法来定义类和构造函数。

示例:

复制代码
class Person {
  // 构造函数
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

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

// 创建实例
const person1 = new Person("Alice", 25);
person1.sayHello(); // 输出: Hello, my name is Alice and I am 25 years old.
解析:
  1. **class**:定义一个类。
  2. **constructor**:类的构造函数,用于初始化对象的属性。new 关键字调用时会自动执行构造函数。
  3. 方法 :类中的方法不需要 function 关键字,直接定义即可。

2. ​继承(Inheritance)​

ES6 提供了 extends 关键字来实现类的继承。

示例:

复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

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

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类的构造函数
    this.breed = breed;
  }

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

const dog1 = new Dog("Buddy", "Golden Retriever");
dog1.speak(); // 输出: Buddy barks.
console.log(dog1.breed); // 输出: Golden Retriever
解析:
  1. **extends**:用于创建子类,继承父类的属性和方法。
  2. **super**:在子类的构造函数中调用 super(),用于调用父类的构造函数。super 必须在 this 之前调用,否则会报错。
  3. 方法重写 :子类可以重写父类的方法(如 speak 方法)。

3. ​静态方法和静态属性

ES6 支持在类中定义静态方法和静态属性,这些方法和属性属于类本身,而不是类的实例。

示例:

复制代码
class MathUtils {
  // 静态方法
  static add(a, b) {
    return a + b;
  }

  // 静态属性
  static PI = 3.14;
}

console.log(MathUtils.add(2, 3)); // 输出: 5
console.log(MathUtils.PI); // 输出: 3.14

// 静态方法和属性不能通过实例访问
const math = new MathUtils();
console.log(math.add(2, 3)); // 报错: math.add is not a function

解析:

  • 静态方法和属性通过类名直接访问,而不是通过实例访问。
  • 静态方法通常用于工具函数或与实例无关的操作。

4. ​私有属性和方法

在 ES6 中,JavaScript 并没有直接支持私有属性和方法,但从 ​ES2022(ES13)​ 开始,引入了 # 符号来定义私有成员。

示例:

复制代码
class Counter {
  // 私有属性
  #count = 0;

  // 公共方法
  increment() {
    this.#count++;
  }

  // 私有方法
  #reset() {
    this.#count = 0;
  }

  getCount() {
    return this.#count;
  }

  reset() {
    this.#reset();
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // 输出: 1
counter.reset();
console.log(counter.getCount()); // 输出: 0

// 无法直接访问私有属性和方法
console.log(counter.#count); // 报错: SyntaxError
counter.#reset(); // 报错: SyntaxError

解析:

  • 使用 # 前缀定义私有属性和方法。
  • 私有成员只能在类的内部访问,外部无法直接访问。

5. ​Getter 和 Setter

ES6 支持通过 getset 关键字定义属性的 getter 和 setter 方法,用于控制对属性的访问和修改。

示例:

复制代码
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  // Getter
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // Setter
  set fullName(name) {
    const [firstName, lastName] = name.split(" ");
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

const person = new Person("Alice", "Smith");
console.log(person.fullName); // 输出: Alice Smith

person.fullName = "Bob Johnson";
console.log(person.firstName); // 输出: Bob
console.log(person.lastName); // 输出: Johnson

解析:

  • **get**:定义属性的读取方法。
  • **set**:定义属性的写入方法。
  • 使用 getter 和 setter 可以在访问或修改属性时添加额外的逻辑。

6. ​继承中的 super

在继承中,super 的作用非常重要,它用于调用父类的构造函数或方法。

示例:

复制代码
class Animal {
  constructor(name) {
    this.name = name;
  }

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

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类的构造函数
    this.breed = breed;
  }

  speak() {
    super.speak(); // 调用父类的方法
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.speak();
// 输出:
// Buddy makes a noise.
// Buddy barks.

解析:

  • 在子类的构造函数中,super() 必须在 this 之前调用。
  • 在子类的方法中,super.methodName() 可以调用父类的方法。

7. ​多态

多态是面向对象编程的一个重要特性,允许子类重写父类的方法,从而实现不同的行为。

示例:

复制代码
class Animal {
  speak() {
    console.log("Animal makes a noise.");
  }
}

class Dog extends Animal {
  speak() {
    console.log("Dog barks.");
  }
}

class Cat extends Animal {
  speak() {
    console.log("Cat meows.");
  }
}

const animals = [new Animal(), new Dog(), new Cat()];
animals.forEach((animal) => animal.speak());
// 输出:
// Animal makes a noise.
// Dog barks.
// Cat meows.

解析:

  • 子类可以重写父类的方法,从而实现不同的行为。
  • 多态使得代码更加灵活和可扩展。

8. ​抽象类

ES6 并没有直接支持抽象类,但可以通过约定或结合 TypeScript 来实现抽象类。抽象类是一种不能被实例化的类,通常用于定义子类的通用接口。

示例(约定方式):

复制代码
class Animal {
  constructor(name) {
    if (new.target === Animal) {
      throw new Error("Animal is an abstract class and cannot be instantiated.");
    }
    this.name = name;
  }

  speak() {
    throw new Error("Method 'speak()' must be implemented.");
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

// const animal = new Animal(); // 报错: Animal is an abstract class
const dog = new Dog("Buddy");
dog.speak(); // 输出: Buddy barks.

解析:

  • 使用 new.target 检查类是否被直接实例化。
  • 抽象方法可以通过抛出错误来强制子类实现。

总结

ES6 的面向对象特性使得 JavaScript 的类和继承更加直观和现代化:

  • **class**:定义类的语法糖。
  • **extends**:实现继承。
  • **super**:调用父类的构造函数或方法。
  • 静态方法和属性 :通过 static 定义。
  • 私有成员 :通过 # 定义(ES2022+)。
  • Getter 和 Setter:控制属性的访问和修改。

ES6 的面向对象特性使得 JavaScript 更加适合大型项目的开发,同时也为开发者提供了更清晰和直观的语法。

相关推荐
0白露17 小时前
原型模式为什么可以解决构建复杂对象的资源消耗问题
原型模式
Hanson Huang6 天前
23种设计模式-原型(Prototype)设计模式
设计模式·原型模式
诺亚凹凸曼7 天前
23种设计模式-创建型模式-原型
设计模式·原型模式
严文文-Chris7 天前
【spring对bean Singleton和Prototype的管理流程】
spring·单例模式·原型模式
东东__net7 天前
01_JavaScript
开发语言·javascript·原型模式
熊大如如10 天前
JavaScript 继承方式总结
开发语言·javascript·原型模式
搞不懂语言的程序员13 天前
原型模式详解
原型模式
小九没绝活14 天前
设计模式-原型模式
java·设计模式·原型模式
海盗强15 天前
prototype和proto的区别
开发语言·javascript·原型模式