《JS 原型继承?不过是借工具箱+认干爹罢了!》

48 阅读1分钟

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 啊?

这时候,请出 callapply ——它们是 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.html4.html 中看到 “Document” ——这其实是浏览器中的全局对象。有趣的是,它也是基于原型链构建的:

document.__proto__ === HTMLDocument.prototype
HTMLDocument.prototype.__proto__ === Document.prototype

这也印证了:JavaScript 万物皆对象,一切皆可继承


总结:三句话搞定原型继承

  1. prototype 是构造函数的“共享工具箱” ,放公共方法;
  2. 继承要用 Object.create 或“空对象中介”,别直接共用父类原型
  3. call/apply 是“临时认爹”,用来借用父类的初始化逻辑

下次再听到“原型链”,别慌——它只是 JavaScript 世界里最朴实的资源共享机制。