JavaScript继承深度解析

JavaScript继承深度解析:从ES5到ES6的全方位指南

在JavaScript的面向对象编程中,继承是一个核心概念。它不仅能够帮助我们减少重复代码,更是实现多态的重要前提。本文将从ES5的原型链继承讲起,逐步深入到ES6的class语法糖,带你全面理解JavaScript中的继承机制。

📖 目录

  1. 引言:JavaScript面向对象与继承的重要性
  2. ES5中的继承实现
  3. ES6中的继承实现
  4. JavaScript中的多态
  5. TypeScript中的继承扩展
  6. 总结与最佳实践

一、引言:JavaScript面向对象与继承的重要性 {#引言}

1.1 面向对象的三大特性

面向对象编程有三大特性:封装继承多态

  • 封装:将属性和方法封装到一个类中,实现数据和行为的组织
  • 继承:让子类直接使用父类的属性和方法,减少重复代码
  • 多态:不同对象在执行相同操作时表现出不同的行为

1.2 为什么继承如此重要?

在软件开发中,我们经常需要创建多个相似但又有差异的类。比如:

javascript 复制代码
// 没有继承时:大量重复代码
class Dog {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    eat() { console.log('狗狗吃东西'); }
    sleep() { console.log('狗狗睡觉'); }
    bark() { console.log('汪汪汪'); }
}

class Cat {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    eat() { console.log('猫咪吃东西'); }  // 重复代码
    sleep() { console.log('猫咪睡觉'); }  // 重复代码
    meow() { console.log('喵喵喵'); }
}

通过继承,我们可以将这些共同的特性抽取到父类中,子类只需要关注自己独有的部分。

1.3 JavaScript继承的特殊性

重要提示 :JavaScript的继承与Java、C++等传统面向对象语言有本质区别。JavaScript使用的是原型链继承 机制,而不是类继承。即使ES6引入了class关键字,它也仅仅是构造函数和原型链的语法糖

理解这一点非常重要------只有深入理解原型链,才能真正掌握JavaScript的继承机制。


二、ES5中的继承实现 {#es5继承}

2.1 理解原型链基础

在深入继承之前,我们需要先理解JavaScript中对象和函数的原型机制。

2.1.1 对象的原型 [[prototype]]

JavaScript中每个对象都有一个特殊的内置属性 [[prototype]],这个属性可以指向另一个对象。

javascript 复制代码
// 字面量方式创建对象
const obj = { name: '张三' };

// 获取对象原型的两种方式
// 方式一:通过 __proto__ 属性(不推荐,存在兼容性问题)
console.log(obj.__proto__);

// 方式二:通过 Object.getPrototypeOf 方法(推荐)
console.log(Object.getPrototypeOf(obj));

内存表现

复制代码
┌─────────────────────────────────────────┐
│            obj 对象                      │
│  ┌─────────────────────────────────┐     │
│  │ name: "张三"                    │     │
│  │ [[Prototype]] ────────────────┼─┼──┐  │
│  └─────────────────────────────────┘   │  │
│                                         │  │
│  ┌─────────────────────────────────┐   │  │
│  │ Object.prototype 对象           │◄──┘  │
│  │ toString: ƒ                     │      │
│  │ valueOf: ƒ                      │      │
│  │ hasOwnProperty: ƒ               │      │
│  │ ...                             │      │
│  └─────────────────────────────────┘      │
└─────────────────────────────────────────┘

当访问对象的属性时,如果对象本身没有这个属性,JavaScript会自动沿着 [[prototype]] 链向上查找,直到找到或到达 Object.prototype(其 [[prototype]]null)。

2.1.2 函数的原型 prototype

所有函数都有一个特殊的 prototype 属性,这个属性指向一个对象。

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// prototype 是函数特有的属性
console.log(typeof Person.prototype); // "object"

关键点prototype 不是因为函数是对象才有的,而是因为它是一个函数才有的这个属性。

2.1.3 constructor 属性

原型对象上有一个默认的 constructor 属性,它指向构造函数本身:

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}

console.log(Person.prototype.constructor === Person); // true

如果我们重写原型对象,需要手动修复 constructor 指向:

javascript 复制代码
// 错误做法:constructor 指向 Object
Person.prototype = {
    constructor: Person, // 需要手动修复
    sayHello: function() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

// 推荐做法:使用 Object.defineProperty
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Person
});
2.1.4 new 操作符的原理

使用 new 关键字调用构造函数时,会执行以下步骤:

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

const p = new Person('张三');

执行过程

  1. 在内存中创建一个新的空对象
  2. 这个对象的 [[prototype]] 被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 指向新创建的对象
  4. 执行构造函数内部的代码
  5. 返回新创建的对象(除非构造函数显式返回另一个对象)

内存图示

复制代码
┌────────────────────────────────────────────────────────────┐
│                                                             │
│   function Person() {}                                      │
│          │                                                 │
│          ▼                                                 │
│   Person.prototype ──────────────────┐                     │
│          │                           │                     │
│          │  constructor ─────────────┤                     │
│          │                           │                     │
│          ▼                           ▼                     │
│   ┌─────────────────┐       ┌─────────────────┐           │
│   │ 原型对象        │       │ p 对象           │           │
│   │ (0xa00)         │       │ (0x200)          │           │
│   │                 │       │                 │           │
│   │                 │◄──┐    │ name: "张三"    │           │
│   │                 │   │    │ [[Prototype]]───┘           │
│   └─────────────────┘   │    └─────────────────┘           │
│                         │                                   │
└─────────────────────────┼───────────────────────────────────┘

2.2 原型链继承(Prototype Chain Inheritance)

2.2.1 实现原理

