浅谈js的常用继承封装和多态

好久没有写博客了,以前对js的继承都很模糊,最近重新整理了些资料重新温习了一下!
首先什么是构造函数?什么是原型?什么是实例化对像?,继承和实例化对象有什么区别?继承方式有哪些?各自有什么区别和优缺点?各自原理是什么?等等,下面我来从浅入深的来一 一介绍下,下面是我的个人见解,如有不对之处,请指正,谢谢!
javaSctript是门弱语言,没有类的概念,但是我们可以像其他语言一样可以模拟类的概念

一.构造函数是什么?什么是原型?什么是实例化对像,如何封装,如何继承?

简单地说构造函数是类函数,函数名与类名完全相同,首字母大写,一般与new关键字一起用,用来创建对象初始化对象的。

function FatherClass(){
this.family=['father','mother','daughter'];
}
var instance1=new FatherClass();
var instance2=new FatherClass();

上面的函数是构造函数,看函数名字:父类,它上面有一个自己的属性family,每个构造函数都有自己的原型对象,构造函数prototype属性所指向的对象称之为原型,原型对象中有个constructor属性又指向构造函数本身。通过构造函数创建对象的过程称之为实例化,创建出来的对象称之为实例,上面的instance1,instance2就是通过FatherClass构造函数创建出来的实例,继承和实例化有什么区别呢,就像你爸妈生了你,你肯定会有些地方会和你爸妈相似,但是又不完全一样,你还可以有你自己的特性,可以说你继承了你爸妈,然后你还会生儿子,儿子又继承了你,而实例化呢,它只是new出了一个新的对象,对象上复制了原型和构造函数的属性和方法,两者还是有很大的区别的,简单的说继承是新的子类继承父,子构造函数继承父构造函数,实例化一个类,只是单纯调用 new 生成一个对象。

说到new,当new 构造函数时候函数体内发生了啥?
1.创建了一个空对象{ };
2.将这个空对象赋值给this,this ={};
3.空对象/this 的_proto_指向构造函数的原型,{}.proto= FatherClass.prototype;继承了构造函数的原型;
4在构造函数内使用this为创建出来的对象新添加成员;
5.默认返回新创建的出来对象;

补充一句:如果后面自己写了return语句,return是空值或者是基本类型都是默认返回新创建出来的对象,如果return写的是object 类型的值,将不会返回新创建的对象,而是返回object类型值(null值除外),此时创建出来的对象和构造函数原型无任何关系。

二.有哪些常见继承方式?

1.原型链继承 先看代码:
//声明父类
function FatherClass(){
this.family=['father','mother','daughter'];
}
//为父类添加共有方法
FatherClass.prototype.life =function(){
console.log('happy every day')
}
//声明子类
function ChilderClass(){};
//继承父类(父类的实例赋值给子类的原型)
ChilderClass.prototype = new FatherClass();
//实例化父类的时候,复制了父类构造函数的属性和方法,并将子类的_proto_指向了父的原型对象,这样就拥有了父类原型上的属性和方法
var instance1 = new ChilderClass();
var instance2 = new ChilderClass();
instance1.family.push('son');

console.log(instance1.family,instance2.family);

当instance1.family.push(‘son’)会影响到其他实例的family属性,这点很容易被忽视,其实原因很简单,因为父类的实例赋值给子类的原型,它会把所有的属性和方法全部复制到子类构造函数的原型,其实构造函数本身没有属性和方法,全部是原型上的,所有的实例都是共同一套属性和方式,当公用的是引用类型时候,如果一个实例改变原型上的这个属性,其他的实例都会受到影响。现在我换个代码看看,请看下面代码

function FatherClass(){
this.family=['father','mother','daughter'];
}
FatherClass.prototype.like=['music','book','fitness'];
var instance1 = new FatherClass();
instance1.family.push('son');
instance1.like.push('study')
var instance2 = new FatherClass();
console.log(instance1.family,instance1.like);  //["father", "mother", "daughter", "son"] , ["music", "book", "fitness", "study"]
console.log(instance2.family,instance2.like); //["father", "mother", "daughter"], ["music", "book", "fitness", "study"]

构造函数里面的方式或者属性是每个实例对象都复制了一次,所以它们是独立的,原型上的方法是公用的,所以只要是引用类型,一个实例对象去改变它会直接影响到其他的实例。
在实际开发中我们可以通过instanceof来检测某个对象是否是某个类的实例,或者说某个对象继承某个类,或者也可以说某个类的构造函数原型是否在该对象的原型链上,以最上面代码为例子,我们输出结果看看:

