JavaScript的继承(实现继承)
原型链
用 原型链 作为实现继承的方法,其基本思想是 利用原型让
一个引用类型 继承
另一个引用类型 的
属性和方法,实现方式就是让
原型对象 等于 另一个类型的实例对象。
回顾构造函数、原型对象和实例之间的关系:
每个构造函数都有个原型属性(prototype),原型属性是一个指针,指向构造函数的原型对象,原型对象默认有个构造属性(constructor),构造属性也是一个指针,指向prototype所在的构造函数,通过构造函数创建的实例对象,其有个默认的[[_proto_]]属性,此属性指向构造函数的原型对象,[[_proto_]]是实例与原型对象相关联的一个连接点。且实例对象共享了原型的属性和方法
假如,我们让原型对象 等于 另一个类型的实例呢?显然,此时的原型对象内有个指向另一个原型的指针(根据实例对象内有个 指向构造函数的原型对象的指针),相应地,另一个原型内部有个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例呢?如此层层递进,就形成了原型与实例的链条,这就是原型链的基本概念。
实现原型链的一个基本模式:
function SuperType () {
this.prototype = true;
}
SuperType.prototype.getSuperValu = function () {
return this.prototype;
};
function SubType () {
this.subprototype = false;
}
//继承了SuperType
SubType.prototype = new SuperType(); //原型对象等于另一个类型的实例 SubType的新原型
SubType.prototype.getSubValue = function () {
return this.subprototype;
};
var instance = new SubType(); //创建SubType的实例
instance.getSubValue(); //调用方法
上述例子定义了两个构造函数:SuperType和SubType,每个类型分别有一个属性和方法。SubType是继承了SuperType的,是通过创建SuperType实例继承的,将SuperType的实例赋值给了SubType.prototype(SubType.prototype变成了SuperType的实例),subType.prototype拥有了SuperType的属性和方法,那么,SubType的实例就可以使用SuperType的属性和方法。
实现的本质就是
重写原型对象,用另一个类型的实例取代。
原本存在于SuperType的实例中的属性和方法现在也存在于SubType.prototype中了,也就是说SubType的实例可以使用这些属性和方法了。有了这层关系后,我们在继承了SuperType的实例的属性和方法的基础上又添加了一个方法getSubValue。
关系图如下:
我们没有使用SubType的默认原型,而是给它新定义了一个原型,且将SuperType的实例赋值给了它,这样SubType.prototype中就有一个指向SuperType.prototype的指针。SubType.prototype中拥有了SuperType.prototype中的属性和方法,也就是说SubType继承了SuperType的属性和方法。我们又在SubType继承了SuperType的属性和方法的基础上添加了一个新的方法getSubValue。
instance中有指向SubType.prototype的指针,而SubType.prototype中有指向SuperType.prototype的指针。
SubType.prototype是SuperType的实例,SuperType中的property就存在于该实例中(因为该实例拥有了SuperType的属性)。
SubType的实例方法查找过程:1、先是查找SubType实例本身 2、再是查找SubType.prototype 3、最后查找SuperType.Prototype
默认的原型
所有引用类型都继承于Object类,这个继承也是通过原型链实现的。
所有函数的 默认原型 都是Object类的实例,因此默认原型内都包含一个指向Object.prototype的指针。这也是每个自定义类型都有toString()方法和ValueOf()方法的原因。
完整的原型链关系图为:
SuperType.prototype是Object的实例,因此其内部有个[[prototype]]属性(也就实例与原型对象之间的连接点)指向Object.prototype。
当调用instance.soString()时,实际上是调用了Object.prototype中的一个方法。
确定原型与实例的关系
确定原型与实例之间的关系,第一种就是前面讲过的用instanceOf操作符,测试实例与原型链中出现过的构造函数,结果返回true。
console.log(instance instanceOf SubType); //true
console.log(instance instanceOf SuperType); //true
console.log(instance instanceOf Object); //true
第二种方法使用isPrototypeOf()方法,只要是在原型链中出现过的原型,就是 该原型链 派生的实例 的原型。
console.log(SubType.prototype.isPrototypeOf(intance)); //true
console.log(SuperType.prototype.isPrototypeOf(intance)); //true
console.log(Object.prototype.isPrototypeOf(intance)); //true
添加原型方法与替换原型方法
我们有时需要子类型重写超类型中的某个方法或添加超类型中不存在的方法,但不管如何,都应该将这两个方法
放于替换(重写)子类型的原型之后。
function SuperType () {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
return this.prototype;
};
function SubType () {
this.subprototype = false;
}
//继承了SuperType
SubType.prototype = new SuperType(); //替换(重写)原型之后
//再添加新方法
SubType.prototype.getSubValue = function () {
return this.subprototype;
};
//再重写超类型中的方法
SubType.prototype.getSuperValue = function () {
return false;
};
//定义SubType的实例
var instance = new SubType();
//定义SuperType的实例
var instance1 = new SuperType();
//SubType的实例调用getSuperValue
console.log(instance.getSuperValue()); //false
//SuperType的实例调用getSuperValue
console.log(instance1.getSuperValue()); //true
console.log(instance.getSubValue()); //false
这里定义两个方法,第一个是为SubType添加方法,第二个方法getSuperValeu是原型中已经存在的一个方法,但重写这个方法会屏蔽原型中原来的方法。也就是说,当SubType的实例调用getSuperValue()方法时,调用的是这个重写的方法。当SuperType的实例调用getSuperValue()方法时,调用的是原来的那个方法。但不管如何,都需要在子类型的原型被替换(重写)之后再定义这两个方法,如果在没有子类型继承超类型之前就重写了getSuperValue()方法的话,那么不管是SubType的实例还是SuperType的实例调用这个重写的getSuperTypeValue()方法时,实际调用的是原来的那个没有被重写的getSuperTypeValue()方法,于是均返回"true"。
我们看下一个例子,就会明白:
function SuperType () {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
return this.prototype;
};
function SubType () {
this.subprototype = false; //在构造函数中添加的属性,是一个副本。
}
//再添加新方法
SubType.prototype.getSubValue = function () {
return this.subprototype;
};
//再重写超类型中的方法
SubType.prototype.getSuperValue = function () {
return false;
};
//继承了SuperType
SubType.prototype = new SuperType(); //替换(重写)原型
//定义SubType的实例
var instance = new SubType();
//定义SuperType的实例
var instance1 = new SuperType();
//SubType的实例调用getSuperValue
console.log(instance.getSuperValue()); //true
//SuperType的实例调用getSuperValue
console.log(instance1.getSuperValue()); //true
//SubType的实例调用getSubValue()
console.log(instance.getSubValue()); //error
从上例可以看出:如果将替换(重写)子类型的原型的语句放于定义这两个方法之后,那么不管是SubType的实例还是SuperType的实例调用这个重写的getSuperValue()方法时,实际调用的是原来的那个没有被重写的getSuperValue()方法,于是均返回"true"。因为SubType是在最后才继承SuperType的,那么SuperType的方法它也是最后才继承的,也就是原来的那个(没有被重写)的方法,所以返回的都是"true"。我们在这里还可以看到另一个现象,那就是SubType的实例调用getSubValue方法返回的是一个错误,那是因为这个方法是在SubType继承SuperType之前定义的,当SubType继承SuperType之后,SubType就只继承了SuperType的方法即getSuperValue(),所以getSubValue()这个方法对于SubType来说就是未定义的,因为它的实例调用时返回"error"。
若此时,将定义getSubValue()方法的语句放于替换(重写)原型语句之后,相当于在继承了别的方法的基础上又新添加一个方法,那么instance调用这个方法就不会出错,返回"false"。
注:subprototype属性是在SubType构造函数中定义的,相当于一个副本,所以instance.getSubValue可以访问到。如果改成是在SubType的原型中定义的,这个原型是指向另一个原型的,那么就访问不到了。如:
SubType.prototype.getSubValue = function () {
return this.subprototype;
};
function SubType () {
this.prototype.subprototype = false;
}
//SubType的实例调用getSubValue()
console.log(instance.getSubValue()); //undefined
还有一点:不能
使用字面量法创建
原型方法,这样相当于重写原型链了,就会断开实例与原型之间的联系。如:
function SuperType () {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
return this.prototype;
};
function SubType () {
this.subprototype = false;
}
//继承了SuperType
SubType.prototype = new SuperType(); //替换(重写)原型
//这段代码重写了SubType的原型链,那么上一行代码失效,SubType与SuperType失去联系。
SubType.prototype = {
getSubValue : function () {
return this.subprototype;
}
};
/*对比这段代码,这段代码是正确地定义方法的语句
SubType.prototype.getSubValue = function () {
return this.subprototype;
};
*/
//定义SubType的实例
var instance = new SubType();
console.log(instance.getSuperValue()); //返回error
原型链的问题
问题一:
原型链的问题同前面说过的原型模式的问题差不多,也是引用类型值的问题,
包含引用类型(如数组)的原型属性会被所有实例共享,这个实例当然也包含通过原型实现继承的某构造函数的原型,这个构造函数的原型对象中就有了这个引用类型,那么通过这个构造函数创建的实例都共享了这个引用类型,问题就回到了原型模式的问题了。
通过原型链的继承,
某构造函数的原型会变成
另一个类型(另一个构造函数)的实例,那么另一个构造函数的实例属性就会在某构造函数的原型中了(变成现在的原型属性了)。
function SuperType () {
this.color = ["red", "blue", "yellow"];
}
function SubType () {
}
//继承了SuperType
SubType.prototype = new SuperType();//通过继承,colors属性就会在SubType.prototype中了,变成了原型属性。那么SubType的所有实例均共享这个数组。
var instance1 = new SubType();
instance1.color.push("orange");
console.log(instance1.color); //["red", "blue", "yellow", "orange"]
var instance2 = new SubType();
console.log(instance2.color); //["red", "blue", "yellow", "orange"]
为SuperType构造函数定义了一个color属性,该属性是一个数组,SuperType的所有实例共享这个数组。当SubType继承了SuperType后,SubType.prototype变成了SuperType的一个实例,相当于SubType.prototype.color,当为SubType的实例instance1添加数组新元素时,这时SubType.prototype也即时得到反应,SubType.prototype.color也添加了新数组元素,因为实例共享原型对象中的属性,所以instance2.color也反应了出来。
这个例子跟原型模式中引用类型引起的问题相似。也是通过改变一个实例,另一个实例也会得到反应一样。
问题二:
在创建子类型的实例时,不能向超类型传递参数。
借用构造函数
解决原型中包含引用类型的值引来的问题,可以通过
借用构造函数。它的基本思想就是:
在子类型构造函数内部调用超类型构造函数。方法是使用apply()和call()方法来完成。
这是一种经典继承方式。这种继承方式没有涉及到原型链。
function SuperType () {
this.color = ["red", "blue", "yellow"];
}
function SubType () {
//SubType继承了SuperType
SuperType.call(this); //子类型内部调用超类型函数 相当于调用SuperType(),这里的是全局的。
}
//继承了SuperType
SubType.prototype = new SuperType(); //原型对象等于另一个类型的实例--继承
var instance1 = new SubType();
instance1.color.push("orange");
console.log(instance1.color); //["red", "blue", "yellow", "orange"]
var instance2 = new SubType();
console.log(instance2.color); //["red", "blue", "yellow"]
这相当于在SubType的环境中调用了SuperType,这里的全局作用下的SuperType,这样一来,每创建一个SubType实例,都会执行一次SuperType构造函数中的代码, 相当于初始化实例, 这样每个SubType实例都有了一个自己的color副本。
借用构造函数有一个优点:
在子类型构造函数中可以向 超类型构造函数 传递参数(作为新对象的额外属性)。
function SuperType (name) {
this.name = name;
}
function SubType () {
//继承了SuperType,并向SuperType传递参数
SuperType.call(this, "Tom"); //call()的第二个参数是作为 为新对象额外添加属性的对象 相当于调用SuperType("Tom")
//实例属性
this.age = 20;
}
var instance1 = new SubType();
console.log(instance1.name); //Tom
组合继承
所谓组合继承就是组合
原型链和借用构造函数这两种方式。思想就是利用
原型链实现对
原型属性和方法的继承,利用
构造函数对
实例本身属性(每个实例有自己的属性)的继承。
<!doctype html>
<html lang="zh-en">
<head>
<meta charset="utf-8">
<title>对象复习</title>
<style>
</style>
</head>
<body>
<script>
function SuperType (name) {
//实例属性(每个实例有自己的属性)每实例化一个对象,则创建一个实例副本。
this.name = name;
this.colors = ["red", "blue", "yellow"]; //引用类型引起的问题
}
//为SuperType
SuperType.prototype.getSuperValue = function () {
return this.name;
};
//原型链继承(主要用于继承原型方法)
SubType.prototype = new SuperType();
//借用构造函数继承实例属性
function SubType (name, age) {
//继承SuperType,并向超类型传递参数
SuperType.call(this, name); //==>SuperType("name");
//定义了自己的age属性
this.age = age;
}
SubType.prototype.getSubValue = function () {
return this.age;
};
var instance = new SubType("Tom", 22);
instance.colors.push("orange");
console.log(instance.colors); //["red", "blue", "yellow", "orange"]
console.log(instance.name);
console.log(instance.age);
console.log(instance.getSubValue());
var instance1 = new SubType("Bob", 25);
console.log(instance1.colors); //["red", "blue", "yellow"]
console.log(instance1.name); //Bob
console.log(instance1.age); //25
console.log(instance1.getSubValue()); //25
</script>
</body>
</html>
上例中,SupType构造函数定义了两个属性:name和color,SuperType原型定义了一个方法sayName(),将SuperType的实例赋值给SubType.prototype,实现原型方法和原型属性继承。同时使用SubType借用构造函数实现实例属性继承,又定义了自己的age属性。这样,SubType的实例对象不仅有自己各自的的两个属性--包括colors属性,还有共享的方法。
问题:
尽管组合继承是常用的继承方法,但它也有一个缺点:无论在什么情况下,都会调用两次SuperType()这个函数,第一次是替换子类型的原型时,第二次是在子类型的内部调用超类型。如下:
SubType.prototype = new SuperType(); //第一次调用SuperType函数
function SubType (name, age) {
//继承SuperType,并向超类型传递参数
SuperType.call(this, name); //==>SuperType("name"); //第二次调用SuperType函数
//定义了自己的age属性
this.age = age;
}
原型式继承
思想:原型基于已有的对象创建新对象,还不必创建自定义类型(不必创建构造函数)。
<!doctype html>
<html lang="zh-en">
<head>
<meta charset="utf-8">
<title>对象复习</title>
<style>
</style>
</head>
<body>
<script>
//原型式继承
function creates (o) {
function f () {}; //创建一个临时构造函数
f.prototype = o; //将传入的对象作为临时构造函数的原型。原型指向这个传入的参数了。
return new f(); //向外返回这个临时构造函数的实例。
}
var person = {
name : "Tom",
age : 21,
colors : ["red", "blue", "yellow"],
jon : "WEBu前端",
sayName : function () {
return this.name;
}
};
/*person1作为一个实例对象,且指向f.prototype,而f.prototype又指向person函数。
相当于实例共享了person对象的所有属性和方法。
*/
var person1 = creates(person);
person1.colors.push("orange");
console.log(person1.colors); //["red", "blue", "yellow", "orange"]
console.log(person1.name); //Tom
console.log(person1.sayName()); //Tom
var person2 = creates(person);
console.log(person2.colors); // ["red", "blue", "yellow", "orange"]
</script>
</body>
</html>
Object.create()
Object.create()方法替代了以上例子中的creates()函数。接收两个参数:第一参数作为新对象原型的对象,第二参数(可选)为新对象定义额外属性的对象。
<!doctype html>
<html lang="zh-en">
<head>
<meta charset="utf-8">
<title>对象复习</title>
<style>
</style>
</head>
<body>
<script>
var person = {
name : "Tom",
age : 21,
colors : ["red", "blue", "yellow"],
jon : "WEBu前端",
sayName : function () {
return this.name;
}
};
/*person1作为一个实例对象,且指向f.prototype,而f.prototype又指向person函数。
相当于实例共享了person对象的所有属性和方法。
*/
var person1 = Object.create(person);
person1.colors.push("orange");
console.log(person1.colors); //["red", "blue", "yellow", "orange"]
console.log(person1.name); //Tom
console.log(person1.sayName()); //Tom
var person2 = creates(person);
console.log(person2.colors); // ["red", "blue", "yellow", "orange"]
</script>
</body>
</html>
从上述两个例子可以看出:person对象中引用类型始终被共享到了原型对象f.prototype中,所创建的实例也共享了引用类型。所以这个方法不是很好。
寄生式继承
寄生式继承的思想:与寄生构造函数和工厂模式相似,创建一个仅用于
封装继承过程 的函数,该函数在内部以某种方式增强对象。
function creates (o) {
function f () {} //定义临时构造函数
f.prototype = o; //将传入的对象作为f临时构造函数的原型对象。增强函数
return new f(); //返回f的实例对象。返回新对象
}
function Jsjc (o) {
var h = creates(o); //通过调用函数创建一个实例对象
//以某种方式增强这个对象 为其添加一个方法
h.sayName = function () {
return this.name;
};
return h; //返回这个新对象
}
var person = {
name : "tom",
age : 23,
color : ["red", "blue", "yellow"]
};
var person1 = Jsjc(person);
person1.name = "Bob";
person1.color.push("orange");
console.log(person1.color); //["red", "blue", "yellow", "orange"]
console.log(person1.name); //Bob
var person2 = Object.create(person);
console.log(person2.name); //tom
console.log(person2.color); //["red", "blue", "yellow", "orange"]
object()函数不是必需的,
只要能返回新对象的函数都适用。
寄生组合式继承
思想:通过
构造函数来继承属性,通过
原型链来继承方法。我们想要的是 超类型的原型的一个副本,利用寄生模式来继承超类型的原型,然后将结果指定给子类型的原型。
这个方法解决了组合继承的问题:无论什么情况下,都要调用两次SuperType()方法。
//这一步相当于SubType.prototype = new SuperType();
function jszhjc (subType, superType) {
var pro = Object(superType.prototype); //调用构造函数创建超类型原型的副本
pro.constructor = subType; //增强对象,弥补重写原型而丢失的constructor默认的值
subType.prototype = pro; //指定对象 将结果(超类型的副本)指定给子类型的原型,这样就实现了原来继承。
}
function SuperType () {
this.name = name;
this.color = ["red", "blue", "yellow"];
}
SuperType.prototype.sayName = function () {
return this.name;
};
function SubType (name, age) {
SuperType.call(this, name); //在SubType内部调用SuperType函数(借用构造函数继承)只调用了一次SuperType()函数
this.age = age;
}
jszhjc(SubType, SuperType); //替换原型
//替换原型后再新添加方法
SubType.prototype.sayAge = function () {
return this.age;
};
var p = new SubType("tom", 21);
p.color.push("orange");
p.name = "jon"; //jon
console.log(p.name); //tom
console.log(p.color); //["red", "blue", "yellow", "orange"]
var p1 = new SubType("Bob", 30);
console.log(p1.color); //["red", "blue", "yellow"]
console.log(p1.name); //tom
这种模式高效率体现在只调用了一次SuperType函数。