JavaScript之继承

来源:互联网 发布:seo原创怎么写 编辑:程序博客网 时间:2024/06/05 22:54

一、继承

1、一般的面向对象语言中继承有两种方式:接口继承与实现继承。其中接口继承只继承方法签名,而实现继承则继承实际的方法。在JavaScript中,函数没有签名,所以ECMAScript中无法实现接口继承,只支持实现继承,且其实现继承主要依靠的是原型链来实现的。

二、原型链

1、原型链实现继承的基本思想是利用原型让一个引用类型继承另外一个引用类型的属性和方法。由于每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,让原型对象等于另一个类型的实例,则该原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针;假如另一个原型又是另一个类型的实例,那么就能实现多重继承,就构成了实例与原型的链条,这就是原型链的基本概念。

2、实现原型链的基本模式如下:

function Parent() {this.parentValue = true;}Parent.prototype.getParentValue = function() {return this.parentValue;};function Child() {this.childValue = false;}// 继承ParentChild.prototype = new Parent();Child.prototype.getChildValue = function() {return this.childValue;};var instance = new Child();instance.getParentValue(); // true
其中的Child类型继承了Parent类型,继承是通过创建Parent类型实例,并将该实例赋值给Child.prototype来实现的。实现的本质是用一个Parent类型实例来重写了Child的原型对象,所以原来存在于Parent实例中的所有属性和方法现在都存在于Child.prototype中了。最后再为Child.prototype添加了一个新的方法。其关系如下:

其中要特别注意的是,instance.constructor现在指向的是Parent,这是因为原来的Child.prototype被Parent的实例重写了,所以其constructor属性现在是Parent实例的constructor属性,所以指向的是Parent。

3、通过实现原型链,本质上也就扩展了原型搜索机制。在通过原型链实现继承的情况下,属性搜索过程会沿着原型链继续向上。

4、默认的原型

所有引用类型默认都继承了Object,该继承方式也是通过原型链来实现的。而且所有函数的默认原型都是Object的实例,所以默认原型都会包含一个内部指针,指向Object.prototype,这就是所有自定义类型都会继承toString()、valueOf()等默认方法的原因。所以上面的关系图实际上还有一层继承关系,完整的原型链关系图如下:


5、确定原型和实例的关系

有两种方式可以确定原型和实例之间的关系:

  • 使用instanceof操作符:只要用该操作符来测试实例与原型链中出现过的构造函数就会返回true
instance instanceof Object;    // trueinstance instanceof Parent;    // trueinstance instanceof Child;     // true
  • 使用isPrototypeOf方法:只要是原型链中出现过的原型都可以说是该原型链所派生的实例的原型,使用该方法就会返回true
