class 出现前,JS 是怎么继承的

不搞明白继承,这辈子可能就困在"搬砖式编程"里了。

下面就按"由浅入深"的顺序,走一遍 JS 继承的进化之路:

从最原始的写法,一直走到相对成熟、可复用的模式。

一、原始时代:完全没有继承,全靠手抄

最早的写法大概是这样:

javascript 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function () {
  console.log('你好,我是 ' + this.name);
};

function Student(name, age, school) {
  this.name = name;   // 又写一遍
  this.age = age;     // 再写一遍
  this.school = school;
}

问题很直接:

  • 父子逻辑重复nameage 的赋值写了两次
  • 一旦字段变化,要改两份,极易出错
  • 明明"学生"就是"人"的一种,却硬生生写成两套

于是你会想到:我能不能在 Student 里面直接借用一下 Person 的构造函数

二、第一步进化:构造函数继承------"先把属性借过来再说"

有了 call / apply,你就能这么干:

javascript 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function () {
  console.log('你好,我是 ' + this.name);
};

function Student(name, age, school) {
  // 借用构造函数,把 this 指向当前实例
  Person.apply(this, [name, age]); // 或 Person.call(this, name, age)
  this.school = school;
}

const s = new Student('张三', 18, '一中');
console.log(s.name, s.age, s.school);

优点:

  • 实例属性继承到了nameage 都有
  • 每个实例各自一份,不会互相污染

缺点也很明显:

  • 原型上的方法继承不到sayHi 定义在原型上,Student 实例是拿不到的

这就像只继承了"身份证信息",没继承"家族技能"。

你会发现:属性搞定了,但行为还不行。

三、第二步:原型链继承------"我要继承你的技能"

很自然就会想到原型链:

javascript 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function () {
  console.log('你好,我是 ' + this.name);
};

function Student(name, age, school) {
  this.school = school;
}

// 关键:原型链继承
Student.prototype = new Person('默认名', 0);
Student.prototype.constructor = Student;

优点:

  • 可以访问父级原型上的方法sayHi 生效了
  • 子类型原型上再继续挂方法也没问题

但这里有几个隐患:

  • new Person('默认名', 0)调用一次父类构造函数
  • 如果父构造函数里有数组、对象等引用类型属性,所有通过原型继承的子实例会共享这份数据,容易出现"你改我也变"的怪事
  • 而且,真正创建学生实例时,你还会在 Student 里再处理一套数据,略显臃肿

于是大家继续折腾,把"构造函数继承"和"原型链继承"合在一起。

四、第三步:组合继承------"属性归你,技能也归你"

所谓组合继承,就是:

  • call/apply 继承实例属性
  • 用原型链继承原型方法
javascript 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function () {
  console.log('你好,我是 ' + this.name);
};

function Student(name, age, school) {
  // 1. 继承实例属性
  Person.apply(this, [name, age]);
  this.school = school;
}

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

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

const s = new Student('李四', 20, '二中');
s.sayHi();
s.study();

优点:

  • 实例属性、原型方法,全都有了
  • 子类型可以继续挂自己的方法
  • 使用体验不错,面试也常问这块

但依然有个小瑕疵:

  • 父类构造函数被调用了两次

    • 一次在 Student 里:Person.apply
    • 一次在 Student.prototype = new Person()

如果父类构造函数比较重,这就多少有点"浪费"。

于是,程序员开始精打细算------能不能省一次调用?

五、错误示范:直接把原型指过去------"共享一份大脑,改谁都头疼"

这时候容易走的一步坑是:
直接把子类原型指向父类原型,觉得又省事又高效:

javascript 复制代码
function Person() {}
Person.prototype.identity = '公民';

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

// 直接引用同一个原型对象
Student.prototype = Person.prototype;  // 大坑
Student.prototype.constructor = Student;
Student.prototype.study = function () {
  console.log('学习中...');
};

表面看:

  • 构造函数只调用了一次
  • 原型看起来也能用

