Skip to main content

JS:继承方式

原型链


用原型链实现继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

实现原型链有一种基本模式,其代码大致如下。

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();
console.log(instance.getSuperValue()); // true

以上代码中定义了两个类型:SuperType 和 SubType。每个类型都有一个属性和方法,它们的主要区别是 SubType 继承了 SuperType ,而继承是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。

调用 instance.getSuperValue() 会经历三个搜索步骤:1)搜索实例;2)搜索SubType.prototype; 3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环的寻找到原型链末端才会停下来。

原型链的问题

最主要的问题来自包含引用类型值得原型,包含引用类型值得原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。下面代码可以说明这个问题。

function SuperType(){
this.colors = ['red', 'yellow', 'blue'];
}
function SubType(){
}
// 继承了SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'yellow', 'blue', 'black']

var instance2 = new SubType();
console.log(instance2.colors);// ['red', 'yellow', 'blue', 'black']

这个例子中的 SuperType 构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType 的每个实例都会有各自包含自己数组的 colors 属性。当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例,因此他也拥有了一个它自己的 colors 属性 。但结果确是 SubType 的所有实例都会共享这一个 colors 属性。

原型链的第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。实践中很少会单独使用原型链。

借用构造函数


在解决原型中包含引用类型值所带来问题的过程中,开始使用一种叫做借用构造函数的技术。及在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定执行环境中执行代码的对象,因此通过使用 apply() 和 call() 方法也可以在新创建的对象上执行构造函数,如下所示:

function SuperType(){
this.colors = ['red', 'yellow', 'blue'];
}
function SubType(){
// 继承了SuperType
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'yellow', 'blue', 'black']

var instance2 = new SubType();
console.log(instance2.colors);// ['red', 'yellow', 'blue']

传递参数 对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数,如下例子:

function SuperType(name){
this.name = name;
}
function SubType(){
// 继承了SuperType,同时还传递了参数
SuperType.call(this, 'Tom');
// 实例属性
this.age = 27;
}

var instance = new SubType();
console.log(instance.name); // 'Tom'
console.log(instance.age); // 27

借用构造函数的问题 如果仅仅是借用构造函数,那也将无法避免构造函数模式存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起了。而且在超类型的原型中定义方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。因此,借用构造函数的技术也很少单独使用。 存在的问题

组合继承


组合继承,也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长得一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有他自己的属性。如下所示:

function SuperType(name){
this.name = name;
this.colors = ['red', 'yellow', 'blue'];
}
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.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType('Tom', 27);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'yellow', 'blue', 'black']
instance1.sayName(); // "Tom"
instance1.sayAge(); // 27

var instance2 = new SubType('Lucy', 26);
console.log(instance2.colors); // ['red', 'yellow', 'blue']
instance2.sayName(); // "Lucy"
instance2.sayAge(); // 26

在这个例子中,SuperType 构造函数定义了两个属性:colors 和 name,SuperType 的原型定义了一个方法 sayName() 。SubType 构造函数在调用 SuperType 构造函数时传入了 name 参数,紧接着又定义了自己的属性 age。然后将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法 sayAge()。 组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为js 中最常用的继承模式。

原型式继承


这种方法并没有使用严格意义上的构造函数,借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。请看以下函数,

function object(o){
function F(){}
F.prototype = o;
return new F();
}

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制,看下面的例子:

var person = {
name: "Tom",
friends: ["Lucy", "Van", "Shelby"]
};
var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Lina';
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // ["Lucy", "Van", "Shelby", "Rob", "Barbie"]

这种原型式继承,要求你必须要有一个对象可以作为另一个对象的基础。如果有那么一个对象的话,可以把它传递给 object() 函数,然后再根据需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象基础的是 person 对象,于是我们把它传入 object() 函数中,然后该函数返回了一个新对象。这个新对象将 person 作为原型,所以他的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着 person.friends 不仅属于 person 所有,而且也会被 anotherPerson 以及 yetAnotherPerson 共享。实际上这就相当于又创建了 person 对象的两个副本。

ECMAScript5通过新增 Object.create() 方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 与 object() 方法的行为相同。

var person = {
name: "Tom",
friends: ["Lucy", "Van", "Shelby"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person, {
name: {
value: "Lina"
},
friends: {
value: ['lili']
}
});
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); // ["Lucy", "Van", "Shelby", "Rob", "Barbie"]
console.log('yetAnotherPerson',yetAnotherPerson); // {name: 'Lina', friends: ["lili", "Barbie"]}

Object.create() 的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。 在没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的,不过,包含引用类型值得属性始终都会共享相应的值,就像使用原型模式一样。

寄生式继承


寄生式继承与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像是真的是它做了所有工作一样返回对象。

function createAnother(original){
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强这个对象
console.log('hi');
}
return clone; // 返回这个对象
}

在这个例子中,createAnother() 函数接收了一个参数,也就是将要作为新对象基础的对象。然后,把这个对象(original)传递给 object() 函数,将返回的结果赋值给 clone。再为 clone 对象添加一个新方法 sayHi(),最后返回 clone 对象。可以这样来使用 createAnother() 函数:

var person = {
name: "Tom",
friends: ["Lucy", "Van", "Shelby"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

这个例子中的代码基于 person 返回了一个新对象 anotherPerson ,新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法。 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object() 函数不是必需的;任何能够返回新对象的函数都适用于此模式。 注:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要哦的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}

在上面的例子中 inheritPrototype() 函数实现了寄生组合式继承的最简单形式。这个函数接收了两个参数:子类型构造函数和超类型构造函数。函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype() 函数的语句,去替换前面例子中为子类型原型赋值的语句了。请看下面的例子:

function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
console.log(this.age);
}

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;寄生组合式继承是引用类型最理想的继承范式了。