Js面向对象漫谈(2) 继承--原型链,借用构造函数,组合式,寄生式, 寄生组合式
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法.
其基本思想就是 利用原型让一个引用类型 继承另一个引用类型的属性和方法.
实现原型链有一种基本模式,大致如下:
function SuperType() { //定义父类 this.property = true; } SuperType.prototype.GetSuperValue = function () { //原型方法 return this.property; } function SubType() { //定义子类 this.SubProperty = false; } //继承父类 SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.SubProperty; } //实例化 var instance = new SubType(); alert(instance.GetSuperValue()); //true
alert(Object.getOwnPropertyNames(SubType.prototype)); //property ,GetSubValue
alert(Object.getOwnPropertyNames(SuperType.prototype)); //constructor,GetSuperValue
alert(instance.constructor.name); //SuperType
这里例子内的 实例以及构造函数和原型的关系如下图:
下面解释一下这个图的大概意思:
我们这里没有使用SubType的默认原型, 而是用SuperType的实例代替了他的默认原型.于是,新原型 就具备的一个 SuperType实例的
所有属性和方法,还拥有一个 指向SuberType 原型对象的一个指针. 最终的结果就导致:instance指向SubType Prorotype,而 SubType Prototype 指向 SuperType Prototype . GetSuperType()方法仍然还在SuperType.prototype中,但是 property则位于SubType.prototype中. 这是因为property是一个实例属性,而getSuperValue()则是个原型方法. 既然 SubType.prototype是 SuperType的实例, 那么property当然位于该实例中了.
从上面的代码可以看出 instance 的构造函数已经变成了 SuperType,而不是SubType.那么原因就在于,SubType的原型对象被 改成了
一个 SuperType的实例, 那么 SubType的原型对象就 指向了 SuperType的原型,而SuperType的原型的构造函数指向了 SuperType,
所以instance的构造函数变成了 SuperType.
事实上,引用类型都默认继承了 Object,而这个继承也是 原型链来实现的. 所以所有函数的默认原型 都是Object的实例,因此默认原型
都会包含一个指向 Object.prototype的指针.
确定 原型和实例的关系
可以通过使用 instanceof操作符,也可以使用 isPrototypeOf()方法,下面看例子:
alert(instance instanceof SubType); //true alert(SubType.prototype.isPrototypeOf(instance)); //true
定义方法需要注意的
子类有时候要重写超类的方法,或对超类进行扩充.那么 给原型添加方法的代码要 放在 替换原型的语句之后.看下面的例子:
function SuperType() { //定义超类 this.property = true; //定义属性 if (typeof this.GetSuperValue != "function") { SuperType.prototype.GetSuperValue = function () { //定义原型方法 return this.property; } } } function SubType() { this.SubProperty = false; } //继承超类 SubType.prototype = new SuperType(); SubType.prototype.GetSubValue = function () { //定义子类的方法,对父类实现扩展 return this.SubProperty; } SubType.prototype.GetSuperValue = function () { //重写超类的方法 return false; } var in1 = new SubType(); alert(in1.GetSuperValue()); //false alert(in1.GetSubValue()) //false var in2 = new SuperType(); alert(in2.GetSuperValue()); //true
还有就是通过 原型链继承时,不能使用对象字面量 创建原型方法.
function SuperType() { this.property = true; if (typeof this.GetSuperValue != "function") { SuperType.prototype.GetSuperValue = function () { return this.property; } } } function SubType() { this.SubProperty = false; } //继承超类 SubType.prototype = new SuperType(); //使用对象字面量添加 新方法,会导致上一行代码无效. SubType.prototype = { getSubValue: function () { return this.SubProperty; }, getValue: function () { return false; } } var in1 = new SubType(); alert(in1.getSuperValue()); //错误
以上代码 SubType.prototype = new SuperType(); 刚刚把 超类的实例赋给子类的原型, 紧接着又将子类的原型替换成了
一个对象字面量.
由于现在的原型包含的是一个 Object的实例,而不是 超类的实例,所以 现在 SubType和 SuperType已经没有关系了.
原型链的问题:
1.最主要的问题来自引用类型值的原型. 包含引用类型值的原型属性 会被 所有实例共享,在上一节 已经演示过了.这正是 在构造函数中定义
属性的原因.
下面看例子:
function fc() { this.colors = ["red", "green"]; } function sc() { } sc.prototype = new fc(); var instance1 = new sc(); instance1.colors.push("blue"); alert(instance1.colors); //red,green,blue var instance2 = new sc(); alert(instance2.colors); //red,green,blue
两个实例的 colors值 是一样的.
借用构造函数
在解决原型中包含引用类型值的问题过程中,使用借用构造函数的技术.
基本思想就是: 在子类型构造函数的内部 调用超类型的构造函数.
下面看一个实例:
function SuperType() { this.colors = ["red", "green"]; } function SubType() { SuperType.call(this); //继承了SuperType } var in1 = new SubType(); in1.colors.push("blue"); alert(in1.colors); //red,green,blue var in2 = new SubType(); alert(in2.colors); //red,green
代码中 SuperType.call(this) 借调了 超类型的构造函数,通过使用call()或者 apply()方法,实例化SubType的环境下 调用 SuperType的构造函数,这样一来, 就会在新的Subtype对象执行SuperType()函数 中定义的对象初始化代码, 结果每个SubType实例都会有具有自己的colors属性的副本.
传递参数:
相对于原型链而言, 借用构造函数的另一个优势,就是在实例化子类 对象的时候,可以向超类构造函数传递参数,下面看例子:
function SuperType() { this.name = arguments[0]; } function SubType() { SuperType.call(this,"gao"); } var in1 = new SubType(); alert(in1.name); //gao
借用构造函数的问题
和构造函数模式一样, 函数定义的 方法不能够重用,每次创建一个实例,都会创建一个方法的实例,那么将会占用大量的资源.那么就会用
到下面的组合继承了.
组合继承
就是结合了 原型链和借用构造函数的优点.
思想就是:使用原型链 继承方法,而用借用构造函数继承属性. 这样既可以通过 在原型上定义方法,实现复用, 通过构造函数定义属性,
可以使每一个实例 都拥有自己的 独有属性.下面看一个例子:
function SuperType(name) { this.name = arguments[0]; //定义属性 if (typeof this.sayName != "function") { SuperType.prototype.sayName = function () {//定义原型方法 alert(this.name); } } } function SubType(name, age) { SuperType.call(this, name); //继承属性 this.age = age; } SubType.prototype = new SuperType(); //继承方法 SubType.prototype.sayAge = function () { //扩展父类的方法,要在 继承父类之后定义 alert(this.age); } var in1 = new SubType("gao", 12); in1.sayName(); //gao in1.sayAge(); //12 var in2 = new SubType("gao1"); in2.sayName(); //gao1
in2.sayAge(); //undefined
我们看到实例间没有共享属性,但是共享了方法.
原型式继承
原型式继承的模型如下:
function object(o) { function F() { }; F.prototype = o; return new F(); }
将object函数传进的对象参数 作为 F构造函数的原型.本质上说,是对传进的参数对象的 一次浅复制,看如下代码:
function object(o) { function F() { }; F.prototype = o; return new F(); } var Person = { //创建一个基础对象 name: "gao", friends:["joe","bob"] } var newPerson = object(Person); //实现继承 alert(newPerson.name); //gao newPerson.friends.push("lily"); alert(Person.friends); //joe,bob,lily
ECMAScript 5通过新增的Object.Create()方法 规范化原型式继承.这个方法接受两个参数,第一个是 用作新对象的原型,另一个是新对象定义额外属性的对象.
下面看一个参数的情况:
var Person = { name: "gao", friends:["joe","bob"] } var newPerson = Object.create(Person); alert(newPerson.name); //gao
下面看有两个参数的情况:
var Person = { name: "gao", friends: ["joe", "bob"] } var newPerson = Object.create(Person, { name: { value: "newgao" }, age: { value: 12 } }); alert(newPerson.name); //newgao alert(newPerson.age); //12
这里还要重复一下,就是原型模式的一个特性,就是 所有实例都会共享原型的 引用类型的属性.
寄生式继承
此种继承的思路 与 寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部 以某种方式增强对象,最后在像真地是它做了所有工作一样,返回对象. 看代码示例:
function co(o) { var clone = Object.create(o); clone.sayHi = function () { alert("hi"); } return clone; } var people = { name:"gao" } var p1 = new co(people); p1.sayHi(); //hi
使用寄生式继承为对象添加函数,会由于不能做到函数的复用而降低效率,这一点与 寄生构造函数模式 类似.
寄生组合式继承
组合继承是 Javascript最常用的继承模式,但是也有不足之处.就是 无论在什么情况下,都会调用两次超类的构造函数.下面回顾一下组合继承:
function SuperType() { this.property = true; if (typeof this.SaySuperValue != "function") { SuperType.prototype.SaySuperValue = function () { return this.property; } } } function SubType() { this.SubProperty = false; SuperType.call(this); //第一次调用超类构造函数 } SubType.prototype = new SuperType(); // 第二次调用构造函数 SubType.prototype.SaySubValue = function () { return this.SubProperty; } var in1 = new SubType(); alert(in1.SaySuperValue()); //true alert(in1.SaySubValue()); //false
下面看一下 寄生组合式继承的代码:
function SuperType() { this.property = true; if (typeof this.SaySuperValue != "function") { SuperType.prototype.SaySuperValue = function () { return this.property; } } } function SubType() { this.SubProperty = false; SuperType.call(this); } function inheritPrototype(subType, superType) { var prototype = Object.create(superType.prototype); //创建超类原型的副本 prototype.constructor = subType; //弥补 子类修改原型指向 所造成的默认构造函数的丢失 subType.prototype = prototype; //修改子类 原型 } inheritPrototype(SubType, SuperType); SubType.prototype.SaySubValue = function () { return this.SubProperty; } var in1 = new SubType(); alert(in1.SaySuperValue()); //true alert(in1.SaySubValue()); //false
上述代码和 组合继承的不同之处在于, 修改子类的原型时,不是 用的超类的实例,而是超类的原型副本.
而实现这个的就是 inheritPrototype()函数,从而,只调用了一次 超类的构造函数, 也因此避免 了 在子类原型 上创建的一些不必要的
,多余的属性. 于此同时,原型链还能保持不变. 因为可以正常使用 instanceof.
两节的小结:
工厂模式: 使用简单的函数创建对象,为对象添加属性和方法,然后返回对象.
构造函数模式:可以创建自定义引用类型,可以向内置对象一样使用 new操作符,缺点是每个成员不能复用.
原型模式: 使用构造函数的 prototype属性来指定那些可以用共享的属性和方法.
javascript主用是通过
原型链实现继承.缺点 每个子类的实例共享属性和方法,没有各自独立的属性.
借用构造函数继承. 可以实现子类的实例有各自独有的属性,但是 子类的方法不能得到复用
组合继承. 调用了两次超类的构造函数
寄生式继承. 缺点同借用构造函数
寄生组合是继承. 比较理想的继承方式.