以下分两类介绍六种继承,以及es6中类的继承
第一类
原型链继承
通过原型链使一个引用类型继承另一个引用类型的属性和方法
引用类型的值在原型链传递中存在的问题:
我们知道js中有值类型和引用类型,其中引用类型包括Object.Array等,引用类型的值有一个特点:在赋值的时候,赋给变量的是它在内存中的地址。换句话说,被赋值完的变量相当于一个指针,这会有什么问题呢?看例子:
function A() {
this.name = "a"
this.color = ['red','green'];
}
function B(){
}
//让B的原型对象指向A的一个实例
B.prototype = new A();
//生成两个个B的实例
var b1 = new B();
var b2 = new B();
//观察color属性
console.log(b1.name)//a
console.log(b2.name)//a
console.log(b1.color)//[red,green]
console.log(b2.color)//[red,green]
//改变b1的name和color属性
b1.name = 'b'
b1.color.push('yellow')
//重新观察color属性
console.log(b1)//b
console.log(b2)//a
console.log(b2.name)
console.log(b1.color)//["red", "green", "yellow"]
console.log(b2.color)//["red", "green", "yellow"]
复制代码
第一个缺陷是 引用类型的值在赋值的时候,赋给变量的是它在内存中的地址。 所以在原型链中如果A(其实就是继承中的父类型)含有引用类型的值也就是上面的color数组,引用类型的值放在堆空间中,那么子类型的实例共享这个引用类型,当它被修改后所有的子类型继承的引用类型都将改变。
第二个缺陷是:在创建子类型的实例(如b1,b2)时,无法向父类型的构造函数中传递参数。比如在上面的例子中,如果A的name属性是要传递参数,实例化b1和b2没法传参。
构造函数继承
为了解决引用类型值带来的问题,可以用构造函数继承的方式,又名伪造对象或者经典继承,
核心思路是:我们在子类型的构造函数中调用父类型的构造函数,这里要用到一个方法call()或者apply()函数,关于这个函数,我这里简单介绍一下,可以简单的理解功能就是,允许一个对象调用另一个对象的方法。函数如下
function A(name) {
this.name = name
this.color = ['red','green'];
}
function B(){
//“借用”就体现在这里,子类型B借用了父类型A的构造函数,从而在这里实现了继承
//在这里我们接受一个参数,并且通过call方法传递到A的构造函数中
A.call(this,name);
}
//生成两个个B的实例
var b1 = new B();
var b2 = new B();
//观察color属性
console.log(b1.name)//a
console.log(b2.name)//a
console.log(b1.color)//['red','green']
console.log(b2.color)//['red','green']
//改变b1的name和color属性
b1.name = 'b'
b1.color.push('black')
//重新观察属性
console.log(b1.name)//b
console.log(b2.name)//a
console.log(b1.color)//['red','green','black']
console.log(b2.color)//["red", "green"]
复制代码
使用该方法可以解决原型继承中的两个问题,实例化出的对象都可以独自修改自身属性,还可以给构造函数传递参数。但是这种继承方式,所有的属性和方法都要在构造函数中定义,比如我们要绑定sayA()方法并继承,就只能写在A的构造函数里面,而写在A prototype的的方法,没法通过这种方式继承,而把所有的属性和方法都要在构造函数中定义的话,就不能对函数方法进行复用。
组合继承
学习了原型链的继承和借用构造函数的继承后,我们可以发现,这两种方法的优缺点刚好互补:
- 原型链继承可以把方法定义在原型上,从而复用方法
- 借用构造函数继承法可以解决引用类型值的继承问题和传递参数问题
因此,就自然而然的想到,结合这两种方法,于是就有了下面的组合继承,也叫伪经典继承,(前面的借用构造函数是经典继承,可以联系起来),具体实现如下
function A(name) {
this.name = name
this.color = ['red','green'];
}
A.prototype.sayA = function(){
console.log("form A")
}
function B(name,age){
//借用构造函数继承
A.call(this,name);
this.age = age;
}
//原型链
B.prototype = new A();
B.prototype.sayB = function(){
console.log("form B")
}
//生成两个个B的实例
var b1 = new B('Mike',12);
var b2 = new B('Bob',13);
//观察color属性
console.log(b1)//{name:'Mike'...}
console.log(b2)//{name:'Bob'...}
b1.sayA()//from A
b2.sayB()//from B
复制代码
最终实现的效果就是,b1和b2都有各自的属性,同时方法都定义在两个原型对象上,这就达到了我们的目的:属性独立,方法复用,这种继承的理解相对简单,因为就是把前两种继承方式简单的结合一下,原型链负责原型对象上的方法,call借用构造函数负责让子类型拥有各自的属性。 组合继承是js中最常用的继承方式
第二类
原型式继承
原型式继承与之前的继承方式不太相同,原理上相当于对对象进行一次浅复制,浅复制简单的说就是:把父对像的属性,全部拷贝给子对象。但是我们前面说到,由于引用类型值的赋值特点,所以属性如果是引用类型的值,拷贝过去的也仅仅是个指针,拷贝完后父子对象的指针是指向同一个引用类型。原型式继承目前可以通过Object.create()方式来实现。 Object.create()接收两个参数:
- 第一个参数是作为新对象的原型的对象
- 第二个参数是定义为新对象增加额外属性的对象(这个是可选属性)
如果没有传递第二个参数的话,就相当于直接运行object()方法。
简单来说就是 我们现在要创建一个新对象B,那么要先传入第一个参数对象A,这个A将被作为B prototype;然后可以再传入一个参数对象C,C对象中可以定义我们需要的一些额外的属性。来看例子
var A = {
name:'A',
color:['red','green']
}
//使用Object.create方法先复制一个对象
var B = Object.create(A);
B.name = 'B';
B.color.push('black');
//使用Object.create方法再复制一个对象
var C = Object.create(A);
C.name = 'C';
B.color.push('blue');
console.log(A.name)//A
console.log(B.name)//B
console.log(C.name)//C
console.log(A.color)//["red", "green", "black", "blue"]
复制代码
在这个例子中,我们只传入第一个参数,所以B和C都是对A浅复制的结果,由于name是值类型的,color是引用类型的,所以ABC的name值独立,color属性指向同一个对象。接下来举个传递两个参数的例子:
var A = {
name:'A',
color:['red','green'],
sayA:function(){
console.log('from A');
}
};
//使用Object.create方法先复制一个对象
var B = Object.create(A,{
name:{
value:'B'
}
});
console.log(B)//Object{name:'B'}
B.sayA()//'from A'
复制代码
这个例子就很清楚的表明了这个函数的作用了,传入的A对象被当做B的原型,所以生成B对象没有sayA()方法,却可以调用该方法(类似于通过原型链),同时我们在第二个参数中修改了B自己的name,所以就实现了这种原型式继承。原型式继承的好处是:如果我们只是简单的想保持一个对象和另一个对象类似,不必大费周章写一堆代码,直接调用就能实现
寄生式继承
寄生式继承和原型继承联系紧密,思路类似于工厂模式,即创建一个只负责封装继承过程的函数,在函数中根据需要增强对象,最后返回对象,是原型对象的加强版,看代码
function createA(形参){
//创建新对象 联系上文中‘如果没有传递第二个参数的话,就相当于直接运行object()方法’当参数是对象传进来就相当与进行了一次原型继承
var obj = Object(形参);
//增强功能,给原型继承的对象添加方法
obj.sayO = function(){
console.log("from O")
};
//返回对象
return obj;
}
var A = {
name:'A',
color:['red','green','blue']
};
//实现继承
var B = createA(A);
console.log(B)//Object {name: "A", color: Array[3]}
B.sayO();//from O
复制代码
继承的结果是B拥有A的所有属性和方法,而且具有自己的sayO()方法,但是这种在函数中封装的方法没办法复用会降低效率。
寄生组合式继承
前面的五种继承分别是原型链,借用构造函数继承,组合继承,原型式继承,寄生式继承,其中,前三种联系比较紧密归为一类,后面两种也比较紧密,而我们要讲的最后一种,是和组合继承还有寄生式继承有关系的。
组合继承仍有缺陷 我们在之前说过,最常用的继承方式就是组合继承,但是看似完美的组合继承依然有缺点:子类型会两次调用父类型的构造函数,一次是在子类型的构造函数里,另一次是在实现原型链的步骤,来看之前的代码:
function A(name) {
this.name = name
this.color = ['red','green'];
}
A.prototype.sayA = function(){
console.log("form A")
}
function B(name,age){
//第二次调用了A
A.call(this,name);
this.age = age;
}
//第一次调用了A
B.prototype = new A();
B.prototype.sayB = function(){
console.log("form B")
}
var b1 = new B('Mike',12);
var b2 = new B('Bob',13);
console.log(B.prototype)//A {name: undefined, color: Array[2]}
复制代码
通过组合继承,只要是A里面原有的属性,B一定会有,b1和b2肯定也会有,因为如果调用属性就该对象中存在就不去它的原型对象上找,这样就造成了一种浪费:B prototyope即A的实例对象上的属性其实我们根本用不上,为了解决这个问题,我们采用寄生组合式继承。 寄生组合式继承的核心思路是其实就是换一种方式实现 B.prototype = new A;从而避免两次调用父类型的构造函数,官方定义是:使用寄生式继承来继承父类型的原型,然后将结果指定给子类型的原型,。`这句话不容易理解,来看例子:
//我们一直默认A是父类型,B是子类型
function inheritPrototype(B,A){
//复制一个A的原型对象
var pro = Object(A.prototype);
//改写这个原型对象的constructor指针指向B
pro.constructor = B;
//改写B的prototype指针指向这个原型对象
B.prototype = pro;
}
复制代码
这个函数很简短,只有三行,函数内部发生的事情是:我们复制一个A的原型对象,然后把这个原型对象替换掉B的原型对象。为什么说这样就代替了 B.prototype = new A;,不妨思考一下,我们最初为什么要把B的prototype属性指向A的一个实例?无非就是想得到A的prototype的一个复制品,然后实现原型链。而现在我们这样的做法,同样达到了我们的的目的,而且,此时B的原型对象上不会再有A的属性了,因为它不是A的实例。因此,只要把将上面的 B.prototype = new A();,替换成inheritPrototype(B,A),就完成了寄生组合式继承。
寄生组合式继承保持了组合继承的优点,又避开了组合继承会有无用属性的缺陷,被认为是最理想的继承方式。
类继承
EcmaScript 2015 (又称ES6)通过一些新的关键字,使类成为了JS中一个新的一等公民。但是目前为止,这些关于类的新关键字仅仅是建立在旧的原型系统上的 语法糖,所以它们并没有带来任何的新特性。不过,它使代码的可读性变得更高,并且为今后版本里更多面向对象的新特性打下了基础。下面主要讲一讲类的继承
语法
类的首字母必须是大写
// 父类
class Father{
}
// 子类继承父类
class Son extends Father {
}
复制代码
子类使用super关键字访问父类的方法,类里面的方法直接会加到原型对象里面,一般把属性放进constructor,把方法放进类里实例化时就会执行。
//定义了父类
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
//子元素继承父类
class Son extends Father {
constructor(x, y) {
//使用super调用了父类中的构造函数
super(x, y);
}
}
let son = new Son(10, 20);
son.sum(); //结果为30
复制代码
- 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的
- 继承中,如果子类里面没有,就去查找父类有没有这个方法,如果有,就执行父类的这个方法(就近原则)
- 如果子类想要继承父类的方法,同时在自己内部扩展自己的方法,利用super 调用父类的构造函数,super 必须在子类this之前调用,要想在子类添加自己的独有属性,要添加constructor,要是还想用父类继承下来的属性,要用super(类名,...),要想在子类添加自己的独有方法也用super
<script>
// 父类有加法方法
class Father{
constructor(x,y) {
this.x = x ;
this.y = y ;
}
sum() {
console.log(this.x + this.y);
}
}
// 子类继承父类加法的方法, 同时 扩展减法方法
class Son extends Father {
constructor (x,y){
// 利用super调用父类的构造函数
// super必须在子类this之前调用
super(x,y);
this.color = color;
}
subtract(){
console.log(this.x - this.y);
}
}
var son = new Son(20,10);
son.subtract();//10
son.sum(); //30
</script>
复制代码
注意:this的指向问题,类里面的共有的属性和方法一定要加this使用.
作者:cm
链接:https://juejin.cn/post/6981247169064304653
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。