面向对象编程-继承

145 阅读8分钟

面向对象编程

概念

面向对象编程就是将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法)。这个对象我们称之为类。

特点

  • 封装:面向对象编程思想其中有一个特点就是封装,就是说把你需要的功能放在一个对象里。
  • 继承:javascript对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

传宗接代 - 继承

类式继承

类的原型对象的作用就是为类的原型添加共有属性和共有方法,但类不能直接访问这些属性和方法,必须通过原型prototype来访问。而我们实例化一个父类的时候,新创建的对象复制了父类的构造函数内的属性和方法并且将原型proto指向了父类的原型对象,这样就拥有了父类的原型对象上的属性和方法以及父类构造函数中复制的属性和方法(通过this设定的),并且这个新创建的对象可直接访问到父类的原型对象上的属性和方法以及复制的属性和方法。

// 类式继承
// 声明父类
function Superclass(){
    this.superValue = true;
}
// 为父类添加方法
Superclass.prototype.getSuperValue = function() {
    return this.superValue;
}

// 声明子类
function Subclass() {
    this.subValue = false;
}
// 为子类添加方法
Subclass.prototpye.getSubValue = function() {
    return this.subValue;
}

// 父类继承子类
Subclass.prototype = new SuperClass();

// 使用子类
var instance = new Subclass();

console.log(instance.getSuperValue()) // true;
console.log(instance.getSubValue()) // false;

instanceof运算符

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

console.log(instance instanceof Superclass) // true
console.log(instance instanceof Subclass) // true
console.log(Subclass instanceof Superclass) // false

instanceof是判断前面对象是否是后面类(对象)的实例,它并不表示两者的继承关系。Subclass继承Superclass时是通过将superClass的实例赋值给Subclass的原型prototype,所以Subclass instanceof Superclass 为false

console.log(subclass.prototype instanceof superclass); //true

类式继承缺点

  1. 由于子类通过其原型prototype对父类实例化,继承了父类。所以在父类中的公有属性(this创建的)要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的公有属性就会直接影响到其他子类。
function Superclass(){
    this.books = ['javascript', 'html', 'css'];
}

function Subclass() {};

Subclass.prototype = new Superclass();

var instance1 = new Subclass();
var instance2 = new Subclass();
console.log(instance2.books);  // ['javascript', 'html', 'css']
instance1.books.push('设计模式');
console.log(instance2.books);  // ['javascript', 'html', 'css', '设计模式']
  1. 由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。

构造函数继承

通过call方法实现构造函数就继承

// 构造函数继承
// 声明父类
function Superclass(id) {
    // 引用类型共有属性
    this.books =  ['Javascript', 'html', 'css'];
    // 值类型共有属性
    this.id = id;
}
//父类声明原型方法
Superclass.prototype.showBooks = function (){
    console.log(this.books);
}

// 声明子类
function Subclass(id) {
  // 继承父类
  Superclass.call(this, id);
}

// 创建第一个子类实例
var instance1 = new Subclass(10);
// 创建第二个子类实例
var instance2 = new Subclass(11);
instance1.books.push('设计模式');

console.log(instance1.books); // ["Javascript", "html", "css", "设计模式"]
console.log(instance1.id);  // 10
console.log(instance2.books); // ["Javascript", "html", "css"]
console.log(instance2.id);  // 11
instance1.showBooks();  // TypeError

注意这里Superclass.call(this, id); 这条语句是构造函数式继承的精华,由于call这个方法可以更改函数的作用环境,因此在子类中,对Superclass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。

构造函数继承的缺点

  1. 构造函数类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,二如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。

为了综合这两种模式的优点,后来有了组合式继承

将优点为我所用 - 组合继承

类式继承和构造函数继承两种模式的特点,类式继承是通过子类的原型prototype对父类实例化来实现的,构造函数式继承是通过在子类的构造函数作用环境中执行一次父类的构造函数来实现的,所有只要在继承中同事坐到这两点即可。

// 类式继承
// 声明父类
function Superclass(name){
    // 值类型共有属性  
    this.name = name;  
    // 引用类型共有属性  
    this.books = ["html", "css", "Javascript"];
}
// 父类原型共有方法
Superclass.prototype.getName = function() {
    console.log(this.name);
}

// 声明子类
function Subclass(name, time) {
    // 构造函数式继承父类name属性
    Superclass.call(this, name);
    
    // 构造函数式继承父类name属性
    this.time = time;
}
// 类式继承 子类原型继承父类
Subclass.prototype = new Superclass();
// 子类原型方法
Subclass.prototpye.getTime = function() {
    console.log(this.time);
}

