JavaScript 原型继承没那么玄!一张图 + 三句话,彻底搞懂
很多人一听到 “原型链” 就头大。其实,JavaScript 的继承机制并不复杂——它只是“共享工具箱”+“临时认爹”的组合技。本文用最生活化的比喻 + 极简图示,带你轻松掌握原型继承的核心逻辑。
一、JS 根本没有“类”,只有对象和原型
先破个误区:JavaScript 从诞生起就没有“类”这个概念!
ES6 的 class 只是语法糖,底层依然是 原型(prototype) 机制。
那它是怎么实现“继承”的?
答案就藏在三个关键词里:prototype、__proto__、constructor。
别怕,我们用一个例子说清楚。
二、一个例子,看懂三者关系
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const p = new Person("小明");
现在,p 是一个实例。它和 Person 之间是怎么连接的?
看这张极简关系图:
p (实例)
│
├─ 自己有:name = "小明"
│
└─ __proto__ ───────────────┐
↓
Person.prototype
│
├─ sayHi: function()
│
└─ constructor ───────→ Person (构造函数)
逐条解释:
p.__proto__指向Person.prototype
→ 当你调用p.sayHi(),JS 先在p自身找,找不到就顺着__proto__去Person.prototype里找。Person.prototype.constructor指回Person
→ 这是 JavaScript 自动设置的“身份标签”,表示“这个原型属于哪个构造函数”。Person函数本身有一个prototype属性
→ 它就是图中那个Person.prototype对象。
✅ 记住口诀:
“实例靠__proto__找原型,原型靠constructor找回构造函数。”
三、想继承?别直接抢爹的工具箱!
现在你想创建一个子类 Student,它除了会 sayHi,还要会 study。
你可能会这样写:
function Student(name, grade) {
this.name = name;
this.grade = grade;
}
// ❌ 危险操作!
Student.prototype = Person.prototype;
看起来好像继承了?但问题来了:
一旦你给 Student.prototype 添加方法:
Student.prototype.study = function() { ... };
所有 Person 实例也会莫名其妙多出 study 方法!
为什么?因为你让两个构造函数共用同一个原型对象——相当于两个部门共用一个工具箱,你在里面放了电钻,结果前台也背上了!
四、空对象当中介:安全继承的关键
怎么办?聪明人发明了 “空对象作为中介” (来自你提供的 2.html):
function F() {} // 1. 创建一个空壳函数
F.prototype = Person.prototype; // 2. 让它的原型指向父类原型
Student.prototype = new F(); // 3. 子类原型 = 空壳实例
Student.prototype.constructor = Student; // 4. 修正身份标签
这样:
Student.prototype是一个全新对象;- 它自己是空的,但它的
__proto__指向Person.prototype; - 你往里面加方法,不会污染父类!
💡 现代写法更简洁:
Student.prototype = Object.create(Person.prototype);
五、call / apply:临时“认爹”,借初始化逻辑
光有方法还不够!Student 实例也需要像 Person 一样初始化 name。
但 Student 构造函数里并没有 this.name = name 啊?
这时候,请出 call 和 apply ——它们是 JS 里的“借爹术”!
function Student(name, grade) {
// 借用 Person 的构造逻辑
Person.call(this, name); // 👈 关键!
this.grade = grade;
}
正如你 readme.md 中所说:
“call/apply 可以指定函数运行时的 this 指向……被指定的函数也立即执行了。”
这里 Person.call(this, name) 的意思是:
“Person 老哥,今天假装我是你 this,帮我跑一下你的构造函数!”
于是 this.name = name 就在 Student 实例上生效了!
call vs apply:传参方式不同
call(this, arg1, arg2)→ 参数一个个传apply(this, [arg1, arg2])→ 参数打包成数组
记不住?记住: “call 是打电话,apply 是发邮件” 。
六、组合继承:工具箱 + 借爹,完美配合
把两招合起来,就是经典的 组合继承:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name); // 借爹初始化属性
this.grade = grade;
}
// 安全继承原型
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// 添加子类特有方法
Student.prototype.study = function() {
console.log(`${this.name} is studying in grade ${this.grade}`);
};
测试一下:
const s = new Student("小红", 3);
s.sayHi(); // ✅ 继承自 Person
s.study(); // ✅ 自己的方法
console.log(s.name); // ✅ 通过 call 初始化
完美!属性靠 call 借,方法靠原型链继承。
七、关于 “Document” 的小彩蛋
你在 3.html 和 4.html 中看到 “Document” ——这其实是浏览器中的全局对象。有趣的是,它也是基于原型链构建的:
document.__proto__ === HTMLDocument.prototype
HTMLDocument.prototype.__proto__ === Document.prototype
这也印证了:JavaScript 万物皆对象,一切皆可继承。
总结:三句话搞定原型继承
prototype是构造函数的“共享工具箱” ,放公共方法;- 继承要用
Object.create或“空对象中介”,别直接共用父类原型; call/apply是“临时认爹”,用来借用父类的初始化逻辑。
下次再听到“原型链”,别慌——它只是 JavaScript 世界里最朴实的资源共享机制。