JavaScript设计模式 Item 4 --继承

1、继承

在javascript中继承是一个非常复杂的话题,比其他任何面向对象语言的中的继承都复杂得多。在大多数其他面向对象语言中,继承一个类只需要使用一个关键字即可。与它们不同,在javascript中要想达到传承公用成员的目的,需要采取一系列措施。更有甚者,javascript属于使用原型式继承的少数语言之一。利益于这种语言的灵活性,你既可使用标准的基于类的继承,也可使用更微妙一些的原型式继承。

2、为什么需要继承?

一般来说,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合。使用继承符合前一个设计原则的需要。借助这种机制,你可以在现有类的基础上进行设计并充分利用它们已经具备的各种方法,而对设计进行修改也更为轻松。假设你需要让几个类都拥有一个按特定方式输出类结构的toString()方法,当然可以用复制加粘贴的办法把定义toString()方法的代码添加到每一个类中,但这样做的话,每当需要改变这个方法的工作方式时,你将不得不在每一个类中重复同样的修改。反之,如果你提供了一个ToStringProvider类,然后让那些类继承这个类,那么toString这个方法只需在一个地方声明即可。

让一个类继承另一个类可能会导致二者产生强耦合,也即一个类的依赖于另一个类的内部实现。我们将讨论一些有助于避免这种问题的技术,其中包括用掺元类为其他类提供方法这种技术。

3、基于类的继承

下面看下面的代码:

 <script type="text/javascript">  

        function Person(name, age)  
        {  
            this.name = name;  
            this.age = age;  
        }  
        Person.prototype.say = function ()  
        {  
            console.log(this.name + " , " + this.age);  
        }  
        function Student(no)  
        {  
            this.no = no;  
        }  
        /**
         * Student的prototype指向Person的对象 
         */</span>  
        Student.prototype = new Person();  
        var stu1 = new Student("0001");  
        stu1.name = "张三";  
        stu1.age = "11";  
        console.log(stu1.no);  
        stu1.say();  
    </script>  

输出结果:

0001   
张三 , 11   

可以看到Student成功集成了Person,并且拥有了Person的say方法,核心代码其实就是一句 Student.prototype = new Person();,下面通过图解来说明原理:

这里写图片描述

将Student.prototype指向new Person() , new Person的_proto_又指向Person Prototype;这样完成了整个继承。

但是这种方式存在问题:

问题1:当父类存在引用类型变量时,造成数据不一致,下面我们给Person添加一个hobbies属性,类型为数组。

<script type="text/javascript">  
       /**
        * 存在问题 
        * 1、无法在Student的构造方法中传递参数用于父类的构造方法 
        * 2、对于引用类型变量,造成数据不一致 
        */  

       function Person(name, age)  
       {  
           this.name = name;  
           this.age = age;  
           this.hobbies = [] ;  
       }  
       Person.prototype.say = function ()  
       {  
           console.log(this.name + " , " + this.age +" , " +this.hobbies);  
       }  
       function Student(no)  
       {  
           this.no = no;  
       }  
       Student.prototype = new Person();  

       var stu1 = new Student("0001");  
       stu1.name = "张三";  
       stu1.age = "11";  
       stu1.hobbies.push("soccer");  
       stu1.say();  

       var stu2 = new Student("0002");  
       stu2.name = "李四";  
       stu2.age = "12";  
       stu2.hobbies.push("girl");  
       stu2.say();  
   </script>  

输出结果:

张三 , 11 , soccer   
李四 , 12 , soccer,girl   

可以看出,李四的hobbies应该只有girl,但是上面的代码让所有对象共享了hobbies属性。
上述的继承方式还存在一个问题:

问题2:在Student的构造方法中,无法使用new Student(“00001” , “张三” , 12) ;创建对象,并初始化name和age属性,必须stu.name, stu.age进行赋值

为了解决上述问题,对上述代码进行修改:

<script type="text/javascript">  

       function Person(name, age)  
       {  
           this.name = name;  
           this.age = age;  
           this.hobbies = [];  
       }  
       Person.prototype.say = function ()  
       {  
           console.log(this.name + " , " + this.age +" , " + this.hobbies);  
       }  

       function Student(name, age, no)  
       {  
           /**
            * 使用call方法,第一个参数为上下文; 
            * 有点类似Java中的super(name,age)的感觉 
            */  
           Person.call(this, name, age);  
           this.no = no;  
       }  

       Student.prototype = new Person();  

       var stu1 = new Student("0001","张三",11);  
       stu1.hobbies.push("soccer");  
       stu1.say();  

       var stu2 = new Student("0002","李四",12);  
       stu2.hobbies.push("cangjin");  
       stu2.hobbies.push("basketball");  
       stu2.say();  
   </script>  

输出:

0001 , 张三 , soccer   
0002 , 李四 , cangjin,basketball   

在Student的构造方法中使用了Person.call(this,name,age)感觉就像super(name,age)【call的第一个参数为上下文】;并且成功解决了对引用属性的共享问题,完美解决。

4、基于原型链的继承

<script type="text/javascript">  

    /**
     * 基于原型链的集成中都是对象 
     * 存在问题: 
     * 1、对于引用类型变量,造成数据不一致 
     */  
    var Person = {  
                name: "人",  
                age: 0,  
                hobbies: [],  
                say: function ()  
                {  
                    console.log(this.name + " , " + this.age + " , " + this.hobbies);  
                }  
            }  
            ;  

    var Student = clone(Person);  
    Student.no ="";  
    Student.sayHello = function()  
    {  
        console.log(this.name  +"hello ") ;  
    }  

    var stu1 = clone(Student);  
    stu1.name = "zhangsan";  
    stu1.age = 12;  
    stu1.hobbies.push("Java");  
    stu1.say();  

    var stu2 = clone(Student);  
    stu2.name = "lisi";  
    stu2.age = 13;  
    stu2.hobbies.push("Javascript");  
    stu2.say();  

    /**
     * 返回一个prototype执行obj的一个对象 
     * @param obj 
     * @returns {F} 
     */  
    function clone(obj)  
    {  
        var F = function ()  
        {  
        };  
        F.prototype = obj;  
        return new F();  

    }   
</script>  

输出:

zhangsan , 12 , Java   
lisi , 13 , Java,Javascript   

可以看出同样存在引用属性不一致的问题,并且整个操作全部基于对象,给人的感觉不是很好,下面通过图解解释下原理:

这里写图片描述

对象间通过一个clone函数,不断的返回一个新的对象,且prototype执行传入的对象,整个继承过程其实就是_proto_不断的指向,形成一个链,所以叫做原型链。

好了,已经介绍完了,js的两种集成的方式,最好使用的还是通过类的继承(上述第一种方案,解决存在问题的)。

文章导航