您的位置:

Javascript 继承实现深度解析

一、原型继承

原型继承是Javascript中最基本也是最核心的继承方式,如何实现原型链的继承呢?由于Javascript中所有的对象都拥有原型属性(即__proto__属性),该属性指向父对象,实现对象之间的继承可以通过指定某个对象(即子对象)的原型__proto__为其父对象。在实践中我们可以使用如下方式来实现原型继承:

  
    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        this.name = name;
    }
    // 指定Child的原型为Parent
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;

    var child1 = new Child('child1');
    var child2 = new Child('child2');
    child1.sayHello(); // hello child1
    child2.sayHello(); // hello child2
    alert(child1.colors); // blue, black, yellow
    child1.colors.push('red');
    alert(child2.colors); // blue, black, yellow, red
  

在上述代码中,我们声明了一个Parent函数和一个Child函数,Parent 函数中有 name 属性和 sayHello 方法,Child 函数中只有 name 属性,我们实现了 Child 的原型继承自 Parent,并把 Child的原型的构造器设置为 Child自己,实例化 Child 后就可以访问到 Parent的属性和方法。

实践中请注意:

  • 实现原型继承时,需要在既定的基础上对其进行开发,建议使用封装性较好的函数进行开发,方便以后在其他项目也可以复用。
  • 你也可以通过Object.create函数进行原型继承,这个函数可以上述代码整合成如下:
  
    function object(p) {
        function F() {}
        F.prototype = p;
        return new F();
    }

    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        this.name = name;
    }
    // 指定Child的原型为Parent
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;

    var child1 = new Child('child1');
    var child2 = new Child('child2');
    child1.sayHello(); // hello child1
    child2.sayHello(); // hello child2
    alert(child1.colors); // blue, black, yellow
    child1.colors.push('red');
    alert(child2.colors); // blue, black, yellow, red
  

二、经典继承

经典继承,即利用 call 或者 apply 方法来实现继承。经典继承相对于原型继承来说,显得更加灵活、高效。在设计模式中,很多模式基于经典继承的思想来构建,这些设计模式中都在使用 call 和 apply 方法来实现调用父类构造函数。在实践中我们可以使用如下方式来实现经典继承:

  
    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        // 将 Parent 的作用域赋给 Child,以实现父类实例化
        Parent.call(this, name);
        this.age = 18;
    }

    var child = new Child('child');
    alert(child.name); // child
    alert(child.age); // 18
    alert(child.colors); // undefined
  

我们声明了两个函数,Parent 函数中有 name 属性和 sayHello 方法,Child 函数中通过使用 Parent.call() 将 Parent 函数的作用域赋给了 Child,从而实现了 Child 对象继承 Parent 对象的属性和方法。这种方式不会污染父类的原型属性,同时也可以在子类实例化时传入属性,实际开发中应用广泛。

实践中请注意:

  • 经典继承可以实现单继承或者多重继承,如下例中既同时继承自Father和Mother:
  
    function Father() {
          this.name = 'father';
      }

    function Mother() {
          this.age = 30;
      }

    function Child() {
          Father.call(this);
          Mother.call(this);
      }

    var child = new Child();
    console.log(child.name); // father
    console.log(child.age); // 30 
  

三、组合继承

组合继承,即将原型继承和经典继承进行组合来实现继承。通过使用原型链实现对超类属性(即方法)的共享,在通过调用父类构造函数来实现 prototype 中属性的定义。在实践中我们可以使用如下方式来实现组合继承:

  
    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        Parent.call(this, name);
    }
    Child.prototype = new Parent();
    Child.prototype.constructor = Child;

    var child1 = new Child('child1');
    var child2 = new Child('child2');
    child1.sayHello(); // hello child1
    child2.sayHello(); // hello child2
    alert(child1.colors); // blue, black, yellow 
    child1.colors.push('red');
    alert(child2.colors); // blue, black, yellow
  

我们声明了两个函数,Parent 函数中有 name 属性和 sayHello 方法,Child 函数中在调用 Parent.call(this, name) 的情况下实现经典继承,然后在原型链上继承了 Parent 的属性和方法,最后将 Child 的构造函数重新赋值给 Child 的原型构造函数,从而实现 Child 对象的属性和方法的继承。关于组合继承,在实践中是一种非常好的方式。

