JavaScript 继承
原型链
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就形成了实例与原型的链条。这就是所谓原型链的基本概念。
理解:
- 每个构造函数都有原型对象,使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
- 每个对象都有构造函数。(任何函数,只要通过new操作符来调用,那它就可以作为构造函数)
- 每个构造函数的原型都是一个对象。
- 那么这个原型对象也会有构造函数。
- 那么这个原型对象的构造函数也会有原型对象。
- 这样就形成一个链式的结构,称为原型链。
- 通过修改原型结构实现继承,就叫做原型继承。
注:实线表示拥有,虚线表示通过构造函数创建对象。
实现原型链有一种基本模式,其代码大致如下:
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();
console.log(instance.getSuperValue()); //true
以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和方法。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。在确立继承关系之后,我们给SubType.prototype添加了一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法。这个例子中的实例以及构造函数和原型之间的关系如下图所示:
在上面的代码中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型;这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部和还有一个指针,指向了SuperType的原型。最终结果就是这样的:instance指向SubType的原型,SubType的原型又指向了SuperType的原型。getSuperValue()方法仍然还在SuperType.prototype中,但property位于SubType.prototype中。这是因为property是一个实例属性,而getSuperValue()则是一个原型方法。既然SubType.prototype现在是SuperType的实例,那么property当然就位于该实例中了。此外,要注意instance.constructor现在指向的是SuperType,这是因为原来的Subtype.prototype中的constructor被重写的缘故。
属性搜索基本原则
- 当访问一个对象的成员的时候,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值。
- 如果没有找到,则继续搜索指针指向的原型对象,如果找到了这个属性,则返回该属性的值。
- 如果没有找到,继续找原型对象的原型对象,如果找到了返回该属性的值。
- 如果没有找到,则继续向上查找,知道Object.prototype,如果还是没有,就报错。
确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方法就是使用instanceof操作符,只要用这个操作符来测试实例和原型链中出现过的构造函数,结果就会返回true。以下几行代码就说明了这点:
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true
由于原型链的关系,我们可以说instance是Object、SuperType或SubType中任何一个类型的实例。因此,测试这三个构造函数的结果都返回了true。
第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true,如下所示:
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true
继承方式
- 借用构造函数
在解决原型中包含引用类型值所带来的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫作伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数,如下所示:
待续....
- 原型式继承
道格拉斯·克罗科福德在2006年写了一片文章,题为Prototyal Inheritance in JavaScript(JavaScript 中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有严格意义上的构造函数。他的喜爱是借助原型可以基于原有的对象创建新对象,同时不必因此创建自定义类型。为了达到这个目的,他给出了如下函数:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制,来看下面的例子:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
var person = {
name:"saimi",
friends:["Tom","Jerry","Mike"]
};
var anotherPerson = object(person);
anotherPerson.name = "sammy";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Greg");
console.log(person.friends); //"Tom","Jerry","Mike","Rob","Greg"
克罗克福德主张的这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给Object()函数,然后在根据具体需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象基础的是person对象,于是我们把它传入到object函数中,然后该函数就会返回一个新对象。这个新对象将person作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本。
ES5通过新增Object.create()方法规范了原型式继承。这个方法接受两个参数:一个用作新对象原型的对象和(可选的)一个新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
var person = {
name:"saimi",
friend:["Tom","Jerry","Mike"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "sammy";
anotherPerson.friend.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friend.push("Greg");
console.log(person.friend); //"Tom","Jerry","Mike","Rob","Greg"
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var person = {
name:"saimi",
friend:["Tom","Jerry","Mike"]
};
var anotherPerson = Object.create(person,{
name: {
value:"Sammy"
}
});
console.log(anotherPerson.name); //"Sammy"
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
- 寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。以下代码示范了寄生式继承模式。
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:"saimi",
friend:["Tom","Jerry","Mike"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
这个例子中的代码基于person返回一个新对象——anotherPerson。新对象不仅具有person的所有属性和方法,而且还有自己的sayHiO()方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面继承模式时使用的object()函数不是必须的;任何能够返回新对象的函数都是用于此模式。
待续...
主文引自:《JavaScript 高级程序设计 第3版》
参考博客:JS高级——原型链