美文网首页
JavaScript 继承

JavaScript 继承

作者: Kevin丶CK | 来源:发表于2019-04-11 16:43 被阅读0次

    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 继承

          本文链接:https://www.haomeiwen.com/subject/gubtwqtx.html