《Javascript高级程序设计》读书笔记——继承与原型链
继承与原型链
原型链
在原型那一节中,讲到了用于搜索对象属性的原型搜索机制;而原型链,本质上 就是对原型搜索机制的扩充: 回想下之前的内容,我们要读取一个Person的实例p属性,会先搜索实例p;如果没有的话 就去它指向的原型上找,即Person.prototype。 如果,我们把原型指向另一种类型Monkey的实例, 例如:Person.prototype = new Monkey();
。这么做之后,如果我们创建Person的实例p,本来在Monkey类型实例上才有的属性,现在要在Person类型的实例p上也可以访问。因为,根据标识符搜索的过程:实例对象p上搜索不到所要的属性,就会搜索到p的原型( new Monkey() )。 这样一来,便相当于在Person类型的实例上拥有了Monkey类型的实例拥有的所有属性,实现了所谓的继承。
下面给出红宝书上的例子:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType(); //继承了SuperType
SubType.prototype.getSubValue = function() { //在继承的原型上添加方法
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //true
这样使用原型链来实现继承有两个明显的短处:
-
虽然上述原型指针指向的是一个实例,但既然是被原型指针所指向的,那么这个原型中的所有属性仍然是会被所有子类型的实例对象共享的。(但我们往往希望的是在继承后子类拥有父类属性 且这些继承而来的属性也是实例属性)
-
在创建子类型的实例时,不能向父类的构造函数中传递参数。
借用构造函数
借用构造函数其实是一种跟原型链没啥关系的模式。“借用”指的是在子类的构造函数内部调用父类型的构造方法;别忘了,函数只不过是在特定环境中执行代码的对象,因此使用apply()或call()方法也可以在新创建的对象上执行构造函数。如下所示 :
function SuperType() {
this.colors = ['red', 'blue', 'gray'];
}
function SubType() {
SuperType.apply(this);
}
var instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'red', 'blue', 'gray', 'yellow' ]
var instance2 = new SubType();
console.log(instance2.colors); //[ 'red', 'blue', 'gray' ]
其实就只是子类型在构造函数中将父类型的构造函数调用一遍。 通俗地说,就是子类型实例先看父类型的实例上拥有哪些属性,然后在自己身上也创建一遍;这样一来子类型实例就拥有了和父类型实例一样的属性
借用构造函数的缺陷有:
- 跟构造函数模式一样,方法都在构造函数中定义,没法复用函数对象。
- 仅仅只是调用了父类型的构造函数,所以也就只是拥有父类型实例中的属性,而父类型的原型上的属性是没有的。
组合继承
组合继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样一来,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。 例子如下:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name,age) {
SuperType.call(this, name); //继承(创建)父类的实例属性
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType("jerry", 18);
instance1.colors.push('black');
console.log(instance1.colors); //['red', 'blue', 'yellow', 'black']
instance1.sayName(); //"jerry"
instance1.sayAge(); //18
var instance2 = new SubType("Tom", 99);
console.log(instance2.colors); //['red', 'blue', 'yellow']
instance2.sayName(); //"Tom"
instance2.sayAge(); 99
红宝书原话:
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为了JavaScript中最常用的继承模式。
可见著者对这种继承方式赞誉有加.... 但如果非要说缺陷(鸡蛋里挑骨头)的话,那就是作为原型的父类型实例中的属性浪费了空间:将SubType.prototype指向了new SuperType(),所以这个原型中是存在着name和colors属性;虽然每个SubType实例对象中都创建了同名属性所以访问时将其覆盖掉了,但原型中的属性是占用空间并且没任何用处的。
原型式继承
这种方式很简单很好理解,就是基于已有的对象作为原型 来 创建新对象,同时还不必创建自定义类型。例子如下:
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "jerry",
friends: ['f1', 'f2', 'f3']
}
var one = object(person);
one.friends.push('f4');
console.log(one.friends); //f1,f2,f3,f4
var two = object(person);
two.friends.push('f5');
console.log(two.friends); //f1,f2,f3,f4,f5
ES5里通过新增Obejct.create()方法规范化了原型式继承;该方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。 第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。
var person = {
name: "jerry",
friends: ['f1', 'f2', 'f3']
}
var anotherPerson = Object.create(person, {
name: {
value: "Tom"
}
});
console.log(anotherPerson.name); //"Tom"
适用场景:
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。
寄生式继承
寄生式继承就是把原型式继承再次封装,然后在新创建的对象上扩展新方法,再把对象返回。
function object(o) {
function F(){};
F.prototype = o;
return new F();
}
function createAnother(orginal) {
var clone = object(orginal);
clone.sayHi = function() {
console.log('Hi');
}
return clone;
}
var person = {
name: "jerry",
friends: ['f1', 'f2', 'f3']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //'Hi'
寄生组合式继承
在讲组合式继承时有讲到过,这种继承方式有个小小的瑕疵:在为子类型赋值原型对象时,是需要调用一次父类型的构造函数的,因此子类型的原型对象中会有跟实例对象中同名的属性(子类型构造函数执行时会创建这些同名的实例属性)但被覆盖了。
为了解决这个小"bug",便在组合式继承的基础上,引入上节介绍的寄生式继承。
我们已经知道,这个"bug"是由于子类型的原型指向的父类型实例中的变量是多余的;那么 我们将这个子类型的原型直接赋值为父类型的原型对象不就解决了吗
通过引入的寄生式继承解决这个"bug":
function object(o) {
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
这个函数接收两个参数:子类型构造函数和父类型构造函数。在函数内部,第一部是创建父类型原型的一个副本。第二部是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
SuperType.prototype.sayNmae = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}