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

1.通过原型链继承

function ExpOne(age) {
    this.age = age || 20;
    this.nameArr = [ 'Tom', 'Cat' ];
}
ExpOne.prototype.say = function() {
    console.log(this.age);
    console.log(this.nameArr);
};
let expOne = new ExpOne(18);
expOne.say(); //18, [ 'Tom', 'Cat' ]

function ExpOne_exp() {}
ExpOne_exp.prototype = new ExpOne();
ExpOne_exp.prototype.eat = function() {
    console.log('eat');
};

//创建实例 ExpOne_expOne
let ExpOne_expOne = new ExpOne_exp(30); //传参无用
ExpOne_expOne.say(); //20, [ 'Tom', 'Cat' ]
ExpOne_expOne.eat(); //eat

ExpOne_expOne.age = 100;
//通过实例ExpOne_expOne对原型中引用类型值的进行修改
ExpOne_expOne.nameArr.push('Jolin');
ExpOne_expOne.say(); //100, [ 'Tom', 'Cat','jolin' ]

//创建实例ExpOne_expTwo
let ExpOne_expTwo = new ExpOne_exp();
ExpOne_expTwo.say(); // 20, [ 'Tom', 'Cat','jolin' ] //可见实例ExpOne_expTwo的引用类型的值受到了影响

expOne.nameArr.push('Jolin2');
expOne.say(); //18, [ 'Tom', 'Cat','Jolin2' ]

ExpOne_expTwo.say(); // 20, [ 'Tom', 'Cat','jolin' ]
ExpOne_exp.prototype.say = function() {
    console.log('我要覆盖ExpOne的属性say');
};
ExpOne_expTwo.say(); //我要覆盖ExpOne的属性say
ExpOne_expOne.say(); //我要覆盖ExpOne的属性say
console.log(ExpOne_exp.prototype.constructor === ExpOne); //true 需要做如下纠正 ExpOne_exp.prototype.constructor = ExpOne_exp

通过原型链来实现继承的原理以及缺点:
原型链继承方案中,原型实际上会变成另一个类型的实例,ExpOne_exp.prototype 变成了 ExpOne 的一个实例,所以 ExpOne 的实例属性 nameArr 就变成了 ExpOne_exp.prototype 的属性,
而原型属性上的引用类型值会被ExpOne_exp所有实例共享,所以多个实例对引用类型的操作会被篡改.
缺点:
1.创建子类型实例时无法向父类型的构造函数传参。
2.子类型多个实例对引用类型的操作有被篡改的风险。
3.子类型的原型上的 constructor 属性被重写了。需要纠正
基于原型链继承的问题,原型链继承通常是不被推荐使用的继承方式。

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

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

//创建动物的构造函数
function Anmail(age) {
    this.age = age || 2;
    this.nameArr = [ 'tom', 'cat' ];
}
//添加方法
Anmail.prototype.run = function() {
    console.log('i am running');
};

let AnmailOne = new Anmail();
AnmailOne.run();

//创建子类 Cat 借用构造函数来实现的继承
function Cat(age) {
    Anmail.call(this, age);
}
let catOne = new Cat(3);
catOne.nameArr.push('linlin');
console.log(catOne); //{ age: 3, nameArr: [ 'tom', 'cat','linlin' ] }

let catTwo = new Cat(4);
console.log(catTwo); //{ age: 4, nameArr: [ 'tom', 'cat' ] }
//catOne.run(); // 报错 TypeError: catOne.run is not a function

Cat.prototype.run = function() {
    console.log('我需要在Cat的原型上再定义一次才可以在实例上访问到');
};
catTwo.run(); //需要在Cat的原型上再定义一次才可以在实例上访问到

经典继承的原理和局限性
将父类的函数在子类的构造函数中调用,在每个实例中执行,这样每个实例中都会有一份Anmail中的属性方法的副本,也就实现了继承Anmail。
这种方式的优势就是可以在子类型构造函数中向父类型构造函数传递参数,同时解决了原型链继承多实例共享构造函数引用类型数据的篡改问题。
存在的问题是:父类型的原型中定义的方法对于子类型以及子类创建的实例都是不可见的,因此只能将方法都在构造函数中定义,方法都在构造函数中定义,每次创建实例都会创建一遍方法。
这种经典的继承方案通常也是不被推荐使用的继承方式。

