美文网首页JavaScript技术
构造函数原型的继承方式分析

构造函数原型的继承方式分析

作者: An的杂货铺 | 来源:发表于2021-05-18 17:40 被阅读0次

    1.通过原型链继承

    function One(age) {
        this.nameArr = [ 'Tom', 'Cat' ];
        this.age = age || 20;
    }
    One.prototype.say = function() {
        console.log(this.nameArr);
        console.log(this.age);
    };
    
    const one = new One(25);
    console.log(one); //{nameArr:[ 'Tom', 'Cat' ],age:25}
    
    //现在创建另一个构造函数Two
    function Two() {}
    //使用One构造函数的实例来作为构造函数Two的原型对象
    Two.prototype = new One();
    //为构造函数Two添加方法 eat
    Two.prototype.eat = function() {
        console.log('eat');
    };
    //基于Two来创建构造函数Two的实例
    const two = new Two(30);
    console.log(two); //{} 空对象 因为构造和函数Two函数体没有内容
    console.log(two.nameArr, two.age); //[ 'Tom', 'Cat' ] 20
    /*虽然 实例two是一个空对象 但是它访问属性的时候会沿着原型链向它的原型对象去找,因为
    Two.prototype = new One();所以可以访问到 nameArr 和 age的属性, 因为在指定Two的原型时
    调用的 new Two() 没有传参 所以age的属性值是默认的 20,创建two时 传入的参数 30无用
    */
    
    //基于Two创建第二个实例 two_2
    const two_2 = new Two();
    console.log(two_2); // {}
    console.log(two_2.nameArr, two_2.age); // [ 'Tom', 'Cat' ] 20
    
    //现在我们修改Two创建的实例 two的引用类型类型属性 nameArr 以及非引用类型属性 age
    two.nameArr.push('kate');
    two.age = 100;
    console.log(two.nameArr, two.age); //[ 'Tom', 'Cat','kate' ] 100
    
    //现在 我们再来看实例 two_2
    console.log(two_2.nameArr, two_2.age); //[ 'Tom', 'Cat','kate' ] 20
    console.log(two.say(), two_2.say()); //[ 'Tom', 'Cat','kate' ] 100  [ 'Tom', 'Cat','kate' ] 20
    console.log(two.eat(), two_2.eat()); //eating eating
    //重写构造函数 Two的say 方法
    Two.prototype.say = function() {
        console.log('我是Two的say方法');
    };
    console.log(two.say(), two_2.say()); //我是Two的say方法 我是Two的say方法
    console.log(Two.prototype.constructor === One); // true //可见 Two的 prototype上的constructor也被篡改了
    

    综上我们可以总结出 通过原型链来实现继承的原理
    通过原型链来实现继承的原理
    原型链继承方案中,父类型创建的实例来作为另一个类型的原型对象,新类型实际上也就变成父类型的一个实例,
    像这里的Two.prototype = new One(), 其原型上的引用类型值会被新类型创建的所有实例共享,当该类型创
    建的一个实例修改了引用类型的属性值,那么该类型创建的其他实例的该引用类型的属性也会被修改,也就是多
    个实例对引用类型的操作会被篡改.
    缺点:
    1.创建子类型实例时无法向父类型的构造函数传参。
    2.子类型多个实例对引用类型的操作有被篡改的风险。
    3.子类型的原型上的 constructor 属性被重写了
    基于原型链继承的问题,原型链继承通常是不被推荐使用的继承方式

    2.借用构造函数来实现的继承(经典继承)

    基于原型链的继承存在的不能传参以及原型的引用类型数据在多个实例共享使用时会出现被篡改的问题,出现了借用构造函数来实现的继承方案。

    //经典继承,也叫做借用构造函数继承
    
    function One(age) {
        this.nameArr = [ 'tom', 'lili' ];
        this.age = age;
    }
    One.prototype.say = function() {
        console.log('hello world');
    };
    const one = new One(15);
    console.log(one); // {nameArr:[ 'tom', 'lili' ],age:15}
    
    //创建构造函数 Two 并借用构造函数One来实现Two继承One
    function Two(age) {
        One.call(this, age);
    }
    //给Two添加方法
    Two.prototype.sing = function() {
        console.log('sing');
    };
    
    //基于Two 创建实例 two1
    const two1 = new Two(20);
    two1.nameArr.push('kate');
    console.log(two1); //{name:[ 'tom', 'lili','kate ],age:20}
    //基于Two 创建实例 two2
    const two2 = new Two(22);
    console.log(two2); //{name:[ 'tom', 'lili' ],age:22}
    console.log(two1.sing(), two2.sing()); //sing sing
    //console.log(two1.say(), two2.say()); //报错 two1.say is not a function
    //为Two定义say方法
    Two.prototype.say = function() {
        console.log('hello');
    };
    console.log(two1.say(), two2.say()); //hello hello
    

    综上,我们可以总结出经典继承的原理
    当子类型需要继承父类型时,在子类型构造函数中通过关键字call来调用父类型,在此时机可以实现
    传参。这样当我们使用子类型再去创建实例时,每次创建实例都会调用一次父类的方法,从而每个子
    类创建的实例都有一份属于当前实例自己的数据,他们是当前父类中定义的属性和方法的副本。
    优点:经典继承的解决了子类型创建实例时的传参问题,同时创建的实例都有一份自己的数据,不会存在
    属性篡改的问题。
    缺点:父类型的原型中定义的方法对于子类型以及子类创建的实例都是不可见的,因此子类型如果需要这些方
    法的话,需要将方法都在构造函数中再定义一次(方法都在构造函数中定义,每次创建实例都会创建一遍方法)。
    或者在子类型的prototype上再定义一次。
    这种经典的继承方案可以使用,但并不是最好的继承方式。

    3.js继承的组合继承

    /*js继承的组合继承  经典继承和原型链继承的组合*/
    
    //创建父类构造函数
    function Person(age, name) {
        this.age = age;
        this.name = name;
        this.arr = [ 1, 2 ];
    }
    Person.prototype.say = function() {
        console.log(this.age, this.name, this.arr);
    };
    let p1 = new Person(18, 'tom');
    console.log(p1); //{ age: 18, name: 'tom', arr: [ 1, 2 ] }
    
    //创建子类构造函数 使用组合继承方式继承Person的属性和方法
    function Man(age, name, sex) {
        Person.call(this, age, name); //借用构造函数实现实例对父类属性和方法的继承
        this.sex = sex;
    }
    //通过原型链继承父类的原型上定义的方法
    Man.prototype = new Person();
    console.log(Man.prototype.constructor === Person); //子类的constructor被篡改
    //修正constructor
    Man.prototype.constructor = Man;
    
    let man1 = new Man(10, 'Tom', 'male');
    console.log(man1); //{age:10,name:'Tom',arr:[1,2],sex:'male'}
    man1.say(); //10,Tom [1,2]
    

    可以看到,组合继承是原型链继承和构造函数继承的优化组合在一起的继承方案,
    也是推荐使用的继承方案,其缺点在于,调用了两次父类构造函数,如果一定要挑
    缺点的话,那就是会有效率问题

    4.原型式继承

    //原型继承
    
    function createObj(obj) {
        function new_Obj() {}
        new_Obj.prototype = obj;
        return new new_Obj();
    }
    
    let one = {
        name: 'tom',
        hobby: [ 'sing', 'reading' ],
        say: function() {
            return 'hello';
        }
    };
    
    let two = createObj(one);
    console.log(two); // {}
    console.log(two.name, two.hobby, two.say()); //tom [ 'sing', 'reading' ] hello
    two.hobby.push('running'); //修改引用类型属性
    let three = createObj(one);
    console.log(three.name, three.hobby, three.say()); //tom [ 'sing', 'reading','running' ] hello
    

    综上可知,原型继承是通过在一个函数内创建一个构造函数,然后利用传入的对象修改构造函数的原型属性,最后返回
    基于这个构造函数创建的实例,然后我们在调用这个函数的时候,就得到一个实例,本质是对传入对象的浅拷贝
    缺点: 无法传参,存在多个实例对引用类型属性修改的篡改问题 不推荐

    5.寄生继承

    //寄生寄生
    function createObj(obj) {
        let newObj = Object.create(obj);
        newObj.say = function() {
            return 'hello';
        };
        return newObj;
    }
    
    let one = {
        name: 'tom',
        age: 20,
        hobby: [ 'singing', 'swimming' ],
        sing: function() {
            return 'world';
        }
    };
    let two = createObj(one);
    let three = createObj(one);
    console.log(two); //{say:function}
    two.hobby.push('reading');
    console.log(two.name, two.hobby, two.say()); //tom [ 'sing', 'swimming','reading' ] hello
    console.log(three.name, three.hobby, three.say()); //tom [ 'sing', 'swimming','reading' ] hello
    

    本质上寄生继承就是对原型继承的第二次封装,
    并且在这第二次封装过程中对继承的对象进行了拓展,这项新创建的对象不仅
    仅有父类中的属性和方法而且还有新添加的属性和方法
    同样的,存在多个实例对引用类型属性修改的篡改问题,
    也是不推荐的继承方案

    6.寄生组合继承

    /*寄生组合继承形态一    很大程度上可以看作是原型继承的进一步优化*/
    //父类 构造函数
    function One(age) {
        this.age = age || 20;
        this.nameArr = [ 'tom', 'lucky' ];
    }
    One.prototype.say = function() {
        console.log('hello');
    };
    //子类构造函数
    function Two(age) {
        One.call(this, age);
        this.sex = 'male';
    }
    const two1 = new Two(20);
    two1.nameArr.push('hehe');
    console.log(two1); //{age:20,nameArr:[ 'tom', 'lucky','hehe' ],sex:'male'}
    //console.log(two1.say()); //报错 two1.say is not a function
    /*
        可知 虽然在子类函数中借用关键字 call 调用了父类构造函数 继承了父类的一些属性
        但是我们可以看到,当子类创建的实例去访问定义在父类原型上的方法时是会报错的,访问不到
        在前面提到的组合继承中,通过修改和修正子类的prototype 可以有效解决这个问题,
        在这里,我们通过组合原型继承和寄生继承同样也可以实现 过程如下
    
    */
    function createObj(obj) {
        function makeNewObj() {}
        makeNewObj.prototype = obj;
        return new makeNewObj();
    }
    Two.prototype = createObj(One.prototype);
    Two.prototype.constructor = Two;
    const three = new Two(23); //新创建一个实例 three 访问say属性
    console.log(three); //{age:23,nameArr:[ 'tom', 'lucky' ],sex:'male'}
    console.log(three.say()); //hello
    

    以下是对以上寄生组合继承方案进行进一步封装

    /*寄生继承的形态二  可以看作是对形态一的进一步封装*/
    
    function Father(age) {
        this.age = age || 40;
        this.nameArr = [ 100, 200, 300 ];
    }
    Father.prototype.say = function() {
        return 'hello world';
    };
    function Child(age) {
        Father.call(this, age);
    }
    
    function Create_Child(o) {
        function makeChild() {}
        makeChild.prototype = o;
        return new makeChild();
    }
    function inheritFn(child, father) {
        child.prototype = Create_Child(father.prototype);
        child.prototype.constructor = child;
    }
    inheritFn(Child, Father);
    const child1 = new Child(20);
    console.log(child1.say()); //hello world
    

    进一步可以简化为

    /*寄生组合继承的最终形态*/
    function Person(age) {
        this.age = age || age;
        this.arr = [ 1, 2, 3 ];
    }
    Person.prototype.say = function() {
        return 'human';
    };
    function Male(age) {
        Person.call(this, age);
    }
    Male.prototype = Object.create(Person.prototype);
    Male.prototype.constructor = Male;
    const male1 = new Male(30);
    console.log(male1); //{age:30,arr:[1,2,3]}
    console.log(male1.say()); // human
    

    寄生组合继承与组合继承相比,它的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了
    在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能
    保持不变;寄生组合式继承是最理想的继承范式。

    7.最优的继承方案 es6的extends

    class Person {
        constructor(name) {
            this.name = name;
            this.hobby = [ 1, 2 ];
        }
        sayHi() {
            console.log('hello');
        }
        play() {
            console.log('basketball');
        }
    }
    class Man extends Person {
        constructor(name, age) {
            super(name);
            this.age = age;
        }
        sayHi() {
            console.log('I am from Man');
        }
    }
    let man1 = new Man('tom', 20);
    console.log(man1);
    man1.sayHi(); //I am from Man
    man1.play(); //basketball
    

    优点:简洁明了,不用手动设置原型。
    缺点:新语法,部分浏览器支持,需要转为 ES5 代码。

    相关文章

      网友评论

        本文标题:构造函数原型的继承方式分析

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