JavaScript高级特性-实现继承的七种方式

声明和约定:

在C++和Java中,我们可以通过关键字class来声明一个类,在JavaScript中没有这个关键字,但我们知道可以通过new一个function创建对象,这个function类似C++和Java中的class,这个function叫做类或者类的构造函数,或者说通过new创建对象的函数叫做构造函数,作为父类时,叫做父类构造函数,作为子类时,叫做子类构造函数;通过构造函数new出来的对象,作为子类时,叫做子类对象或者子类的实例,作为父类时,叫做父类对象或者父类的实例。

1. 原型链继承

继承是OO语言中非常重要的一个特性,在C++、Java中实现继承都非常简单,因为这类语言原生支持继承,只是使用符号或者关键字即可完成继承,但是在JavaScript中实现继承没有那么容易,只能依靠JavaScript语言本身的其它特性来实现,这个特性也就是原型链。设想如下:使用组合构造函数和原型模式的方式创建一个父类构造函数,然后以同样的方式创建一个子类构造函数,使用父类构造函数new一个对象并且将其赋值给子类构造函数的prototype属性,这样子类的构造函数的原型对象就是new出来的父类对象,而父类对象中拥有父类所有的属性和父类共享的方法,因此实现了继承,代码:

function SuperClass(){
        this.property="super"
    }
    SuperClass.prototype.showProperty=function(){
        console.log(this.property);
    }

    function SubClass(){
        this.name="sub"
    }

    SubClass.prototype
=new SuperClass(); SubClass.prototype.showName=function(){ console.log(this
.name);
    }

    var sub1=new SubClass();
    sub1.showProperty();  //super
    sub1.showName();   //sub

以上代码中需要注意的就是必须先给子类构造函数的prototype属性赋值为父类的实例,然后才可以向子类构造函数的prototype添加共有的方法,因为如果先给子类的构造函数添加方法然后再赋值为父类的实例,相当于子类构造函数的的prototype先指向了默认创建的原型对象,先添加方法就把方法添加到了默认的原型对象,此时再赋值为父类的实例,相当于切断了子类构造函数与默认创建的原型对象之间的关系,而将prototype指向了父类的实例,此时的父类的实例并没有添加的方法,所以在子类对象执行自己的函数时根本找不到对应的方法。

在以上代码的基础上添加测试代码后如下:

function SuperClass(){
        this.property="super"
        this.color=["red","blue"];
    }
    SuperClass.prototype.showProperty=function(){
        console.log(this.property);
    }

    function SubClass(){
        this.name="sub"
    }

    SubClass.prototype=new SuperClass();
    SubClass.prototype.showName=function(){
        console.log(this.name);
    }

    var sub1=new SubClass();
    sub1.showProperty();  //super
    sub1.showName();   //sub

    var sub2=new SubClass();
    sub2.showProperty();  //super
    sub2.showName();   //sub

    sub1.property
="modified"


    sub1.showProperty();  
//modified sub2.showProperty(); //super

    sub1.name="sub1";
    sub1.showName();  //sub1
    sub2.showName();  //sub

    console.log(sub1.color.toString());  //red,blue
    console.log(sub2.color.toString());  //red,blue

    sub1.color.push(
"yellow"); console.log(sub1.color.toString()); //red,blue,yellow console.log(sub2.color.toString()); //red,blue,yellow

代码中所有的输出结果都以注释给出了,其中代码标注的地方输出结果值得研究,第一块标注的地方修改了sub1的property属性,输出sub1和sub2的property属性,但结果sub1的property属性是modified,sub2输出结果是super。第二块标注的地方明明只修改了sub1的color属性,但是sub1和sub2的color属性输出结果一致。出现这种情况的原因是:我们将父类对象作为子类构造函数的原型对象,所以父类对象中所有的属性和方法在子类对象中均是共享的,也就是说,所有的子类对象共享一份父类对象的属性和方法,所以在第二块代码标注的地方出现了修改sub1的color属性,sub2的color属性也改变了,而且与sub1的color属性完全一致。至于第一块标注的地方,修改sub1的property属性,而sub2的属性没有发生变化,是因为给sub1的property属性赋值,相当于给sub1添加了一个自己的属性property并且赋值为modified,而并非是给sub1原型的property属性赋值,当调用sub1的showProperty函数的时候,优先从sub1的自己的属性里搜索属性名,所以输出结果是modified,而sub2并没有自己的property属性,所以输出的property属性是sub1和sub2共有的原型里的属性,所以输出结果是super。

