JavaScript中6种继承方式

原型链

实现的原理:每个构造函数都有一个原型对象,原型对象中都包含着一个指向构造函数的指针,而实例对象中都有一个指向原型对象的指针。如果让子类的原型对象等于父类的实例对象,那么父类的实例对象的属性和方法,现在也存在于子类的原型对象中了,子类的实例对象就可以访问到父类的属性和方法了。通过给子类的原型对象添加一个方法,就可以实现在继承的基础上又添加一个新方法。

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(instance.getSubValue());				//false

alert(instance.constructor == SuperType);	//true
alert(instance.constructor == SubType);		//false

上面代码中的构造函数,原型链如下:

要注意的是:此时 instance.constructor 指向的是 SuperType,这是因为 SubType 的原型指向了 SuperType 的原型,而SuperType 的原型里 constructor 属性指向的是 SuperType

默认的原型

任何引用类型都默认继承了Object,而且也都是通过原型链继承的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个指向 Object.prototype 的指针。所以上面代码的完整原型链如下:

默认的原型

确定原型和实例的关系

  1. 使用 instanceof 操作符

    // object:某个实例对象,constructor:某个构造函数
    object instanceof constructor

    instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

    alert(instance instanceof Object);			//true
    alert(instance instanceof SuperType);		//true
    alert(instance instanceof SubType);			//true
  2. 使用 isPrototypeOf() 方法
    只要原型链上出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf() 都会返回 true

    alert(Object.prototype.isPrototypeOf(instance));		//true
    alert(SuperType.prototype.isPrototypeOf(instance));		//true
    alert(SubType.prototype.isPrototypeOf(instance));		//true

谨慎地定义方法

  • 如果要给子类添加新方法或者覆盖父类的方法,一定要在替换原型的语句之后再添加新方法。
  • 在给原型添加新方法时,不能使用字面量的形式来添加,因为这样会重写原型链。

原型链的问题

  1. 通过原型链来继承时,子类的原型实际上是父类的实例,于是父类的实例的属性变为了子类的原型属性,当属性值为引用类型时,就会出现原型模式同样的问题。
  2. 在创建子类实例时,不能向父类的构造函数传递参数。

借用构造函数模式

在子类的构造函数内调用父类的构造函数。这样每个子类实例在创建时都会执行父类构造函数中的对象初始化代码,于是子类的每个实例都会有一份自己的属性和方法。

function SuperType(name) {
	this.name = name;
	this.sayName = function() {
		alert(this.name);
	}
}
function SubType(name, age) {
	SuperType.call(this, name);
	this.age = age;
}
var instance = new SubType("Tom", 12);
instance.sayName();		//"Tom"
alert(instance.age);	//"12"

存在的问题:

  • 子类的实例都会有一个相同的方法 sayName(),函数的复用性不好。
  • 父类的原型对象的方法对子类是不可见的。

组合继承

使用原型链对原型属性和方法进行继承,使用借用构造函数对实例属性进行继承。结合了原型链和借用构造函数的优点,同时避免了它们的缺点。而且还能使用 instanceofisPrototypeOf() 方法来检测对象类型。

function SuperType(name) {
	this.name = name;
	this.colors = ["white", "black"];
}
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 指向了 SuperType,需要把 constructor 赋值为 SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
	alert(this.age);
}
var instance1 = new SubType("Tom", 12);
instance1.colors.push("orange");
instance1.sayName();		//"Tom"
instance1.sayAge();			//12
alert(instance1.colors);	//"white,black,orange"
var instance2 = new SubType("Jerry", 10);
instance2.sayName();		//"Jerry"
instance2.sayAge();			//10
alert(instance2.colors);	//"white,black"

原型式继承

以一个对象作为另一个对象的基础,把这个基础对象传入 object() 函数,再根据需求对得到的对象进行修改。

function object(o) {
	function F() {}
	F.prototype = o;
	return new F();
}
var person = {
	name: "Tom",
	colors: ["white", "black"]
};
var person1 = object(person);
person1.name = "Jerry";
person1.colors.push("orange");
alert(person1.name);		//"Jerry"
alert(person1.colors);		//"white,black,orange"
var person2 = object(person);
alert(person2.name);		//"Tom"
alert(person2.colors);		//"white,black,orange"

使用 Object.create() 方法实现规范化原型式继承:

var person = {
	name: "Tom",
	colors: ["white", "black"]
};
var anotherPerson = Object.create(person, {
	name: {
		value: "Jerry"
	}
});
alert(anotherPerson.name);		//"Jerry"

存在的问题:当基础对象的属性值为引用类型时,同样会出现原型模式的问题。

寄生式继承

创建一个封装继承过程的函数,在函数内部以某种方式增强对象,然后再返回这个对象。

function createObj(obj) {
	var clone = Object.create(obj);
	clone.sayHi = function() {
		alert("Hi");
	}
	return clone;
}
var person = {
	name: "Tom",
	colors: ["white", "black"]
};
var anotherPerson = createObj(person);
anotherPerson.sayHi();	//"Hi"

存在的问题:不能做到函数的复用;当基础对象的属性值为引用类型时,同样会出现原型模式的问题。

寄生组合式继承

组合继承的问题:调用了两次父类的构造函数,第一次是在创建子类型原型的时候,第二次是在子类型构造函数内部。第一次调用时,子类型原型会得到一组父类型的实例属性,第二次调用时,在子类型实例上又创建了一组父类型的实例属性。于是第二组的实例属性会屏蔽第一组原型上的同名属性。
寄生组合式继承:通过借用构造函数继承实例属性,通过原型链的混成形式来继承方法。

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

function inheritPrototype(subType, superType) {
	var proto = object(superType.prototype);			//得到父类型原型的副本
	proto.constructor = subType;						//设置副本的 constructor 属性
	subType.prototype = proto;							//替换子类型的原型
}

function SuperType(name) {
	this.name = name;
	this.colors = ["white", "black"];
}
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);
};
var instance1 = new SubType("Tom", 12);
instance1.sayName();			//"Tom"
instance1.sayAge();				//12
instance1.colors.push("orange");
alert(instance1.colors);		//"white,black,orange"
var instance2 = new SubType("Jerry", 10);
instance2.sayName();			//"Jerry"
instance2.sayAge();				//10
alert(instance2.colors);		//"white,black"

上面代码的原型链如下:

寄生组合式继承只调用了一次父类的构造方法,并且因此避免了在子类原型上创建不必要的属性。与此同时,原型链还能保持不变,因此还能使用 instanceofisPrototypeOf() 方法来检测对象类型。