原型链继承的核心思想是:让子类的原型对象等于父类的实例

javascript 复制代码
// 定义父类
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.running = function() {
    console.log(`${this.name} is running`);
};

Person.prototype.eating = function() {
    console.log(`${this.name} is eating`);
};

// 定义子类
function Student(sno) {
    this.sno = sno;
}

// 核心:让子类的原型等于父类的实例
Student.prototype = new Person();

// 修复子类的 constructor 指向
Student.prototype.constructor = Student;

// 给子类添加独有的方法
Student.prototype.studying = function() {
    console.log(`${this.name} is studying, 学号: ${this.sno}`);
};

// 测试
const stu = new Student('张三', 18, '001');
stu.running(); // "张三 is running"
stu.eating();   // "张三 is eating"
stu.studying(); // "张三 is studying, 学号: 001"
2.2.2 内存结构图
复制代码
┌────────────────────────────────────────────────────────────────────┐
│                        原型链继承内存结构                           │
├────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  function Person()        function Student()                        │
│         │                       │                                  │
│         ▼                       ▼                                  │
│  Person.prototype ──→  Student.prototype                          │
│         ▲                       │                                  │
│         │   new Person()        │                                  │
│         │                       ▼                                  │
│         │              ┌─────────────────┐                         │
│         │              │ Person 实例对象  │                         │
│         │              │ (作为原型)       │                         │
│         │              │                  │                         │
│         │              │ name: undefined  │                         │
│         │              │ age: undefined   │                         │
│         │              │ [[Prototype]]────┼────→ Person.prototype   │
│         │              │ running: ƒ       │                         │
│         │              │ eating: ƒ       │                         │
│         │              └─────────────────┘                         │
│         │                                                         │
│  ┌──────┴───────┐          ┌─────────────────┐                     │
│  │Person.prototype│          │ Student 实例     │                     │
│  │               │◄──┐      │                  │                     │
│  │ constructor ───┘   │      │ sno: "001"       │                     │
│  │ running: ƒ        │      │ [[Prototype]]─────┘                     │
│  │ eating: ƒ         │      │                  │                     │
│  └───────────────────┘      │ studying: ƒ      │                     │
│         ▲                   └─────────────────┘                     │
│         │                                                         │
│  ┌──────┴───────┐                                                 │
│  │ Object.proto  │                                                 │
│  │               │                                                 │
│  │ toString: ƒ   │                                                 │
│  │ ...           │                                                 │
│  └───────────────┘                                                 │
│         ▲                                                          │
│         │                                                         │
│  [Object: null prototype] {}                                       │
│                                                                     │
└────────────────────────────────────────────────────────────────────┘
2.2.3 原型链查找顺序

当访问 stu.running 时,JavaScript会按照以下顺序查找:

  1. stu 对象本身查找 → 没找到
  2. stu.__proto__(即 Student.prototype)查找 → 没找到
  3. stu.__proto__.__proto__(即 Person.prototype)查找 → 找到了!
2.2.4 原型链继承的弊端

⚠️ 原型链继承存在三个重要问题

javascript 复制代码
// 问题一:引用类型属性被多个实例共享
function Parent() {
    this.hobbies = ['读书', '运动'];
}

function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child1 = new Child();
child1.hobbies.push('编程');
console.log(child1.hobbies); // ['读书', '运动', '编程']

const child2 = new Child();
console.log(child2.hobbies); // ['读书', '运动', '编程'] ⚠️ 被污染了!
javascript 复制代码
// 问题二:无法给父类构造函数传递参数
function Person(name) {
    this.name = name;
}

function Student(sno) {
    this.sno = sno;
}
Student.prototype = new Person(); // name 是 undefined
Student.prototype.constructor = Student;

const stu = new Student('001');
console.log(stu.name); // undefined ⚠️ 无法获取到参数
javascript 复制代码
// 问题三:创建子类实例时,无法初始化父类属性
const stu = new Student('002');
// 父类的 name 和 age 属性无法被正确初始化
2.2.5 应用场景

原型链继承适用于:

  • 场景一:需要创建大量对象,且这些对象共享相同的方法
  • 场景二:父类和子类的属性都是基本类型,不需要在构造函数中定制
  • 场景三:原型链继承是其他继承方式的基础

2.3 借用构造函数继承(Constructor Stealing)

2.3.1 实现原理

借用构造函数继承(也称为经典继承伪造对象 )的核心思想是:在子类构造函数内部调用父类构造函数

javascript 复制代码
// 定义父类
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.hobbies = ['读书', '运动'];
}

Person.prototype.running = function() {
    console.log(`${this.name} is running`);
};

// 定义子类
function Student(name, age, sno) {
    // 核心:在子类构造函数中调用父类构造函数
    Person.call(this, name, age);  // 或 Person.apply(this, arguments)
    this.sno = sno;
}

// 继承父类的原型方法(但通过原型链方式)
Student.prototype = new Person();
Student.prototype.constructor = Student;

// 添加子类独有的方法
Student.prototype.studying = function() {
    console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};

// 测试
const stu1 = new Student('张三', 18, '001');
stu1.hobbies.push('编程');
console.log(stu1.hobbies); // ['读书', '运动', '编程']

const stu2 = new Student('李四', 20, '002');
console.log(stu2.hobbies); // ['读书', '运动'] ⚠️ 引用类型问题已解决!

stu1.running(); // "张三 is running" ⚠️ 父类方法可以调用
2.3.2 call 和 apply 的区别
javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}

function Student(name, age, sno) {
    // call 方式:逐个传递参数
    Person.call(this, name, age);
    
    // apply 方式:传递参数数组
    // Person.apply(this, [name, age]);
    
    // 特殊用法:传递 arguments 对象
    // Person.apply(this, arguments);
    
    this.sno = sno;
}
2.3.3 借用构造函数继承的优缺点