这种方式实现继承还有个缺点就是无法向父类构造函数传参,有如下代码:

function Person(name,age,sex){
        this.name=name;
        this.age=age;
        this.sex=sex;
    }

    Person.prototype.sayName=function(){
        console.log(this.name);
    }

    function Student(name,age,sex,grade){
        this.grade=grade;
    }

    Student.prototype
=new
 Person();
    student.prototype.sayGrade=function(){
        console.log(this.grade);
    }

代码标注的地方是这种方式实现继承的关键,而且也是唯一用到父类构造函数的地方,如果在此时将参数传入父类的构造函数,那么所有的新创建的子类对象中父类的属性值将会完全一致,这样的继承失去了活性,根本没有意义。

总的来说,这种方式也就是仅仅实现了继承,优点几乎没有,缺点有:1.没有完成封装,子类实现继承时的代码均在全局作用域中。2.无法向父类的构造函数传参,即使传参了也没有意义。3.所有子类对象共享父类对象的属性和方法。其中第三点是最为致命的一点。

2. 借用构造函数

这种方式也叫伪造对象或者经典继承,思想就是在子类的构造函数中调用父类的构造函数,并将执行环境传入父类的构造函数。如下:

function Person(name,age,sex){
        this.name=name;
        this.age=age;
        this.sex=sex;

        if(typeof arguments.callee.sayName != "function"){
            arguments.callee.prototype.sayName=function(){
                console.log(this.name);
            }
        }
    }

    function Student(name,age,sex,grade){
        Person.call(this,name,age,sex);
        this.grade=grade;
    }

    var stu1=new Student("yangyule",23,"male",3);

    console.log(stu1.name);

这种方式完美的解决了父类的属性变成子类对象共有属性的问题,需要注意的就是在子类构造函数中调用父类构造函数的时候,需要将this作为参数传入父类构造函数的call函数中,这样在父类函数中的this即为新创建的子类对象,如果没有传入this,直接在子类构造函数中调用父类构造函数,父类函数中的this为全局对象,在浏览器中运行代码的时候即为window,无法实现继承。但是仅仅使用这种方式实现继承缺点也比较严重,就是父类的方法子类没有办法继承,不管父类是通过组合构造函数和原型模式还是动态原型模式,都无法避免这个问题,但可以使用组合继承来解决。

3. 组合继承

组合继承就是组合原型链继承和借用构造函数继承的继承方式,这种继承也叫作伪经典继承,是使用最多的继承。这种继承在继承父类属性的时候使用借用构造函数的方式,继承父类方法的时候使用原型链继承,所以避免了单独使用这两种继承方式带来的问题。

function Person(name,age,sex){
        this.name=name;
        this.age=age;
        this.sex=sex;

        if(typeof this.sayName != "function"){
            arguments.callee.prototype.sayName=function(){
                console.log(this.name);
            }
        }
    }

    var person1=new Person("yangyule",23,"male");
    person1.sayName();  //yangyule

    function Student(name,age,sex,grade){
        Person.call(this,name,age,sex);
        this.grade=grade;
    }

    Student.prototype=new Person();
    Student.prototype.sayGrade=function(){
        console.log(this.grade);
    }

    var stu1=new Student("vail",23,"male",4);
    stu1.sayName();  //vail
    stu1.sayGrade();  //4

    var stu2=new Student("hale",23,"male",3);
    stu2.sayName();  //hale
    stu2.sayGrade();  //3

    stu1.age=21;
    console.log(stu1.age);  //21
    console.log(stu2.age);  //23

这种方式解决了主要问题,但带来了新的问题,虽然所有从父类继承而来的属性在子类对象中也是私有属性,不是共享属性(因为在子类的构造函数中调用了父类构造函数,并传入了this),但是由于把子类构造函数的原型对象设置成了父类对象,所以在子类对象的原型中会有一份来自父类的属性存在,也就是说每个子类对象中其实有两份一模一样的来自父类的属性,一份存在于子类对象中,另一份存在于子类对象的原型中,由于访问实例的属性的时候优先搜索自己的属性,所以修改了stu1的age,实际上是修改了stu1自己的age属性,而不是stu1原型中age属性的值,所以stu2的age属性还是23,而不是21。来自父类的属性,除了每个子类对象私有之外,内存中还存在一个子类对象的原型对象,这些来自父类的属性也存在于子类对象的原型对象中,所以造成了内存空间的少部分浪费。这种方式需要注意的就是使用原型链继承父类方法的时候,必须先将子类构造函数的prototype属性设置为父类对象,然后在向子类构造函数的prototype添加方法,和使用原型链实现继承是一样的。这种方式还有一个问题就是封装性,因为要实现继承父类的方法,所以不得不在全局环境下写更多的代码。

