您的位置:首页 > Web前端 > JavaScript

js学习笔记:对象——继承

2016-09-22 22:39 375 查看

伪“类“继承

每个构造函数有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针。

如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么这个关系将层层递进下去,构成了实例与原型的链条,此为原型链

实现原型链的基本模式:

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


继承实现的本质是令SubType的原型对象等于SuperType的实例,即重写了原型对象代之以另一个类型的实例。原来存在于SuperType实例中的所有属性和方法,都存在于SubType.prototype中。

在此基础上,又为SubType.prototype添加一个新方法



最终结果就是SubType的实例指向SubType的原型,SubType的原型又指向SuperType的原型。

getSuperValue()仍然在SuperType.prototype中,但property属性是在SubType.prototype中,因为property是一个实例属性,而getSuperValue则是一个原型方法。

此时instance.constructor指向的是SuperType。因为SubType的原型被重写了,instance指向SubType的原型,SubType的原型再指向SuperType的原型,而SuperType原型的constructor指针指向SuperType。

实现原型链,本质上是扩展了之前的原型搜索机制。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。

如,调用instance.getSuperValue()会经历三个步骤:

搜索实例

搜索SubType.prototype

搜索SuperType.prototype

在找不到属性或方法时,搜索过程要层层递进一直到原型链末端才停下来。

如果我们尝试去获取对象的某个属性值,但该对象没有此属性名,那么js会试着从原型对象中获取属性值。如果那个原型对象也没有该属性,那么再从它的原型中寻找,以此类推,直到该过程最后到达终点Object.prototype。如果想要的属性完全不存在于原型链中,那么结果就是undefined。

这个过程称为委托

继承Object

所有引用类型都默认继承于Object,这个继承也是通过原型链实现的

所有函数的默认原型都是Object的实例,因此所有原型对象都会默认包含一个指向Object.prototype的指针。这也是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。

因此上面的例子其实是SubType继承了SuperType,SuperType继承了Object。当调用instance.toString()时,实际上是调用了保存在Object.prototype中的方法。

确定原型和实例的关系

instanceof 操作符:只要用来测试实例与原型链中出现过的构造函数,就会返回true

instance instanceof Object;  //true
instance instanceof SuperType;  //true
instance instanceof SubType;  //true


由于原型链的关系,可以说instance 是Object、SuperType、SubType中任何一个类型的实例,因此都会返回true

isPrototypeOf():同样,只要是原型链中出现过的原型,此方法都会返回true

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


谨慎地定义方法

子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法,不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。不然先给原型添加方法,而后又将原型重新赋了值(等于超类的实例),就等于没有添加方法。

给原型添加方法也不能使用对象字面量,不然又会重写原型,断掉继承的原型链

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();
instance.getSuperValue(); //出错!!


以上代码展示了刚刚把SuperType的实例赋给原型,就又将原型替换成了一个对象字面量,原型链已经被切断,SubType和SuperType没有任何关系了。

原型链的问题

包含引用类型值(如数组)属性的原型,会有所有实例共享属性的问题。本来可以利用在构造函数中定义属性来解决问题,但通过原型实现继承时,原型会变成另一个类型的实例,那么原先的实例属性就变成了原型属性了……

function SuperType(){
this.colors = ["red","blue","green"];
}
fucntion SubType(){}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
instance1.colors; //"red","blue","green","black"

var instance2 = new SubType();
instance2.colors;  //"red","blue","green","black"


SuperType构造函数定义了一个包含数组(引用类型值)的属性colors,SuperType的每个实例都会有各自包含自己数组的colors属性。

SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,它也拥有了自己的colos属性,就跟专门创建了SubType.prototype.colors一样。结果就是所有SubType的实例都会共享这一个colors属性。

原型链的第二个问题是,在创建子类型的实例时,不能向超类型的构造函数中传递参数。

因此很少单独使用原型链。

借用构造函数

为了解决原型中包含引用类型值所带来的问题,使用借用构造函数技术(伪造对象/经典继承)

基本思想:在子类型构造函数的内部调用超类型构造函数。

