原型继承(Prototypal Inheritance)的实现与优势

好嘞!各位观众老爷们,欢迎来到今天的“原型继承脱口秀”!我是你们的老朋友,人称“代码界的段子手”的程序猿阿甘,今天咱要聊聊JavaScript里一个既神秘又好用的东西——原型继承。

别听到“继承”俩字就觉得枯燥,今天阿甘保证,咱把这概念嚼碎了,揉烂了,再用幽默风趣的语言给您喂下去,保证您听完之后,不仅能理解原型继承,还能用它写出更漂亮、更高效的代码!

开场白:话说,这原型继承是个啥玩意儿?

各位想想,咱们人类是怎么一代一代传下来的?爹妈生孩子,孩子继承爹妈的基因,有些像爹,有些像妈。这原型继承,就好比是JavaScript里的“基因传递”。只不过,传递的是属性和方法,而不是身高和长相。

更通俗点说,原型继承就是让一个对象(孩子)能够使用另一个对象(爹妈)的属性和方法。这“爹妈”对象,我们称之为“原型对象(prototype object)”。

第一幕:揭开原型链的神秘面纱

要理解原型继承,就必须先搞清楚一个概念——原型链(prototype chain)。这原型链,就像一条蜿蜒曲折的小路,连接着一个个对象,最终指向一个共同的“祖先”。

咱们先来看看JavaScript里每个对象自带的几个“神器”:

  • __proto__(隐式原型): 每个对象都有一个__proto__属性,它指向创建该对象的构造函数的原型对象。简单来说,就是告诉对象“你的爹妈是谁”。
  • prototype(显式原型): 每个函数都有一个prototype属性,它指向一个对象,这个对象就是通过该函数创建的所有对象的原型对象。也就是说,prototype是“爹妈”用来指定“孩子”的“基因库”。
  • constructor 每个原型对象都有一个constructor属性,它指向创建该原型对象的构造函数。可以理解为“爹妈”指明自己是谁生的。

这三个“神器”之间,存在着一种神奇的三角关系:

对象.__proto__  ===  构造函数.prototype
构造函数.prototype.constructor === 构造函数

是不是有点绕?没关系,咱们画个图就明白了:

graph LR
    A[构造函数] --> B(prototype对象)
    B --> A
    C[对象] --> B
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#ccf,stroke:#333,stroke-width:2px
    style C fill:#cfc,stroke:#333,stroke-width:2px
    linkStyle 0,1,2 stroke-width:2px;

这张图清晰地展示了对象、构造函数和原型对象之间的关系。对象通过__proto__指向原型对象,原型对象通过constructor指向构造函数,而构造函数的prototype又指向原型对象。

重点来了! 当访问一个对象的属性或方法时,JavaScript会先在该对象自身查找,如果找不到,就会沿着__proto__指向的原型对象查找,如果还找不到,就继续沿着原型对象的__proto__向上查找,直到找到为止。这条由__proto__连接起来的查找路径,就是原型链

原型链的顶端是Object.prototype,它是所有对象的“老祖宗”。Object.prototype__proto__指向null,表示原型链的终结。

第二幕:原型继承的几种实现方式

