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 更加适合大型项目的开发,同时也为开发者提供了更清晰和直观的语法。

相关推荐
geovindu9 小时前
CSharp: Prototype Pattern
开发语言·后端·设计模式·.net·原型模式·创建型模式
Seven凹凸Man13 小时前
WorkBuddy之产品经理原型设计及PRD实战
ai·原型模式
ttod_qzstudio2 天前
【软考设计模式】原型模式:“克隆“对象与深拷贝、浅拷贝精讲
设计模式·原型模式
希冀1235 天前
【JavaScript】Javascript—JS进阶—Day03
开发语言·javascript·原型模式
想吃火锅100524 天前
【前端手撕】instanceof
前端·javascript·原型模式
UXbot25 天前
帮助企业低门槛开展AI应用开发的平台推荐
前端·低代码·ui·交互·产品经理·原型模式·web app
UXbot25 天前
如何选择适合公司项目的UI设计工具?企业选型指南
前端·低代码·ui·团队开发·原型模式·设计规范·web app
UXbot25 天前
原型设计工具如何帮助新人快速进入产品行业?
前端·低代码·ui·交互·团队开发·原型模式·web app
sunny.day1 个月前
js原型与原型链
开发语言·javascript·原型模式·js原型链
UXbot1 个月前
AI网页开发工具能替代工具吗?5大平台对比
前端·人工智能·低代码·ui·原型模式·web app