Front-end web developer——[to be a better man]
文章字体大小Font Size文章字体大小:12px, 14px
Jul 22

通过迭代和属性复制对DOM element进行扩展

在Javascript前端编程中,使用过Prototype.js的人,会知道,只要使用一个简单的语句:

$("msg").hide();

就可以把id为msg的元素隐藏;同样的,$("msg").show()则可以把隐藏的元素显示出来。像这种对元素的扩展,是怎样做出来的呢?

在说明这个问题之前,先问一个问题:为什么要这样做?

为什么要对DOM element进行扩展?

如果你在Javascript编辑中,不使用任何库,即祼编去实现一些说大不大说小不小应用时,相信你一定会事先$一下:

var $=function (s){
return typeof(s)=="string" ? document.getElementById(s) : s;
};

或者更简单的

function $(){return document.getElementById(s);}

这样一来可以加快你写代码的速度,二来可以为页面节省不少字节。

但如果我们还需要很多对DOM的操作,而且这些操作具有一定的复杂性重用,需要分离出来,如对属性className的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rmCls=function(el,_c){// the full name is removeClassName
    el=$(el);if(!el) return null;
    var c=el.className.split(" "),str="";
    for(var i in c){if(c[i]!=_c) str+=" "+c[i];}
    el.className=str;
    return el;
},
adCls=function(el,c){// the full name is addClassName
    el=$(el);if(!el) return null;
    this.rmCls(el,c).className+=" "+c;
    return el;
},
hasCls=function(el,_c){// the full name is hasClassName
    var el=$(el),c=(el.className || "").split(" "), l=c.length;
    while(l--){if(c[l]==_c) return true;};
    return false;
};

当然,我们也可以直接使用$(”msg”).className=”on”来操作,但这对存在多个类名的元素来说就不那么便利了。但现在也不太便利,因为我们要把元素也写到参数中:
adCls($("msg"),"on");
如果可以这样似乎会更清爽些:
$("msg").adCls("on");

单独的一行看不出什么便利,但如果这样:
// or more simple like the codes bellow
var m=$("msg"),
condition = (...m.Method1()...,.m.Method2()..,.m.Method3()..);
m[condition ? "rmCls":"adCls"](”on”);

会不会更简洁呢?

如何对DOM element进行扩展?

第一步:还是从$方法下手:
$=function(s){
var el=document.getElementById(s);
if(true){
el.rmCls=function(arg2){rmCls(el,arg2);};
el.adCls=function(arg2){adCls(el,arg2);};
el.hasCls=function(arg2){hasCls(el,arg2);};
}
return el;
}