但实质上问题巨大:

  • Student.prototypePerson.prototype同一个对象
  • 你给 Student.prototype 加的 study,其实是加在 Person.prototype
  • 你把 Student.prototype.constructor 改回 Student,等于修改了 Person.prototype.constructor
  • 简单说:父类和子类共用一套"脑子",你改一点,两边都受影响

这正是那段"直接继承 prototype 会影响双方 constructor"的典型场景,属于"看起来很聪明、实际上很危险"的写法。

六、进化完成:中介函数 + 组合继承------"我们有联系,但各自独立"

最后的成熟形态就是:

  1. 继续用 call/apply 继承实例属性
  2. 原型链这一块,通过一个"中介函数"来做转接,避免直接改动父原型

一个常见的封装是这样的:

javascript 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function () {
  console.log('你好,我是 ' + this.name);
};

function Student(name, age, school) {
  Person.apply(this, [name, age]);  // 继承实例属性
  this.school = school;
}

function extend(Parent, Child) {
  function F() {}                 // 中介函数
  F.prototype = Parent.prototype; // 连接到父原型
  Child.prototype = new F();      // 子原型基于 F 的实例
  Child.prototype.constructor = Child;
}

extend(Person, Student);

Student.prototype.study = function () {
  console.log(this.name + ' 在 ' + this.school + ' 学习');
};

const s = new Student('王五', 22, '三中');
s.sayHi();
s.study();

这一套的好处:

  • 父类构造函数只调用一次 (在 Student 构造函数内部)

  • 子类实例通过原型链,能访问到父类原型的方法

  • 子类的原型是通过中介函数 F 间接连接到父类原型的

    • 修改子类原型不会直接动到父类原型
  • 可以把 extend 抽出来复用,任何构造函数对都能用这套模式

这一套通常被称作寄生组合继承,在传统 ES5 写法里,是非常主流、相对"成熟"的方案。

七、写在最后:理解比背代码更重要

后来有了 class / extends 语法糖,看起来就像别的语言那样:

javascript 复制代码
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHi() {
    console.log('你好,我是 ' + this.name);
  }
}

class Student extends Person {
  constructor(name, age, school) {
    super(name, age);
    this.school = school;
  }
  study() {
    console.log(this.name + ' 在 ' + this.school + ' 学习');
  }
}

但底层的精神,其实还是我们上面走过的那条路:
构造函数 + 原型链 + 避免共享副作用

当你真正亲手走一遍:

  • 从复制粘贴
  • 到构造函数继承
  • 到原型链继承
  • 到组合继承
  • 再到"直接改原型"翻车
  • 最后用中介函数实现寄生组合继承

就不再只是"会用继承",而是理解了它为什么要这么设计

相关推荐
霍理迪几秒前
CSS布局方式——定位
前端·css
星光不问赶路人2 分钟前
TypeScript 架构实践:从后端接口到 UI 渲染数据流的完整方案
前端·vue.js·typescript
ttyyttemo3 分钟前
Dagger技术的使用学习
前端
IT_陈寒7 分钟前
Redis性能翻倍的5个关键策略:从慢查询到百万QPS的实战优化
前端·人工智能·后端
码界奇点16 分钟前
基于React与TypeScript的后台管理系统设计与实现
前端·c++·react.js·typescript·毕业设计·源代码管理
cehuishi952725 分钟前
python和arcgispro的实践(AI辅助编程)
服务器·前端·python
Summer不秃33 分钟前
使用 SnapDOM + jsPDF 生成高质量 PDF (含多页分页, 附源码)
前端·javascript·vue.js·pdf·node.js
踏浪无痕38 分钟前
从救火到防火:我在金融企业构建可观测性体系的实战之路
后端·面试·架构
AC赳赳老秦39 分钟前
工业互联网赋能智造:DeepSeek解析产线传感器数据驱动质量管控新范式
前端·数据库·人工智能·zookeeper·json·flume·deepseek
Student_Zhang1 小时前
一个管理项目中所有弹窗的弹窗管理器(PopupManager)
前端·ios·github