了解了原型链,咱们就可以开始探讨原型继承的几种实现方式了。

  1. 直接指定原型对象:

    这是最简单粗暴的方式,直接用__proto__或者Object.setPrototypeOf()来指定一个对象的原型对象。

    let animal = {
      name: '动物',
      eat: function() {
        console.log('吃东西');
      }
    };
    
    let cat = {
      name: '小猫',
      miao: function() {
        console.log('喵喵喵');
      }
    };
    
    // 将 cat 的原型指向 animal
    Object.setPrototypeOf(cat, animal); // 或者 cat.__proto__ = animal;
    
    console.log(cat.name); // 输出: 小猫 (cat 自身有 name 属性)
    console.log(cat.eat);  // 输出: function() { console.log('吃东西'); } (从 animal 继承)
    cat.eat();            // 输出: 吃东西

    这种方式简单直接,但是不推荐使用__proto__,因为它不是标准属性,可能会有兼容性问题。Object.setPrototypeOf()是标准方法,更安全可靠。

  2. 构造函数 + prototype

    这是最常用的方式,通过构造函数的prototype属性来指定原型对象。

    function Animal(name) {
      this.name = name;
      this.type = '动物';
    }
    
    Animal.prototype.eat = function() {
      console.log(this.name + '吃东西');
    };
    
    function Cat(name, color) {
      Animal.call(this, name); // 借用 Animal 的构造函数,继承属性
      this.color = color;
    }
    
    // 将 Cat 的原型指向 Animal 的实例
    Cat.prototype = new Animal();
    Cat.prototype.constructor = Cat; // 修正 constructor 指向
    
    Cat.prototype.miao = function() {
      console.log(this.name + '喵喵喵');
    };
    
    let kitty = new Cat('Kitty', '白色');
    console.log(kitty.name); // 输出: Kitty
    console.log(kitty.type); // 输出: 动物
    kitty.eat();            // 输出: Kitty吃东西
    kitty.miao();           // 输出: Kitty喵喵喵

    这种方式比较常用,但是也有一些缺点:

    • 继承了原型对象上的属性: Cat.prototype = new Animal() 会继承 Animal 实例上的属性,如果 Animal 的构造函数里有一些不希望被继承的属性,也会被继承过来。
    • 需要修正 constructor 指向: 由于 Cat.prototype 指向了 Animal 的实例,Cat.prototype.constructor 也会指向 Animal,需要手动修正为 Cat
  3. Object.create()

    Object.create() 方法可以创建一个新对象,并将指定的对象作为新对象的原型对象。

    let animal = {
      name: '动物',
      eat: function() {
        console.log(this.name + '吃东西');
      }
    };
    
    let cat = Object.create(animal);
    cat.name = '小猫';
    cat.miao = function() {
      console.log(this.name + '喵喵喵');
    };
    
    console.log(cat.name); // 输出: 小猫
    cat.eat();            // 输出: 小猫吃东西
    cat.miao();           // 输出: 小猫喵喵喵

    这种方式比较灵活,可以避免继承原型对象上的属性,而且不需要修正 constructor 指向。

  4. 组合继承:

    组合继承结合了构造函数继承和原型链继承的优点,避免了它们各自的缺点。

    function Animal(name) {
      this.name = name;
      this.type = '动物';
    }
    
    Animal.prototype.eat = function() {
      console.log(this.name + '吃东西');
    };
    
    function Cat(name, color) {
      Animal.call(this, name); // 构造函数继承,继承属性
      this.color = color;
    }
    
    // 原型链继承,继承方法
    Cat.prototype = Object.create(Animal.prototype);
    Cat.prototype.constructor = Cat; // 修正 constructor 指向
    
    Cat.prototype.miao = function() {
      console.log(this.name + '喵喵喵');
    };
    
    let kitty = new Cat('Kitty', '白色');
    console.log(kitty.name); // 输出: Kitty
    console.log(kitty.type); // 输出: 动物
    kitty.eat();            // 输出: Kitty吃东西
    kitty.miao();           // 输出: Kitty喵喵喵

    组合继承是比较完善的继承方式,既可以继承属性,又可以继承方法,而且避免了继承原型对象上的属性和修正 constructor 指向的问题。

  5. 寄生组合继承:

    寄生组合继承是对组合继承的优化,避免了调用两次父构造函数。

    function Animal(name) {
      this.name = name;
      this.type = '动物';
    }
    
    Animal.prototype.eat = function() {
      console.log(this.name + '吃东西');
    };
    
    function Cat(name, color) {
      Animal.call(this, name); // 构造函数继承,继承属性
      this.color = color;
    }
    
    // 寄生式继承,创建 Animal.prototype 的副本
    function inheritPrototype(subType, superType) {
      let prototype = Object.create(superType.prototype); // 创建副本
      prototype.constructor = subType; // 修正 constructor 指向
      subType.prototype = prototype; // 将副本赋值给子类型的 prototype
    }
    
    inheritPrototype(Cat, Animal);
    
    Cat.prototype.miao = function() {
      console.log(this.name + '喵喵喵');
    };
    
    let kitty = new Cat('Kitty', '白色');
    console.log(kitty.name); // 输出: Kitty
    console.log(kitty.type); // 输出: 动物
    kitty.eat();            // 输出: Kitty吃东西
    kitty.miao();           // 输出: Kitty喵喵喵

    寄生组合继承是目前最推荐的继承方式,它避免了所有已知的问题,并且性能也比较好。

第三幕:原型继承的优势

说了这么多,原型继承到底有什么好处呢?

  1. 代码复用: 通过原型继承,子对象可以继承父对象的属性和方法,避免重复编写代码,提高代码的复用性。
  2. 扩展性: 可以通过原型继承来扩展现有对象的功能,添加新的属性和方法,而不需要修改原始对象。
  3. 灵活性: 原型继承非常灵活,可以根据需要选择不同的继承方式,来满足不同的需求。
  4. 节省内存: 所有子对象共享父对象的原型对象上的方法,不需要为每个子对象都创建一份方法,节省内存空间。

总结:

原型继承是JavaScript中一种非常重要的机制,它允许对象之间共享属性和方法,实现代码复用和扩展。虽然原型继承的概念比较抽象,但是只要理解了原型链的原理,掌握了不同的继承方式,就可以灵活运用原型继承来编写更高效、更优雅的代码。

尾声:

好了,今天的“原型继承脱口秀”就到这里了。希望大家通过今天的讲解,能够对原型继承有更深入的理解。记住,编程之路,永无止境,咱们下期再见!

(PS: 如果您觉得今天的讲解对您有所帮助,请点个赞,转发一下,您的支持是我最大的动力!🙏)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注