Home > Article > Web Front-end > JavaScript framework design seed module_javascript skills
The seed module is also called the core module, which is the first part of the framework to be executed. Even a single-file function library like jQuery is divided into many internal modules. Some modules must be executed immediately at the front when executed, and some modules are only executed when used. Some modules are dispensable, have a weak sense of existence, and can only run under specific browsers.
The seed module is one of the pioneers. The methods in it do not necessarily need to be fully functional and well designed, but they must be extremely scalable, commonly used, and stable.
Extensibility means that other modules can be included through them; common use means that most modules can use them to prevent duplication of work. Stability means that it is not easily replaced by new methods during version iterations.
Referring to the implementation of many frameworks and libraries, we believe that the seed module includes the following functions: object expansion, arraying, type determination, simple binding and unloading, conflict-free handling, module loading and domReady. The content of this chapter is based on The mass Framework seed module is a template.
https://github.com/RubyLouvre/mass-Framework
1. Namespace
As the initial part of a framework, the seed module is responsible for assisting in building the global infrastructure. jQuery has a good start, using IIFE (immediately invoked function expression).
LIFE is the most important infrastructure in modern JavaScript frameworks. It wraps itself like a cell to prevent variable pollution. Just like a foothold, this is the namespace, such as prototype.js and mootools. They make you unable to feel the existence of the framework. Its significance reaches every corner of the entire execution environment such as javascript, DOM, BOM, etc., and has a profound impact on native The object prototype can be extended. Due to the strong opposition of Douglas (the author of JSON), new frameworks are built on namespaces.
Let’s see how to simulate namespaces in JavaScript. JavaScript is all based on objects, but only objects that meet the type can meet the requirements, such as function, RegExp, and Object, but the most commonly used ones are object and function. We add an attribute to an object, and this attribute is an object. We can add an object to this object. In this way, we can build our framework in an orderly manner. If the user wants to call a method, he can call it in the form of xxx.yyy.zzz().
if( typeof(Ten) === "undefined" ){ Ten = {}; Ten.Function = { /*略*/ } Ten.Array = { /*略*/ } Ten.Class = { /*略*/ } Ten.JSONP = new Ten.Class(/*略*/ ) Ten.XHR = new Ten.Class(/*略*/ ) }
Looking at the implementation of major libraries, they basically define a global variable as a namespace at the beginning, and then extend it, such as Base2's Base, Ext's Ext, jQuery's jQuery, YUI's YUI, and dojo's dojo, mochKit for MochiKit. From the perspective of the degree of pollution of global variables, they are divided into two categories:
prototype.js and mootools are classified into the same category as Base2. The philosophy of Prototype is to extend the native objects of javascript. In the early years, prototype was almost called the de facto standard. Therefore, the problem of coexistence with other libraries is not considered. Basic Prototype has also developed excellent class libraries such as script.aculo.us, rico, Plotr, protoChart, Script2 and a large category of paid plug-ins. Moreover, almost all plug-ins with some origins are related to Prototype, such as lightBox. mootools is an upgraded version of prototype.js, which is more OO and fully replicates its API. Base2 wants to fix IE bugs and let IE have standard browser APIs, so it also pollutes all native objects.
The second category is jQuery, YUI, and EXT frameworks. YUI and Ext are built by nesting objects within objects. jQuery takes a different approach. It is selector-oriented, so its namespace is a function that facilitates users to pass in the string of the css expressor. It then searches through the selector and finally returns a jQuery object instance.
jQuery initially used $ as its namespace like Prototype. Therefore, it implemented the coexistence mechanism of many libraries. You can switch between $ and jQuery at will. The principle of jQuery's multi-library coexistence is very simple, so it later became the coexistence mechanism of many libraries. Standard configuration for Xiaoku. First, save the namespace into a temporary variable (note that this object is not part of your own framework at this time, it may be prototype.js or others), and then use noConflict to put it back.
//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; } })
Reference: http://zhidao.baidu.com/question/1239712776390687219.html
But jQuery’s noConflict is only useful for single-file class library frameworks, and cannot be copied like Ext. Therefore, after renaming the namespace, set Ext to null, and then introduce it into a new javascript file through dynamic loading. The file will be called with Ext, which will cause an error.
2. Object extension
We need a mechanism to add new functionality to our namespace. This method is usually called extend or mixin in JavaScript. Before the birth of the property descriptor (Property Descriptor), a JavaScript object could add, change, and delete its members at will. Therefore, extending an object is very convenient. A simple extension method implementation looks like this.
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 Farmework的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.数组化
浏览器下存在很多类数组对象,如function内的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、boolearn、function、undefined、object这6种数据类型,无法识别null,RegExpArgument等细分的对象类型。
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比较,后面4个的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,isFunctoion,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中,判断一个变量是否为函数非常复杂。
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 ); };
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>
以上所述就是本文的全部内容了,希望大家能够喜欢。