继承
ES5 继承
// 定义一个父类
function Parent(name) {
// 属性
this.name = name || 'parent'
this.friends = ['xiaoli', 'red', 'blue']
// 实例方法
this.say = function() {
console.log(this.name + ',hello')
}
}
// 原型方法
Parent.prototype.eat = function(food) {
console.log(this.name + ' eat ' + food)
}
1、原型链继承
核心:将父类的实例作为子类的原型,这样子类就可以继承父类原型上的所有属性和方法
// 定义一个子类
function Child(name, age) {
this.age = age
this.sayAge = function(){
console.log(this.name + '年龄' + this.age)
}
}
// 核心代码:将父类的实例作为子类的原型,这样子类就可以继承父类原型上的所有属性和方法
Child.prototype = new Parent()
var lxt = new Child('lxt', 25)
var tt = new Child('tt', 18)
console.log(lxt.name) // parent 问题1:不能传参
console.log(lxt.say()) // parent,hello
console.log(lxt.eat('food')) // parent eat food
lxt.friends.push('green')
console.log(lxt.friends) // ['xiaoli', 'red', 'blue', 'green']
console.log(tt.friends) // ['xiaoli', 'red', 'blue', 'green'] 问题2:引用类型属性被实例共享
// 问题3: 无法实现多继承
缺点
- 不能传参
- 引用类型属性被实例共享
- 无法实现多继承
2、借用构造函数继承(有时候也叫做伪造对象或经典继承)
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Child(name, age) {
// 核心代码
Parent.call(this, name)
}
var lxt = new Child('lxt', 25)
var tt = new Child('tt', 18)
console.log(lxt.name) // lxt 解决题1问题1:不能传参(现在可以传参)
console.log(lxt.say()) // lxt,hello
console.log(lxt.eat('food')) // 报错 问题1:只能继承父类的实例属性和方法,不能继承原型属性和方法
lxt.friends.push('green')
console.log(lxt.friends) // ['xiaoli', 'red', 'blue', 'green']
console.log(tt.friends) // ['xiaoli', 'red', 'blue'] 解决题1问题2:引用类型属性被实例共享(不存在实例共享)
lxt instanceof Parent // false
lxt instanceof Child // true
特点:
- 可以传参
- 可以多继承(call多个父类对象)
- 不存在引用类型属性被实例共享问题
缺点
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 实例不是父类的实例,只是子类的实例
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能(lxt.say和tt.say是两个方法)
3、组合继承(也叫伪经典继承)
核心:将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Child(name, age) {
Parent.call(this, name)//第二次调用 Parent()
this.age = age
}
Child.prototype = new Parent() //第一次调用 Parent()
Child.prototype.constructor = Child
var lxt = new Child('lxt', 25)
var tt = new Child('tt', 18)
console.log(lxt.name) // lxt
console.log(lxt.say()) // lxt,hello
console.log(lxt.eat('food')) // lxt eat food // 解决题2问题1
lxt.friends.push('green')
console.log(lxt.friends) // ['xiaoli', 'red', 'blue', 'green']
console.log(tt.friends) // ['xiaoli', 'red', 'blue']
lxt instanceof Parent // true
lxt instanceof Child // true
特点:
- 可以传参
- 可以继承父类实例属性和方法,也可以继承父类原型属性和方法
- 既是父类的实例也是子类的实例
- 函数可以复用(用parent.prototype上的方法)
- 不存在引用类型被实例共享
缺点
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
4、原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o) {
function F(){}
F.prototype = o
return new F()
}
// 本质上是object()对传入其中的对象执行了一次浅复制
- 这种原型式继承,要求必须要有一个对象可以作为另一个对象的基础
- 用这种方式创建的对象相当于是传入参数对象的副本
- 在只想让一个对象与另一个对象保持类似的情况下,原型继承是完全可以胜任的。不过别忘了,原型模式下的缺点:引用类型属性的共享问题。
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
这个方法接受两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
IE8及以下不支持Object.create()方法
5、寄生式继承
寄生式继承与原型式继承紧密相关,与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
function createAnother(original) {
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强这个对象
alert("hi");
};
return clone; // 返回这个对象
}
- 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
6、寄生组合继承
因为使用组合继承,调用了两次Parent构造函数,现在出现了两组属性,一组在实例上,一组在Child原型中。
解决这个问题的办法就是使用寄生组合式继承
- 借用构造函数来继承属性,通过原型链的混成形式来继承方法
- 不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype; //指定对象
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
inheritPrototype(Parent, Child)
7、用 Object.create实现类式继承
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
如果你希望能继承到多个对象,则可以使用混入的方式。
function Child() {
Parent.call(this);
Father.call(this);
}
// 继承一个类
Child.prototype = Object.create(Parent.prototype);
// 混合其它
Object.assign(Child.prototype, Father.prototype);
// 重新指定constructor
Child.prototype.constructor = Child;
Child.prototype.myMethod = function() {
// do a thing
};
总结
ECMAScript 支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。在没有类的情况下,可以采用下列模式创建对象。
-
工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
-
构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用 new 操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
-
原型模式,使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。
-
组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。
JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。
-
原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。
-
解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能往超类型构造函数中传递参数,但是没有函数复用。
-
使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
-
此外,还存在下列可供选择的继承模式。
-
原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
-
寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
-
寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
参考链接: JavaScript高级程序设计中的继承