众所周知,继承是面向对象编程思想中的三大特点(封装,继承,多态)之一。
所谓继承,通俗来讲就是子类自动拥有父类的属性和方法, 继承可以提高代码的复用性。
继承也是前端里面的重要的一个知识点,在实际工作和面试中也会经常的遇到,在
《JavaScript高级程序设计》一书中介绍了原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承以及寄生组合式继承这6种继承继承方式。然而在ES6中也是新增了class类继承。
一:原型链继承
原型链继承基本思想是让原型属性(每一个构造函数都有一个原型对象prototype)等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述的关系依然成立,以此类推,也就形成了一个原型链。
// 1、父类
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
// 2、子类1
function Doctor(dept) {
this.dept = dept;
}
// 建立继承关系(原型的方式):子类(构造函数)的原型属性 指向 父类的对象(实例)
//它相当于完全删除了 Doctor 的 prototype 对象原先的值,然后赋予了一个新的值。
Doctor.prototype = new Person("娜可露露", "女");
Doctor.prototype.seat= function() {
console.log(this.name + "在野区");
}
let d1 = new Doctor("神经科");
console.log(Doctor.prototype.constructor == Person); //true
以上代码定义了两个类型:Person和Doctor。我们通过创建Parent的实例,并将该实例赋给Child.prototype实现了Doctor继承Person。本质是重写了Doctor的原型对象,代之以一个新类型的实例。也可以说,原来存在于Person的实例中的所有属性和方法,现在也会存在于Child.prototype中了。在确立了继承关系之后,我们给Doctor.prototype添加了一个方法,这样就在继承了Parent的属性和方法的基础上又添加了一个新方法。
任何一个prototype对象都有一个constructor属性指向它的构造函数。如果没有Doctor.prototype = new Person()这一行,Doctor.prototype.constructor 是指向 Doctor的,加了这一行以后,Child.prototype.constructor指向Person。
但是在这里我们要注意两点:
1. 先定义原型继承关系,再添加子类的特有方法或属性(原型的属性,即共享的属性和方法要在原型继承关系确立后,再定义)。
2. 利用原型链继承,给子类添加特有的原型方法时,不可以重写prototype(不要再给prototype属性赋JSON类型的值就行)
具体看代码:
// 原型(链)继承的注意点:
// 一、先完成继承关系,再增加子类特有的属性和方法。
// 二、在写子类特有属性和方法时,不能重新prototype(不能再直接给prototype赋值为json对象)
// 1、父类
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.eat = function(str) {
console.log(this.name + "在吃" + str + ",天在看…………");
}
// 2、子类1
function Doctor(dept) {
this.dept = dept;
}
// 先定义原型继承关系,再添加子类的特有方法或属性
Doctor.prototype = new Person("不知火舞", "女");
Doctor.prototype.seat = function() {
console.log(this.name + "在中路...");
}
// 不能重新prototype属性,这样等于重新赋值为新的
// Doctor.prototype = {
// seat:function(){
// console.log(this.name+"在中路……");
// }
// }
原型链继承的缺点:
1. 创建子类型的实例时,没法传递参数给被继承类型。或者说,是没办法在不影响所有对象实例的情况下,给父类型的构造函数传递参数。
2. 被继承的类型(父类)里包括引用类型的属性的时候,它会被所有实例共享其值。(也就是说每个实例对引用类型属性的修改都会影响其他的实例)。
二:借用构造函数(经典继承)
这种方式,其实就是在子类型构造函数中借用父类的构造函数来生成自己的实例对象的属性。它可以向父类型的构造函数传递参数来初始化自己的实例属性。使用apply()和call()方法也可以在新创建的对象上执行构造函数。
//定义一个构造函数Person();
function Person(name1,sex1,age1){
this.name=name1;
this.sex=sex1;
this.age=age1;
this.eat = function(){
}
}
//定义一个构造函数Teacher()
function Teacher(name1,sex1,age1,course1){
Person.call(this,name1,sex1,age1);//第一个参数代表原函数的this指向,后面的参数为原函数的参数
this.course=course1;
}
var t = new Teacher("扁鹊","男",25,"语文");
alert(t.name+","+t.sex+","+t.course);
上面代码中Person.call()这一行代码“借调”了父类型的构造函数。通过使用call()或apply()方法改变原构造函数的this指向,我们实际是在新建的构造函数Teacher实例的环境下调用了Person构造函数。
借用构造函数继承的优点:
1、借用构造函数可以在子类型构造函数中向父类型构造函数传递参数。
2、解决了每个实例对引用类型属性的修改都会被其他的实例共享的问题。
借用构造函数继承的缺点:
1、在父类型的原型上定义的方法,对于子类型是不能继承的,结果所有类型都只能使用构造函数模式。
2、单独使用这种借用构造函数的模式,方法都在构造函数中定义,无法实现函数复用。每个实例都会拷贝一份,所以每次创建一个实例也就会重新生成一个方法,方法也是一个对象,相当于实例化了一个对象,那么这样会浪费内存,所以,很少很少单独使用。
三、组合继承
结合前两种方式:原型链式继承和借用构造函数方式继承,我们就能解决前面提出的那些问题。
1)、利用原型链继承(父类prototype上的)共有的属性和方法,
2)、利用Call/Apply来继承父类构造函数里的属性(和方法)。
其基本思想是使用原型链实现对原型属性和方法的继承,在通过借用构造函数实现对实例属性的继承。既通过在原型上定义方法实现了函数的复用,又可以保证每个实例都有它自己的属性。
// 组合继承:
// 把原型(链)继承和call,apply继承结合起来。各自发挥自己的优势。
function Person(name,sex){
if(arguments.length>0){
this.name = name;
this.sex = sex;
}
}
Person.prototype.eat = function(str){
console.log(this.name+"在吃"+str);
}
function Doctor(name,sex,dept){
// call和apply继承:把父类构造函数里的属性和方法继承下来。
Person.apply(this,arguments);
this.dept = dept;
}
// 原型继承:把父类原型属性上内容(属性和方法)继承下来。
Doctor.prototype = new Person();
let d1 =new Doctor("米莱迪","女","儿科");
let d2 = new Doctor("亚瑟","男","骨科");
console.log(d1.name);//米莱迪
console.log(d2.name);//亚瑟
d1.eat("油泼面");//米莱狄在吃油泼面
如上,组合继承解决了原型链和借用构函数继承的缺陷,结合了两者的优点,成为了js中常用的继承模式。
当然,组合继承也并不是完美的,其最大的问题就是无论什么情况下,都会调用两次父类型构造函数,第一次是创建子类型原型的时候,即Doctor.prototype= new Person(),第二次是在子类型构造函数的内部,即Person.apply(this, name)。因此,子类型最终会包含父类型对象的全部实例属性,我们不得不在调用子类型构造函数时重写这些属性。这也算是组合集继承的缺点。
四:原型式继承
原型式继承是由道格拉斯·柯珞克德福于2006在《JavaScript中的原型式继承》一文中提出的。他的思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。意思就是创建一个函数,将参数作为一个对象的原型对象。
//创建一个函数fun,内部定义一个构造函数Son
function fun(obj) {
function Son() {};
//将Son的原型对象设置为参数,参数是一个对象,完成继承;
Son.prototype = obj;
//将Son实例化后返回,即返回的是一个实例化对象;
return new Son();
}
var parent = {
name: '安琪拉'
}
var son1 = fun(parent);
var son2 = fun(parent);
console.log(son1.name); //安琪拉
console.log(son2.name); //安琪拉
它的缺点是包含引用类型值的属性始终都会共享相应的值,和使用原型链继承模式一样。
五:寄生式继承
寄生式继承是和原型式继承紧密相关的一种思路,也是由上文中的原型式继承的提出者推广的一种继承模式。它的思路是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在返回增强之后的对象。(在原型式继承的基础上,在函数内部丰富对象)
function fun(obj) {
function Son() {};
Son.prototype = obj;
return new Son();
}
//再原型式继承的基础上,封装一个JiSheng函数;
function JiSheng(obj) {
var clone = fun(obj);
//将fun函数返回的对象进行增强,新增Say方法,最后返回;
clone.Say = function() {
console.log('新增的方法');
}
return clone;
}
var parent = {
name: '黄忠'
}
//调用JiSheng函数两次,分别赋值给变量parent1、parent2;
var parent1 = JiSheng(parent);
var parent2 = JiSheng(parent);
//对比parent1、parent2,结果为false,实现独立;
console.log(parent2.Say == parent1.Say); // false
ES5有一个新的方法Object.create(),这个方法相当于封装了原型式继承。这个方法可以接收两个参数:第一个是新对象的原型对象(可选的),第二个是新对象新增属性,所以上面代码也可以写:
function JiSheng(obj) {
var clone = Object.create(obj);
clone.Say = function() {
console.log('新增的方法');
}
return clone;
}
var parent = {
name: '黄忠'
}
var parent1 = JiSheng(parent);
var parent2 = JiSheng(parent);
对比parent1、parent2,结果为false,实现独立;
console.log(parent2.Say == parent1.Say); // false
优缺点:和借用构造函数继承类似,调用一次函数就得创建一遍方法,无法实现函数复用,效率较低。
六、寄生组合式继承
利用组合继承和寄生继承各自优势 组合继承方法我们已经说了,它的缺点是两次调用父级构造函数,一次是在创建子级原型的时候,另一次是在子级构造函数内部,那么我们只需要优化这个问题就行了,即减少一次调用父级构造函数,正好利用寄生继承的特性,继承父级构造函数的原型来创建子级原型。
基本思路是,不必为了指定子类型的原型而调用父类型的构造函数,我们需要是父类型原型的一个副本。本质上,就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
//封装一个函数JiSheng,两个参数,参数1为子级构造函数,参数2为父级构造函数
function JiSheng(son, parent) {
//利用Object.create(),将父级构造函数原型克隆为副本clone
var clone = Object.create(parent.prototype); //创建对象
//将该副本作为子级构造函数的原型
son.prototype = clone; //指定对象
//给该副本添加constructor属性,因为上一行中修改原型导致副本失去默认的属性
clone.constructor = son; //增强对象
}
function Parent(name) {
this.name = name;
this.type = ['法师', '射手', '打野'];
}
Parent.prototype.Say = function() {
console.log(this.name);
}
function Son(name) {
Parent.call(this, name);
}
JiSheng(Son, Parent);
son1 = new Son('妲己');
son2 = new Son('伽罗');
son1.type.push('辅助');
son2.type.push('肉盾');
console.log(son1.type); //['法师', '射手', '打野', '辅助']
console.log(son2.type); //['法师', '射手', '打野', '辅助']
son1.Say(); //妲己
son2.Say(); //伽罗
缺点:和组合继承一样,只不过没有组合继承的调用两次父类构造函数的缺点。
七:ES6 Class继承
ES6 引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
Class可以通过extends关键字来实现继承,让子类继承父类的属性和方法。
ES6规定,子类必须在constructor()
方法中调用super()
,否则就会报错。所以父类的构造函数必定会先运行一次
super: super表示父类(的构造函数),在子类的构造函数里,必须要调用父类的构造函数,而且,要写在子类构造函数里的第一句话;
// ES6的继承:
// 1、继承关系的关键字:extends;
// 2、子类构造函数里必须调用super(),而且必须写在子类构造函数的第一句话
// 3、ES6的这种写法就是个语法糖(换了写法,本质一样)
class Person {
constructor(name, sex) {
this.name = name;
this.sex = sex;
}
eat(str) {
console.log(this.name + "在吃" + str);
}
}
// extends关键字完成继承关系
class Doctor extends Person {
constructor(name, sex, dept) {
// super就是父类
super(name, sex); //这就相当于调用了父类的构造函数。这句话必须放在子类构造函数里的第一句话
this.dept = dept;
}
}
let d1 = new Doctor("娜可露露", "女", "儿科");
let d2 = new Doctor("宫本武藏", "男", "骨科");
console.log(d1.name);//娜可露露
console.log(d2.name);//宫本武藏
d1.eat("油泼面");//娜可露露再吃油泼面
super的作用:
1、可以用来调用父类的构造函数。
2、当父类和子类里有同名的方法时,那么,super,可以用来调用父类的方法;this可以用来调用子类的方法。
class Person {
constructor(name, sex) {
this.name = name;
this.sex = sex;
}
eat(str) {
console.log(this.name + "在吃" + str);
}
}
// extends关键字完成继承关系
class Doctor extends Person {
constructor(name, sex, dept) {
// super就是父类
super(name, sex);
this.dept = dept;
this.eat("北京烤鸭"); //娜可露露在吃北京烤鸭、宫本武藏在吃北京烤鸭
super.eat("南京板鸭"); //super:就是父类的对象//娜可露露在吃南京板鸭、宫本武藏在吃南京板鸭
}
eat(str) {
console.log(this.name + "在吃" + str);
}
}
let d1 = new Doctor("娜可露露", "女", "儿科");
let d2 = new Doctor("宫本武藏", "男", "骨科");
用class的好处就是:极大地简化了原型链代码;缺点就是:不能兼容所有的浏览器。