全新的链式操作
这是每一个框架都遇到的问题,是使用原型扩展实现链式调用,还是把方法都绑定都一个对象中。如果使用原型扩展就意味着与其他所有走这条路的框架为敌,在这条路上有两个令人望而生畏的对手——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
浙公网安备 33010602011771号