优点

✅ 解决了引用类型属性被共享的问题

✅ 可以在子类构造函数中向父类传递参数

✅ 每个子类实例都有独立的父类属性副本

缺点

❌ 父类的方法无法复用(每个实例都会创建一份新的方法)

❌ 父类的原型方法对子类实例不可见

❌ 函数调用了两次(一次在继承时,一次在创建实例时)

javascript 复制代码
// 问题演示:父类方法不可复用
function Person(name) {
    this.name = name;
    // 每次 new Person 都会创建新的 sayHello 函数
    this.sayHello = function() {
        console.log(`Hello, I'm ${this.name}`);
    };
}

function Student(name, sno) {
    Person.call(this, name);
    this.sno = sno;
}

const stu = new Student('张三', '001');
// stu.sayHello 是独立函数,不是共享的方法

2.4 组合继承(Combination Inheritance)

2.4.1 实现原理

组合继承是JavaScript中最常用的继承模式,结合了原型链继承和借用构造函数继承的优点。

javascript 复制代码
// 定义父类
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.hobbies = ['读书', '运动'];
}

Person.prototype.running = function() {
    console.log(`${this.name} is running`);
};

Person.prototype.eating = function() {
    console.log(`${this.name} is eating`);
};

// 定义子类
function Student(name, age, sno) {
    // 第二次调用父类构造函数:为每个实例初始化独立的属性
    Person.call(this, name, age);
    this.sno = sno;
}

// 第一次调用父类构造函数:创建父类实例作为子类原型
Student.prototype = new Person();
Student.prototype.constructor = Student;

// 添加子类独有的方法
Student.prototype.studying = function() {
    console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};

// 测试
const stu1 = new Student('张三', 18, '001');
const stu2 = new Student('李四', 20, '002');

// 验证引用类型独立性
stu1.hobbies.push('编程');
console.log(stu1.hobbies); // ['读书', '运动', '编程']
console.log(stu2.hobbies); // ['读书', '运动'] ✓ 独立

// 验证原型方法可访问
stu1.running(); // "张三 is running" ✓
stu1.studying(); // "张三 (学号: 001) 正在学习" ✓
2.4.2 组合继承的内存结构
复制代码
┌────────────────────────────────────────────────────────────────────┐
│                        组合继承内存结构                             │
├────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌─────────────────┐              ┌─────────────────┐            │
│   │ Student 实例    │              │ Student.prototype │            │
│   │ stu1 (0x200)    │              │ (Person 实例)     │            │
│   │                 │              │                  │            │
│   │ name: "张三"    │              │ name: undefined  │ ←─┐        │
│   │ age: 18         │              │ age: undefined   │ ←─┤ 第一次  │
│   │ hobbies: [...]  │              │ hobbies: [...]   │ ←─┤ 调用    │
│   │ sno: "001"      │              │ [[Prototype]]────┼─►│ Person  │
│   │ [[Prototype]]───┼──────┐       │ running: ƒ       │   │        │
│   └─────────────────┘      │       │ eating: ƒ        │   │        │
│                            │       │ studying: ƒ      │            │
│                            │       └─────────────────┘            │
│                            │                ▲                     │
│                            └────────────────┘                     │
│                                          │                        │
│                                          ▼                        │
│                               ┌─────────────────┐                  │
│                               │ Person.prototype │                  │
│                               │                  │                  │
│                               │ constructor ─────┤                  │
│                               │ running: ƒ       │                  │
│                               │ eating: ƒ        │                  │
│                               └─────────────────┘                  │
│                                                                     │
│   问题:name、age、hobbies 在两处存在!                            │
│   - Student.prototype 上有一份(undefined/[...])                 │
│   - stu1/stu2 实例上有一份("张三"/18/["读书"...])                │
│                                                                     │
└────────────────────────────────────────────────────────────────────┘
2.4.3 组合继承的问题

⚠️ 组合继承最大的问题是:父类构造函数被调用了两次

  1. 第一次:Student.prototype = new Person()
  2. 第二次:Person.call(this, name, age)

这导致:

  • 父类属性在原型对象上有一份(浪费内存)
  • 父类属性在子类实例上也有一份
  • 虽然访问时会优先读取实例上的属性,但原型上的属性确实存在

2.5 原型式继承(Prototypal Inheritance)

2.5.1 渊源

原型式继承由道格拉斯·克罗克福德(Douglas Crockford,JSON的创立者)在2006年提出。

2.5.2 实现原理

原型式继承的核心思想是:通过一个临时构造函数,让新对象的原型指向已有对象

