JavaScript--浅谈继承
ECMAScript实现继承的方式:
1.原型链
利用原型让一个引用类型继承另一个引用类型的属性和方法。
function Father(){ this.name = 'father' } function Son(){ } //继承Father,将Father的实例赋给Son的原型 Son.prototype = new Father() var son1 = new Son() son1.name // father 继承了Father的name属性
之前在《浅谈创建对象》中,我们提出 原型模式创建对象 是可以修改原型的,如下
1 function Father(){ 2 this.name = 'father' 3 } 4 5 function Son(){ 6 } 7 8 //给Son添加原型属性 9 Son.prototype.age = '1' 10 11 //重写Son原型对象,继承Father 12 Son.prototype = new Father() 13 14 //创建Son实例,可以放在修改原型之前,修改原型能够从实例上立即反映出来 15 var son1 = new Son() 16 17 Son.prototype.job = '程序员' 18 19 son1.age // error 20 //在重写Son原型对象之前 给Son原型添加属性是没用的,重写会切断与之前原型对象的联系 21 22 son1.job // 程序员 23 24 Son.prototype = { 25 height: 1.70 26 } 27 28 son1.height // error 上述方法也是属于重写原型对象
类似于原型模式,单独使用原型链实现继承的问题:
a.原型链中的引用类型的值会被所有实例共享,并可以修改,而修改后会反映到所有的实例上,这样实例就不能拥有私有的引用类型的属性。
b.在创建子类型实例时,我们无法在不影响其他实例的同时向父类型的构造函数中传递参数。
2.借用构造函数
function Father(name){ this.name = name; this.colors = ['red','green','blue'] } function Son(name){ //在子类型构造函数中调用父类型的构造函数,通过call函数等强制绑定作用域 Father.call(this,name) } var son1 = new Son('张三') son1.name //张三 son1.colors.push('yellow') var son2 = new Son('李四') son2.name //李四 son1.colors // ['red','green','blue','yellow'] son2.colors // ['red','green','blue']
相对原型链而言,借用构造函数可以向父类型的构造函数传参。
类似于构造函数模式,单独使用借用构造函数模式实现继承的问题:
a.方法都在构造函数中定义,每次创建对象就会重新实例化一次方法属性
b.父类型的原型属性,子类型是访问不到的。
3.组合继承
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
1 function Father(name){ 2 this.name = name; 3 this.colors = ['red','green','blue'] 4 } 5 Father.prototype.sayName = function (){ 6 alert(this.name) 7 } 8 9 function Son(name,age){ 10 Father.call(this,name); 11 this.age = age; 12 } 13 14 //重写Son的原型实现继承,这种重写会导致Son的实例的constructor属性都变成父类型的构造函数Father,Son.constructor //Father 而不是Son 15 Son.prototype = new Father() 16 //对于某些对constructor属性比较在意的场景,可以手动的将Son的constructor属性设置成Son,Son.constructor //Son 17 Son.prototype.constructor = Son; 18 Son.prototype.sayAge = function(){ 19 alert(this.age) 20 } 21 22 var son1 = new Son('张三',20) 23 son1.colors.push('yellow') 24 son1.sayName(); //张三 25 son1.sayAge(); //20 26 27 var son2 = new Son('李四',21) 28 son2.sayName(); //李四 29 son2.sayAge(); //21 30 31 son1.colors // ['red','green','blue','yellow'] 32 son2.colors // ['red','green','blue']
无论什么情况下,都会调用2次父类型的构造函数,
第一次在创建子类型原型的时候,Son.prototype = new Father(),
第二次在子类型构造函数内部,Fahter.call(this,name)
4.原型式继承
1 var Father = { 2 name:'father', 3 friends:['张三','李四'] 4 } 5 //只传入一个参数的情况下,这2种写法的行为一样的 6 var son1 = Object(Father) 7 son1.name = '张三' 8 var son2 = Object.create(Father) 9 son2.friends.push('王五') 10 11 son1.name // 张三 12 son2.name // 张三 ,使用object.create 类似于原型链的继承方式,超类原型的所有属性都是共享的 13 son1.friends // ['张三','李四'] 14 son2.friends // ['张三','李四','王五'] 15 16 var son3 = Object.create(Father,{ 17 name:{ 18 value:'王五' 19 } 20 }) 21 son3.name // 王五 22 son3.friends // ['张三','李四','王五']
在不想创建构造函数,只是想一个对象与另一个对象保持类似的情况下,原型式继承是可以胜任的,不过他们的引用类型的属性还是所有实例共享的。
5.寄生式继承
function createSon(Father){ var Son = Object(Father); //在这里可以添加属于子类型的属性 Son.sayHi = function(){ alert('hi') } return Son }
和原型式继承一样,在不考虑自定义类型和构造函数的情况下,只想返回一个类似的新对象时,寄生式继承可以做到,不过和构造函数模式类似,方法都在函数中定义,每次创建对象就会重新实例化一次方法属性,所有的实例的sayHi方法都是一个新的实例
6.寄生组合式继承
为了解决组合继承调用2次父类型构造函数的问题
首先我们要搞清楚为什么组合继承会调用2次父类型的构造函数,组合继承的2次调用分别是 子构造函数中的 借用构造函数 和 原型链继承的实现。
其实多余的一次调用就是 原型链继承的实现中,我们必须要拿到一个父类型的实例用来作为子类型的原型,这样修改子类型的原型只会修改父类型的实例原型,而不会影响父类型的其他实例。
这就是为什么 在原型链继承中,我们用 Son.prototype = new Father() //获取父类型的实例重写子类型的原型 而不是 Son.prototype = Father.prototype // 这样写的话,我们在Son.prototype上新添加一个属性,会影响到Father的其他实例
怎么才能只获得一个与父类型prototype相似的对象而不调用父类型的构造函数呢?
在不考虑自定义类型和构造函数的情况下,只想返回一个类似的新对象时,寄生式继承可以做到
fucntion expendPrototype(Son,Father){ //创建父类型的原型副本 var prototype = Obejct(Father.prototype); //弥补重写原型而失去的默认的constructor属性 prototype.constructor = Son; //重写子类型的原型 Son.prototype = prototype; }
这样 我们用这个方法去代替之前的 Son.prototype = new Father(),从而解决了多一次无用的构造函数调用.