3.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;
}
//通过prototype继承父类的方法
Man.prototype = new Person();
//为Man重新定义constructor
console.log(Man.prototype.constructor === Person);
Man.prototype.constructor = Man;

let man1 = new Man(10, 'Tom', 'male');
console.log(man1);
man1.say();

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

4.原型式继承

function create(o) {
    function F() {}
    F.prototype = o;
    //console.log(new F());
    return new F();
}

let obj = {
    arr: [ 1, 2 ],
    say: function() {
        console.log(this.arr);
    }
};
let F1 = create(obj);
console.log(F1); //{}
console.log(F1.arr); //[1,2]
F1.arr.push(3);
console.log(F1.arr); //[1,2,3]
let F2 = create(obj);
console.log(F2.arr); //[1,2,3]

原型式继承的原理:创建一个构造函数,构造函数的原型指向对象,然后调用 new 操作符创建实例,并返回这个实例,本质是对传入对象的一个浅拷贝
缺点:原型的引用类型属性会在各实例之间共享。

5.寄生继承

function Person(obj) {
    let newObj = Object.create(obj);
    newObj.sayHi = function() {
        console.log('Hi');
    };
    return newObj;
}
let objP1 = {
    name: 'tom',
    age: 20,
    hobby: [ 'singing', 'swimming' ],
    say: function() {
        console.log('hello');
    }
};
let newObjP1 = Person(objP1);
newObjP1.hobby.push('running');

console.log(newObjP1);
console.log(newObjP1.hobby); //[ 'singing', 'swimming', 'running' ]
console.log(newObjP1.name, newObjP1.age);
newObjP1.sayHi();

let newObjP2 = Person(objP1); //[ 'singing', 'swimming', 'running' ]
console.log(newObjP2.hobby);

本质上寄生继承就是对原型继承的第二次封装,
并且在这第二次封装过程中对继承的对象进行了拓展,这项新创建的对象不仅
仅有父类中的属性和方法而且还有新添加的属性和方法
同样的,寄生继承中引用类型属性会在各实例之间共享,
寄生继承也是一种不推荐的继承方案

6.寄生组合继承

function Create(obj) {
    function P() {}
    P.prototype = obj;
    return new P();
}
function Person(name) {
    this.name = name;
    this.hobby = [ 'reading', 'fishing' ];
}
Person.prototype.say = function() {
    console.log('hi');
};
function P(name, age) {
    Person.call(this, name);
    this.age = age;
}
P.prototype = Create(Person.prototype);
P.prototype.constructor = P; //修复构造函数指向
let p1 = new P('tom', 19);
p1.hobby.push('traveling');
console.log(p1); //{ name: 'tom', hobby: [ 'reading', 'fishing','traveling' ], age: 19 }
p1.say(); //hi
let p2 = new P('cat', 10);
console.log(p2); //{ name: 'tom', hobby: [ 'reading', 'fishing' ], age: 10 }

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

function Create(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}
function inheritPrototype(child, parent) {
    let prototype_inherit = Create(parent.prototype);
    prototype_inherit.constructor = child;
    child.prototype = prototype_inherit;
}
//封装inheritPrototype的目的:1.子类继承父类 2.重新修正子类constructor的指向
function Parent(name) {
    this.name = name;
    this.hobby = [ 'reading', 'fishing' ];
}
Parent.prototype.say = function() {
    console.log('hello');
};
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
inheritPrototype(Child, Parent);
let p1 = new Child('tom', 10);
console.log(p1); //{ name: 'tom', hobby: [ 'reading', 'fishing' ], age: 10 }
console.log(Child.prototype.constructor); //Child

进一步可以简化为

function Parent(name) {
    this.name = name;
    this.hobby = [ 'reading', 'fishing' ];
}
Parent.prototype.say = function() {
    console.log('hello');
};
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let p1 = new Child('tom', 10);
p1.hobby.push('he');
console.log(p1); //{ name: 'tom', hobby: [ 'reading', 'fishing' ], age: 10 }
let p2 = new Child('cat', 20);
console.log(p2); //{ name: 'cat', hobby: [ 'reading', 'fishing' ], age: 20 }
console.log(Child.prototype.constructor); //Child

寄生组合继承与组合继承相比,它的高效率体现它只调用了一次 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 代码。

你可能感兴趣的:(构造函数原型的继承方式分析)