javascript 复制代码
// 原始原型式继承函数
function createObject(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

// 现代等价实现:Object.create()
const person = {
    name: '张三',
    age: 18,
    hobbies: ['读书', '运动'],
    running: function() {
        console.log(`${this.name} is running`);
    }
};

const student = Object.create(person);
student.name = '李四';
student.sno = '001';

console.log(student.name);      // "李四"(自有属性)
console.log(student.hobbies);    // ['读书', '运动'](继承属性)
student.running();               // "李四 is running"
console.log(student.__proto__ === person); // true
2.5.3 Object.create 的 Polyfill
javascript 复制代码
if (!Object.create) {
    Object.create = function(o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}
2.5.4 应用场景

原型式继承适用于:

  • 只需要继承已有对象的属性和方法
  • 不需要创建"类"
  • 类似于浅拷贝,但保持了原型链关系
javascript 复制代码
// 场景:创建多个相似对象
const animalBase = {
    type: '动物',
    diet: '杂食',
    sleep: function() {
        console.log(`${this.name} 在睡觉`);
    }
};

const dog = Object.create(animalBase);
dog.name = '旺财';
dog.bark = function() {
    console.log('汪汪汪');
};

const cat = Object.create(animalBase);
cat.name = '咪咪';
cat.meow = function() {
    console.log('喵喵喵');
};

dog.sleep(); // "旺财 在睡觉"
cat.sleep(); // "咪咪 在睡觉"

2.6 寄生式继承(Parasitic Inheritance)

2.6.1 实现原理

寄生式继承是原型式继承的增强版,结合了工厂模式的思想:创建一个封装继承过程的函数,在内部增强对象,最后返回

javascript 复制代码
// 寄生式继承函数
function createStudent(person, sno) {
    // 通过原型式继承创建对象
    const student = Object.create(person);
    
    // 增强对象:添加子类特有的属性和方法
    student.sno = sno;
    student.studying = function() {
        console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
    };
    
    return student;
}

// 基类对象
const person = {
    name: '默认姓名',
    age: 0,
    running: function() {
        console.log(`${this.name} is running`);
    }
};

// 使用寄生式继承创建学生对象
const stu1 = createStudent(person, '001');
const stu2 = createStudent(person, '002');

stu1.name = '张三';
stu1.studying(); // "张三 (学号: 001) 正在学习"
stu2.name = '李四';
stu2.studying(); // "李四 (学号: 002) 正在学习"
2.6.2 寄生式继承的改进版本
javascript 复制代码
function createStudent(person, name, age, sno) {
    // 原型式继承
    const student = Object.create(person);
    
    // 增强:添加/覆盖属性
    student.name = name;
    student.age = age;
    student.sno = sno;
    
    // 增强:添加方法
    student.studying = function() {
        console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
    };
    student.takeExam = function() {
        console.log(`${this.name} 正在参加考试`);
    };
    
    return student;
}

const personProto = {
    running: function() {
        console.log(`${this.name} is running`);
    },
    eating: function() {
        console.log(`${this.name} is eating`);
    }
};

const stu = createStudent(personProto, '王五', 20, '003');
stu.running();    // "王五 is running"
stu.eating();     // "王五 is eating"
stu.studying();   // "王五 (学号: 003) 正在学习"
stu.takeExam();   // "王五 正在参加考试"
2.6.3 寄生式继承的优缺点

优点

✅ 可以在继承时增强对象,添加额外的方法和属性

✅ 代码封装性好,易于理解

✅ 灵活性高,可以根据需要定制继承行为

缺点

❌ 方法需要在每次继承时重新创建,无法复用

❌ 增强的方法没有放在原型上,浪费内存

❌ 与构造函数模式类似,存在函数重复创建的问题

2.7 寄生组合式继承(Parasitic Combination Inheritance)⭐

2.7.1 为什么需要寄生组合式继承?

回顾组合继承的问题:

  1. 父类构造函数被调用两次:一次创建子类原型,一次创建实例
  2. 父类属性存在两份:一份在原型上,一份在实例上

寄生组合式继承正是为了解决这两个问题。

2.7.2 实现原理

核心思想:不再让子类的原型等于父类的实例,而是等于父类原型的副本

javascript 复制代码
// 寄生组合式继承的核心函数
function inheritPrototype(SubType, SuperType) {
    // 创建父类原型对象的副本
    const prototype = Object.create(SuperType.prototype);
    
    // 修复 constructor 指向
    prototype.constructor = SubType;
    
    // 将副本设置为子类原型
    SubType.prototype = prototype;
}

// 定义父类
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.hobbies = ['读书', '运动'];
}

Person.prototype.running = function() {
    console.log(`${this.name} is running`);
};

Person.prototype.eating = function() {
    console.log(`${this.name} is eating`);
};

// 定义子类
function Student(name, age, sno) {
    // 只在实例创建时调用一次父类构造函数
    Person.call(this, name, age);
    this.sno = sno;
}

// 使用寄生组合式继承
inheritPrototype(Student, Person);

// 添加子类独有的方法
Student.prototype.studying = function() {
    console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
};

// 测试
const stu1 = new Student('张三', 18, '001');
const stu2 = new Student('李四', 20, '002');

// 验证引用类型独立性
stu1.hobbies.push('编程');
console.log(stu1.hobbies); // ['读书', '运动', '编程']
console.log(stu2.hobbies); // ['读书', '运动'] ✓ 独立

// 验证原型方法可访问
stu1.running();   // "张三 is running"
stu1.studying();  // "张三 (学号: 001) 正在学习"

// 验证原型链正确
console.log(stu1 instanceof Student); // true
console.log(stu1 instanceof Person);  // true
console.log(stu1 instanceof Object);   // true
2.7.3 内存结构对比

组合继承的内存结构

复制代码
Student.prototype → Person实例(有name,age,hobbies) → Person.prototype
                    ↑
                    浪费内存:name/age/hobbies 重复存储

寄生组合式继承的内存结构

复制代码
Student.prototype → Person.prototype副本(无name/age/hobbies) → Person.prototype
2.7.4 寄生组合式继承的优势

只调用一次父类构造函数 :性能最优

原型属性不重复 :避免内存浪费

原型链完整 :instanceof 和 isPrototypeOf 正常工作

原型方法可复用:方法定义在原型上,所有实例共享

2.7.5 完整的寄生组合式继承封装
javascript 复制代码
/**
 * 寄生组合式继承 - 完整封装
 * @param {Function} SubType 子类构造函数
 * @param {Function} SuperType 父类构造函数
 */
function inheritPrototype(SubType, SuperType) {
    // 1. 创建父类原型对象的副本
    const prototype = Object.create(SuperType.prototype);
    
    // 2. 添加 constructor 属性,防止丢失
    prototype.constructor = SubType;
    
    // 3. 将副本赋值给子类原型
    SubType.prototype = prototype;
}

// 父类
function Animal(name) {
    this.name = name;
    this.colors = ['black', 'white'];
}

Animal.prototype.move = function() {
    console.log(`${this.name} is moving`);
};

// 子类
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

// 继承
inheritPrototype(Dog, Animal);

// 子类方法
Dog.prototype.bark = function() {
    console.log(`${this.name} is barking: 汪汪汪`);
};

// 测试
const dog1 = new Dog('旺财', '金毛');
const dog2 = new Dog('咪咪', '柯基');

dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'] ✓ 独立

dog1.move();  // "旺财 is moving"
dog1.bark();  // "旺财 is barking: 汪汪汪"

2.8 ES5继承方式对比总结

继承方式 优点 缺点 适用场景
原型链继承 实现简单,方法可复用 引用类型共享,无法传参 简单场景
借用构造函数 可传参,引用类型独立 方法不可复用 需要定制属性
组合继承 综合两者优点 调用两次构造函数 最常用
原型式继承 简洁,无需构造函数 方法不可复用 快速对象创建
寄生式继承 可增强对象 方法不可复用 定制继承对象
寄生组合式继承 性能最优,结构清晰 实现稍复杂 最佳实践

三、ES6中的继承实现 {#es6继承}

3.1 class 语法糖的本质

ES6引入了 class 关键字,但它仅仅是JavaScript构造函数和原型链的语法糖

javascript 复制代码
// ES6 class 写法
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    running() {
        console.log(`${this.name} is running`);
    }
    
    eating() {
        console.log(`${this.name} is eating`);
    }
}

// 等价的 ES5 构造函数写法
function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.running = function() {
    console.log(`${this.name} is running`);
};

Person.prototype.eating = function() {
    console.log(`${this.name} is eating`);
};
3.1.1 class 的声明方式
javascript 复制代码
// 方式一:类声明(不能提升,类似 const/let)
class Person {
    constructor(name) {
        this.name = name;
    }
}

// 方式二:类表达式
const Animal = class {
    constructor(type) {
        this.type = type;
    }
};

// 方式三:命名类表达式
const Vehicle = class Car {
    constructor(brand) {
        this.brand = brand;
    }
};
3.1.2 class 的特性
javascript 复制代码
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // 实例方法 - 定义在原型上
    sayHello() {
        console.log(`Hello, I'm ${this.name}`);
    }
    
    // 静态方法 - 定义在类本身
    static create(name, age) {
        return new Person(name, age);
    }
    
    // getter 和 setter
    get info() {
        return `${this.name}, ${this.age}岁`;
    }
    
    set info(value) {
        const [name, age] = value.split(',');
        this.name = name;
        this.age = parseInt(age);
    }
}

