三、继承
面向对象语言中,继承是一个很优秀的概念。虽然js无法实现接口继承,但是因为它优秀的原型链概念,可以通过原型链来实现继承。js中实现继承的方法一共有6种,我们一一来剖析吧!
3.1原型链式继承
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
实现方法:让原型对象等于另一个类型的实例,可以层层递进,就构成了实例与原型的链条,最终形成原型链
function Father(){
this.FatherName = "曹操";
}
Father.prototype.getFatherName = function(){
return this.FatherName;
};
function Son(){
this.SonName = "曹植";
}
//继承了Father
Son.prototype = new Father();
Son.prototype.getSonName=function(){
return this.SonName;
};
var Cao = new Son();
console.log(Cao.getFatherName()); //曹操
console.log(Cao.getSonName()); //曹植
解释:通过创建Father的实例,并将该实例赋给Son.prototype,也就是说,以一个新类型的实例重写了原型对象。把原来存在于Father的实例中的所有属性和方法,现在也存在了Son.prototype中。所有给Son.prototype添加的新方法getSonName()在继承了Father的属性和方法的基础上又添加了一个新方法。
缺点:
- 父类型的共有属性如果是引用类型,就会在子类型中被所有实例共用,因此一个子类型实例更改子类原型从父类型构造函数中继承来的共有属性就会直接影响到其他子类型
function Father(){
this.FatherName = ["曹操"];
}
Son.prototype = new Father();
var Cao1 = new Son();
var Cao2 = new Son();
Cao1.FatherName.push("曹操1");
console.log(Cao1.FatherName); //曹操,曹操1
console.log(Cao2.FatherName); //曹操,曹操1
- 在创建子类型实例时,没有办法在不影响所有对象实例的情况下,向父类型的构造函数中传递参数
3.2 借用构造函数的继承
基本思想:在子类型构造函数的内部调用父类型构造函数。
实现方法:使用apply()和call()方法在新创建的对象上执行构造函数。把父类构造函数的this指向子构造函数的实例上,所以父级所有属性在子类型上都有
function Father(){
this.FatherName = ["曹操"];
}
function Son(){
// 继承自Father
Father.call(this)
}
var Cao1 = new Son();
var Cao2 = new Son();
Cao1.FatherName.push("曹操1");
console.log(Cao1.FatherName); //曹操,曹操1
console.log(Cao2.FatherName); //曹操
解释:通过使用call()方法(或者apply()),我们在(未来将要)新创建的子类型实例的环境下调用了父类型的构造函数,这样就在新子类型对象上执行父类型函数中定义的所有对象初始代码。子类型中的每个实例就会拥有自己单独的FatherName属性副本了。
优点——传递参数
相对于原型链而言,借用构造函数可在子类型的构造函数中向父类型构造函数传递参数。
function Father(FatherName){
this.FatherName = FatherName;
}
function Son(){
// 继承自Father,同时还传递了参数
Father.call(this,"曹操");
//实例属性
this.age = "18"
}
var Cao = new Son();
console.log(Cao.FatherName); //曹操
console.log(Cao.age); //18
缺点
由于这种类型的继承没有涉及到原型prototype,所以父类型的原型方法不会被子类型继承,如果想被子类型继承,就必须要放在构造函数中,这样创建出来的每个实例就会单独拥有一份而不能共用,但是这样,就违背了代码复用的原则。
3.3 组合继承
基本思想:组合继承也被称为伪经典继承,指的是将原型链和借用构造函数的技术组合在一块,从而发挥各自优点的一种继承模式。
实现方法:使用原型链实现对原型属性和方法的继承,借用构造函数来实现对实例属性的继承。
这样做的好处是,既通过在原型上定义方法实现了函数的复用,又能保证每个实例都有它自己的属性。
function Father(FatherName){
//值类型共有属性
this.FatherName = FatherName;
//引用类型共有属性
this.fathers = ["曹操","孙权"];
}
//父类原型共有方法
Father.prototype.getFatherName = function(){
return this.FatherName;
};
//声明子类型
function Son(FatherName,age){
// 构造函数式继承自Father,同时还继承了父类属性
Father.call(this,FatherName);
//子类型中新增共有属性
this.age = age;
}
//原型链式继承了Father
Son.prototype = new Father();
//子类原型方法
Son.prototype.getAge=function(){
return this.age;
};
var Cao1 = new Son("曹操1",18);
var Cao2 = new Son("曹操2",28);
Cao1.fathers.push("曹操11");
console.log(Cao1.fathers); //曹操,孙权,曹操11
console.log(Cao2.fathers); //曹操,孙权
console.log(Cao1.getFatherName()); //曹操1
console.log(Cao1.getAge()); //18
console.log(Cao2.getFatherName()); //曹操2
console.log(Cao2.getAge()); //28
解释:子类型的实例中更改了父类型继承下来的引用类型如fathers,没有影响到其他实例,并且子类型实例化过程中又将参数传递给父类型的构造函数中,如FatherName。
缺点:
这种方式在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类型原型链式继承时,又调用了一遍父类型的构造函数。因此在这里,父类型的构造函数被调用了两遍。
3.4 原型式继承
基本思想:借助原型在基于已有的对象创建新对象,同时还不必因此创建自定义类型。
实现方法:在函数内部先创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "曹操",
friends:["刘备","孙权"]
};
var person1 = object(person);
person1.name = "曹操1";
person1.friends.push("刘备1");
var person2 = object(person);
person2.name = "曹操2";
person2.friends.push("刘备2");
console.log(person.name); //曹操
console.log(person.friends); //刘备,孙权,刘备1,刘备2
console.log(person1.name); //曹操1
console.log(person1.friends); //刘备,孙权,刘备1,刘备2
console.log(person2.name); //曹操2
console.log(person2.friends); //刘备,孙权,刘备1,刘备2
3.5 寄生式继承
基本思路:是对原型继承的第二次封装,并且在第二次封装的过程中对继承的对象进行拓展。
实现方法:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象对象,最后再像真地是它做了所有工作一样返回对象。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createObject(father){
var clone = object(father);
clone.sayName = function(){
console.log("你好");
};
return clone;
}
var person = {
name: "曹操",
friends:["刘备","孙权"]
};
var person1 = createObject(person)
person1.sayName(); //你好
console.log(person.name); //曹操
console.log(person.friends); //刘备,孙权
console.log(person1.name); //曹操
console.log(person1.friends); //刘备,孙权
缺点:
与构造函数式继承类似,由于不能做到函数的复用而降低效率
3.6 寄生组合式继承
基本思想:不必为了指定子类型的原型而调用父类型的构造函数,利用父类型原型的一个副本。
实现方法:使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
这种方法被认为是最完美的继承范式。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(son,father){
var prototype = object(father.prototype); //创建对象
prototype.constructor = Son; //增强对象
Son.prototype = prototype; //指定对象
}
function Father(name){
this.name = name;
this.friends = ["曹操","刘备"];·
}
Father.prototype.sayName = function(){
return this.name;
};
function Son(name,age){
Father.call(this,name)
this.age = age;
}
inheritPrototype(Son,Father);
Son.prototype.sayAge = function(){
return this.age;
};
var Cao1 = new Son("曹操1",18);
var Cao2 = new Son("曹操2",28);
console.log(Cao1.friends); //曹操,刘备
console.log(Cao2.friends); //曹操,刘备
console.log(Cao1.sayName()); //曹操1
console.log(Cao1.sayAge()); //18
console.log(Cao2.sayName()); //曹操2
console.log(Cao2.sayAge()); //28