console.log(instance1 instanceof FatherClass );//true
console.log(instance1 instanceof ChilderClass);//true
console.log(ChilderClass instanceof FatherClass );//false
console.log(instance1 instanceof Object);//true

因为是ChilderClass.prototype是FatherClass 的实例,所以是ChilderClass.prototype instanceof FatherClass .
为什么防止写漏new关键字而报错,所以我们平时写代码的时候可以这样写:

function FatherClass(){
//判断在函数体内执行过程中this是否为当前这个对象(如果是说明是用new创建出来的)
if (this instanceof FatherClass) {
this.family = ['father', 'mother', 'daughter'];
} else {
return new FatherClass()
}
var instance1 = new FatherClass();

我们来试着封装一下原型链继承,顺便了解下constructor属性是啥,请看下面代码:

function FatherClass(){
this.family=['father','mother','daughter'];
}
function ChilderClass(){};
function inherit(){
ChilderClass.prototype = new FatherClass();
ChilderClass.prototype.constructor = ChilderClass;
return new ChilderClass();
}
var instance1 = inherit();
console.log(instance1.constructor == ChilderClass);//true
consoel.log(instance1.constructor == ChilderClass.prototype.constructor)//true

costructor是构造函数的原型上的属性,指向构造函数,因为原型上有这个属性,所以实例化出来的对象也具有这样属性,这就是为什么instance1.constructor == ChilderClass.prototype.constructor。

总结下原型链继承的优缺点
优点:
1.实例是子的实例也是父的实例。
2.父上面或者原型增加方法或者属性,子都能访问到。
3.简单容易实现。
缺点:
1.子类通过原型对父进行了实例化,所以继承了父类,如果父类的共有属性是引用类型,子类实例更改子类原型从父类构造函数继承下来的共有属性会直接影响到其他子类。
2.子类的继承是通过原型对父类进行实例化,在创建父类的时候无法向父类传递参数,无法对父类构造函数内的属性进行初始化。
3.无法实现多继承。

2.构造函数继承

javascript是很灵活的,自然也有其他的继承方法,比如常见的构造函数继承。

function FatherClass(name){
//引用类型共有属性
this.family=['father','mother','daughter'];
//值类型共有属性
this.name = name;
}
//父类声明原型方法
FatherClass.prototype.life =function(){
console.log('happy every day');
}
//声明子类
function ChilderClass(name,age){
//继承父类
  FatherClass.call(this,name);
  this.age=age;
}
//创建第一个实例
var instance1 = new ChilderClass('lily',18);
instance1.family.push('sister');
//创建第二个实例
var instance2 = new ChilderClass('andy',20);
console.log(instance1.family,instance1.name,instance1.age);//["father", "mother", "daughter", "sister"] "lily" 18
console.log(instance2.family,instance2.name,instance2.age);//["father", "mother", "daughter"] "andy" 20

FatherClass.call(this,name);这句是构造函数的继承的精华,由于call这个方法可以更改函数的作用环境,改变this指向,因此在子类中,调用FatherClass方法会将子类中的变量在父类中执行一遍,由于父类中是给this绑定的,所以子类也会继承父类的共有属性,这种类型继承是没有涉及到prototype,所以父类的原型不会被子类继承。并且子类构造函数可以传递参数,这样可以有自己的属性,也可以继承父类的属性,并且实例化出来的对象上的方法互相不受影响。

总结下构造函数继承的优缺点:
优点:
1.子类实例更改子类原型从父类构造函数继承下来的共有属性不会影响其他子类。
2.创建子类实例时,可以向父类传递参数。
3.可以实现多继承(call多个父类对象)。
缺点:
1.实例不是父的实例而是子的实例,所以不会继承父类原型上的方法。
2.无法实现函数复用,每个子类都有父类的副本,影响性能。

3.组合继承

所以组合继承,顾名思义把类继承和构造函数继承综合在一起,类继承是用过子类原型prototype对父类实例化来实现,构造函数继承是用过子类构造函数作用环境执行一次父类构造函数来实现的,所以组合继承就是同时做到这两点的,先看代码:

//组合继承
//声明父类
function FatherClass(name) {
//引用类型共有属性
  this.family = ['father', 'mother', 'daughter'];
//值类型共有属性
  this.name = name;
}
//父类原型方法共有方法
FatherClass.prototype.life = function () {
  console.log('happy every day');
}
//声明子类
function ChilderClass(name,age) {
//构造函数继承父类的name属性
  FatherClass.call(this, name);
//子类中新增自己的属性age
  this.age =age;
}
//类式继承 子类原型继承父类
ChilderClass.prototype = new FatherClass();
//子类的constructor指向自己
ChilderClass.prototype.constructor = ChilderClass;
//子类原型方法
ChilderClass.prototype.getAge = function () {
  return this.age;
}
var instance1 = new ChilderClass('lily',18);
instance1.family.push('sister');
console.log(instance1.family, instance1.getAge ())//["father", "mother", "daughter", "sister"] 18
var instance2 = new ChilderClass('andy',20);
console.log(instance2.family, instance2.getAge ())// ["father", "mother", "daughter"] 20

在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式,子类的实例中更改了父类继承下来的引用类型属性family,更本不会影响到其它的子类,子类实例化过程又可以将参数传递到父类的构造函数中,这样就融合了类继承和构造函数继承的优点,并且过滤掉了其缺点,但是这种组合继承实际上是操作了两次父类构造函数,我们在使用子构造函数时候执行了一次父类构造函数,在实现子类原型继承又调用了一次构造函数,因此父类构造函数执行了两次,这还不是最完美的解决方式。

上面我提到了 子类的实例中更改了父类继承下来的引用类型属性family,更本不会影响到其它的子类 为什么?这个和类继承不是一个道理吗?为什么类继承实例化出来的对象操作引用类型会影响到其他的子类?而这里不会?
其实如果你真正看理解了组合类型的特性你就不会有所疑问,上面我已经提到了组合类型在父类构造函数操作了两次,当第一次执行子类构造函数时去调用父类构造函数时,已经把父类上的属性或者方法(这里没有涉及到prototype)已经继承了,第二次子类原型继承再次调用构造函数又把这些属性和方法继承到子类原型上,此时自身和原型上都有family属性,属性查找方式是先找自身有没有,没有再去原型上找,那么实例化操作的family属性是自身的,所以其他互不受影响。

总结下组合继承的优缺点:
优点:
1.融合了原型继承和构造函数继承的优点。
2.既是子类的实例,也是父类的实例。
3.不存在引用属性共享问题。
4.可传参。
缺点:
1.调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)。

