好嘞!各位观众老爷们,欢迎来到今天的“原型继承脱口秀”!我是你们的老朋友,人称“代码界的段子手”的程序猿阿甘,今天咱要聊聊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
,表示原型链的终结。
第二幕:原型继承的几种实现方式
了解了原型链,咱们就可以开始探讨原型继承的几种实现方式了。
-
直接指定原型对象:
这是最简单粗暴的方式,直接用
__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()
是标准方法,更安全可靠。 -
构造函数 +
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
。
- 继承了原型对象上的属性:
-
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
指向。 -
组合继承:
组合继承结合了构造函数继承和原型链继承的优点,避免了它们各自的缺点。
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
指向的问题。 -
寄生组合继承:
寄生组合继承是对组合继承的优化,避免了调用两次父构造函数。
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喵喵喵
寄生组合继承是目前最推荐的继承方式,它避免了所有已知的问题,并且性能也比较好。
第三幕:原型继承的优势
说了这么多,原型继承到底有什么好处呢?
- 代码复用: 通过原型继承,子对象可以继承父对象的属性和方法,避免重复编写代码,提高代码的复用性。
- 扩展性: 可以通过原型继承来扩展现有对象的功能,添加新的属性和方法,而不需要修改原始对象。
- 灵活性: 原型继承非常灵活,可以根据需要选择不同的继承方式,来满足不同的需求。
- 节省内存: 所有子对象共享父对象的原型对象上的方法,不需要为每个子对象都创建一份方法,节省内存空间。
总结:
原型继承是JavaScript中一种非常重要的机制,它允许对象之间共享属性和方法,实现代码复用和扩展。虽然原型继承的概念比较抽象,但是只要理解了原型链的原理,掌握了不同的继承方式,就可以灵活运用原型继承来编写更高效、更优雅的代码。
尾声:
好了,今天的“原型继承脱口秀”就到这里了。希望大家通过今天的讲解,能够对原型继承有更深入的理解。记住,编程之路,永无止境,咱们下期再见!
(PS: 如果您觉得今天的讲解对您有所帮助,请点个赞,转发一下,您的支持是我最大的动力!🙏)