这是每一个框架都遇到的问题,是使用原型扩展实现链式调用,还是把方法都绑定都一个对象中。如果使用原型扩展就意味着与其他所有走这条路的框架为敌,在这条路上有两个令人望而生畏的对手——Prototype与mootools。如果把方法都绑定都一个对象中(我通常称之为命名空间对象),方法调用起来就不那么优雅,即使是jQuery,也只能让实现节点的链式操作。但一个框架所能达到的高度,是由它的基础设施决定。jQuery在它所涉及的方面算是做得尽善尽美了,但有没有想到,mootools实现与此相同的功能,所需的代码少多了。这是因为jQuery就一个jQuery对象在干活,而mootools那边却都是武装到牙齿的Array,String,Number,Class,Event,Element等一大堆对象。原型扩展的好处显然易见,我们直接就可以在字面量上实现链式操作。如果是第二种,想实现链式操作,就需要在一个自定义对象进行原型扩展,但这也意呋着链式操作只能在实例的方法中进行,需要new一下。John Resigs想歪脑,搞了个无new实例化,减轻这种调用的痛苦(可能对其他语言的人来说,一大堆分开的方法调用不算什么,但在JS界,已经大规模使用链式操作,而你写JS时还是一个个方法地调用,明显是不合潮流,会“被落后”!)
// jQuery $.trim(" abc "); // Google Closure Library goog.string.trim(" abc "); // Dojo Toolkit dojo.string.trim(" abc ");
像jquery这样分层结构不明显的库,会把这些工具方法都依附到命名空间对象上,但如果库的规模很大,像Google Closure那样就不行,会很乱很乱,调用方法时内部有一个方法寻找的过程,这里会出现性能耗消。由于直接是返回没有什么扩展的原生对象,第二次调用就可能“链”不起来了。
//假设我已为jQuery添加了capitalize方法 $.capitalize($.trim(" abc "));
是不是很丑鄙呢?!但现在我想通了,我的框架现在还很弱小,绝对不能与Prototype、mootools为敌,要不就会被它们扼杀于襁褓之中。我想了好久,把原生对象的(原型)方法划分为三个层次。第一种是所有浏览器都支持的,第二种是IE6不支持,但已列入ECMA草案的,如javascript1.6的迭代器,它们不使用新的语言特征就能模拟出来的,第三种是自定义方法,话需如此,有些方法,许多主流框架都实现了的,如string的capitalize、camelize、substitute,array的unique、flatten,object的each或forEach。第一种我们不用管,第二种只是个兼容的问题,实现方法大同小异,反正效果出来是一样就行了。第三种如果也加入到原型中,很容易与其他类库造成命名冲突,因为它们有时仅仅是名字一样,要达到的目的完全是两码事。嗯,又是时候隆重推介我全新的链式操作。
我们知道,query之所以能链式调用,它的方法每次都返回拥有所有方法的对象。这种对象,我们称之为实例,因为它可以廉价地调用其原型链上的方法。我们反过来想,原型链其实也是一个个对象。我们可以独立地实现这些对象,我称之为扩展方法集合体,如stringExt、numberExt、arrayExt。剩下的是“实例”问题,“实例”能拥有所有方法,包括原生的以及自定义的。很明显,让一个对象干四种原生对象的活是不现实的,我相应地搞了四种对象。这些对象,我称之为代理对象,都是方法集体合,但这些方法与扩展方法集合体的截然不同,它们都是代理方法,里面的逻辑一模一样,不同的是函数体上附了一个方法名,如“toArray”、"camelize"啦。最开始的时候,我们把这个操作对象放进一个入口函数(chain)。这其实是一个适配器,但为了简单起见,我暂时略去这些逻辑,在里面直接调用链式函数(adjustProxy)就算。此函数会根据操作对象的类型,选择不同的代理对象,或者干脆不做,直接返回。最着就等这个代理对象的某个方法被调用了,我说过它只是代理方法,唯一不同的是方法名与所在对象。被调用时,它会先从自己身上得到方法名与从内部的this那里得到操作对象target与其类型。就算这类型其实也可以通过计算得到,但既然上次已计算过,就不谓重复而已。有了方法名,我们就判定操作对象是否天生支持此方法,没有则从相应扩展方法集合体寻找相应同名方法。然后是调用方法,把得到的结果再放进链式函数(adjustProxy)中……这样就实现链式操作了。
//by 司徒正美 http://www.cnblogs.com/rubylouvre/ 2010.10.17 var lang = {}; // get from qwap function getType(obj){ var type = typeof obj; if(type === 'object'){ if(obj===null) return 'null'; else if(obj.window==obj) return 'window'; //window else if(obj.nodeName) return (obj.nodeName+'').replace('#',''); //document/element else if(!obj.constructor) return 'unknown'; //to_s 为Object.prototype.toString else return to_s.call(obj).slice(8,-1).toLowerCase(); } return type; } function oneObject(array,val){ var result = {},value = val !== void 0 ? val :1; for(var i=0,n=array.length;i < n;i++) result[array[i]] = value; return result; } function makeProxy(type,method){ var object = lang[type+"Proxy"] = {}; method.forEach(function(name){ object[name] = function(){ var name = arguments.callee.name, obj = this.target, method = obj[name] ? obj[name] : lang[this.type+"Ext"][name]; return adjustProxy(method.apply(obj,arguments)); } object[name].name = name; }) } var adjustOne = oneObject(["string","array","number","object"]); function adjustProxy(obj){ var type = getType(obj); if(adjustOne[type]){ var proxy = lang[type+"Proxy"]; proxy.target = obj; proxy.type = type; proxy.toString = function(){ return this.target+""; } proxy.valueOf = function(){ return this.target; } return proxy; }else{ return obj } } function chain(obj){ return adjustProxy(obj) }
- lang,这是内部的私有对象,负责存放四种类型的扩展方法集合体与相应的代理对象。
- getType,辅助函数,功能同is。
- oneObject,我框架中一个重要辅助函数,目的是生成用于if分支的哈希对象。
- makeProxy,创建一个代理对象。它里面的方法都有一个name,用于反射。这些代理方法会返回另一个代理对象,并把真正的返回值附于其上。
- adjustProxy,调整代理对象。这方法会在代理方法中调用,其valueOf与toString用于打破链式操作,返回我们想要的结果。
// lang的全貌 lang = { stringProxy:{/*……*/}, stringExt:{/*……*/}, numberProxy:{/*……*/}, numberExt:{/*……*/}, arrayProxy:{/*……*/}, arrayExt:{/*……*/}, objectProxy:{/*……*/}, objectExt:{/*……*/} }
接着我们就定义这些扩展方法集合体吧,里面的函数就像定义原型函数那样就行了。
String的扩展:
//by 司徒正美 http://www.cnblogs.com/rubylouvre/ 2010.10.17 var stringExt = lang.stringExt = { //判断一个字符串是否包含另一个字符 contains: function(string, separator){ return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1; }, startsWith: function (pattern) { return this.indexOf(pattern) === 0; }, endsWith: function (pattern) { var d = this.length - pattern.length; return d >= 0 && this.lastIndexOf(pattern) === d; }, toArray:function(crash){ return !!crash ? this.split('') : this.split(/\s+/g); }, //得到字节长度 byteLen:function(){ return this.replace(/[^\x00-\xff]/g,"--").length; }, empty: function () { return this.valueOf() === ''; }, blank: function () { return /^\s*$/.test(this); }, //length,新字符串长度,truncation,新字符串的结尾的字段 //返回新字符串 truncate :function(length, truncation) { length = length || 30; truncation = truncation === void(0) ? '...' : truncation; return this.length > length ? this.slice(0, length - truncation.length) + truncation :String(this); }, camelize:function(){ return this.replace(/-([a-z])/g, function($1,$2){ return $2.toUpperCase(); }); }, capitalize: function(){ return this.replace(/\b[a-z]/g, function(s){ return s.toUpperCase(); }); }, underscore: function() { return this.replace(/([a-z0-9])([A-Z]+)/g, function(match, first, second) { return first+"_"+(second.length > 1 ? second : second.toLowerCase()); }).replace(/\-/g, '_'); }, toInt: function(radix) { return parseInt(this, radix || 10); }, toFloat: function() { return parseFloat(this); }, escapeRegExp: function(){ return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); }, //http://www.cnblogs.com/rubylouvre/archive/2010/02/09/1666165.html padLeft: function(digits, radix, filling){ var num = this.toString(radix || 10); filling = filling || "0"; while(num.length < digits){ num= filling + num; } return num; }, padRight: function(digits, radix, filling){ var num = this.toString(radix || 10); filling = filling || "0"; while(num.length < digits){ num += filling; } return num; }, // http://www.cnblogs.com/rubylouvre/archive/2009/11/08/1598383.html times :function(n){ var str = this,res = ""; while (n > 0) { if (n & 1) res += str; str += str; n >>= 1; } return res; }, //要替换的内容要用#{}包围起来 substitute : function(object, regexp){ return this.replace(regexp || (/\\?\#{([^{}]+)\}/g), function(match, name){ if (match.charAt(0) == '\\') return match.slice(1); return (object[name] != undefined) ? object[name] : ''; }); } }
Array的扩展:
var arrayExt = lang.arrayExt = { //深拷贝当前数组 clone: function(){ var i = this.length, result = []; while (i--) result[i] = cloneOf(this[i]); return result; }, //判断数组是否包含此元素 contains: function (el) { return this.indexOf(el) !== -1; }, without:function(){//去掉与传入参数相同的元素 var args = A_slice.call(arguments); return this.filter(function (el) { return args.indexOf(el) !== -1; }); }, //http://msdn.microsoft.com/zh-cn/library/bb383786.aspx //移除 Array 对象中某个元素的第一个匹配项。 remove: function (item) { var index = this.indexOf(item); if (index !== -1) return arrayExt.removeAt.call(this,index); return null; }, //移除 Array 对象中指定位置的元素。 removeAt: function (index) { return this.splice(index, 1); }, //对数组进行洗牌,但不影响原对象 // Jonas Raoni Soares Silva http://jsfromhell.com/array/shuffle [v1.0] shuffle: function () { var shuff = this.concat(), j, x, i = shuff.length; for (; i > 0; j = Math.random(i-1), x = shuff[--i], shuff[i] = shuff[j], shuff[j] = x) {}; return shuff; }, min: function() { //比Math.min.apply({}, this) 高效 return Math.min.apply(Math, this); }, max: function() { return Math.max.apply(Math, this); }, //从数组中随机抽选一个元素出来 random: function () { return arrayExt.shuffle.call(this)[0]; }, ensure: function() { //只有原数组不存在才添加它 var args = A_slice.call(arguments); args.forEach(function(el){ if (this.indexOf(el) < 0) this.push(el); },this); return this; }, //取得对象数组的每个元素的特定属性 pluck:function(name){ var result = [],prop; this.forEach(function(el){ prop = el[name]; if(prop != null) result.push(prop); }); return result; }, sortBy: function(fn, scope) { var array = this.map(function(el, index) { return { el: el, re: fn.call(scope, el, index) }; }).sort(function(left, right) { var a = left.re, b = right.re; return a < b ? -1 : a > b ? 1 : 0; }); return arrayExt.pluck.call(array,'el'); }, compact: function () {//以数组形式返回原数组中不为null与undefined的元素 return this.filter(function (el) { return el != null; }); }, unique: function () { //返回没有重复值的新数组 var result = []; for(var i=0,l=this.length; i < l; i++) { if(result.indexOf(this[i]) < 0){ result.push(this[i]); } } return result; }, flatten: function() { var result = []; this.forEach(function(value) { if (is(value,"Array")) { result = result.concat(arrayExt.flatten.call(value)); } else { result.push(value); } }); return result; }, //var a = [0,1,2,9]; //var a_ = [0,5,2]; //puts(a.diff(a_)) //--> 1,9 diff : function(array) { var result = [],l = this.length,l2 = array.length,diff = true; for(var i=0; i<l; i++) { for(var j=0; j<l2; j++) { if (this[i] === array[j]) { diff = false; break; } } diff ? result.push(this[i]) : diff = true; } return result.unique(); } };
Number的扩展:
var numberExt = lang.numberExt ={ times: function(fn, bind) { for (var i=0; i < this; i++) fn.call(bind, i); return this; }, padLeft:function(digits, radix, filling){ return stringExt.padLeft.apply(this,[digits, radix, filling]); }, padRight:function(digits, radix, filling){ return stringExt.padRight.apply(this,[digits, radix, filling]); }, upto: function(number, fn, scope) { for (var i=this+0; i <= number; i++) fn.call(scope, i); return this; }, downto: function(number, fn, scope) { for (var i=this+0; i >= number; i--) fn.call(scope, i); return this; }, round: function(base) { if (base) { base = Math.pow(10, base); return Math.round(this * base) / base; } else { return Math.round(this); } } } var mathFns = ["abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "exp", "floor", "log", "pow","sin", "sqrt", "tan"]; mathFns.forEach(function(name){ numberExt[name] = function(){ return Math[name](this); } });
Object的扩展:
function isPureObject(obj){ return !!(obj && is(obj,"Object") && obj[CTOR] === Object && obj[CTOR][PROTO].hasOwnProperty("isPrototypeOf")); } // get from mootools function cloneOf(item){ if(is(item,"Array")){ return arrayExt.clone.call(item); }else if(isPureObject(item)){ return objectExt.clone.call(item); }else{ return item; } } // get from mootools function mergeOne(source, key, value){ if(is(value,"Array")){ source[key] = arrayExt.clone.call(value); }else if(isPureObject(value)){ if(is(source,"Object")){ objectExt.merge.call(source[key], value); }else{ source[key] = objectExt.clone.call(value); } }else{ source[key] = value; } return source; }; var objectExt = lang.objectExt = { //取其子集组成一个新对象,keys为一个字符串数组 subset: function(keys){ var results = {}; for (var i = 0, l = keys.length; i < l; i++){ var k = keys[i]; results[k] = this[k]; } return results; }, forEach: function(fn,scope){ var names = Object.keys(this),n = names.length,name while(n){ name = names[--n]; fn.call(scope,this[name],name,this); } }, clone: function(){ var clone = {}; for (var key in this) clone[key] = cloneOf(this[key]); return clone; }, merge: function(k, v){ var target = this,obj,key //为目标对象添加一个键值对 if (typeOf(k) == 'string') return mergeOne(target, k, v); //合并多个对象 for (var i = 0, l = arguments.length; i < l; i++){ obj = arguments[i]; for ( key in obj) mergeOne(target, key, obj[key]); } return target; } }
最后是定义四个代理对象了。
var stringFns = [ "charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "localeCompare", "match", "quote","replace", "search", "slice", "split", "substring", "toLowerCase", "toLocaleLowerCase", "toUpperCase", "toLocaleUpperCase", "trim", "toJSON"] makeProxy("string",stringFns.concat(Object.keys(stringExt))); var arrayFns = [ "toLocaleString","concat", "join", "pop", "push", "shift", "slice", "sort", "reverse","splice", "unshift", "indexOf", "lastIndexOf", "every", "some", "forEach", "map","filter", "reduce", "reduceRight"] makeProxy("array",arrayFns.concat(Object.keys(arrayExt))); var numberFns = ["toLocaleString", "toFixed", "toExponential", "toPrecision", "toJSON"] makeProxy("number",numberFns.concat(Object.keys(numberExt))); var objectFns = [ "toLocaleString", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable" ]; makeProxy("object",objectFns.concat(Object.keys(objectExt)));
使用如下:
alert(chain("eee").capitalize().toArray(true)) //E,e,e