笔记之--javascript--11--继承
前言:大多OO语言都支持两种继承方式: 接口继承和实现继承 ,而ECMAScript中无法实现接口继承,ECMAScript只支持实现继承,而且其实现继承主要是依靠 原型链 来实现。
1、一,关于原型
- function Person(){}//这里我们声明一个函数Person,js中函数是对象,也是构造函数
- console.log(Person.prototype)//打印一下Person对象的原型,会出现什么呢?如下图所示:
大家在图中看到了,Person对象的原型拥有一个constructor,它指向Person的构造函数,即Person本身,另外一个属性是__proto__属性,这个属性我会在后文中说明。
到这里,大家肯定会明白了,一个对象建立后,会产生一个局部的“小链式结构”,即Person对象拥有一个prototype属性,这个属性指向原型对象,在原型对象中又有一个构造器constructor,指向构造函数。用一张图来说明:
那么,原型对象的作用是什么呢?这个原型对象包含由特定类型的实例共享的属性和方法。大家要注意共享这两个字,用一段代码解释下
- function Person(){
- this.name="bob" //这是一个实例属性
- }
- Person.prototype.eat=function(){ //给对象的原型对象添加一个eat的方法,接下来,new的实例会共享这个方法
- return "food";
- }
- var p1=new Person(); //这里究竟发生生了什么?
- p1.eat()//->food
- var p2=new Person();
- p2.eat()//->food,所以只要是Person的对象,他们都会共享原型对象的方法,当然,p1.name也会共享Person的实例属性,因为p1是Person的一个实例
好了,到这里原型的概念我们已经讲完了,大家或许会疑问,上面的new一个Person实例的过程中究竟发生了什么呢?为什么这个实例能够访问到原型对象中的方法?其实,在这个过程过程中,p1实例拥有了一个指针,这个指针指向构造函数的原型对象。此时原型对象中的方法自然能够被实例所访问。用一张图来说明下:
这里,我们总结下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象拥有一个指向构造函数的指针,而实例拥有一个指向原型对象的内部指针(这就是前面所提到的[[Prototype]],即__proto__,要注意的是这个__proto__属性在chrome浏览器中是可以看到的,而在大部分浏览器是隐藏的!)
二,关于原型链继承
好了,说了这么多终于到回到我们的主角了【原型链】,提出一个思考:如果我们让原型对象等于另外一个对象的实例,将会有一个什么样的结果呢?先看下面一段代码
- function Person(){
- this.name="bob";
- }
- Person.prototype.eat=function(){
- return "food";
- }
- function Student(){}
- Student.prototype=new Person();//将Person实例赋给Student的原型对象
- var one=new Student();
- one.name//bob
- one.eat()//food,Student的实例能访问到Person对象的实例方法,也能访问到其原型属性中的方法
以上就是原型链继承的一种基本模式,那么我们怎么解释这样的原理呢?之前说过,对象的实例拥有一个指向原型对象的指针,那么student的原型对象拥有了Person对象实例后,自然也拥有一个指向Person原型对象的指针。此时,我们再new一个Student实例one时,one实例包含一个指向Student原型的指针,而Student.prototype拥有一个指向Person原型对象的指针,Person原型本身包含一个指向自身构造函数的指针。这样一来,就构成了实例与原型的链条。这就是所谓的原型链的概念!
用一张图描绘一下上面讲的情况:
大家要注意一下,这里的one对象的constructor现在指向谁呢?它并不指向Student,因为Student的原型指向另一个对象--Person的原型,而这个原型对象的constructor指向的是Person.
三,原型链方法的改写及注意的问题
- function Person(){
- this.name="bob" ;
- }
- Person.prototype.eat=function(){
- return "food";
- }
- function Student(){}
- //Student.prototype.eat=function(){
- // return "food1";
- //}
- //注意如果更改原型语句的代码放在替换之前,那么下面one.eat()的结果将仍然是food
- //,原因很简单,前面对prototype对象的修改,在后面的替换一句中被Person实例对象覆盖了
- //,换句话说,就是现在的prototype实例中仍旧是以前的eat方法
- Student.prototype=new Person();
- Student.prototype.eat=function(){
- return "food1";
- }
- var one=new Student();
- console.log(one.eat());//food1
- function Person(){
- this.name="bob" ;
- }
- Person.prototype.eat=function(){
- return "food";
- }
- function Student(){}
- Student.prototype=new Person();
- Student.prototype={
- run:function(){
- return "run";
- }
- };
- var one=new Student();
- console.log(one.eat());//Uncaught TypeError: undefined is not a function
- function Person(){
- this.name="bob" ;
- }
- Person.prototype.eat=function(){
- return "food";
- }
- function Student(){}
- Student.prototype=new Person();
- Student.prototype.constructor=Student;//把Student原型对象中原本指向Person构造函数的对象强行指向到Student
- var one=new Student();
- console.log(one.eat());//food
四,如何确定原型和实例关系
- function Person(){
- this.name="bob" ;
- }
- Person.prototype.eat=function(){
- return "food";
- }
- function Student(){}
- Student.prototype=new Person();
- Student.prototype.constructor=Student;
- var one=new Student();
- var person=new Person();
- console.log(one instanceof Student);//true
- console.log(one instanceof Person);//true
- console.log(person instanceof Person);//true
- console.log(person instanceof Student);//false
- function Person(){
- this.name="bob" ;
- }
- Person.prototype.eat=function(){
- return "food";
- }
- function Student(){}
- Student.prototype=new Person();
- var one=new Student();
- var person=new Person();
- console.log(Student.prototype.isPrototypeOf(one));//true
- console.log(Person.prototype.isPrototypeOf(one));//true
- console.log(Person.prototype.isPrototypeOf(person));//true
- console.log(Student.prototype.isPrototypeOf(person));//false
原型链继承的问题
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);
} |