妙用字面量对象来设计代码

最近一直在学习一些巧妙的设计代码编写的技巧,比如:单例模式,模块模式,类模式,字面量模式等等。其中单例模式、模块模式的写法又可以根据不同的特点有几种不同的变体。哈哈,有点走题了,本文主要说的不是这些,而是怎样利用字面量对象来设计代码。

当初利用三元运算符来代替多个if的逻辑,使得代码看起来简单,虽然牺牲了一点代码可读性,但是代码看起来没有那么臃肿了。对于if逻辑多的时候,可以使用switch来代替众多的if…else if逻辑,但是对于每一个分支比较复杂或者是很类似的时候,看到switch…case一连串,也感觉不爽,那么就是用字面量对象来代替吧,就如我下面所写的那样:

var checkFn=function(operation){
  return {
	'=':function(e) { return (e.getAttribute(attr) == attrValue); },
        '~':function(e) { return (e.getAttribute(attr).match(new RegExp('\\b'+attrValue+'\\b'))); },
        '|':function(e) { return (e.getAttribute(attr).match(new RegExp('^'+attrValue+'-?'))); },
        '^':function(e) { return (e.getAttribute(attr).indexOf(attrValue) == 0); },
        '$':function(e) { return (e.getAttribute(attr).lastIndexOf(attrValue) == e.getAttribute(attr).length - attrValue.length); },
        '*':function(e) { return (e.getAttribute(attr).indexOf(attrValue) > -1); },
        'a':function(e) { return e.getAttribute(attr);}
   }[operation];
}

上面就可以避免使用switch来罗列基于operation的分支了,代码简洁多了。很多时候,利用字面量对象可以很巧妙地去处理if等判断逻辑,使得程序无需判断,就可以直接通过字面量对象得到想要的结果。下面是摘自MoolTools库开头的一段代码,读起来很顺畅,也很巧妙:

