🐱 手写原型继承:从“翻车现场”到优雅实现的进阶之路

40 阅读5分钟

在 JavaScript 的世界里,继承就像是一道必做的家庭作业——你可能不想写,但早晚得面对。而由于 JS 是基于 原型(prototype) 的语言,它的继承方式和 Java、C++ 这些“正经类语言”完全不同,初学者常常一头雾水。

别担心!今天我们就来一场轻松愉快的“继承大冒险”,带你从最简单的尝试开始,一路踩坑、避雷,最终掌握最安全高效的继承写法!


一、方法①:直接赋值 Cat.prototype = Animal.prototype ❌💥

我们先来看一个看起来很“直觉”的做法:

function Animal(name, age) {
    this.name = name;
    this.age = age;
}
Animal.prototype.species = '动物';

function Cat(name, age, color) {
    Animal.apply(this, [name, age]);
    this.color = color;
}

// ⚠️ 危险操作!
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;

🤔 看起来没问题?其实大错特错!

这种方式就像是把整个家族族谱原封不动地复制给下一代。虽然子类能用父类的方法,但问题来了👇:

🔥 两个构造函数共享同一个原型对象!

这意味着:

  • 如果你在 Cat.prototype 上加个新方法,比如 .eat()……
  • 那么 Animal 实例也能 .eat() 了!🤯
  • 更可怕的是,所有其他继承自 Animal 的类也会突然获得这个能力!

这就像你给猫咪加了个“吃罐头”技能,结果狮子、老虎、甚至你家仓鼠也都会吃了——严重污染父类原型

📦 浅拷贝小课堂 💡

你可能会想:“那我能不能做个‘拷贝’呢?”
答案是:不能只做浅拷贝!

// 错误示范:浅拷贝也不行
Cat.prototype = { ...Animal.prototype }; // 只复制属性,断开 __proto__ 链!

这样虽然新建了一个对象,但丢失了原型链连接,instanceof 会失效,也无法动态更新父类新增的方法。

📌 总结一句话

🚫 Cat.prototype = Animal.prototype 是典型的“共享灾难”,属于 禁止使用的反模式


二、方法②:用 new Animal() 当原型 ✅🟡

为了避免共享问题,有人灵机一动:不如创建一个 Animal 的实例作为 Cat 的原型?

Cat.prototype = new Animal(); // 创建一个“空壳”动物
Cat.prototype.constructor = Cat;

✅ 优点:终于有独立原型啦!

此时:

  • Cat.prototype.__proto__ === Animal.prototype
  • 原型链成立 ✅
  • 可以自由扩展 Cat.prototype.eat = function(){} 而不影响 Animal

👏 恭喜!你现在有了自己的“猫生”空间!

❌ 缺点:副作用太多,像在走钢丝 🤸‍♂️

但这个方法也有明显问题:

  1. 必须调用父类构造函数

    • 即使你不关心参数,也得执行 new Animal(undefined, undefined)
    • 如果 Animal 构造函数里有网络请求、DOM 操作或抛异常?那就完蛋了!
  2. 内存浪费

    • 创建了一个无意义的临时实例,只为拿它的 [[Prototype]]
    • 就像为了喝一口井水,先把整口井搬回家一样离谱 😅
  3. 参数尴尬

    • 在设置继承时根本不知道将来 Cat 实例的具体数据,传参很别扭

所以,这种方法虽然比第一种安全,但仍不够优雅。


三、方法③:利用空对象作为中介 ✅✅✅✨(推荐!)

终于到了我们的主角登场时刻——使用空函数作为中介,完美解决前两种方案的所有痛点!

function extend(Child, Parent) {
    var F = function() {};           // 🧱 空砖块,啥也不干
    F.prototype = Parent.prototype;  // 把它贴上“父类皮肤”
    Child.prototype = new F();       // 用这块砖搭出子类原型
    Child.prototype.constructor = Child;
}

然后这样使用:

extend(Cat, Animal);

🌈 它到底好在哪?

