前言:大多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);
}
|