看完这篇,或许对继承会有不一样的认识?

前言

要了解继承,我们得先了解一下new,new之后到底发生了什么?

javaScript没有类做对象的抽象模式,他能够不通过类直接创建对象,相比于其他的面向对象语言,javaScript才是真正的面向"对象"的编程语言,那么什么是继承呢?

继承是指:在已存在的类的基础上(父类,基类,超类),拓展出新的类(子类),其重要意义就是使代码可以复用,子类中也拥有父类的方法和属性

注意:1、以下代码父类都是指SuperClass ,子类都是指SubClass,方便理解

         2、打印结果没写全可以自行cv代码打印

"new"之后究竟发生了什么?

window.name = "小明";

	function Human() {
		console.log( this.name );
	}
	Human.prototype = {
		name: "小暗",
	}
	let h1 = Human(); //小明
	let h2 = new Human(); //小暗
	console.log( h1 ); //undefined
	console.log( h2.name ); //小暗
	console.log( h2.__proto__ ); //{name:'小暗'}
	console.log( h2.prototype ); //undefined

javascript的类在es6之前,没有class语法糖时,用的是构造函数实现的,与class不用的是,构造函数既是类,也是函数,既可以使用"函数名()"的方式执行,也可以使用"new 函数名()"的方式执行,这二者的效果是截然不同,在上面的例子中,使用函数名()打印的是小明,而另外一个则打印了小暗,由此可以知道,new实际上是吧构造函数原型(prototype)上得属性,放在了原型链(__proto__)上,那么当实例化对象取值时候就会在原型上取,而实例化对象的上prototype已经不见了

所以我们可以简单的理解为new实际上将构造函数的prototype上得属性放在了实例化对像__proto__上,那通过实例化对象.属性名进行取值.

那么new如何实现呢?

exports.newClass = function () {
   // 新增一个容器,用来装载构造函数(目标类)prototype上的所有属性
    let _target = new Object();
    //不能直接通过 this() 来运行构造函数,所以用一个变量装载
    let _this = this; 
    // 核心部分:将构造函数prototype上的所有属性放到新容器中
  _target.__proto__ = _this.prototype; 
    // 执行构造函数,相当于执行class中的constructor
  _this.apply(_target, arguments); 
    //将新的容器返回,此时通过 _target[属性名]就可以访问 this.prototype 中的属性了
  return _target; 
};

上述代码将new实现了一下,其中最重要的一步就是将构造函数的上prototype上得所有属性放在了新容器中,最后获得实例化对象的__proto__上就就有了构造函数原型中的所有属性了

说了这么多,目的只是让我们了解:实例化一个构造函数,实际上可以简单的将类的prototype上的属性转移到了实例化对象中,这样有助于我们理解继承的实现

同时,我们还应该了解原型,以及原型链,继承的概念

原型:每一个构造函数都有一个prototype属性,这个属性会在生成实例的时候,称为实例对象的原型对象,js中的每个对象都继承另一个对象,后面称为“原型”对象。

原型链:每个对象都有一个proto属性,对象的属性和方法,有可能定义在自身,也有可能定义在他的原型对象,由于原型本身也是对象,又有自己的原型,所以形成了一条原型链

原型链的作用:读取对象的某个属性时,javascript引擎先寻找对象本身的属性,也就是我们设置的属性,叫做对象属性,当我们设置这个值时,首先查看他有没有对象属性,没有就添加这个属性,有的话就修改这个对象属性,如果找到了他的对象属性属性就返回,如果找不到,就到他的原型里找,如果一直到最顶层的Object.prototype还是找不到的话,就返回undefined,原型属性,也可以理解为他的默认值。

继承:继承就是在子类构造函数中继承父类构造函数的私有属性和原型属性,我们在子类构造函数中使用call和apply方法调用父类构造函数,并且改变其this指向为子类构造函数的this,此时子类的构造函数就继承了父类的私有方法和属性,将父类的实例化对象赋值给子类的原型对象,此时子类就继承了父类的原型属性和原型方法

补充理解:

对象属性是针对该对象的,其他对象并不能获得

原型属性是针对该类对象所有的属性

所以在对象中就有私有属性(对象属性)和共有属性(原型属性)