//用于给对象添加静态方法
Native.genericize = function(object, property, check){
	if ((!check || !object[property]) && typeof object.prototype[property] == 'function') object[property] = function(){
		var args = Array.prototype.slice.call(arguments);
		return object.prototype[property].apply(args.shift(), args);
	};
};
//用于给对象添加type属性
Native.typize = function(object, family){
	if (!object.type) object.type = function(item){
		return ($type(item) === family);
	};
};
//具体的实现过程
;(function(){
	var natives = {'Array': Array, 'Date': Date, 'Function': Function, 'Number': Number, 'RegExp': RegExp, 'String': String};
	for (var n in natives) new Native({name: n, initialize: natives[n], protect: true});

	var types = {'boolean': Boolean, 'native': Native, 'object': Object};
	for (var t in types) Native.typize(types[t], t);

	var generics = {
		'Array': ["concat", "indexOf", "join", "lastIndexOf", "pop", "push", "reverse", "shift", "slice", "sort", "splice", "toString", "unshift", "valueOf"],
		'String': ["charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "match", "replace", "search", "slice", "split", "substr", "substring", "toLowerCase", "toUpperCase", "valueOf"]
	};
	for (var g in generics){
		for (var i = generics[g].length; i--;) Native.genericize(natives[g], generics[g][i], true);
	}
})();

别看上面的代码多,也别看它这样做是否合理,其实它已经很好的处理了使用相应的原型的方法来实现静态方法,它的实现方式感觉不错,看起来有种恍然一亮的感觉,思维上可以借鉴的地方很多。

字面量对象还可以做很多其他的事情,来简化代码的编写,有待你去发觉,^_^。

扩展阅读:《Organizing Javascript code》、《How I write JavaScript Widgets》、《http://www.klauskomenda.com/code/javascript-programming-patterns/(需要翻墙)》

javascript coding patterns

JavaScript code的编写模式多种多样,那么接下来总结一下大概都有哪几种,看看你是否都使用到了这些模式和思维:

  1. Separation of concerns
    即是我们平时挂在嘴边的行为、样式、结构分离(Content-markup、Presentation-CSS、Behavior-JavaScript),这在代码的扩展性、可维护性、易迁移性方面都可以得到很大的提升
  2. Literals,JSON,namespace
    Literals指的是codding尽量字面量化,对象字面量、数组字面量、函数字面量、正则字面量等等,这些都比使用new来实例Object、Array、Function性能上更好,甚至也推荐使用变脸的形式来声明函数,避免直接使用function来声明一个函数。
    JSON提供一种更简洁、更直观的数据传输,在大小和解析方面都胜于XML。
    namespace是尽量将每一个APP封装在一个独立的命名空间下,避免跟其他的APP模块产生耦合,产生错误。“低耦合”是namespace的一个终极目标。
  3. Self-executing functions
    顾名思义,就是指自定义函数,它用的比较多的场合是在闭包中,提供给code一个纯净的上下文执行环境,而不会造成全局变量泛滥
  4. Callbacks
    回调函数,用过“鸡查询(jQuery)”的人都知道,在执行完一个函数之后,再调用回调函数进行下一步的操作,做到了“流畅的执行流”。
  5. Borrowing methods
    别看Borrowing这个单词是“借”的意思,感觉有点高深,其实就是利用了apply和call两个方法来借用其他类或者对象的方法来实现功能,比较多的场合是借用数组的一些方法,比如slice、join等等,比如:[].apply(arguments,[1,3])。这只是普通用的比较多的情况,但是它还可以用在这样的一个场合,类似于“继承”:我首先使用一个“基类”—即是一个包含通用的方法和属性的对象,之后其他的结构跟它类似的对象就可以通过apply或者call来“借用”这个“基类”的方法,说的可能有些枯燥,上点代码:

    var Base={
       name:"",
      "getName":function(){
          return this.name;
       },
       "setName":function(name){
           this.name=name;
       }
    }
    //有了上面这个“骨架”之后,接下来就可以“借用”了
    //比如从服务器端获得了这样的一个JSON结构:
    var json={
      "test1":{ "name":"lilei","age":22},
      "test2":{ "name":"Sam","age":30},
      "test3":{ "name":"Susa","age":15}
    }
    //这样我们就可以这样来调用了:
    alert(Base.getName.call(json.test1));
    ///////////////
    ////////这只是个简单的例子,实用性并不高,提供的是一种设计思路,在某种场合或许就可以利用它
    
  6. Functions returning functions
    在函数中再返回函数,这也是比较普通的了,为特定的场合所使用
  7. Functions overwriting themselves
    函数重写它自己,这点或许很多人都知道怎么回事,但是怎么去利用它,还比较模糊。它的一个非常适用的场合就是处理浏览器兼容性的判断,也就是所谓的“Lazy Definition”。它就是在第一次执行的时候就根据不同的条件进行判断,来重写自身函数,这样在第二次之后的调用中就可以直接调用,而无需再判断兼容性等等,比如:

    var addEvent=function(el,type,fn){
      addEvent=el.addEventListener ? function(el,type,fn){
        el.addEventListener(type,fn,false);
      }:function(el,type,fn){
        el.attachEvent("on"+type,fn);
      };
      return addEvent(el,type,fn);
    }
    
  8. Function properties
    这个就是给函数添加属性,通常使用这个无非是为了临时储存数据或者在这个函数的命名空间下在执行期间一直储存数据。比如cache的实现等。其实这里的Function可以延伸到DOM节点等任何可以设置属性的对象上。不知道大家有没有看过John Resig写的那两个addEvent和removeEvent方法,就是在DOM中临时储存数据来实现在IE下正确的removeEvent。
  9. Configuration object
    看过或者使用过YUI的,都知道它其中的对象、类、方法中都充斥着config参数,这里说的Configuration object多多少少就是这种应用,不过一种比较好的说法是“在参数列表中,如果参数数目多,那么将必须的参数独立起来,其他的可选参数都放到一个字面量对象里,即是Config参数”,这是有一定合理性的,一来代码看起来比较直观,明确区分了必须的参数和可选的参数,同时使得可选的参数无需按照顺序罗列出来。
  10. Constructor return values
    在构造函数中返回值。这里就有一点误区了:如果返回的值是对象,那么new实例化的时候,实例对象就是该返回的对象,如果是普通的数值(字符串、数字、布尔值等),就忽略该返回的值,按照正常的方式实例化。
  11. Forgeting new
    把new给“忘记”了吧。这里的“忘记”并不是说不去使用new,或者忽略它的存在,它在很多场合下还是得用(比如继承、一些带变量的正则对象等),这里的Forgeting的意思也是从继承等的角度去说的,尽量避免去使用new来实例化对象,道格拉斯曾经提出一种“伪函数化”的继承方式,例如:

    var Base = function(name){
      var that = {};
      that.name=name;
      return that;
    }
    var subClass=function(name,age){
      var that = Base(name);
      that.age=age;
      return that;
    }
    //例如上面的例子,“继承”的方式就是利用了一个字面量作为载体,进行添加、删除、修改属性和方法等等,这样就可以给不同的子类扩展不同的方法和属性。
    
  12. Chaining
    链式调用,“鸡查询”库可谓是开创了先河,它的简易性和便利性使得它的用户群猛彪,使得Prototype、ExtJs、YUI等不得不也借鉴它的方法调用的方式。
  13. Inheritance
    继承。对,这里说的就是继承,面向对象思想中的其中之一,其实在一般的程序中,很少用到继承的方式来实现功能,但是如果从代码的扩展性、维护性等方面考究,以及从库、框架的角度来架构的时候,可能就需要继承这个技巧来实现整个代码的编写了。
  14. Mixins
    这个词不好理解,YUI中的augment函数或许有些人看过它的源码,基本的应用场合是扩展对象,例如:

    /* Augment function, improved. */
    function augment(receivingClass, givingClass) {
       if(arguments[2]) { // Only give certain methods.
          for(var i = 2, len = arguments.length; i < len; i++) {
             receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
          }
       }
       else { // Give all methods.
          for(methodName in givingClass.prototype) {
             if(!receivingClass.prototype[methodName]) {
                receivingClass.prototype[methodName] = givingClass.prototype[methodName];
             }
          }
       }
    }
    

    之前我也写过一片博文《享元类ShareClass》,原理跟这个差不多。

  15. Private variables
    私有变量。“保持全局环境变量的干净”,这个被说的很多了。JavaScript可以模拟出私有成员、特权函数(即是静态方法),而实现这个仅仅需要一个闭包函数作为上下文执行环境,来于闭包外部的执行环境区分开来。静态方法仅仅需要给该对象或者构造函数添加方法,这样就使得该方法具有独享性。
  16. Dynamic script tags
    动态添加script标签。这个主要是用于异步动态加载外部的js文件,使得js的加载和执行不会阻塞后续资源的加载。这里罗列了七种加载js的方式:http://www.ilovejs.net/lab/loadjs/。不过有一点值得说明的是:浏览器解析过的script标签,浏览器不会重新去解析,即使你通过text属性修改了script标签内的代码,或者修改了script标签的src属性,浏览器也不会去重新解析它。
  17. Modules
    模块,也叫做组件。模块化开发已经广泛为前端界的人所知晓了,它并不是一个很新的东西,而是一种代码设计的思维,从代码的可维护性、独立性、松散耦合方面来设计代码。这个就跟面向对象的思想一样,都是一种设计方式,来适用于不同的场合。

更加DIY的selector查询DOM

在上一篇文章中(谈谈松散耦合、颗粒度)提到的将javascript库或者框架更加向开发人员开放,DIY功能,尽量保持核心代码的精简和稳定,只开放接口,让开发人员根据特定的需求去DIY功能,而不是内定一整套方法集,供开发人员去使用。对此我例举了我写的query2.js为例子,这里就特别拿它出来说说。

query2中查询方式也是从左到右,这个是比较普通也容易实现的方式,有人说从右到左进行查询的话,效率会比较高,查询的复杂度较低。我想这就要开具体的selector了,如果是这样的“.test p”,那么从右到左的话复杂度就更高一些了,可能还得递归到document.body节点查询父元素的className是否等于test。所以双方面都有利有弊。

但是从左到右的查询方式还有一个比较苦恼的问题,就是“去重”(这是昨天跟瓶子吃饭的时候他提及到的问题),比如:

<div>
  <div>
    <p>Hello world</p>
  </div>
</div>

这时候如果使用“div p”来查询p标签元素的时候,如果不去重,就会导致使用getElementsByTagName查询到两个p元素,如果是在查询结果后给p标签添加事件,那么问题就大了。所以再进行查询的时候,必须得去重。

query2加入的去重的功能,同时还支持querySelectorAll(注意:对于不符合CSS指定的selector,querySelectorAll会报错),它最大的功能,就是可以DIY属于自己的selector,可以根据自己的需求来指定selector,具体示例可以查看谈谈松散耦合、颗粒度中给出的代码例子。源码展示如下:

(全文…)

谈谈松散耦合、颗粒度

“松散耦合”、“颗粒度”是相对于javascript库或者javascript框架来说的,它涉及的概念和包含的知识是很多的,而其中之一,就是说javascript库或者框架从整理来看是容易拆分,也容易自定义的组合,来实现特定需求的功能。之前有很多说法说YUI的编写方法才更接近于javascript的本质,虽然上手的门槛很高,但是对于一个真正的javascript程序员来说,反而更值得;而jQuery似乎另类了,改变了普通的编写javascript的编写方式,虽然说它封装的很好,“用最少的代码实现复杂的功能”,但是它的可读性就很差了,虽然门槛很低,毫无javascript基础的程序员在看了它的API文档之后都能够利用它写出很多丰富的效果。但是,作为程序员,这样下去行吗?

jQuery是毒药 —— 很多人都这么说。本人也举得,从一个程序员的成长来看,jQuery是弊大于利。我们更应该做的是学习它的原理和架构,而不是拿来用而已。

一个很经常会发生的问题:我只需要用到一个javascript库中的几个方法,却需要加载整个库的体积,而整个库封装的又十分复杂,拆分起来十分繁琐,怎么办?自己写呗……

一个javascript库的本意虽然说是为了提高开发效率、解决浏览器的兼容性问题、统一开发规范以及解决安全隐患等等。但是在设计javascript库的时候,我们是不是应该多留多点空间给使用者去DIY它的具体的功能,让使用者多点接触到javascript的本质,以及非常容易的缩减库的体积呢?就从这几点出发,YUI3是比较符合“松散耦合”、“颗粒度”的原则的,虽然说jQuery可以通过fn方法来扩展,但都是基于它整个已经封装好的库来说的。但是YUI3也有它的缺点:学习成本高、整体体积庞大、可能一步小心在use方法里多加载几个文件,或者使用”*”,那么需要异步加载的js文件也是很多的、以及它内部组件之间的依赖关系也是比较复杂的。

为了说明我的想法,首先从我编写的query鸡查询函数(目前还没完善,先拿来做例子)开始说起,我的想法是:只在query里实现最基本的selector查询,比如ID、class、tag、属性,之后用户可以自定义selector添加到查询函数当中,来实现自己特定的查询需求,从这点出发,改写了之前所写的query代码(之前的query.js),目前它只集合了ID、class、tag、属性选择器(或者也可以干脆连这些使用者都可以重新定义自己的功能函数,自己来实现,DIY很爽~),其他的自定义选择器都可以自己DIY,并且编写它的实现方式(留给使用者更多的编写底层代码的空间,爱怎么实现就怎么实现,方法是多样的),比如:给selector添加“:first”和“:l”来实现CSS3中的:first-child和:last-child的功能:

//q:表示当前的selector,比如“div p:first”,就是div和p:first,使用split通过空格切分的单个selector
//p:表示是上一级查询的结果,是数组类型的。
query.config.addSelector(":first",function(q,p){
  var tagName = q.split(":first")[0],
	  returnEl = [],
	  index=0,
	  tmp=null;
  for(var i=0,l=p.length;i<l;i++){
	(tmp=query(tagName,[p[i]])) && (returnEl[index++]=tmp[0]);
  }
  return returnEl;
});
///////////////////////////////////////////////////////////
query.config.addSelector(":l",function(q,p){
  var tagName = q.split(":l")[0],
	  returnEl = [],
	  index=0,
	  tmp=null;
  for(var i=0,l=p.length;i<l;i++){
	(tmp=query(tagName,[p[i]])) && (returnEl[index++]=tmp[tmp.length-1]);
  }
  return returnEl;
});

通过这样的方式,使得core核心代码尽可能的小,可扩展性强,同时也提供给使用者更多的编码空间,了解javascript这门语言的核心知识。目前经过压缩过后的query2-min.js和未压缩的query2.js提供浏览。并附带实例:《点点看呗

扯谈完毕,欢迎拍砖~

在IE中模拟Worker

2010-8-8 update:在IE8下的eval是不支持下面两种的用法的,很杯具,都会提示“对象不支持此属性或方法”的错误。

//第一:
eval("onmessage=function(str){alert(str);}");
onmessage("shllo");
//第二:
var s = document.createElement("script"),h=document.getElementsByTagName("head")[0];
s.text="onmessage=function(str){alert(str);}";
h.appendChild(s);

onmessage("hello");

说到这个,还发现一点很容易造成失误的地方:在浏览器解析过的script中的代码,浏览器不会重新对该script标签内的行内脚本、外联脚本执行,就算是重新给script标签定义行内脚本或者修改它的src来链接到其他的javascript脚本,浏览器都不会重新解析。

——————- 提IE8 eval的兼容性分界线 ——————–

从上一篇文章《Web Worker浅析》中,我们了解到Worker的思维是跟Ajax类似的,包括它也不支持跨域调用javascript文件,这说明底层的数据交互还是跟Ajax的模式类似(也有可能就是使用了Ajax的方式)。但是由于IE8及以下版本都不支持Worker,所以IE就不能充分利用Worker的优点来优化浏览器执行javascript代码的性能了。但是既然Worker跟Ajax类似,那么就让IE使用Ajax的方式来实现吧,这也不是不行的。

在IE中实现Worker机制,一个比较棘手的问题是postMessage、onmessage在主页面和Worker之间的调用问题,从代码上看,主页面是通过Worker类的实例化对象来调用postMessage、onmessage,而Worker里是直接声明和调用。所以在IE里,就可以模拟Worker的操作方式了。下面是我在IE下实现的方式:

  1. 在IE下重新定义Worker类,并且带有一个postMessage方法,这样就的话不会跟支持Worker的浏览器相冲突
  2. 使用Ajax的方式来加载外联的javascript文件,并通过eval执行返回的代码
  3. 通过一个全局的字面量对象,来实现两个文件之间的数据传输
  4. 不能改变标准的Worker类的编写方式,这个是一定要做到的

从上面的几点思路出发,编写了下面的实现代码:

(function(g){
   if(!document.all) return;

   var xhr=function(){
     var x = null;
     try{
	   x = new ActiveXObject("Msxml2.XMLHTTP");
	 }catch(e){
       x = new ActiveXObject("Microsoft.XMLHTTP");
	 }
	 return x;
   }

   g.postMessage = function(data){
     g._evt_.data = data;
   }

   var Worker = function(url){
     this.url = url;
   }

   Worker.prototype={
     postMessage:function(data){
       g._evt_={};
	   g._evt_.data=data;
	   var x=xhr(),t=this;
	   x.open("GET",this.url,true);
	   x.onreadystatechange=function(){
	     if(x.readyState === 4 && (x.status === 304 || x.status === 200)){
                   //直接使用eval(x.responseText)在IE8下会提示错误,很奇怪,可能跟eval函数有关了
                   //为此不得不使用这种不太牢固的方法
		   eval(x.responseText.replace("onmessage","var onmessage"));
		   onmessage(g._evt_); //执行Worker中定义的onmessage方法
		   t.onmessage(g._evt_); //执行主页面中的onmessage方法
		   g._evt_ = null;
		 }
	   }
	   x.send();
	 }
   }
   g.Worker = Worker;
})(this);

在使用上有一点得注意,虽然在代码上没改变,但是在主页面中postMessage和onmessage的顺序得保持:先写onmessage、接着写postMessage方法,主要是为了在IE下能兼容。比如:

var worker = new Worker("js.js");

worker.onmessage = function(evt){
  alert(evt.data);
}
 //注意postMessage方法一定要在onmessage后面声明,否则会导致代码只会有一次有效。
worker.postMessage("supersha");

到目前为止,测试还算良好,《测试页面》,在各个浏览器下都能够跑起来。

目前这个只是个简单的实现方案,代码上还是比较简单的,有待进一步的完善……

overflow:hidden带来的问题

即时更新:@drunber提供的解决办法:设置父元素的padding或者border、float、position:absolute也可以解决问题,经测试验证通过。但是有一点需要说明的是:上下padding和border是必须要有数值的,设置为0无效。

但是对于动画来说,overflow:hidden是必须要设置的,否则里面的文本不会被遮盖。

———===========—– 即时更新分界线 ——===========———–

最近接到个需求,在搜索结果中添加下拉动画,高度和透明度都需要渐变。在测试过程中,遇到这样的一个问题:在一个div容器中包含了几个p标签,而且全在默认样式的情况,这样通过offsetHeight,clientWidth,scrollHeight等等获得div的高度,之后动态设置div的overflow样式属性为hidden,这样上面所获得的高度不准确了,《点击测试吧》,这样的后果就是动画被和谐了。

从这里的overflow:hidden对div的高度的差异可以看出:容器包含内容的解析原理。正常默认情况下,比如div包含一个p标签,这样div的宽高是会忽略p标签默认的margin值;但是一旦给div设置了overflow:hidden之后,div的实际宽高就需要加上p的margin值了。不过值得庆幸的是,在给div设置了overflow:hidden之后,通过offsetHeight等来获取div的宽高,是准确的。

从上面的分析中对于开头遇到的问题,就可以有三种方法来解决了:第一、在动态设置overflow:hidden之后来获取容器的高度,《点点测试吧》;第二、将p标签的margin清0处理,《再点点呗》;第三、干脆就不要使用p标签,改用没有默认margin的div吧,很勉强的方法,《最后点击测试下》。

Tip: 经过测试发现,设置overflow属性为hidden、auto都存在上面所说的情况。