var instance1 = new Subclass("js book", 2014);
instance1.books.push("设计模式") 
console.log(instance1.books); // ["html", "css", "Javascript", "设计模式"] 
instance1.getName();  // js book 
instance1.getTime(); // 2014 

var instance2 = new Subclass("css book", 2013);
console.log(instance2.books); // ["html", "css", "Javascript"] instance2.getName();  // css book
instance2.getTime();  // 2013

组合继承缺点

  1. 我们在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数。因此父类构造函数调用了两遍。

原型式继承

对类式继承的一个封装,其中的过渡对象就相当于类式继承中的子类,只不过在原型式中作为一个过渡对象出现的,目的是为了创建要返回的新的实例化对象。

跟类式继承一样,父类对象中的值类型的属性被复制,引用类型的属性被共用。

// 原型式继承
function inheritObject(o) {
    // 声明一个过度函数对象
    function F(){};
    // 过渡对象的原型继承父对象
    F.prototype = o;
    // 返回过渡对象的一个实例,该实例的原型继承了父对象
    return new F();
}

寄生式继承

是对原型继承的二次封装,并且在这第二次封装过程中对继承的对象进行了扩展,这样新创建的对象不仅仅有父类中的属性和方法而且还添加新的属性和方法。

// 寄生式继承
// 声明基对象
var book = {
    name: 'js book',
    alikeBook: ['css book', 'html book']
}

function createBook(obj) {
    // 通过原型继承方式创建新对象
    var o = inheritObject(obj)
    // 拓展新对象
    o.getName = function() {
        console.log(name);
    }
    
    // 返回拓展后的新对象
    return o;
}

这种思想的作用也是为了寄生组合式继承模式的实现。

终极继承者 - 寄生组合式继承

/**
* 寄生式继承 继承原型
* 传递参数 Subclass 子类
* 传递参数 Superclass 父类
**/
function inheritPrototype(Subclass, Superclass){
    // 复制一份父类的原型副本保存在变量中
    var p = inheritObject(Superclass.prototype);
    // 修正因为重写子类原型导致子类的constructor属性被修改
    p.constructor = Subclass;
    // 设置子类的原型
    Subclass.prototype = p;
}

组合式继承中,通过构造函数继承的属性和方法是没有问题的,所以这里主要探究通过寄生式继承重新继承父类的原型。需要继承的仅仅是父类的原型,不再需要调用父类的构造函数,因为在构造函数继承中已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承便可得到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象中p的constructor指向的不是Subclass子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复其constructor属性指向不正确的问题,最后将得到的复制对象p赋值给子类的原型,这样子子类的原型就继承了父类的原型并且没有执行父类的构造函数。

// 定义父类
function Superclass(name) {
    this.name = name;
    
    this.colors = ['red', 'blue', 'green'];
}
// 定义父类原型方法
Superclass.prototype.getName = function() {
    console.log(this.name);
}

// 定义子类
function Subclass(name, time) {
    // 构造函数式继承
    Superclass.call(this, name);
    // 子类新增属性
    this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(Subclass, Superclass);

// 增强子类新增原型方法
Subclass.prototype.getTime = function() {
    console.log(this.time);
}

// 创建实例
var instance1 = new Subclass('js book', '2021');
var instance2 = new Subclass('css book', '2022');

instance1.colors.push("black");
console.log(instance1.colors); //["red", "blue", "green", "black"] console.log(instance2.colors); //["red", "blue", "green"] instance2.getName();  //css book 
instance2.getTime();  //2013

注意的点:子类再想添加原型方法时必须通过prototype.对象,通过点语法的形式一个一个添加方法了,否则直接赋予对象就会覆盖掉从父类原型继承的对象了。

继承原理.png 图-继承原理

多种调用方式 - 多态

概念

在javascript根据传入的参数做判断来实现多用调用的方式。

function Add(){ 
    // 无参数算法 
    function zero(){  return 10; } 
    // —个参数算法 
    function one(num){  return 10 + num; } 
    // 两个参数算法 
    function two(num1, num2){  return num1 + num2; } 
    // 相加共有方法 
    this.add = function(){  
        var arg = arguments,  
        // 获取参数长度  
        len = arg.lengthswitch(len){  
            // 如果设有参数   
            case 0:  
                return zero();  
            // 如果只有—个参数   
            case 1:   
                return one(arg[0]);  
            // 如果有两个参数   
            case 2:   
                return two(arg[0], arg[1]);  
        } 
    } 
} 
// 实例化类 var A = new Add(); 
//测试 
console.log(A.add());  // 10 
console.log(A.add(5)); // 15 
console.log(A.add(6,7)); // 13