在ECMAscript中描述了原型链的概念,并将原型链作为实现继承的主要方法,其基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数和原型还有实例之间的关系:每个构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(proto)。 来一张手残党画的图
其实每一个Function都是Object基类的一个实例,所以每一个Function上都有一个__proto__指向了Object.prototype。当查找一个实例的属性时,会先从这个实例的自定义属性上找,如果没有的话通过__proto__去实例所属类的原型上去找,如果还没有的话再通过原型(原型也是对象,只要是对象就有__proto__属性)的__proto__到Object的原型上去找,一级一级的找,如果没有就undefined。
可以说引用类型之间的继承就是通过原型链机制实现的。到这里我们就可以看一下第一种继承方法,原型继承
原型继承
把父类的私有+公有的属性和方法,都作为子类公有的属性。
核心:不是把父类私有+公有的属性克隆一份一模一样的给子类的公有吧;他是通过__proto__建立和子类之间的原型链,当子类的实例需要使用父类的属性和方法的时候,可以通过__proto__一级级找上去使用;
function Parent(){ this.x = 199; this.y = 299;}Parent.prototype.say = function(){ console.log('say')}function Child(){ this.g = 90;}// 将父类的实例挂到子类的 prototype 上Child.prototype = new Parent();var p = new Parent();var c = new Child();console.dir(c)
实现的本质是重写了原型对象 ,通过将子类的原型指向了父类的实例,所以子类的实例就可以通过 __proto__访问到 Child.prototype 也就是 Parent的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法。这样就做到了将父类的私有、公有方法和属性都当做子类的公有属性。这样就通过原型链实现了继承。但是别忘了默认的原型,因为所有引用类型都是继承了Object的,所有说子类也可以访问到Object上的方法如toString() 、valueOf() 等
这个时候我们可以通过instanceof检测一下会发现
console.log(c instanceof Object) //trueconsole.log(c instanceof Parent) //trueconsole.log(c instanceof Child) //true
但是,需要我们注意一点的是,有的时候我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后。
function Parent(){ this.x = 199; this.y = 299;}Parent.prototype.say = function(){ console.log('say')}function Child(){ this.g = 90;}/*在这里写子类的原型方法和属性是没用的,因为会改变原型的指向,所以应该放到重新指定之后Child.prototype.Bs = function(){ console.log('Bs')}*/Child.prototype = new Parent();Child.prototype.constructor=Child//由于重新修改了Child的原型导致默认原型上的constructor丢失,我们需要自己添加上Child.prototype.Bs = function(){ console.log('Bs')}Child.prototype.say = function(){ console.log('之后改的')}var p = new Parent();var c = new Child();console.dir(c)c.Bs() //Bsc.say() // 之后改的p.say() //say 不影响父类实例访问父类的方法
原型继承的问题:
- 子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们要清楚一件事情就是我们操作基本数据类型的时候操作的是值,在操作应用数据类型的时候操作的是地址,如果说父类的私有属性中引用类型的属性,那他被子类继承的时候会作为公有属性,这样子类一操作这个属性的时候,会影响到子类二。
- 在创建子类的实例时,不能向父类型的构造函数中传递参数。应该说是没有办法在不影响所有对象实例的情况下,给父类的构造函数传递参数
所以在实际中很少单独使用原型继承
call继承
第二种继承是call继承,call方法的使用相信大家应该很熟悉,将方法的this指向改变同时执行方法。 在子类构造函数中 父类.call(this) 可以将父类的私有变成子类的私有
function Parent() { this.x = 100; this.y = 199;}Parent.prototype.fn = function() {}function Child() { this.d = 100; Parent.call(this); //构造函数中的this就是当前实例}var p = new Parent();var c = new Child();console.log(p) //Parent {x: 100, y: 199}console.log(c) //Child {d: 100, x: 100, y: 199}
这个是很好理解的,在子类的构造函数中,改变父类的this指向,改变为子类的实例,同时运行父类方法,这样父类中的this.x就变成了 子类的实例.x ,通过这种方法就可以继承了父类的私有属性,且只能继承父类的私有属性和方法。也许你会问那我Parent.prototype.call(this) 不就可以继承父类的公有属性和方法么,我只能默默的说一句,call是Function的方法。
冒充对象继承
冒充对象继承的原理是循环遍历父类实例,然后父类实例的私有方法全部拿过来添加给子类实例
function Parent(){ this.x = 100;}Parent.prototype.getX = function(){ console.log('getX')}function Child(){ var p = new Parent(); for(var attr in p){//for in 可以遍历到原型上的公有自定义属性 this[attr] = p[attr] } //以下代码是只获得到私有方法和属性,如果不加这个的话就可以遍历到所有方法和属性 /*if(e.hasOwnProperty(attr)){ this[attr] = e[attr] } e.propertyIsEnumerable()*///可枚举属性==> 可以拿出来一一列举的属性}var p = new Parent();var c = new Child();console.dir(c)
这个就不过多解释了,重要的只有一点:for in 可以遍历到原型上的公有自定义属性 ,所以他可以拿到私有和公有的属性和方法,这个你可以遍历私有和公有的,需要你加限制条件。但是如果不做hasOwnProperty判断那么就是把父类的公有的和私有的都拿过来当私有的。
混合继承
混合继承,那就肯定是混合的啦,将call继承和原型继承集合在一起
无论是私有的还是公有的都拿过来了。但是有个问题就是子类的原型上的多了一套父类私有属性,但是不会产生问题。因为子类的私有属性也有一套相同的通过call继承拿过来的
function Parent(){ this.x=100;}Parent.prototype.getX = function(){}function Child(){ Parent.call(this);}Child.prototype = new Parent();Child.prototype.constructor = Child;var p = new Parent();var c = new Child();console.log(c)//Child {x: 100}
混合继承有多重方式,这种是call和原型混合的,你也可以call和冒充对象继承混合,等等,多种方式。具体使用就看自己的业务场景了。
这种混合继承的最大问题就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,没错,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
还有一种就是 call+拷贝继承
//混合继承:call继承+拷贝继承 function extend(newEle,oldEle){ for(var attr in oldEle){ newEle[attr]=oldEle[attr]; } } function F(){ this.x=100; this.showX=function(){} } F.prototype.getX=function(){}; F.prototype.getX1=function(){}; var f1=new F; console.dir(f1) function S(){ F.call(this)//call继承 } extend(S.prototype, F.prototype);//拷贝继承 S.prototype.cc=function(){ } var p1=new S; console.dir(p1);
这种方式使用call继承将父类的私有方法继承过来,使用for in 拷贝 将父类的公有属性和方法继承过来,比较实用。
中间件继承
中间件继承就是通过原型链的机制,子类的prototype.__proto__本来应该是直接指向Object.prototype。从父类的原型上的__proto__也可以到Object.prototype ==> 在父类.prototype上停留了下,父类.prototype就是一个中间件,所以子类可以继承到父类的公有方法当做自己的公有方法。
function Parent() { this.x = 100;}Parent.prototype.getX = function() {};function Child() {}// 父级的原型对象 相当于一个跳板Child.prototype.__proto__ = Parent.prototype;var p = new Parent();var c = new Child();console.log(c);
寄生组合式继承
寄生式组合: call继承+Object.create()
所谓寄生组合式继承就是通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。 基本思路是不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类型原型的一个副本而已。 本质上,就是使用寄生式继承父类的原型,然后再将结果指定给子类的原型。
所以我们就新建一个方法
function inheritPrototype(subType,superType){ var prototype = Object.create(superType.prototype);//创建对象 prototype.constructor = subType;//增强对象 subType.prototype = prototype;//指定对象}
解释一下:
1、第一步是创建父类型原型的一个副本。
2、第二步是为创建的副本增加constructor属性,从而弥补了因为重写原型而失去的默认的constructor属性。
3、第三步是将创建的对象赋值给子类型的原型。
function F() { this.x = 100;}F.prototype.showX = function() {};function S() { this.y = 200; F.call(this); //只继承了私有的;}function inheritPrototype(subType, superType) { var prototype = Object(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //指定对象}inheritPrototype(S, F);var p1 = new S();console.dir(p1);
经典继承(道格拉斯继承)
与上面的大同小异,已知一个对象o,需要创建一个新的对象,这个新的对象继承自对象o。
//功能封装function create(o) { function F(){} F.prototype=o; return new F(); }var o={name:"张三",age:18};var o2=create(o);//这样o2就继承自o了
以上是 ES5部分的继承
ES6的继承
es6的继承主要要注意的是class的继承
- 基本用法:Class之间通过使用extends关键字,这比通过修改原型链实现继承,要方便清晰很多
举例:
class Super { constructor(){ this.sup = 'super??' } superFun(){ console.log('父-function') }}class Sub extends Super{ constructor(){ super() this.name = 'sub??' } subFun(){ super.superFun() console.log('subFun'); }}let p = new Sub()
子类必须要在construct 中 调用super方法,否则新建实例的时候会报错。
因为子类没有自己的this对象,而是继承父类的this对象,只能通过super() 这种方式。
super 关键字:既是函数 也是对象
它作为函数,是父类的构造函数,只能在子类的构造函数里用,但是使用环境是子类,相当于在子类的环境中,调用父类的构造函数。sub继承Super,sub里面用的super()相当于以下代码
Super.prototype.constructor.call(this);//这里的this是sub的this
它作为对象,在普通过的方法中(非构造方法,非静态方法),或指向父类的原型对象,可以使用super.fun() 的方法调用。但是,父类的属性(this.xxx)是不能被子类以super.xxx的形式进行访问的。 因为这些属性是父类实例的属性,不是父类的属性。只有写成this.prototype.xxx 才属于父类,而不是实例。
它作为对象,在静态方法里,是父类本身。所以可以调用父类本身的静态函数。
ES6规定,通过super.someFunction()调用父类的方法时,环境一定是子类的this,这样我感觉不会乱。打个比方,在子类里写 super.someValue,就相当于写到this.someValue里了,再去读super.someValue的时候,就读不到刚刚设置的那个值了,我觉得是因为ES6只拦截了对super的写操作,把所有写操作都挪到子类的this环境里了,但是读操作还是会走到父类里,所以读super.someValue,相当于读 A.prototype.someValue,父类的值。
ES5 和 ES6 的继承的区别
ES5不能继承原生构造函数,原因是它先建立子类实例,再往里加父类的属性,原生构造函数(Array)什么的,有内部属性,不让读,所以不行。ES6比较机智,先建立父类的实例,放子类的this上,再执行子类的构造函数,所以可以继承。
记忆模式
整理一些简单的文字描述,快速记住所有的方式
原型继承
方式:子类.prototype = 父类的实例。
效果:父(公私)-> 子(公)
call继承
方式:子类中执行 父类.call(this)
效果:父(私)-> 子(私)
冒充对象继承
方式:子类中执行 for in 循环父类的实例
效果:父(公私) -> 子(私)
for in 循环比较灵活,可以循环实例:也可以使用 hasOwnProperty 来进行公有的过滤;可以循环父类的prototype 添加到子类的prototype上。
混合继承
方式:就是前几个随意组合呗,看自己的业务情况
中间件继承
方式:子类.prototype.proto = 父类.prototype;
效果:父(公)-> 子(公)
寄生组合式继承
方式:call + Object.create()
效果:父(私公) -> 子(公私)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。