JavaScript 继承

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。所以我们只要改变对象的原型,就可以做到继承。
先设置一个统一的父类Person

    // 定义一个Person类
    function Person(name) {
        // 属性
        this.name = name || 'Person';
        // 实例方法
        this.sleep = function () {
            console.log(this.name + '正在偷懒~~~!');
        }
    }
    // 原型方法
    Person.prototype.work = function (job) {
        console.log(this.name + '的工作是:' + job);
    };

1、原型链继承

将父类的实例作为子类的原型

function Chinese(address) {
        this.address = address || "earth";
    }
   //要在改变原型对象之后
    Chinese.prototype.skin = '黄色';
    Chinese.prototype = new Person();
    // Test Code
    let chinese = new Chinese('亚洲中国');
   console.log(chinese.skin);//undefined
    console.log(chinese.name);//Person
    console.log(chinese.address);//亚洲中国
    chinese.work('IT');//Person的工作是:IT
    chinese.sleep();//Person正在偷懒~~~!
    console.log(chinese instanceof Person); //true 
    console.log(chinese instanceof Chinese); //true
    let beijing = new Chinese('北京');
    console.log(beijing.name);//Person

优点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例;
  2. 父类新增原型方法/原型属性,子类都能访问到;
  3. 简单,易于实现。

缺点:

  1. 要想为子类新增原型属性和方法,得注意一下要在改变原型对象之后,如上面代码的skin原型属性,(算不上缺点,注意点);
  2. 无法实现多继承;
  3. 来自原型对象的所有属性被所有实例共享;
  4. 创建子类实例时,无法向父类构造函数传参。

2、构造继承

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

    function Chinese(address,name) {
        Person.call(this,name);
        this.address = address || "earth";
    }
    //Test Code
    let chinese = new Chinese('亚洲中国','北京');
    console.log(chinese.name);//北京
    console.log(chinese.address);//亚洲中国
    chinese.sleep();//北京正在偷懒~~~!
    console.log(chinese instanceof Person); //false 
    console.log(chinese instanceof Chinese); //true
    let shanghai = new Chinese('亚洲中国','上海');
    console.log(shanghai.name);//上海
    console.log(shanghai.address);//亚洲中国
    shanghai.sleep();//上海正在偷懒~~~!
    console.log(shanghai instanceof Person); //false 
    console.log(shanghai instanceof Chinese); //true
    chinese.work('IT');//Uncaught TypeError: chinese.work is not a function

优点:

  1. 解决了1中,子类实例共享父类引用属性的问题;
  2. 创建子类实例时,可以向父类传递参数;
  3. 可以实现多继承(call多个父类对象)。

缺点:

  1. 实例并不是父类的实例,只是子类的实例;
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法;(上面代码中原型方法work就会报错);
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。

3、实例继承

为父类实例添加新特性,作为子类实例返回

    function Chinese(address,name) {
        this.address = address || "earth";
        var instance = new Person();
        instance.name = name || '北京人';
        return instance;
    }
    // Test Code
    var chinese = new Chinese('亚洲中国','上海人');
    console.log(chinese.name);//上海人
    chinese.sleep();//上海人正在偷懒~~~!
    chinese.work('IT');//上海人的工作是:IT
    console.log(chinese instanceof Person); // true
    console.log(chinese instanceof Chinese); // false

优点:

  1. 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果。

缺点:

  1. 实例是父类的实例,不是子类的实例;
  2. 不支持多继承。

4、拷贝继承

把父对象的属性,全部拷贝给子对象

   function deepCopy(p, c) {
        var c = c || {};
        let mp = new p();
        for (var i in mp) {
            if (typeof mp[i] === 'object') {
                c[i] = (mp[i].constructor === Array) ? [] : {};
                deepCopy(mp[i], c[i]);
            } else {
                c[i] = mp[i];
            }
        }
        return c;
    }

    function Chinese(address) {
        this.address = address || "earth";
    }
    let chinese= new deepCopy(Person,Chinese);
     console.log(chinese.name);//上海人
    chinese.sleep();//上海人正在偷懒~~~!
    chinese.work('IT');//上海人的工作是:IT
    console.log(chinese instanceof Person); // false
    console.log(chinese instanceof Chinese); // false

上面写的是深拷贝,递归父类所以的属性和方法。
优点:

  1. 支持多继承。

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性);
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)。

5、组合继承

通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

 function Chinese(address,name) {
        Person.call(this,name);//第一次调用父类构造器
        this.address = address || "earth";
    }
    Chinese.prototype = new Person();//第二次调用父类构造器
    Chinese.prototype.constructor=Chinese;
    let chinese = new Chinese('亚洲中国','上海人');
    console.log(chinese.name);//上海人
    console.log(chinese.address);//亚洲中国
    chinese.sleep();//上海人正在偷懒~~~!
    chinese.work('IT');//上海人的工作是:IT
    console.log(chinese instanceof Person); // true
    console.log(chinese instanceof Chinese); // true

为了防止原型链紊乱,编程时务必要遵守一点:如果替换了prototype对象,那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。
特点:

  1. 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

