重構javascript

  最近看了兩本書《精通javascript》(csdn的編輯器出問題了版面只能這樣了大家見諒,url:http://www.verycd.com/topics/2753377/)和《javascript語言精粹》(url:http://www.verycd.com/topics/2762001/),讓我對javascript這個再熟悉不過的腳本語言有了新的認識。javascript使用簡單形式多變這不禁讓開發人員放鬆對碼的控制,特別是現在javscript的有很多方便開發庫比如jQuery。但是在輕鬆愜意之時我們的代碼就變得換亂不堪、難於維護。大家可以現在打開你最近寫的網頁代碼,看看裏面的javascript是不是充斥了$('#mainContain").height()或者getGridSelectIds()這樣的代碼。反正我的項目裏是這樣的,現在我終於下定決心要改變這種局面了。我不想因爲站點主體div的名稱發生了改變而使某一個頁面中的佈局變得錯亂,也不想在一個頁面中直接調用另一個嵌入頁面的某個方法。並且這個嵌入頁面的方法還會去獲取外部頁面上某些元素的一些屬性,而且都是根據id來獲取來的。這實在令人很不爽,雖然開發容易了但是維護代價增加了。我採取的解決之道就是讓javascript面向對象化。在javascript中實現面向對象這樣的文章已經無數了我也沒必要在重複寫一篇。我只想強調一下oo的思想是很重要的武器,所以在這裏只說說如何重構我項目中的代碼的。也許這些問題你也遇到過,希望對你也有一點點啓發。
  第一點:使用匿名函數寫法封閉每一個頁面中的javascript代碼,這樣在頁面相互嵌入的情況下你不能直接使用其他頁面中定義的方法或變量。例如:
<script>
//頁面A
(function() {
 var name = 'fh';
 var sayHello = function() {
  return name;
 };
})();
//頁面B
(function() {
 var name = 'lucifer';
 alert(sayHello());   //sayHello is not defined
})();
</script>
  第二點:使用命名空間約束代碼的訪問。命名空間我們在c#或者java裏已經使用了很長時間了這東西能夠很好的防止重名和使代碼模塊化。下面給出的是我自己寫的一個命名空間實現
(function() {
 String.prototype.s_trim = function() {
        return this.replace(/^/s|/s+$/, '');
    }
 
 var namespaces = {};
    namespaces.length = 0;
   
    //相同命名空間不重複生成
    var _existNamespace = function(nArr) {
        var tmp = namespaces;
        for(var i=0; i<nArr.length; ++i) {
            var n = nArr[i];
            if(!tmp[n]) {
                return {flag: false};
            }
            else {
                tmp = tmp[n];
            }
        }
        return {flag: true, ns: tmp};
    };
   
    //命名空間
    this.s_namespace = function(ns) {
        var arr = ns.split('.');
        var exist = _existNamespace(arr);
        if(exist.flag) {
            return exist.ns;
        }
        else {       
            for(var i=0; i<arr.length; ++i) {
                var n = arr[i];
                if(!namespaces[n]) {
                    if(namespaces.length && (i - 1) >= 0) {
                        namespaces[arr[i-1]][n] = {};
                    }
                    else {
                        namespaces[n] = {};                   
                    }
                    namespaces.length += 1;
                }
            }
            return _existNamespace(arr).ns;
        }
    };
   
    //導出命名空間
    this.s_import = function(ns) {
        var exist = _existNamespace(ns.split('.'));
        if(exist.flag) {
            return exist.ns;
        }
        else {
            alert('找不到命名空間: ' + ns);
        }
    };       
    this.s_from = function(ns) {
        var that = {};
        var ns = s_import(ns);
       
        that.s_import = function(expr) {
            var names = [];
            if(expr === '*') {
                for(var k in ns) {
                    names.push(k);
                }
            }
            else {
                names = expr.split(',');
            }
            for(var i=0; i<names.length; ++i) {
                var key = names[i].s_trim();
                window[key] = ns[key];
            }
        };
        return that;
    };
})();
我這裏有個命名約定:全局方法以s_開頭,下面看看命名空間的使用:
//頁面A
(function() {
 var ns_a = s_namespace('A');
 var name = 'fh';
 ns_a.sayHello = function() {
  return name;
 };
 ns_a.test = function() {
  alert('test');
 };
})();
//頁面B
(function() {
 var ns_a = s_import('A');
 var name = 'lucifer';
 alert(ns_a.sayHello());   //sayHello is not defined
})();
//頁面C
(function() {
 s_from('A').s_import('*');
 alert(sayHello());
})();
//頁面D
(function() {
 s_from('A').s_import('sayHello, test');
 test();
})();
這裏展現了命名空間的創建s_namespace('A')和兩種到處方式s_import與s_from().s_import.前一種導出方式只能到處一個具體的命名空間然後你使用這個命名空間的對象來訪問其中的成員。而第二種方式是指定導出該命名空間下的某些成員(*是全部,或者sayHello, test)到當前作用域內,所以你可以直接使用sayHello或test。
  第三點:使用繼承體現來使javascript代碼更加模塊話。可能是受到上面提到的兩本書的影響我並沒有使用prototype方式來實現繼承,而是使用了函數化方式來實現的。我覺得這種方式看起來更容易理解也很好使用,比如實現私有變量、函數,調用父類構造函數和在覆寫父類函數的時候調用該父類方法。好了讓我們看看實現的代碼:
(function() {
 //全站點命名空間
 var ns_site = s_namespace('site');
 //實現繼承
 var extend = Function.prototype.extend = function() {
  //例如Base.extend(),這時that爲Base
        var that = this;
  //extend.caller就是子類型
        that.subClass = extend.caller;
  //構造父類的實例對象
        var instance = that.apply(that, arguments);
        //父類中的方法元信息
  var methods = {};
  for(var k in instance) {
   var v = instance[k];
   if(v.constructor === Function) {
     methods[k] = v;
   }
  }
  //爲了實現類似於base.BaseMethod這種oo常見需求
  instance.superMethod = function(name) {
   var args = [];
            for(var i=1; i<arguments.length; ++i) {
                args.push(arguments[i]);
            }
   return methods[name].apply(instance, args);
  };
        return instance;
    };
    //判斷一個類型是不是另一個類型的子類型
    var isSubClass = Function.prototype.isSubClass = function(superClass) {
        var that = this;
        var subClass = that;
        while(subClass) {
            if(subClass === superClass) {
                return true;
            }
            else {
                subClass = subClass.superClass;
            }
        }
        return false;
    };
    //基礎父類,類似於object,所有的子類都要繼承它
    var Base = ns_site.Base = function() {
        var that = {};
       
        //對象實例的類型信息
        var superClass = Base;
  //因爲使用apply方式調用所以this就是extend方法中的that
        var subClass = this.subClass;
        while(subClass) {
            subClass.superClass = superClass;
   //clsObj是實例的類型元信息
            that.clsObj = subClass;
            superClass = that.clsObj;
            subClass = superClass.subClass;
        }
        //判斷一個對象是不是某一類型的實例
        that.isInstance = function(cls) {
            var clsObj = that.clsObj;
            while(clsObj) {
                if(clsObj === cls) {
                    return true;
                }
                else {
                    clsObj = clsObj.superClass;
                }
            }           
            return false;
        };            
        return that;
    };
})();
在這裏先說明一下我比較討厭this關鍵字,這東西在使用的時候你要充分考慮好作用域上下文它不一定代表什麼,所以我採取了that = {}這種創建對象的方式。下面看一下簡單的用例:
(function() {
 s_from('site').s_import('*');
 var Persion = function(name, age) {
  var _name = name;
  var _age = age;
  var that = Base.extend();
  that.getName = function() {
   return _name;
  };
  return that;
 };
 var User = function(name, age, pwd) {
  var _pwd = pwd;
  var that = Persion.extend(name, age);
  that.getName = function() {
   return 'user/' name is ' + that.superMethod('getName');
  };
  return that;
 }
 
 var user = User('lucifer', 27, 'aa');
 alert(user.getName());
 alert(user.isInstance(Persion));
 alert(User.isSubClass(Base));
})();
在上面的例子中你可以很自然的從User構造函數中調用父類Persiond的構造函數並且覆寫getName方法也很簡單。不過javascript並不是真正意義上的oo語言所以它還是沒辦法實現在父類中調用子類覆寫方法這個功能(當然如果你有實現的方法請告知我,萬分感謝)。最讓讓我們看看事件機制的實現:
//事件
var Event = ns_site.Event = function(obj) {
 var _events = {};
 var that = {};
 that.on = function(type, fn, params) {
  var handle = { fn: fn, params: params };
  if(_events[type]) {
   _events[type].push(handle);
  }
  else {
   _events[type] = [handle];
  }
  return that;
 };
 that.fire = function(type) {
  if(_events[type]) {
   var arr = _events[type];
   for(var i=0; i<arr.length; ++i) {
    var handle = arr[i];
    var fn = handle.fn;
    if(typeof(fn) === 'string') {
     fn = obj[fn];
    }
    if(fn) {
     fn.apply(obj, handle.params || [type]);
    }
   }
  }          
  return that;
 };
 return that;
};
它的實現很簡單就是記錄一個類中的事件,並只能在類中觸發事件這符合事件的定義。並因爲使用apply方式調用,所以你可以在事件處理函數中通過this來使用該類的實例它等同於我們平時使用的sender。事例代碼:
(function() {
 s_from('site').s_import('*');
 var Form = function(id) {
  var that = Base.extend();
  var _id = id;
  var _event = Event(that);
  that.getId = function() {
   return _id;
  };
  that.loadEvent = function(fn) {
   _event.on('load', fn);
  }
  that.show = function() {
   _event.fire('load');
  }
  that.myLoad = function() {
   alert(_id + ' is loading.');
  }
  _event.on('load', 'myLoad');
  return that;
 };
 
 var form1 = Form('form1');
 
 form1.loadEvent(function() {
  alert('hello ' + this.getId());
 });
 
 form1.show();
})();
好了有了這些基礎工具我就可以對我的項目中的代碼進行重構了最爲主要的工作就是把一些公用的組件封裝爲類似WinForm中的組件。比如jqGrid使我們項目裏的一個核心組件並對它做出了一些定製,我就先定義了一個基礎類Workspace該類中有getName,init,load,show等方法,然後在封裝jqGrid作爲一個Grid控件。這樣在頁面中就可以先添加一個Worksapce然後再往上放Grid等控件,Workspace在show的時候它的子類也就可以init,load後並show出來了。

代碼看着不舒服的下源碼吧(url: http://download.csdn.net/source/1768189),並推薦firefox和firbug插件來調試js。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章