Function Template和css utility function
原理
继承让对象具有另一个对象的api,自动具有某些方法的行为。那么能否让一函数具有另一个函数的部分行为,并且让它以另一个函数的行为为模板而扩展?
其实在java,C++中通过拆分组合多个方法也可以实现这一目的,但js的'元类继承' (我不知道是谁提出的概念,我是从winter的博客上最初看到的)的概念使之实现上面的目的更为灵活。例如以下这个小例子:
var child = function (ruleTableFunc){ return this.parent = new Function('if (' + ruleTableFunc + '()) alert(1);'); } var a = new child(function(){ alert(2); return true}); a();
这样对逻辑的封装更为紧凑一些。
之所以想到这个话题,是因为目前读了2组库的代码设置/获取css属性, 设置/读取html element的attribute的函数,发现有关判断浏览器兼容性的代码,都仅仅是一些规则,而基本的行为都比较相似而且很简单,我希望能进一步拆分规则作为静态的结构,放入一个rule table,如果程序执行时刻,用户查询或设置的规则行为是在这些rule tabl中匹配的内容,则执行有关这个属性的特定规则,如果属于一般属性,则去执行默认的行为;另外,随着浏览器的发展,我们仅仅补充,修改,添加(可能写的时候并不能涵盖全部的特殊规则)规则表中的特殊规则,由于,规则单独放置,这样静态规则的行为和一致性的行为区分开,利于js框架的不断扩充;
所以写了一个exceptionalRuleFunctionTemplate的基类,基于此基类可以扩展类似CSS, attrs这类方法;
尽管基于元类继承可以实现这种模式,但如果逻辑比较复杂,导致过于复杂的字符串组织也是比较烦人的事情,于是写了第二个类似经典OO方法中的实现;
function child(){ //this is where we put the static rule of matching; this.rule = function(){ alert(1); return true; } //this is where we put the implementation of default action; this.action = function(){ alert(2); } var self = this; return function (){ console.log('we might add some additional behaviour here!'); self.superclass.apply(self, arguments); } } function parent(){ //in order not to override the method definition when inovked from //child, we only have to judge the dynamic chain of invocation; if (arguments.callee.caller == null){ this.rule = function(){}; this.action = function(){}; } //this is where we put the whole action logic of how to use rule/action ; var result; if (!(result = this.rule(arguments))) return result = this.action(arguments); return result; } child.prototype = new parent(); child.prototype.constructor = child; child.prototype.superclass = parent; //new a child , we get a function of var a = new child(); a();
注意在调用父类的行为时,需要判断动态链才能避免子类的函数定义被覆盖;
实践
基于前端现有库的底层模块机制,我实现了一版设置读取css属性的模板函数ExeptionalRuleFunctionTemplate和CSSTemplate, 用它实现设置和读取css属性的功能;
//exceptional rule function template;
lib.Class('ExeptionalRuleFunctionTemplate',{
ns: lib.lang,
extend: lib.Object,
construct: function(){
var arg = arguments;
//notice we need to judge the dynamic chain of function in order not to
//make the methods definition in subclass overriden by parent's ;
if (!arg.callee.caller || arg.callee.caller == lib.Class ) {
this.exceptionalRule = lib.emptyMethod;
this.execDefaultAction = lib.emptyMethod;
}
//this is where we put action in common;
this.execRule = function(){
var _result;
switch (_result = this.exceptionalRule.apply(this, arg)){
//===
case undefined: //undefined value meaning that we get a unmatch
return undefined; //goto defaultAction part;
default:
return _result; //in all other case we just dont wanna go to defualtAction part
//even when result is false, null , "", 0, etc.
}
}
var result;
try {
if (undefined ===(result = this.execRule()))
return result = this.execDefaultAction.apply(this,arg);
return result;
}
catch(e){
//todo lib.Error;
}
}
});
lib.reg("lib.DOM", function(){
lib.DOM.CSSGetRuleTbl = {
//the special opacity rule
"opacity": function(elem){
if (( document.defaultView && document.defaultView.getComputedStyle) || window.getComputedStyle)
//in this case we wanna execution goes to defaultAction part
//so we merely return false bypassing the execution;
return undefined;
if (elem.currentStyle){
//#dbg
//if (!elem.currentStyle.hasLayout)
// this is where a dbg flag could lead to problem;
if (!elem.currentStyle.hasLayout)
console.log("filter setting would not take effect on an element without its layout triggered");
//#end
//under this branch, whatever result, we dont wanna go to defaultAction part.
var val = new String(elem.style.filter), result;
try {
val.replace(/\s*alpha\(\s*opacity\s*=(\d+)\s*\)\s*/i, function(){
return result = arguments[1];
});
result || val.replace(/\s*progid:.+\.alpha\s*\(opacity\s*=\s*(\d+)\)/i, function(){
return result = arguments[1];
})
result || function(){
try{
result = elem.filters.item("DXImageTransform.Microsoft.Alpha").opacity;
}
catch(e){
try{
result = elem.filters.item("alpha").opacity;
}
catch(g){
result = null;
}
}
return result;
}()
|| ( result = null);
}
catch(z){
result = null; // in this case, although we dont fetch a valid value from this rule
// we shouldnt let execution goes to the action part
}
return result;
}
return undefined;
}
};
lib.DOM.CSSSetRuleTbl = {
"float":function(elem, value){
var prop = lib.IE && "styleFloat" || "cssFloat";
elem.style[prop] = value;
},
"opacity" : function(elem, value){
//todo:
//elem.style.filter = 'alpha(opacity = 0.3 * 100)'
//elem.style.filter = "DXImageTransform.Microsoft.Alpha(opacity = 0.3 * 100)";
}
};
});
lib.reg("lib.DOM", function(){
//usage sample:
//get:
//lib.$('div').css("width"); //get the width of els[0] in lib.DOM.Element;
//lib.DOM.css(lib.$('div').get(5), 'width'); //return width of 5th elem is element wrapper
//set:
//lib.$('div').css({'width': '200px', 'display': 'none'}); //set els[0] those css attrs;
//lib.$('div').css("width", '200px'); //set the els[0] 's width to 200px;
//lib.DOM.css(lib.$('div').get(2), {'width': '200px', 'display': 'none'}); //set a elem those css attrs
//lib.DOM.css(elem,'width', '500px');
Object.extend(lib.DOM, {
//lib.DOM.CSSTemplate function
css: new (lib.Class("CSSTemplate", {
ns: lib.DOM,
extend: lib.lang.ExeptionalRuleFunctionTemplate,
construct: function(){
var self = this,
assert = lib.lang.assert,
elWrp = lib.DOM.Element,
getCSSRule = function(elem, name){
arguments.length == 1 && (
name = elem,
elem = this.get(0)
);
//CSS exceptional table:
var tbl = lib.DOM.CSSGetRuleTbl;
return typeof tbl[name] == "function" ? tbl[name](elem) : undefined;
},
setCSSRule = function (elem,name,value){
var tbl = lib.DOM.CSSSetRuleTbl;
return typeof tbl[name] == "function" ? (tbl[name](elem,value), 1) : undefined;
},
getCSSAction = function(){
var foo = new Function("s", "return function(elem, name) { \
if (arguments.length == 1 && assert(this instanceof elWrp, 'this value should be a Element wrapper object!') && assert(typeof elem == 'string', 'TypeError')) { \
name = elem; elem= this.get(0); \
}\
return eval(s); \
}");
//http://jsbin.com/obehiy , a case in FF 3.6 where if defaultView were not to used , then property can't be read from a iframe;
if (document.defaultView && document.defaultView.getComputedStyle)
return foo("document.defaultView.getComputedStyle(elem,null)[name]");
if (window.getComputedStyle)
return foo("window.getComputedStyle(elem,null)[name]");
if (document.body.currentStyle)
return foo("elem.currentStyle[name]");
return function(){
throw "getCSS(): there is no matched api for getting computed style in current browser!";
}
}(),
setCSSAction = function (elem, name, value){
elem.style[name] = value;
};
return function(elem, name, value){
var arg = arguments;
if (arg.length == 1){
assert(this instanceof lib.DOM.Element, "this value must be a Element wrapper object!");
if (typeof elem == "string") {
//lib.$('div').css("width");
self.exceptionalRule = getCSSRule;
self.execDefaultAction = getCSSAction;
return self.superclass(elem);
}
else if (assert(elem.constructor == Object, "a CSS attr json formatted object should be provided!")){
//lib.$('div').css({'width': '200px', 'display': 'none'});
lib.lang.each(elem, function(value, props){
arg.callee.call(this, this.get(0),props, value);
});
return;
}
}
if (arg.length == 2){
//lib.$('div').css("width", '200px');
if (typeof elem == 'string' && assert(this instanceof lib.DOM.Element, "this value must be a Element wrapper object!")) {
arg.callee.call(this,this.get(0), elem, name);
return;
}
if (assert(lib.lang.isDOM(elem), "elem should be a DOM element!")){
if (typeof name == "string") {
//lib.DOM.css(lib.$('div').get(5), 'width') //get
self.exceptionalRule = getCSSRule;
self.execDefaultAction = getCSSAction;
return self.superclass(elem,name);
}
else if (assert(name.constructor == Object, "a CSS attr json formatted object should be provided!")){
//lib.DOM.css(lib.$('div').get(2), {'width': '200px', 'display': 'none'}) //set
lib.lang.each(name, function(value, props){
arg.callee(elem, props, value);
});
return;
}
}
}
self.exceptionalRule = setCSSRule;
self.execDefaultAction = setCSSAction;
self.superclass(elem, name, value)
return;
}
}
})),
attrs: function(){
}
}) //end of Object.extend;
})
针对这组实现有以下说明:
- new lib.DOM.CSSTempate的返回的函数,相当于child,为了支持多种调用形式,我增加对其父类函数调用之前的一些参数调整以及针对具体情况设定rule和action;目的是支持从包装集调用以及传入dom元素调用;这样在包装集的实现中只需要把函数引用赋值过去即可.并且调用的方式也可以支持多种调用;例如lib.DOM.css(lib.$("#id")[0], "width", "500px")和lib.DOM.css(lib.$("#id")[0], {"width": "500px","height": "1000px"}),都可以使用;
- 函数模板中增加了execRule方法,是为了处理在获取css属性值时,适合特殊规则匹配的值,但未能获取有效的值(例如fetch 某个元素的opacity,但该元素没有设置该项),在这种情况下不应去继续匹配action阶段,而未能匹配到特殊的规则,执行应当去action部分时,匹配特殊规则的部分返回应是undefined,因此在这种情况下,以上实现是这样区分:
- 规则部分返回undefined,是query的属性不适用于特殊的规则,应当去action部分匹配;
- 规则部分返回null,针对该属性,应当在特殊规则中匹配,但未能取到有效的值的情况;那么执行应从rule部分退出而不继续匹配action部分;对于这一点我在获取opacity的rule的实现中,若最终未能匹配,则设定为null,但这并不是一个硬性要求,如果rule中匹配到undefined(该属性没有被设定),那么执行会去action部分,当然,原本应属于特定规则的,在action部分也应该获得一个undefined;
- 对于最终的结果,undefined和null都应归属于匹配失败的情况,如果需要判断取值的结果,最好不简单的在boolean context中用隐式转换判断调用css函数是否获得有效的值;
- 最大的好处在于,把规则部分和行为部分拆开,分离静态的结构与动态的行为,对于浏览器兼容性方面的判断,其实是一个琐碎的东西,也没什么技术含量,随着浏览器的演变,这部分最易引起变化,另外,在一个框架撰写的最初,很可能作者并未知道全部的需要特殊处理的属性(例如设置css的float的cssFloat/styleFloat这些需特殊处理的情况),分离的rule这部分可能是最需要补充添加的内容,但利用lib.reg及其回调函数把它单独放置,以待后续可能的更改和扩充;