3.原型式继承

顾名思义,继承的方式肯定是在原型上,这个和原型链继承有些相似,那具体是怎么实现请看代码:

//原型式继承
function inherit(obj){
//声明一个过渡函数
function F(){};
//过渡函数的原型继承父对象
 F.prototype =obj;
//返回过渡对象的实例,该实例的原型继承父对象
 return new F();
}
var Father={
family:['father', 'mother', 'daughter']
}
var instance1= inherit(Father);
instance1.family.push('son');
var instance2= inherit(Father);
console.log(instance1.family);// ["father", "mother", "daughter", "son"]
console.log(instance2.family);// ["father", "mother", "daughter", "son"]

看出来和原型链继承的区别了吗?
原型链继承是父类的实例化对象引用给子类的原型(相当父子的原型一定要在一条原型链上),而原型式继承相当 子实例对象.proto=obj,obj如果是父类的实例对象就和原型链继承类似,obj也可以是父类的原型。有没有发现这个就是 Object.create()的原理,因为Object.create()是Es5中出来的,可能会存在兼容问题,所以可以自己封装一个:

function create(obj) {
        if (Object.create) {
        return Object.create(obj)
        } 
        else {
            function inherit(obj) {
            //声明一个过渡函数
            function F() {};
            //过渡函数的原型继承父对象
            F.prototype = obj;
            //返回过渡对象的实例,该实例的原型继承父对象
            return new F();
        }
    }
}

总结下原型继承的优缺点:
优点:
1.操作简单,相当复制一个对象。
缺点:
1.引用属性共享问题。
2.无法实现复用。

4.寄生式继承(如虎添翼)

寄生是两种生物生活在一起,一方收益,一方受害,受害生物为收益生物提供营养和居中场所。
我们这里的寄生就是上面原型式继承的过渡对象,寄生式继承和工厂模式类似,创建一个仅用于封装继承的函数,该函数在内部以某种方式来增强对象,最后返回对象。。

