一、前言
这篇开始主要介绍代码复用模式(原书中的第六章),任何一位有理想的开发者都不愿意将同样的逻辑代码重写多次,复用也是提升自己开发能力中重要的一环,所以本篇也将从“继承”开始,聊聊开发中的各种代码复用模式。
其实在上一章,我感觉这本书后面很多东西是我不太理解的,但我还是想坚持读完,在以后知识逐渐积累,我会回头来完善这些概念,算是给以前的自己答疑解惑。
二、类式继承VS现代继承模式
1.什么是类式继承
谈到类式继承或者类classical,大家都有所耳闻,例如在java中,每个对象都是一个指定类的实例,且一个对象不能在不存在对应类的情况下存在。
而在JS中其实并没有原生的类的概念,且JS的对象都可以随意的创建修改,并不需要依赖类。如果真要说,JS也有与类相似的构造函数,其语法也是通过new运算符得到一个实例。
假设工厂要生产一批杯子,接到的图纸信息是,杯子高12cm,杯口直径8cm,按照常识,我们不可能按照信息一个个的去做,最好的方法是直接做一个模具出来,然后灌浆批量生产。
这里每个杯子就是一个对象,一个实例,而这个提前定义好杯子信息的模具就是一个“类”,通过这个模具(类),我们就可以快速生产多个继承了模具信息(高度,直径等)的杯子(实例)了。
//不合理做法 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只是一个函数,而这种做法也只是与类相似,在这里我们将这种实现方式称为“类式继承”。
虽然我们在讨论类式继承,但还是尽量避免使用类这个字,在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,为四月项目重构做准备。