整体结构
Query整个库文件,是一个大的匿名函数(function(window,undefined){...})(window);
把window作为参数,立即执行.
匿名函数好处是能够产生一个私有空间,函数内所有变量/函数在该私有作用域内,避免和外面作用域冲突.
1传入window作为参数有两个好处:
解释执行jQuery库时,在jQuery函数作用域内就找到了window(实参在函数作用域中),不需要到顶层作用域找到window对象
能够压缩代码(jquery-XXX.min.js版本):(function(a,b){})(window);// a就是window了,少用7个字母
2 增加undefined参数的作用:避免undefined被重写:
假如在jQuery匿名函数之前有这样的代码: undefined ="now it's defined"; alert(undefined );
那么jQuery函数执行的时候,undefined就不是未定义的了!
而(function(window,undefined){...})(window); 只传递了一个参数,所以undefined就是真的未定义
3从代码看出来, jQuery是一个(构造)函数:function(selector,context){...}
返回的是一个new出来的对象.因此我们构造jQuery对象,不需要 new $(...),
因为返回值就是 new 出来的jQuery对象.这体现了jQuery的设计思想:write less, domore!
4 从最后一行代码 window.jQuery = window.$ = jQuery;
看出,把$和jQuery作为属性值赋给了window全局对象.因此外部程序可以访问到 $ 和 jQuery
5 从代码 jQuery.fn = jQuery.prototype = ...
看出, jQuery.fn 就是 jQuery对象的原型. 因此原型上有init等各个属性和方法.
而jQuery.fn.init.prototype = jQuery.fn, 所以调用 newjQuery.fn.init(selector, context)
创造出来的对象,有 jQuery.prototype上所有的属性和方法.
(function(window, undefined){
var jQuery = function(selector, context) {
returnnew jQuery.fn.init(selector, context);
};
//jQuery.fn 就是jQuery.prototype,因此原型上有init等各个属性和方法
jQuery.fn =jQuery.prototype = {
init :function(selector, context) {
//为this初始化各种属性
//this指向的是新创建的对象
return this;
}
// 大量jQuery对象属性,这里省略
};
jQuery.fn.init.prototype= jQuery.fn;
//$变成window全局的方法了,这样所有代码都能使用$
window.jQuery= window.$ = jQuery;
})(window);
extend和each:
jQuery内部一开始定义了很多工具方法,为jQuery后面部分实现所利用.
这里重点分析 extend和each方法.
extend
extend 方法是用于扩展一个对象. 这是一个很重要的方法,它既用于扩展jQuery本身,
也用于扩展jQuery对象(原型),还能用于扩展其他对象.而这是通过用户传入参数的不同而做区分的.
完整的extend方法是这样的:
extend([p_deep], p_target, p_object1, [p_objectN])
1 p_deep指定是否深度复制待复制对象
2 p_target是要被扩展的对象, p_objecti 是要被复制的对象
3 当只有一个对象传入时,是扩展jQuery或jQuery对象原型,
而有多个对象传入时,第一个是被扩展对象,其他是被复制对象.
jQuery中,这种一个函数,多次复用的的情况很多. 这也许就是jQuery能够那么小的原因吧!
extend方法的详细代码分析,请看2.js中的注释. 这里仅仅强调几点:
1 在复制属性时,jQuery做了很多条件判断,特别是当复制的对象和扩展对象是同一对象时, 要避免递归复制
2 extend方法中,调用了jQuery的 isPlainObject/isArray方法.
但是这些方法是在后面才用extend方法添加到jQuery上的.
这就有了鸡和鸡蛋的先后问题了.而原因是后面jQuery.extend执行的时候,并没有指定深度复制.因此条件
if ( deep && copy&& ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) )
在deep打住,也就没调用 jQuery还没有的isPlainObject/isArray方法.
因此我们再次认识到,javascript是解释执行的.只要在真正执行那个方法的时候,那个方法存在了就没问题.这种动态的特性,需要我们适应.
3 extend方法会覆盖源对象的同名属性
在定义了extend方法之后, 马上就使用了jQuery.extend方法,为jQuery添加了不少属性和方法.其中有我们常用的each方法.
each
each方法会在对象的每个成员上调用回调函数.
如果对象没有length属性或者是函数的的,用 foreach方式遍历
如果有length,用下标方式遍历.
这里拿出下标遍历的代码:
for ( var value = object[0];
i < length&& callback.call( value, i, value ) !== false; value = object[++i] ) {}
可以看出,它是遍历object下标0到length-1的成员, 把下标和成员作为参数调用回调函数callback,直到回调返回false为之. 而我们知道,jQuery对象内包含了多个dom元素.
因此我们写回调函数时
function(index, domEle){/*this代表dome元素,domEle===this*/}
而当回调函数返回false时,条件不满足,结束循环.
(function(window, undefined) {
var jQuery =function(selector, context) {
return newjQuery.fn.init(selector, context);
};
jQuery.fn =jQuery.prototype = {
init :function(selector, context) {
return this;
}
};
jQuery.fn.init.prototype =jQuery.fn;
window.jQuery = window.$ =jQuery;
/*****************************part2begin****************************/
jQuery.extend = jQuery.fn.extend= function() {
var target = arguments[0]|| {},
i = 1, length = arguments.length,deep = false, options, name, src, copy;
if( typeof target === "boolean" ) {
deep = target;
target =arguments[1] || {};
i = 2;
}
if ( typeof target !=="object" && !jQuery.isFunction(target) ) {
target = {};
}
if( length === i ) {
target= this;
--i;
}
for ( ; i < length; i++) {
if ( (options =arguments[ i ]) != null ) {
for ( name inoptions ) {
src =target[ name ];
copy =options[ name ];
if( target === copy ) {
continue;
}
if ( deep &© && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) {
var clone = src&& ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src
:jQuery.isArray(copy) ? [] : {};
target[name ] = jQuery.extend( deep, clone, copy );
} elseif ( copy !== undefined ) {
target[name ] = copy;
}
}
}
}
return target;
};
jQuery.extend({
noConflict:function( deep ) {
window.$= _$;
if( deep ) {
window.jQuery= _jQuery;
}
returnjQuery;
},
isReady: false,
ready: function(){/*.............*/},
bindReady: function(){/*.............*/},
isFunction: function( obj ){ return toString.call(obj) ==="[object Function]"; },
isArray: function( obj ) { return toString.call(obj) ==="[object Array]"; },
isPlainObject: function(obj ) {},
isEmptyObject: function(obj ) {},
each:function( object, callback, args ) {
var name, i = 0,
length =object.length,
isObj =length === undefined || jQuery.isFunction(object);
if ( args ) {
if ( isObj ){
for (name in object ) {
if( callback.apply( object[ name ], args ) === false ) {
break;
}
}
} else {
for( ; i < length; ) {
if( callback.apply( object[ i++ ], args ) === false ) {
break;
}
}
}
} else {
if ( isObj ){
for (name in object ) {
if( callback.call( object[ name ], name, object[ name ] ) === false ) {
break;
}
}
} else {
for( var value = object[0];
i< length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
}
}
return object;
}
});
/**********************part2end*************************/
})(window);
命名冲突
由于$是不少js库使用的名称,因此为了避免冲突,jQuery提供了一个noConflict,返回的是jQuery本身.
这样用户能够执行决定jQuery使用什么样的名称.
一开始,先保存原有的window.jQuery和window.$属性.
_jQuery = window.jQuery,
_$ = window.$;
在后面提供一个方法:
noConflict: function( deep) {
/**
* 把原来的$属性恢复了
*/
window.$ = _$;
if ( deep ) {
window.jQuery= _jQuery;
}
/**
*返回jQuery,使用什么名称,用户决定吧
*/
return jQuery;
}
因此如果用户调用jQuery.noConfict(),那么$就不再代表jQuery了.
可以 var XXX = jQuery.noConfict(). XXX就是jQuery的名称.
jQuery插件机制
jQuery提供两种插件机制:
1 jQuery.fn.extend(object)
2 jQuery.extend(object)
正如内核分析一开始介绍的,jQuery.fn 就等同于 jQuery.prototype, 也等同于jQuery.fn.init.prototype.
因此第一种方式能为jQuery原型添加新属性/方法. 于是 newjQuery.fn.init(..)得到的jQuery对象也就有了这些新属性/方法.
而第二种方式是扩展jQuery本身.因此扩展出来的方法/属性,可以在全局使用 $.newFunc(...);
Js代码架构问题
通常JS代码分为三层:
1、 底层用最核心的代码——公共(工具)类库
2、 常用的业务逻辑的类库——即公共的业务逻辑
3、 针对页面具体JS的代码,解决具体的问题
比如jQuery库,主要是第一第二层的代码.jQuery一开始就定义了extend,each,pushStack等工具函数. 这些函数在后面的属性操作,事件处理,DOM操作,CSS操作等代码中被经常的使用.对于我们而言,除非真的要写js框架,否则我们主要是写第三层的页面相关的js代码. 这些代码往往变动较大,并且和业务关联很大.因此在这一层,首先应该强调理解好需求以及业务.
对于大量的js,我们应该考虑下面两个问题:
返回结果形式
js代码提供的方法,应该如何被外部访问到?又该如何对外隐藏内部实现?
代码组织
如何组织代码,才能有良好的可维护性和可读性?
第一个问题,其实主要解决api友好以及命名空间的问题.
常用的方式是通过匿名函数,并且传入一个window或$(jQuery)作为参数:
(fuction($){
//为$或$.fn添加属性/方法
})($);
在匿名函数内部把定义的方法作为属性添加到window或$上.
这样在后面就可以这样访问匿名函数内定义的方法:
window.myFunc(...) $.myFunc(...) myFunc(...)
jQuery就是把window作为匿名函数参数的执行的.最后暴露给我们的就是 $.
匿名函数一大好处就是能创建一个私有空间,即作用域链,避免和外界命名冲突.
如果采用这种方式,需要注意一点的是,避免在window或 $上添加大量方法/属性.
最好和jQuery那样,仅仅把$作为属性添加到window上,而extend/ajax等其他方法,都是
$的方法,而不直接是window的方法. 这主要是避免同名方法冲突.
向zTree的3.5版本中,也就仅仅向$添加了一个属性: $.fn.zTree
而其他方法都是在zTree下的.
第二种方式是调用一个函数,返回一个json格式.
function createMathUtil(){
return {
sortArray:function(){...},
standardDeviation:function(){...}
......
}
}
这种方式较少使用,不做介绍.
第二个问题:代码组织
当js代码超过200行的时候,如果不对js代码做好组织,会难于维护.
因此需要组织好代码.在组织代码时,主要是考虑如何划分模块.下面给出一些
常用模块:
常量/设置,数据,初始化,事件,用户操作/页面显示,回调函数,工具......
模块划分应该结合业务需求,并且应该尽量减少模块耦合性,提高模块内聚性.
每个模块通常可以采用json的格式封装起来.比如下面就是zTree3.5一开始的代码:
(function($){
var settings = {}, roots ={}, caches = {},
//default consts of core
_consts= {
className: {......},
event: {},
id: {},
line: {},
folder: {},
node: {}
},
//default setting of core
_setting= {
treeId:"",
treeObj: null,
view: {......},
data: {
key: { },
simpleData:{......},
keep: {}
},
async: {},
callback: {}
},
......
});
一开始的_const和_setting就是常量和设置.
模块划分有一点要注意的是,子模块的深度不能太大,否则访问一个属性时很不方便,像下面那样:
kynamic.kyanmicTreeOption.pNode.kid
一共四层!
我们再看看老师的权限树代码整体解构:
var privilege = {
data:{ },
init:{
initEvent:function(){
},
initData:function(){
}
},
operation:{
divOption:{
},
userOption:{
},
privilegeTreeOption:{
}
}
};
我认为这样的格式有两个很明显的缺点:
1 强制的把所有js代码放到一个json中了.我很怀疑其必要性.
2 把整个json赋给了 var privilege,结果在json里面访问其他属性,总是要以 privilege. 开始, 因此很容易导致访问一个属性要达到4层! 相当不爽!
其实模仿 jQuery/zTree那样使用匿名函数就很好,像下面这样:
(function(window) {
var _data = { },
_init = {
initEvent:function(){
},
initData:function(){
// 访问data内的值,只需要 data.XXX
// 不需要 kynamic.data.XXX
}
},
_operation = {
divOption:{
},
userOption:{
},
privilegeTreeOption:{
}
}
// 最后把我们要暴露给外部的方法赋给 window就可以了
window.kynamic ={init:_init};
})(window);
采用匿名函数,在匿名函数内部,每个模块是平级的,并且访问模块内的属性时减少一层kynamic.
并且在匿名函数内,编码自由度较 json格式内部编码更高.
Post请求重用
createTree封装生成树的方法
我一直很不明白,为什么老师还封装Post和zTree方法.
因为在我看来,$.post和zTree函数已经是很简单的了,没必要对它们再做封装.
封装肯定要有好处才去做的.常用好处是:
1 提高代码可读性,内聚性.比如减少程序员需要记忆的参数.
2 提高代码可重用性. 由于我们主要是写页面相关的js代码,因此主要是封装公共的业务逻辑的js代码.
而老师createTree的封装是这样的:
(function($){
var treePlugin = '';
var setting = {
isSimpleData: true,
treeNodeKey:"mid",
treeNodeParentKey:"pid",
showLine: true,
root: {
isRoot: true,
nodes: []
}
};
$.fn.createTree =function(treeJSON){
//在调用的时候,把程序员写的setting覆盖掉原来的setting
var treeObj =$(this);
var treePlugin = '';
$.tree =function(){};
$.extend(setting,treeJSON.setting);
//createTree方法是由树的容器调用
$.post(treeJSON.url,treeJSON.parameter,function(data){
//treeObj.zTree(setting,data.menuitemList);
treeJSON.callback(treeObj,data,setting);
});
//setInterval("blackF(treePlugin)",50);
}
})($);
// 使用createTree
$("#kynamicTree").createTree({
setting:{
treeNodeKey:'kid',
callback:{
"click":function(event,treeId, treeNode){
$.tree.addNodes(treeNode,{
kid:100,
name:'aa',
isParent:true,
pid:1
},false);
}
}
},
url:'kynamicAction_showKynamics.action',
callback:function(treeObj,data,setting){
$.tree =treeObj.zTree(setting,data.kynamicList);
}
});
结果你发现,createTree并没有简化多少. 其实原因很简单,因为这样封装,最多也就
省略setting 的几个参数,使用默认值而已. 事实上,这样封装,使得程序员还得把所有
内容封装为一个json格式,这就要程序员看createTree指定的 key值了.
更恶心的是,程序员要么记住所有默认参数,要么在setting中写所有设置.
事实上,默认参数的使用都应该有意义,程序员才能记住.像下面那样的默认参数,
也就仅仅限于访问menuitem数据库有用而已,如果使用其他的结点id,就没用了.
treeNodeKey:"mid",
treeNodeParentKey:"pid",
总之,我很不赞同封装zTree,以及$.post. jQuery和zTree已经封装好了,我们没必要再封装,
除非我们要封装的是公共业务逻辑.