function inherit(obj){
//声明一个过渡函数
function F(){};
//过渡函数的原型继承父对象
F.prototype =obj;
//返回过渡对象的实例,该实例的原型继承父对象
return new F();
}
var Father={
family:['father', 'mother', 'daughter'],
name:'lily'
}
function create(obj){
var o = inherit(obj);
o.getName = function(){
//这里实例化出来对象上加上了getName属性来增加对象然后把它返回出来
console.log(this.name)
}
return o;
}
var instance = create(Father)
console.log(instance.family,instance.name)//["father", "mother", "daughter"] "lily"

总结下原型继承的优缺点:
优点:
1.相当原型对象外面套了层壳,优点参照原型式继承。
2.类似工厂函数。
3.对对象进行了增强。
缺点:
1.引用属性共享问题。
2.没涉及到原型,不能达到函数复用,导致效率变低。

5.寄生式组合继承(最常用的终极继承方式)

顾名思义我们将上面的寄生式继承和组合继承混合使用不就是寄生式组合继承了,先看寄生式继承,过渡函数只继承父类自身的属性或者方法,和父类prototype没有任何关系,那怎么才能和父类的prototype有关系?看下面代码:

function inheritPrototype(father,childer){
// 复制一份原型副本保存再变量中
var p = inherit(father.prototype);
childer.prototype = p;
//重写子类原型导致子类的constructor属性被修改
childer.prototype.constructor = childer;
}
//声明父类
function FatherClass(name){
this.family=['father','mother','daughter'];
this.name = name
}
//为父类添加共有方法
FatherClass.prototype.getName =function(){
console.log(this.name);
}
//声明子类
function ChilderClass(name,age){
//子类继承父类(没涉及到父类原型)
FatherClass.call(this,name)
this.age =age;
};
//子类继承父类原型
inheritPrototype(FatherClass,ChilderClass);
ChilderClass.prototype.getAge= function(){
console.log(this.age)
}
var instance1 = new ChilderClass('lily',18);
instance1.family.push('son');
var instance2 = new ChilderClass('andy',20);
console.log(instance1.family,instance1.age);//["father", "mother", "daughter", "son"] 18
instance1.getName();//lily
instance1.getAge();//18
console.log(instance2.family,instance2.age);//["father", "mother", "daughter"] 20
instance2.getName()//andy
instance2.getAge();//20

寄生式组合继承是通过寄生式继承来继承父类原型上的属性或者方法,利用构造函数继承来继承父类自身上的属性或者方法,完美具备这两种继承的优点,重点是修复了组合继承的问题。对象的继承,更像是函数的功能用法,如何用函数做到复用,组合。上述几个继承的方法都可以手动修复他们的缺点,但就是多了这个手动修复就变成了另一种继承模式。

6.多继承方式

一些面向对象的语言支持多继承,但是javascipt中能实现吗?能实现,但是有一些局限性,javascript主要依赖原型链实现,只有一条原型链,那怎么实现 多继承?所以理论上是不能继承多个父类的,但是javascript是灵活的,可以通过一些小技巧可以继承多个对象的属性来实现类的多继承,下面我们来先说下如何继承单对象的属性,看下面代码:

var inherit = function(target,obj){
	for(var property in obj){
		target[property] = obj[property]
	}
	return target;
}

这个相当是对单个对象的属性的一个复制过程。既然可以一个一个对象的属性复制继承,如果我们传递多个对象将如何实现多对象的属性复制继承,看下面代码:

var inherit = function(){
     target =arguments[0];
     for (var i=1;i<arguments.length;i++){
         for( var prototype in arguments[i]){
             target[prototype] = arguments[i][prototype]
          }
       }
      return target
    }
var a ={name:'lily'};
var b ={age:18};
var c = {sex:"woman"};
console.log(inherit(a,b,c))//{name: "lily", age: 18, sex: "woman"}

其实构造函数继承也可以实现多个类的继承,你可以call不同的类就能实现多个类的继承,方法很多,可以自己去思考

二.javascript多种调用方式(多态)

可以简单的理解为多态在javascript中是同一个方法的多种调用方式,多种形态显示,看一个比较简单的代码:

function Add(){
function nothing(){
    return 0;
}
function one(a){
    return a;
}
function two(a,b){
     return a+b
}
this.add = function(){
    var length =arguments.length
    switch(length){
        case 0:
        return nothing();
        case 1:
        return one(arguments[0]);
        case 2:
        return two(arguments[0],arguments[1]);
    }
}
}
var p =new Add();
console.log(p.add());//0
console.log(p.add(10));//10
console.log(p.add(10,20));//30

对于多态类,当我们调用add方法时候,它会根据传参不同调用的方法不同导致结果不同。

  • 49
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 45
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值