JS核心知识-对象继承

197 阅读4分钟

继承是面向对象编程的核心特性之一。与Java、Python、C#等典型的面向对象语言不同,JavaScript采用基于原型的继承机制。在《JS核心知识-原型和原型链》中我们已经探讨了原型的基本概念,本文将重点解析如何通过原型和原型链实现对象继承,并详细介绍各种继承方式及其应用场景。

面向对象编程核心概念

本文主要讲对象的继承,它是面向对象中核心特征之一。那什么是面向对象编程呢?

面向对象编程(OOP)将现实世界的事物抽象为程序中的对象,通过对象间的交互解决问题。以餐厅运营为例:

  • 将"人"抽象为Person对象,包含姓名、技能等属性和方法
  • 通过继承Person实例化出顾客、厨师、服务员等不同角色
  • 按照"顾客点餐 → 服务员接单 → 厨师烹饪 → 服务员上菜 → 顾客用餐"的流程协同工作 image.png

面向对象三大特征

面向对象编程都基本具有以下特性:

1. 封装

将数据和操作数据的方法捆绑成独立单元,隐藏内部细节,仅暴露必要接口。

示例:  手机对象提供打电话、发短信等功能,用户无需了解内部实现细节。

例如: 把手机看做对象,提供了打电话、发短信、安装应用的功能,这些功能的细节我们并不知道。

2. 继承

子对象自动获取父对象的属性和方法,实现代码复用和层次关系构建。

JavaScript通过原型实现继承:

function Animal(name) {
    this.name = name || 'animal';
}

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

function Cat(name) {
    this.name = name;
}

// 设置原型继承
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

const cat = new Cat('Fluffy');
cat.speak(); // "Fluffy makes a sound"

3. 多态

同一方法在不同对象上产生不同行为,提升代码灵活性和扩展性。。

class Animal {
    speak() {
        console.log("Animal makes a sound");
    }
}

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

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

function makeAnimalSpeak(animal) {
    animal.speak(); // 同一方法,不同表现
}

makeAnimalSpeak(new Dog()); // "Woof! Woof!"
makeAnimalSpeak(new Cat()); // "Meow!"

JavaScript继承方式详解

JavaScript的原型继承机制非常灵活,衍生出多种继承模式。

1. 原型链继承

function Person(name) {
    this.name = name || 'father';
    this.colors = ['red', 'blue'];
}

Person.prototype.sayHello = function() {
    return `Hello, I'm ${this.name}`;
};

function Student() {
    this.name = 'student';
}

// 关键步骤:设置原型链
Student.prototype = new Person();

const student1 = new Student();
const student2 = new Student();

student1.colors.push('green');
console.log(student1.colors); // ['red', 'blue', 'green']
console.log(student2.colors); // ['red', 'blue', 'green'] - 共享问题!
console.log(student1.sayHello()); // "Hello, I'm student"

image.png 优缺点分析:

  • ✅ 优点:实现简单,继承完整原型链

  • ❌ 缺点

    • 引用类型属性被所有实例共享
    • 无法向父构造函数传参
    • 无法实现多继承

2. 构造函数继承

构造函数继承其实就是借用函数call方法改变了this的指向。从而继承其他构造函数中的属性和方法

function Person(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
    this.sayHello = function() {
        return `Hello, I'm ${this.name}`;
    };
}

function Student(name) {
    // 借用父类构造函数
    Person.call(this, name);
}

const student1 = new Student('Alice');
const student2 = new Student('Bob');

student1.colors.push('green');
console.log(student1.colors); // ['red', 'blue', 'green']
console.log(student2.colors); // ['red', 'blue'] - 问题解决!
console.log(student1.sayHello()); // "Hello, I'm Alice"

优缺点分析:

  • ✅ 优点

    • 解决引用类型共享问题
    • 支持向父类传参
    • 可实现多继承(多个call调用)
  • ❌ 缺点

    • 无法继承原型方法
    • 方法需在构造函数中定义,内存占用大

3. 组合继承(经典继承)

组合继承结合了原型

