在 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
👏 恭喜!你现在有了自己的“猫生”空间!
❌ 缺点:副作用太多,像在走钢丝 🤸♂️
但这个方法也有明显问题:
-
必须调用父类构造函数
- 即使你不关心参数,也得执行
new Animal(undefined, undefined) - 如果
Animal构造函数里有网络请求、DOM 操作或抛异常?那就完蛋了!
- 即使你不关心参数,也得执行
-
内存浪费
- 创建了一个无意义的临时实例,只为拿它的
[[Prototype]] - 就像为了喝一口井水,先把整口井搬回家一样离谱 😅
- 创建了一个无意义的临时实例,只为拿它的
-
参数尴尬
- 在设置继承时根本不知道将来
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 深度解析,敬请期待!🚀