4. 完美继承

这种模式是我自创,解决了其它继承模式的一些问题。我学习编程语言是从其他经典OO语言开始的,所以我一直特别在意构造函数的封装性,希望把与类相关的属性和函数写在构造函数中,因此创建对象我特别愿意使用动态原型模式,而我的这种继承方式也要求构造函数必须使用动态原型模式创建。这种继承的思想是在父类的构造函数中检查函数的caller属性是否为空,如果不为空的话,使用for in遍历父类构造函数的原型对象(for in并不会遍历对象的原型中的方法和属性,函数的原型对象是对象,所以使用for in只会遍历函数的原型对象,而不会遍历原型对象的原型对象),将其所有的属性和方法赋值给caller.prototype,继承的时候在子类的构造函数中使用父类构造函数的call方法,并将this作为参数传入,即可完成继承。

function Person(name,age,sex){
        this.name=name;
        this.age=age;
        this.sex=sex;

        if(typeof arguments.callee.sayName != "function"){
            arguments.callee.prototype.sayName=function(){
                console.log(this.name);
            }
        }

        if(arguments.callee.caller != null && typeof arguments.callee.caller.sayName != "function"){
            for(key in arguments.callee.prototype){
                //console.log(key +": "+arguments.callee.prototype[key]);
                arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
            }
        }
    }

    var person1=new Person("yangyule",23,"male");
    person1.sayName();  //yangyule

    function Student(name,age,sex,grade){
        Person.call(this,name,age,sex);
        this.grade=grade;

        if(typeof arguments.callee.sayGrade != "function"){
            arguments.callee.prototype.sayGrade=function(){
                console.log(this.grade);
            }
        }

        if(arguments.callee.caller != null && typeof arguments.callee.caller.sayGrade != "function"){
            for(key in arguments.callee.prototype){
                //console.log(key +": "+arguments.callee.prototype[key]);
                arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
            }
        }
    }

    var student1=new Student("vile",23,"male",3);
    student1.sayName();  //vile
    student1.sayGrade();  //3

    function Undergraduate(name,age,sex,grade,major){
        Student.call(this,name,age,sex,grade);
        this.major=major;
    }

    var cstu1=new Undergraduate("vhile",23,"male",4,"software");
    cstu1.sayName();  //vhile
    cstu1.sayGrade();   //4

    var cstu2=new Undergraduate("vilne",23,"male",2,"math");
    cstu2.sayName();  //vilne
    cstu2.sayGrade();   //2

    console.log(cstu1 instanceof Undergraduate);  //true
    console.log(cstu1 instanceof Student);  //false
    console.log(cstu1 instanceof Person);  //false
    console.log(cstu1.constructor);  //function Undergraduate(...)...

以上代码中除了判断caller是否为空,还判断了caller中是否有继承而来的函数,这样做避免了每次创建对象的时候都为子类构造函数的prototype赋值,优化了执行性能。从上述的代码可以看出,Undergraduate类继承了Student类,Student类又继承了Person类,所以Undergraduate类对象既可以使用Student类中方法,也可以使用Person类中的方法,实现了连续继承。这种继承方式不仅没有破坏封装性,而且还可以检测对象类型、代码量更少,每次代码的变动也不大,每次只需要将检查函数的函数名改为该构造函数原型中的任意一个函数名即可。Java中没有C++中的多继承,但是Java有接口,而本方式实现了类似C++的多继承,只需要在子类的构造函数中调用所有父类的构造函数即可,这种方式是所有方式里唯一实现了多继承的方式。

