一个空函数,如何成就 JS 继承的“完美方案”?

JavaScript 继承的终极方案:寄生组合式继承详解

在 JavaScript 的世界里,继承一直是开发者绕不开的话题。由于其基于原型(prototype)的独特机制,实现高效、安全、可维护的继承并非易事。从早期的原型链继承、构造函数继承,到组合继承,再到如今被广泛推崇的 寄生组合式继承(Parasitic Combination Inheritance) ,我们终于找到了一个近乎完美的解决方案。

本文将结合实践与原理,带你深入理解为什么寄生组合式继承被称为"JavaScript 继承的终极方案"。


一、继承的痛点:为什么需要"终极方案"?

在 ES6 class 语法出现之前,JavaScript 的继承主要依赖函数和原型。但每种方式都有明显缺陷:

1. 原型链继承

js 复制代码
function Animal() {}
Animal.prototype.eat = function() { console.log('eating'); };

function Cat() {}
Cat.prototype = new Animal(); // ❌ 问题:调用了 Animal 构造函数!
  • 缺点:必须执行父类构造函数,可能带来副作用(如初始化 DOM、发送请求),且无法传参。

2. 构造函数继承

js 复制代码
function Cat(name) {
  Animal.call(this, name); // ✅ 实例属性继承
}
  • 缺点:无法继承原型上的方法,方法无法复用,内存浪费。

3. 组合继承(常用但有冗余)

js 复制代码
function Cat(name) {
  Animal.call(this, name); // 第一次调用 Animal
}
Cat.prototype = new Animal(); // 第二次调用 Animal ❌
  • 缺点 :父类构造函数被调用了 两次,效率低下。

二、寄生组合式继承:优雅的解决方案

核心思想

  • Parent.call/apply(this) 继承实例属性(支持传参、无副作用)
  • 用一个空的中介函数 继承原型方法(不调用父类构造函数)

完整实现

js 复制代码
 function Animal (name,age){
            this.name = name;
            this.age = age;
        };
        Animal.prototype.species = '动物';
        function Cat (name,age,color){
            //{} 空对象 <- this
            // 
            // 构造函数式继承
            // 手动指定
            //Animal.call(this,name,age);
            Animal.apply(this,[name,age]);// 数组传递
            console.log(this);
            this.color = color;
            
        }
        // 
        function extend (Child,Parent){
            var F = function(){};//函数表达式 有开销但不大
             F.prototype = Parent.prototype;
             Child.prototype = new F();// 实例的修改,不会影响到原型对象
             Child.prototype.constructor = Child;
        }
        extend(Cat,Animal);
        Cat.prototype.eat = function(){
            console.log('吃');
        }
        const cat = new Cat('小白',2,'黑色');
        console.log(cat.species);