const p = Person.create('张三', 18);
p.sayHello();          // "Hello, I'm 张三"
console.log(p.info);   // "张三, 18岁"
p.info = '李四, 20';
console.log(p.info);   // "李四, 20岁"
3.1.3 class 的本质验证
javascript 复制代码
class Person {
    constructor(name) {
        this.name = name;
    }
}

console.log(typeof Person);                    // "function"
console.log(Person === Person.prototype.constructor); // true

// 所有方法都定义在 prototype 上
console.log(Person.prototype.sayHello);       // undefined(没有定义)

3.2 extends 关键字的使用

ES6使用 extends 关键字实现继承,比ES5的实现简洁得多。

3.2.1 基础用法
javascript 复制代码
// 父类
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    running() {
        console.log(`${this.name} is running`);
    }
    
    eating() {
        console.log(`${this.name} is eating`);
    }
}

// 子类使用 extends 继承父类
class Student extends Person {
    constructor(name, age, sno) {
        // 调用父类构造函数
        super(name, age);  // 必须先调用 super
        this.sno = sno;
    }
    
    studying() {
        console.log(`${this.name} (学号: ${this.sno}) 正在学习`);
    }
}

// 测试
const stu = new Student('张三', 18, '001');
stu.running();   // "张三 is running" - 继承自父类
stu.eating();    // "张三 is eating" - 继承自父类
stu.studying();  // "张三 (学号: 001) 正在学习" - 子类自有
3.2.2 extends 的原型链关系
javascript 复制代码
class Person {}
class Student extends Person {}

console.log(Student.prototype.__proto__ === Person.prototype); // true

const stu = new Student();
console.log(stu.__proto__.__proto__ === Person.prototype);      // true
console.log(stu instanceof Student);                             // true
console.log(stu instanceof Person);                              // true
3.2.3 继承的内存结构(ES6)
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    ES6 Class 继承内存结构                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  class Person                     class Student                  │
│         │                               │                        │
│         ▼                               ▼                        │
│  Person.prototype ──────────────► Student.prototype            │
│         ▲                               │                        │
│         │                               │ extends                │
│         │                               ▼                        │
│  ┌──────┴───────┐             ┌─────────────────┐               │
│  │ Person.prototype│            │ [[Prototype]]──┼──→ Person   │
│  │               │            │ constructor ─────┤   prototype  │
│  │ constructor ──┼──────┐     │                 │               │
│  │               │      │     │ studying: ƒ     │               │
│  └──────┬───────┘      │     └─────────────────┘               │
│         │              │                    ▲                   │
│         │              │                    │                   │
│         ▼              │                    │                   │
│  ┌───────────────┐    │                    │                   │
│  │Object.prototype│◄───┴────────────────────┘                   │
│  │               │                                               │
│  └───────────────┘                                               │
│         ▲                                                         │
│         │                                                         │
│  [Object: null prototype] {}                                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

