初涉JavaScript模式 (13) : 代码复用 【上】
引子
博客断了一段时间,不是不写,一是没时间,二是觉得自己沉淀不够,经过一段时间的学习和实战,今天来总结下一个老生常谈的东西: 代码复用。
为何复用
JS门槛低,故很多人以为写几个特效就会JS,其实真正混前端的人都知道,JS写的好和写的不好差距是很大的,前段时间看到一个前端为了实现一个浮层的显示与隐藏花了100多行代码,去遍历各个子元素,挨个去设置display。还有一个很常见的场景,为了给一个UL下面的n个li绑定事件,遂遍历所有的li去bind,这些例子在日常的开发中,比比皆是,为什么有这种情况,一和业务有关,而他们压根不知道有更好的方式。造成这张现象的深层次原因无非有两种,一是他们无意去优化自己的代码,完成即好,还有一种便是没有一个好的指引,想优化却不知从何开始,时间一久,随着代码量的增加,大神也救不了你。
前段时间负责Pad端的局部代码重构,说的好听叫重构,其实就是代码组件化,由于本鸟资历尚浅,加上对一些东西的认知度也不够,做的过程中看了很多的书,但是还是觉得把握不了精髓,但是期间学到的一些比较好,可以对大家代码结构化提供一些帮助的手段。故本鸟在这里献丑一二,希望起到一个抛砖引玉的作用。期间不仅是谈代码复用,在我眼中代码的优化也是取决于代码复用是否有意义的关键指标,所以文中也会说到如何进行代码优化的几个常用手段。
罗嗦了这么多,下面切入正题,本段的标题叫 “为何复用”,相信大家看了我举得2个例子,也能猜到一二,我的理解就是优化自己的代码,说到底还是为了更加方便以后的开发。比如你写的一段为了实现验证的代码,如果不考虑代码复用,你每次都写一遍,造成代码冗余不说,光是看到那一摸一样的代码,你自己不难受吗,至少我是受不了。还有在很多场景下,大家都知道对DOM元素的操作很浪费性能,但是还是一遍一遍的去轮询,为了取某个index去遍历整个数组,为了方便图省事,定义n个全局变量,自己都不清楚prototype就给原生对象绑定n个他自己都不知道的方法。。等等。这些常识性的错误,在代码复用之前都应该极力去避免。我的观点是,好的代码才能去复用,不是随便写几个恶心的代码就跑去给别人用,这是对你自己代码的侮辱,更深的是对JS的不尊重。吐槽了这么多,舒服多了。。
简而言之,代码复用是对高强度代码的整理,减少重复开发的时间。
常用代码复用的手段
本鸟下面总结了下自己知道的几种方式,没有好坏之分,这要结合业务场景和你喜欢的方式,一句话 “适合你的才是最好的”
普通继承
在强类型语言中,实现代码复用很难绕过继承,而JS的强大之处在于模拟,故你们能做的我也能做,而在JS中,代码复用使用继承也是可行的。
首先,先上一段最基本的继承的实现代码。
```javascript function Animal(name){ } Animal.prototype.run = function(){ console.log(this.name + " is running!!!"); } function Cat(name){ this.name = name; } Cat.prototype = new Animal(); var tom = new Cat("tom"); tom.run(); // tom is running ```
上面的继承,说的直白点就是把Cat的原型重定义了,链接了原型链,以至于Cat和Animal链接起来了,故在Animal上定义的方法和属性,在Cat上也能通过委托访问到。这是一种最简单的继承,但是也有他的缺陷,一个子构造函数并不是总是希望继承全部的父构造函数的所有属性和方法。这样就违背了我们代码复用的准则,尽可能的减少代码的冗余。
借用式继承
为了解决普通继承的缺陷,我们可以在子构造函数中去借用父构造函数去构造本地的this,而无需去继承那些父构造函数原型上的方法。上代码:
```javascript function Animal(name){ this.name = name; } Animal.prototype.run = function(){ console.log(this.name + " is running!!!"); } function Cat(name){ Animal.call(this, name); } var tom = new Cat("tom"); console.log(tom.name); // tom tom.run(); //undefined is not a function ```
通过上面的代码,我们发现Cat只是简单的使用call调用了父构造方法,从而达到无需继承Animal的prototype上的run方法。甚至我们可以使用借用式继承去实现多重继承,上代码:
```javascript function Person(){ this.legs = 2; this.hands = 2; } function Man(){ this.sex = "Male"; } function Guy(){ Person.call(this); Man.call(this); } var wr = new Guy(); console.dir(wr); ```
我们发现Guy构造函数分别借用了Person和Man两个构造函数,从而实现了多继承,这和Java不支持多继承(普通类)就有了区别了,JS确实是无所不能啊。
借用式的缺点
借用式的优点在于可以获得父类自身属性的真实副本(包括定义在this的方法),而且不会存在覆盖父类属性的风险。缺点也很明显,无法继承父类的原型属性。
但是我们的JS多么强大,当然有解决方案,我们可以手动的给子类指定原型赋为父类,上代码:
```javascript function Animal(name){ this.name = name; } Animal.prototype.run = function(){ console.log(this.name + " running!"); } function Cat(name){ Animal.call(this,name); } Cat.prototype = new Animal(); var tom = new Cat("tom"); console.log(tom.name); //tom console.log(tom.run()); //tom running! ```
以上代码,我们通过重新设置了Cat的prototype去获得了父类的原型方法,但是这样做也有缺陷,父类的构造函数被调用了2次,因此导致了其效率低下,而且如果在父类和子类中存在同名属性,子类会重复继承2次
共享原型
这种方式是通过把父类和子类的prototype指向同一个原型,所以父类和所有的子类都可以访问到父类的所有原型上的方法,这样做的前提是把父类所有可继承的方法都写在原型中,代码如下所示:
```javascript function Animal(name){ this.name = name; } Animal.prototype = { constructor : Animal, sayName : function(){ console.log("my name is " + this.name); } } function Cat(name){ this.name = name; } Cat.prototype = Animal.prototype; var tom = new Cat("tom"); tom.sayName(); //my name is tom console.dir(Animal.prototype); //Animal console.dir(Cat.prototype); //Animal ```
这样的做法确实帮Cat继承了Animal上的方法,但是也是有缺点的,如果父类的某个子类去修改了原型那么将会修改所有的这条原型链上的所有的类,而且我们发现这样做只是共享了原型,我们无法去继承父类通过this设置的属性和方法,所以我们可以使用下面的方式
临时构造函数
简而言子,这种方式就是在父类的原型和子类的原型间设置一个缓冲区,断开了父类于子类之间的直接链接关系,从而解决了共享原型方式带来的问题,同时有可以继续享受原型链带来的好处,代码如下:
```javascript var inherit = (function(){ var buffer = function(){}; return function(child, parent){ buffer.prototype = parent.prototype; child.prototype = new buffer(); child.prototype.constructor = child; } }()); ```
上面代码中,我们设置了一个buffer,去充当child和parent之间的缓冲区,这样的化我们每次仅需创建一次临时构造函数,这里有个小技巧,为了避免重复创建buffer我们吧buffer放在闭包中存储。
小结
在上面我们总结的代码优化方式的几种方式,在下一篇,本鸟还会梳理下一些其他的方式和一些有趣的小技巧,总算书没白读。。如在文中发现错误,欢迎指正。