昨天和一个以前在培训机构当老师的同事聊天,聊到js的继承,才发现自己虽然一直会用,但是给别人讲的时候却不能很清晰的讲明白。正好周末凑个时间,来试着写一下js的继承,以便自己将来翻看。
1、原型链继承
function Father(name,age){
this.name = name || 'father'
this.age = age || '未知'
this.likes = ['reading','music']
}
Father.prototype.say = function(){
return `my name is ${this.name}`
}
function Child(name){
this.name = name || 'child'
this.sex = 'boy'
}
Child.prototype = new Father()
以上代码实现了两个类型Father和Child,通过创建Father的实例,并将该实例赋值给Child.prototype来实现了继承。原本存在于Father的实例的所有属性和方法,现在都存在于了Child.prototype上。试着来调用一下。
var c1 = new Child()
console.log(c1.name) //child
此时我们可以证明父类的属性被子类所覆盖,严格意义上将,并不是被覆盖,而是js的在查找属性时,现在自身查找,如果找不到就去__proto__上面去找,如果没有就一直向上,知道找到或者最终为null为止。如下图:
可以很容易理解下面代码的输出结果: console.log(c1.age) //未知
console.log(c1.say()) //my name is child
console.log(c1.sex) //boy
此外如果需要对Child添加方法,一定要在替换原型的语句之后。同时添加代码如下,可以发现原型链继承有一个很明显的缺点:
var c2 = new Child('lili')
c1.likes.push('run')
console.log(c1.likes) //["reading", "music", "run"]
console.log(c2.likes) //["reading", "music", "run"]
即引用数据类型,会被所有实例所共享,对其中一个实例改变时会引起其他实例的改变。
2、借用构造函数实现继承
function Father(name){
this.name = name || 'father'
this.likes = ['reading','music']
this.say = function(){
return `my name is ${this.name}`
}
}
function Child(name){
Father.call(this,name)
this.sex = 'boy'
}
Father.prototype.show = function(){
return `i likes ${this.likes.toString()}`
}
var c1 = new Child('huaihuai')
var c2 = new Child()
c1.likes.push('running')
console.log(c1.likes) //["reading", "music", "running"]
console.log(c2.likes) //["reading", "music"]
console.log(c1.say === c2.say) //fasle
console.log(c1)
显然已经解决了引用类型属性被说有实例共享的问题。但是 函数也是引用数据类型,也没有办法共享了。也就是说所有实例中的函数,虽然功能一样,但是都不是同一个函数,相当于每实例化一个实例,就复制了一边代码。并且如下图:
对于父类原型上的方法show也是没有办法继承的!3、组合继承
function Father(name){
this.name = name
this.likes = ['reading','music']
}
Father.prototype.say = function(){
return `my name is Father`
}
function Child(name){
Father.call(this,name) //构造函数继承 第一次调用父类的构造函数
}
Child.prototype = new Father() //第二次调用父类构造函数
Child.prototype.constuctor = Child
var c1 = new Child('huaihuai')
var c2 = new Child('shuaishuai')
c1.likes.push('running')
console.log(c1.likes) //["reading", "music", "running"]
console.log(c2.likes) //["reading", "music"]
console.log(c1.say === c2.say) //true
console.log(c1)
此时即可以让不同的实例分别有自己的属性(即解决引用数据类型被所有实例共用的问题),又能使用同一个函数。避免了原型链继承和构造函数继承的缺陷,同时融合了他们的优点。是js中较为常用的继承方式。
4、原型式继承
function object(proto){
function F(){}
F.prototype = proto
return new F()
}
var person = {
name:'shuaishuai',
likes:['read','music']
}
var anotherPerson = object(person)
var yetAnotherPerson = object(person)
anotherPerson.likes.push('running')
console.log(anotherPerson.likes) //["read", "music", "running"]
console.log(yetAnotherPerson.likes) //["read", "music", "running"]
console.log(person.likes) //["read", "music", "running"]
console.log(anotherPerson)
在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
这意味着person.likes不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本。
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。
var person = {
name:'shuaishuai',
likes:['read','music']
}
var anotherPerson = Object.create(person)
var yetAnotherPerson = Object.create(person)
anotherPerson.likes.push('running')
console.log(anotherPerson.likes) //["read", "music", "running"]
console.log(yetAnotherPerson.likes) //["read", "music", "running"]
console.log(person.likes) //["read", "music", "running"]
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相 同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属 性。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson)
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
5、寄生式继承
function object(proto) {
function F() {}
F.prototype = proto
return new F()
}
function createAnother(original) {
var clone = object(original)
clone.sayHi = function () {
alert('hi')
}
return clone
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
var yetAnotherPerson = createAnother(person)
yetAnotherPerson.friends.push('aaa')
console.log(anotherPerson.friends) //["Shelby", "Court", "Van", "aaa"]
console.log(yetAnotherPerson.friends) //["Shelby", "Court", "Van", "aaa"]
console.log(yetAnotherPerson.sayHi === anotherPerson.sayHi) //false
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示 范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一 点与构造函数模式类似。
6、寄生组合式继承
function object(proto) {
function F() {}
F.prototype = proto
return new F()
}
function inheritPrororype(Child, Father) {
var prototype = object(Father.prototype)
prototype.constructor = Child
Child.prototype = prototype
}
function Father(name) {
this.name = name || 'father'
this.likes = ['reading', 'music']
}
Father.prototype.say = function () {
return `my name is Father`
}
function Child(name) {
Father.call(this, name) //构造函数继承 第一次调用父类的构造函数
}
inheritPrororype(Child,Father)
var c1 = new Child('huaihuai')
var c2 = new Child('shuaishuai')
c1.likes.push('running')
console.log(c1.likes) //["reading", "music", "running"]
console.log(c2.likes) //["reading", "music"]
console.log(c1.say === c2.say) //true
寄生组合式继承,通过借用构造函数来继承属性,通过原型链的混入形式来继承方法。这种方法只调用了以此父类构造函数,并且因此避免了在子类的prototype上面创建不必要的多余的属性。因此是引用类型最理想的继承方法。
参考资料
《javaScript高级程序设计 第三版》
《javaScript权威指南 第六版》
个人觉得关于js的继承《javascript高级程序设计》比《javascript权威指南》更容易理解。