继承是面向对象中一个比较核心的概念,其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而ECMAScript只支持继承,不支持接口继承,而实现继承的方式依靠原型链完成。
1.原型链实现继承
function Box(){ this.name = 'Lee'; //被继承的函数叫做超类型(父类,基类) } function Desk(){ this.age = 100; //继承的函数叫做子类型(子类,派生类) }
function Table(){
this.level = 'AAAAA';
} //通过原型链继承,超类型实例化后的对象实例,赋值给子类型的原型属性 //new Box()会将Box构造里的信息和原型里的信息都交给Desk
//Desk()的原型,得到的是Box的构造+原型里的信息 Desk.prototype = new Box(); Table.prototype = new Desk();
var desk = new Desk(); console.log(desk.name); //Lee
var table = new Table();
console.log(table.name); //Lee
console.log(table.age); //100
原型链继承流程图
注意:以上原型链继承还缺少一环,那就是Object,所有的构造函数都继承自Object。而继承Object是自动完成的,并不需要程序员手动继承。
存在的情况:
function Box(){ this.name = 'Lee'; } Box.prototype.name = 'Luck'; function Desk(){ this.age = 100; } Desk.prototype = new Box(); var box = new Box(); var desk = new Desk(); console.log(desk.name); //Lee 就近原则,实例里有就返回,没有就查找原型 //子类型从属于自己活他的超类型 console.log(desk instanceof Object); //true 自动继承Object console.log(desk instanceof Desk); //true console.log(desk instanceof Box); //true console.log(box instanceof Desk); //false
继承也有之前问题,比如字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。
为了解决引用共享和超类型无法传参的问题,采用一种叫借用构造函数的技术,或者称为对象冒充(伪造对象、经典继承)的技术来解决这两种问题。
2.对象冒充继承
//使用对象冒充继承 function Box(name,age){ this.name = name; this.age = age; } //若在此定义原型信息 Box.prototype.family = '家庭'; function Desk(name,age){ //对象冒充,对象冒充只能继承构造函数里的信息 Box.call(this,name,age); } var desk = new Desk('Lee',20); console.log(desk.name); //Lee console.log(desk.family ); //undefined 继承不到原型信息
借用构造函数虽然解决了刚才两种问题,但没有原型,复用(里面有方法必须保持独立,构造函数里的方法,放在构造里,每次实例化,都会分配一个内存地址,浪费空间,所以最好放在原型里,保证多次实例化只有一个地址)则无从谈起。所以,需要原型链+借用构造函数的模式,这种模式称为组合继承。
3.组合继承(应用广泛)
原型链+借用构造函数的模式
1 function Box(name,age){ 2 this.name = name; 3 this.age = age; 4 } 5 6 Box.prototype.run = function(){ 7 return this.name + this.age; 8 } 9 10 function Desk(name,age){ 11 Box.call(this,name,age); //对象冒充只继承构造函数里的信息 第二次调用Box 12 } 13 Desk.prototype = new Box(); //原型继承只继承原型里的信息 第一次调用Box
14 var desk = new Desk('Lee',20);
15 console.log(desk.run()); //Lee20 若没有第十三行,则继承不到,会报错
解决的问题:
(1)传参问题
(2)原型链方法的继承
(3)方法的共享
4.原型式继承
这种继承借助原型并基于已有的对象创建新对象,同时还不必因此创建自定义类型。
//临时中转函数 function obj(o){ //o表示将要传递进去的一个对象 function F(){}; //F构造是一个临时新建的对象 F.prototype = o; //将o对象实例赋值给F构造的原型对象 return new F(); //最后返回这个得到传递过来对象的对象实例 } //F.prototype = o; 其实就相当于Desk.prototype = new Box(); //这是字面量的声明方法,相当于var box = new Box(); var box = { name:'Lee', age:20, family:['哥哥'] } //box1就等于new F() var box1 = obj(box); console.log(box1.name); //Lee box1.family.push('弟弟'); var box2 = obj(box); console.log(box2.family); //哥哥,弟弟 引用类型共享了
其实原型式继承就相当于原型链继承。仅此变了一下结构。
缺点:引用类型共享了
5.寄生式继承
寄生式继承=原型式继承+工厂模式
目的是为了封装创建对象的过程。
//临时中转函数 function obj(o){ function F(){}; F.prototype = o; return new F(); } //寄生函数 function create(o){ var f = obj(o); f.run = function(){ //对F进行扩展 return this.name + '!!'; } return f; } var box = { name:'Lee', age:20, } var box1 = creat(box); console.log(box1.name); //Lee console.log(box1.run()); //Lee!!
此时,box1不仅仅拥有了box的属性和方法,还拥有了自己的run()方法。
注意:使用寄生式继承方式来为对象添加函数,由于不能达到函数复用,导致效率变低,这与构造函数模式类似。
组合式继承是JS最常用的继承模式,但也存在些问题,就是超类型在使用过程中被调用两次:一次是创建子类型的时候,另一次是在子类型构造函数的内部。
寄生组合继承解决了两次调用的问题。
6.寄生组合继承
1 //临时中转函数 2 function obj(o){ 3 function F(){}; 4 F.prototype = o; 5 return new F(); 6 } 7 //寄生函数 8 function create(box,desk){ 9 var f = obj(box.prototype); 10 f.constructor = desk; //调整原型构造指针使得desk.constructor指向自己,不加这句的话,指向Box 11 desk.prototype = f; 12 } 13 function Box(name,age){ 14 this.name = name; 15 this.age = age; 16 } 17 Box.prototype.run = function(){ 18 return this.name + this.age +'!!'; 19 } 20 function Desk(name,age){ 21 Box.call(this,name,age); //对象冒充 22 } 23 //通过寄生组合继承老师先继承 24 create(Box,Desk); 25 var desk = new Desk('Lee',20); 26 console.log(desk.run()); //Lee20!!