这两天,把原型链这一系列以及继承方式给整理和回顾一下.真的令人崩溃....本来写了一半了,突然实验室断电,只好重新再写..
1.构造函数与实例
首先,我们先来讲述下构造函数,是用来创建对象的函数,本质上也是函数.与其他函数的区别在于调用方式不同:
如果通过new操作符来调用的,就是构造函数;如果没有通过new操作符来调用的,就是普通函数.
首先,我们先来举个例子:
function Product(name,id){ this.name=name; this.id=id; } //当作构造函数在使用 var product= new Product('Sam',11); //当作普通函数调用时,这里相当于给window对象添加了name以及id属性 Product('House',13); console.log(product);//Product {name:"Sam",id:11) console.log(name);//House console.log(id);//13
在var product=new Product('Sam',11);中,通过new操作符调用了Product,并且生成了product,这里的Product就称为构造函数,product称为Product函数对象的一个实例.
2.原型对象
当我们创建一个函数的时候,函数对象都会有个prototype属性,这个属性是一个指针,指向它的原型对象.原型对象的本质也是一个对象.
function Product(name,id){ this.name=name; this.id=id; } console.log(Product.prototype)//object{constructor:Product}
可以看到Product.prototype指向了一个对象,即Product的原型对象,并且这个对象有一个constructor属性,又指向了Product函数对象.我们可以通过一张图来解释:(直接照了,画图忒麻烦...)
从图上,我们可得知:
1.函数对象的prototype指向原型对象,原型对象中的constructor指向函数对象.实例对象的_proto_属性指向原型对象,这里的_proto_是内部属性,可以理解为它是存在的,但是不允许我们去访问,其作用为允许实例通过该属性访问原型对象中的属性与方法.
function Product(name,id){ this.name=name; this.id=id; } Product.prototype.price=45; var product1 = new Product('Apple',11); var product2 = new Product('Pear',17); product2.price=9; console.log(product1.price)//45 console.log(product2.price)//9
这里我们未给product1实例设置price属性,但是因为_proto_的存在,会访问原型对象中对应的属性.并且,我们给product2设置price属性后输出的是9,说明只有当实例本身不存在对应的属性或方法时,才回去找原型对象上的对应属性或方法.
OK,上述的话算是把构造函数、实例以及原型对象有了一定的认识,那么而我们就来看看继承.继承的话,主要思路就是利用原型链,我们先来看看原型链的先.
原型链的原理的话是让一个引用类型继承另一个引用类型的属性与方法.
原型对象的话通过constructor指向构造函数,而实例通过_proto_指向原型对象
那如果某个函数的原型对象等于另一个函数的实例呢,会出现怎么样的状况呢:
function yee(){ } //在yee的原型上绑定say()方法 yee.prototype.say = function (){ console.log('just yee '); } function gene(){ } //让gene的原型对象指向yee的实例 gene.prototype = new yee(); //在gene的原型上绑定talk()方法 gene.prototype.talk = function (){ console.log('gene') } var b1 = new gene(); b1.say();//just yee
首先,我们创建了gene与yee两个函数对象,同时也就生成了它们的原型对象.接着我们给yee添加了say()方法.之后,较为关键的一步为gene.prototype=new yee(),我们让函数对象gene的prototype指针指向了一个yee,这也就是为什么gene的原型对象里不再有constructor属性,其实gene本身是有真正的原型对象,但是因为我们改写了指针,使它指向了另一个对象,因此gene之后的原型对象,现在无法被访问了,取而代之的时这个新的原型对象yee的一个实例,自然就没有constructor属性了.接下来,我们又在gene中增加了一个talk属性,之后我们生成了一个gene的实例,b1.我们调用b1的say()发现可以执行,那么为什么可以执行呢?
因为,b1沿着_proto_属性,可以访问到gene prototype,gene prototype又沿着_proto_属性访问yee prototype,最终在yee.prototype上找了say(),并执行.
总而言之,这种b1继承了yee的属性与方法,这种由_proto_不断把实例与原型对象联系起来的结构,即为原型链.
还要记住一点,就是当我们需要在子类中添加新的方法或者是重写父类方法时,一定一定要放置在原型的语句之后哦.我们来看下面代码:
function yee(){ } //在yee的原型上绑定say()方法 yee.prototype.say = function (){ console.log('just yee '); } function gene(){ } //让gene的原型对象指向yee的实例 //gene.prototype.foo()=function(){} //在这里写子类的原型方法与属性是无效的,因为会改变掉圆形的指向,因此要写在重定向之后. gene.prototype = new yee(); //在gene的原型上绑定talk()方法 gene.prototype.talk = function (){ console.log('gene') } var b1 = new gene(); b1.say();//just yee
原型链的继承方式的话,父类新增原型方法,子类都可以访问到,且简单易于实现,但是创建子类实例时,无法向父类构造函数传参.
二、借用构造函数实现继承
这种方式关键在于,在子类型中构造函数中通过call()函数直接调用父类型构造函数
<script>
function Person(name,age){ this.name=name, this.age=age, this.setName=function (){} } Person.prototype.setAge =function (){} function kid(name,age,id){ Person.call(this,name,age) //相当于this.Person(name,age)
//this.name=name
//this.age=age
this.id = id } var s1= new kid('Jerry',9,11) </script>
但是,这种方式只是实现了部分的继承,想要在子类中再调用其他属性,是无效的.
console.log(s1.setName());//Uncaught Error:setName()is not a function
构造函数相较于原型链的继承话,可以实现多继承(使用call函数),但是实例并不是父类的实例,仅仅只是子类的实例;且只能继承父类的实例属性与方法,不能继承原型属性与方法,且无法实现函数复用.
三、原型链与借用构造函数的组合继承
通过调用父类构造,继承父类的属性并保存传参的优点,然后将父类实例作为子类原型,实现函数复用.
function Person(name,age){ this.name=name, this.age=age, this.setName=function (){} } Person.prototype.setAge =function (){ console.log('ljy')} function kid(name,age,id){ Person.call(this,name,age) //相当于this.Person(name,age) //this.name=name //this.age=age this.id = id this.setScore=function(){} } kid.prototype= new Person() kid.prototype.constructor = kid//关键一步 kid.prototype.talk=function(){} var s1= new kid('Jerry',9,11) console.log(s1.constructor)//kid
这种方式的话,融合了原型链继承与构造函数的优点,是极为常用的一个继承模式.可以继承实例属性以及方法,也可以继承原型属性以及方法,函数也可复用;缺点即为调用了两次父类构造函数.
四、组合继承优化1
这种方式通过父类原型与子类原型指向同一对象,子类可以继承到父类的公有方法当作自己的公有方法,而且不会初始化两次实例方法,避免的组合继承的缺点.
function Person(name,age){ this.name=name, this.age=age, this.setAge=function(){} } Person.prototype.setAge=function(){ console.log("ljy")} function kid(name,age,id){ Person.call(this,name,age) this.id=id this.setScore=function(){}} kid.prototype=Person.prototype kid.prototype.talk=function(){} var s1=new kid('Jerry',10,12) console.log(s1)
但是,用了这种方法的话,没办法辨别是子类还是父类实例化,我们用下面代码就可以看出来:
console.log(s1 instanceof kid,s1 instanceof Person)//true true console.log(s1.constructor)//Person
五、组合继承优化2
借助原型可以基于已有的对象来创建对象,var B= Object.create(A)以A对象为原型,生成了B对象.B继承了A的所有属性与方法.
function Person(name,age){ this.name=name, this.age=age, this.setAge=function(){} } Person.prototype.setAge=function(){ console.log("ljy")} function kid(name,age,id){ Person.call(this,name,age) this.id=id this.setScore=function(){}} kid.prototype=Object.create(Person.prototype) kid.prototype.constructor=kid var s1=new kid('Jerry',11,13) console.log(s1 instance of kid, s1 instance of Person)//true true console.log(s1.constructor)//kid
同时,kid继承了所有的Person原型对象的属性与方法.这个方法很好!
六、ES6中class的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法.实质上class方法依旧是原型上的实现,它只是原型的语法糖.
class Person { //调用类的构造方法 constructor(name, age) { this.name = name this.age = age } //定义一般的方法 showName() { console.log("调用父类的方法") console.log(this.name, this.age); } } let p1 = new Person('ljy', 23) console.log(p1) //定义一个子类 class Student extends Person { constructor(name, age, salary) { super(name, age)//通过super调用父类的构造方法 this.salary = salary } showName() {//在子类自身定义方法 console.log("调用子类的方法") console.log(this.name, this.age, this.salary); } } let s1 = new Student('Kitty', 26, 1000000000) console.log(s1) s1.showName()