function SuperClass1(name){
        this.name=name;
        if(typeof this.sayName != "function"){
            arguments.callee.prototype.sayName=function(){
                console.log(this.name);
            }
        }

        if(arguments.callee.caller != null && typeof arguments.callee.caller.sayName != "function"){
            for(key in arguments.callee.prototype){
                arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
            }
        }
    }

    function SuperClass2(age){
        this.age=age;
        if(typeof this.sayAge != "function"){
            arguments.callee.prototype.sayAge=function(){
                console.log(this.age);
            }
        }

        if(arguments.callee.caller != null && typeof arguments.callee.caller.sayAge != "function"){
            for(key in arguments.callee.prototype){
                arguments.callee.caller.prototype[key]=arguments.callee.prototype[key];
            }
        }
    }

    function SubClass(name,age,property){
        SuperClass1.call(this,name);
        SuperClass2.call(this,age);
        this.property=property;

        if(typeof this.sayProperty != "function"){
            arguments.callee.prototype.sayProperty=function(){
                console.log(this.property);
            }
        }
    }

    var sub1=new SubClass("yangyule",23,"I am an object");
    sub1.sayName();  //yangyule
    sub1.sayAge();  //23
    sub1.sayProperty();  //I am an object

以上代码中SubClass继承了SuperClass1和SuperClass2,并成功调用了相应的函数。

完美模式并非一个缺点都没有,因为用到了函数的callee和caller,在严格模式下运行会导致错误。但也仅仅只有这一个微不足道的缺点,大家可以放心的使用。完美模式是我最推荐使用的继承方式。

5. 原型式继承

这种继承方式虽然也是借助于原型链实现继承,但是和原型链继承完全不同,而且它的主要思想和原型的关系也不大(所有的继承都是依赖于原型),至于为什么叫原型式继承,我在《JavaScript高级程序设计》第三版中见到此方式这样叫,所以我也就这样叫了。

之前的几种方式,我们都是竭尽所能的让JavaScript模仿经典的OO语言实现继承,我们不妨换个方式思考一下问题,JavaScript语言相当灵活,我们与其让两个类实现继承,不如直接把父类对象当做一个空对象的原型,在需要的时候向空对象添加属性和方法不是更好吗?

function Person(name,friends){
        this.name=name;
        this.friends=friends;

        if(typeof this.sayFriends != "function"){
            arguments.callee.prototype.sayFriends=function(){
                console.log(this.friends.toString());
            }
        }
    }

    var student1=Object.create(new Person("yangyule",["Bart","Devin"]));
    student1.grade=3;

    student1.sayFriends();  //Bart,Devin

    student2=Object.create(new Person("vile",["YangYule","Evan"]));
    student2.grade=4;

    student2.sayFriends();  //YangYule,Evan
    student1.sayFriends();  //Bart,Devin

其中的Object.create函数接受一个必需的参数和一个可选的参数,第一个参数将作为新对象的构造函数的原型对象,第二参数为新对象额外定义的属性的对象。当给Object.create函数传入一个参数时,它的行为如下:

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

当给create函数传入的对象相同时,产生的对象就是继承自同一个对象。这种继承方式适合对象间的继承,而不是类间的继承,也就是说在不需要批量使用子类对象的时候适合使用这种方式。

6. 寄生式继承

寄生式继承的主要思想是将父类对象拷贝一份,然后再添加需要的方法,这和工厂模式的思想类似。

function createAnother(original){
        var clone = Object(original);
        clone.sayHi=function(){
            alert("hi");
        }
        return clone;
    }
    var me = {name:"yangyule"};
    var clone = createAnother(me);
    clone.sayHi();

这种方式子类继承了父类所有的属性和方法,而且还可以根据需要添加方法,但是缺点是函数不能复用

7. 寄生组合式继承

这种继承方式就比较扯淡了,这种继承方式的提出主要是解决组合继承调用两次构造函数,造成子类对象和子类原型对象中存在两份父类属性的问题,它的主要思想是在子类的构造函数中调用一次父类构造函数,然后将子类构造函数的prototype属性设置为父类构造函数的原型对象的拷贝对象,这样就解决了组合继承带来的问题。但这样做远不如完美继承来的痛快~~~

function Person(name,age,sex){
        this.name=name;
        this.age=age;
        this.sex=sex;

        if(typeof this.sayName != "function"){
            arguments.callee.prototype.sayName=function(){
                console.log(this.name);
            }
        }
    }

    function Student(name,age,sex,grade){
        Person.call(this,name,age,sex);
        this.grade=grade;
    }

    Student.prototype=Object.create(Person.prototype);
    Student.prototype.sayGrade=function(){
        console.log(this.grade);
    }

    var student1=new Student("yangyule",23,"male",3);
    student1.sayName();  //yangyule
    student1.sayGrade();  //3

    console.log(Person.prototype.sayGrade);  //undefined
posted @ 2016-10-17 09:25  Smile_Coding  阅读(875)  评论(0编辑  收藏  举报