感悟:
最近看了一些关于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。
总结:组合继承通过借用构造函数技术和重写原型对象,可以让一个对象继承另一个对象的属性和方法、原型属性和原型方法。在组合继承中借用构造函数技术和重写原型对象都是必要的,缺少了任何一个都会使继承不能正常工作。
组合继承也存在缺点,主要是被继承的对象的实例属性的重复调用。