function SuperType(){
this.colors = ["red","blue","green"];
}

//继承了SuperType
fucntion SubType(){
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
instance1.colors; //"red","blue","green","black"

var instance2 = new SubType();
instance2.colors;  //"red","blue","green"


SuperType.call(this);这一句实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数,这样就会在新SubType对象上执行SuperType()函数中定义的对象初始化代码,这样每个SubType实例就会有自己的colors属性了。

传递参数

相对于原型链而言,借用构造函数可以在子类型构造函数中向超类型构造函数传递参数

function SuperType(){
this.name = name;
}

//继承了SuperType,同时还传递了参数
fucntion SubType(){
SuperType.call(this,"nicholas");

//实例属性
this.age = 29;
}

var instance = new SubType();
instance.name; //"nicholas"
instance.age;  //29


借用构造函数的问题

和构造函数模式的问题一样,属性和方法都在构造函数中定义,共享方法无从谈起。而且在超类型的原型中定义的方法,子类型也是看不见的。因此也很少单独使用借用构造函数。

组合继承

将原型链和借用构造函数的技术组合到一块。

使用原型链实现对原型属性和方法的继承

通过借用构造函数实现对实例属性的继承

这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性

function SuperType(){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}

fucntion SubType(name,age){
//继承属性
SuperType.call(this,name);

this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}

var instance1 = new SubType("nicholas",29);
instance1.colors.push("black");
instance1.colors; //"red","blue","green","black"
instance1.sayName(); //"nicholas"
instance1.sayAge();  //29

var instance2 = new SubType("greg",27);
instance2.colors; //"red","blue","green"
instance2.sayName(); //"greg"
instance2.sayAge();  //27


这样,两个子类型的实例既分别拥有自己的属性(包括colors属性),又可以使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为最常用的继承模式。而且,instanceof和isPrototypeOf()也能够成功识别基于组合继承创建的对象。

原型式继承

前面那些原型+构造函数式的继承,其实是模仿其他面向对象语言继承的一种“伪类”实现。但在js里,其实也有更好的选择,因为继承实际上就是对象之间的联系而已。

在一个纯粹的原型模式中,我们将摒弃类而专注于对象。也就是一个新对象可以继承一个旧对象的属性。

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


在函数object内部:

先创建了一个临时性的构造函数F()

将传入的对象作为这个构造函数的原型

返回这个临时类型的一个新实例

其实本质上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");

person.friends; //"shelby","court","van","rob","barbie"


这种原型式继承,必须有一个对象可以作为另一个对象的基础。这个例子中,object返回的对象以person为原型,所以它的原型中就包含一个基本类型值属性name和引用类型值属性friends。这就意味着person.friends不仅属于person,也会被所有实例(anotherPerson和yetAnotherPerson)共享。

Object.create()

ES5新增的这个方法规范了原型式继承,用于创建一个使用原对象作为原型的新对象。

接收两个参数:

用作原型的对象

为新对象定义额外属性的对象(可选)

在传入一个参数的情况下,Object.create()与object()方法的行为相同。

var person = {
name:"nicholas",
friends:["shelby","court","van"]
}

var anotherPerson = Object.create(person);
anotherPerson.name = "greg";
anotherPerson.friends.push("rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "linda";
yetAnotherPerson.friends.push("barbie");

person.friends; //"shelby","court","van","rob","barbie"


Object.create()第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
name:"nicholas",
friends:["shelby","court","van"]
}

var anotherPerson = Object.create(person,{
name:{
value:"greg"
}
});

anotherPerson.name; //"Greg"


在没有必要兴师动众地创建构造函数,而是只想让一个对象与另一个对象保持类似的情况下,原型式继承就可以胜任。

但是,包含引用类型值的属性始终都会共享相应的值。

当然,Object.create()也可以与构造函数继承组合使用:

//Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}

Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};

// Rectangle - subclass
function Rectangle() {
Shape.call(this); //call super constructor.
}

Rectangle.prototype = Object.create(Shape.prototype);

var rect = new Rectangle();

rect instanceof Rectangle //true.
rect instanceof Shape //true.

