从零手写JavaScript继承函数:一场关于"家族传承"的编程之旅
从零手写JavaScript继承函数:一场关于"家族传承"的编程之旅
引言:JavaScript的"与众不同"
在JavaScript的世界里,继承不是简单的复制粘贴,而是一场关于"原型链"的奇妙冒险。想象一下:别的语言继承就像领养孩子,直接给一套新房子和新衣服;而JavaScript的继承更像是家族传承——孩子不仅有自己的家,还能随时去祖辈家里串门拿东西!
今天,就让我们一起揭开JavaScript继承的神秘面纱,亲手打造一个属于自己的"家族传承"系统。
一、原型链继承:直截了当的"家族企业"
让我们先来看看JavaScript中最"朴实"的继承方式。
一个动物王国的故事
假设我们有一个Animal(动物)家族:
function Animal(name, age) {
this.name = name; // 名字
this.age = age; // 年龄
}
Animal.prototype.species = '动物'; // 所有动物都有的物种属性
现在,Cat(猫)家族想要继承Animal家族的优良传统。最简单的做法是什么?
方法一:直接"认祖归宗"
function Cat(name, age, color) {
// 先把Animal家族的基本功学过来
Animal.call(this, name, age);
this.color = color; // 猫特有的毛色
}
// 关键一步:成为Animal家族的"亲传弟子"
Cat.prototype = new Animal();
// 但别忘了改个名,不然别人还以为你是Animal
Cat.prototype.constructor = Cat;
const garfield = new Cat('加菲猫', 2, '黄色');
console.log(garfield.species); // ✅ 输出:动物(成功继承了物种!)
这里发生了什么?
-
Cat.prototype = new Animal():相当于Cat家族把Animal请来当顾问 - 现在所有Cat都可以通过"顾问"访问Animal家族的资源
但这种做法有个大问题...
场景想象:你想请Animal当顾问,结果人家拖家带口、把全部家当都搬来了!new Animal()创建了一个完整的Animal实例,但我们需要的仅仅是Animal的"知识库"(原型),而不是它的全部身家。
三大痛点:
- 浪费内存:Animal实例可能很大,但Cat只需要它的原型
-
参数尴尬:
new Animal()时需要参数,但作为原型时不知道传什么 - 效率低下:每次继承都要创建一个可能永远用不着的实例
二、走捷径的诱惑:直接"共享家谱"
有人可能想:"既然只是要原型,那直接共享不就行了?"
// 看似聪明的偷懒方法
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
危险!这是个陷阱!
// 猫家族想给自己加个技能
Cat.prototype.eatFish = function() {
console.log('我爱吃鱼!');
};
// 但意外发生了...
const dog = new Animal('旺财', 3);
dog.eatFish(); // 😱 输出:我爱吃鱼!(狗怎么爱吃鱼了?!)
问题所在:
-
Cat.prototype和Animal.prototype指向同一个对象 - 给Cat添加方法,Animal也会"被学会"
- 就像两个部门共用同一个印章,一方修改,另一方遭殃
三、终极方案:聪明的"中间人"策略
我们需要一个既能继承知识,又不造成混乱的方法。这就是我们的"空函数中介"模式——一个聪明的"传话筒"。
手写extends函数:打造完美的家族传承
function extend(Parent, Child) {
// 1. 请一个"中间人"(空函数F)
// 它就像家族间的专业翻译,只传话,不添乱
var F = function() {};
// 2. 让中间人学习Parent的知识库
F.prototype = Parent.prototype;
// 3. 让Child拜中间人为师
Child.prototype = new F();
// 4. 给Child正名:你姓Child,不是Parent
Child.prototype.constructor = Child;
}
来看看这个精妙的传承系统如何工作
// 使用我们的extend函数
function Cat(name, age, color) {
// 继承Animal的"个人能力"
Animal.apply(this, [name, age]);
this.color = color; // 猫的独有特征
}
// 启动传承仪式!
extend(Animal, Cat);
// 猫家族发展自己的特色
Cat.prototype.purr = function() {
console.log('喵呜~发出呼噜声');
};
// 见证奇迹的时刻
const kitty = new Cat('小橘', 1, '橘色');
console.log(kitty.species); // ✅ "动物"(继承了Animal的物种)
kitty.purr(); // ✅ "喵呜~发出呼噜声"(猫的独有技能)
const bird = new Animal('小鸟', 0.5);
console.log(bird.purr); // ✅ undefined(完全没影响到Animal!)
为什么这个方案如此优雅?
三层隔离保护:
- 第一层:Cat有自己的原型对象
- 第二层:通过中间人F访问Animal的原型
- 第三层:对Cat原型的修改完全不影响Animal
内存关系图:
kitty(猫实例)
↓ "我可以找我的家族要东西"
Cat.prototype(猫家族知识库)
↓ "我学自中间人F"
F.prototype(= Animal.prototype)
↓ "我来自Animal家族"
Animal.prototype(动物家族知识库)
↓ "我是所有对象的起点"
Object.prototype
四、完整实战:打造动物世界的继承体系
让我们把理论变成实战代码:
// 增强版extend:更智能的传承系统
function extend(Child, Parent) {
// 1. 请专业中间人(开销极小)
var F = function() {};
// 2. 中间人学习Parent的全部知识
F.prototype = Parent.prototype;
// 3. Child拜师学艺
Child.prototype = new F();
Child.prototype.constructor = Child;
// 4. 给Child一个"家谱"(可选但很贴心)
Child.uber = Parent.prototype;
// 5. 现代JavaScript的额外支持
if (Object.setPrototypeOf) {
Object.setPrototypeOf(Child.prototype, Parent.prototype);
}
}
// 动物家族基类
function Animal(name, age) {
this.name = name;
this.age = age;
}
Animal.prototype.breathe = function() {
return '我在呼吸新鲜空气';
};
// 猫家族
function Cat(name, age, color) {
// 先学Animal的"生存技能"
Animal.call(this, name, age);
this.color = color;
}
// 启动传承
extend(Cat, Animal);
// 猫家族的独门绝技
Cat.prototype.climbTree = function() {
return '我能爬上最高的树!';
};
// 看看成果
const tom = new Cat('汤姆', 3, '蓝灰色');
console.log(tom.breathe()); // ✅ "我在呼吸新鲜空气"
console.log(tom.climbTree()); // ✅ "我能爬上最高的树!"
console.log(tom.color); // ✅ "蓝灰色"
五、现代JavaScript:语法糖背后的真相
ES6给了我们更优雅的写法:
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
breathe() {
return '我在呼吸新鲜空气';
}
}
class Cat extends Animal {
constructor(name, age, color) {
super(name, age); // 这行相当于 Animal.call(this, name, age)
this.color = color;
}
climbTree() {
return '我能爬上最高的树!';
}
}
重要提醒:class只是"语法糖",底层依然是我们的原型继承。理解原型,才能真正掌握JavaScript的继承精髓。
总结:继承的智慧
通过这次探索,我们学到了:
- 原型实例化继承 → 简单粗暴但笨重(请整个家族当顾问)
- 直接原型继承 → 危险捷径(共用家谱,一损俱损)
- 空函数中介模式 → 优雅方案(专业中间人,隔离又高效)
编程就像家族传承:
- 好的继承应该像家训传承:后代学习前辈的智慧,但有自己的发展
- 坏的继承就像财产纠纷:边界不清,互相影响
- 我们的
extend函数就像是找到了完美的家族信托方案
进阶思考
如果你要继续优化这个extend函数,你会添加哪些功能?
- 多重继承:像继承多个家族的优秀基因?
- 方法混入:像选择性学习不同师父的绝招?
- 静态方法继承:连家族的传统仪式也一起继承?
动手挑战:尝试实现一个支持多重继承的extend函数,让一个类可以同时继承多个父类的特性。把你的代码分享到评论区,看看谁的实现最优雅!
记住:在JavaScript的世界里,理解原型链就像掌握家族的秘密通道。通过这些通道,你可以在不破坏原有结构的前提下,构建出强大而灵活的代码"家族"。现在,你也是掌握这个秘密的开发者了!