1. 种子模块
种子模块也叫核心模块,是框架的最先执行的部分。
粽子模块包含功能:对象扩展,数组化,类型判定,简单的事件绑定与卸载,无冲突处理,模块加载与domReady。本章讲解以mass Framework的种子模块为范本。
1.1 命名空间
种子模块作为一个框架的最开始部分,除了负责辅建全局的基础设施外,你有没有想到给读者一个震撼的开场呢?俗话说,好的开头时成功的一半。
时下“霸主”jQuery 就有一个很好的开头——IIFE(立即调用函数表达式),一下子吸引住读者,让读者吃了一颗定心丸。
IIFE是现代JavaScript框架最主要的基础设施,它像细胞膜一样包裹自身,防止变量污染。但我们总得在Window里设置一个立足点,这个就是命名空间。
1 if(typeof(Ten) === "undefined") { 2 Ten = {}; 3 Ten.Function = {} 4 Ten.Array = {} 5 Ten.Class = {} 6 Ten.JSONP = new Ten.Class() 7 Ten.XHR = new Ten.Class() 8 9 }
纵观各大类库的实现,一开始基本都是定义一个全局变量作为命名空间,然后对它进行扩展,如Base2的Base、Ext的Ext,jQuery的jQuery、YUI的YUI、dojo的dojo等。从全局变量的污染程度来看,分为两大类。
Prototype、mootools与Base2归为一类。Prototype的哲学是对JavaScript原生对象进行扩展。
第二类是jQuery、YUI、EXT这些框架。YUI与EXT就像上面给出的代码那样,以叠罗汉方式构架的。jQuery则另辟蹊径,它是以选择器为导向的,因此它的命名空间是一个函数,方便用户把CSS表达式字符串传进来,然后通过选择器引擎进行查找,最后返回一个jQuery实例。jQuery初期非常弱小,它想让别人用自己的框架,但也想像Prototype那样使用美元符号作为命名空间。因此它特意实现了多库共存机制,在$,jQuery与用户指定的命名空间中任意切换。
jQuery的多库共存原理很简单,因此后来也成为许多小库的标配。首先把命名空间保存到一个临时变量中,注意这时这个对象并不是自己框架的东西,可能是Prototype.js等巨头的,然后再搞个noConflict放回去。
1 _jQuery = window.jQuery, _$ = window.$; //先把可能存在的同名变量保存起来 2 3 jQuery.extend({ 4 noConflict: function(deep) { 5 window.$ = _$;//这时再放回去 6 if(deep) { 7 window.jQuery = _jQuery; 8 } 9 return jQuery; 10 };
但jQuery的noConflict只对单文件的类库框架有用,想EXT就不能复制了。因此把命名空间改名后,将EXT置为null,然后又通过动态加载方式引入新的JavaScript文件,该文件再以EXT调用,将会导致报错。
mass Framework对JQuery的多库共存进行改进,它与jQuery一样有两个命名空间,一个是美元符号,一个是根据URL动态生成的长命名空间(jQuery就是jquery)
namespace=DOC.URL.replace(/(#.+|W)/g,'');
短的命名空随便用户改名,长的命名空间则是加载新的模块时用的,虽然用户在模块中使用$做命名空间,但当JavaScript问及加载下来时,我们会对立面的内容再包一层,将$指向正确的对象,具体实现见define方法。
1.2 对象扩展
我们需要一种机制,将新功能添加到我们的命名空间上。这方法在JavaScript通常被称做extend或mixin。JavaScript对象在属性描述符(Property Descriptor)没有诞生之前,是可以随意添加、更改、删除其成员,因此扩展一个对象非常便捷。一个简单的扩展方法实现是这样。
1 function extend(destination,source){ 2 for(var property in source) 3 destination[property]=source[property] 4 return destination; 5 }
不过,旧版本IE在这里有个问题,它认为像Object的原型方法就是不应该被遍历出来,因此for in循环是无法遍历名为valueOf、toString的属性名。这导致,后来人们模拟Object.keys方法实现时也遇到了这个问题。
Object.keys = Object.keys || function(obj) { var a = []; for(a[a.length] in obj); return a; }
不同的框架,这个方法不同的实现,如EXT分为apply与applyIf两个方法,前者会覆盖目标对象的同名属性,而后者不会。dojo允许多个对象合并在一起。jQuery还支持深拷贝。下面是mass Framework的mix方法,支持多对象合并与选择是否覆写。
1 function mix(target, source) { //如果最后参数是布尔,判定是否覆写同名属性 2 var args = [].slice.call(arguments), 3 i = 1, 4 key, ride = typeof args[args.length - 1] == "boolean" ? args.pop() : true; 5 6 if(args.length === 1) { //处理$.mix(hash)的情形 7 target = !this.window ? this : {}; 8 i = 0; 9 } 10 while(source = arge[i++]) { 11 for(key in source) { 12 if(ride || !(key in target)) { 13 target[key] = source[key]; 14 } 15 } 16 } 17 return target; 18 }
1.3 数组化
浏览器下存在许多类数组对象,如function内的arguments,通过document.forms、form.elements、document.links,select.options、document.getElementsByName、childNodes、children等方式获取的节点集合(HTMLCollection、NodeList),或依照某些特殊写法的自定义对象。
1 var arrarLike={ 2 0:"a", 3 1:"1", 4 2:"2", 5 length:3 6 }
类数组对象是一个很好的存储结构,不过功能太弱了,为了享受纯数组的那些便捷方法,我们在处理它们前都会做一下转换。
通常来说,只要[].slice.call 就能转换了,但旧版本IE下的HTMLCollection、NodeList不是Object的子类,采用如上方法将导致IE执行异常。我们看一下各大库怎么处理。
1 //jQuery的makeArray 2 var makeArray = function(array, results) { 3 var ret = results || []; 4 5 if(array != null) { 6 // The window, strings (and functions) also have 'length' 7 8 if(array.length == null || type === "string" || type === "function") 9 ret[0] = array; 10 else 11 while(i) 12 ret[--i] = array[i]; 13 14 } 15 return ret; 16 }
jQuery对象是用来储存与处理dom元素的,它主要依赖于setArray方法来设置和维护长度与索引,而setArray的参数要求是一个数组,因此makeArray的地位非常重要。这方法保证就算没有参数也要返回一个空数组。
Prototype.js的$A方法:
1 function $A(iterable){ 2 if(iterable) 3 return []; 4 if(iterable.toArray) 5 return iterable.toArray(); 6 var length=iterable.length ||0,results=new Array(length); 7 while(length--) 8 results[length]=iterable[length]; 9 return results; 10 }
mootools的$A方法:
1 function $A(iterable) { 2 if(iterable.item) { 3 var l = iterable.length, 4 array = new Array(l); 5 while (l--) 6 array[l]=iterable[l]; 7 return array; 8 } 9 return Array.prototype.slice.call(iterable); 10 }
Ext的toArray()方法:
1 var toArray = function() { 2 returnisIE ? 3 function(a, i, j, res) { 4 res = []; 5 Ext.each(a, function(v) { 6 res.push(v); 7 }); 8 } : 9 function(a, i, j) { 10 return Array.prototype.slice.call(a, i || 0, j || a.length); 11 } 12 }();
Ext的设计比较巧妙,功能也比较强大。它开始就自动执行自身,以后就不用判定浏览器了。它还有两个可选参数,对生成的数组进行操作。
dojo的toArray和Ext一样,后面两个参数是可选的,只不过第二个是偏移量,最后一个是已有的数组,用于把新生的新组元素合并过去。
1.4 类型的判定
JavaScript存在两套类型系统,一套是基本数据类型,另一套是对象类型系统。基本数据类型包括6种,分别是:undefined,string,null,boolean,function,object。基本数据类型是通过typeof来检测的。对象类型系统是以基础类型系统为基础的,通过instanceof来检测。然而,JavaScript自带的这两套识别机制非常不靠谱,于是催生了isXXX系列。就拿typeof来说,它只能粗略识别出string,number,boolean,function,undefined,object这6种数据类型,无法识别Null、RegExp等细分对象类型。
让我们看一下这里面究竟有多少陷阱。
1 typeof null; //"object" 2 typeof document.childNodes; //safari "function" 3 typeof document.createElement("embed"); //"function" 4 typeof /d/i; //实现了ecma262v4的浏览器返回"function" 5 typeof window.alert; //"IE678 "object" 6 7 var iframe = document.createElement('iframe'); 8 document.body.appendChild(iframe); 9 xArray = window.frames[window.frames.length - 1].Array; 10 var arr = new xArray(1, 2, 3); //[1,2,3] 11 arr instanceof Array; //false 12 arr.constructor === Array; //false 13 14 window.onload = function() { 15 alert(window.constructor); //IE67 undefined 16 alert(document.constructor);//IE67 undefined 17 alert(document.body.constructor);//IE67 undefined 18 alert((new ActiveXObject('Microsoft.XMLHTTP')).constructor)//IE6789 undefined 19 } 20 21 isNaN("aaa");//true 22
上面分4组,第一组是typeof的坑。第二组是instanceof的陷阱,只是原型上存在此对象的构造器它就返回true,但如果跨文档比较,iframe里面的数组实例就不是父窗口的Array的实例。第三组相关constructor的陷阱,在旧版本IE下DOM与BOM对象的constructor属性是没有暴露出来的。最后有关NaN,NaN对象与null,undefined一样,在序列化时是原样输出的,但isNaN这方法非常不靠谱,把字符串、对象放进去也返回true,这对我们的序列化非常不利。
另外,在IE下typeof 还会返回unknow 的情况。
另外,以前人们总是以document.all(在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。这个概念的名字来源于由James Whitcomb Riley 提出的鸭子测试)是否存在来判定IE,这其实是很危险的。因为用document.all 来获取页面中的所有元素是不错的注意,这个方法Firefox,Chrome觊觎好久了,不过人们都这样判定,于是有了chrome这样的闹剧。
在判定undefined、null、string、number、boolean、function这6个还算简单,前面两个可以分别于void(0)、null比较,后面4个直接typeof也可满足90%的情形。这样说是因为string,number,boolean可以包装成“伪对象”,typeof无法按照我们的意愿工作了。
typeof new Boolean(1);//"object" typeof new Number(1);//"object" typeof new String("1");//"object"
这些还是最简单的,难点在于RegExp与Array。判定RegExp类型的情形很少,Array则不一样。
isArray 早些年的探索:
1 function isArray(arr) { 2 return arr instanceof Array; 3 } 4 5 function isArray(arr) { 6 return !!arr && arr.constructor == Array; 7 } 8 9 function isArray(arr) { 10 return arr != null && typeof arr === "object" && 'splice' in arr && 'join' in arr; 11 } 12 13 function isArray(array) { 14 var result = false; 15 try { 16 new array.constructor(Math.pow(2, 32)) 17 } catch(e) { 18 result = /Array/.test(e.message) 19 } 20 return result; 21 }
至于null、undefined、NaN直接这样
1 function isNaN(obj) { 2 return obj !== obj; 3 } 4 5 function isNull(obj) { 6 return obj === null; 7 } 8 9 function isUndefined(obj) { 10 return obj === void 0; 11 }
最后要判定的对象是window,用于ECMA是不规范Host对象,window对象属于Host,所以也没有被约定,就算Object.prototype.toString 也对它无可奈何。
1 jQuery.isPlainObject = function(obj) { 2 //首先排除基础类型不为Object的类型,然后是DOM节点与window对象 3 if(jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow(obj)) { 4 return false; 5 } 6 //然后回溯它的最近的原型对象是否有isPlainObject 7 //旧版本IE的一些原生对象没有暴露constructor、Prototype,因此会在这里过滤 8 try { 9 if(obj.constructor && !hasOwn.call(obj.constructor.prototype, "isPlainObject")) { 10 return false; 11 } 12 } catch(e) { 13 return false; 14 } 15 return true; 16 }
1.5 主流框架引入的机制——domReady
domReady其实是一种名为“DOMContentLoaded”事件的别称,不过由于框架的需要,它与真正的DOMContentLoaded有一点区别。在许多旧的JavaScript数据中,它们都会教导我们把JavaScript逻辑写在window.onload回调中,以防DOM树还没有建完就开始对节点操作,导致出错。而对于框架来说,越早介入对DOM的干涉就越好,如要进行什么特征侦测之类的。domReady还可以满足用户提前绑定时间的需求,因为有事页面图片等资源过多,window.onload就迟迟不能触发,这时若没有绑定事件,用户点哪个按钮没反应。因此主流框架都引入了domReady机制,并且费了很大劲兼容所有的浏览器,具体策略如下。
(1)对于支持DOMContentLoaded事件的使用DOMContentLoaded事件。
(2)旧版本IE使用Diego Perini发现的著名hack!
1 function IEContentLoaded(w, fn) { 2 var d = w.document, 3 done = false, 4 init = function() { 5 if(!done) { 6 done = true; 7 fn(); 8 } 9 }; 10 (function() { 11 try {//在DOM未建完之前调用元素doScroll抛出错误 12 d.documentElement.doScroll("left"); 13 } catch(e) { 14 setTimeout(arguments.callee, 50); 15 return; 16 } 17 init();//没有错误则执行用户回调 18 })(); 19 //如果用户是在domReady之后绑定这个函数呢?立即执行它 20 d.onreadystatechange = function() { 21 if(d.readyState == 'complete') { 22 d.onreadystatechange = null; 23 init(); 24 } 25 }; 26 }
此外,IE还可以通过script defer hack进行判定。
http://www.cnblogs.com/pigtail/archive/2012/06/18/2553556.html
1.6 无冲突处理
无冲突处理也叫多库共存。不得不说,$是最重要的函数名,这么多框架都爱用它做自己的命名空间。在jQuery还比较弱小的时候,如何让人们使用它呢?当时Prototype是主流,jQuery于是发明了noConflict函数,下面是源代码:
1 var window = this, 2 undefined, 3 _jQuery = window.jQuery, 4 _$ = window.$; 5 //把window存入闭包中的同名变量,方便内部函数在调用window时不用费大力气查找它 6 //_jQuery与_$用于以后重写 7 jQuery = window.jQuery = window.$ = function(selector, context) { 8 //用于返回一个jQuery对象 9 return new jQuery.fn.init(selector, context); 10 } 11 jQuery.extend({ 12 noConflict: function(deep) { 13 //引入jQuery类库后,闭包外面的window.$与window.jQuery都存储着一个函数 14 //它是用来生成jQuery对象或在domReady后执行里面的函数的 15 //回顾最上面的代码,在还没有把function赋给它们时,_jQuery与_$已经被赋值了 16 17 //因此它们俩的值比如是undefined 18 //因此这种放弃控制权的技术很简单,就是用undefined把window.$里面的jQuery系的函数清除掉 19 //这时Prototype或mootools的$就可以使用了 20 21 window.$ = _$; //相当于window.$=undefined 22 23 //如果连你的程序也有一个jQuery的东西,jQuery可以大方地连这个也让渡出去 24 //这时就要为noConflict添加一个布尔值,为true 25 if(deep) 26 window.jQuery = _jQuery; //相当于window.jQuery=undefined 27 return jQuery; 28 } 29 });
使用时,先引入别的库,然后引入jQuery,使用调用$.noConflict()进行改名,这样就不影响别的$运作了。
mass Framework更进一步,在引入种子模块的script标签上定义一个nick属性,那么释放出来的命名空间就是你的那个属性值。里面也偷偷实现了jQuery那种机制。
<script nick="AAA" src="mass.js"></script> <script> AAA.log(1); </script>