Object.prototype.isPrototypeOf(instance);   // trueParent.prototype.isPrototypeOf(instance);   // trueChild.prototype.isPrototypeOf(instance);    // true
6、子类型添加新方法或覆写父类型方法
当子类型添加父类型中不存在的方法或者子类型覆写父类型中某个方法时,给其原型添加或覆写方法的代码一定要放在替换原型的语句之后,如下:
function Parent() {this.parentValue = true;}Parent.prototype.getParentValue = function() {return this.parentValue;};function Child() {this.childValue = false;}// 继承ParentChild.prototype = new Parent();// 添加新方法Child.prototype.getChildValue = function() {return this.childValue;};// 覆写父类型的方法Child.prototype.getParentValue = function() {return false;};var instance = new Child();instance.getParentValue(); // false
覆写的getParentValue方法就会覆盖父类型中的对应的方法,所以这里的instance.getParentValue()调用返回了false;但是通过Parent的实例来调用getParentValue()依然是调用原来的方法,返回true。要注意的是,子类型添加和覆写的这两个方法一定要在用Parent实例替换Child的原型之后再定义这两个方法。
7、原型链继承与对象字面量
在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链,如下:
function Parent() {this.parentValue = true;}Parent.prototype.getParentValue = function() {return this.parentValue;};function Child() {this.childValue = false;}// 继承ParentChild.prototype = new Parent();// 使用对象字面量添加新方法,会导致上一行代码无效Child.prototype = {getChildValue: function() {return this.childValue;}};var instance = new Child();instance.getParentValue(); // 出现错误
8、原型链存在的问题
问题一:原型链主要的问题在于包含引用类型值的原型。由于包含引用类型值的原型属性会被所有实例共享,所以一般在构造函数,而不是原型对象中定义属性。在通过原型链来实现继承时,原型实际上会变成另一个类型的实例,于是,原先的实例属性也就变成了现在的原型属性了,如下:
function Parent() {this.colors = ["red", "blue", "green"];}function Child() {}// 继承ParentChild.prototype = new Parent();var instance01 = new Child();instance01.colors.push("black");instance01.colors; // red, blue, green, blackvar instance02 = new Child();instance02.colors; // red, blue, green, black
Parent的构造函数定义了一个colors属性,Parent的每个实例都会各自包含自己的colors属性,当Child通过原型链继承了Parent之后,Child.prototype就变成了一个Parent的实例,所以它有一个自己的colors属性,结果就导致了Child的所有实例共享了这个colors属性。
问题二:在创建子类型的实例时,没有办法在不影响所有对象实例的情况下,给父类型的构造函数传递参数。
由于这两个问题存在,所以实践中很少单独使用原型链。
三、借用构造函数
1、借用构造函数的技术也叫做伪造对象或经典继承。基本思想是在子类型构造函数的内部调用父类型构造函数。由于函数只不过是在特定环境中执行代码的对象,所以通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数,如下:
function Parent() {this.colors = ["red", "blue", "green"];}function Child() {// 继承了ParentParent.call(this);}var instance01 = new Child();instance01.colors.push("black");instance01.colors; // red, blue, green, blackvar instance02 = new Child();instance02.colors; // red, blue, green
其中的代码"Parent.call(this)"借调了父类型的构造函数,通过使用call()方法(apply()方法也可以),实际上是在(未来将要)新创建的Child实例的环境下调用了Parent的构造函数,这样就会在新Child对象上执行Parent()函数中定义的所有对象初始化代码,结果就是Child的每个实例都会具有自己的colors属性的副本了。这也解决了原型链存在的问题一。
2、借用构造函数还可以在子类型构造函数中向父类型构造函数传递参数,如下:
function Parent(name) {this.name = name;}function Child(name, age) {// 继承了Parent,同时还传递了参数Parent.call(this, name);this.age = age;}var instance = new Child("zhangsan", 21);instance.name; // zhangsaninstance.age;  // 21
如果要确保父类构造函数不会重写子类型的属性,应该在调用父类型的构造函数后,再添加应该在子类型中定义的属性。
3、借用构造函数的问题
如果仅仅采用借用构造函数,那么会出现构造函数模式创建自定义类型的问题,即方法都需要定义在构造函数中,所以导致了每个实例对于相同功能的方法不能复用,而是每个实例单独一份,函数复用失效。而且,在父类型的原型对象中定义的方法,对于子类型来说也是不可见的,导致所有类型都只能使用构造函数模式。所以,也是很少单独使用借用构造函数的。
四、组合继承
1、组合继承也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一起的一种继承模式。其思路就是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数的复用,又能保证每个实例都有它自己的属性。如下:
function Parent(name) {this.name = name;this.colors = ["red", "blue", "green"];}Parent.prototype.getName = function() {return this.name;};function Child(name, age) {// 继承了Parent的实例属性,同时还传递了参数Parent.call(this, name);this.age = age;}// 继承Parent原型方法Child.prototype = new Parent();Child.prototype.getAge = function() {return this.age;};var instance01 = new Child("zhangsan", 21);instance01.colors.push("black");instance01.colors; // red, blue, green, blackinstance01.getName(); // zhangsaninstance01.getAge(); // 21var instance02 = new Child("lisi", 22);instance02.colors; // red, blue, greeninstance02.getName(); // lisiinstance02.getAge(); // 22
instance01和instance02两个实例就分别拥有自己的属性(name,age,colors),但是它们又拥有相同的方法getName和getAge。组合继承避免了原型链和借用构造函数的缺陷,是最常用的继承模式,这样的模式对于instanceof操作符和isPrototype()方法也能够用于识别基于组合继承创建的对象。
五、原型式继承
1、这种模式并没有使用严格意义上的构造函数,是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了使用这种模式,需要使用以下函数:
function object(o) {function F() {}F.prototype = o;return new F();}
在object函数内部,先创建了一个临时的构造函数,然后将传入的对象作为该构造函数的原型对象,最后返回这个临时构造函数的一个新实例,从本质上看,object()对传入其中的对象执行了一次浅复制。如下:
var person = {name: "zhangsan",colors: ["red", "blue", "green"]};var p1 = object(person);p1.name = "lisi";p1.colors.push("black");var p2 = object(person);p2.name = "wangwu";p2.colors.push("pink");person.colors; // red, blue, green, black, pink
这里把person对象传入object()函数,然后该函数就会返回一个新对象,这个新对象将person对象作为自己的原型对象,所以它的原型对象中就包含一个基本类型值属性和一个引用类型值属性,所以person.colors不仅属于person,还被p1和p2共享,name属性虽然也被共享了,但是p1和p2对象都定义了自己的name属性,屏蔽掉了原型对象中的name属性。
2、在ECMAScript5中新增了Object.create()方法来规范了原型式继承,该方法接收两个参数,一个是用作新对象原型的对象,另一个是为新对象定义额外属性的对象(可选)。当只传入一个参数时,Object.create()方法与上面的object(o)方法的行为相同,如下:
var person = {name: "zhangsan",colors: ["red", "blue", "green"]};var p1 = Object.create(person);p1.name = "lisi";p1.colors.push("black");var p2 = Object.create(person);p2.name = "wangwu";p2.colors.push("pink");person.colors; // red, blue, green, black, pink
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符来定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性,如下:
var person = {name: "zhangsan",colors: ["red", "blue", "green"]};var p = Object.create(person, {name: {value: "lisi"}});p.name = "lisi";
支持Object.create()方法的浏览器有:IE9+、Firefox4+、Safari5+、Opera12+、Chrome。
六、寄生式继承
1、寄生式继承是与原型式继承紧密相关的一种思路,其思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。如下:
function createPerson(original) {// 这里的object函数就是原型式继承中的object函数// 以original作为新创建对象的原型创建了新对象var clone = object(original);// 增强新创建的对象clone.sayHi = function() {return "hi";};return clone;}var person = {name: "zhangsan",colors: ["red", "blue", "green"]};var p = createPerson(person);p.sayHi(); // hi
不过,使用寄生式继承来为对象添加方法,会由于不能做到函数复用而导致降低效率,这点与构造函数模式类似。
七、寄生组合式继承
1、对于之前的组合继承模式来说,最大的问题在于无论什么情况下,都会调用两次父类型构造函数:一次是在创建子类型原型的时候;另一次是在子类型构造函数内部调用父类型构造函数时。而且虽然子类型最终会包含父类型对象的全部实例属性,但是不得不在调用子类型构造函数时重写这些属性,如下:
function Parent(name) {this.name = name;this.colors = ["red", "blue", "green"];}Parent.prototype.getName = function() {return this.name;};function Child(name, age) {// 继承了Parent的实例属性,同时还传递了参数// 属于第二次调用父类型构造函数Parent.call(this, name);this.age = age;}// 继承Parent原型方法// 属于第一次调用父类型构造函数Child.prototype = new Parent();Child.prototype.constructor = Child;Child.prototype.getAge = function() {return this.age;};var instance = new Child("zhangsan", 21);
在第一次调用Parent构造函数时,Child.prototype会得到两个属性:name和colors,它们都是Parent的实例属性,只不过现在位于Child的原型对象中;当调用Child的构造函数时,又会调用一次Parent的构造函数,这次又在新对象上创建了实例属性name和colors,于是这两个属性就屏蔽了原型中的两个同名属性,如下图:有两组name和colors属性,一组在实例instance上,另一组在Child的原型上,这就是两次调用父类构造函数的结果。

而寄生组合式继承就能解决组合式继承的这个问题,所谓寄生组合式继承指的是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其基本思路是不必为了指定子类型的原型而调用父类型的构造函数,所需要的无非就是父类型原型的一个副本而已。本质上就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。如下:
function inheritPrototype(child, parent) {// 这里的object函数就是原型式继承中的object函数// 使用父类型的原型对象作为新对象的原型来创建一个新对象var prototype = object(parent.prototype);   // 创建对象prototype.constructor = child;              // 增强对象child.prototype = prototype;                // 指定对象}function Parent(name) {this.name = name;this.colors = ["red", "blue", "green"];}Parent.prototype.getName = function() {return this.name;};function Child(name, age) {Parent.call(this, name);this.age = age;}inheritPrototype(Child, Parent);Child.prototype.getAge = function() {return this.age;};
其中在inheritPrototype函数中,第一步是创建父类型原型对象的一个副本;第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的的默认的constructor属性。最后将新创建的副本对象赋值给子类型的原型,这样就避免了两次调用父类型构造函数而造成的属性屏蔽的问题了,现在它只调用了一次父类型构造函数,从而避免了在Child.prototype上创建不必要的、多余的属性。同时,原型链还能保持不变,所以还可以正常使用instanceof操作符和isPrototypeOf()方法。这种模式在一般情况下是最理想的继承模式。


参考书籍:《JavaScript高级程序设计》(第三版)

0 0
原创粉丝点击