JavaScript继承大冒险:从“原型江湖”到“class殿堂”

引言:编程世界的"家族传承"

想象一下,你正在设计一个游戏角色系统。所有角色都有共通的属性:生命值、攻击力、移动速度...但法师会放火球,战士能开狂暴,盗贼可以潜行。你会为每个角色重复写相同的代码吗?当然不!这时候,继承就派上用场了!

今天,让我们一起探索JavaScript中继承的奇妙世界,看看这门语言是如何从ES5的"原型江湖"进化到ES6的"class殿堂"的。

🏰 第一站:ES5的"原型江湖"

什么是原型继承?

在ES5时代,JavaScript没有类(class)的概念,它玩的是原型链这套独门武功。每个对象都有一个隐秘的"祖宗"------原型对象(prototype)。

javascript 复制代码
// 让我们从"人"这个基础开始
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 通过原型添加方法
Person.prototype.sayHello = function() {
    console.log(`你好,我是${this.name},今年${this.age}岁`);
};

// 创建实例
const zhangsan = new Person('张三', 25);
zhangsan.sayHello(); // 输出:你好,我是张三,今年25岁

原型继承的几种招式

招式1:原型链继承(最基础款)

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

// 关键一步:让Student的原型指向Person的实例
Student.prototype = new Person();
Student.prototype.constructor = Student; // 修复构造函数指向

Student.prototype.study = function() {
    console.log(`${this.name}正在学习,年级:${this.grade}`);
};

const xiaoming = new Student('小明', 16, '高一');
xiaoming.sayHello();  // 继承了Person的方法
xiaoming.study();     // 自己的方法

问题:所有实例共享同一个父类实例,一个修改,全家遭殃!

招式2:构造函数继承(借用父类构造函数)

javascript 复制代码
function Student(name, age, grade) {
    // 关键:在子类中调用父类构造函数
    Person.call(this, name, age);
    this.grade = grade;
}

const lisi = new Student('李四', 17, '高二');
lisi.name;  // 可以访问
lisi.sayHello(); // 报错!没有继承原型上的方法

问题:只能继承实例属性,原型方法没继承到!

招式3:组合继承(经典款)

javascript 复制代码
function Student(name, age, grade) {
    // 继承实例属性
    Person.call(this, name, age);
    this.grade = grade;
}

// 继承原型方法
Student.prototype = new Person();
Student.prototype.constructor = Student;

const wangwu = new Student('王五', 18, '高三');
wangwu.sayHello(); // 可以!
wangwu.study();    // 如果定义了,也可以!

缺点:调用了两次父类构造函数,有点浪费资源

招式4:寄生组合继承(终极完美版)

javascript 复制代码
function inheritPrototype(child, parent) {
    // 创建一个以父类原型为原型的新对象
    const prototype = Object.create(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

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

// 优雅地继承原型
inheritPrototype(Student, Person);

Student.prototype.study = function() {
    console.log(`${this.name}正在学习`);
};

ES5继承流程图

flowchart TD A[创建子类构造函数] --> B[子类中调用父类构造函数
Parent.call(this, ...args)] B --> C[设置原型继承] C --> D{选择继承方式} D --> E[原型链继承] D --> F[构造函数继承] D --> G[组合继承] D --> H[寄生组合继承] E --> I[问题:所有实例共享
同一个父类实例] F --> J[问题:无法继承
原型方法] G --> K[问题:调用两次
父类构造函数] H --> L[🎉 完美解决所有问题]

🏛️ 第二站:ES6的"class殿堂"

ES6带来了class语法糖,让继承变得像喝咖啡一样简单!

class基础语法

javascript 复制代码
// 用class定义父类
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    sayHello() {
        console.log(`你好,我是${this.name},今年${this.age}岁`);
    }
}

// 用extends实现继承
class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);  // 必须先调用super!
        this.grade = grade;
    }
    
    study() {
        console.log(`${this.name}正在${this.grade}学习`);
    }
}

// 使用起来超级简单
const xiaohong = new Student('小红', 16, '高一');
xiaohong.sayHello(); // 继承的方法
xiaohong.study();    // 自己的方法

class的进阶特性

javascript 复制代码
class Teacher extends Person {
    constructor(name, age, subject) {
        super(name, age);
        this.subject = subject;
    }
    
    // 静态方法(类方法)
    static getProfession() {
        return '教师';
    }
    
    // getter/setter
    get teachingYears() {
        return this.age - 22; // 假设22岁开始教书
    }
    
    set teachingYears(years) {
        this.age = years + 22;
    }
    
    // 方法重写
    sayHello() {
        super.sayHello(); // 可以调用父类方法
        console.log(`我教${this.subject}`);
    }
}

console.log(Teacher.getProfession()); // "教师"
const mrWang = new Teacher('王老师', 35, '数学');
console.log(mrWang.teachingYears); // 13

🔍 终极对决:ES5 vs ES6继承

让我们通过一个对比表看清两者的区别:

特性 ES5原型继承 ES6 class
语法 函数+原型链 class关键字
继承方式 手动设置原型链 extends关键字
构造函数调用 需要手动调用父构造函数 通过super()调用
静态方法 直接在构造函数上定义 static关键字
私有字段 没有原生支持 #私有字段
代码可读性 较低,理解成本高 高,接近传统OOP
本质 基于原型的继承 语法糖,本质还是原型继承

