最近学习JS的感悟-1

       还记得我大二的时候开始接触JS,那个时候从图书馆借了N多的书籍,然后边看边用editplus写,然后遇到问题,各种DEBUG,在做项目的时候,各种兼容性问题,真是痛苦啊。由于项目需要尽快写完,所以就开始接触了jquery,还是从图书馆抱了几本书,然后下载了jquery源码,然后边看书籍边写代码,看了几章之后,觉得貌似不难,然后就从网上下载了jquery的文档,对照着文档,对其调用搞得算是比较清楚了。

        现在看来,我觉得学习jquery反而使我走了弯路,用jquery是比较方便,也不用考虑兼容性问题了,而且调用非常简单优雅,但是,反而我对原生js感觉越来越陌生了,也导致了后面感觉完全离不开jquery了,想去写一个XXX组件,想了一下,思路有了,然后写的时候遇到各种问题,然后就又回到jquery了。

         从去年暑假的时候,我决定离开jquery了,jquery是一把双刃剑,开发的时候是方便,但是,作为一个初学者,我觉得这是很不利的。

         然后就开始下载JS的电子书,可能是自己比较浮躁吧,看书真心看不进去,我还是喜欢边看边写代码这种。写了一段时间,渐渐的觉得最开始的感觉慢慢回来了,当然,也遇到了N多的问题。

        到寒假的时候,决定自己的毕设不使用现在成熟的JS库,反而自己来写一个不完善的库,这样学习的更多,当然,也比较费时间。

        开始写的感觉真是痛苦啊,什么都不懂,所以就去看了看tangram的源码,为什么看tangram呢,其实原因比较搞笑,当时校招的时候我面试百度前端,被刷掉了,当时面试官让我看看它们百度使用的JS库tangram,我就想看看它们那个库到底有什么了不起的。。。

        写这个库,首先使用了命名空间,我比较喜欢toper,所以我首先定义了一个变量:


var tp = tp || {};
        这种方式也是借鉴了tangram的写法,采用对象字面量的形式。这样所有toper定义的方法都放在了tp这个私有空间内了,比如对DOM的操作就放在tp.dom中。


       由于这个库完全是为毕设做的,所以这里面的很多文件都是为实现毕设的某些功能而写的。

      我采用的结构是core+组件的方式,tp.core.js(压缩后为tp.core-min.js),而其他的组件每个组件一个文件,而组件之间可能存在依赖关系,这种依赖关系就通过AMD解决。

      在没有写这个库之前,即使是我使用jquery,每一个JS文件我都是直接在HTML文件中使用script标签写进去的,而现在需要采用这种异步模块加载的方式,如果要使用非核心模块,那么需要:


tp.use(["tp.a","tp.b"],function(a,b) {

})
      使用use方式,它会自动去下载tp.a.js和tp.b.js,下载完成之后,执行回调函数。


      同样,在tp.a.js中,也不能使用普通的JS的写法了,而要使用:

 

define("tp.a",["tp.c","tp.d"],function(c,d) {
   tp.modules.add("tp.a",function() {

    });
});


     define的第一个参数是该组件的名字(需要唯一,所以我还是按照命名空间的方式写的),第二个参数是这个组件所依赖的组件,第三个参数是回调函数,也就是当依赖的组件下载完成之后,回调执行,而tp.modules.add就可以将这个模块加载到整个库中,这样的话才能使用tp.use调用。

      这种方式我在tangram中没有看到,我是看了淘宝的KISSY之后学习到的,也就是所谓的AMD(异步模块定义)。

      暂时AMD的实现方式是通过setInterval,但是即将被重构

      我之前写了一篇日志来实现AMD,当然,效率低下,反正大家看看就行了http://my.oschina.net/mingtingling/blog/113815

      然后就是事件了,事件是一个比较恼火的事情,东西比较多,我把它放在了tp.event这个空间中。

      首先是添加和移除事件监听器,由于IE和非IE采用的方式不一样,IE采用attachEvent和detechEvent,非IE采用addEventListener和removeEventListener,而且IE只支持冒泡(从当前元素冒泡到根元素),而非IE支持冒泡和捕获(从根元素捕获到当前元素)。最开始我是这样做的:


tp.event.on = function(element,event,fn) {
		if (window.attachEvent) {
			//IE
			//第三个参数_realFn是为了修正this
			var realFn = function(e{fn.call(element,e);};
			_realEventCallbackFns[fn] = realFn;
			element.attachEvent("on" + event,realFn);
		} else if (window.addEventListener) {
			element.addEventListener(event, fn,false);
		} else {
			element["on" + event] = fn;
		}
};
     也就是在一个函数内部去判定是否是IE,然后相应的执行相应的函数,但是这样,如果添加的事件监听器很多,每次都if什么的,我个人感觉很不好,所以我后面添加了一个辅助函数:



var _listeners = {},
		_addEventListener,
		_removeEventListener;
	if (window.attachEvent) {
		
		var _realEventCallbackFns = {};
		_addEventListener = function(element,event,fn) {
			//第三个参数_realFn是为了修正this
			var realFn = function(e) {fn.call(element,e);};
			_realEventCallbackFns[fn] = realFn;
			element.attachEvent("on" + event,realFn);
		};
		_removeEventListener = function(element,event,fn) {
			element.detachEvent("on" + event,_realEventCallbackFns[fn]);
		};
	} else if (window.addEventListener) {
		_addEventListener = function(element,event,fn,capture) {
			element.addEventListener(event, fn,capture);
		};
		_removeEventListener = function (element,event,fn,capture) {
			element.removeEventListener(event,fn,capture);
		};
	} else {
		_addEventListener = function(element,event,fn) {
			element["on" + event] = fn;
		};
		_removeEventListener = function(element,event) {
			delete element["on" + event];
		};
	}
           这样,整个判定只需要执行一次,后面调用的时候只需要使用_addEventListener即可,当然,由于采用了闭包,tp.event命名空间之外是不可访问这几个函数的。


           那这样,tp.event.on就变得非常简单了:


tp.event.on = function(element,event,fn) {
		_addEventListener(element,event,fn,false);
         };
          而且这样还有一个好处,之前的方式只能采用冒泡方式,但现在,可以使用捕获,当然,只能非IE才能使用,这样在后面使用事件代理一些非冒泡的事件的时候非常有用,比如blur和focus事件。


           除了事件监听器,还需要事件事件的添加,删除等,也就是add,fire,remove等,这里就不说了。

          在使用事件代理的时候,我们经常要获取到事件的目标元素,而IE和非IE又是不一样的,所以需要单独写一个函数:


tp.event.getTarget = function(event) {
		return event.target || event.srcElement;
	};
          常用的功能当然还是阻止事件冒泡以及阻止默认事件的发生,很遗憾,IE和非IE处理方式还是不一样的,比如阻止冒泡IE采用的是cancelBubble,而其他浏览器采用的是stopPropagation,所以还是需要写:



tp.event.preventDefault = function(event) {
		if(event.preventDefault) {
			event.preventDefault();
		} else {
			event.returnValue = false;
		}
	};
	tp.event.stopPropagation = function(event) {
		if(event.stopPropagation) {
			event.stopPropagation();
		} else {
			event.cancelBubble = true;
		}
	};
         事件这一块儿实际上我做了N多东西,但是由于讲不完,所以暂时不说了。

        注意一下啊,由于JS变量作用域没有block,所以请不要使用下面这种:

var arr = new Array();
if(xxx) {
   for(var i = 0,len = arr.length ; i < len; i++) {

   }
} else {
   for(var i = 0,len = arr.length ; i < len; i++) {

   }
}
      这样使用变量i已经被重复定义了,所以需要把变量i定义在if之前,即:
var arr = new Array(),
    i;

          事件之后,当然就是DOM了,感觉每个库在这个上面都做了很多工作。

         首先是ready的判定,关于这个可以看我另外一篇日志:http://my.oschina.net/mingtingling/blog/110282

         这里我主要讲一下tp.dom.query,也就是查询怎么做的,首先看看常用的查询有:#aa,.aa,input。

         #aa这种比较简单,因为JS提供了API,也就是document.getElementById;input这种也比较好搞,因为JS有document.getElementsByTagName;但是.aa这种方式就比较纠结了,因为JS没有提供API,幸好,在一些浏览器中还是提供了API:document.getElementsByClassName,而那些没有提供这个API的就比较悲剧了,只能遍历所有节点,也就是使用document.getElementsByTagName(*):

          我这儿写了一个辅助函数:


var _getElementsByClassName = null;
		if(document.getElementsByClassName) {
				_getElementsByClassName = function(str) {
					var fetchedEles = document.getElementsByClassName(str),
						eles = [];
				
					for(var i = 0, len = fetchedEles.length; i < len; i++) {
						eles.push(fetchedEles[i]);
					}
					return eles;
				};
		} else {
			_getElementsByClassName = function(str,openClassScan) {
				var eles = [],
					allElements = document.getElementsByTagName("*"),
					i;
				if(openClassScan) {
					for (i = 0; i< allElements.length; i++ ) {
						if (tp.dom.containClass(allElements[i],str)) {
							eles.push(allElements[i]);
						}
					}
				} else {
					for (i = 0; i< allElements.length; i++ ) {
						if (str === allElements[i].className) {
							eles.push(allElements[i]);
						}
					}
				}
				return eles;
			};
		}
            我这儿写了一个openClassScan参数,解释一下,这个参数是为了解决类似于<div class = "a b"></div>这种,因为如果要支持通过API查询如class:a,那么需要每个节点都判定是否contain这个class,比较费时间,而我认为很多时候不需要,所以默认我关闭了。


            PS:使用了原生的document.getElementsByClassName的肯定不受这个影响的。

           我把每一个查询如:tp.dom.query("#aa input")分为两种,一种为简单查询(也就是如查询:#aaa),另外一种是复杂查询,每个复杂查询都是由很多简单查询构成的,比如#aaa input,就可以切成:#aaa和input。

           所以,在每个查询的最开始,需要将传递的查询格式化,比如#aa >input这种格式化为:#aa > input,多个空格变为1个空格,>两边必须有一个空格等。

           之后写一个辅助函数,判定是否是复杂查询,如果是,那么切开查询字符串,切成数组。

           我认为:#aa input这种实际上就是通过document.getElementById查询之后然后查询它的子孙节点中的所有满足tagName为input的元素;而#aaa > input这种就是查询它的孩子节点中是否有这种满足条件的元素。现在整个流程比较简单了,对于一个复杂查询,首先进行一个简单查询,然后按照查询的结果集合,进行一次遍历,对每个节点查询它的孩子节点或子孙节点,将所有满足条件的放入到另外一个数组,如果该数组为空,那么直接返回空数组,否则,继续进行下一次查询(依旧查询孩子节点或子孙节点)。

           我认为,就这样一个功能比较简单的query就够了,不必要实现类似于jquery里面的如此复杂的查询,如果要使用它,其实也很简单,因为jquery的查询引擎sizzle已经开源,完全可以将它加入到这个库,而现在toper也是这么做的,要调用sizzle就使用:


tp.use("tp.dom.sizzle",function(sizzle) {});
          感觉JS的兼容性真心很头疼啊,就比如在DOM这一块儿,为了兼容,我都做了很长时间。当然,DOM这一块儿肯定不止这么一点内容,暂时也不写了。


          除了DOM,对变量类型的判定和浏览器的检测也是很重要的。

         首先,类型判定,由于JS是弱类型语言,而有时候是需要判定它的类型的,当然也可以使用typeof 去判定,暂时我是这么做的:

  

tp.type = tp.type || {};
tp.type.isArray = function(ele) {
	return "[object Array]" === Object.prototype.toString.call(ele);
};
tp.type.isFunction = function(ele) {
	return "[object Function]" === Object.prototype.toString.call(ele);
};
tp.type.isObject = function(ele) {
	return ("function" === typeof ele) || !!(ele && "object" === typeof ele);
};
tp.type.isString = function(ele) {
	return "[object String]" === Object.prototype.toString.call(ele);
};
tp.type.isNumber = function(ele) {
	return "[object Number]" === Object.prototype.toString.call(ele) && isFinite(ele);
};
tp.type.isBoolean = function(ele) {
	return "boolean" === typeof ele;
};
tp.type.isElement = function(ele) {
	return ele && ele.nodeType == 1;
};
tp.type.isUndefined = function(ele) {
	return "undefined" === typeof ele;
};


        我看了一下,不同的库的判定方式不一样,我这儿使用的是tangram的判定方式。

        然后就是浏览器判定,我是这么写的:


(function() {
	var ua = navigator.userAgent;
	tp.browser.isIe = ua.hasString("MSIE") && !ua.hasString("Opera");
	tp.browser.isFirefox = ua.hasString("Firefox");
	tp.browser.isChrome = ua.hasString("Chrome");
	tp.browser.isWebKit = ua.hasString("WebKit");
	tp.browser.isGecko = ua.hasString("Gecko") && !ua.hasString("like Gecko");
	tp.browser.isOpera = ua.hasString("Opera");
	tp.browser.isSafari = ua.hasString("Safari") && !ua.hasString('Chrome');
	tp.browser.isStrict = ("CSS1Compat" === document.compatMode);
})();
       当然,还有浏览器版本的判定,暂时就不贴出来了。这里基本思路就是判定navigator.useAgent返回的字符串中,每个浏览器里面的这个字符串是不一样的,当然,这个过程比较恶心,而且有可能后面某一个浏览器会改变它的userAgent,导致整个判定失效,比如IE,听别人说后面新IE要把userAgent搞成firefox,真心搞不懂,这是要逆天吗?


       除了这种判定方式,还可以通过判定是否有某一个函数或某一个变量来判定,这种判定方式我忘记叫什么名字了,反正之前这种叫浏览器嗅探。

       除了代码之外,工具也很重要,另一篇日志介绍JS工具的:http://my.oschina.net/mingtingling/blog/113295

        对动画有兴趣的童鞋,可以看看我的最近学习JS的感悟-2,关于动画的http://my.oschina.net/mingtingling/blog/127296

       好吧,貌似又超时了,先就这样吧,感觉每次写这种日志都会耗费不少时间。


你可能感兴趣的:(js,明庭令,js库,Toper)