js基于原型链的继承
继承的5种方式和优缺点分析
<!DOCTYPE html> <html lang=""> <head> <meta charset="UTF-8"> <title>OOP</title> </head> <body> <script type="text/javascript"> /** * 方式一,缺点原型链上的方法无法使用 * @constructor */ var Parent1=function () { this.name="parent1" }; Parent1.prototype.say=function () { return "say"; }; var Child1=function () { Parent1.call(this); this.type="child1" }; var p1=new Parent1(),c1=new Child1() console.log("方式一",p1,c1); try{ console.log(c1.say()); }catch(e){ console.log("调用say方法出错"+e.toString()); } /** * 方式二,缺点子类的继承属性是引用 */ var Parent2=function () { this.name="parent2"; this.p=[1,2,3]; }; Parent2.prototype.say=function () { return "say"; }; var Child2=function () { this.type="child2" }; Child2.prototype=new Parent2(); var p2=new Parent2(),c2_1=new Child2(),c2_2=new Child2(); console.log("方式二",p2,c2_1); c2_1.p.push(4); console.log("方式二改变c2_1,c2_2也变了",c2_1.p,c2_2.p); console.log("方式二子类可以调用父类的方法",c2_1.say()); /** * 方式三,缺点构造函数执行了2次 */ var Parent3=function () { this.name="parent3"; this.p=[1,2,3]; }; Parent3.prototype.say=function () { return "say"; }; var Child3=function () { Parent3.call(this); this.type="child3" }; Child3.prototype=new Parent3(); var p3=new Parent3(),c3_1=new Child3(),c3_2=new Child3(); console.log("方式三",p3,c3_1); c3_1.p.push(4); console.log("方式三改变c3_1,c3_2没变",c3_1.p,c3_2.p); console.log("方式三子类可以调用父类的方法",c3_1.say()); /** * 方式四,缺点子类和父类的构造函数相同,不利于判断实例到底属于那个类 */ var Parent4=function () { this.name="parent4"; this.p=[1,2,3]; }; Parent4.prototype.say=function () { return "say"; }; var Child4=function () { Parent4.call(this); this.type="child4" }; Child4.prototype=Parent4.prototype; var p4=new Parent4(),c4_1=new Child4(),c4_2=new Child4(); console.log("方式四",p4,c4_1); c4_1.p.push(4); console.log("方式四改变c4_1,c4_2没变",c4_1.p,c4_2.p); console.log("方式四子类可以调用父类的方法",c4_1.say()); console.log("方式四子类和父类的构造函数相同",p4.constructor===c4_1.constructor); /** * 方式五,最好的通用写法避免以上的缺点 */ var Parent5=function () { this.name="parent5"; this.p=[1,2,3]; }; Parent5.prototype.say=function () { return "say"; }; var Child5=function () { Parent5.call(this); this.type="child5" }; Child5.prototype=Object.create(Parent5.prototype); Child5.prototype.constructor=Child5; var p5=new Parent5(),c5_1=new Child5(),c5_2=new Child5(); console.log("方式五",p5,c5_1); c5_1.p.push(4); console.log("方式五改变c5_1,c5_2没变",c5_1.p,c5_2.p); console.log("方式五子类可以调用父类的方法",c5_1.say()); console.log("方式五子类和父类的构造函数不相同",p5.constructor===c5_1.constructor); </script> </body> </html>

分析:
方式一:方式一我们在子类的构造函数内通过调用父类的构造函数来实现继承。我们使用了一个call方法来改变父类构造函数的this指向,使他指向子类构造函数的this。
这种方式虽然实现了部分继承,父类的name属性继承了下来,但父类原型对象上的方法并没有继承下来子类实例c1调用say方法时出错。
方式二:方式一我们无法将父类原型对象上的方法继承下来,那我们来想想怎样才能把这些方法继承下来呢?我们可以new一个父类的实例把他赋值给子类的原型对象,这样子类就能获得父类原型对象上的方法。
这种方式实现了继承,子类可以调用父类的方法(c2_1.say()执行了),但当我们执行c2_1.p.push(4)后c2_2.p也发送了改变。这是因为我们是直接把一个父类的实例赋值给子类的原型对象,这样每当我们
new一个子类的时候,他们的原型对象都是指向同一个对象(也就是是引用),所以改变c2_1.p的话c2_2.p也跟着改变。
方式三:方式一和方式二各有缺点,方式一无法继承父类的原型对象上的方法,但子类的属性是构造函数运行时赋予的,也就是说是不同的实例,他们的属性引用是不同的,不会出现方式二的情况。方式二可以使用父类原型对象上的方法,但是属性也是赋值在了原型对象内,这就导致所有子类的实例的继承来的属性的引用相同。我们发现这2种方式的优缺点刚好互补,所以结合这2种方式就能相互弥补缺点,这就是方式三。
上述代码中c3_1可以调用say方法,改变c3_1.p,c3_2.p没有改变。但是注意我们查看p3和c3_1会发现c3_1有2个name属性,一个是在外层的name,一个是在__proto__中的name如图

我们通过属性查找c3_1.name找到的是外层的name,如下图我们进行验证,当我们改变c3_1.name时c3_1.__proto__.name没变,这是肯定的因为他们的指向不同。注意属性查找时会首先查找这个对象本身有没有这个属性,如果没有才去原型链上进行查找。

方式四:方式三虽然实现了继承,也挺好的,但我们发现方式三内有2个name属性,这是因为我们调用了2次构造函数,一次是在Parent3.call(this),另一次是在Child3.prototype=new Parent3(),这样显然是重复的,是有问题的,那我们有什么方法可以优化呢?方法四就是为了优化这个问题的,在方式四中我们直接把Parent4.prototype赋值给Child4.prototype,这样就避免使用new运算时又执行一次构造函数。
方式四看起来很完美了,但熟悉原型链的人会发现方式四也有问题。原型链中子类的实例的__proto__属性的__proto__属性指向父类的prototype 也就是c4_1.__proto__.__proto__应该严格等于Paren4.prototype但结果却并不相等,而且因为他们的原型对象相同导致他们实例的constructor属性也相同,这是不符合继承规则的,不利于我们判断实例到底属于哪个类。如下图

哪怎么决解这个问题呢?这就是方式五
方式五:关键代码Child5.prototype=Object.create(Parent5.prototype); Child5.prototype.constructor=Child5; 我们先利用Object.create方法把Parent5.prototype赋值给Child.prototype.__proto__(赋值引用),把Parent5.prototype.constructor赋值给Child.prototype.constructor(这里应该是值的拷贝,不是赋值引用)然后将子类原型对象的构造函数进行覆盖(使他指向子类的构造函数)。注意不能Child5.prototype=Parent5.prototype; Child5.prototype.constructor=Child5; 这样因为这样父类的原型对象的构造函数也指向了子类的构造函数,因为第一句话是引用赋值。

浙公网安备 33010602011771号