实践中请注意:

  • 在编写组合继承时,需要注意不要重复实例化父对象,我们可以使用 Object.create(Parent.prototype) 的方式来创建一个中介者。
  
    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        Parent.call(this, name);
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;

    var child1 = new Child('child1');
    var child2 = new Child('child2');
    child1.sayHello(); // hello child1
    child2.sayHello(); // hello child2
    alert(child1.colors); // blue, black, yellow 
    child1.colors.push('red');
    alert(child2.colors); // blue, black, yellow
  

四、寄生组合继承

寄生组合继承,即相对于组合继承来说不会重复实例化父对象,而是创建一个空的函数作为中介者来实现继承。在实践中我们可以使用如下方式来实现寄生组合继承:

  
    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        Parent.call(this, name);
    }
    var __ = function() {};
    __.prototype = Parent.prototype;
    Child.prototype = new __();
    Child.prototype.constructor = Child;

    var child1 = new Child('child1');
    var child2 = new Child('child2');
    child1.sayHello(); // hello child1
    child2.sayHello(); // hello child2
    alert(child1.colors); // blue, black, yellow 
    child1.colors.push('red');
    alert(child2.colors); // blue, black, yellow
  

在上述代码中,我们通过创建名( __ )叫占位符(thunk)的函数作为中介者来实现继承,并将 Child 的原型指向这个中介器,避免了在使用 Object.create() 方法的重复实例化问题。实践证明在大多数情况下,寄生组合继承是一种更好的继承方式,我们还可以通过Module模式对寄生组合继承进行封装,易于复用。

实践中请注意:

  • 当某个父类中有多个子类时,我们可以将父类中公共的方法提取出来,在Child构造函数的里面进行调用,达到代码复用的目的。
  
    function Parent(name){
        this.name = name;
        this.sayHello = function(){
            alert('hello ' + this.name);
        }
    }
    Parent.prototype.colors = ['blue', 'black', 'yellow'];

    function Child(name){
        Parent.call(this, name);
    }
    function extend(child, parent){
        var Middle = function(){};
        Middle.prototype = parent.prototype;
        child.prototype = new Middle();
        child.prototype.constructor = child;
        child.prototype.parent = parent.prototype;
        return child;
    }

    extend(Child, Parent);

    var child1 = new Child('child1');
    var child2 = new Child('child2');
    child1.sayHello(); // hello child1
    child2.sayHello(); // hello child2
    alert(child1.colors); // blue, black, yellow 
    child1.colors.push('red');
    alert(child2.colors); // blue, black, yellow
  

五、ES6 继承

ES6 提出了 extends 关键字,可以极大的简化继承的操作,我们只需要在子类中添加 extends 关键字,并在解析 super 关键字时使用 new 关键字调用父类的构造函数即可。在实践中我们可以使用如下方式来实现ES6继承:

  
     class Parent{
        constructor(name){
            this.name = name;
        }
        sayHello(){
            alert('hello ' + this.name);
        }
     }
     Parent.prototype.colors = ['blue', 'black', 'yellow'];

     class Child extends Parent{
        constructor(name){
            super(name)
        }
     }

     let child1 = new Child('child1');
     let child2 = new Child('child2');
     child1.sayHello(); // hello child1
     child2.sayHello(); // hello child2
     alert(child1.colors); // blue, black, yellow 
     child1.colors.push('red');
     alert(child2.colors); // blue, black, yellow
  

在上述代码中,我们声明了一个 Parent 类和一个 Child 类,Child 类直接继承自 Parent 类,我们只需要在 Child 类中添加 extends 关键字,并在解析 super 关键字时使用 new 关键字调用父类的构造函数即可,如果是需要继承父对象的属性和方法,则直接使用 super 关键字即可。ES6 继承更加直观舒适,使用起来也非常方便。

实践中请注意:

  • ES6继承在编码时需要注意浏览器的兼容性问题,如果是在较低版本的浏览器中使用的话,需要通过 babel 来实现ES6的编译。
  • 可以在ES6中通过 Symbol.hasInstance 重