2019独角兽企业重金招聘Python工程师标准>>>
寄生组合式继承
前言:
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
既然要学习最理想的继承,那就要知道,不理想的继承是什么样子(反模式)和继承的发展过程。
先决条件:
阅读、理解、掌握此篇文章所涉及的知识点---《类式继承》
起步:构造函数继承( 借用构造函数 constructor stealing)
别称:伪造对象或经典继承
构造函数继承基本思想:在子类构造函数的内部调用超累构造函数
构造函数继承的重点:SuperClass.call(this, [arguments]);
首先复习一下call与apply:
MDN地址:call apply
call()
方法调用一个函数, 其具有一个指定的this
值和分别地提供的参数(参数的列表)。apply()
方法调用一个具有给定this
值的函数,以及作为一个数组(或类似数组对象)提供的参数。- 区别:call()方法的作用和 apply() 方法类似,区别就是
call()
方法接受的是参数列表,而apply()
方法接受的是一个参数数组。 - 作用:在调用一个存在的函数时,你可以为其指定一个
this
对象。this
指当前对象,也就是正在调用这个函数的对象。apply
与call()
非常相似,不同之处在于提供参数的方式。apply
使用参数数组而不是一组参数列表。apply
可以使用数组字面量(array literal),如fun.apply(this, ['eat', 'bananas'])
,或数组对象, 如fun.apply(this, new Array('eat', 'bananas'))
。
实现代码:
//声明父类
function SuperClass(id) {
//引用类型共有属性
this.books = ['js', 'html', 'css'];
this.id = id;
}
//增加父类公共方法
//为父类原型对象上添加一个获取值的方法
SuperClass.prototype.showBooks = function () {
console.log(this.books)
}
//声明子类
function SubClass(id) {
//继承父类--在子类构造函数的内部调用超累构造函数
SuperClass.call(this, id);
}
//实例化
var instance1 = new SubClass(1);
console.log(instance1)
var instance2 = new SubClass();
console.log(instance1 instanceof SuperClass) //false 因未没有从原型上继承
//所以
console.log(instance1.books) //['js','html','css']
instance1.books.push("设计");
console.log(instance1.books) //['js','html','css','设计']
console.log(instance2.books) //['js','html','css']
为什么这样SubClass就有了SuperClass的共有属性了呢?
这又不得不说new关键字的作用了,有new关键字调用的构造函数相当于对当前对象的this不断赋值,而没有使用new关键字,直接调用构造函数会将this指向全局对象(页面中是window,你可以试验一下,无new关键字调用构造函数),构造函数内的共有属性,也就是带 this. 的(注意构造函数中的this关键字,它就代表了新创建的实例对象)会赋值到全局对象上。
而使用SuperClass.call(this, id)则将当前调用对象指向SubClass函数(-->未来的实例<--),SuperClass的共有属性也会赋值到SubClass函数(-->未来的实例<--)。所以,子类继承了父类的共有属性。
可以粗暴的记忆为:call或apply 使得 SubClass函数内执行了SuperClass()函数内定义的所有对象初始化代码!
注意:因为这种类型的继承没有涉及到原型链(prototype),所以父类的原型方法不会被子类继承,如果想让子类继承,必须放在父类构造函数内以共有方法的形式继承。
优点:可以在子类中向父类传递参数, 如:SuperClass.call(this, id);
缺点:
- 每个实例都会单独拥有一份不能共用的方法和变量,违背了代码复用的原则。
- 无法继承父类的原型。
- 在超类的原型中定义的方法,对子类型而言也是不可见的
总结:构造函数继承把所有的属性和方法都为每个实例单独拷贝了一份,虽然实现了实例之间的数据隔离,但是对于那些本来就应该是公共的属性和方法来说,重复而无意义的复制也无疑是增加了额外的内存开销。而构造函数继承的另一个称呼借用构造函数表明了此种继承方式的真相——子类借用了超类(父类)的构造函数,
进阶:组合继承 (combination inheritance)
别称:伪经典继承
先决条件:上面已经说了构造函数(借用构造函数)继承的缺点了,下面看一下类式继承的缺点。
类式继承两大缺点:
- 引用类型值的误修改--原型属性中的引用类型属性会被所有实例共享-子类实例更改从父类原型继承来的引用类型共有属性会影响其他子类。
- 无法传递参数--由于子类的继承是靠其prototype对父类的实例化实现的,所以无法传递参数。
可以看出,构造函数继承的优点(可传参,父类共有属性实例单独拥有)弥补了类式继承的缺点,类式继承通过原型链继承的方式弥补了构造函数继承缺点(无法继承父类原型)。
组合继承实质:将原型链和构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。背后思路:
- 原型链实现对原型属性和方法的继承
- 借用构造函数来实现对实例属性的继承
实现代码:
/**
组合式继承
*/
//声明父类
function SuperClass(name) {
//值类型共有属性
this.name = name;
//引用类型共有属性
this.books = ['js', 'html', 'css'];
}
//增加父类公共方法
//为父类原型对象上添加一个获取值的方法
SuperClass.prototype.getName = function () {
console.log(this.name)
}
//声明子类
function SubClass(name, time) {
/*继承父类 */
//构造函数式继承父类实例属性--并传参name
SuperClass.call(this, name);//-----------------缺点是构造函数调用了两次(第二次)
//子类中新增共有属性
this.time = time;
}
//类式继承-子类原型继承父类
SubClass.prototype = new SuperClass();//-----------------缺点是构造函数调用了两次(第一次)
SubClass.prototype.constructor = SubClass;//将构造函数属性指回自己
//添加子类原型方法
SubClass.prototype.getTime = function () {
console.log(this.time)
}
//实例化
var instance1 = new SubClass('666');
console.log(instance1.__proto__)
var instance2 = new SubClass('deqiu');
console.log(instance1 instanceof SuperClass) //true 原型上继承了
//所以
console.log(instance1.books) //['js','html','css']
instance1.books.push("设计");
console.log(instance1.books) //['js','html','css','设计']
console.log(instance2.books) //['js','html','css'] //
重点:在子类构造函数中调用执行父类构造函数,在子类原型上实例化父类就是组合继承继承模式
关键代码如下:
//类式继承-子类原型继承父类
SubClass.prototype=new SuperClass();
缺点:父类构造函数的重复调用执行,会造成浪费,如果父类构造函数共有属性极多,会导致运行速度减慢。
一共执行了两次:
- 第一次:实现子类原型的类式继承时,调用了一次父类构造函数以获取实例。
- 第二次:子类构造函数内部调用父类构造函数时。
总结:组合继承避免了原型链和构造函数(借用构造函数)继承的缺陷,融合了它们优点,称为JavaScript中最常用的继承模式。
重要的插曲:原型式继承
前言:原型式继承是后续继承的基础知识点,所以作为重要的插曲穿插进来了。
一个人:
道格拉斯·克罗克福德(Douglas Crockford)世界著名前端大师、JSON的创立者.是JavaScript开发社区最知名的权威人士,JSON、JSLint、JSMin和ADSafe之父,《JavaScript: The Good Parts》(中文书名为《JavaScript语言精粹》)一书作者。
来自《JavaScript高级程序设计》一书中的引用:
道格拉斯·克罗克福德在2006年写了一篇文章,题为Prototypal Inheritance in JavaScript(JavaScript中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数。
function object(o) {
function F() {};//临时构造函数
F.prototype = o;//传入对象o作为临时构造函数的原型对象
return new F();//返回临时构造对象实例
}
注意:ECMAScript5通过新增Object.create()方法规范了原型式继承。这个方法接受两个参数:
- 用作新对象原型的对象;
- (可选的)一个为新对象定义额外属性的对象;
在传入一个参数的情况下,Object.create()和object()方法的行为相同。
总结:object()对传入其中的对象执行了一次浅复制,以传入的对象作为返回临时构造对象的原型。
增强:寄生式继承(parasitic)
前言:寄生式继承的思路是创建一个封装基础过程的函数,该函数内部以某种方式来增强对象,最后在想真地是它做了所有工作一样返回对象。
注意:此示例代码中使用 Object.create() 替换了上述的object()方法。
实现代码:
function createBook(obj) {
//通过原型继承方式创建新对象
var o = Object.create(obj);
//增强对象
o.getName = function () {
console.log(this.name);
}
return o;
}
var book = {
name: "js book",
alikeBook: ['css', 'html', 'nodejs']
};
var instance = createBook(book);
instance.alikeBook.push("666");
instance.getName();//"js book"
帮助理解的实际应用:
var arr = ['数组就不是对象了啊?', 'JS里一切皆对象', '第三句是废话', '其实第四句也是'];
var newArr = Object.create(arr);
//增强对象
newArr.getLast = function () {
console.log(this[this.length - 1]);
}
newArr.getLast();//其实第四句也是
console.log(newArr);
console.log(newArr.__proto__)// ["数组就不是对象了啊?", "JS里一切皆对象", "第三句是废话", "其实第四句也是"]0: "数组就不是对象了啊?"1: "JS里一切皆对象"2: "第三句是废话"3: "其实第四句也是"length: 4__proto__: Array(0)
注意:只对当前对象进行了增强,其他数组对象不受影响。
缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用儿降低效率。
总结:寄生式继承其实和原型式继承的实现有些相似,不过寄生式继承在原型式继承的基础上添加了在创建实例的函数中以某种形式来增强对象,最后返回对象。
最终的BOSS战:寄生组合式继承
先决条件:务必掌握组合继承的全部知识点!因为以下所述都是基于组合继承知识进行展开的(寄生式继承其实也要掌握)。
前言:寄生组合式继承,实际是通过借用构造函数来继承属性,通过原型链形式来继承方法。
实质:不必为了指定子类的原型而调用超类构造函数,我们只需超类的原型副本即可——使用寄生式继承来继承超类原型,然后将结果(实例)指定给子类原型。
BOSS战一阶段:inheritPrototype函数
inheritPrototype函数即是实现寄生式继承的方法:
/**
* subType- 子类型构造函数
* superType-超类型构造函数
*/
function inheritPrototype(subType, superType) {
//1.创建了超类(父类)原型的(副本)浅复制
var prototype = Object.create(superType.prototype);
/*
2.修正子类原型的构造函数属性
constructor属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数
prototype.constructor 未修改前指向的 superType,为了弥补因重写原型而失去的默认constructor属性。
*/
prototype.constructor = subType;
// 3.将子类的原型替换为超类(父类)原型的(副本)浅复制
subType.prototype = prototype;
}
BOSS战二阶段:使用inheritPrototype函数
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
alert(this.name);
};
function SubType(name, age) {
//构造函数式继承--子类构造函数中执行父类构造函数
SuperType.call(this, name);
this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
alert(this.age);
}
var instance = new SubType("lichonglou");
console.log(instance.name)
// console.log(instance.constructor)//指向SubType 如果没有修正原型的构造函数,则会指向父类构造函数
BOSS战终章:这个例子展示了寄生式组合继承的完整使用,且其高效率体现在它只调用了一次超类(父类)的构造,并且避免了在子类prototype上面创建不必要,多余的属性。
总结
整篇文章都是基于我学习《JavaScript高级程序设计(第3版)》第6章面向对象的程序设计总结提炼的。篇幅可能过于长,且照本宣科,但是整篇文章写下来,梳理了继承的发展过、知识点加深了理解。而且还对继承中涉及到了其他方面的知识点进行了复习,对于我自己帮助还是很大的。至于你能收获多少,就看你理解能力以及细致程度了。
参考资料:
《JavaScript高级程序设计(第3版)》(第6章面向对象的程序设计)