js继承之借用构造函数继承

from:http://www.cnblogs.com/sarahwang/p/6879161.html

我的上一篇文章介绍了,原型链继承模式。但是单纯的原型链模式并不能很好地实现继承。

一、原型链的缺点

1.1 单纯的原型链继承最大的一个缺点,来自于原型中包含引用类型的值。

本来,我们没有通过原型链实现继承前,子类的所有实例都共享着原型上的所有属性和方法。通过子类实例,可以访问原型上的属性,但是,不能重写原型上的属性。

复制代码
//定义一个学生类
function Student(stuID, schoolName) {
    this.stuID = stuID;
    this.schoolName = schoolName;
}
//所有学生都有这样一个特征
Student.prototype.characteristic = '年轻有朝气';
            
var stu1 = new Student(1001,'第一小学');
console.log(stu1.stuID);                //1001
console.log(stu1.characteristic);       //'年轻有朝气'
            
//重写characteristic
stu1.characteristic = '活泼可爱';
console.log(stu1.characteristic);       //'活泼可爱'

var stu2 = new Student(1002,'第一小学');
console.log(stu2.characteristic);       //'年轻有朝气'
console.log(Student.prototype);         //{characteristic: "年轻有朝气"}
复制代码

  上面这段代码,stu1实例可以访问到原型上的属性 characteristic, 打印的是“年轻有朝气”;而后,我们又设置 stu1.characteristic = '活泼可爱',这时打印 stu1.characteristic 就变成了“活泼可爱”。那是不是说明原型上的 characteristic 属性值已经被我们通过实例 stu1 重写了呢?并没有。我们发现访问 stu2.characteristic 的值还是 “年轻有朝气”。所以原型上的属性值是不会通过实例被重写的。

  当实例中,存在和原型中同名的属性时,会自动屏蔽原型上的同名属性。stu1.characteristic = = '活泼可爱' 实际上是给实例stu1添加了一个本地属性 characteristic,所以当我们再次访问stu1.characteristic 时,访问的是实例的本地属性,而不是原型上的characteristic属性(它因和本地属性名同名已经被屏蔽了)。

  原型上的 characteristic 值是一个基本类型的值,如果是一个引用类型呢?这其中又会有一堆小九九。

  其实原型上任何类型的值,都不会被实例所重写。在实例上设置与原型上同名属性的值,只会在实例上创建一个同名的本地属性。但是,原型上引用类型的值可以通过实例进行修改,而且所有的实例访问到的该引用类型的值也会随之改变。

复制代码
//定义一个学生类
function Student(stuID, schoolName) {
    this.stuID = stuID;
    this.schoolName = schoolName;
}
//所有学生都有这样一个特征
Student.prototype.characteristic = '年轻有朝气';
Student.prototype.examItems = ['语文','数学','英语'];  //考试项目


var stu1 = new Student(1001, '第一小学');
console.log(stu1.examItems); //['语文','数学','英语']

//修改examItems
stu1.examItems.push('科学');
console.log(stu1.examItems); //['语文','数学','英语','科学']

var stu2 = new Student(1002, '第一小学');
console.log(stu2.examItems); //['语文','数学','英语','科学']
复制代码

  我们在刚才的 Student 原型上又添加了一个考试项目属性 examItems, 这是个引用类型的值。最初实例 stu1 访问 examItems 时,得到的是 ['语文','数学','英语'],然后我们通过stu1.examItems.push('科学') 给examItems 这个数组又添加了一个“科学”项,再次打印时,数组变成了 ['语文','数学','英语','科学']。可怕的是,此后打印实例 stu2.examItems 也变成了 ['语文','数学','英语','科学']。

  因此,我们得出结论,原型上任何类型的属性值都不会通过实例被重写,但是引用类型的属性值会受到实例的影响而修改。

1.2 原型链不能实现子类向父类中传参。这里就不细说了。

二、借用构造函数

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

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

  所以,其实单独使用原型链继承或者借用构造函数继承都有自己很大的缺点,最好的办法是,将两者结合一起使用,发挥各自的优势。我将在下一篇文章作出解释。

文章内容全都参考于《JAVASCRIPT 高级程序设计》)

猜你喜欢

转载自www.cnblogs.com/wulihong/p/8907384.html