关于继承这块,算得上是JavaScript一个大知识点了,一直没有好好梳理,这次斗胆自己总结一下。算是给自己学习JavaScript一个交代。
1.从原型说起
讲到继承,不得不提原型、构造函数、实例对象这三者关系。试问我们有从设计者角度考虑为什么有这三者吗?这里有一篇讲解JavaScript作者设计原型、原型链的心路历程。
2.需求分析
谈到继承,我们应该想到一个优秀的继承设计应该满足哪些需求。
- 每一个实例对象应该有属于实例自身的属性和方法,且定义修改实例自身的属性和方法不会影响其他实例。
- 定义的“公有”方法可以共享给后代,无须后代临时创建。
3.实现继承
看似上面短短两个需求,可用JavaScript实现起来却并不简单,其中的缘由也和JavaScript这门语言设计之初并不是遵循面向对象的原则有关。但总之,有需求就得想办法把他实现了。3.1 原型链继承
原型链继承,应该是JavaScript这门语言最直观明显的继承方式了,其基本思想就是同构原型继承多个引用类型的属性和方法。如果原型对象是另一个类型的实例,那就意味着这个原型对象本身有一个内部指针(proto)指向另一个类型的原型对象,这就在实例和原型之间形成了一条原型链。 ```javascript function SuperType() { this.property = true }
SuperType.prototype.getSuperValue = function() { return this.property }
function SubType() { this.subproperty = false }
// 继承SuperType SubType.prototype = new SuperType() SubType.prototype.getSubValue = function () { return this.subproperty }
let instance = new SubType() console.log(instance.getSubValue()); // false
我把上面的代码画了下面一幅图,更加清晰明了。原型链继承就是利用原型链的的特性。创建一个父类的实例对象,并将子类的原型对象指向父类的实例对象。即`SubType.prototype = new SuperType()`。那么子类实例对象通过原型链即可访问到父类定义的相关方法和属性。<br />**原型链继承的缺点:**1. 子类在实例化时不能给父类的构造函数传参。2. 原型链上的定义的属性或方法会共享给所有子类实例对象,任何一个子类实例对象对其更改都会影响所有子类实例对象。(这既是原型链优点也是其缺点,真难伺候🙁🙁☹️)<a name="ndtK0"></a>#### 3.2 盗用构造函数继承(经典继承)实现基本思路就是:**在子类构造函数中调用父类构造函数**。(所以盗用意思就是子类偷父类的东西?这也算偷?我不理解。)```javascriptfunction SuperType(name) {this.name = namethis.colors = ['red', 'blue', 'green']}function SubType(name) {// 继承SuperTypeSuperType.call(this, name)}let instance1 = new SubType('Nicholas')instance1.colors.push('black')console.log(instance1.colors); // ['red', 'blue', 'green', 'black']let instance2 = new SubType('Sam')console.log(instance2.colors); // ['red', 'blue', 'green']
大概根据代码的意思画的图如下(不知道对不对啊😂),当实例化子类的时候,实际上是调用的父类的构造方法。实际上父类(SuperType)构造函数并没有和子类实例对象产生关联(指的可以用指针__proto__、prototype、constructor访问)。
盗用构造函数的优点完全就是针对着原型链继承缺点来设计的。
- 可以给父类构造函数传参(终于可以自定义属性了)
- 每个子类实例属性拥有父类属性,且随意更改不会影响其他的实例对象(不共享)。
缺点:
- 必须在构造函数中定义方法,因此函数不能重用。(TNND,真难伺候啊)
- 子类实例对象不能访问父类原型对象。(当然啦,看上面的图就明白了)
3.3 组合继承
完美地继承应该是定义在构造函数中的属性和方法都应该作为实例属性或方法,不对其他实例共享。而定义在原型对象上的属性或者方法应该对所有子类实例对象共享。
反观上面的原型链继承,SubType.prototype = new SuperType()这一句直接让父类中不应该被共享的属性安排在了子类的原型对象上,被所有子类实例对象共享。
反观上面的盗用构造函数继承,虽然父类的构造函数中的属性方法都作为了子类实例属性或方法,但父类原型对象上的属性方法连访问都访问不到。
于是乎,我们要达到完美继承,直接组合上面两个不就完事了?这便是组合继承的基本思路:原型链继承原型上的属性方法,盗用构造函数继承实例属性方法。 ```javascript function SuperType(name) { this.name = name this.colors = [‘red’, ‘blue’, ‘green’] }
SuperType.prototype.sayName = function() { console.log(this.name); }
function SubType(name, age) { SuperType.call(this, name) this.age = age }
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function() { console.log(this.age); }
let instance = new SubType(“Nicholas”, 29)
<br />看上去基本已经是完美继承了吧?但是还是有问题(程序员就是精益求精呀!)。<br />首先,父类构造函数被调用了两次,一次在子类构造函数中调用,另一次在设置子类原型对象时new了一下父类构造函数。其次,由于调用了两次父类构造函数,导致父类构造函数属性会出现在子类实例对象和子类原型对象上,这很不美观。<br />因此,组合继承也不是完美的,挑刺总计如下:1. 调用了两次父类构造函数2. 子类实例对象和子类原型对象上有重复字段。<a name="Unedl"></a>#### 3.4 原型式继承2006年,Douglas Crockford 写了一篇文章:《Javascript中的原型式继承》介绍了一种不通过构造方法来实现继承的方式:```javascriptfunction object(o){function F(){}F.prototype = oreturn new F();}let person = {friends: ['Shelby', 'Court', 'Van']}let anotherPerson = object(person)anotherPerson.name = 'Greg'anotherPerson.friends.push('Rob')let yetAnotherPerson = object(person)yetAnotherPerson.name = 'Linda'yetAnotherPerson.friends.push('Barbie')console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
咋一看,这和new F() F.prototype.friends = []原型链继承也没什么区别。indeed,从原型图来看没有区别。但是思想上的区别还是巨大的。原型链继承主张用父类的实例对象作为子类的原型对象,这会导致父类构造函数中不想被共享的变量被子类实例所共享。
然而,原型式继承主张不使用父类实例对象作为子类原型对象,而是将其原型对象直接作为子类原型对象。这很cool😎,这样的思想也被ECMAScript5采纳,通过增加Object.create()方法将原型式继承的概念规范化。
3.5 寄生式继承
寄生式继承与原型式继承十分接近,区别也仅仅是对原型继承做了一层封装。
function createAnother(original){let clone = object(original)clone.sayHi = function() {console.log('hi');}return clone;}let person = {friends: ['Shelby', 'Court', 'Van']}let anotherPerson = createAnother(person) // 寄生式继承anotherPerson.name = 'Greg'anotherPerson.sayHi()
3.6 寄生式组合继承
终于祭出了终极大杀器——寄生式组合继承。前面铺垫了原型式继承、寄生式继承都是为了引出寄生式组合继承的,他的“竞品”实际上是前面提到的组合继承,但解决了组合继承的缺点。
function object(o){function F(){}F.prototype = oreturn new F();}function inheritProperty(SubType, SuperType){let prototype = object(SuperType.prototype)prototype.constructor = SubTypeSubType.prototype = prototype}function SuperType(name) {this.name = namethis.colors = ['red', 'blue', 'green']}function SubType(name, age) {SuperType.call(this, name)this.age = age}inheritProperty(SubType, SuperType)SuperType.prototype.sayName = function() {console.log(this.name);}SubType.prototype.sayAge = function() {console.log(this.age);}let instance = new SubType("Nicholas", 29)

F的原型对象实际上是调用父类的一个副本,(这里体现的寄生式继承)然后将返回的新对象赋给子类原型。并将这个原型和子类构造函数建立关联。而子类构造函数中调用父类构造函数将父类中的实例集成到子类实例中。
4.总结(重点)
本文从需求分析开始,阐明了继承应该满足一下两点:
- 构造方法中定义的属性方法应该由实例自身保存,不应该共享。
- 原型上的属性方法应该共享给所有后代实例对象。
接着,我们讲到原型链继承,发现只满足需求2,不满足需求1.
而盗用构造函数继承只满足需求1,不满足需求2.
终于出现了一个满足需求1、2的继承方式——组合继承。但仍然有缺点——父类构造方法调用两次,导致子类有重复属性方法。
随着新的继承思想出现——原型式继承、寄生式继承,最终出现了完美的继承模式(寄生组合继承)。它满足了需求1、2的同时还只调用了一次父类构造方法。绝了。
