笔记之--javascript--11--继承

前言:大多OO语言都支持两种继承方式: 接口继承和实现继承 ,而ECMAScript中无法实现接口继承,ECMAScript只支持实现继承,而且其实现继承主要是依靠 原型链 来实现。

1、一,关于原型

首先,我们不得不解释下原型的概念:我们创建的每一个函数都有一个原型属性,即prototype,这个属性是一个指针,指向原型对象。
[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){}//这里我们声明一个函数Person,js中函数是对象,也是构造函数  
  2. console.log(Person.prototype)//打印一下Person对象的原型,会出现什么呢?如下图所示:  


大家在图中看到了,Person对象的原型拥有一个constructor,它指向Person的构造函数,即Person本身,另外一个属性是__proto__属性,这个属性我会在后文中说明。

到这里,大家肯定会明白了,一个对象建立后,会产生一个局部的“小链式结构”,即Person对象拥有一个prototype属性,这个属性指向原型对象,在原型对象中又有一个构造器constructor,指向构造函数。用一张图来说明:



那么,原型对象的作用是什么呢?这个原型对象包含由特定类型的实例共享的属性和方法。大家要注意共享这两个字,用一段代码解释下

[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob" //这是一个实例属性  
  3. }  
  4. Person.prototype.eat=function(){ //给对象的原型对象添加一个eat的方法,接下来,new的实例会共享这个方法  
  5.   return "food";  
  6. }  
  7. var p1=new Person();  //这里究竟发生生了什么?  
  8. p1.eat()//->food  
  9. var p2=new Person();  
  10. p2.eat()//->food,所以只要是Person的对象,他们都会共享原型对象的方法,当然,p1.name也会共享Person的实例属性,因为p1是Person的一个实例    

好了,到这里原型的概念我们已经讲完了,大家或许会疑问,上面的new一个Person实例的过程中究竟发生了什么呢?为什么这个实例能够访问到原型对象中的方法?其实,在这个过程过程中,p1实例拥有了一个指针,这个指针指向构造函数的原型对象。此时原型对象中的方法自然能够被实例所访问。用一张图来说明下:

这里,我们总结下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象拥有一个指向构造函数的指针,而实例拥有一个指向原型对象的内部指针(这就是前面所提到的[[Prototype]],即__proto__,要注意的是这个__proto__属性在chrome浏览器中是可以看到的,而在大部分浏览器是隐藏的!)

二,关于原型链继承

好了,说了这么多终于到回到我们的主角了【原型链】,提出一个思考:如果我们让原型对象等于另外一个对象的实例,将会有一个什么样的结果呢?先看下面一段代码

[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob";    
  3. }  
  4. Person.prototype.eat=function(){  
  5.   return "food";  
  6. }  
  7. function Student(){}  
  8. Student.prototype=new Person();//将Person实例赋给Student的原型对象  
  9. var one=new Student();  
  10. one.name//bob  
  11. one.eat()//food,Student的实例能访问到Person对象的实例方法,也能访问到其原型属性中的方法  

以上就是原型链继承的一种基本模式,那么我们怎么解释这样的原理呢?之前说过,对象的实例拥有一个指向原型对象的指针,那么student的原型对象拥有了Person对象实例后,自然也拥有一个指向Person原型对象的指针。此时,我们再new一个Student实例one时,one实例包含一个指向Student原型的指针,而Student.prototype拥有一个指向Person原型对象的指针,Person原型本身包含一个指向自身构造函数的指针。这样一来,就构成了实例与原型的链条。这就是所谓的原型链的概念!

用一张图描绘一下上面讲的情况:

大家注意一下,这里的one对象的constructor现在指向谁呢?它并不指向Student,因为Student的原型指向另一个对象--Person的原型,而这个原型对象的constructor指向的是Person.

三,原型链方法的改写及注意的问题

有时候,子类型需要改写超类型当中的方法,或者添加新的方法,一定要注意给原型添加代码一定要放在继承语句(即替换原型语句)的后面,
[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob" ;  
  3. }  
  4. Person.prototype.eat=function(){  
  5.   return "food";  
  6. }  
  7. function Student(){}  
  8. //Student.prototype.eat=function(){   
  9. //  return "food1";         
  10. //}             
  11. //注意如果更改原型语句的代码放在替换之前,那么下面one.eat()的结果将仍然是food  
  12. //,原因很简单,前面对prototype对象的修改,在后面的替换一句中被Person实例对象覆盖了  
  13. //,换句话说,就是现在的prototype实例中仍旧是以前的eat方法  
  14. Student.prototype=new Person();  
  15. Student.prototype.eat=function(){  
  16.   return "food1";  
  17. }  
  18. var one=new Student();  
  19. console.log(one.eat());//food1  
