Ruby's Louvre

每天学习一点点算法

导航

全新的链式操作

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

posted on 2010-10-17 23:30  司徒正美  阅读(3329)  评论(4编辑  收藏  举报