共计 7241 个字符,预计需要花费 19 分钟才能阅读完成。
在 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()
成果:父(私公)-> 子(公私)