js 类的继承

js 类的继承用起来很方便,网上搜一个extend函数就可以直接调用,但是理解起来却不容易。

我就按我的理解来说吧,不对的地方大家一定要批评指正。

所谓继承,实际上就是子类可以调用父类内部的变量或方法。当看到“调用内部变量或方法”这几个字眼的时候我们往往会先想到call或apply,当然这是可以的,因为最基本的方法就是通过call或apply来实现继承。

可是最常用的还是通过prototype的方式来实现继承。

首先,调用一个类,首先要将其实例化,要想实现继承,那么实例化出来的对象其实就是同时具备子类与父类里面的所有变量与方法。于是这个问题可以简化成如何让实例化出来的对象可以包含多个类中的内容

实例化需要new关键字,详情见:http://web.zhaicool.net/440.html

new分三步:

1、创建空对象;
2、将类的prototype属性赋值给新创建的空对象的__proto__属性 ;
3、通过call方法让新创建的空对象能调用类里面的变量及方法;

实例化出来的对象要想同时具备两个类中的内容,肯定需要在第二步与第三步入手。

第三步好说,只要写两个call就可以,比如

function Animal(){
this.species = “动物”;
}

function Cat(name,color){
this.name = name;
this.color = color;
}

var cat1 = {};
Cat.call(cat1,”大毛”,”黑色”);
Animal.call(cat1);
alert(“这只猫的名字是”+cat1.name+”,是一只”+cat1.color+”的猫,猫都属于”+cat1.species);

能够正确输出说明实例化的cat1可以同时读取Cat跟Animal内部的变量,但是对于定义在prototype上的属性该咋办,不能直接用cat1 .__proto__ = Animal.prototype+””+Cat.prototype,因为prototype的数值类型是对象,对象不能通过简单的“+”来实现合并。

所以想到这一步,我们就可以想到,因为子类要继承父类,实例化的对象又只能对子类进行new,还要包含父类内容,那么就只能先把父类的内容赋值给子类,然后在对子类进行new。

首先,先把父类的prototype的对象值付给子类:

Cat.prototype= Animal.prototype;

代码:

function Animal(){
this.species = “动物”;
}
Animal.prototype.Eat=function(){alert(“动物都需要吃饭的”)};

function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype= Animal.prototype;
Cat.prototype.Voice=function(){alert(“猫叫起来喵喵喵的”)};

var cat1=new Cat(“大毛”,”黄色”);

alert(cat1.species );//undefined
cat1.Eat();//动物都需要吃饭的
cat1.Voice();//猫叫起来喵喵喵的

cat1.species是undefined因为Cat不能直接调用Animal里的变量,所以需要call方法调用:

function Animal(){
this.species = “动物”;
}
Animal.prototype.Eat=function(){alert(“动物都需要吃饭的”)};

function Cat(name,color){
this.name = name;
this.color = color;

Animal.call(this);
}
Cat.prototype= Animal.prototype;
Cat.prototype.Voice=function(){alert(“猫叫起来喵喵喵的”)};

var cat1=new Cat(“大毛”,”黄色”);

alert(cat1.species );//动物
cat1.Eat();//动物都需要吃饭的
cat1.Voice();//猫叫起来喵喵喵的

但是,不能这么写,因为这么写不仅需要加上call方法,而且如果直接赋值的话(Cat.prototype= Animal.prototype),那么修改子类的prototype就会同时修改掉父类的prototype,造成混乱。

如下:

function Animal(){
this.species = “动物”;
}
Animal.prototype.Eat=function(){alert(“动物都需要吃饭的”)};

function Cat(name,color){
this.name = name;
this.color = color;

Animal.call(this,Cat);
}
Cat.prototype= Animal.prototype;

Cat.prototype.Eat=function(){alert(“吃猫粮”)};
Cat.prototype.Voice=function(){alert(“猫叫起来喵喵喵的”)};

var animal=new Animal();
animal.Eat();//吃猫粮

var cat1=new Cat(“大毛”,”黄色”);

alert(cat1.species );//动物
cat1.Eat();//吃猫粮
cat1.Voice();//猫叫起来喵喵喵的

运行之后发现Animal上的Eat方法被Cat的Eat方法覆盖了。

当然我也曾想到用一个空对象中转,如:

var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = F.prototype;

但这样做也不行,因为此处的变量F是引用类型,修改Child.prototype依然会同时修改掉Parent.prototype。(此处要明白变量的值类型与引用类型的区别)

