一文带你从头到尾梳理es5继承(构造-原型-拷贝)

717

借着学习红宝书的机会,结合阮大佬的继承三部曲从头到尾总结一下继承的笔记(前端入门小白,如果有问题还请大佬指正)

继承

个人总结:继承就是将一个父对象传给多个子对象使用的方式

我们首先假设有一个对象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