jQuery Utilities 分类下的函数(或属性)的实现方式分析
程序员文章站
2022-04-26 17:32:14
...
jQuery Utilities 分类下的函数(或属性)的实现方式分析
本文将介绍jQuery Utilities 分类下的函数(或属性)的实现方式。
首先,可以先从 jQuery 官方 API 中找到 Utilities 分类的介绍:
http://api.jquery.com/category/utilities/
其中,不进行分析的函数(或属性)包括:
jQuery.boxMode:已经不推荐使用,可以用 jQUery.support.boxModel 代替
queue相关函数:与动画相关,并且也属于 “Custom” 分类,等分析 “Custom” 分类下的函数时再一并分析
已经分析过的函数(或属性):
jQuery.browser :http://xxing22657-yahoo-com-cn.iteye.com/blog/1035780
jQuery.data() :http://xxing22657-yahoo-com-cn.iteye.com/blog/1042440
jQuery.extend() :http://xxing22657-yahoo-com-cn.iteye.com/blog/1025297
jQuery.unique() :http://xxing22657-yahoo-com-cn.iteye.com/blog/1038480
jQuery.support :http://xxing22657-yahoo-com-cn.iteye.com/blog/1044984
另外,jQuery.isArray(), jQuery.isPlainOject(), jQuery.isWindow(), jQUery.type() 在分析 jQuery.extend() 时已经一并分析。
这里更给出的实现都是简化的版本,没有考虑性能优化、异常处理和某些特殊情况。实际上,jQuery的完整实现完全可以自己在未压缩版的源代码(这里分析的版本是 jQuery 1.6)中 search,因为这里主要讲解实现思路,作为引导和参考,所以下面的分析中将忽略一些次要因素,以避免冲淡主题。
jQuery.globalEval()
通常情况下使用 eval,this 所指向的都是当前对象;有的时候我们希望 this 指向全局对象( window ),就可以使用 jQuery.globalEval() 了。这在动态加载外部 JavaScript 文件并执行时十分有用。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> a = { func : function() { eval("alert(this === window)"); }, globalFunc: function() { $.globalEval("alert(this === window)"); } } a.func(); // alert( flase ) a.globalFunc(); // alert( true ) </script>
可以看到,通常情况下调用附加在对象上的函数,并在函数中使用 eval() 时, this 并不指向 window;但如果使用 globalEval() ,那么 this 就会指向 window 。
简单的实现方式如下:
$ = { globalEval: function(data) { (function() { eval(data); }()) } }
在匿名函数内使用 eval 。由于匿名函数不指向任何一个对象,因此其指向的对象也就是全局对象( window )了。jQuery 的源代码中还先检查了 window.execScript 是否存在,如果存在则优先使用 window.execScript 。
jQuery.parseJSON() 与 jQuery.parseXML()
jQuery 与后台的数据交互最好采用某种规范的格式,经常使用的就是 JSON 和 XML 格式了。从后台得到的数据通常是字符串,还需要将字符串转为 JavaScript 对象,这也就是 jQuery.parJSON() 和 jQuery.parseXML() 的功能了。
jQuery.parseJSON() 的功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> obj = $.parseJSON('{ "s": "string value", "i": 1234 }'); document.write('obj.s = ' + obj.s); document.write('<br />'); document.write('obj.i = ' + obj.i); </script>
显示结果如下:
obj.s = string value obj.i = 1234
jQuery.parseXML() 的功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> str = "<root><customer name='John'></customer></root>"; xml = $.parseXML(str); customer = xml.getElementsByTagName ("customer")[0]; name = customer.getAttribute ("name"); document.write("customer.name = "+ name); </script>
显示结果如下:
customer.name = John
这两个函数的简单实现如下:
$ = { parseJSON: function(data) { return new Function("return " + data)(); }, parseXML : window.DOMParser ? function(data) { var parser = new DOMParser(); return parser.parseFromString(data, "text/xml"); } : function(data) { xml = new ActiveXObject("Microsoft.XMLDOM"); xml.async = "false"; xml.loadXML(data); return xml; } }
parseJSON() 只需要直接创建一个 Function ,并将内容设置为 "return " + data 就可以了。这类似于 eval 。因为 JSON 本身就是 JavaScript 的对象(或数组)的格式,因此转换起来异常简单。同样地,jQuery 的源代码中优先检查了 window.JSON.parse ,如果存在则使用浏览器内置的解析器。虽然可能在性能上略有提升,但也可能导致不同浏览器中的解析行为有所不同(通常我会把这个检查注释掉)。
parseXML() 需要分两种情况,大多数浏览器都可以使用 DOMParser 进行解析,但 IE 中需要使用 ActiveXObject。jQuery 源代码中还有一些抛出 error 的处理。
jQuery.trim()
jQuery.trim() 函数用于去除字符串中的空白字符。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> alert($.trim(' aaaa\r\n\t')); // aaaa </script>
我们可以用正则表达式实现这个功能:
$ = { trim : function(text) { return text == null ? '' : text.toString() .replace(/^\s+/, '') .replace(/\s+$/, ''); } };
jQuery的源代码中还对 String.prototype.trim 进行检查,如果浏览器本身支持这个函数,就优先使用浏览器自带的 trim 函数。
jQuery.isFunction()
该函数用于检查一个变量是否用于表示一个函数的引用。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> function a() {} alert($.isFunction(a)); // true </script>
实现方式如下:
$ = function(){ // implemention of class2type and type(obj) ... return { isFunction: function( obj ) { return type(obj) === "function"; } } }();
其中 type 和 class2type 的实现请参考: http://xxing22657-yahoo-com-cn.iteye.com/blog/1025297
jquery.isEmptyObject()
该函数用于检查一个对象是否为空对象。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> a = {} alert($.isEmptyObject(a)); // true </script>
实现方式如下:
$ = { isEmptyObject: function(obj) { for (var name in obj) { return false; } return true; } };
jQuery.isXMLDoc()
该函数用于检查一个对象是否表示一个 XML 文档。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> str = "<root><customer name='John'></customer></root>"; xml = $.parseXML(str); customer = xml.getElementsByTagName ("customer")[0]; name = customer.getAttribute ("name"); document.write("customer.name = "+ name); </script>
简单实现如下:
$ = { // parseXML : ..., isXMLDoc: function( elem ) { var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; } };
parseXML() 的实现请参考之前的分析。
isXMLDoc() 的实现方式为检查元素的 documentElement 是否存在,存在且不为 HTML ,则表示这是一个 XML 文档。
jQuery.contains()
检查一个 DOM Element 是否包含了另一个 DOM Element。
功能测试代码如下:
<div id="a"> <div id="b" /> </div> <script type="text/javascript" src="jquery.js"></script> <script> window.onload = function() { var a = document.getElementById('a'); var b = document.getElementById('b'); alert($.contains(a, b)); // true alert($.contains(b, a)); // false } </script>
简单实现如下:
$ = { contains: document.documentElement.contains ? function(a, b) { return a !== b && (a.contains ? a.contains(b) : true); } : function(a, b) { return !!(a.compareDocumentPosition(b) & 16); } }
根据不同浏览器的支持情况,借助浏览器内置的 contains() 或 compareDocumentPosition() 函数来实现这个功能。
jQuery.inArray()
命名为 indexOf 可能会更合适些。返回一个元素在数组中的序号,不存在则返回 -1 。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> alert( $.inArray( 3, [1, 2, 3, 4] ) ); // 2 alert( $.inArray( 5, [1, 2, 3, 4] ) ); // -1 </script>
简单实现如下:
$ = { inArray: function(elem, array) { for (var i = 0, length = array.length; i < length; i++) { if (array[i] === elem) { return i; } } return -1; } };
遍历数组,检查是否包含所查找的元素,找到则返回对应的序号,否则返回 -1。除了数组,也可以用于遍历 “类数组”的对象,如 {0:"aaa", 1: "bbb", length: 2}, 函数调用时的隐藏变量 “arguments” 就是典型的“类数组”对象。
jQuery 源代码的实现中,在浏览器支持 Array.prototype.indexOf 的情况下,会优先采用浏览器自带的 indexOf 函数。
jQuery.merge()
合并两个数组或“类数组”的对象( arguments 就是典型的“类数组”)。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> a = ['aaa', 'bbb']; b = ['ccc', 'ddd']; $.merge(a, b); for (i in a) { alert(a[i]); // aaa, bbb, ccc, ddd } a = ['aaa', 'bbb']; b = {0: 'ccc', 1: 'ddd'} $.merge(a, b); for (i in a) { alert(a[i]); // aaa, bbb, ccc, ddd } </script>
简单实现如下:
$ = { merge : function(a, b) { for (var i = a.length, j = 0; b[j] !== undefined; ++i, ++j) { a[i] = b[j]; } a.length = i; return a; } }
jQuery 的源代码中的分类更详细些,数组用 for (i = 0; i < leng; ++i) 的方式进行遍历。这可能也是一种性能优化吧,不过实际上并不必要。
jQuery.makeArray()
将单个对象或“类数组”对象转为数组。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> a = $.makeArray('aaa'); a.push('bbb'); alert(a) // aaa, bbb b = $.makeArray({0:'aaa', 1:'bbb', length: 2}); b.push('ccc'); alert(b) /// aaa, bbb, ccc </script>
简单实现如下:
$ = function(){ // implemention of class2type, type(obj), isWindow(obj), merge(a, b) ... function makeArray(array) { var ret = []; if ( array != null ) { var t = type(array); if ( array.length == null || t === "string" || t === "function" || t === "regexp" || isWindow(array) ) { ret.push(array); } else { merge(ret, array); } } return ret; }; return { merge: merge, makeArray: makeArray } }();
首先检查输入参数是否为“单个”对象,是的话就 push 到数组中,不是的话就 merge 到数组中。
class2type, type(obj), isWindow(obj) 的实现请参考: http://xxing22657-yahoo-com-cn.iteye.com/blog/1025297
meger(a, b) 的实现请参考上一小节。
jQuery.each()
该函数提供一种遍历数组和对象的函数,支持传入一个回调函数,在遍历过程中执行。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> a = ['aaa', 'bbb']; $.each(a, function(i, value){ alert('a[' + i + '] = ' + value); // a[0] = aaa, a[1] = bbb }) b = {a: 'aaa', b: 'bbb'} $.each(b, function(name, value){ alert('b.' + name + ' = ' + value); // b.a = aaa, b.b = bbb }) </script>
简单实现如下:
$ = { each : function(obj, callback) { for (var n in obj ) { if (callback.call(obj[n], n, obj[n]) === false) { break; } } } };
同样,jQuery 的源代码对数组和对象采用了不同的遍历方式。
更正:
这里区分数组(或“类数组”)和对象还是有必要的。
$('body')方式产生的对象就是一个“类数组”,有length,有下标等,但又有其他属性(及函数),直接用 for (i in $('body'))遍历是有问题的(会遍历到我们不关心的“隐藏”属性),还是应该检查 length 属性,用 for (i = 0; i < xxx.length; ++i) 遍历。
$ = { // isFunction() { ... } each : function(obj, callback) { var length = obj.length, isObj = length === undefined || isFunction(obj); if (isObj) { for (var n in obj ) { if (callback.call( obj[n], n, obj[n]) === false ) { break; } } } else { for (var i = 0; i < length; ++i) { if (callback.call(obj[i], i, obj[i]) === false) { break; } } } } };
jQuery.grep()
用于过滤数组,第一个参数表示被过滤的数组,第二个参数表示用于判断是否被过滤的函数。
第三个参数为 true ,表示执行过滤的函数的返回值为 false 时保留;否则表示执行过滤的函数的返回值为 true 时保留。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> var arr = [ 1, 2, 3, 4 ]; ret = $.grep(arr, function(value, i){ return (value != 3 && i >= 1); }); alert(ret); // 2, 4 ret = $.grep(arr, function(value, i){ return (value != 3 && i >= 1); }, true); alert(ret); // 1, 3 </script>
简单实现如下:
$ = { grep : function(elems, callback, inv) { var ret = []; inv = !!inv; for (var i = 0; i < elems.length; ++i) { if (inv !== !!callback(elems[i], i)) { ret.push(elems[i]); } } return ret; } };
jQuery.map()
该函数提供对每个元素进行变换并产生一个新数组的功能。
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> var arr = {a: false, b: 111, c: 'ccc', d: 'ddd'}; ret = $.map(arr, function(value, i){ if (!value || i === 'd') { return undefined; } if (typeof value === 'number') { return value * 2; } else { return [value, value]; } }); alert(ret); // 222, ccc, ccc </script>
可以看出,如果返回值为 undefined 或 null 时,表示该元素不会被保留;因此,我们完全可以用 jQuery.map() 代替 jQuery.grep() 。
简单实现如下:
$ = { map: function(elems, callback) { var ret = []; for (var key in elems) { value = callback(elems[key], key); if ( value != null ) { ret.push(value); } } return ret.concat.apply([], ret); } };
对每个 key 调用 callback 并检查返回值,如果不为 undefined 或 null 则 push 到结果中。
这个函数很容易让人想起大名鼎鼎的 MapReduce,实际上,还真的有人做了 jQuery 的 MapReduce Plugin:
http://plugins.jquery.com/project/MapReduce
jQuery.proxy()
用于改变函数中 this 指向的对象。有两种使用方式
1.用第一个参数表示 this 指向的对象,第二个参数表示函数名称(必须为参数一的成员函数)
2.用第一个参数表示被改变上下文的函数,第二个参数表示 this 指向的对象
功能测试代码如下:
<script type="text/javascript" src="jquery.js"></script> <script> a = { name: 'name of a' } b = { name: 'name of b', getName: function(){ return this.name; } } alert(b.getName()); // name of b proxy = $.proxy(b.getName, a); alert(proxy()); // name of a a.onclick = b.getName; alert(a.onclick()); // name of a a.onclick = $.proxy(b, 'getName'); alert(a.onclick()); // name of b </script>
实现方式如下:
$ = { proxy: function(fn, context) { return typeof context === 'string' ? function() { return fn[context].apply(fn, arguments); } : function() { return fn.apply(context, arguments); }; } };
实际上是利用了 finction.apply 可以指定上下文环境( this 指向的对象)的特性。根据参数类型,使用不同的 function.applay 方式。
finction.apply 的第一个参数表示 this 指向的对象,第二个参数为调用函数时的参数数组。
jQuery.removeData()
移除附加在对象上的数据。由于一些浏览器不允许直接 delete DOM Elemnt 的属性,因此 jQuery 进行了对象类型和浏览器支持的检查,选择性地采用 delete 和直接置为 undefined 两种方式来移除数据。但下面的简单实现中统一采用将要移除的数据置为 undefined 的方法。
功能测试代码如下:
移除 name , value 方式附加的数据:
<script type="text/javascript" src="jquery.js"></script> <script> obj = {}; $.data(obj, 'name', 'value'); document.write("$.data(obj, 'name') = " + $.data(obj, 'name') + '<br />'); $.removeData(obj, 'name'); document.write("$.data(obj, 'name') = " + $.data(obj, 'name')); </script>
移除通过对象附加的数据:
<script type="text/javascript" src="jquery.js"></script> <script> obj = {}; $.data(obj, {name1: 'value1', name2: 'value2'}); document.write("$.data(obj, 'name1') = " + $.data(obj, 'name1') + '<br />' ); document.write("$.data(obj, 'name2') = " + $.data(obj, 'name2') + '<br /><br />'); $.removeData(obj, 'name1'); document.write("$.data(obj, 'name1') = " + $.data(obj, 'name1') + '<br />' ); document.write("$.data(obj, 'name2') = " + $.data(obj, 'name2') + '<br /><br />'); $.removeData(obj); document.write("$.data(obj, 'name1') = " + $.data(obj, 'name1') + '<br />' ); document.write("$.data(obj, 'name2') = " + $.data(obj, 'name2')); </script>
移除附加到 DOM Element 上的数据:
<div id="div_test" /> <script type="text/javascript" src="jquery.js"></script> <script> window.onload = function() { div = document.getElementById('div_test'); $.data(div, 'name', 'value'); document.write($.data(div, 'name')); document.write('<br />'); $.removeData(div, 'name'); document.write($.data(div, 'name')); } </script>
实现方式如下:
$ = function() { // implemention of globalCache, getCache(obj) ... // function data(obj, name, value) { ... } // function isEmptyObject(obj) { ... } function removeData(obj, name, value) { if (name) { getCache(obj)[name] = undefined; if (!isEmptyObject(obj)) { return; } } if (obj.nodeType) { globalCache[obj[expando]] = undefined; } else { obj[expando] = undefined; } } return { isEmptyObject : isEmptyObject, data : data, removeData : removeData } }();
globalCache, getCache(obj), data(obj, name, value) 的实现方式请参考:
http://xxing22657-yahoo-com-cn.iteye.com/blog/1042440
isEmptyObject 的实现请参考之前的小节。
jQUery.now()
返回当前时间:
function() { return (new Date()).getTime(); }
jQuery.noop()
空函数:
noop: function() {}
以上就是就是我对 jQuery Utilities 分类下的函数的实现方式的分析了。
题外话
最近一直在看 jQuery 的源代码,jQuery 源码算是一个学习JavaScript编程模式的经典范例吧。之所以决定花时间钻研JavaScript,一个重要的原因是最近 server-side JavaScript 和 一些 “JavaScript Friendly” 的 NoSQL 数据库似乎在慢慢兴起。仔细想想, JavaScript 也算是一门各大厂商都在努力改善的语言了(浏览器之争推动了 JavaScript 性能的提升),并且 JavaScript 的一些语言特性也比较有吸引力。
分析 jQuery 源码的思路,是先从 API 入手,“自底向上”地进行分析,先把基础函数了解清楚,再去看“高阶函数”的实现。具体分析某个函数(或属性)时,先测试这个函数(或属性)的功能,再想办法自己实现一遍,然后与jQuery的实现进行比较。
我简单地搜索了一下,这边分析过jQuery源代码的朋友也不少,能否分享一下经验?