建个厂,你就能理解原型链了。

36 阅读5分钟

我一直不太喜欢发文章,我习惯默默地观察这个行业的发展。但是最近远程面了几个来面试的前端工作人员,我发现原型链这个问题(不是我要问,是上面给的题目),要么是支支吾吾的,要么是死记硬背,换一个问法就不会了。

这是我第一篇文章,我不知道写得好不好,但我只是希望大家能更生动形象的去理解这个东西。它真的不难。我就简单举一个例子。

我有个饮料厂,我什么类型的饮料都生产。

// 这是我厂里的(生厂线/构造函数)
function Drink(flavor, volume) {
    this.flavor = flavor;
    this.volume = volume;
}

// 这是我饮料的(配方/原型方法)
Drink.prototype.describe = function () {
    console.log(`这是一瓶${this.volume}ml的${this.flavor}饮料`);
}

// 现在,我生产一瓶柠檬味,500ml的(产品/实例)
const lemonDrink = new Drink('柠檬味', 500);
// 让我们(看看配方/调用原型方法)
console.log(lemonDrink.describe()); // 这是一瓶500ml的柠檬味饮料

没问题吧,就简单的函数调用,能理解吧。主要是接下的继承。

// 我要加开另外一条(生产线/子类),这条生产线要在原有生产线的基础上增加自己的(特色/方法)
// 比如说:它专门制作含咖啡因的饮料
function EnergyDrink(flavor, volume, caffeine) {
    Drink.call(this, flavor, volume);  // 借第一条生产线准备原料/继承父类属性
    this.caffeine = caffeine + 'mg';   // 新增咖啡因原料/新增子类自己的属性
}
// 配方复制/原型继承
// 1. 将原生产线的配方拿过来作为第二生产线的配方基础
EnergyDrink.prototype = Object.create(Drink.prototype); 
// 2. 修改生产标签为第二条生产线
EnergyDrink.prototype.constructor = EnergyDrink;  

难点在于(原型继承)这里,解释稍微多点就不在代码里写了:

  1. Object.create(Drink.prototype)这里创建了一个空对象,这个空对象的原型(__proto__)指向Drink.prototype。这意味着,所有通过EnergyDrink生产线生产的饮料,默认都会继承Drink的所有配方(方法)。
  2. 由于第一步操作,EnergyDrink.prototype 的 constructor 属性会被覆盖为 Drink(因为它的原型是 Drink.prototype)(第一生产线的生产标签),所以需要手动修正 constructor 指向 EnergyDrink,是为了保持原型链的完整性,让实例知道它真正来自哪个构造函数(修改生产线标签为第二生产线:虽然配方是第一生产线的,但是实际上是第二生产线的机器生产的)

再举个例子: 假设父类 Drink 的原型上有一个基础配方(describe() 方法),这一步相当于让子类 EnergyDrink 的生产线拿到了这份基础配方,但此时子类的配方还是空白的,可以添加自己的特色步骤(比如 checkCaffeine())。

// 子类特有方法 / 第二生产线自己的配方
EnergyDrink.prototype.checkCaffeine = function() {
    console.log(`本产品含咖啡因${this.caffeine}`);
};

// 第二生产线生产一瓶带咖啡因的饮料/实例
const redBull = new EnergyDrink("牛磺酸味", 500, 150);

// 原型链验证 / 检查配方
console.log(monster instanceof EnergyDrink); // true
console.log(monster instanceof Drink);      // true
console.log(Object.getPrototypeOf(monster) === EnergyDrink.prototype); // true
redBull.describe();       // 继承自父类 / 第一生产线配方  "这是一瓶500ml的牛磺酸味饮料"
redBull.checkCaffeine();  // 自己的方法 / 第二生产线配方  "本产品含咖啡因150mg"

先总结一下第二生产线流程:

  1. 新建子类生产线EnergyDrink 构造函数)

  2. 复印父类配方Object.create()) → 获得基础方法(如 describe()

  3. 在复印的配方上添加新步骤(如 checkCaffeine()

  4. 修正包装标签(修复 constructor) → 确保饮料标明正确来源

  5. 实际生产new EnergyDrink()):

    • 工人先走父类流程(Drink.call(...))设置基础属性(口味、容量)
    • 再走子类流程设置特色属性(咖啡因含量)

总结

总结一下生产线整体流程:

  1. 配方模板(原型对象):

    • Drink.prototype 是基础配方(容量+口味+描述方法)
    • EnergyDrink.prototype 是进阶配方(额外添加咖啡因检测)
  2. 生产线(构造函数):

    • Drink() 是基础生产线
    • EnergyDrink() 是升级生产线,先复用基础配置,再添加新功能
  3. 生产流程(new 操作):

    const redBull = new EnergyDrink(...);
    
    • 先走 EnergyDrink 自己的产线(添加咖啡因)
    • 再走父类 Drink 的产线(设置口味和容量)

也可以说:原型链它的本质是 JavaScript 的“流水线继承系统”:

  1. 每条生产线(构造函数)都有自己的配方模板(prototype)
  2. 新品研发(子类)时,先复印父类配方(Object.create)
  3. 修正新品包装标签(constructor 指向)
  4. 添加独家秘方(子类方法)
  5. 生产时先走父类流水线准备基础原料(Drink.call)
redBull(实例) 
→ EnergyDrink.prototype(含 checkCaffeine) 
  → Drink.prototype(含 describe) 
    → Object.prototype 
      → null (原型链的尽头是null)

关键区别

  • prototype 继承:解决方法共享问题(配方继承)
  • constructor 修正:解决身份标识问题(生产线标签)
  • Drink.call(this, ...) (构造函数中的调用):解决属性初始化问题(原料投放)

会了没,不会就先去开个饮料厂。

还不会?那就记住:

父类 = 第一生产线(不带咖啡因的)

子类 = 第二生产线(带咖啡因的)

属性 = 原料(第一生产线:味道、容量, 第二生产线:咖啡因)

方法 = 配方(第一生产线:describe,第二生产线: checkCaffeine

原型链就是让属性和方法的指向正确,也就是你要让红牛是牛磺酸味还带咖啡因,生产标签上写是第二生产线生产就行了。

image.png

结合这个图(百度随便找的,侵删请联系),建厂流程打上去加深印象吧。