rect.move(1, 1); //Outputs, "Shape moved."


其中,
Rectangle.prototype = Object.create(Shape.prototype);
这句代码的后半部分返回一个以
Shape.prototype
为原型的新对象,把这个对象赋值给
Rectangle.prototype
,即
Rectangle.prototype
的原型为
Shape.prototype


注意!以下两种方式不太可取:

Rectangle.prototype = Shape.prototype;

Rectangle.prototype = new Shape();


第一种方式只是令Rectangle的原型直接引用Shape.prototype,而并不是联系到一起,除非你只想使用Shape对象而不使用Rectangle对象,否则这样做没有什么意义。

第二中方式其实是上面提到过的继承方式:

Object.create()
与传统继承中
Rectangle.prototype = new Shape()
的方式有异曲同工之妙,都是使子类原型指向父类原型,但是传统方式会使
Rectangle.prototype
拥有Shape的实例属性:

//采用Object.create(),只是让子类的原型指向父类的原型,而不会使子类原型获得父类的实例属性
Rectangle.prototype.x;  //undefined

//改用传统继承方式
Rectangle.prototype = new Shape();
//Rectangle.prototype为Shape的一个实例,拥有实例属性
Rectangle.prototype.x;  //0


Object.create()只是单纯地指定一个新对象的原型,而如果这个新对象是另一个原型对象,那么就形成了这个原型对象和另一个原型的关联,形成了继承关系。

Object.create(null)
会创建一个不拥有原型链的对象,这样特殊的空对象完全不会受原型链的干扰,因此非常适合用来存储数据。

在ES5之前的环境中如果想实现相同的功能的话,就只能借助之前在原型式继承中定义的函数来部分实现:

if(!Object.create){
Object.create = function(o){
function F(){}
F.prototype = o;
return new F();
}
}


寄生式继承

寄生式继承与原型式继承紧密相关。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装过程的函数,该函数在内部以某种方式来增强对象。

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


createAnother()函数接受了一个作为新对象基础的对象作为参数

把这个对象传递给object函数,将返回的结果赋给clone

为clone对象添加新方法

返回clone对象

可以如下这么使用createAnother函数:

var person = {
name:"nicholas",
friends:["shelby","court","van"]
}

var anotherPerson = createAnother(person);
anotherPerson.sayHi();


我觉得其实和原型式继承没什么不同,只是把新建对象和为对象添加属性或方法的函数封装在一起了。

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。其中的object函数也不是必须的,任何能够返回新对象的函数都适用于此模式。

但是使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

寄生组合式继承

组合继承最大的问题是:无论什么情况下,都会调用两次超类构造函数:

在创建子类型原型时

在子类型构造函数内部

function SuperType(){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}

fucntion SubType(name,age){
//继承属性
SuperType.call(this,name); //第二次调用SuperType()

this.age = age;
}

//继承方法
SubType.prototype = new SuperType(); //第一次调用SuperType()
SubType.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}

var instance1 = new SubType("nicholas",29);


在第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们现在是SubType的原型属性



当调用SubType构造函数创建新对象时,会第二次调用SuperType时,这时又在新对象上创建了两个实例属性:name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。



因此,其实有两组name和colors属性,一组在实例上,一组在SubType原型上。这就是调用两次SuperType构造函数的结果。

解决方法就是:寄生组合式继承。

寄生组合式继承的思路是:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

意思是,不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。

本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

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


创建超类型原型的一个副本

为创建的副本添加constructor属性,弥补因重写原型而失去的默认constructor属性

将新创建的对象赋给子类型的原型

这样就可以替换之前例子中为子类型原型赋值的语句了。

function SuperType(){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}

fucntion SubType(name,age){
//继承属性
SuperType.call(this,name); //第二次调用SuperType()
this.age = age;
}

//继承方法
inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge = function(){
alert(this.age);
}


这个例子的高效率体现在只调用了一次SuperType构造函数,因此避免了在SubType.prototype上创建不必要的、多余的属性。且与此同时,原型链保持不变,也还能正常使用instanceof和isPrototypeof()。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  javascript