并且还有一点需要我们知道的是:__proto__他是对象中的原型(也称为原型链)针对对象的 prototype 他是类的原型 针对类的 对象的原型和构造他的类的原型是完全相同的 也就是子类.__proto__===父类.prototype完全相等

Instanceof 用来判断某个引用类型对象是否属于某个类,返回值是true,或者false

好了,了解完之后就开始我们的主题。

第一种:原型对象继承

//创建父类
function SuperClass(name){
	this.name=name;
}
SuperClass.prototype.eat=function(){
	console.log("父类的方法")
}
//创建子类
function SubClass(id){
	this.id=id;
}
//实现继承关系
SubClass.prototype=new SuperClass("老黄")
//子类添加新的方法
SubClass.prototype.stydy=function(){
	console.log("子类的方法")
}
//实例化对象
let s=new SubClass(123)
//添加对象属性
s.name="老黑";
console.log(s)

第二种:原型链继承

//创建父类		
function SuperClass( props ) {
		this.state = props;
		this.info = {
			color: "red"
		}
	}
	SuperClass.prototype = {
		name: "ainimal"
	}
	SubClass.prototype = new SuperClass()
//创建子类
	function SubClass() {
		this.price = 123;
	}

	let s = new SubClass();
	let s1 = new SubClass();
	console.log( s, s.__proto__, s.__proto__.__proto__ );
	//上面打印出来是{price:123,state: undefined}
	console.log( s1.name, s1.info );
	//上面打印出来是ainimal  object 
	console.log( s === s1 )//fasle

原型链继承优点:简洁方便,子类拥有父类及父类prototype上得属性

缺点:1、子类通过prototype继承父类,只能父类单向传递属性给子类,无法向父类传递参数,为什么要向父类传递参数呢,如果父类中的某属性对参数有依赖关系,此子类继承父类关系就需要在new SuperClasss()时传递参数

2、当父类原型上得引用属性改变时,所有子类实例相对应的引用属性都会改变,即继承的引用类型属性都有引用关系

3、子类只能继承一个父类(因为继承方式是直接修改子类的prototype,如果再次修改,会将其覆盖)

4、继承语句浅不能修改子类的prototype因为此类继承会覆盖子类原型

第三种:借用构造函数(apply,call)

为了原型链继承的问题,我们就有了第三种继承方式,叫做借用构造函数的技术(有时候也叫伪造对象或经典继承),他的核心思想是:在子类构造函数的内部调用超类型构造函数。

那么在SubClass构造函数中使用SuperClass.call直接运行构造函数,然而直接执行构造函数和使用new实例化构造函数是完全不同的

使用前者(直接运行函数),在下方代码中会将SuperClass构造函数里初始化的属性带到SubClass中,而SuperClass.prototype中的name属性并未带到Subclass中

使用后者(new实例化构造函数), 则会将SuperClass.prototype中的属性带到SuperClass实例化对象的__proto__上,那么我们来看下面例子

function SuperClass( props ) {
		this.state = props;
		this.info = {
			color: "red"
		}
	}
	SuperClass.prototype = {
		name: "ainimal"
	}

	function SuperClass1() {
		this.id = "888";
	}

	function SubClass() {
		SuperClass.call( this, ...arguments )
		SuperClass1.call( this, ...arguments )
		this.price = 123;
	}
	let s = new SubClass();
	let s1 = new SubClass();
	console.log( s, s.__proto__, s.__proto__.__proto__ );
	// SubClass Object Object
	//Subcalss{id:"888",info:{color:'red'},price:123,state: 依旧是undefined}
	console.log( s1.name, s1.info, s1.state );
	//上面打印出来是 undefined object{color:"red"} undefined
	// 接下来,如果我们尝试修改s1实例化以后的属性,我们来看看s1有没有受到影响
	s.info.color = "yellow";
	console.log( s1.name, s1.info ) //undefined {color:"red"}
	console.log( s instanceof SubClass ) //true
	console.log( s instanceof SuperClass ) //false

从上面案例可以看出其优点是:

1、可以在子类构造函数中向超类或者父类构造函数传递参数,如果不理解的话,我们再来看下面这个案例