特性表现
🔐 是否安全?✅ 安全!不共享原型
🧼 是否干净?✅ 不调用父类构造函数,零副作用
💡 是否灵活?✅ 可自由扩展子类原型
🧩 是否轻量?✅ 中介函数极简,几乎无开销

🔍 原理图解 🖼️

cat (实例)
└── __proto__ → Cat.prototype (即 new F())
     └── __proto__ → Animal.prototype
          └── species: "动物"

👉 cat 可以访问 species
👉 修改 Cat.prototype 不会影响 Animal
👉 完美!

💬 小贴士:这其实就是 Object.create() 的 polyfill!

现代 JS 提供了更简洁的方式:

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

而上面那个 extend 函数,就是手动实现了 Object.create() 的逻辑。
所以在老浏览器中(如 IE8),这种“空函数中介”就是标准解法!


四、动态属性与“属性屏蔽”现象 🎭

再看一个小例子,感受 JS 的灵活性:

function Cat() {}
Cat.prototype.species = '猫科动物';

const cat = new Cat();
cat.species = "hello"; // 给实例添加 own property

console.log(cat.species);        // "hello" 🎉
console.log(Cat.prototype.species); // "猫科动物" 🐾

发生了什么?这就是著名的 属性屏蔽(Property Shadowing)

  • 当你给实例设置同名属性时,JS 会在该实例上创建一个“自有属性”
  • 后续读取时优先使用实例属性,屏蔽原型上的值
  • 删除后又能恢复访问原型值

💡 类似于“儿子改名字不影响老爸”

delete cat.species;
console.log(cat.species); // → 又变回 "猫科动物"

五、封装通用继承工具函数 🧰

为了让代码更整洁,我们可以把这套逻辑封装成一个可复用的工具:

function extend(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    
    Child.prototype = new F();
    Child.prototype.constructor = Child;

    // 可选:保存父类引用,方便 super 调用
    Child.superclass = Parent.prototype;
}

🧪 使用示例

function Animal(name, age) {
    this.name = name;
    this.age = age;
}
Animal.prototype.sayHi = function () {
    console.log(`Hi, I'm ${this.name}`);
};

function Cat(name, age, color) {
    Animal.call(this, name, age); // 构造函数继承
    this.color = color;
}

extend(Cat, Animal); // 原型继承

Cat.prototype.meow = function () {
    console.log("Meow~ 🐱");
};

// 测试
const cat = new Cat("加菲猫", 2, "橙色");
cat.sayHi();  // Hi, I'm 加菲猫
cat.meow();   // Meow~ 🐱
console.log(cat instanceof Animal); // true ✅
console.log(cat instanceof Cat);    // true ✅

🎯 完美实现组合继承:

  • 属性靠 call/apply 传递 ✅
  • 方法靠原型链共享 ✅
  • 安全高效无副作用 ✅

✅ 总结:继承三部曲对比表 📊

方法是否安全是否调用构造函数推荐度适用场景
Cat.prototype = Animal.prototype❌ 共享危险❌ 不调用❌ 禁止使用
Cat.prototype = new Animal()✅ 独立原型✅ 必须调用⭐⭐⭐兼容性要求低时可用
空对象中介 / Object.create✅✅ 最安全❌ 不调用⭐⭐⭐⭐⭐✅ 强烈推荐

🎓 写在最后:理解原理才能驾驭语法糖 ☕

ES6 后我们有了 class Cat extends Animal 这样的语法糖,写起来非常爽。
但要知道,它底层依然是基于这些原型技巧实现的!

🔍 知道“轮子怎么造”,才能修好“跑偏的车”

掌握手写原型继承,不仅能让你写出更健壮的代码,还能在面试中自信地说出:“我知道 extends 背后发生了什么!” 😎


🎉 恭喜你完成本次继承之旅!
现在你可以骄傲地说:

“我不是只会写 class 的程序员,我是懂原型的男人/女人/非二元性别者!” 🐾

继续探索 JavaScript 的奇妙世界吧,下一站:寄生组合继承 or ES6 Class 深度解析,敬请期待!🚀