3.3 super 关键字的三种使用场景

super 是ES6继承中最重要的关键字,它有三种使用场景。

3.3.1 在构造函数中使用 super
javascript 复制代码
class Parent {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

class Child extends Parent {
    constructor(name, age, school) {
        // 必须先调用 super,才能使用 this
        super(name, age);
        this.school = school;
    }
}

const child = new Child('张三', 10, '第一小学');
console.log(child.name);    // "张三"
console.log(child.age);     // 10
console.log(child.school);  // "第一小学"

⚠️ 重要规则

  • 在子类构造函数中,必须先调用 super() 才能使用 this
  • 如果定义了构造函数,必须调用 super()
  • 如果不调用 super(),JavaScript会报错
javascript 复制代码
// 错误示例
class Child extends Parent {
    constructor(name, age) {
        // 报错:Must call super constructor before using 'this'
        this.name = name;
        this.age = age;
    }
}
3.3.2 在实例方法中使用 super
javascript 复制代码
class Person {
    constructor(name) {
        this.name = name;
    }
    
    greet() {
        return `你好,我是 ${this.name}`;
    }
}

class Student extends Person {
    constructor(name, grade) {
        super(name);
        this.grade = grade;
    }
    
    // 在实例方法中使用 super 调用父类方法
    greet() {
        // 调用父类的 greet 方法
        const parentGreeting = super.greet();
        return `${parentGreeting},我是 ${this.grade} 年级学生`;
    }
}

const student = new Student('小明', 5);
console.log(student.greet()); 
// "你好,我是 小明,我是 5 年级学生"
3.3.3 在静态方法中使用 super
javascript 复制代码
class Person {
    static create(name, age) {
        return new Person(name, age);
    }
}

class Student extends Person {
    static create(name, age, grade) {
        // 调用父类的静态方法
        const person = super.create(name, age);
        person.grade = grade;
        return person;
    }
}

const student = Student.create('张三', 15, 3);
console.log(student instanceof Student); // true
console.log(student.grade);               // 3
3.3.4 super 使用总结
使用场景 语法 说明
构造函数 super(args) 调用父类构造函数,必须先于 this 调用
实例方法 super.method(args) 调用父类的实例方法
静态方法 super.staticMethod(args) 调用父类的静态方法

3.4 继承内置类

ES6允许我们继承JavaScript的内置类(如Array、Map、Set等)。

3.4.1 继承 Array
javascript 复制代码
// 创建一个只包含偶数的数组类
class EvenArray extends Array {
    constructor(...args) {
        // 过滤出偶数
        const evenArgs = args.filter(n => n % 2 === 0);
        super(...evenArgs);
    }
    
    // 添加元素时自动过滤奇数
    push(...items) {
        const evenItems = items.filter(n => n % 2 === 0);
        super.push(...evenItems);
    }
}

const evenNums = new EvenArray(1, 2, 3, 4, 5, 6);
console.log(evenNums);          // [2, 4, 6]
console.log(evenNums.length);  // 3

evenNums.push(7, 8, 9);
console.log(evenNums);          // [2, 4, 6, 8] - 7和9被过滤

// 继承原生方法
console.log(evenNums.filter(n => n > 3)); // [4, 6, 8]
console.log(evenNums.map(n => n * 2));    // [4, 8, 12, 16]
3.4.2 继承 Map
javascript 复制代码
class PersistentMap extends Map {
    constructor() {
        super();
        this.storage = [];
    }
    
    set(key, value) {
        super.set(key, value);
        this.storage.push({ key, value });
        return this;
    }
    
    getHistory() {
        return this.storage;
    }
}

const map = new PersistentMap();
map.set('name', '张三');
map.set('age', 18);
console.log(map.get('name')); // "张三"
console.log(map.getHistory()); // [{key: 'name', value: '张三'}, {key: 'age', value: 18}]
3.4.3 继承 Error
javascript 复制代码
class CustomError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = 'CustomError';
        this.statusCode = statusCode;
        // 修复 V8 原型链的堆栈追踪
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, CustomError);
        }
    }
}

try {
    throw new CustomError('出错了!', 500);
} catch (e) {
    console.log(e.message);     // "出错了!"
    console.log(e.statusCode);  // 500
    console.log(e.name);       // "CustomError"
}

3.5 混入(Mixin)实现多继承

JavaScript的类只支持单继承(只有一个父类)。当我们需要在一个类中添加多个类的功能时,可以使用Mixin模式。

3.5.1 Mixin 的基本实现
javascript 复制代码
// 定义两个 Mixin
const FlyMixin = {
    fly() {
        console.log(`${this.name} is flying`);
    }
};

const SwimMixin = {
    swim() {
        console.log(`${this.name} is swimming`);
    }
};

const WalkMixin = {
    walk() {
        console.log(`${this.name} is walking`);
    }
};

// Mixin 函数
function mixin(target, ...mixins) {
    Object.assign(target.prototype, ...mixins);
}

// 创建类
class Animal {
    constructor(name) {
        this.name = name;
    }
}

// 混入多个Mixin的功能
mixin(Animal, FlyMixin, SwimMixin, WalkMixin);

const duck = new Animal('鸭子');
duck.fly();   // "鸭子 is flying"
duck.swim();  // "鸭子 is swimming"
duck.walk();  // "鸭子 is walking"
3.5.2 ES6 Class Mixin 写法
javascript 复制代码
// 第一个Mixin
const Flyable = (SuperClass) => class extends SuperClass {
    fly() {
        console.log(`${this.name} is flying`);
    }
};

// 第二个Mixin
const Swimmable = (SuperClass) => class extends SuperClass {
    swim() {
        console.log(`${this.name} is swimming`);
    }
};