function SuperClass(name){
	this.name=name;
}
function SubClass(){
//继承了SuperClass,同时还传递了参数
	SuperClass.call(this,"thomas");
	//实例属性
	this.age=30;
}
let s=new SubClass();
console.log(s.name)//"thomas"
console.log(s.age)//30

以上案例中,SuperClass接收一个参数name,在子类SubClass构造函数内部调用SuperCLass构造函数时,实际上是为SubClass的实例设置了name属性

2、可以继承多个父类

3、继承同一个父类的子类的属性之间不会有引用关系(因为父类构造函数的执行是在每个子类中call(this)了,从而父类构造函数执行时,this代表着每个子类)

缺点:父类(SuperClass) prototype上得属性无法继承,只能继承父类构造函数的属性,正是因为这点,父类的函数无法复用(指的是无法复用父类prototype中的函数,只能通过父类构造函数将函数放在子类中)

针对父类的函数无法复用的理解:

父类SuperClass每次在子类SubClass中执行都会在每个子类重新初始化this.属性或者this.函数,这些属性是属于每个子类单独的,这样既增加了性能负担又使父类原型中的公共属性无法复用

而假如这些函数或者属性在SuperClass的prototype上,并且子类能继承父类,则所有子类用公共属性的都是父类的,此时就达到了复用效果,而原型链继承能够实现这个效果,于是我们开始下面这种组合继承。

第四种:组合继承

组合继承有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到了一块的一种继承方式

通过上面的借用构造函数实现继承,我们了解到不能继承父类原型上得属性,而原型链继承无法传参给父类,组合继承正好将两者的缺点都规避掉了,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现堆实例属性的继承,这样的话,既通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有他自己的属性。

let num = 0;
	function SuperClass( props ) {
		this.state = props;
		this.info = {
			color: "red"
		}
		console.log( ++num ) //1 2 3
	}
	SuperClass.prototype = {
		name: "ainimal"
	}

	SubClass.prototype = new SuperClass();

	function SubClass() {
		SuperClass.call( this, ...arguments )
		this.price = 123;
	}
	let s = new SubClass();
	let s1 = new SubClass();
	console.log( s, s.__proto__, s.__proto__.__proto__ );
	// SubClass Object Object
	//Subcalss{info{color:'red'},price:123,state:undefined}
	//Object{info:{color:'red'},state:undefined}
	//Object{name:"ainimal"}
	console.log( s1.name, s1.info, s1.state );
	//上面打印出来是 ainimale object{color:"red"} undefined
	// 接下来,如果我们继续尝试修改s1实例化以后的属性,我们来看看s1有没有受到影响
	console.log( s1.name, s1.info ) //ainimal {color:"red"}
	console.log( s instanceof SubClass ) //true
	console.log( s instanceof SuperClass ) //true

然而,组合继承在实例化父类和执行父类构造函数时执行了两次SuperClass,实际上原型链继承为了解决构造函数继承上父类的prototype无法被子类继承的问题,以下代码中new SuperClass()确实会将父类的prototype继承到子类中,但是也会将SuperClass构造函数中的操作又执行了一遍。

从以上代码中可以看到,new SuperClass()确实会将父类的prototype继承到子类中,但是也会将SuperClass构造函数中的操作又执行一遍(console.log(++num)执行了3次),而且原型链继承是将子类的原型直接替换掉,所以无法继承多个父类的问题也被延续下来了(但是也可以在父类上多加一次继承,使多个类形成原型链关系,达到多继承的目的,即A,B,C三个类,A要继承B和C,那么就让A继承B再继承C)

综上,我们来总结下组合继承的优缺点

优点:解决原型链继承和构造函数继承的主要问题

缺点:父类构造函数执行两遍,性能损耗

第五种:原型式继承

原型式继承是基于原型链继承的封装,特点和原型链继承一样,继承的引用类型属性都有引用关系

原型式继承的过渡对象F实际上就是原型链继承的子类构造函数,这么做相比原型链继承的特点:减少性能开销(子类是空白的构造函数,没有任何内容),对应的,无法在子类构造函数中初始化属性

这种方法并没有使用严格意义上得构造函数,实现思想是借助原型可以基于已有的对象创建新对象,同时还不比因此创建自定义类型。那么我们来看下面代码

