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 + ' 学习');
  }
}

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

当你真正亲手走一遍:

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

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

相关推荐
摘星编程2 分钟前
在OpenHarmony上用React Native:ActionSheet确认删除
javascript·react native·react.js
2501_944521595 分钟前
Flutter for OpenHarmony 微动漫App实战:推荐动漫实现
android·开发语言·前端·javascript·flutter·ecmascript
Amumu121381 小时前
Vue核心(三)
前端·javascript·vue.js
CoCo的编程之路1 小时前
2026 前端效能革命:如何利用智能助手实现“光速”页面构建?深度横评
前端·人工智能·ai编程·comate·智能编程助手·文心快码baiducomate
源代码•宸1 小时前
Leetcode—509. 斐波那契数【简单】
经验分享·算法·leetcode·面试·golang·记忆化搜索·动规
RFCEO1 小时前
HTML编程 课程五、:HTML5 新增语义化标签
前端·html·html5·跨平台·语义化标签·可生成安卓/ios·html最新版本
Irene19911 小时前
JavaScript中,为什么需要手动清理事件
javascript·手动清理事件监听器
摘星编程1 小时前
OpenHarmony环境下React Native:Zustand持久化存储
javascript·react native·react.js
2501_944521591 小时前
Flutter for OpenHarmony 微动漫App实战:图片加载实现
android·开发语言·前端·javascript·flutter·php
摘星编程1 小时前
在OpenHarmony上用React Native:Recoil选择器异步数据
javascript·react native·react.js