继承:相信很多学习过Java等面向对象语言的同学,都会接触过继承,它们实现继承的主要方式是接口继承和实现继承。但由于JavaScript函数没有签名,所以无法实现接口继承。ECMAScript支持实现继承,而且实现继承主要是依靠原型链来实现。
一、原型链继承:原型链是实现继承的主要方法,其主要思想是利用原型让一个引用类型继承另一个引用类型的属性和方法(如下面的代码)。这里先说一下构造函数、原型和实例的关系吧,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
function Father(){
this.firend = ["kobe","james","jodan"];
}
Father.prototype.sayHi = function(){
console.log("HHHHHI~~");
}
function Son(){
}
//这一句继承了Father
Son.prototype = new Father();
var son1 = new Son();
son1.sayHi(); // => HHHHHI~~
解析:上面代码中,并没有使用Son默认提供的原型,而是给它一个新的原型(即是构造函数Father的一个实例)。这样新的原型不仅具有作为Father的实例所拥有的全部属性和方法,而且其内部还有一个指针(实例原本就有一个指向构造函数的指针),指向了Fether的原型。
原型继承存在几个缺点:
1、如果用字面量的方法去给Son添加新方法,会导致继承无效(下面的sayHi方法失效了,说明继承无效)。
function Father(){
this.firend = ["kobe","james","jodan"];
}
Father.prototype.sayHi = function(){
console.log("HHHHHI~~");
}
function Son(){
}
//这一句继承了Father
Son.prototype = new Father();
//这是字面量的形式添加新方法,会导致上一行代码无效
Son.prototype = {
sayName: function(){
console.log("爹爹,我是你儿子啊,别不认我啊")
}
}
var son1 = new Son();
son1.sayName(); // => 爹爹,我是你儿子啊,别不认我啊
son1.sayHi(); // => Uncaught TypeError: son1.sayHi is not a function
2、第二个问题主要来自包含引用类型值的原型,因为这些属性会被所有实例共享。也就是说父类的实例属性会变成子类的原型属性。(例如以下代码:创建了两个Son的实例,往son1实例friend数组添加一个元素,在读取son2的friend数组时,也增加了一个元素,这明显是不合理的。)
function Father(){
this.friend = ["kobe","james","jodan"];
}
Father.prototype.sayHi = function(){
console.log("HHHHHI~~");
}
function Son(){
}
//这一句继承了Father
Son.prototype = new Father();
var son1 = new Son();
var son2 = new Son();
son1.friend.push("wade");
console.log(son1.friend); // => ["kobe","james","jodan","wade"]
console.log(son2.friend); // => ["kobe","james","jodan","wade"]
二、借用构造函数继承:在解决原型中包含引用类型值多带来的问题的过程中,开发人员开始使用一种叫借用构造函数的技术。这种方式的基本思想是在子类型的构造函数的内部调用超类型的构造函数。因为函数只是在特定类型环境中执行代码的对象,因此可以通过使用apply()或者call()方法来改变this的指向实现继承。
借用构造函数所解决的问题:
1、构造函数解决了上一篇博客中的原型链继承中的第二个问题,即因为父类型引用类型属性会被所有实例共享。也就是说父类的实例属性会变成子类的原型属性。
function Father(){
this.friend = ["kobe","james","jodan"];
}
Father.prototype.sayHi = function(){
console.log("HHHHHI~~");
}
function Son(){
//使用call来改变this指向,实现继承Father
Father.call(this);
}
var son1 = new Son();
var son2 = new Son();
son1.friend.push("wade");
console.log(son1.friend); // => ["kobe","james","jodan","wade"]
console.log(son2.friend); // => ["kobe","james","jodan"]
上诉代码的解析:通过使用call()方法,实际上是在新创建的Son实例的环境下调用了Father构造函数。这样一来,就会在新Son对象上执行Father()函数中定义的多有对象初始化代码。因此每一个son实例就都会有自己的friend属性的副本了。
2、解决了原型链继承中不能实现传递参数的缺陷,因为构造函数可以再子类型构造函数中向超类型构造函数传递参数。
function Father(name){
this.name = name;
}
function Son(name,age){
//使用call来改变this指向,实现继承Father
Father.call(this,"name");
this.age = age;
}
var son1 = new Son(“James”,22);
console.log(son1.name); // => James
console.log(son1.age); // => 22
</script>
以上代码Father只接受一个参数name,该参数直接赋值给一个属性。在Son构造函数内部调用Father构造函数是,实际上是为Son的实例设置了name属性。为了确保Father构造函数不会重写,可以再调用超类型构造函数后,再添加应该在子类型中定义的属性。
借用构造函数继承中存在的问题:
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
三、组合继承:指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。主要的思想是使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。这样,不但能通过在原型上定义方法实现了函数服用,又能够保证每个实例都有它自己的属性。举个栗子:
function Father(name){
this.name = name;
this.friend = ["kobe","james","zhouqi"]
}
Father.prototype.sayName = function(){
console.log("我是Father的儿子,我叫"+this.name);
}
function Son(name,age){
//使用call来改变this指向,实现继承Father实例属性
Father.call(this,name);
this.age = age;
}
//继承原型属性和方法
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log("今年"+this.age+"岁啊");
}
var son1 = new Son("xiaoming",22);
son1.friend.push("yaoming");
console.log(son1.friend); // => ["kobe", "james", "zhouqi", "yaoming"]
son1.sayName(); // => 我是Father的儿子,我叫xiaoming
son1.sayAge(); // => 今年22岁啊
var son2 = new Son("xiaohong",18);
console.log(son2.friend); // => ["kobe", "james", "zhouqi"]
son2.sayName(); // => 我是Father的儿子,我叫xiaohong
son2.sayAge(); // => 今年18岁啊
在上面的栗子中,Father构造函数定义了两个属性:name和friend。Father的原型定义了一个方法sayName()。Son构造函数在通过call()方法调用Father构造函数是传入了name参数。然后将Father的实例赋值给了Son原型。这样,就可以让两个不同的Son实例不但拥有了自己的属性(包括继承而来的friend属性),而且又可以使用相同的方法。
总之,组合继承避免了原型链和借用构造函数的缺陷,融合了它们之间的优点,成为了JavaScript中最常用的继承模式。
四、原型式继承:并没有使用严格意义上的构造函数。它的思想主要是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,给出如下函数:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
在object()函数的内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的新实例。本质上来说,object()对传入其中的对象执行了一次浅复制。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name:"kobe",
friend:["james","yaoming","sunyue"]
}
var per1 = object(person);
per1.name = "bozhu";
console.log(per1.name); // => bozhu
per1.friend.push("zhouqi");
console.log(per1.friend); // => ["james", "yaoming", "sunyue", "zhouqi"]
var per2 = object(person);
per2.name = "ziho";
console.log(per2.name); // => ziho
console.log(per2.friend);// => ["james", "yaoming", "sunyue", "zhouqi"]
原型式继承,要求必须有一个对象可以作为另一个对象的基础。如果有那么一个对象的话,可以把它传递给object()函数,然后再根据具体的需求对得到的对象加以修改即可。
在上面的例子中,我们把person作为另一个对象的基础,于是就把它传入到object()函数中,然后该函数就会返回一个新的对象。这个新对象是以person为原型的,所以它的原型就包含了一个基本类型值属性name,和一个引用类型值属性friend。这就意味着person.friend不仅属于person所有,而且也会呗per1和per2共享。
ES5通过新增的Object.creaate()方法规范了原型式继承。这种方法接收两个参数:一个是用作新对象原型的对象和(可选的)一个为新对象定义的额外属性对象。只传入第一个参数时,Object.create()与objec()方法的行为相同。
var person = {
name:"kobe",
friend:["james","yaoming","sunyue"]
}
var per3 = Object.create(person, {
name: {
value: "ziho"
}
});
console.log(per3.name);// => ziho
像这个例子中,在第二个参数定义的属性,都会覆盖原型对象上的同名属性。值得注意的是,包含引用类型值的属性始终都会共享相应的值。就像使用原型模式一样。
五、寄生式继承是与原型是继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
再举个栗子:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function createParasitic(original){
var clone = object(original);
clone.sayHi = function(){
alert("Hi~~");
};
return clone;
}
var person = {
name: "kobe",
friend: ["ziho","bozhu","yaoming"]
};
var per1 = createParasitic(person);
per1.sayHi(); // => Hi~~
console.log(per1.name); // => kobe
console.log(per1.friend); // => ["ziho","bozhu","yaoming"]
在这个例子中 createParasitic()函数接收了一个参数,也就是将要作为新对象基础的对象。然后把这个对象(original)传递给object()函数,将返回的结果赋值给新对象clone。然后在为clone对象添加一个新方法sayHi()。最后基于person对象创建了一个新的对象per1。新对象不仅有了person中的所有属性,还增加了一个自己的sayHi()方法。
总结,再考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必须的,任何可以返回新对象的函数都适用此模式。
六、寄生组合式继承
组合继承的缺点: 组合继承是JavaScript最常用的继承模式,不过它也有不足之处。组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部。
function Father(name){
this.name = name;
this.friend = ["kobe","james","zhouqi"]
}
Father.prototype.sayName = function(){
console.log("我是Father的儿子,我叫"+this.name);
}
function Son(name,age){
//使用call来改变this指向,实现继承Father实例属性
Father.call(this,name); //第二次调用Father
this.age = age;
}
//继承原型属性和方法
Son.prototype = new Father(); //第一次调用Father
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log("今年"+this.age+"岁啊");
}
在上面的代码中,第一次调用Father构造函数时,Son.prototype 会得到两个属性:name和friend;它们都是Father的实例属性,只不过现在位于Son的原型中。当调用Son构造函数时,又会调用一次Father构造函数,这一次又在新对象上创建了实例属性name和friend。于是这两个属性就屏蔽了原型中的两个同名属性。
寄生组合式继承,就是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其主要的思想是:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非就是超类型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
下面这个例子的inheritPrototype() 函数实现了寄生组合继承的最简单形式。
function inheritPrototype(son,father){
var prototype = object(father.prototype); //创建对象
prototype.constructor = son; //增强对象
son.prototype = prototype; //指定对象
}
这个函数接收两个参数:子类型和超类型的构造函数。在函数年内部,第一步创建了超类型原型的一个副本。第二步为创建的副本添加constructor属性,从而弥补因重写原型而失去默认constructor属性。最后是将创建的对象(即副本)赋值给子类型的原型。
function Father(name){
this.name = name;
this.frinds = ["kobe","ziho","bozhu"];
}
Father.prototype.sayName = function() {
console.log(this.name);
}
function Son(name,age){
Father.call(this,name);
this.age = age;
}
function inheritPrototype(son,father){
var prototype = object(father.prototype); //创建对象
prototype.constructor = son; //增强对象
son.prototype = prototype; //指定对象
}
inheritPrototype(Son,Father);
Son.prototype.sayAge = function(){
console.log(this.age);
}
上面这个例子的高效率体现在他只调用了一次Father构造函数,并因此避免了在Son.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。