借着学习红宝书的机会,结合阮大佬的继承三部曲从头到尾总结一下继承的笔记(前端入门小白,如果有问题还请大佬指正)
继承
个人总结:继承就是将一个父对象传给多个子对象使用的方式
我们首先假设有一个对象Cat,猫有一些属性比如名字,颜色,还有一个固定属性type
我们有很多猫,它们的名字,种类不同,但是type相同,我们该如何表示这些猫?当然为每只猫单独创建一个对象,其中包含猫的各种信息是可以的,但是非常不便于维护,这时我们根据猫的构造函数来创建这些具体的猫
//猫
function Cat(name,color){
this.name = name;
this.color = color;
this.type = "猫科动物";
}
//创建猫的子类(各种具体的猫)
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
//你会发现
cat1.type === cat2.type //false
//父类的type好像在每个子类里面都有备份诶?这样很浪费内存,明明他们都是一样的,我们让所有的子类type都指向同一个地址就好,我们改进一下
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype.type = "猫科动物";
//这样你会发现,创造猫的实例后,所有的实例type都指向同一个地址了
再进一步,我们发现type属性是硬编码的,这样很不好,type属性应该被抽象到更高一级,不如就叫Animal吧,如果Cat要使用Animal里面的属性,这样就必须使用继承了
function Animal(){
this.type = "动物";
this.say = function(){
console.log("i can say");
}
this.fly = function(){
console.log("i can fly")
}
}
function Cat(name,color){
this.name = name;
this.color = color;
}
这样我们就要考虑,如何我们创建的猫的实例如何访问到Animal里面的type属性呢?
构造函数绑定
第一种方法是使用构造函数绑定
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("大毛","黄色");
alert(cat1.type); // 动物
构造猫的实例必须通过构造函数,那我们直接在Cat的构造函数里面调用父类Animal的构造函数就可以了,这就让每一个Cat实例都有type属性了,但是Animal里面的引用类型呢?
console.log(cat1.say == cat2.say); //false
console.log(cat1.fly == cat2.fly); //false
又回到了原来的问题,每一个Cat实例里面都有say方法,也有fly方法,不对啊,每一个猫的实例里为什么都有单独的say、fly方法,如果父类的构造函数有很多不同的方法,那我们都要拿到实例里面吗? 当然不是这样,我们只需要属于猫的方法就可以了,fly当然就不是猫实例里面该有的方法!
你会发现上面创建Cat实例的时候我们就考虑过如何优化!那就是将其放在prototype中
function Animal(){
this.type = "动物";
}
Animal.prototype.say = function(){
console.log("i can say");
}
Animal.prototype.fly = function(){
console.log("i can fly");
}
其他的代码不变,我们再来看一下
console.log(cat1.say); //undefined
哦。Cat实例现在里面只有Animal构造函数里面的东西,Animal原型上的函数是访问不到的,那如何才能访问的到呢
prototype模式
如何访问Animal原型上的函数呢,对啦,我们可以,构造一个Animal实例啊,通过Animal实例就可以访问到原型上的函数了,
那这个Animal实例如何与Cat子类联系起来呢?
我们这样想,有一个Cat的实例,要访问Animal原型上的函数,它会怎么找呢?首先实例会先去Cat构造函数内找函数,找不到然后再去Cat原型上去找函数,原型上也找不到的话,就会去Object的prototype上去找(默认原型会有一个内部指针指向Object.prototype),如果我们把Animal实例赋值给Cat类的原型,当Cat实例访问Cat构造函数找不到时,就会进入Cat原型也就是Animal实例,进入Animal实例,由于构造函数没有给实例生成没有想要的方法,自然就会访问到Animal的原型!,这样就达到我们想要的目标了,我们将代码改成如下形式
function Animal(){
this.type = "动物";
}
Animal.prototype.say = function(){
console.log("i can say");
}
Animal.prototype.fly = function(){
console.log("i can fly");
}
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("大毛1","黄色1");
console.log(cat1.say); //ƒ (){console.log("i can say");}
console.log(cat1.say == cat2.say); //true
这样发现基本符合我们的要求了,中间有一行Cat.prototype.constructor = Cat是为什么呢,不加这一行其实输出结果也是一样的,但是,我们知道prototype中存储着一个指向构造函数的指针,本来Cat的原型内构造函数指针是指向Cat构造函数的(也就是说new Cat()产生的实例是通过Cat构造函数生成的)但是,将Animal实例赋值给Cat原型后,Cat.prototype.constructor也就是Animal一个实例的constructor,它是指向Animal构造函数的!额,你能说Cat实例是由Animal构造函数生成的吗,这会导致继承链的紊乱,因此我们手动改回来,让它指向Cat
组合继承
我们现在有一个需求,在父类里面加上一个colors数组,让每一个实例都可以往里面存储自己的颜色!做法如下
function Animal(){
this.type = "动物";
this.colors = [];
}
Animal.prototype.say = function(){
console.log("i can say");
}
Animal.prototype.fly = function(){
console.log("i can fly");
}
function Cat(name){
//Animal.call(this);
this.name = name;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛");
var cat2 = new Cat("大毛1");
cat1.colors.push("grey");
cat2.colors.push("white");
console.log(cat1.colors); //["grey","white"]
!发现问题了,父类构造函数里面的属性会通过父类实例赋值给子类的原型,所有Cat实例是共享Cat原型里面的colors的,我们想到刚刚不是使用构造函数里面绑定了父类里面的属性吗,在这里使用prototype继承基础上,我们可以再加上构造函数绑定(在上面的代码中去掉Cat构造函数中构造函数的注释),这样就把Animal构造函数内的colors拿到了Cat构造函数中,每个Cat实例就有自己的colors属性了,这就是组合继承
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象
注意,组合继承调用了两次父类的构造函数,第一次在new Animal的时候,这时colors和type属性在子类原型中,第二次是new Cat的时候调用父类构造函数,把type与colors属性拿到子类构造函数中,相当于覆盖了原型中的这两个属性
利用空对象作为中介
如果父类的所有属性方法都在原型上,我们使用prototype模式实现继承时都会创建父类的实例,这是比较耗费内存的,这时我们可以将父类原型赋值给一个空对象的原型,再将空对象的实例赋给子类原型就可以了,这样每次创建的都是空对象的实例,基本不占内存
function Animal(){
}
Animal.prototype.say = function(){
console.log("i can say");
}
Animal.prototype.fly = function(){
console.log("i can fly");
}
function Cat(name){
this.name = name;
}
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛");
cat1.say(); //"i can say"
有一点不好的地方在于,如果Animal构造函数中有组合继承那样的需求,就无法用该方法
寄生组合式继承
上面组合继承会调用两次父类构造函数,那么有没有什么方式减少这个调用的次数呢,首先根据组合继承的代码例子,如果需要父类的colors属性,那么构造函数绑定是无法避免的要调用父类构造函数。另外还有一次调用父类构造函数是为了访问父类原型,这一次需不需要父类的实例呢?答案是:其实可以不需要!
那我们如何访问到父类的原型呢
我们把父类原型拷贝一份然后赋值给子类原型不就可以了吗,然后改一下子类的constructor属性让它指向自身好
function Animal(){
this.type = "动物";
this.colors = [];
}
Animal.prototype.say = function(){
console.log("i can say");
}
Animal.prototype.fly = function(){
console.log("i can fly");
}
function Cat(name){
Animal.call(this);
this.name = name;
}
Cat.prototype = Object.create(Animal.prototype); //创造一个原型是Animal原型的新对象
Cat.prototype.constructor = Cat
var cat1 = new Cat("大毛");
var cat2 = new Cat("大毛1");
cat1.colors.push("grey");
cat2.colors.push("white");
console.log(cat1.colors); //["grey"]
这种方法在YUI的 YAHOO.lang.extend()中使用,并且只调用了一次构造函数,避免了在Cat的原型上创建不必要的属性
非构造函数的继承
最后我们介绍非构造函数的继承
还是使用猫和动物的例子,假设他们是两个对象,而且不是构造函数的形式
var cat = {
name: 'cat1',
color: 'blue'
}
var Animal = {
type: '猫科'
}
我们如何让Cat这个对象去继承Animal对象呢
我们想到之前的利用空对象作为中介,Animal对象内部无非是一些属性和方法,我们可以将其作为一个空构造函数的原型!
var cat = {
name: 'cat1',
color: 'blue'
}
var Animal = {
type: '猫科'
}
function F() {
}
F.prototype = Animal
var cat1 = new F();
cat1.name = cat.name
cat1.color = cat.color
console.log(cat1.type) //'猫科'
事实上还是利用了prototype的方式来实现继承,只是这次父类与子类是具体的对象而不是构造函数了
拷贝
除了以上的使用原型继承,我们也可以将父类对象直接复制一份到子类对象,这样子类对象也有父类对象的属性和方法了
var cat = {
name: 'cat1',
color: 'blue'
}
var Animal = {
type: '猫科',
color: ["white","grey"]
}
//拷贝函数
function simpleCopy(parent){
var copy = {}
for (let i in parent){
copy[i] = parent[i];
}
return copy
}
//使用
var cat1 = simpleCopy(Animal);
cat1.name = cat.name
cat1.color = cat.color
但是这样有一个问题,我们知道对象里面的引用类型(数组、函数、对象),存储的是指向引用类型地址的指针,也就是说,上面使用simpleCopy函数生成的对象,里面的color数组都是同一个!我们修改一下该函数,让其递归的复制引用类型里面的所有值
function deepCopy(parent,child){
var child = {}
for (let i in parent){
if(typeof parent[i] === 'object'){
child[i] = (typeof p[i] === Object) ? {}: [] ; //引用类型判断是对象还是数组
deepCopy(parent[i],child[i]);
}else{
child[i] = parent[i];
}
}
return child
}
这样就会遍历拷贝对象里面的所有属性了
看到阮一峰文章下面的评论里面有一条很有意思,如何不调用构造函数,从而把构造函数里面的属性继承
function A(){ alert('执行了'); this.name = 'zzz'; } function B() { } ... //如何让B的实例可以访问到A的name属性而且不alert
暂时想不到什么好的方法。。先留个坑吧
5