function object( o ) {
		function F() {}
		F.prototype = o;
		return new F();
	}

	function SuperClass( props ) {
		this.state = props;
		this.info = {
			color: "red"
		};
	}
	SuperClass.prototype = {
		name: "thomas"
	}
	let o = new SuperClass( true );
	let s = object( o );
	let s1 = object( o )
	s1.price = 8888;
	//F{price:8888} 点开之后是price:8888,再点开原型就可以看到info:{color:'red'} state:true
	// 再点开里面的原型是name:"thomas"
	console.log( s1, s1.__proto__, s1.__proto__.__proto__ );
	console.log( s1.price, s1.name );//8888 'thomas'
	// s打印出来是F{},里面有info:{color:'red'} state:true 原型里面是name:'thomas'
	// thomas {color:'red'}
	console.log( s, s.name, s.info );
	// 设置s1的属性
	s1.info.color = "blue";
	// thomas {color:'blue'}
	console.log( s.name, s.info );

注意:这里打印的值有的没写上去,主要是因为有的属性在原型里,不好用文字写,可以自己把代码打印出来看看结果进行比对。

看完上面的案例,是不是觉得原型式继承和Object.create()很像??create函数的原理就是生成一个新的对象,这个新的对象的--proto--等于传入的对象,让我们回忆一下上面讲到的new的原理,new实际上就是将prototype放在实例化对象的--proto--上,不难理解,上面代码中F.prototype=o和new F() 做的就是这一步。

从本质上来讲的话,object()对传入其中的对象进行了一次浅复制,Es5中新增了Object.create()方法规范化了原型式继承。

优点:无子类构造函数开销,相当于实现了对象的浅复制

缺点:继承时无法向父类传参

和原型链继承一样,继承父类的引用类型属性都有引用关系。

第六种 寄生式继承

寄生式继承实际上在原型式继承的基础上做了二次封装,可以看成是工厂模式+原型式继承,将继承步骤放在新的函数中,可以在子类构造函数上子类添加子类独有的函数和属性,因此叫做寄生式继承,就好像子类独有的属性方法寄生在下面的transit函数中中一样,使用这种继承在新建子类时,每个子类中的属性都不一样,违背了代码复用的效果,我们先来看看代码

function object( o ) {
		function F() {}
		F.prototype = o;
		return new F();
	}

	function SuperClass( props ) {
		this.state = props;
		this.info = {
			color: "red"
		};
	}
	SuperClass.prototype = {
		name: "thomas"
	}

	function transit() {
		let sub = object( o );
		sub.type = {
			electricity: true,
			gasoline: false
		};
		return sub;
	}
	let o = new SuperClass( true );
	let s = object( o );
	let s1 = object( o )
	s1.price = 8888;
    console.log( s.type === s1.type ); // true 说明每个子类的属性都一样
	//F{price:8888} 点开之后是price:8888,再点开原型就可以看到info:{color:'blue'} state:true
	// 再点开里面的原型是name:"thomas"
	console.log( s1, s1.__proto__, s1.__proto__.__proto__ );
	console.log( s1.price, s1.name ); //8888 'thomas'
	// s打印出来是F{},里面有info:{color:'blue'} state:true 原型里面是name:'thomas'
	// thomas {color:'red'}
	console.log( s, s.name, s.info );
	// 设置s1的属性,影响了s的属性
	s1.info.color = "blue";
	// thomas {color:'blue'}
	console.log( s.name, s.info );

 

优点:1、无子类构造函数的开销2、继承父类所有属性3、子类拥有自己的属性

缺点:1、继承时无法向父类传参2、和原型链继承一样,继承父类的引用类型 属性都有引用关系

3、子类公共属性无法原型上定义,导致无法复用。

针对代码无法复用的理解

回顾一下上面的借用构造函数继承对代码复用的理解,子类构造函数中直接执行父类构造函数并改变this指向从而达到将父类属性初始化到子类中。而寄生式继承则是每次生成的子类都是新的构造函数F,所以在继承时单独给sub增加属性实际上时操作不同的子类构造函数,而如果这个做法能在子类prototype中进行,那么子类的函数及属性就可以复用。

第七种 寄生组合式继承

前面我们已经了解了很多种继承,是否你已经疲倦?那我们最后来看我们的寄生组合式继承,我们前面说的组合继承是我们最常用的继承模式,不过他也有自己的不足,那么我们来写一个例子来帮助我们理解组合继承的缺点,加深你的理解,让我门来看一下下面的这个组合继承

//父类
function SuperClass(name){
	this.name=name;
	this.color="red";
}
//父类的方法
SuperClass.prototype.sayName=function(){
	console.log(this.name)
}
//子类
function SubClass(name,age){
//第二次调用SuperClass()
	SuperClass.call(this,name);
	this.age=age;
}
//第一次调用SuperClass()
SubClass.prototype=new SuperClass();
SubClass.prototype.constructor=SuperClass;
SubClass.prototype.sayAge=function(){
	console.log(this.age)
}
	let s = new SubClass( "thomas", 39 )
	console.log(  s.__proto__, s.__proto__.__proto__ )

在以上组合继承的案例中,第一次调用SuperClass构造函数时,SubClass.prototype会得到两个属性,name和color,他们都是SuperClass的实例属性,只不过现在位于SubClass的原型中,也就是--proto--中,当调用SuperCLass的构造函数时,又会调用一次SuperClass构造函数,这一次又在新对象上创建了实例属性name和color,于是这两个属性就屏蔽了原型中的两个同名属性。

怎么理解呢,就是有两组name和color的属性:一组是在实例上,一组在SuperClass的原型中,这就是调用两次SuperClass构造函数的结果

如果了解了前面几种继承方式,那么再来看看下面这种最终的继承模式(在es6的class语法糖出现之前),寄生组合式继承是最理想的继承。

function extend( SuperClass, SubClass ) {
		SubClass.prototype = Object.create( SuperClass.prototype );
		SubClass.prototype.type = SuperClass;
	}

	function SuperClass( props ) {
		this.state = props;
		this.info = {
			color: "red"
		}
	}
	SuperClass.prototype = {
		name: "thomas"
	}

	function SubClass() {
		//调用一下父类的构造函数,将父类的属性放在子类中
		this.type.call( this, ...arguments )
	}
	extend( SuperClass, SubClass );
	SubClass.prototype.name = "other thomas"
	let s = new SubClass();
	let s1 = new SubClass();
	console.log( s, s.__proto__, s.__proto__.__proto__ );
	console.log( s.name ) //other thomas
	s1.info.color = "blue";
	console.log( s.info )//{color:'red'}
	console.log( s1.info )//{color:'blue'}

总结一下寄生组合式继承的优缺点

优点:解决了组合式继承的父类构造函数调用两次的问题,只创建了一次父类属性,并且子类拥有父类原型上的属性

缺点:多继承问题和子类prototype被修改

搞明白上面几种继承,突然觉得他和深复制有点像,没错,js继承的类被继承时,其属性和行为也会被复制到子类中,js中没有类只有对象,而我们所说的类的继承,实际上时基于对象的深复制。

那我们再完善总结一下我们的组合式继承步骤并且写两个案例

案例一

1、先写extend函数
function extend(subClass,supClass) {
// 创建一个空类,空的构造函数
            function A() {}
//  设置这个空类的原型是父类的原型,这样保证这个空类和父类一样,但是没有父类构造函数内容
//  这样就解决直接继承时调用两次构造函数的情况
            A.prototype=supClass.prototype;
//  将这个与父类相似的类实例化后赋值给子类的原型,这样子类的原型就是这个空类的实例化对象
//  因此子类的原型里面就有了空类的原型下所有属性和方法
            subClass.prototype=new A();
//  子类的原型指针指向子类的构造函数
            subClass.prototype.constructor=subClass;
//   因为可能在子类使用到父类原型
//   设置子类的属性superClass是父类的原型对象
            subClass.superClass=supClass.prototype;
//   如果父类原型的指针没有指向父类的构造函数,仍然指向的是对象
            if(supClass.prototype.constructor===Object.prototype.constructor){
//   将父类的原型指针指向父类构造函数
                supClass.prototype.constructor=supClass;
            }
       }
2、新建父类和父类所有原型属性和方法
 function Ball(user) {
            this.name=user;
            console.log("thomas");
        }
        Ball.prototype={
            a:1,
            b:2,
            c:function () {
                this.a=10;
            }
        };
3、新建子类,并且在子类的构造函数中执行 父类构造函数.call(this,参数1,参数2..)
function Box(user) {
           Ball.call(this,user);
       }
        extend(Box,Ball);
        Box.prototype.d=function () {
            
        };
        Box.prototype.e=5;
        Box.prototype.c=function () {
            Box.superClass.c.call(this);
            console.log(this.a);
        };
4、使用extend函数,填入子类和父类,完成继承
注意点:只能使用子类.prototype.属性/方法=...设置添加新的子类方法和属性
			不能使用 子类.prototype={...}这种添加属性方法,这样会覆盖继承的方法
			覆盖父类的方法,子类.prototype.父类方法=function(){}   
			子类.superClass.父类方法.call(this,参数...);
			这句写在子类覆盖父类方法中
 5、最后就可以使用new来实例化对象了
  let b1=new Box("xie");
        b1.c();
        console.log(b1);

案例二 父类是Ainimal,有属性高,宽,并且有跑,跳的方法,然后有猫和狗继承父类方法,并且添加自己的方法进去

 function Ainimal( height, weight ) {
            this.height = height;
            this.weight = weight;
        }
        Ainimal.prototype = {

            run: function () {
                console.log( "跑" )
            },
            jump: function () {
                console.log( "跳" )
            }
        };

        function extend( subClass, supClass ) {
           
            function A() {}
            A.prototype = supClass.prototype;           
            subClass.prototype = new A();
            // 子类的原型指针指向子类的构造函数
            subClass.prototype.constructor = subClass;
            // 因为可能在子类使用到父类原型
            // 设置子类的属性superClass是父类的原型对象
            subClass.superClass = supClass.prototype;
            // 如果父类原型的指针没有指向父类的构造函数,仍然指向的是对象
            if ( supClass.prototype.constructor === Object.prototype.constructor ) {
                // 将父类的原型指针指向父类构造函数
                supClass.prototype.constructor = supClass;
            }
        }

        function Cat( height, weight ) {
            Ainimal.call( this, height, weight )
        }
        extend( Cat, Ainimal );
        Cat.prototype.wash = function () {
            console.log( "我是猫,我除了跑和跳,我还会洗脸" )
        }
        let c = new Cat( 200, 300 );
        console.log( c.height, c.weight );
        c.wash();
        c.jump();
        c.run();
        // 新建一个狗类
        function Dog( height, weight ) {
            Ainimal.call( this, height, weight )

        }
        extend( Dog, Ainimal );
        Dog.prototype.shake=function(){
            console.log("我是一条小狗,我除了会跑,会跳,我还会摇尾巴")
        }
        let d=new Dog(300,400);
        console.log(d.height,d.weight);
        d.jump();
        d.run();
        d.shake();

体会到寄生组合式继承的妙用了

第八种 Es6的继承

es6的继承比较简单,下面就直接放上关键代码

class SubClass extends SuperClass {
            constructor( r ) {
                super( r ); //执行超类中constructor(构造函数)
            }
            // (webstorm编辑器可以看到) override 覆盖 这里因为重写父类中createBall方法,这样就会将其覆盖
            //  覆盖的目的是重写,父类的那个方法内容不再执行,而执行当前这个子类的这个方法
 						//覆盖原来方法中属性
            createBall( parent ) {
                //  super.方法()就是执行父类的当前这个方法
                //  通常用于我们需要在父类的该方法中加入更多语句,或者修改部分语句
                let div = super.createBall( parent );
                div.style.borderRadius = "0px";
                return div;
            }
  				//增加新的属性
            setWH( w, h ) {
                this.ball.style.width = w + "px";
                this.ball.style.height = h + "px";
            }
        }

ps~原生js我个人认为最重要的还是面向对象的编程思想,那么就离不开继承,从函数嵌套再到过程式开发,再到面向对象的开发,举例想想,如果我去一家新公司接手别人项目之后,如果他写的代码很乱,再举个例子当我看见jQuery写的项目时第一反应是,很难改。那我们就没办法去下手添加新功能,那就只能重写了,前端的路任重而遥远,继续加油。

posted @ 2022-02-17 11:45  thomas_001  阅读(52)  评论(0)    收藏  举报