有问题的小明提出一个问题:首先来理解一个概念,何为继承?
解:子类的实例可以访问父类的属性和方法(此篇主要讲ES5语法,还不涉及到类,就叫构造函数)
一:通过原型链实现继承
首先我们定义一个父构造函数,父构造函数有name和friend属性,并且父构造函数的原型对象有runing方法。
function Person(){
this.name="person";
this.friends=['a']
}
Person.prototype.runing=function(){
console.log("runing")
}
我们在定义一个子构造函数,有一个son属性
function Student (){
this.sno="001"
}
问子构造函数如何继承父构造函数呢?
解:通过原型链实现继承,即将子构造的原型对象指向父构造函数的实例对象
实现思路: Student 和Teacher都是子构造函数,Person是父构造函数
Student.prototype=new Person()
Teacher.prototype=new Person()
pass:这里有个点需要注意,应该先改变子构造函数的原型指向,再向子构造函数的原型添加属性或方法,反之则是加在了子构造函数之前的原型对象上面了哦
完整代码:
//定义父构造函数
function Person(){
this.name="person";
this.friends=['a']
}//给父构造函数的原型对象添加方法
Person.prototype.runing=function(){
console.log("runing")
}
//定义子构造函数
function Student (){
this.sno="111"
}
//将子构造函数的原型指向父构造函数的实例对象
Student.prototype=new Person()
//给子构造函数原型增加属性或方法
Student.prototype.studying=function(){
console.log("studying")
}
Student.prototype.name='why'
var stu=new Student();
stu.studying();//studying
stu.runing();//runing
上面就是通过原型链实现继承的核心代码啦?
在这里,小伙伴思考一下,如果我们直接打印stu会出来什么呢?
console.log(stu);//??打印什么呢 Person{sno:'111'}
对,直接打印stu对象输出 Person{sno:‘111’},为什么直接打印stu没有出现name属性呢?
解:因为name属性是保存在new Person()出来的实例对象上面,直接打印是看不到这个属性的,哈哈
这个时候小明又有另外一个问题了,为啥继承的name和friends属性没有出来呢?
解:这属于原型链继承的弊端之一,继承的父构造函数的属性直接打印是看不到的
来,我们画个图理解一下把
图解说:首先Person构造函数有自己显示原型对象 prototype,Person的原型原型上面有constructor属性指回构造函数本身,和runing方法,还有自己本身的–proto–隐式原型对象指向Object对象
pass:(如果要理解这个,要先弄清楚原型链直接的关系,如不懂,请看前篇文章)
本身Studnet构造函数有自己默认的原型对象,现在我们把他改了,让Stunden构造函数的原型对象指向new Person()出来的实例对象(暂且先叫p对象);后续我们在Stunden构造函数的原型对象添加的属性或方法都在这个p对象上面,这点要注意
1-1:原型链继承的弊端
因为通过原型链继承,所以有一些属性是保存在p对象上(修改子构造函数原型对象指向的对象)
1:直接打印对象是看不到继承过来的属性的
2.属性会被多个对象共享,如果修改引用类型数据,值会受到影响,直接添加是不会的
3.不能给父构造函数传参数,因为这个对象是一次性创建的(没办法定制化)
好,接下来我们看第二种实现方案
二:通过借用构造函数实现继承
为了解决原型链继承的弊端,话不多说,接下来看第二种实现继承的方法把 !
思路:在子构造函数里面,通过显示绑定this,来调用父构造函数
小明问?如果通过借用构造函数实现继承呢?上代码:
//定义父构造函数
function Person(name,age,friends){
this.name=name;
this.age=age;
this.friends=friends
}
//给父构造函数的原型对象添加方法
Person.prototype.eating=function(){
console.log("eating")
}
//定义子构造函数
function Stunent(sno,name,age,friends){
this.sno=sno;
Person.call(this,name,age,friends);//这里通过传入当前this,通过call调用父构造函数实现传参
}
var p=new Person();
//将子构造函数原型指向父构造函数实例
Stunent.prototype=p
Stunent.prototype.food="西红柿"
var stu1=new Stunent('111','why',18,['lilei']);
console.log(stu1) //Person { sno: '111', name: 'why', age: 18,['lilei'] }
var stu2=new Stunent('112','kobe',18,['james']);
console.log(stu2) //Person { sno: '111', name: 'kobe', age: 18,['james'] }
console.log(stu1.food) //西红柿
可解决原型链实现继承的不能传参弊端,也避免了原型数据共享问题
核心代码解说:
Person.call(this,name,age,friends);//这里通过传入当前this,通过call调用父构造函数实现传参
这一步的伪代码相当与给当前的this对象(Stunent构造函数的this对象)上面加name,age,friends属性,借用Person构造函数的执行代码,所以就实现了继承
如果还不理解的话,那直接上图把
pass:( 图中少了一笔,stu2对象的隐式原型也是指向p对象哒!)
图解:图中,stu1和stu2对象的隐式原型,还有Stundent构造函数的显示原型都执行p对象,而p对象的隐式原型指向 Person构造函数的显示原型对象,图中可以看出p对象上面多出了一份父构造函数里面的属性。
2-1:借用构造继承的弊端
1.父构造函数至少会被调用两次(一次是在创建子构造函数原型的时候,一次是在子构造函数调用父构造函数的时候)
2.子构造函数指向的原型(父构造函数的实例)上面会多出一份父构造函数的属性或方法
有木有发现,其实到了这里,实现继承已经没有问题了,虽然她不是很完美。
那我们接下来看第三种实现继承的方法把!
三:原型式继承
原型式继承可以解决借用构造函数实现继承的弊端
思路:创建一个对象,让这个对象的原型指向父构造函数的原型对象
那我们如何创建一个对象呢?这时候小明又提出问题,创建对象不是很简单嘛?直接通过字面量创建,或者通过 new Object创建吗?
解:bigo只答对了一半,我们要修改创建这个对象的原型,让他指向父构造函数原型对象哦!
修改对象的原型有两种方案:
1.可通过Object.setPrototype()方法修改对象的原型
Object.setPrototypeOf(obj, prototype)//obj:要设置其原型的对象;prototype:该对象的新原型
2.Object.create(proto,[propertiesObject])
Object.create(proto)//方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
创建这个对象的核心代码 :第一种方式:
function createObject(proto){
//第一种方法:直接通过 setPrototypeOf 设置对象的原型
var obj={};
Object.setPrototypeOf(obj,proto);
return obj;
}
第二种方式:
function createObject(proto){
//第二种方法 :定义一个函数,更改函数的原型,使他指向父构造函数的原型,然后 new一个函数的返回对象,这样该对象的原型就间接指向父构造函数的原型了
function foo (){};
foo.prototype=proto;
return new foo()
}
还是给小伙伴们整理下三种方式汇总把:
完整代码如下:
function Person(name,age){
this.name=name;
this.age=age
}
Person.prototype.runing=function(){
console.log("runing")
}
function Teacher(name,age,leader){
this.leader=leader;
Person.call(this,name,age)
// this.age=age;
// this.name=name;
}
function createObject(proto){
//第一种方法:直接通过 setPrototypeOf 设置对象的原型
// var obj={};
// Object.setPrototypeOf(obj,proto);
// return obj;
//第二种方法 :定义一个函数,更改函数的原型,使他指向父构造函数的原型,然后 new一个函数的返回对象,这样该对象的原型就间接指向父构造函数的原型了
function foo (){};
foo.prototype=proto;
return new foo()
}
var obj1=createObject(Person.prototype);
Teacher.prototype=obj1;
var t1=new Teacher('张三','18','教授')
var t2=new Teacher('李四','18','教授');
t2.age="19";
console.log(t2) //Person { leader: '教授', age: '19', name: '李四' }
console.log(t1) //Person { leader: '教授', age: '18', name: '张三' }
t1.runing() //runing
console.log(Object.getPrototypeOf(t1))//Person {}
我们可以发现原型式继承可以解决借用构造函数继承的两个弊端哦
四:寄生式继承
寄生式和原型式继承有点像,但不同的是在原型式继承的方案上进行了增强。
思路: 寄生式继承的思路是将原型式继承和工厂模式联合在一起,在创建对象的时候,对这个对象进行增强。
核心代码:
function createObject(proto){
function foo (){};
foo.prototype=proto;
return new foo()
};
function createStudent(proto,val){
var newObj=createObject(proto);
//增强该对象,添加独有的方法
newObj.feature=val;
}
完整代码:
function createObject(proto){
function foo (){};
foo.prototype=proto;
return new foo()
};
function createStudent(proto,val){
var newObj=createObject(proto);
//增强该对象,添加独有属性
newObj.feature=val;
return newObj;
}
function Person(name,age){
this.name=name;
this.age=age
}
Person.prototype.runing=function(){
console.log("runing")
}
function Teacher(name,age,leader){
this.leader=leader;
Person.call(this,name,age)
}
var tempObj=createStudent(Person.prototype,'特征')
Teacher.prototype=tempObj;
var t1=new Teacher('李老师','77','高级教授');
console.log(t1)//Person { leader: '高级教授', name: '李老师', age: '77' }
// createStudent 方法给t1原型对象新增的增强的属性
console.log(t1.feature)//特征