`
runfriends
  • 浏览: 226461 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

javascript里住着魔鬼

 
阅读更多

 

这一次还是说一些小问题。这些“小”问题在工程规模小的情况下不算什么事,不过在大规模的软件工程中却是非常不好的实践。同时个人认为这些问题也不符合良好的代码规范。既然是小问题,为什么还说它们是魔鬼?魔鬼都存在于细微处,至于它们会导致什么严重问题,且看我一一道来。

要说的第一个问题是全局变量。在js代码规范里,全局变量是受到严格限制的,甚至是禁止使用的。从各种著名的js库的代码风格中就可见一二。不论jQueryExt,还是yui它们只定义了一个全局对象,就是它们的名字。Dojo对于全局对象也有严格控制,它们分别是dojodojoxdijit,这三个全局对象划分了dojo的核心api、尚不稳定的api、以及各类富客户端组件。

对于被严格限制的全局对象,并利用局部变量、闭包、成员属性、回调函数作为组件、模块、功能之间的交互手段显而易见的好处就是,配合类似java的包特性创建用点访问的链式调用,不同的组件、模块、功能在不同的包里定义,各方互不干扰,每个组件、模块、功能能且只能操作自己组件内部定义的变量,将不虞因全局变量重名带来的各种意料不到的后果。

另外一个各个不同的组件都在各自包内定义,只对外开放有限的接口;而不像全部使用全局变量、全局函数一样,什么都能被组件外部访问到,这样做可以最大程度的降低在运行时其它组件对本组件的影响,而一个组件发生了变化也会把对其它组件影响降低到最低,因为暴露出来的接口非常少,如果一次修改对依赖它的另外某个组件产生了影响,会花费更少的时间去发现它,并修正它。暴露的接口越少维护和开发成本也会越低,需要的人更少,同样多的人就能做更多的事。

也许有人会问如果模块之间交互时需要大量的参数,而且在不同情况下可能会需要不同的参数该怎么办呢?难道全局变量不是最简单的办法吗?全局变量乍看起来似乎是一个简单的方法,但是它的简单不是简明、简易,而是简单粗暴。而且在这个过程中仍然难以避免与其它模块的重名问题。这些问题可能暂时不会暴发,不过随着工程规模越来越大,暴发问题的可能性也会越来越大。另一方面,使用全局变量,并不能降低模块间的耦合性,反而会因为各个模块都要依赖全局对象而在模块间产生了黏性,它们严格依赖全局对象才能运行,对于js代码的组织、重构、升级都不得不考虑全局对象可能会对组件产生的影响,而且作为组件开发者有时候恐怕难以确认从全局对象得到的值究竟是不是当前这一次调用得到的还是之前的某一次调用得到的,为了减少出错的可能性,在组件用完退出的时候还要做很多全局清理工作;而没有人能保证执行这些清理工作的函数会不会被开发人员调用,当然组件开发人员可以在组件自己的页面触发unload事件时执行清理。这样做又为组件开发人员带来了更大的代码量。代码量越大,工作量就越大,给将来维护代码、变更接口同样要维护清理函数,会给从开发到维护、从测试到运维带来更多困难。将来参数、接口、需求发生变更,这些代码都要跟着变更,是一个恐怖的工作。而且大多数时候,需要修改的代码量不多,但是要修改许多处代码,要修改的内容过于分散、零碎,容易遗漏。足见这么做的问题就是维护困难、内聚低,也没有降低耦合性。

那么有没有简单易行,又能降低耦合,提高内聚,还能集中管理代码的方法呢?

答案显而易见。如今已经有各种不同的设计模式可以用来解决这样的问题。比如可以使用oop,配合ioc实现js的对象注入。可以创建自定义事件模型,把组件间的交互定义为事件,通过回调函数完成交互工作。可以使用观察者模式,被依赖的组件作为观察者,依赖其它组件的组件作为被观察者,当交互发生时,被观察者通知观察者,观察者执行功能并返回被观察者需要的结果。观察者模式或自定义事件,可以由一个单例对象统一管理所有的自定义事件或观察者和被观察者,所有组件交互数据也都通过这个单例对象传播,一方面既杜绝了全局对象可能引起的问题,另一方面在单例对象中并不保留任何业务数据和对象的引用,它只是起到中转站的作用也就省去了清理数据的工作。另外大多数工作都交给这个单例对象完成了,开发人员也就有更多时间去关注业务实现,二者的实现方式差不多,也不是本文的重点,不再赘述。上述三种方法都不是最简单和耦合最低的。下面介绍一种最简单和耦合最低的模式——发布/订阅模式。

名字可能大家都已耳熟能详了。这里只介绍下它在组件交互中的意义和原理,如果大家对它的具体实现有兴趣,将来有机会我可能还会写一篇专门介绍在不借助第三方库的前提下如何实现发布/订阅模式。

发布/订阅模式与观察者模式、事件模型有相似之处,它也有三个对象——发布者(publisher)、订阅者(subscriber)、主题(topic)——参与整个处理过程。我们同样需要一个单例对象(我们暂且叫它pubsub)管理所有的主题以及每个订阅者的处理方式。

发布者可以是任何js对象,它首先用一个字符串向pubsub注册。这个字符串就是主题名,如果已经有同名主题存在就抛出异常(也可以认为相同的主题可以由不同的发布者发布,这样就不必抛出异常。但在这种情况下就必须为主题定义主题发布规范,不同的主题可以有不同的规范、格式,相同的主题必须遵守相同的规范、格式),这样就能在最短的时间内解决重名问题,为了避免不同模块的开发人员注册同名的主题,最好为主题定义一个命名规范,用类似定义java包名和类名的方式定义主题名。随后发布者就可以发布自己的主题了(pub(topic)),主题可以是一个数字、一个字符串、一个正则表达式、甚至可以是一个函数或一个复杂的对象。发布者只负责发布主题,不必关心有没有订阅者或订阅者如何处理,以及处理后该做什么。一旦主题发布了就跟发布者没有关系了。

任意对象也可以是订阅者,一个对象既可以是订阅者也可以是发布者,甚至一个对象可以订阅自己发布的主题。只要订阅者与发布者知道相同的主题名就可以实现二者之间的交互。订阅者通知pubsub自己要订阅一个主题同时向pubsub传递一个对象或回调函数(视具体实现而定),如果一个主题尚未被注册或发布就抛出异常(严格的发布订阅模式要求抛出异常,不过个人认为这里可以灵活处理,视具体要求不同我们可以订阅尚不存在的主题,因为我们确定知道这样的主题一定会被注册并发布,而且我们的模块需要这些主题)。

最后任意对象都可以作为主题内容,只要内容遵守主题的约定、规范、格式就行。一个主题被发布了,pubsub机制调用订阅者处理对象,将主题作为参数传入订阅者处理对象。

这就是发布/订阅模式的整个过程。是不是很简单?善加利用它,就能杜绝全局变量对windowtop对象的污染。同时又能杜绝因重名引起的模块交互问题。当然最后不得不提的一点是,如果一个对象不再需要某个主题最好从pubsub对象取消订阅,一个对象不再发布主题时就从pubsub对象注销(在这里,如果有多个发布者发布同一主题可以不必实现注销功能)。不取消/注销的话不会对程序功能造成影响,不过会占用内存,成为垃圾,影响性能。

第二个问题,就是重复代码太多。复制已有代码稍加修改就完成一个新功能,这似乎是一个效率很高的工作。但是随着项目进行,这些代码将会成为维护人员的梦魇。最突出的问题就是需求一变、接口一变,这些逻辑相似又不完全一样的代码就要全都改一遍,可能这些代码分散在多处,容易遗漏。还是那句话:代码量越大,维护工作量越大,工作量越大,越容易出错。所以即使是为了自己,将来修改更容易,也绝对不要这么做。可能有人会想将来上线了就不是我维护了,那时候我在哪还不知道呢。不过在你离开以前,任何需求、接口的改变还是你要负责的,所以这种想法不可以有。即使说将来要走,结构简要、逻辑清晰、层次分明简洁的代码显然更受欢迎,交接更容易。这么做也更容易锻炼自己的技术水平,即使你不要成为技术大拿,曾经力求简明、干净、清晰的代码也会对从事其它工作更有益处。代码风格清新、干净、简洁,做其它事情也会有同样的风格。谁不喜欢这样的人。也不用担心由于自己的代码一目了然,一看就懂,会对自己有不利影响;相反所有人都会因为看到了一目了然的代码,而去喜欢这些代码的作者。你甚至会逐渐形成一种氛围,进而创建一个微型的以你的代码风格为基础的生态环境。将来这个环境中的人可能会发生变化,但是它已经有了灵魂,它会产生更大的影响,在你不在的地方发挥作用,影响公司内越来越多的人。很伟大吧。所以坚持两个原则——凡是被使用至少两次的代码都尽量抽象出新的方法;一个方法代码行尽量少,尽量保持一个屏幕就能显示完整整个方法的定义。

最后的问题,就是=====!=!==的区别,还有parseInteval、‘+’连接字符串等问题。

关于js的相等性比较,==!=会发生自动类型转换,所以有时候可能会引起不易察觉的错误。就像在java里我们都建议重写equals进行对象的相等性比较一样,在js里都建议使用===!==

parseInt会把字符串找到的第一个数字子串转化成Number类型,所以’123’’abc123’调用parseInt的结果一样都是123。这种特性的意义我只发现在需要把’1px’这样的字符串转化成数字时有用,其它时候只可能带来混乱。不过个人还是觉的为了代码风格统一还是不要用parseInt(‘1px’)这样的代码。

eval函数的用处不多说,既然这篇文章是要讲魔鬼的,就只说下潜藏在eval里的魔鬼。Js代码被加载完成后,js引擎会把js代码文本解析成js语法树然后基于这棵语法树完成我们希望js去完成的工作。解析语法树的过程一般只在加载完成时执行一次。而evalnew Function使js引擎再次执行,把传入的实参再次解析成一棵语法树。这种方式,产生了在java中使用’+’连接sql/hql的类似结果(关于这个问题请参考我在上一期发表的文章)。还有一个问题是当传入的实参来自不受信站点时会产生安全问题,当然这种问题在我们这里不再赘述。另外传说ecma组织将要在标准中去掉eval。有时候我们还是不得不使用evalnew Function的,比如将json串转化成js对象。

至于使用‘+’连接字符串,除ie以外其它浏览器都对这个过程进行了优化,没有性能问题。唯独ie需要特殊对待,使用‘+’连接字符串在ie中同样有性能问题。我们可以使用[‘a’,’b’,’c’].join(‘’)连接一个字符串,join用接收的参数分割数组中的每个元素,并连接成一个字符串。前面的那个join调用构造一个值是’abc’的字符串。如果一个字符串太长,可以在一个字符串中断行,每行尾以‘\’结束。

0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics