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, 我是动物 小猫
解析:
Animal
是一个构造函数,Animal.prototype
是它的原型对象。- 当调用
new Animal("小猫")
时,会创建一个新对象cat
,并将cat.__proto__
指向Animal.prototype
。 - 当调用
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
解析:
Child.prototype = new Parent()
:将Child
的原型对象指向Parent
的一个实例,这样Child
的实例就可以访问Parent.prototype
上的方法。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
解析:
Parent.call(this, name)
:在子类构造函数中调用父类构造函数,确保每个子类实例都有自己的属性副本。Child.prototype = Object.create(Parent.prototype)
:通过Object.create
创建一个新的对象,该对象的原型指向Parent.prototype
,从而避免直接修改Child.prototype
。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 的 class
和 extends
ES6 引入了 class
和 extends
关键字,使得继承的语法更加简洁和直观。
示例:
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
解析:
class
和extends
是语法糖,底层仍然是基于原型的继承。super()
用于调用父类构造函数。
- 原型链继承:通过将子类的原型指向父类的实例实现继承,但存在共享属性的问题。
- 借用构造函数:在子类构造函数中调用父类构造函数,解决属性共享问题,是现代 JavaScript 中最推荐的继承方式。
- ES6
class
和extends
:语法糖,底层仍然是基于原型的继承,推荐使用。
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.
解析:
- **
class
**:定义一个类。 - **
constructor
**:类的构造函数,用于初始化对象的属性。new
关键字调用时会自动执行构造函数。 - 方法 :类中的方法不需要
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
解析:
- **
extends
**:用于创建子类,继承父类的属性和方法。 - **
super
**:在子类的构造函数中调用super()
,用于调用父类的构造函数。super
必须在this
之前调用,否则会报错。 - 方法重写 :子类可以重写父类的方法(如
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 支持通过 get
和 set
关键字定义属性的 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 更加适合大型项目的开发,同时也为开发者提供了更清晰和直观的语法。