function Person(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

Person.prototype.sayHello = function() {
    return `Hello, I am ${this.name}`;
};

function Student(name, grade) {
    // 1. 构造函数继承 - 继承实例属性
    Person.call(this, name);
    this.grade = grade;
}

// 2. 原型链继承 - 继承原型方法
Student.prototype = Object.create(Person.prototype);
// 修复constructor指向
Student.prototype.constructor = Student;

const student = new Student('Alice', 5);
console.log(student.sayHello()); // "Hello, I am Alice"
console.log(student instanceof Student); // true
console.log(student instanceof Person); // true

优缺点分析:

  • ✅ 优点:结合两种模式优点,是常用继承方式
  • ❌ 缺点:父类构造函数被调用两次,轻微性能损耗

4. 原型式继承

适用于基于现有对象创建新对象。

function createObject(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}

const parent = {
    name: 'Parent',
    colors: ['red', 'blue'],
    getName: function() {
        return this.name;
    }
};

const child1 = createObject(parent);
const child2 = createObject(parent);

child1.name = 'Child1';
child1.colors.push('green'); // 影响所有实例!

console.log(child1.getName()); // "Child1"
console.log(child2.colors); // ['red', 'blue', 'green'] - 共享问题

ES5中的Object.create()可替代此模式:

const child = Object.create(parent);

image.png 原型式继承适用于不需要构造函数创建对象的情况,但是它仍然存在原型链继承的问题(数据存在污染)

5. 寄生式继承

在原型式继承基础上增强对象功能。

function createEnhancedObject(obj) {
    const clone = Object.create(obj);
    
    // 增强对象功能
    clone.getColor = function(index) {
        return this.colors[index];
    };
    
    clone.introduce = function() {
        return `I'm ${this.name}`;
    };
    
    return clone;
}

const parent = {
    name: 'Parent',
    colors: ['red', 'blue'],
    getName: function() {
        return this.name;
    }
};

const child = createEnhancedObject(parent);
child.name = 'Child';

console.log(child.introduce()); // "I'm Child"
console.log(child.getColor(0)); // "red"

适用场景:主要关注对象而非类型构造函数的场景。

6. 寄生组合式继承(最优方案)

function inheritPrototype(SubType, SuperType) {
    // 创建原型对象副本
    const prototype = Object.create(SuperType.prototype);
    // 修复constructor指向
    prototype.constructor = SubType;
    // 设置子类原型
    SubType.prototype = prototype;
}

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
    return this.name;
};

function Child(name, age) {
    // 继承实例属性
    Parent.call(this, name);
    this.age = age;
}

// 继承原型方法(关键改进)
inheritPrototype(Child, Parent);

// 可添加子类特有方法
Child.prototype.getAge = function() {
    return this.age;
};

const child1 = new Child('Kevin', 18);
const child2 = new Child('Daisy', 20);

child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green'] - 无共享问题
console.log(child1.getName()); // "Kevin"
console.log(child1.getAge()); // 18

优势

  • 只调用一次父类构造函数
  • 原型链保持完整
  • 是最理想的继承方式

image.png

ES6的class方式继承

class Parent {
    constructor(name) {
        this.name = name;
        this.colors = ['red', 'blue'];
    }
    
    getName() {
        return this.name;
    }
    
    static staticMethod() {
        return 'Parent static method';
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name); // 相当于Parent.call(this, name)
        this.age = age;
    }
    
    // 方法重写
    getName() {
        return `${super.getName()} (child)`;
    }
    
    getAge() {
        return this.age;
    }
}

const child = new Child('Alice', 10);
console.log(child.getName()); // "Alice (child)"
console.log(child.getAge()); // 10
console.log(Child.staticMethod()); // "Parent static method"

底层原理:class本质仍是基于原型的语法糖,babel转译后其实就是寄生组合式继承。

总结与选择建议

继承方式优点缺点适用场景
原型链继承实现简单引用类型共享、无法传参简单的原型扩展
构造函数继承解决共享问题、支持传参无法继承原型方法需要属性隔离
组合继承结合两者优点父类调用两次传统项目使用
原型式继承轻量、灵活共享问题依然存在对象克隆扩展
寄生式继承功能增强方法难以复用对象功能扩展
寄生组合继承性能最优、完整继承实现稍复杂现代库和框架
ES6 Class语法简洁、易维护需要转译支持现代开发

实践建议

  • 现代项目优先使用 ES6 Class
  • 库/框架开发推荐 寄生组合式继承
  • 简单对象扩展可使用 原型式继承
  • 理解底层原理有助于应对复杂场景

掌握JavaScript继承机制,能够帮助开发者编写出更加优雅、可维护的面向对象代码,构建复杂的应用程序架构。希望此文能帮忙你!┏(^0^)┛