但这种写法,明眼人就看出,是比较笨拙的写法,而且还会出现的情况就是函数递归次数过多,浏览器卡死:(

写到这里我发现,当我们企图让代码变得简单时,却进入了另一种复杂。
怎么个复杂法?请诸君继续往下看。

第二步:通过循环迭代复制对象属性

说到查看javascript的对象属性,大家不免会想到这一句:
for(var i in theObject){"key:"+i+",value:"+theObject[i];}
除了javascript内置的方法和属性,其它均可通过它来找到;相信不少人也通过他来对对象进行简单的WATCH,如查看DOM在不同浏览器之间的差异就常常用到它。我们把上面的代码用下面的代码换上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__G={'rmCls':rmCls,'adCls':adCls,'mthCls':mthCls};
$=function(s){
  var el=document.getElementById(s);
  if(el && !el._extendedByMyScriptName){
    for(var i in __G){
      var _tmpFn=function(_fn,_o){//delegate function
          return function(){
            if(typeof(_fn)=="function");
              return _fn.apply(null,
                [_o].concat(Array.prototype.slice.call(arguments)));
          }
      };
      el[i]=_tmpFn(__G[i],el);
    }
    //this statement is use to stop from too much recursion
    el._extendedByMyScriptName=true;
  }
 return el;
}

是不是很复杂?

  1. 关于__G:把相关的函数用JSON的方式组织起来,好进行属性复制;
  2. _tmpFn,实际也起到代理curry作用
  3. _extendedByMyScriptName,这个变量目的就是阻止不必要的函数递归。因为所要复杂的源方法中,可能应用到了$。

第三步:看代码累了吧?听个小故事吧,是关于这个DOM element扩展的小花絮:
funny其实这个DOM element扩展,是刚才在重写一个JS小特效的过程中想到的。这个特效还没完成,但我去跳到这里想另一件事情了。没办法,这是过份执着完美与喜欢钻牛角尖的人的通病吧。
关于_extendedByMyScriptName,我开始想了很久,并翻看了Prototype.js1.6的代码多次也不知道。当时我就想:没理由的,这个包中的此类函数为什么不会递归,是不是有什么高深的算法?于是继续查看,但由于这个包中的代码通常是上下四处应用,常得在上下文跳来跳去,头晕眼花,真是人肉STEP UP了。幸好在正要晕倒前让我看到“element._extendedByPrototype”,终于明白,原来复杂的事情可以通过简单的手段去解决!反之,某些看起来简单的事情却是实现起来可能是相当复杂的。在这种幻真幻假的世界里,我们只有不断的开放视野地去学习才能更有效地辨别真假。Isn’t It ?

Prototype.js中DOM element扩展的实现方式:

Prototype.js包中有一全局对象Element,即window.Element,其中包含了很多像上面描述的方法,你可以用两种很常见的方式来为元素添加类:
element=$(element);
Element.addClassName(element,classname);
// and the easier way
element.addClassName(classname);

由于Prototype.js是一个公共库,为了更好地提高可重用性,所以它的开发者们使用了高度的面向对象方式来编写代码,使得除了基础支持部分(如$和$A),其它各功能模块之间的耦合度十分低,如上面刚说到的代码就是一个例子:
//methods list line 1573
Element={
method1:function(arg1,arg2){...},
method2:function(arg1,arg2){...},
...
}
$=function(s){
var el=document.getElementById(s);
//a big great codes here...
...
return el;
}

但降低耦合度是需要付出代价的,我们可能需要更多的代码和可能会降低程序的运行效率,如Prototype.js的extend方法,它就是一个通过迭代方式来实现对象属性复制的方法:
(为了说明问题——代码行数和运行效率,我把从Prototype.js1.6中摘出的代码简化,去掉不必要的干扰,同时旁边注上其所在的行号。)
(To make thins simple,I strip the codes from Prototype.js1.6, you can view the resource in Prototype.js1.6 throw the line number.)
function (destination, source) {//line 104
for (var property in source)
destination[property] = source[property];
return destination;
};

但是,正如上文的例子那样,对DOM element的扩展并不是一个简单的属性复制就可以的,因为原来的Element下的方法是有两参数的,因此我们还须进行curry和合理利用闭包的相关特性:
methodize=function(fn,element) {//line 243
  return function() {
  return fn.apply(null, [element].concat($A(arguments)));
 };
}
//line 2546
if (!element || element._extendedByPrototype ||
 element.nodeType != 1 || element == window) return element;
for (property in methods) {//line 2555
 value = methods[property];
 if (Object.isFunction(value) && !(property in element))
 element[property] = methodize(value,element);
}

虽然Prototype.js中的代码可读性高了很多,但想要对DOM element进行扩展,仍然得花一翻功夫啊;为了变得简单却进入了另一种复杂,这又说明了Javascript编程中除了编程思想外,另一个值得考虑的问题就是代码行数。

我想,用最简洁的代码实现更高效的应用才是Javascript编辑的王道吧。这时,我们可以知道为什么jQuery会那么流行了。

后记:

本来想通过这个话题讨论javascript代码行数与效率的问题,但写着写着就迷失了方向,而且很想把面写得广一些,但越想写得多,就越混乱,结果,就变成这个样子了。

Popularity: 64% [?]

 tags Tags: ,

respondLeave a Reply

:mrgreen: :| :twisted: :arrow: 8O :) :? 8) :evil: :D :idea: :oops: :P :roll: ;) :cry: :o :lol: :x :( :!: :?:

&| &