写在前面
如果对继承还没有了解,或者了解不深的同学,希望本文能够对你有所帮助,在阅读过程中有任何疑问或者意见,欢迎留言交流
父类函数约定
// 先约定好一个超类,即我们要去继承的父类
// 虽然没有强行规定,但是默认约定构造函数名采用大驼峰命名规则
function Super (firstName) {
// 定一个实例属性
this.firstName = firstName;
}
// 写一个原型属性,方便后面讲解
Super.prototype.lastName = function (lastName) {
return lastName;
}
/*
为什么上面的原型属性lastName不写成如下方式
Super.prototype = {
lastName: function (lastName) {
return lastName;
}
}
这两种写法有什么不一样? 可以先思考一下
*/
复制代码
使用 call || apply 方法继承
function Sub (firstName) {
Super.call(this, firstName);
}
var aSub = new Sub('张');
console.log(aSub.firstName); // 赵
console.log(aSub.lastName('三')); //Uncaught TypeError: aSub.lastName is not a function
复制代码
为什么会报错
可以看到在输出aSub.lastName('三')的时候,程序报错了,先不慌,我们先打印一下aSub看看包含了什么
实例 aSub 的构造函数是谁?
console.log(aSub instanceof Sub) // true;
console.log(aSub instanceof Super) // false;
复制代码
还是 call 方法,他并没有牵涉到原型指向的改变,所以对于使用call方法来说,并不会改变实例对象的指向的; 该谁 new 出来的谁就领走
吃瓜群众小明: instanceof是什么鬼?
吃瓜群众大黄:instanceof运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上
吃瓜群众小明:说人话
吃瓜群众大黄:判断孩子是不是他爹的
吃瓜群众小明: 这...
是否能多继承
肯定是能实现多继承的,我说了不算,看下面代码
// 再添加一个父类
function Super2 (lastName){
this.lastName = lastName;
}
// ok 看下面子类函数
function Sub (firstName, lastName) {
Super.call(this, firstName);
Super2.call(this, lastName);
}
var bSub = new Sub('张', '三');
console.log(bSub.firstName,bSub.lastName); // 张三
复制代码
好了好了,代码已经告诉我们了,call 是可以实现多继承的,但是注意一点,如果多个父类函数存在同样的实例属性,那么后面继承的父类中的实例属性会覆盖前面继承的父类中的实例属性;如上例中的Super2会覆盖Super中的实例属性,前提是实例属性名字一样,不一样就各自安好
吃瓜群众小明: 什么是多继承?
吃瓜群众大黄:一个子类函数可以继承多个父类函数
吃瓜群众小明:说人话
吃瓜群众大黄:一个单纯的女孩子能不能有多个干爹
吃瓜群众小明: 这...
父类中的实例属性对于所有子类来说是否是独立的
吃瓜群众小明: 能解释一下上面的题目啥意思不?
吃瓜群众大黄:这不够清楚?
吃瓜群众小明:能说人话不?
吃瓜群众大黄:爸爸有好几个孩子,要给孩子一百钱用,嗯,话没说清楚,这一百块钱是多个孩子共一百,还是每个孩子都有一百呢?
吃瓜群众小明: 这...
算了,还是看代码吧
// 我们为父类添加一个数组吧
function Super(firstName){
this.firstName = firstName;
this.like = [];
}
function SubA (firstName){
Super.call(this);
}
function SubB (firstName){
Super.call(this);
}
var aSubA = new SubA();
var aSubB = new SubB();
aSubA.like.push('music');
console.log(aSubA.like, aSubB.like) // ['music'] []
复制代码
其实说到底还是call方法,他将父类的实例属性拷贝到了自己这里,这就是他自己的了,私有的了,不和他人共用
总结
综上所述,call 之类方法继承存在以下特点:
优点:
- 简单,地球人都会写
- 可以实现多继承
- 可以在调用的时候自由传参(路人甲:这算什么优点? 路人乙:滚,哎)
缺点:
- 不能继承原型上的属性
中性:
- 子类继承而来的实例属性都是独立的
- 实例对象指向的构造函数没有发生改变,谁new的还是指向谁,其实主要是原型指向没有发现改变
原型继承
function Sub(firstName, lastName) {}
Sub.prototype = new Super();
var aSub = new Sub('张');
console.log(aSub.firstName) // undefined
复制代码
可以看到,在实例化的时候穿的参数好像并没有起作用,那换种写法吧
function Sub(firstName, lastName) {}
Sub.prototype = new Super('张');
var aSub = new Sub();
console.log(aSub.firstName) // 张
复制代码
这下有用了,说明原型继承在子类实例化的时候传参并不能起作用,而是在父类实例化赋值给子类原型的时候才有作用,接着往下看,这里就体现了自由传参是多么幸福的一件事
function Super (firstName, lastName) {
this.firstName = firstName;
this.like = [];
}
function aSub(firstName, lastName) {}
function bSub(firstName, lastName) {}
aSub.prototype = new Super();
bSub.prototype = new Super();
var aSubA = new aSub();
var bSubB = new aSub();
aSubA.like.push('music');
console.log(aSubA.like, bSubB.like) // ["music"] ["music"]
复制代码
可以看到我们只是在aSubA的like属性添加了music,但是在bSubB.like中也有,这说明了他们是共用一个属性的,再往下看
console.log(aSubA instanceof Super) // true
console.log(aSubA instanceof aSub) // true
复制代码
ok,这可以知道实例对象的构造函数指向是不明确的,主要的原因是因为原型继承的时候,aSub.prototype 发生了改变,指向了Super,我们打印一下aSubA
很明显,aSubA在原型链中查找aSub的时候,首先查找到了自己的构造函数aSub,那么aSubA instanceof aSub 肯定是成立的
然后aSubA在原型链中查找Super的时候,会通过原型链继续向上查找aSub.__proto__发现指向了Super,所以aSubA instanceof Super 也是存在的,这就是为什么上面两个判断都是true
所以在这里给个小建议,每次改变构造函数原型的时候,当然是直接赋值给构造函数原型的时候,建议手动修改constructor属性的指向,修改为他自己的构造函数;这样会在某些时刻不会因为constructor指向而产生不必要的麻烦;
比如上面 增加一条 aSubA.constructor = aSub;
还记得文章开头的问题吗?
Super.prototype.lastName = function () {} 与
Super.prototype = {lastName:function () {}}的区别;
Super.prototype.lastName 是在Super.prototype对象增加一个属性;
而Super.prototype = {}这种方式是重写了Super.prototype对象
我们重写的Super.prototype对象是没有constructor的属性的,这里也建议加上constructor属性,且添加值为该构造函数;当然,具体的根据实际情况而定
原型继承的特点
优点
- 简单快速高效
- 继承了父类中所有的属性,包括原型属性和实例属性
缺点
- 不能自由传参,只能在父类函数构造赋值给子类函数原型的时候才能传参
- 不能实现多继承,因为后面赋值覆盖前面的赋值
中性
- 所有子类函数公用一套属性
- 由于改变了原型指向,导致了实例对象指向的构造函数不明
组合继承
简单说就是call继承和原型继承的结合,看实例
function Sub(firstName, lastName){
Super.call(this,firstName, lastName) // 第二次
}
Sub.prototype = new Super(); // 第一次
var aSub = new Sub('张');
console.log(aSub.firstName, aSub.lastName('三')); // 张 三
复制代码
当然既然是两者的结合,肯定继承了两个的所有优点,但是却带来了另一个问题,那就是实例化属性会重复赋两遍值,当然会面的会覆盖前面的,如上函数,第一次是在new Super()实例化并赋值的时候,会将所有的实例属性给到Sub,第二次是Sub实例化的时候,会调用call方法重新将实例属性在赋一遍,当然这问题不大,只是性能上来说多余了,而这也是当前使用最多的继承方式
组合继承的特点
优点
- 继承了call与原型继承的所有优点
缺点
- 实例属性重复赋值,即赋值了两遍
中性
- 子类继承而来的实例属性都是独立的
- 由于改变了原型指向,导致了实例对象指向的构造函数不明
实例继承
先看代码
function Sub(firstName, lastName) {
var backFun = new Super(firstName, lastName);
return backFun;
}
var aSub = new Sub();
复制代码
其实这里就是取了一个巧,子类函数里面,实例化父类函数,并将父类函数返回去; 所以肯定父类所有的所有属性他都会有
对于实例继承来说,不管是将Sub作为一个构造函数还是普通函数,如果不添加新的属性,是毫无区别的,因为最终执行的都会是父类的构造函数new Super; new Sub() === new Super();
如果不需要改变父类实例属性的值,你甚至都可以直接这样写
function Sub(firstName, lastName) {
return new Super(firstName, lastName);
}
var aSub = new Sub() || Sub();
复制代码
实例继承的特点
优点
- 书写简单,容易理解
- 完美的继承父类所有的属性
缺点
- 无法完成多继承,主要因为return 只能返回一个值,除非你是用对象的方式来写,但是这样可读性就差了,而且继承而来的属性需要处理
特点
- 父类的就是子类的,当然这里的子类实例对象是不属于子类构造函数的,因为其实执行子类构造函数就是执行了父类构造函数
可以动手试试 aSub instanceof Sub 和 aSub instanceof Super
对象冒充继承
还是看代码
function Sub(firstName, lastName) {
this.methods = Super; // 将函数复制给实例属性methods
this.methods(firstName, lastName); // 执行函数
delete this.methods; // 删除临时创建多余的实例属性
}
var aSub = new Sub('张') ;
console.log(aSub.firstName)
复制代码
this.methods()执行的时候,methods里面的this指向的是Sub,所以js会将Super中的所有this下面的属性全部提取到Sub对象中,可能我猜这也是为什么这种继承被称为冒充继承的原因了,将父类作为自己的一个实例属性,在执行的过程中,将父类的属性冒充成自己的属性,嗯,这非常流氓0.0,不信,那我们打印一下aSub吧
这之前我们做一个小小的改变,是为了看的更清楚
function Sub(firstName, lastName) {
this.methods = Super; // 将函数复制给实例属性methods
this.methods(firstName, lastName); // 执行函数
}
var aSub = new Sub() ;
console.log(aSub)
复制代码
看到没有,在methods属性中没有任何任何属性,而属性firstName却跑到了Sub对象下面
冒充继承的特点和call方法的特点差不多,可以参考call继承特点
寄生组合继承
看名字就知道这种方式实现起来可能会有点繁杂了,其实只是组合继承的一种变异而已,主要是为了解决父类实例属性多次赋值的问题,好了直接看代码吧
function Sub(firstName){
Super.call(this, firstName)
}
function methods() {
/*
主要步骤 为什么写成一个函数?主要是体现这一步的重要性,
还有就是method方法只会在这里使用,没必要暴露出去
*/
var method = function () {};
method.prototype = Super.prototype;
Sub.prototype = new method();
Sub.prototype.contructor = Sub;
}
methods();
var aSub = new Sub();
复制代码
在methods方法中,主要是利用了原型链查找的特点,我们先定一个空的函数表达式method;然后将他的原型指向父类的原型,最后将子类的原型指向空函数method的实例化;
看这一步
function methods() { // 主要步骤
var method = function () {};
method.prototype = Super.prototype;
Sub.prototype = new method();
Sub.prototype.contructor = Sub;
}
methods();
复制代码
这一步能简化为 Sub.prototype = Super.prototype吗? 这一步的目的不就是为了让sub能够与继承Super的原型属性吗?
道理是这个道理,如果不考虑原型属性共用的话,完全可以直接Sub.prototype = Super.prototype,但是,这样写了,原型属性就全部共用了,子类A改变了了原型属性值 的话,那么子类B也会发生改变,因为都是共用的父类的原型属性,
如果不想共用原型属性的话,可以像如上处理,因为Sub.prototype = new method() 这一步,不同的子类都创建了一个新的new method(); 这里可以想想一下函数的执行,每次执行都会创建一个新的OA,所以肯定都是相互独立的,不会互相影响
Object.create大家认识一下,这个方法实现很简单,看下面代码
Object.create = function (objProto) {
var Fun = function() {};
Fun.prototype = objProto;
return new Fun();
}
复制代码
是不是感觉有点眼熟,是的,很大一部分都和我们上面的method方法差不多,于是我们进行改造
function Sub(firstName){
Super.call(this, firstName)
}
Sub.prototype = Object.create(Super.prototype);
var aSub = new Sub();
复制代码
ok 简化多了
寄生组合方法比较完美的解决了各种继承所带来的问题,但是确实需要大家多原型及原型链有一定的认识
继承的方式主要是看你对原型的理解程度,所以说原型才是我们真正要彻底征服的那个对象
写在最后
这篇文章感觉在草稿里躺了好久了,这段时间一直忙,也没时间来写博客,所以趁着这段时间稍微闲一些,赶紧完善一下发出去了
如果喜欢的可以点个赞0.0
还是那句话,如果本文有误的地方欢迎指正,或者有什么建议的也可以留言交流,当然,谢绝无脑喷,文明上网,社会和谐,哈哈哈
最后:欢迎大家关注我的微信公众号:大前端js,当然为了回馈大家关注,里面我放了一些学习资源,热烈欢迎大家关注交流前端方面但不局限前端方面的知识;