Javascript之对象组合继承

感悟:

最近看了一些关于Javascript对象继承的知识,发现自己之前虽然看了一些书,但是很多知识都忘了。虽然很多东西都忘了,但再次看的过程中对这些东西不会再向刚接触时那么陌生,而且理解起来也比之前顺畅和透彻多了。

充分说明:多看书是有意义的。

————————————————————————————————————————————————————————————————————————————————————————————碎碎念

关于对象之间的继承,在Javascript中主要是通过原型对象链来实现的,这一点与java这种基于类的面向对象语言有明显的不同,Javascript是基于原型的面向对象语言(大部分人说是基于对象的语言两种说法的观点不同)。

下面来具体说一下继承的实现方式:

一、组合继承

1.组合继承将原型链和借用构造函数技术组合在一起。通过使用apply或者是call借用构造函数,借用对象可以得到被借用对象的实例属性。首先来看一个栗子:

function People(){
    this.species = "人类";
}

People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
}

function Person(name, sex){
    People.apply(this,arguments);
//此处是 Person 的实例属性,当然也可以添加一些实例方法 this.name = name; this.sex = sex; } var person1 = new Person("二狗","男"); alert(person1.species);//人类 alert(person1.nationality);//undefined alert(person1.showSpecies());//Uncaught TypeError: person1.showSpecies is not a function

通过结果可以看到,person1 是构造函数 Person 的一个实例,因为构造函数 Person 使用了借用构造函数技术

 People.apply(this,arguments);

Person 就可以获得 People 的实例属性 species;但是 Person 无法获得 People 的原型属性:nationality 和原型方法:showSpecies();

如果想让 Person 获得 People 的原型属性和原型方法,需要让 Person 获得 People 的原型对象(隐式)上的属性和方法。

本质上讲:就是要重写 Person 的原型对象。

a. 一种简单实用的做法是直接将 People 的原型属性和原型方法复制给 Person:(也称之为拷贝继承)

function People(){
    this.species = "人类";
}
People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
}

function Person(name, sex){
    People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}
//遍历并复制
for(var i in People.prototype){
    Person.prototype[i] = People.prototype[i];
}

var person1 = new Person("二狗","男");
alert(person1.species);//人类
alert(person1.nationality);中国
alert(person1.showSpecies());人类

注意:上面这种复制是将 People.prototype 复制给了 Person.prototype。Person 拥有了和 People 一样的隐式原型对象。通过对原型对象上的数组进行操作可以证实:

function People(){
    this.species = "人类";
}

People.prototype.colorArray = ["red", "blue"];

function Person(name, sex){
    People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}

for(var i in People.prototype){
    Person.prototype[i] = People.prototype[i];
}

var person1 = new Person("二狗","男");
alert(person1.colorArray.push("green"));//3

var person2 = new People("二毛","男");
alert(person2.colorArray.push("black"));//4

两个不同构造函数实例化得到的对象,他们操作原型对象上的数组是同一个,说明这种原型对象上的原型属性和原型方法的复制是遵循一般的 Javascript 复制规则的。

可以对这个隐式的原型对象做一些别的操作,比如:

修改隐式对象对某个已引用方法:

function People(){
    this.species = "人类";
}
People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
};
People.prototype.cheers = function(){
    return "中国加油!";
};

function Person(name, sex){
    People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}

for(var i in People.prototype){
    Person.prototype[i] = People.prototype[i];
}

Person.prototype.cheers = function(){
    return "中国必胜!";
};

var person1 = new Person("二狗","男");
alert(person1.species);//人类
alert(person1.nationality);//中国
alert(person1.showSpecies());//人类
alert(person1.cheers());//中国必胜!

var person2 = new People("二毛","男");
alert(person2.cheers());//中国加油!

可以看到:person1 利用修改构造函数对应的原型对象中的方法,引用了一个新的方法。也可以向下面的样子先引用一个方法,再通过继承覆盖之前的引用,当然,这样做没什么意义:

function People(){
    this.species = "人类";
}
People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
};
People.prototype.cheers = function(){
    return "中国加油!";
};

function Person(name, sex){
    People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}

Person.prototype.cheers = function(){
    return "中国必胜!";
};


for(var i in People.prototype){
    Person.prototype[i] = People.prototype[i];
}


var person1 = new Person("二狗","男");
alert(person1.species);//人类
alert(person1.nationality);//中国
alert(person1.showSpecies());//人类
alert(person1.cheers());//中国加油!

var person2 = new People("二毛","男");
alert(person2.cheers());//中国加油!

b. 将 People 的实例赋值给 Person 的原型对象,同时修改 Person 原型对象的 constructor 属性值为自身。

如果只是单单将 People 的实例赋值给 Person 的原型对象, 而不修改 Person 原型对象的 constructor 属性会怎么样?

来看看修改 Person 原型对象前后 Person 构造函数的 Person.prototype.constructor 属性:

function People(){
    this.species = "人类";
}

function Person(name, sex){
    People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}
alert(Person.prototype.constructor);//Person这个完整的函数

Person.prototype = new People();
alert(Person.prototype.constructor);//People这个完整的函数

可以看到在将 People 实例赋值给 Person 前后,Person 的构造函数变了,如果这个时候什么都不做,那么再对 Person 实例化:

function People(){
    this.species = "人类";
}