// 第三个Mixin
const Walkable = (SuperClass) => class extends SuperClass {
    walk() {
        console.log(`${this.name} is walking`);
    }
};

// 多重继承:通过组合多个Mixin
class Animal {
    constructor(name) {
        this.name = name;
    }
}

// 从右到左依次混入
class Duck extends Walkable(Swimmable(Flyable(Animal))) {
    quack() {
        console.log(`${this.name} is quacking: 嘎嘎嘎`);
    }
}

const duck = new Duck('唐老鸭');
duck.fly();    // "唐老鸭 is flying"
duck.swim();   // "唐老鸭 is swimming"
duck.walk();   // "唐老鸭 is walking"
duck.quack();  // "唐老鸭 is quacking: 嘎嘎嘎"
3.5.3 多个Mixin的场景示例
javascript 复制代码
// 可绑定的Mixin
const Bindable = (Base) => class extends Base {
    bind(event, handler) {
        if (!this._handlers) this._handlers = {};
        if (!this._handlers[event]) this._handlers[event] = [];
        this._handlers[event].push(handler);
        return this;
    }
    
    trigger(event, data) {
        if (this._handlers && this._handlers[event]) {
            this._handlers[event].forEach(handler => handler(data));
        }
        return this;
    }
};

// 可序列化的Mixin
const Serializable = (Base) => class extends Base {
    toJSON() {
        const obj = {};
        for (const key in this) {
            if (this.hasOwnProperty(key)) {
                obj[key] = this[key];
            }
        }
        return obj;
    }
    
    static fromJSON(json) {
        return Object.assign(new this(), json);
    }
};

// 可验证的Mixin
const Validatable = (Base) => class extends Base {
    validate(rules) {
        for (const [field, validator] of Object.entries(rules)) {
            if (!validator(this[field])) {
                throw new Error(`Validation failed for ${field}`);
            }
        }
        return true;
    }
};

// 组合使用
class User extends Validatable(Serializable(Bindable(Object))) {
    constructor(name, email) {
        super();
        this.name = name;
        this.email = email;
    }
}

const user = new User('张三', 'zhangsan@example.com');
user.bind('save', () => console.log('用户已保存'));
user.trigger('save');

const json = user.toJSON();
console.log(json);

const restored = User.fromJSON(json);
console.log(restored.name); // "张三"

四、JavaScript中的多态 {#多态}

4.1 什么是多态?

多态(Polymorphism)是面向对象编程的三大特性之一。维基百科的定义:

多态指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。

简单理解:不同对象在执行相同操作时,表现出不同的行为。

4.2 JavaScript中的多态实现

JavaScript天然支持多态,因为它的弱类型和动态类型特性。

4.2.1 基于继承的多态
javascript 复制代码
class Shape {
    area() {
        throw new Error('子类必须重写 area() 方法');
    }
}

class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }
    
    area() {
        return Math.PI * this.radius * this.radius;
    }
}

class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    
    area() {
        return this.width * this.height;
    }
}

class Triangle extends Shape {
    constructor(base, height) {
        super();
        this.base = base;
        this.height = height;
    }
    
    area() {
        return 0.5 * this.base * this.height;
    }
}

// 多态:同一个函数处理不同类型的对象
function printArea(shape) {
    console.log(`面积: ${shape.area()}`);
}

const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
const triangle = new Triangle(3, 4);

printArea(circle);     // "面积: 78.53981633974483"
printArea(rectangle); // "面积: 24"
printArea(triangle);   // "面积: 6"
4.2.2 基于参数类型的多态
javascript 复制代码
function format(value) {
    if (typeof value === 'string') {
        return value.toUpperCase();
    } else if (typeof value === 'number') {
        return value.toFixed(2);
    } else if (Array.isArray(value)) {
        return value.join(', ');
    } else if (typeof value === 'object') {
        return JSON.stringify(value);
    }
    return String(value);
}

console.log(format('hello'));     // "HELLO"
console.log(format(3.14159));      // "3.14"
console.log(format([1, 2, 3]));    // "1, 2, 3"
console.log(format({a: 1}));       // "{"a":1}"
4.2.3 多态的实际应用
javascript 复制代码
class Animal {
    speak() {
        throw new Error('子类必须实现 speak() 方法');
    }
}

class Dog extends Animal {
    speak() {
        return '汪汪汪';
    }
}

class Cat extends Animal {
    speak() {
        return '喵喵喵';
    }
}

class Cow extends Animal {
    speak() {
        return '哞哞哞';
    }
}

// 农场主给所有动物喂食
function feedAnimals(animals) {
    animals.forEach(animal => {
        console.log(`喂食: ${animal.speak()}`);
    });
}

const animals = [
    new Dog(),
    new Cat(),
    new Cow(),
    new Dog()
];

feedAnimals(animals);
// 喂食: 汪汪汪
// 喂食: 喵喵喵
// 喂食: 哞哞哞
// 喂食: 汪汪汪

五、TypeScript中的继承扩展 {#typescript}

5.1 TypeScript 类的基本继承

TypeScript在ES6的基础上增强了类型系统。

typescript 复制代码
// 基类
class Person {
    protected name: string;
    protected age: number;
    
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    
    greet(): string {
        return `你好,我是 ${this.name}`;
    }
}

// 子类
class Student extends Person {
    private grade: number;
    
    constructor(name: string, age: number, grade: number) {
        super(name, age);
        this.grade = grade;
    }
    
    greet(): string {
        return `${super.greet()},我是 ${this.grade} 年级学生`;
    }
}

const student = new Student('张三', 15, 3);
console.log(student.greet()); // "你好,我是 张三,我是 3 年级学生"

5.2 访问修饰符

修饰符 类自身 子类 其他
public
protected
private
readonly ✅ (仅读)
typescript 复制代码
class Base {
    public publicProp = 'public';
    protected protectedProp = 'protected';
    private privateProp = 'private';
    readonly readonlyProp = 'readonly';
}

class Derived extends Base {
    accessProps() {
        console.log(this.publicProp);     // ✅ OK
        console.log(this.protectedProp);  // ✅ OK
        // console.log(this.privateProp); // ❌ 错误
        console.log(this.readonlyProp);   // ✅ OK
    }
}

const instance = new Base();
console.log(instance.publicProp);        // ✅ OK
// console.log(instance.protectedProp);  // ❌ 错误
// console.log(instance.privateProp);   // ❌ 错误

5.3 抽象类

typescript 复制代码
abstract class Shape {
    abstract area(): number;  // 抽象方法,子类必须实现
    
    describe(): string {
        return `面积: ${this.area()}`;
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super();
    }
    
    area(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Rectangle extends Shape {
    constructor(private width: number, private height: number) {
        super();
    }
    
    area(): number {
        return this.width * this.height;
    }
}

const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(shape => console.log(shape.describe()));
// "面积: 78.53981633974483"
// "面积: 24"

5.4 接口继承类

typescript 复制代码
class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

interface Point3D extends Point {
    z: number;
}

class Point3DImpl implements Point3D {
    x: number;
    y: number;
    z: number;
    
    constructor(x: number, y: number, z: number) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

六、总结与最佳实践 {#总结}

6.1 ES5 vs ES6 继承对比

特性 ES5 ES6
实现方式 原型链 + 构造函数 class + extends
代码量 较多,需要手动处理原型链 简洁,语法糖
内存效率 组合继承有冗余 寄生组合式更优
可读性 需要理解原型链机制 更接近传统OOP
兼容性 所有浏览器 现代浏览器(IE不支持)

6.2 最佳实践建议

6.2.1 现代项目推荐
javascript 复制代码
// ✅ 推荐:使用 ES6 class
class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        console.log(`${this.name} makes a noise.`);
    }
}

class Dog extends Animal {
    speak() {
        console.log(`${this.name} barks.`);
    }
}
6.2.2 需要兼容老项目时
javascript 复制代码
// ⚠️ 兼容老项目的寄生组合式继承
function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a noise.`);
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
    console.log(`${this.name} barks.`);
};

6.3 常见坑点和注意事项

6.3.1 super 调用顺序
javascript 复制代码
// ❌ 错误:先使用 this
class Child extends Parent {
    constructor(name) {
        this.name = name;      // 报错
        super(name);            // 必须在 this 之前调用
    }
}

// ✅ 正确:先调用 super
class Child extends Parent {
    constructor(name) {
        super(name);            // 先调用
        this.name = name;       // 后使用 this
    }
}
6.3.2 方法覆盖和 super 调用
javascript 复制代码
class Parent {
    method() {
        console.log('Parent method');
    }
}

class Child extends Parent {
    method() {
        super.method();         // 调用父类方法
        console.log('Child method');
    }
}
6.3.3 静态方法继承
javascript 复制代码
class Parent {
    static create(name) {
        return new Parent(name);
    }
}

class Child extends Parent {}
console.log(Child.create('test') instanceof Child); // true

6.4 继承方式选择指南

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     继承方式选择决策树                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  是否需要兼容 IE/老浏览器?                                      │
│      │                                                          │
│      ├── 是 → 使用 ES5 寄生组合式继承                            │
│      │                                                          │
│      └── 否 → 是否需要继承多个类?                               │
│                  │                                              │
│                  ├── 是 → 使用 Mixin 模式                        │
│                  │                                              │
│                  └── 否 → 使用 ES6 class extends                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

6.5 最终建议

  1. 现代项目(ES6+) :直接使用 class extends 语法,简洁直观
  2. 需要兼容性 :使用寄生组合式继承 inheritPrototype()
  3. 多继承需求:使用 Mixin 模式
  4. 内置类扩展 :使用 extends 继承 Array/Map/Error 等
  5. TypeScript 项目:充分利用类型系统和访问修饰符

📚 参考资料

  • MDN Web Docs - 继承与原型链
  • ECMAScript 2015 Specification
  • 《JavaScript高级程序设计》(第4版)
  • 《你不知道的JavaScript》(上卷)

💡 提示:理解JavaScript继承的最佳方式是动手实践。建议读者将本文中的代码示例在浏览器控制台或Node.js环境中实际运行一遍,通过调试器观察原型链的变化,这样才能真正掌握继承的精髓。

相关推荐
lsx2024062 小时前
jEasyUI 创建 CRUD 数据网格
开发语言
Vect__2 小时前
C++无痛转go第一天,从hello world到切片
开发语言·c++·golang
yugi9878382 小时前
MATLAB 实现平板裂纹扩展模拟、气孔/夹杂物分析
开发语言·matlab
青山师2 小时前
Java注解深度解析:从元数据机制到框架开发基石
java·开发语言·注解·javase·java面试·后端开发·java核心
费曼学习法2 小时前
Vue 响应式系统源码级剖析:从 Object.defineProperty 到 Proxy
javascript·vue.js
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第35题:怎样声明一个类不会被继承?什么场景下会用
java·开发语言·后端·面试
游乐码2 小时前
c#特殊语法
开发语言·c#
无限进步_2 小时前
【C++】AVL树完全解析:从平衡因子到四种旋转
c语言·开发语言·数据结构·c++·后端·算法·github
神奇小汤圆2 小时前
快手一面:为什么要求用Static来修饰ThreadLocal变量?
javascript