但是大家要注意一下一种情况,在通过原型链继承时,不能通过对象字面量个方式来更新原型对象

[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob" ;  
  3. }  
  4. Person.prototype.eat=function(){  
  5.   return "food";  
  6. }  
  7. function Student(){}  
  8.   
  9. Student.prototype=new Person();  
  10. Student.prototype={  
  11.   run:function(){  
  12.     return "run";  
  13.   }  
  14. };  
  15. var one=new Student();  
  16. console.log(one.eat());//Uncaught TypeError: undefined is not a function   
在上面的代码中,把Person的实例赋给Student的原型,接下来又把原型改写成另一个对象字面量,现在原型包含的是Object实例,不再是Person实例,因此原型链已经被切断了,也就是说Student和Person没关系了。

但是思考下面一段代码,我这样改写,会切断原型链吗?
[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob" ;  
  3. }  
  4. Person.prototype.eat=function(){  
  5.   return "food";  
  6. }  
  7. function Student(){}  
  8.   
  9. Student.prototype=new Person();  
  10. Student.prototype.constructor=Student;//把Student原型对象中原本指向Person构造函数的对象强行指向到Student  
  11. var one=new Student();  
  12. console.log(one.eat());//food  
从代码运行的情况来看,这个动作并没有切断原型链的继承,原因何在?
大家一定要主要实例和原型对象是通过[[Prototype]]来实现关系链接的,换句话说,实例里面的[[Prototype]]指针指向原型对象,这里我改写了constructor后,并没有将[[Prototype]]的指向改变,当然也就没有改变整个原型链的继承关系! 这一点要非常注意!!!

四,如何确定原型和实例关系

可以通过两种方式来确定原型和实例之间的关系,第一种是instanceof操作符,这个操作符用来测试实例与原型链中出现过的构造函数,看下面的一段代码
[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob" ;  
  3. }  
  4. Person.prototype.eat=function(){  
  5.   return "food";  
  6. }  
  7. function Student(){}  
  8.   
  9. Student.prototype=new Person();  
  10. Student.prototype.constructor=Student;  
  11. var one=new Student();  
  12. var person=new Person();  
  13. console.log(one instanceof Student);//true  
  14. console.log(one instanceof Person);//true  
  15. console.log(person instanceof Person);//true  
  16. console.log(person instanceof Student);//false  
最后一个出现了false,什么原因,instanceof的工作是什么呢?
A intanceof B,它的工作原理是,检测对象B的prototype指向的对象是否出现在Ad对象的[[Prototype]]链上,换句话说,A对象的原型链继承线路上,有没有B的存在。最后一个例子中Student的原型对象指向Person的原型对象,person实例也是指向Person的原型对象,而Person的原型对象指向了Person的构造函数本身,所以这条person对象的原型链路上,并没有出现Student的原型对象,故最后一个person不是Student的实例!

那么,第二种检测方式是isPrototypeOf()方法,同理,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
[javascript]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. function Person(){  
  2.   this.name="bob" ;  
  3. }  
  4. Person.prototype.eat=function(){  
  5.   return "food";  
  6. }  
  7. function Student(){}  
  8.   
  9. Student.prototype=new Person();  
  10.   
  11. var one=new Student();  
  12. var person=new Person();  
  13. console.log(Student.prototype.isPrototypeOf(one));//true  
  14. console.log(Person.prototype.isPrototypeOf(one));//true  
  15. console.log(Person.prototype.isPrototypeOf(person));//true  
  16. console.log(Student.prototype.isPrototypeOf(person));//false 
  17.  

原型链继承的问题

1. 最主要的问题来自包含引用类型值的原型,我们知道共享原型是存在问题的,抛出一个例子