所以直接赋值是有缺陷的,但是我们可以先把Animal.prototype实例化一下,再赋值给Child.prototype,就不会造成混乱了。

Cat.prototype=new  Animal();

上代码:

function Animal(){
this.species = “动物”;
}
Animal.prototype.Eat=function(){alert(“动物都需要吃饭的”)};

function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype=new Animal();
Cat.prototype.Voice=function(){alert(“猫叫起来喵喵喵的”)};
var cat1=new Cat(“大毛”,”黄色”);

alert(cat1.species );//动物
cat1.Eat();//动物都需要吃饭的
cat1.Voice();//猫叫起来喵喵喵的

可以看出,实现了继承。而且没有通过call方法。因为new Animal()的时候已经掉用过一次call了。

但是这样做还是有不完善的地方,就要从constructor属性说起了。

下面引用阮一峰博客中的话:

每个prototype对象都有一个constructor属性,指向它的构造函数。

如果没有”Cat.prototype = new Animal();”这一行,Cat.prototype.constructor是指向Cat的;

加了这一行以后,Cat.prototype.constructor指向Animal。

alert(Cat.prototype.constructor == Animal); //true

更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。

alert(cat1.constructor == Cat.prototype.constructor); // true

因此,在运行”Cat.prototype = new Animal();”这一行之后,cat1.constructor也指向Animal!

alert(cat1.constructor == Animal); // true

这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是Cat.prototype.constructor = Cat;的意思。

这是很重要的一点,编程时务必要遵守。

其实说实话,虽然知道了会导致继承链的紊乱,但是紊乱的后果是什么我就不知道从何验证了。既然大师们都让加上Cat.prototype.constructor = Cat;这一行,那咱就加上。

于是我们可以把以上代码封装称一个集成函数:

function extend(child,parent){
child.prototype = new parent();
child.prototype.constructor=child;
}

调用的时候可以这么用:extend(Cat,Animal);就实现了继承。

但是这么写有很大一个弊端就是:不能对父类的构造函数传递参数

比如:Animal类改成如下方式,其他不变:

function Animal(name){
this.species =”动物”+ name;
}

那么我们在写extend方法的时候就要写成child.prototype = new parent(arg1,arg2,…);

不一样的父类传入的参数肯定也不一样,而且实例化的时候传入的参数也不是固定的。那么此处的 new parent该怎么传参是个问题。

还按上面的代码看:

function Animal(name){
this.species =”动物的名字叫”+ name;
}

Cat的继承就要写成:Cat.prototype = new Animal(“大毛?二毛?小花?”);

那么传入的参数该怎么写?根本不好控制,这样做耦合度太强。

以上的那个问题只是表现出来的形式,根本的问题是:如果在子类定义时就将属性赋了值,对象实例就不能再更改自己的属性了。这样就变成了类拥有属性,而不是对象拥有属性了。 

这句话理解过来就是,如果在上面的代码我们写成new Animal(“大毛?”),那么在实例化子类Cat的时候,所有的猫的实例对象都会从子类中获取一个共同的属性值:大毛。而且无法更改。如果这个属性值是父类跟子类共用的话,那更将是灾难。就像我举出的这个例子,实例化出来的猫的名字都叫大毛。这是非常不符合规矩的。

解决以上问题的办法我们首先想到的是我们定义父类的时候只定义成空类,不传任何参数,所有参数都定义在prototype上,这样做虽然也可以,但是这样做如果涉及到一些操作就会出问题,比如私有变量的定义。一个变量我只想在父类内部访问,定义在prototype就会暴露出来。

最好的办法就是用一个空对象中转。

function extend(Child, Parent) {

var F = function(){};

F.prototype = Parent.prototype;

Child.prototype = new F();

Child.prototype.constructor = Child;

Child.uber = Parent.prototype;//此处是一个自定义扩展属性,不一定非得用uber,目的是在定义子类时调用

}

调用的时候还是如此调用:extend(Cat,Animal);但是需要在Cat类中加上如此一句才行:Cat.uber.constructor.call(this);

即:function Cat(name,color){
Cat.uber.constructor.call(this);
this.name = name;
this.color = color;
}

Cat.uber在定义extend方法时被定义,等于Parent.prototype,即Parent.prototype.constructor.call(this)。通过call方法来获取父类构造函数中的对象。就可以解决掉父类传参问题。

比如Animal类需要传入一个name,那么在子类Cat里就可以这样写:Cat.uber.constructor.call(this,name);

 

未经允许不得转载:前端撸码笔记 » js 类的继承

上一篇:

下一篇: