重读JS(六)面向对象的程序设计 - (3)继承
本章内容
本节内容
- 原型链
- 借用构造函数
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链
ECMAScript将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾下构造函数、原型和实例对象的关系:
每个构造函数都有一个原型对象、原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。通过上一小节的图来解释:
基本概念
那么,加入我们让原型对象等于另一个类型的实例,,结果会怎么样呢?
显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。代码与关系图如下
仔细想想上一节中强调很多变的连接存在于实例与构造函数的原型对象之间,而非实例与构造函数之间。即实例中的指针仅指向原型,而不指向构造函数这句话,关系图就不难理解了。
实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,就是原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确立了继承关系后,给SubType.prototype添加了一个新方法,这样就实现了继承SuperType的属性和方法的基础上又添加了一个新方法。
调用instance.getSuperValue()经历三个搜索步骤:
1)搜索实例
2)搜索SubType.prototype;
3)搜索Super.prototype;找到
在找不到属性或方法的前提下,搜索过程总是要一环一环到原型链才停下。
注意1.看步骤1和步骤2的位置,不能调换,要是不知道原因的话就等于之前的内容都白学了,在这里不解释,回去看第四章。
注意2.instance.constructor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写了的缘故。
默认的原型
确定原型和实例的关系
只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
instanceof
isPrototypeOf()
谨慎地定义方法
子类有时候需要重写超类型中的某个方法,或者添加超类型中不存在的某个方法。但不管怎么样,给原型添加方法的代码(2)一定要放在替换原型(1)的语句之后。
不要问为什么,问就是不懂引用类型的存储问题,回去看第四章的内容。
上一节原型模式中的原话:当为实例新创建一个与它原型对象中已存在的属性相同的属性时,会屏蔽原型对象中的那个属性,但不会修改那个属性。可用通过delete(person1.属性名)来重新访问原型对象中的属性。因此这里当SuperType的实例调用getSuperValue()时,还回继续调用原来的那个方法。
同时,需注意,不能使用对象字面量创建原型方法,这样做会重写原型链。
原型链的问题
最主要的问题来自包含引用类型的原型。我们上一节关于原型对象的问题中介绍过包含引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是原先的实例属性也就顺理成章地编程了现在的原型属性了。下图很明显了:
第二个问题是,在创建子类型的实例时,没有办法在不影响所有对象实例的情况下,向超类型的构造函数中传递参数。
因此很少会单独使用原型链。接下来看结构构造函数部分。
借用构造函数
基本思想:即在子类型构造函数的内部调用超类型构造函数。也称伪造对象或经典继承。
别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数
代码中加深的那一行代码“借调”了超类型的构造函数,通过使用apply()或call()方法,实际是在(未来将要)创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的脚本了。
传递参数
相对于原型链而言,借用构造函数优势就是可以在子类型构造函数中向超烈性构造函数传递参数
以下代码中的SuperType只接收一个参数name,该参数会直接赋给一个属性。在SubType构造函数内部调用SuperType构造函数时,实际是为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起。而且,在超类型的原型中定义方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。因此,构造函数的技术也很少单独使用。
⭐组合继承
基本思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。也称伪经典继承。
在这个例子中,SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName0。Subtype构造函数在调用SuperType构造函数时传人了name参数,紧接着又定义了它自己的属性age。然后,将SuperType的实例赋值给Subtype的原,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的subType实例既分别拥有自己属性——包括colors属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScnpt中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象
原型式继承
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承式完全可以胜任的。
初稿:
这种方法并没有使用严格意义上的构造函数,想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
Object.create()
ECMAScript5通过新增Object.create()方法规范化了原型式继承。
参数一:用作新对象原型的对象,
参数二:为新对象定义额外属性的对象(可选),与Object.defineProperties()方法的第二个参数格式相同
寄生式继承
这个例子中的代码基于person返回了一个新对象——anotherPerson,新对象不仅有person的所有属性和方法,而且还有自己的sayHi()方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,继承式也是一种有用的模式。前面示范继承式时使用object()函数也不是必需的,任何能够返回新对象的函数都适用于此函数。
寄生组合式继承
回顾组合继承
组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。虽然子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性,
上图加粗字体的行中是调用SuperType构造函数的代码。在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造数时,又会调用一次SuperType构造丞数,这一次又在新对象上创建了实例属性name和colors。
于是,这两个属性就屏蔽了原型中的两个同名属性。因此,有两组name和colors属性:一组在实例上,一组在SubType原型中。这就是调用两次SuperType构造函数的结果。为解决,引入寄生组合式继承(通过构造函数来继承属性)
寄生组合式继承
基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。基本模式如下:
在函数内部,第一步是创建超类型原型的一个副本。第二部是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。
这个例子的高效率体现在它只调用了一次SuperType构造函数,避免了在SubType.prototype上面创建不必要的多余的属性。于此同时,原型链还能保持不变,还能够正常使用instanceof和isPrototypeOf()。
开发人员普遍认为寄生组合式继承式引用类型最理想的继承范式。
小结
本节方法集合
检测类型
只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
instanceof:a instanceof A
isPrototypeOf() :A.prototypei.isPrototypeOf(a)
Object.create():原型式继承