?
1
2
3
4
5
6
7
8
9
10
function Person () {
}
Person.prototype = {
friends = [ "a" , "b" ];
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push( "c" );
console.log(person1.friends); // "a","b","c"
console.log(person2.friends); //"a","b","c"

通过引用实例改变了原型中本来中的值,同时也影响了其他实例。(这就是为什么引用类型值要定义在构造函数中而非原型中的原因)

在原型链中同样也会有同样的情况出现:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function A () {
this .numbers = [1,2,3];
}
function B() {
}
B.prototype = new A();
var b = new B();
var a = new A();
b.numbers.push(4);
b.numbers; //1234
var b2 = new B();
b2.numbers; //1234
a.numbers; //123

我们看到出现了和上面一样的情况(在通过原型来继承时,原型实际上会变成另一个类型的实例。于是原先的实例属性也就顺理成章的变成了现在原型属性了)。

二、借用构造函数

2.1 实现原理

实现原理是,在子类的构造函数中,通过 apply( ) 或 call( )的形式,调用父类构造函数,以实现继承。

复制代码
//定义一个超类/父类: 人
function Person (name, age) {
    //人都有姓名,年龄,会吃饭,会睡觉
    //传入出生年份 year,自动计算年龄
    this.name = name;
    this.age = age;
    this.eat = function () {
        alert('吃饭');
    }
    this.sleep = function () {
        alert('睡觉');
    }
}

//定义一个子类: 学生
//学生Student也是人,自然要继承超类 Person 的所有属性和方法
//学生都应当有姓名、年龄、会吃饭、会睡觉
//当然学生也有自己的一些属性:学号,学校名称等,和方法,比如都要去做一件事:写作业
function Student (stuID, schoolName, name, age) {
    this.stuID = stuID;
    this.schoolName = schoolName;
    //用call调用 Person,以实现继承
    Person.call(this, name, age);
}

Student.prototype.doHomework = function () {
    alert('做作业');
}

//实例化一个学生
var stu1 = new Student(1001, '第一小学', '王宝宝',20);
console.log(stu1.stuID);       //1001
console.log(stu1.schoolName);  //'第一小学'
console.log(stu1.name);        //'王宝宝'
console.log(stu1.age);         //20
stu1.eat();                    //'吃饭'
stu1.sleep();                  //'睡觉'
stu1.doHomework();             //'做作业'    
复制代码

  上面代码定义了一个父类函数 Person 和一个子类函数 Student, 在子类构造函数中,我们通过 call 的方式调用了父类构造函数 Person实现了继承。别忘了,函数只不过是一段可以在特定作用域执行代码的特殊对象,我们可以通过 call 方法指定我函数的作用域。

  在 stu1 = new Student() 构造函数时,Student 内部 this 的值指向的是 stu1, 所以 this.stuID =stu1.stuID, 所以 Person.call(this, name, age) 就相当于Person.call(stu1, '王宝宝', 20),就相当于 stu1.Person('王宝宝',20)。最后,stu1 去调用 Person 方法时,Person 内部的 this 指向就指向了 stu1。那么Person 内部this 上的所有属性和方法,都被拷贝到了stu1上。说到这里,大家应该清楚一点点了吧。

  总之,在子类函数中,通过call() 方法调用父类函数后,子类实例 stu1, 可以访问到 Student 构造函数和 Person 构造函数里的所有属性和方法。这样就实现了子类向父类的继承。

 

2.2 缺点

  这种形式的继承,每个子类实例都会拷贝一份父类构造函数中的方法,作为实例自己的方法,比如 eat()。这样做,有几个缺点:

  1. 每个实例都拷贝一份,占用内存大,尤其是方法过多的时候。

  2. 方法都作为了实例自己的方法,当需求改变,要改动其中的一个方法时,之前所有的实例,他们的该方法都不能及时作出更新。只有后面的实例才能访问到新方法。

3.组合继承

基本思想:将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。

例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function SuperType(name) {
this .name = name;
this .colors = [ "red" , "blue" , "green" ];
}
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( "EvanChen" ,18);
instance1.colors.push( "black" );
consol.log(instance1.colors); //"red","blue","green","black"
instance1.sayName(); //"EvanChen"
instance1.sayAge(); //18
var instance2 = new SubType( "EvanChen666" ,20);
console.log(instance2.colors); //"red","blue","green"
instance2.sayName(); //"EvanChen666"
instance2.sayAge(); //20

4.原型式继承

基本想法:借助原型可以基于已有的对象创建新对象,同时还不必须因此创建自定义的类型。

原型式继承的思想可用以下函数来说明:

?
1
2
3
4
5
function object(o) {
function F(){}
F.prototype = o;
return new F();
}

例子:

?
1
2
3
4
5
6
7
8
9
10
11
var person = {
name: "EvanChen" ,
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" );
console.log(person.friends); //"Shelby","Court","Van","Rob","Barbie"

ECMAScript5通过新增Object.create()方法规范化了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和一个作为新对象定义额外属性的对象。

?
1
2
3
4
5
6
7
8
9
10
11
var person = {
name: "EvanChen" ,
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" );
console.log(person.friends); //"Shelby","Court","Van","Rob","Barbie"

5.寄生式继承

基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正是它做了所有工作一样返回对象。

例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
alert( "hi" );
};
return clone;
}
var person = {
name: "EvanChen" ,
friends:[ "Shelby" , "Court" , "Van" ];
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); ///"hi"

6.寄生组合式继承

基本思想:通过借用函数来继承属性,通过原型链的混成形式来继承方法

其基本模型如下所示:

?
1
2
3
4
5
function inheritProperty(subType, superType) {
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}

例子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
inheritProperty(SubType,SuperType);
SubType.prototype.sayAge = function () {
alert( this .age);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值