关键点解析:

  1. 为什么使用空函数 F

    空函数 F 的唯一作用是作为一个"中介桥梁"。它本身不执行任何逻辑(无副作用),但通过 new F() 创建的对象会自动将其内部原型([[Prototype]])指向 F.prototype。当我们把 F.prototype 设置为 Parent.prototype 时,这个新对象就成为了一个自身为空、但能访问父类所有原型方法的代理对象

  2. Child.prototype = new F() 的本质是什么?

    这行代码创建了一个"干净"的原型对象:

    • 不是 Parent 的实例(不会调用 Parent 构造函数)
    • 自身没有属性(避免污染子类原型)
    • 它的 __proto__ 指向 Parent.prototype,因此能通过原型链访问所有父类方法
    • 这相当于现代写法:Object.create(Parent.prototype)
  3. 实例属性 vs 原型方法的分离继承

    • 实例属性 (如 name, age, color)通过 Animal.apply(this, [name, age]) 在子类构造函数中初始化,每个实例独立,支持传参。
    • 原型方法/共享属性 (如 species, eat)通过原型链继承,所有实例共享,节省内存。
    • 两者解耦,各司其职,互不干扰。
  4. 为什么说"实例的修改不会影响到原型对象"?

    因为 Cat.prototype 是一个独立的新对象 (由 new F() 创建),它只是链接到 Animal.prototype,而非与之相等。

    所以:

    js 复制代码
    Cat.prototype.meow = function() {};
    console.log(Animal.prototype.meow); // undefined ✅ 安全隔离

    而如果直接写 Cat.prototype = Animal.prototype,就会造成原型污染。

  5. 性能与安全性双赢

    • 父类构造函数仅在子类实例化时调用一次 (通过 apply/call
    • 原型方法零复制、零冗余,完全复用
    • 无副作用、无内存浪费、类型系统完整

虽然Child.prototype.constructor = Child;这行代码依然会将f的constructor修改为Child,但是我们并不关心它,因为我们创建它的初衷就是利用它将Child连接到Animal的原型链上,至于f最后怎么样我们并不关心


三、为什么它是"终极方案"?

特性 寄生组合式继承 其他方式
✅ 不重复调用父构造函数 ✔️ 只在 call 时调用一次 组合继承调用两次
✅ 支持传参 ✔️ 原型链继承不支持
✅ 方法复用 ✔️ 所有实例共享原型方法 构造函数继承无法复用
✅ 原型链完整 ✔️ instanceof 正确 多数方式可做到
✅ 无副作用 ✔️ 不执行 new Parent() 原型链/组合继承会执行

💡 关键优势 :通过空函数 F 作为中介,只继承原型,不执行父类构造逻辑,既安全又高效。


四、原理图解

js 复制代码
Cat.prototype (new F())
    │
    └─ [[Prototype]] → F.prototype = Animal.prototype
                            │
                            ├
                            └─ constructor: Animal

// 修复后:
Cat.prototype.constructor = Cat
  • Cat.prototype 是一个空壳对象,自身无属性
  • 但它通过原型链无缝访问 Animal.prototype 的所有方法
  • 每个 new Cat() 实例通过 Animal.apply(this) 初始化自己的属性,互不干扰

五、现代替代:ES6 class 也是这么干的!

虽然我们现在常用:

js 复制代码
class Animal {
  constructor(name) { this.name = name; }
  sayName() { console.log(`I am ${this.name}`); }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name);
    this.color = color;
  }
  meow() { console.log('Meow!'); }
}

但你知道吗?extends 在底层正是采用了寄生组合式继承的思想

Babel 编译后的代码几乎就是我们上面手写的 extend 模式。

所以,理解寄生组合继承,就是理解现代 JavaScript 继承的本质。


六、总结

  • 寄生组合式继承 = 构造函数继承(实例属性) + 寄生式原型继承(原型方法)
  • 它解决了所有传统继承方式的痛点,是 ES5 时代最推荐的继承模式
  • 即使在 ES6+ 时代,理解它依然至关重要------因为 class 只是语法糖,底层逻辑不变
  • 如果你在阅读老项目或面试中遇到继承问题,这套方案就是你的"终极武器"

🌟 记住这个模板

js 复制代码
function extend(Child, Parent) {
  const F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
}

掌握它,你就掌握了 JavaScript 继承的精髓。


相关推荐
韩曙亮1 小时前
【Web APIs】元素可视区 client 系列属性 ② ( 立即执行函数 )
前端·javascript·dom·client·web apis·立即执行函数·元素可视区
羽沢311 小时前
vue3 + element-plus 表单校验
前端·javascript·vue.js
小飞侠在吗2 小时前
vue Hooks
前端·javascript·vue.js
阿拉伯柠檬2 小时前
实现一个异步操作线程池
开发语言·数据结构·c++·面试
小茴香3532 小时前
vue3的传参方式总结
javascript·vue.js·typescript
梵得儿SHI2 小时前
Vue 核心语法深度解析:生命周期与响应式之计算属性(computed)与侦听器(watch/watchEffect)
前端·javascript·vue.js·计算属性·侦听器·缓存机制·数据派生
anuoua2 小时前
歼20居然是个框架-基于 Signals 信号的前端框架设计
前端·javascript·前端框架
我爱学习_zwj2 小时前
Node.js模块管理:CommonJS vs ESModules
开发语言·前端·javascript
咬人喵喵2 小时前
网页开发的“三剑客”:HTML、CSS 和 JavaScript
javascript·css·html