JavaScript框架设计:种子模块核心机制解析
本文深入解析JavaScript框架中的种子模块,涵盖命名空间构建、对象扩展、数组化转换、类型判定及DOMReady机制。以mass Framework为范本,对比jQuery、Prototype等主流库的实现差异,阐述如何构建稳定、可扩展且无冲突的基础设施。
种子模块概述
种子模块,亦称核心模块,是JavaScript框架中最早执行的部分。即便如jQuery这般单文件函数库,内部也划分为多个模块,其中部分模块需在初始化时立即执行,而另一些则仅在特定功能被调用时才加载。部分模块可能并非必需,存在感较低,仅在特定浏览器环境下运行。
作为框架的先锋,种子模块内的方法未必要求功能面面俱到或设计完美,但必须具备极强的扩展性、高频使用性以及稳定性。
扩展性意味着能够通过它们引入其他模块;常用性确保绝大多数模块都能依赖它们,避免重复开发;稳定性则保证在版本迭代过程中,这些核心功能不易被新方法轻易替代。
参考众多框架与库的实现,种子模块通常具备以下功能:对象扩展、数组化、类型判定、简单的绑定与卸载、无冲突处理、模块加载以及DOMReady。本章内容以mass Framework的种子模块实现为范本进行讲解。
https://github.com/RubyLouvre/mass-Framework
命名空间构建
1. 命名空间
种子模块作为框架的起始部分,负责构建全局基础设施。jQuery提供了一个优秀的开篇示例,即使用IIFE(立即调用函数表达式)。
IIFE是现代JavaScript框架中最基础的基础设施,它如同细胞般包裹自身,防止变量污染。这构成了一个立足点,即命名空间。例如prototype.js和mootools,它们让用户感觉不到框架的存在,其意义深远至JavaScript、DOM、BOM等整个执行环境的各个角落,甚至对原生对象原型进行扩展。尽管道格拉斯(JSON作者)曾极力反对,但新框架均在命名空间上进行了构建。
我们探讨如何在JavaScript中模拟命名空间。JavaScript一切皆对象,但只有符合特定类型的对象才符合要求,如function、RegExp、Object,其中最常用的是object和function。通过向一个对象添加属性,而该属性又是一个对象,并继续为其添加子对象,我们可以有条不紊地构建框架。用户调用方法时,通常采用xxx.yyy.zzz()的形式。
if( typeof(Ten) === "undefined" ){
Ten = {};
Ten.Function = { /*略*/ }
Ten.Array = { /*略*/ }
Ten.Class = { /*略*/ }
Ten.JSONP = new Ten.Class(/*略*/ )
Ten.XHR = new Ten.Class(/*略*/ )
}
纵观各大类库的实现,初期通常定义一个全局变量作为命名空间,然后对其进行扩展。例如Base2的Base、Ext的Ext、jQuery的jQuery、YUI的YUI、dojo的dojo以及MochiKit的mochKit。根据全局变量污染程度的不同,可分为两类:
prototype.js、mootools与Base2归为一类。Prototype的哲学是对JavaScript原生对象进行扩展,早年它差点成为事实标准,因此未充分考虑与其他库共存的问题。基于Prototype,衍生出script.aculo.us、rico、Plotr、protoChart、Script2等优秀类库及大量收费插件。许多有渊源的插件,如lightBox,几乎都与Prototype有关。mootools是Prototype的升级版,更具面向对象特性,全面复制其API。Base2则旨在修复IE的bug,使IE拥有标准浏览器的API,因此也污染了所有原生对象。
第二类包括jQuery、YUI和Ext。YUI和Ext采用对象嵌套对象的方式构建。jQuery则另辟蹊径,以选择器为导向,因此其命名空间是一个函数,方便用户传入CSS表达式字符串,通过选择器查找后返回jQuery对象实例。
jQuery最初也像Prototype使用$作为命名空间,因此实现了多库共存机制,可在$和jQuery之间任意切换。jQuery的多库共存原理简单,后来成为许多小库的标配。首先,将命名空间保存到一个临时变量中(此时该对象并非自身框架的东西,可能是prototype.js或其他库),然后再使用noConflict方法将其放回去。
//jQuery1.2
var _jQuery = window.jQury , _$ = window.$; //把可能存在同名变量先保存起来
jQury.extend({
noConflict : function(deep) {
window.$ = _$; //这时再放回去
if (deep) //补充 if ( deep && window.jQuery === jQuery )
window.jQury = _jQuery;
return jQury;
}
})
参考:http://zhidao.baidu.com/question/1239712776390687219.html
但jQuery的noConflict仅对单文件类库框架有效,如Ext便无法直接复制。因此,将命名空间改名后,将Ext置为null,然后通过动态加载方法引入新的JavaScript文件。该文件以Ext调用,会导致报错。
对象扩展机制
2. 对象扩展
我们需要一种机制,将新功能添加到命名空间中。这种方法在JavaScript中通常称为extend或mixin。在属性描述符(Property Descriptor)诞生之前,JavaScript对象可以随意添加、更改、删除其成员,因此扩展对象非常便捷。一个简单的扩展方法实现如下。
function extend (destination,source){
for (var property in source)
destination[property] = source[property];
return destination;
}
不过,旧版本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方法,支持多对象合并与选择是否覆写。
function mix(target,source){ //如果最后参数是布尔,判定是否覆盖同名属性
var args = [].slice.call(arguments), i = 1, key,
ride = typeof args[args.length - 1] == "boolean" ? args.pop() : true;
if (args.length === 1){ //处理$.mix(hash)的情形
target = !this.window ? this : {};
i = 0;
}
while ((source = args[i++])) {
for (key in source){ //允许对象糅杂,用户保证都是对象
if (ride || !(key in target)) {
target[key] = source[key];
}
}
}
return target;
}
数组化转换
3. 数组化
浏览器中存在许多类数组对象,如函数内的arguments,通过document.forms、form.elements、document.links、select.options、document.getElementsByName、document.getElementsByTagName、childNodes、children等方式获取的节点集合(HTMLCollection、NodeList),或按特殊写法自定义的对象。
类数组对象是一种良好的存储结构,但功能较弱。为了使用纯数组的便捷方法,我们在处理它们之前通常会进行转换。
通常来说,使用[].slice.call即可完成转换,但功能不够全面。在旧版本的HTMLCollection、NodeList中,它们并非Object的子类,采用上述方法会导致IE执行异常。我们看一下
jQuery:makeArray
var makeArray = function(array) {
var ret = [] ;
if(array != null){
var i = array.length;
if(i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval)
ret[0] = array;
else
while (i)
ret(--i) = array[i];
}
return ret;
}
mass的实现,一开始就进行区分,直接调用[].slice.call,对于IE则手动实现一个slice方法
$.slice = window.dispatchEvent ? function(nodes,start,end){
return [].slice.call(nodes,start,end);
} : function (nodes,start,end){
var ret = [],
n = nodes.length;
if (end === void 0 || typeof end === "number" && isFinite(end)){
start = parseInt (start,0) || 0;
end = end == void 0 ? n:parseInt (end,10);
if(start < 0){
start += n;
}
if (end > n) {
end =n
};
if (end < 0) {
end += n
};
for (var i = start; i < end; ++i){
ret[i-start] = nodes[i];
}
}
return ret;
}
类型判定方法
4. 类型的判定
JavaScript存在两套类型系统:一套是基本数据类型,另一套是对象类型系统。基本数据类型包括6种,分别是undefined、string、null、boolean、function、object。基本数据类型通过typeof检测。对象类型系统以基础类型系统为基础,通过instanceof检测。然而,JavaScript自带的这两套识别机制非常不靠谱,于是催生了isXXX系列。以typeof为例,它只能粗略识别出string、number、boolean、function、undefined、object这6种数据类型,无法识别null、RegExp、Argument等细分的对象类型。
typeof null // => "object"
typeof document.childNodes //=> safari: "function"
typeof document.creatElement('embed') //=> ff3-10 "function"
typeof document.creatElement('object') //=> ff3-10 "function"
typeof document.creatElement('object') //=> ff3-10 "function"
typeof /\d/i //在实现了ecma262v4的浏览器返回"function"
typeof window.alert //ie678 "object"
var iframe = document.creatElement("iframe")
document.body.appendChild(iframe)
xArray = window.frames[window.frames.length - 1].Array;
var arr = new xArray(1,2,3) //=>[1,2,3]
arr instanceof Array ;// false
isNaN("aaa") //=> true
另外,过去人们常以document.all来判断是否为IE,这其实很危险。虽然用document.all获取页面所有元素是个不错的注意,FF和Chrome打算使用很久了,但人们都这样判断,导致在Chrome下出现这样的闹剧。
typeof document.all //undefined
document.all //HTMLAllCollection [728] (728为元素总数)
判定undefined、null、string、number、boolean、function这六个还算简单,前两个可分别与void(0)、null比较,后四个的typeof也可满足90%的情形。之所以这样说,是因为string、number、boolean可以包装成伪对象。
typeof new Boolean(1); //=>"object"
typeof new Number(1); //=>"object"
typeof new String("aa"); //=> "object"
这些还不是最难的,难点在于RegExp与Array。判定RegExp的情况很少。Array则不同,有关isArray的实现不下二十种,皆因鸭式辨型被打破。直到prototype.js将Object.prototype.toString发掘出来。此方法直接输出内部的[[Class]],绝对精准。有了它,95%的陷阱被跳过。
function isArray(arr){
return arr instanceof Array;
}
function isArray(arr){
return !!arr && arr.constructor === Array;
}
function isArray(arr) { //prototype.js 1.6
return arr != null && typeof arr === "object" && 'splice' in arr && 'join' in arr;
}
function isArray(arr){// Douglas Crockford(JSON作者,反对原型污染)
return typeof arr.sort == "function"
}
function isArray(array){ //kriszyp
var result = false;
try{
new array.constructor (Math.pow(2,32))
} catch (e){
result = /Array/.test(e.message)
}
return result;
};
function isArray(o){//kangax
try{
Array.prototype.toString.call(o);
return true;
} catch (e) {
}
return false;
}
function isArray(o){ //kangax
if(o && typeof o == 'object' && typeof o.length == 'number' && isFinite(o.length))
{
var _origLength = o.length;
o[o.length] = '__test__';
var _newLength = o.length;
o.length = _origLength;
return _newLength == _origLength + 1;
}
return false
}
至于null、undefined、NaN直接这样写
function isNaN(obj) {
return obj !== obj
}
function isNull(obj) {
return obj === null;
}
function isUndefined(obj){
return obj === void 0;
}
最后要判定的是对象是否为window,由于ECMA未规范Host对象,window对象属于Host,因此未被约定。即使Object.prototype.toString也对它没办法
[object Object] IE6
[object Object] IE7
[object Object] IE8
[object Window] IE9
[object Window] ff3.6
[object Window] opera10
[object DOMWindow] safari 4.04
[object global] chrome5.0
不过根据window.window和window.setInterval去判定更加不靠谱,用一个技巧即可完美识别IE6、IE7、IE8的window对象,其他情况仍用toString。这个神奇的hack就是,window与document互相比较,如果顺序不一样,结果也是不一样的!
剩下的就是一些经典方法:
在prototype.js中,拥有isElement、isArray、isHash、isFunction、isString、isNumber、isDate、isUndefined。
mootools有一个typeOf判定基本类型,instanceOf判定自定义“类”。
RightJS有isFunction、isHash、isString、isNumber、isArray、isElement、isNode。
Ext有比较全面的判断,isEmpty、isArray、isDate、isObject、isSimpleObject、isPrimitive、isFunction、isNumber、isMumeric、isString、isBoolean、isElement、isTextNode、isDefined、isIterable,应有尽有。最后,还有typeOf判断基本类型。
Underscore.js有isElement、isEmpty、isArray、isArgument、isObject、isFunction、isString、isNumber、isFinite、isNaN、isBoolean、isDate、isRegExp、isNull、isUndefined。
jQuery就不与其他框架一样了,在jQuery1.4中只有isFunction、isArray、isPlainObject、isEmptyObject。isFunction、isArray用户肯定用得较多,isPlainObject是用来判断是否是纯净的JS对象,既不是DOM、BOM对象,也不是自定义“类”的实例对象,制造它的目的最初是为了深拷贝,避开像window那样自己引用自己的对象。isEmptyObject是用于数据缓存的系统,当此对象为空时,就可以删除它。
//jQuery2.0纯净数组的判定思路
jQuery.isPlainObject = function(obj){
//首先排除基础类型不为Object的类型,然后是DOM节点与window对象
if(jQuery.type(obj) !== "object" || object.nodeType || jQuery.isWindow(obj)){
return false;
}
//然后回溯它的最近的原型对象是否有isPrototypeOf.
//旧版本的IE一些原生的对象没有暴露constructor、prototype。因此在这里过滤掉
try{
if (obj.constructor && !hasOwn.call(obj.constructor.prototype,"isPrototypeOf")){
return false;
}
} case(e) {
return false;
}
return true;
}
avalon.mobile中有一个更精简的版本,只支持最新的浏览器,可以大胆地使用ECMA262v5新API
avalon.isPlainObject = function(obj){
return obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype
}
isArrayLike也是一个常用的方法,但是判定一个类数组太难了,唯一的识别方法就是判断有一个大于0或等于0的length属性,此外,还有一些共识,如window与函数和元素节点(如form元素),不算类数组,虽然它们都满足前面的条件。因此,至今jQuery都没有把它暴露出来。
//jQuery2.0
function isArrayLike(obj){
var length = obj.length , type = jQuery.type(obj);
if (jQuery.isWindow(obj)){
return false;
}
if (obj.nodeType === 1 && length){
return true
}
return type === "array" || type !== "function" && (length === 0 || typeof length === "number" && length > 0 && (length -1) in obj);
}
// avalonjs
function isArrayLike (obj) {
if (obj && typeof obj === "object"){
var n = obj.length
if (+n === n && !( n % 1) && n >= 0){//检测length是否为非负整数
try{
if ({}.prototypeIsEnumerable.call(obj,'length') === false){
return Array.isArray(obj) || /^\s?function/.test(obj.item || obj.callee)
}
return true;
} catch (e) { //IE的NodeList直接报错
return true;
}
}
}
return false
}
在Prototype.js1.3版本中的研究成果(Object.prototype.toString.call)就应用于jQuery,在jQuery1.2中,判断一个变量是否为函数非常复杂。
代码如下:
isFunction : function(fn){
return !!fn && typeof fn != "string" && !fn.nodeName && fn.constructor != Array && /^[\s[]?function/.test(fn + "");
}jQuery1.43中引入isWindow来处理makeArray中对window的判定,引入isNaN用于确保样式赋值的安全。同时引入type代替typeof关键字,用于获取基本数据的基本类型。
class2type = {};
jQuery.each("Boolean Number String Function Array Date RegExpObject".split(" "),function( i , name ){
class2type[ "[object " + name + "]" ] = name.toLowerCase();
});
jQuery.type = function(obj){
return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";
}
jQuery1.7中添加isNumeric代替isNaN。这是个不同于其它框架的isNumber,它可以是个字符串,只要外观上像数字就可以了。但jQuery1.7还做了一个违背之前提到稳定性的事情,冒然去掉jQuery.isNaN,因此,基于旧版jQuery的有大批插件失效。
//jQuery.1.43-1.64
jQuery.isNaN = function ( obj ) {
return obj == null || !rdigit.test( obj ) || isNaN( obj );
}
//jQuery1.7就是isNaN的去反版
jQuery.isNumeric = function ( obj ) {
return obj != null && rdigit.test( obj ) && !isNaN( obj );
}
//jQuery.1.71 - 1.72
jQuery.isNumeric = function ( obj ) {
return !isNaN( parseFloat(obj) ) && isFinite( obj );
}
//jQuery2.1
jQuery.isMumeric = function( obj ) {
return obj - parseFloat(obj) >= 0;
}
massFarmeWork的思路与jQuery一致,尽量减少isXXX系列的代码,把is Window、isNaN、nodeName等方法做了整合。代码较长,既可以获取类型,也可以传入第二参数进行类型比较。
var class2type = {
"[objectHTMLDocument]" : "Document",
"[objectHTMLCollection]" : "NodeList",
"[objectStaticNodeList]" : "NodeList",
"[objectIXMLDOMNodeList]" : "NodeList",
"[objectDOMWindow]" : "window",
"[object global]" : "window",
"Null" : "Null",
"undefined" : "undefined"
},
toString = class2type.toString;
"Boolean,Number,String,Function,Array,Date,RegExp,Window,Document,Arguments,NodeList".replace($.rword,function( name ) {
class2type[ "[object" + name + "]" ] = name;
});
//class2type这个映射几乎把常用的判定对象一网打尽
mass.type = function( obj , str ){
var result = class2type[ (obj == null || obj !== obj) ? obj : toString.call(obj) ] || obj.nodeName || "#";
if(result.charAt(0) === "#") { //兼容旧版浏览器的个别情况,如window.opera
//利用IE678 window === document为true,document === window为false
if( obj == obj.document && obj.document != obj ) {
result = "window"; //返回构造器名称
} else if ( obj.nodeType === 9 ) {
result = "Document";
} else if ( obj.callee) {
result = "Arguments";
} else if ( isFinite(obj.length) && obj.item ) {
result = "NodeList" //处理节点集合
} else {
result = toString.call(obj).slice(8,-1);
}
}
if(str){
result str === result;
}
return result;
}
然后type方法就十分轻松了,用toSring.call(obj)得出值的左键,直接从映射中取得。IE678,我们才费一些周折处理window、document、argument、nodeList等对象。
百度的七巧板基于实用主义,判定也十分严谨。与EXT一样,能想到的写上,并且判定十分严谨。
目前版本2.0.2.5 http://tangram.baidu.com/api#baidu.type()
baidu.isDate = function( unknow ) {
return baidu.type(unknow) == "date" && unknow.toString() != 'Invalid Date' && !isNaN(unknow);
};
baidu.isNumber = function( unknow ) {
return baidu.type(unknow) == "number" && isFinite( unknow );
};
DOMReady机制
5. 主流框架的引入机制-domReady
domReady其实是一种名为"DOMContentLoaded"事件的别称,不过由于框架的需要,它与真正的DOMContentLoaded有一点区别。在很多新手和旧的书中,很多人将其写在window.onload回调中,防止DOM树还没有建完就开始对节点操作。而对于框架来说,越早介入DOM就越好,如要进行特征侦测之类的。domready还可以满足用户提前绑定事件的需求,因为有时页面图片过多等,window.onload事件迟迟不能触发,这时用户操作都没有效果。因此,主流的框架都引入了domReady机制,并且费了很大周折才兼容所有浏览器。具体的策略如下:
对于支持DOMContentLoaded事件使用DOMcontentLoaded事件 旧版本IE使用Diego perini发现的著名Hack
//by Diego Perini 2007.10.5
function IEContentLoaded (w, fn) {
var d = w.document, done = false,
// 只执行一次用户的回调函数init()
init = function () {
if (!done) {
done = true;
fn();
}
};
(function () {
try {
// DOM树未创建完之前调用doScroll会抛出错误
d.documentElement.doScroll('left');
} catch (e) {
//延迟再试一次~
setTimeout(arguments.callee, 50);
return;
}
// 没有错误就表示DOM树创建完毕,然后立马执行用户回调
init();
})();
//监听document的加载状态
d.onreadystatechange = function() {
// 如果用户是在domReady之后绑定的函数,就立马执行
if (d.readyState == 'complete') {
d.onreadystatechange = null;
init();
}
};
}
此外,IE还可以通过script defer hack进行判定
document.write("<script id=__ie_onload defer src=//0 mce_src=http://0></scr"+"ipt>");
script = document.getElementById("__ie_onload");
script.onreadystatechange = function() { //IE即使是死链也能触发事件
if (this.readyState == "complete")
init(); // 指定了defer的script在dom树建完才触发
};
不过还有个问题,如果我们的种子模块是动态加载的,在它插入DOM树时,DOM树是否已经建完呢?这该怎么触发ready回调?jQuery的方案是,连onload也监听了,但如果连onload也没赶上,就判定document.readyState等于complete。(完美)可惜FF3.6之前没有这属性,看mass的方案
var readyList = [];
mess.ready = function( fn ) {
if ( readyList ) {
fn.push( fn );
} else {
fn();
}
}
var readyFn ,ready = W3C ? "DOMContentLoaded" : "readyStatechange";
function fireReady() {
for (var i = 0 , fn; fn = readyList[i++]) {
fn();
}
readyList = null;
fireReady = $.noop; //惰性函数,防止IE9调用_checkDeps
}
function doScrollCheck() {
try { //IE下通过doScrollCheck检测DOM树是否建设完
html.doScroll("left");
fireReady();
} catch (e){
setTimeout(doScrollCheck);
}
}
//FF3.6前,没有readyState属性
if (!document.readyState){
var readyState = document.readyState = document.body ? "complete" : "loading";
if (document.readyState === "complete") {
fireReady(); //如果在domReay之外加载
} else {
$.bind(document,ready,readyFn = function(){
if(W3C || document.readyState === "complete") {
fireReady();
if(readyState){ //IE下不能该项document.readyState
document.readyState = "complete";
}
}
});
if (html.doScroll){
try {//如果跨域就报错,证明有两个窗口
if (self.eval === parent.eval) {
doScrollCheck();
}
} catch (e) {
doScrollCheck();
}
}
}
}
无冲突处理
6. 无冲突处理
无冲突处理也叫多库共存,$是这个重要的函数名,以至于大家都爱拿它来做自己的命名空间。当jQuery开始发展时,Prototype是主流,jQuery发明了noConflict函数
var window = this,
undefined,
_jQuery = window.jQuery,
_$ = window.$,
//将window存入闭包中的同名变量,方便内部函数调用windows不用太麻烦查找它。
//_jQuery与_$用于以后重写
jQuery = window.jQuery = window.$ = function(selector, context){
//用于返回一个jQuery对象
return new jQuery.fn.init(selector,context);
}
jQuery.extend({
noConflict : function(deep) {
//引入jQuery类库后,闭包外边的window.$与window.jQuery都储存着一个函数
//它是用来生成jQuery对象或在domReady后执行里面的函数
//回顾最上面的代码,在还没有把function赋值给它们时,_jQuery和_$已经被赋值了,因此,它们两的值必然是undefined
//因此,这种放弃控制权的技术很简单,就是用undefined把window.$里边jQuery函数清除掉。
//这时,prototype或mootools的$就被拿走了
window.$ = _$; //相当于window.$ = undefined,如果你有一个叫jQuery的库,也能大方的过渡出去。
//这时,需要给noConflict添加一个布尔值,true
if (deep)
//但我们必须使用一个东西接纳jQuery与jQuey的入口函数
//闭包里边的东西除非被window等宿主引用,否则是不可见的
//因此,我们把闭包里的jQuery return出去,外面用一个变量接纳就可以
window.jQuery = _jQuery; //相当window.jQuery = undefined
return jQuery;
}
})
使用时,先引入别人的库,然后引入jQuery,使用调用$.noConflict()进行改名,这样就不影响别人的$运行了。
mass的操作方式是在script标签上定义一个nick属性,那么释放出来的命名空间就是你的那个属性值。里面实现了类似jQuery的机制。
<script nike="aaa" src="mass.js"></script>
<script>
aaa.log("xxxxxx")
</script>
以上所述就是本文的全部内容了,希望大家能够喜欢。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