6、 原型继承(很多文章喜欢叫寄生组合继承)

通过这种方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

  function Chinese(address, name) {
        Person.call(this, name);//第一次调用父类构造器
        this.address = address || "earth";
    }

    (function () {
        // 创建一个没有实例方法的类
        var Super = function () { };
        Super.prototype = Person.prototype;
        //将实例作为子类的原型
        Chinese.prototype = new Super();
    })();

    Chinese.prototype.constructor = Chinese;
    let chinese = new Chinese('亚洲中国', '北京人');
    console.log(chinese.name);//北京人
    console.log(chinese.address);//亚洲中国
    chinese.sleep();//北京人正在偷懒~~~!
    chinese.work('IT');//北京人的工作是:IT
    console.log(chinese instanceof Person); // true
    console.log(chinese instanceof Chinese); // true

这种方式是最好的,但是实现有点复杂,需要寄托一个临时的类,这样在改变子类的prototype对象时,仅仅实例化了一个拥有父类prototype对象的类,并没有实例化父类的方法和属性。
归根到底,就是想办法让子类拥有父类的实例属性和方法,还有父类的原型属性和方法。

总结

JavaScript的原型继承实现的最佳方式就是:
1、定义子类的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this;
2、借助中间函数F实现原型链继承,最好通过封装的函数完成(可以复用);

function inherits(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

3、继续在子类的构造函数的原型上定义新方法。

7、 Object.create()(终极大招)

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

     let person = {
        name: "Person",
        sleep: function(value) {
          console.log(`My name is ${this.name}. I am ${value}`);
        }
      };
      let child = Object.create(person);
      console.log(child.name);//Person
      child.name='child';
      console.log(child.name);//child
      child.sleep('sleeping!');//My name is child. I am sleeping!

上例中,child通过Object.create()创建,原型为对象person。child具有person的所以属性和方法。

7.1 语法
Object.create(proto, [propertiesObject])
参数:

proto:新创建对象的原型对象。
propertiesObject:可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。")的第二个参数。

返回值

一个新对象,带着指定的原型对象和属性。

第六点中的继承进行改造
 //将这个方法改造
    (function () {
        // 创建一个没有实例方法的类
        var Super = function () { };
        Super.prototype = Person.prototype;
        //将实例作为子类的原型
        Chinese.prototype = new Super();
    })();
   //使用Object.create(),实现
   Chinese.prototype =Object.create(Person.prototype);

完整代码如下:

      // 定义一个Person类
      function Person(name) {
        // 属性
        this.name = name || "Person";
        // 实例方法
        this.sleep = function() {
          console.log(this.name + "正在偷懒~~~!");
        };
      }
      // 原型方法
      Person.prototype.work = function(job) {
        console.log(this.name + "的工作是:" + job);
      };
      function Chinese(address, name) {
        Person.call(this, name); //第一次调用父类构造器
        this.address = address || "earth";
      }

      Chinese.prototype = Object.create(Person.prototype);
      Chinese.prototype.constructor = Chinese;
      let chinese = new Chinese("亚洲中国", "北京人");
      console.log(chinese.name); //北京人
      console.log(chinese.address); //亚洲中国
      chinese.sleep(); //北京人正在偷懒~~~!
      chinese.work("IT"); //北京人的工作是:IT
      console.log(chinese instanceof Person); // true
      console.log(chinese instanceof Chinese); // true

是不是比上面都要简单,其实就两步:
第一步使用Object.create,创建一个实例对象(__proto__指向父类的prototype),然后把子类的prototype对象指向这个对象,这样:子类的prototype指向Object.create创建的实例对象,这个实例对象的__proto__又指向父类的prototype。
记得要改变子类的constructor的指向,将constructor属性指回原来的构造函数。
第二步在之类中调用父类构造器。
这样就完美,继承了父类的私有属性和方法,也继承了父类的原型链上的方法和属性。

如果你希望能继承到多个对象,则可以使用混入的方式

//子类
function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}
// 继承一个父类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它类
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do a thing
};

Object.assign会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。

PS:

我们知道JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。其实新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。有兴趣的可以去找些资料看看,和java的class一样的用法。

Class继承
        class Earth {
            work(job) {
                console.log(this.name + '的工作是:' + job);
            }
        }
        class Person extends Earth {
            constructor(name) {
                super();// 记得用super调用父类的构造方法!
                this.name = name;
            }
            sleep() {
                console.log(this.name + '正在偷懒~~~!');
            }
        }

        class Chinese extends Person {
            constructor(name, address) {
                super(name); // 记得用super调用父类的构造方法!
                this.address = address;
            }
        }
        let mChinese = new Chinese('上海');
        mChinese.sleep();//上海正在偷懒~~~!
        mChinese.work('IT');//上海的工作是:IT

是不是很简单o(∩_∩)o 哈哈!注意一下,不是所有的主流浏览器都支持ES6的class。如果一定要现在就用上,记得babel工具转码。

你可能感兴趣的:(JavaScript 继承)