深刻浅出JavaScript继承

写在前面

若是对继承尚未了解,或者了解不深的同窗,但愿本文可以对你有所帮助,在阅读过程当中有任何疑问或者意见,欢迎留言交流前端

父类函数约定

// 先约定好一个超类,即咱们要去继承的父类
    // 虽然没有强行规定,可是默认约定构造函数名采用大驼峰命名规则
    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
很明显父类Super中的原型上的属性是没有继承到的,这是为何呢?究其缘由仍是call方法的原理,他只是循环拷贝了父类中的全部实例属性,并无拷贝原型属性

实例 aSub 的构造函数是谁?

console.log(aSub instanceof Sub) // true;
console.log(aSub instanceof Super) // false;
复制代码

仍是 call 方法,他并无牵涉到原型指向的改变,因此对于使用call方法来讲,并不会改变实例对象的指向的; 该谁 new 出来的谁就领走bash


吃瓜群众小明: instanceof是什么鬼?
吃瓜群众大黄:instanceof运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另一个要检测对象的原型链上
吃瓜群众小明:说人话
吃瓜群众大黄:判断孩子是否是他爹的
吃瓜群众小明: 这...微信


是否能多继承

确定是能实现多继承的,我说了不算,看下面代码app

// 再添加一个父类
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中的实例属性,前提是实例属性名字同样,不同就各自安好函数


吃瓜群众小明: 什么是多继承?
吃瓜群众大黄:一个子类函数能够继承多个父类函数
吃瓜群众小明:说人话
吃瓜群众大黄:一个单纯的女孩子能不能有多个干爹
吃瓜群众小明: 这...
性能


父类中的实例属性对于全部子类来讲是不是独立的


吃瓜群众小明: 能解释一下上面的题目啥意思不?
吃瓜群众大黄:这不够清楚?
吃瓜群众小明:能说人话不?
吃瓜群众大黄:爸爸有好几个孩子,要给孩子一百钱用,嗯,话没说清楚,这一百块钱是多个孩子共一百,仍是每一个孩子都有一百呢?
吃瓜群众小明: 这...
学习


算了,仍是看代码吧ui

// 咱们为父类添加一个数组吧
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方法,他将父类的实例属性拷贝到了本身这里,这就是他本身的了,私有的了,不和他人共用this

总结

综上所述,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)
复制代码

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,固然为了回馈你们关注,里面我放了一些学习资源,热烈欢迎你们关注交流前端方面但不局限前端方面的知识;

相关文章
相关标签/搜索