function Person(name, sex){
    People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}
alert(Person.prototype.constructor);//Person这个完整的函数

Person.prototype = new People();
alert(Person.prototype.constructor);//People这个完整的函数
var person1 = new Person("二狗","男"); alert(person1.constructor);//People这个完整的函数

会发现构造函数 Person 的实例: person1 的构造函数居然不是 Person 而是 People,这显然不对。

也就是说构造函数 People 在将其实例赋值给 Person 的原型对象时,同时也将 Person 的原型对象的属性 constructor 也更换了(很明显,因为 constructor 是 prototype 的属性,皮之不存,毛将焉附?),

而 prototype.constructor 的值表示由当前构造函数对象 实例化的 函数对象的构造函数(简单点说 Person.prototype.constructor 就是告诉构造函数对象 Person 的实例,他们是谁构造的,

而 person1.constructor 让作为实例对象的 person1 直指它自己的构造函数)。——可以看出,正常情况下 Person.prototype.constructor === person1.constructor 应该成立。

function People(){
        this.species = "人类";
}

function Person(name, sex){
        People.apply(this,arguments);
        this.name = name;
        this.sex = sex;
}
alert(Person.prototype.constructor);//Person这个完整的函数

Person.prototype = new People();
Person.prototype.constructor = Person;

var person1 = new Person("二狗","男");

alert(person1.constructor === Person.prototype.constructor);//true

通过上面讲到的组合继承的两种实现方式,虽然能够实现属性和方法、原型属性和原型方法的继承,但是存在一个不足:无论在什么情况下,被继承的函数对象都会被调用两次。

先来看一个有趣的地方,假如:在组合继承中不使用 借用构造函数技术 而直接重写原型对象,会发生什么?

function People(){
    this.species = "人类";
    this.arr = [1];
}
People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
};

function Person(name, sex){
    // People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}

Person.prototype = new People();//让People的实例属性变成Person的原型属性
Person.prototype.constructor = Person;

var person1 = new Person("二狗","男");

person1.arr.push(2);//1,2
alert(person1.species);//人类
alert(person1.arr);
alert(person1.nationality);//中国
alert(person1.showSpecies());//人类

var person2 = new People("二毛","男");
person2.arr.push(3);
alert(person2.arr);//1,3
alert(person1.arr);//1,2

这样看来在构造函数 People 和 构造函数 Person 的实例对象中都能正常使用 People 的实例属性。如果是单个构造函数 Person 的多个 实例对象呢?

function People(){
    this.species = "人类";
    this.arr = [1];
}
People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
};

function Person(name, sex){
    // People.apply(this,arguments);
    this.name = name;
    this.sex = sex;
}

Person.prototype = new People();//让People的实例属性变成Person的原型属性
Person.prototype.constructor = Person;

var person1 = new Person("二狗","男");
var person3 = new Person("三狗","男");

person1.arr.push(2);
alert(person1.arr);//1,2

person3.arr.push(4);
alert(person3.arr);//1,2,4
alert(person1.arr);//1,2,4

var person2 = new People("二毛","男");
person2.arr.push(3);
alert(person2.arr);//1,3
alert(person1.arr);//1,2,4
var person4 = new People("四毛","男");
person4.arr.push(5);
alert(person4.arr);//1,5
alert(person2.arr);//1,3

注意看构造函数 People 和构造函数 Person 的实例通过相同操作后结果的差异。

构造函数 People 的实例会各自拥有自己的数组 arr, 各自的操作间是不会相互影响的,但是构造函数 Person 的实例都拥有相同的数组 arr 引用,他们操作的是同一个数组。那么实例化得到的不同对象无法正常使用各自的 arr。

所以在组合继承中,借用构造函数和原型链缺一不可。

这里的 person1.__proto__ 是帮助我们拿到 person1 的构造函数 Person 的原型对象。可以看到,上面直接将 People 的实例属性转化为 Person 的原型属性,而 People 的原型属性和原型方法也成为 Person 的原型属性和原型方法。

但是,组合继承也存在缺点,最明显的就是:无论在什么情况下,只要实例化了使用组合继承后的对象,被继承的对象都会调用两次。

function People(){
    this.species = "人类";
}
People.prototype.nationality = "中国";
People.prototype.showSpecies = function(){
    return this.species;
};

function Person(name, sex){
    People.apply(this,arguments);//第二次调用People()
    this.name = name;
    this.sex = sex;
}

Person.prototype = new People();//第一次调用People()
Person.prototype.constructor = Person;

var person1 = new Person("二狗","男");

第一次发生在将 People 的实例对象潜复制给 Person 的原型对象(可以是直接复制,也可以是通过 People 的实例 new People() );

第二次发生在调用构造函数 Person 时。

在第一次调用的时候,其实 Person 就已经拿到了 People 的实例化属性 species,而第二次调用发生在实例化 Person 的时候,通过借用构造函数,Person 又一次的调用了 People,会在 Person1 上创建实例属性 species。

总结:组合继承通过借用构造函数技术和重写原型对象,可以让一个对象继承另一个对象的属性和方法、原型属性和原型方法。在组合继承中借用构造函数技术和重写原型对象都是必要的,缺少了任何一个都会使继承不能正常工作。

组合继承也存在缺点,主要是被继承的对象的实例属性的重复调用。