JavaScript深入之继承的多种方式和优缺点

有问题的小明提出一个问题:首先来理解一个概念,何为继承?
解:子类的实例可以访问父类的属性和方法(此篇主要讲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对象哒!)

图解:图中,stu1stu2对象的隐式原型,还有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)//特征

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值