类继承

课后整理 2020-12-10

类继承的设计方法:在子类中调用父类构造函数。

在JavaScript中实现类继承,需要主要3个技术问题。

【示例1下面示例演示了一个三重继承的案例,包括基类、父类和子类,它们逐级继承。

//基类Base
function Base( x ){                                    //构造函数Base
    this.get = function(){                          //私有方法,获取参数值 
        return x;
    }
}
Base.prototype.has = function(){                 //原型方法,判断get()方法返回值是否为0
    return ! ( this.get() == 0 );
}
// 父类Parent
function Parent(){                                     //构造函数Parent
    var a = [];                                          //私有数组a
    a = Array.apply( a, arguments  );           //把参数转换为数组 
    Base.call( this, a.length );                    //调用Base类,并把参数数组长度传递给它 
    this.add = function(){                         //私有方法,把参数数组补加到数组a中并返回 
        return a.push.apply( a,  arguments ); 
    }
    this.geta = function(){                        //私有方法,返回数组a
        return a; 
    }
}
Parent.prototype = new Base();                   //设置Parent原型为Base的实例,建立原型链 
Parent.prototype.constructor = Parent;         //恢复Parent类原型对象的构造器 
Parent.prototype.str = function(){               //原型方法,把数组转换为字符串并返回 
    return this.geta().toString();
}
// 子类Sub
function Sub(){                                         //构造函数 
    Parent.apply( this, arguments  );           //调用Parent类,并把参数数组长度传递给它 
    this.sort = function(){                         //私有方法,以字符顺序对数组进行排序 
        var a = this.geta();                       //获取数组的值 
        a.sort.apply( a, arguments  ); 
                                                               //调用数组排序方法sort()对数组进行排序 
    }
}
Sub.prototype = new Parent();                    //设置Sub原型为Parent实例,建立原型链 
Sub.prototype.constructor = Sub;                //恢复Sub类原型对象的构造器 
// 父类Parent的实例继承类Base的成员 
var parent  = new Parent( 1, 2, 3,  4 );        //实例化Parent类 
console.log( parent.get() );                         //返回4,调用Base类的方法get()
console.log( parent.has() );                         //返回true,调用Base类的方法has()
// 子类Sub的实例继承类Parent和类Base的成员 
var sub  = new Sub( 30, 10, 20, 40  );         //实例化Sub类 
sub.add( 6, 5 );                                         //调用Parent类方法add(),补加数组 
console.log( sub.geta() )                             //返回数组30,10 ,20,40 ,6,5
sub.sort()                                                  //排序数组 
console.log( sub.geta() )                             //返回数组10,20 ,30,40 ,5,6
console.log( sub.get() )                              //返回4,调用Base类的方法get()
console.log( sub.has() );                             //返回true,调用Base类的方法has()
console.log( sub.str() );                              //返回10,20 ,30,40 ,5,6

【设计思路】

设计子类Sub继承父类Parent,而父类Parent又继承基类Base。Base、Parnet、Sub三个类之间的继承关系是通过在子类中调用父类的构造函数来维护的。

例如,在Sub类中,Parent.apply( this, arguments );能够在子类类中调用父类,并把子类的参数传递给父类,从而使子类拥有父类的所有属性。

同理,在父类中,Base.call( this, a.length );把父类的参数长度作为值传递给基类,并进行调用,从而实现父类拥有基类的所有成员。

从继承关系上看,父类继承了基类的私有方法get(),为了确保能够继承基类的原型方法,还需要为它们建立原型链,从而实现原型对象的继承关系,方法是添加语句行Parent.prototype = new Base();。

同理,在子类中添加语句Sub.prototype = new Parent();,这样通过原型链就可以把基类、父类和子类串连在一起,从而实现子类能够继承父类属性,还可以继承基类的属性。

【示例2下面尝试把类继承模式封装起来,以便规范代码应用。

【实现代码】

function extend(Sub,Sup){                         //类继承封装函数 
    var F = function(){};                          //定义一个空函数 
    F.prototype = Sup.prototype;               //设置空函数的原型为父类的原型 
    Sub.prototype = new F();                    //实例化空函数,并把父类原型引用传递给子类 
    Sub.prototype.constructor =  Sub;         //恢复子类原型的构造器为子类自身 
    Sub.sup = Sup.prototype;                    //在子类定义一个私有属性存储父类原型 
    //检测父类原型构造器是否为自身 
    if(Sup.prototype.constructor  == Object.prototype.constructor){ 
        Sup.prototype.constructor  =Sup    //类继承封装函数 
    }
}

第1步,定义一个封装函数。设计入口为子类和父类对象,函数功能是子类能够继承父类的所有原型成员,不设计出口。

function extend(Sub,Sup){                         //类继承封装函数 
    //其中参数Sub表示子类,Sup表示父类 
}

第2步,在函数体内,首先定义一个空函数F,用来实现功能中转。设计它的原型为父类的原型,然后把空函数的实例传递给子类的原型,这样就避免了直接实例化父类可能带来的系统负荷。因为在实际开发中,父类的规模可能会很大,如果实例化,会占用大量内存。

第3步,恢复子类原型的构造器子类自己。同时,检测父类原型构造器是否与Object的原型构造器发生耦合。如果是,则恢复它的构造器为父类自身。

【应用代码】

下面定义两个类,尝试把它们绑定为继承关系。

function A(x){                                          //构造函数A
    this.x = x;                                          //私有属性x
    this.get = function(){                          //私有方法get()
        return this.x;
    }
}
A.prototype.add = function(){                    //原型方法add()
    return this.x + this.x;
} 
A.prototype.mul = function(){                    //原型方法mul()
    return this.x * this.x;
}
function B(x){                                          //构造函数B
    A.call(this,x);                                    //在函数体内调用构造函数A,实现内部数据绑定 
}
extend(B,A);                                             //调用封装函数,把A和B的原型捆绑在一起 
var f = new B(5);                                      //实例化类B
console.log(f.get())                                    //继承类A的方法get(),返回5
console.log(f.add())                                   //继承类A的方法add(),返回10
console.log(f.mul())                                  //继承类A的方法mul(),返回25

【提示】

在继承类封装函数中,有这么一句Sub.sup = Sup.prototype;,在上面的代码中没有被利用,那么它有什么作用呢?为了解答这个问题,先看下面的代码:

extend(B,A);
B.prototype.add = function(){                    //为B类定义一个原型方法 
    return this.x + "" +  this.x
}

上面的代码是在调用封装函数之后,再为B类定义了一个原型方法,该方法名与基类中原型方法add同名,但是功能不同。如果此时测试程序,会发现子类B定义的原型方法add()将会覆盖父类A的原型方法add()。

console.log(f.add())                                   //返回字符串55,而不是数值10

如果在B类的原型方法add()中调用父类的原型方法add(),从而避免代码耦合现象发生:

B.prototype.add = function(){                    //定义子类B的原型方法add()
    return B.sup.add.call(this);                  //在函数内部调用父类方法add()
}