JavaScript中对象继承方式(ES5)
原型链继承
其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
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; }; var instance = new SubType(); alert(instance.getSuperValue()); //true
SubType.prototype 现在是 SuperType的实例,那么 SuperType中的property =true当然就位于该实例(SubType.prototype )中了。
调用instance.getSuperValue()会经历三个搜索步骤:
1)搜索实例;
2)搜索 SubType.prototype;
3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过 程总是要一环一环地前行到原型链末端才会停下来。
还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这 样做就会重写原型链,如下面的例子所示 :
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; }, someOtherMethod : function (){ return false; } }; var instance = new SubType(); alert(instance.getSuperValue()); //error!
以上代码展示了刚刚把 SuperType 的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个 Object 的实例,而非 SuperType 的实例,因此我们设想中的原型链已经被切断——SubType 和 SuperType 之间已经没有关系了 。
优点:
1)SubType实例既是子类SubType实例,又是父类SuperType实例
缺点:
1)通过原型来实现继承时,原型实际上会变成另一个类型的实例。 (SubType.prototype 现在是 SuperType的实例)。于是,原先的实例属性(比如SuperType中的peoperty属性)也就顺理成章地变成了现在的原型属性了。 所以如果原先的实例属性(SuperType的实例属性)是引用类型的,那么就会被SubType中的实例纂改。
2)在创建子类型的实例时,不能向超类型的构造函数中传递参数。 实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
构造函数继承
在子类型构造函数的内部调用(通过使用 apply()和 call()方法 )超类型构造函数。
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ //继承了 SuperType SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new SubType(); alert(instance2.colors); //"red,blue,green"
这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会具有自己的 colors 属性的副本了 。
优点:
1)可以在子类型构造函数中向超类型构造函数传递参数。
2)解决了父类的属性会被实例共享的问题
缺点:
1)不能实现函数复用,每次实例化子类,都要执行父类函数,重新声明父类所定义的方法。
2)在父类原型中定义的属性和方法,子类是不可见的,所以不能继承父类原型中定义的属性和方法。
组合继承(常用)
思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ //继承属性 SuperType.call(this, name); this.age = age; } //继承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Nicholas"; instance1.sayAge(); //29 var instance2 = new SubType("Greg", 27); alert(instance2.colors); //"red,blue,green" instance2.sayName(); //"Greg"; instance2.sayAge(); //27
在这个例子中, SuperType 构造函数定义了两个属性: name 和 colors。 SuperType 的原型定义了一个方法 sayName()。 SubType 构造函数在调用 SuperType 构造函数时传入了 name 参数,紧接着又定义了它自己的属性 age。然后,将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法 sayAge()。这样一来,就可以让两个不同的 SubType 实例既分别拥有自己属性——包括 colors 属性,又可以使用相同的方法了。
优点:
1)解决了原型链继承中父类属性被实例共享的问题
2)解决了构造函数继承中不能继承其原型中的属性和方法的问题以及父类方法得不到复用的问题
缺点:
1)调用了父类两次(一次是生成子类实例时候new SubType()中调用了SuperType.call(),一次是生成父类实例也就是子类原型(SubType.prototype = new SuperType())的时候又调用了一次)
2)基于上一点,导致原型中存在相同的两份父类的属性和方法。(子类实例中instance1拥有父类的name和colors属性,子类的原型中SubType.prototype也拥有父类的name和colors属性)。
原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){ function F(){} F.prototype = o; return new F(); }
在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲, object()对传入其中的对象执行了一次浅复制。来看下面的例子。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
克罗克福德主张的这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给 object()函数,然后再根据具体需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象基础的是 person 对象,于是我们把它传入到 object()函数中,然后该函数就会返回一个新对象。这个新对象将 person 作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着 person.friends 不仅属于 person 所有,而且也会被 anotherPerson以及 yetAnotherPerson 共享。
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与 object()方法的行为相同 。
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person); //等同于object函数 anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
优点:兼容性好
缺点:与原型模式一样,引用类型的属性会共享相应的值。
寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){ var clone = object(original); //通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式来增强这个对象 alert("hi"); }; return clone; //返回这个对象 }
在这个例子中, createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然后,把这个对象( original)传递给 object()函数,将返回的结果赋值给 clone。再为 clone 对象添加一个新方法 sayHi(),最后返回 clone 对象。可以像下面这样来使用 createAnother()函数:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
这个例子中的代码基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person的所有属性和方法,而且还有自己的 sayHi()方法。主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
优点:兼容性好
缺点:与构造函数一样不能做到函数复用
寄生组合式继承(最理想)
组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。 解决这个问题方法——寄生组合式继承。
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //方式一使用object函数(需自己定义)创建对象 //var prototype = Object.create(superType.prototype);//方式二使用Object中的方法create创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //指定对象 }
这个示例中的 inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了,例如 :
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); };
这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和 isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
优点:
1)只继承了父类构造函数一次
2)子类的prototype只有子类通过prototype声明的属性和方法
参考书籍:
JavaScript高级程序设计(第3版)