1、前言前端
这篇开始主要介绍代码复用模式(原书中的第六章),任何一位有理想的开发者都不肯意将一样的逻辑代码重写屡次,复用也是提高本身开发能力中重要的一环,因此本篇也将从“继承”开始,聊聊开发中的各类代码复用模式。java
其实在上一章,我感受这本书后面不少东西是我不太理解的,但我仍是想坚持读完,在之后知识逐渐积累,我会回头来完善这些概念,算是给之前的本身答疑解惑。app
2、类式继承VS现代继承模式函数
1.什么是类式继承性能
谈到类式继承或者类classical,你们都有所耳闻,例如在java中,每一个对象都是一个指定类的实例,且一个对象不能在不存在对应类的状况下存在。学习
而在JS中其实并无原生的类的概念,且JS的对象均可以随意的建立修改,并不须要依赖类。若是真要说,JS也有与类类似的构造函数,其语法也是经过new运算符获得一个实例。this
假设工厂要生产一批杯子,接到的图纸信息是,杯子高12cm,杯口直径8cm,按照常识,咱们不可能按照信息一个个的去作,最好的方法是直接作一个模具出来,而后灌浆批量生产。spa
这里每一个杯子就是一个对象,一个实例,而这个提早定义好杯子信息的模具就是一个“类”,经过这个模具(类),咱们就能够快速生产多个继承了模具信息(高度,直径等)的杯子(实例)了。prototype
//不合理作法 let cup1 = { height:12, diameter:8 }; let cup2 = { height:12, diameter:8 }; // ...... let cupn1000= { height:12, diameter:8 }; //构造函数的作法 function MakeCup () { this.length = 12, this.diameter = 8; }; let cup1 = new MakeCup(); let cup2 = new MakeCup(); //......... let cup1000 = new MakeCup();
在上述代码中,MakeCup就是一个包含了全部实例共有信息的“类”,固然在JS中,咱们更喜欢将这个类称为构造函数,毕竟MakeCup只是一个函数,而这种作法也只是与类类似,在这里咱们将这种实现方式称为“类式继承”。code
虽然咱们在讨论类式继承,但仍是尽可能避免使用类这个字,在JS中构造函数或者constructor更为精准,毕竟每一个人对于类的理解可能不一样,将类与构造函数混合在一块儿容易混淆。
2.1类式继承1--默认模式(借用原型)
如今有下面两个构造函数Child()与Parent(),要求是经过Child来建立一个实例,而且这个实例要得到构造函数Parent的属性。咱们假设经过inherit函数实现了需求。
function Parent(name) { this.name = name || 'Adam'; }; Parent.prototype.say = function () { console.log(this.name); }; //空的child构造函数 function Child(name) {}; //继承 inherit(Child, Parent);
那么这个inherit函数如何实现,第一种思路,咱们经过new Parent()获得一个实例,而后将Child函数的prototype指向该实例。
function inherit(C, P) { C.prototype = new P(); } inherit(Child, Parent); let kid = new Child(); kid.say();//Adam
很明显,构造函数Child继承了构造函数Parent的属性,因此由构造函数Child建立的实例天然也继承了这些属性,那么这个过程当中间到底发生了什么?咱们尝试跟踪原型链。
提早说明,为了方便理解,咱们就假设对象啊,原型啊,都在同一空间内,当咱们new Parent()时,就获得了一个实例,此时在内存中也新开了一个空间存放这个实例(下图中的2区域)。
构造函数Parent的原型链
如今咱们尝试访问say()方法,可是2号空间并无这个方法,可是经过_proto_指向Parent构造函数的prototype属性时,竟然能够访问这个方法(1区域),这也是为何咱们总在前面说,建议将全部实例都须要用到的属性添加在prototype上,由于这样在每次new时,不用每次新开内存时都建立一次。
咱们再来看看在使用inherit函数后,再使用let kid = new Child()建立实例时发生了什么,以下图。
继承以后的原型链
一开始Child构造函数是空的,什么属性都没有(上图1区域),当inherit函数执行时,Child函数的prototype属性指向了new Parent()对象,也就是2区域。
当咱们new Child()获得一个实例kid并使用say方法时,因为自身没有,只能顺着_proto_找到了new Parent(),结果此对象也没有,重复了咱们上面的图解步骤,继续顺着_proto_找到了Parent.prototype,终于找到了say()方法。
当say()方法被调用时,咱们输出this.name,而此时this指向的是new Child(),结果new Child()又没有这个name属性,跟say同样,再找到2,再到1区域,顺利输出了Adam。这样是否是很清晰了呢?
咱们再来为实例kid加点属性,看看原型链的变化,以下图
let kid = new Child(); kid.name = 'Patrick' kid.say();//Patrick
继承并给实例添加属性后的原型链
当咱们为实例添加了name属性时,其实只是为new Child()添加了name属性(区域3),并不会影响到new Parent(),这也是为何说,每一个实例都是一个独立的个体。当咱们再次寻找say()方法时,仍是同样的顺着_proto_找到了Parent.prototype,而当咱们调用say方法输出name属性时,因为当前this指向kid,且kid本身有了name属性,因而顺利输出了Patrick。
而当咱们delete kid.name删除掉以前赋值的Patrick时,再次调用,能够发现又输出了Adam,因此原型链继承就是,先从本身身上找,找不到,顺着_proto_向上,直至找到null中止(原型链的顶端是null)。
2.1原型链的优势与弊端
原型链继承的坏处在于,在继承父对象中你想要的属性的同时,也会继承父对象你不想要的属性,好比上方代码,我只想要父对象原型链上的say方法,结果你仍是把构造函数中的name属性打包给我了。
上面这种模式的第二个坏处是,我不能给我最终的实例kid传递形参,假设我想最终输出时间跳跃,要么kid.name = ‘时间跳跃’,要么在父构造函数时就传递好参数Parent(‘时间跳跃’)。但这样咱们得不停的修改Parent对象。
let kid = new Child('时间跳跃'); kid.say();//Adan
但若是一个属性或方法须要复用,它仍是应该被添加在构造函数的原型prototype上;两点理由,第一,加在原型链上,new实例时不须要反复建立属性形成内存浪费,第二,简化构造函数的属性能减轻对不须要这些属性的实例的困扰,这也是原型链继承的好处。
3.类式继承2---借用构造函数
咱们在上个例子中,遇到了没法经过子对象传参到父对象的问题,咱们修改Child构造函数,以下,就能够实现子对象传参了。
function Child(a, b, c, d) { Parent.apply(this, arguments); }; let kid = new Child('时间跳跃'); console.log(kid.name);//时间跳跃
实现原理很简单,当咱们new Child()时,经过apply再次应用了Parent函数,但Parent执行时此时的this指向了Child,也就是说Child想有name属性,但是我没有this.name的赋值操做,因而经过apply改变this的原理,借用了Parent函数中的this.name = name || 'Adam'这句代码,变相的来为Child构造函数添加属性,它等同于Child.name = '时间跳跃' || 'Adam'。
注意,此处只是借用这句代码来为Child构造函数添加属性,并无修改Parent构造函数的属性,咱们尝试输出Parent的实例,能够发现name属性仍为Adam。
let parent = new Parent(); let kid = new Child('时间跳跃'); console.log(kid, parent);//时间跳跃 Adam
咱们在上面原型链的例子中,Child的实例去继承Parent的属性,说是继承,实际上是经过原型链去找,虽然能拿到,但本质上这个属性仍是别人的,本身手里没有,哪天Parent心情很差,把name属性给删了,Child啃老的行为也基本到头了。
但下面Child构造函数中使用apply的作法就不一样了,我直接借用Parent的代码来为本身添加只属于本身的name属性,管你Parent怎么操做name属性,都跟我不相关。若是说第一种继承是引用,那么这种作法就更像是复制,我复制你有的属性,就不用引用了。
有点授人以鱼不如授人以渔的寓意,也有点深浅拷贝的意思。
我稍微修改了上面的代码,使用原型链指向继承获得了实例kid与使用call复制属性获得的实例son,分别输出了它们的hasOwnProperty判断,这里答案应该能明白了。
function Parent(name) { this.name = ["echo", "时间跳跃", "听风是风"]; }; Parent.prototype.say = function() { console.log(this.name); }; //获得一个实例 let parent = new Parent(); function Child() {}; //修改Chilkd的原型指向 Child.prototype = parent; function Son() { Parent.call(this); }; let kid = new Child(); let son = new Son(); console.log(parent.hasOwnProperty('name'));//true console.log(kid.hasOwnProperty('name'));//false console.log(son.hasOwnProperty('name'));//true
照理说,实例parent与实例son的name属性是自身的,不像kid这个没骨气的是靠引用地址借来的,咱们分别修改三个实例的name属性,这段代码是我本身改的,当出个题,看看下面三个console分别输出什么,学继承,也当原型链的题来考考本身。
function Parent() { this.name = ["echo", "时间跳跃", "听风是风"]; }; Parent.prototype.say = function() { console.log(this.name); }; let parent = new Parent(); function Child() {}; Child.prototype = parent; let kid = new Child(); function Son() { Parent.call(this); }; let son = new Son(); parent.name.push('二狗子'); son.name.push('狗剩'); kid.name.push('狗蛋'); console.log(parent.name);//? let parent1 = new Parent(); let kid1 = new Child(); console.log(parent1.name);//? console.log(kid1.name);//?
有没有以为使用call或者apply的构造函数方式很厉害,但这种模式也有本身的弊端,虽然它借用了父构造函数的属性建立代码,很遗憾它并没办法继承父构造函数的prototype属性。咱们写个简单的例子:
function Parent(name) { this.name = name || "Adam"; }; Parent.prototype.say = function () { console.log(this.name); }; function Child (name) { Parent.apply(this,arguments); }; let kid = new Child('Patrick'); console.log(kid)//undefined
跟上面同样,咱们经过原型图来看看这段代码继承关系。
尽管咱们经过改变this指向为kid建立了name属性,但当找say方法时,因为此时的this指向Child,而Child的prototype并无提供这个方法,因此没法找到。
3.1利用构造函数模式实现多继承
利用构造函数加apply的方式,咱们能够同时继承多个构造函数的属性,像这样:
function Cat () { this.legs = 4; this.say = function () { console.log('喵~') } }; function Bird() { this.wings = 2; this.fly = true; } function CatWings() { Cat.apply(this); Bird.apply(this); }; let miao = new CatWings(); console.dir(miao);
简直不能在方便,那么到这里位置,咱们大概介绍了类式继承,默认模式,也就是构造函数的property指向你须要继承的实例,构造函数模式(结合call或apply)。
第二种构造函数模式的弊端在于不能继承原型,而添加在原型上的每每又是可复用的方法,这点比较遗憾。
但它也有好处,例如它能得到父对象成员的拷贝,不存在子对象修改能影响父对象的风险。那么这个遗憾咱们能不能解决呢,若是在构造函数的模式上继承原型呢。下面的一种模式来解决这个问题。
JS模式这本书我可能最近,至少一周须要放放了,昨天跟组长说咱们如今前端ES6规范都没用,确实low了点,因此我这边想尽快把ES6实践到项目中,这几天打算把ES6过一遍,因此想写写ES6的笔记。反正无论学什么,只要愿意学,老是没坏处的。
我为何要写这段话呢,说的像我有不少读者,要提早说明同样。其实根本没人看个人博客啊...
那么这篇就写到这里了,接下来先放置一下,这本书还剩下两章,我会坚持读完,接下来好好学习一下ES6,为四月项目重构作准备。