可视化对比:继承的内部机制

flowchart TD subgraph ES5原型继承 A[构造函数Person] --> B[Person.prototype] B --> C[实例对象
__proto__指向原型] D[子类构造函数Student] --> E[Student.prototype = new Person] E --> F[实例共享原型链] end subgraph ES6 class继承 G[class Person] --> H[内部创建构造函数和原型] I[class Student extends Person] --> J[自动设置原型链
通过super连接] J --> K[语法简洁
底层还是原型] end L[🎯 核心真相] --> M[ES6的class只是语法糖
底层仍然是基于原型的继承!]

🎭 真实场景:游戏角色系统

让我们用两种方式实现同一个游戏角色系统:

ES5实现版

javascript 复制代码
// 基础角色
function GameCharacter(name, hp) {
    this.name = name;
    this.hp = hp;
}

GameCharacter.prototype.attack = function() {
    console.log(`${this.name}发起攻击!`);
};

// 战士
function Warrior(name, hp, strength) {
    GameCharacter.call(this, name, hp);
    this.strength = strength;
}

// 设置原型链
Warrior.prototype = Object.create(GameCharacter.prototype);
Warrior.prototype.constructor = Warrior;

Warrior.prototype.specialAttack = function() {
    console.log(`${this.name}使用狂暴斩击!伤害:${this.strength * 2}`);
};

ES6实现版

javascript 复制代码
class GameCharacter {
    constructor(name, hp) {
        this.name = name;
        this.hp = hp;
    }
    
    attack() {
        console.log(`${this.name}发起攻击!`);
    }
}

class Warrior extends GameCharacter {
    constructor(name, hp, strength) {
        super(name, hp);
        this.strength = strength;
    }
    
    specialAttack() {
        console.log(`${this.name}使用狂暴斩击!伤害:${this.strength * 2}`);
    }
}

// 使用
const conan = new Warrior('野蛮人柯南', 100, 15);
conan.attack();        // "野蛮人柯南发起攻击!"
conan.specialAttack(); // "野蛮人柯南使用狂暴斩击!伤害:30"

看到区别了吗?ES6版本明显更清晰、更易读!

💡 最佳实践与常见坑点

1. super()必须在使用this之前调用

javascript 复制代码
class Child extends Parent {
    constructor(value) {
        // ❌ 错误!必须先调用super
        // this.value = value;
        // super();
        
        // ✅ 正确
        super();
        this.value = value;
    }
}

2. class中定义的方法是添加到原型上的

javascript 复制代码
class MyClass {
    method1() { }  // 在原型上
    method2 = () => { } // 在实例上(箭头函数)
}

// 等价于ES5
function MyClass() {
    this.method2 = function() { };
}
MyClass.prototype.method1 = function() { };

3. 继承内置类

javascript 复制代码
class MyArray extends Array {
    // 可以自定义数组方法
    get first() {
        return this[0];
    }
    
    get last() {
        return this[this.length - 1];
    }
}

const arr = new MyArray(1, 2, 3);
console.log(arr.first); // 1
console.log(arr.last);  // 3

🚀 总结:如何选择?

什么时候用ES5方式?

  • 维护老代码时
  • 需要深度控制原型链时
  • 环境不支持ES6时

什么时候用ES6 class?

  • 绝大多数情况下!
  • 新项目开发
  • 需要更好的可读性和维护性
  • 团队协作项目

结语:继承的哲学

JavaScript的继承演变告诉我们一个道理:好的语言特性应该让复杂的事情变简单,而不是让简单的事情变复杂

ES5的原型继承就像手动挡汽车------控制精细但操作复杂;ES6的class就像自动挡------简单易用,让开发者更专注于业务逻辑。

无论选择哪种方式,都要记住:理解底层的原型机制,才能真正掌握JavaScript的继承。毕竟,class只是华丽的包装,原型才是那颗不变的初心。

现在,你已经掌握了JavaScript继承的两种姿势。下次写代码时,你会选择留在"原型江湖",还是踏入"class殿堂"呢?🤔


互动时间:你在实际项目中遇到过哪些继承的坑?或者有什么有趣的继承使用场景?欢迎在评论区分享! 👇

相关推荐
Dragon Wu1 分钟前
React Native KeyChain完整封装
前端·javascript·react native·react.js·前端框架
晚霞的不甘4 分钟前
Flutter for OpenHarmony 布局探秘:从理论到实战构建交互式组件讲解应用
开发语言·前端·flutter·正则表达式·前端框架·firefox·鸿蒙
运筹vivo@8 分钟前
BUUCTF: [极客大挑战 2019]BabySQL
前端·web安全·php·ctf
Beginner x_u2 小时前
前端八股文 Vue下
前端·vue.js·状态模式
提笔了无痕9 小时前
Web中Token验证如何实现(go语言)
前端·go·json·restful
戌中横9 小时前
JavaScript——Web APIs DOM
前端·javascript·html
Beginner x_u9 小时前
如何解释JavaScript 中 this 的值?
开发语言·前端·javascript·this 指针
HWL567910 小时前
获取网页首屏加载时间
前端·javascript·vue.js
烟锁池塘柳010 小时前
【已解决】Google Chrome 浏览器报错 STATUS_ACCESS_VIOLATION 的解决方案
前端·chrome
速易达网络10 小时前
基于RuoYi-Vue 框架美妆系统
前端·javascript·vue.js