每個人都有自己的程式設計風格,也無可避免的要去感受別人的程式設計風格——修改別人的程式碼。”修改別人的程式碼”對於我們來說的一件很痛苦的事情。因為有些程式碼並不是那麼容易閱讀、可維護的,讓另一個人來修改別人的程式碼,或許最終只會修改一個變數,調整一個函式的呼叫時機,卻需要花上1個小時甚至更多的時間來閱讀、縷清別人的程式碼。本文一步步帶你重構一段獲取位置的”元件”——提升你的javascript程式碼的可讀性和穩定性。
本文內容如下:
- 分離你的javascript程式碼
- 函式不應該過分依賴外部環境
- 語義化和複用
- 元件應該關注邏輯,行為只是封裝
- 形成自己的風格的程式碼
分離你的javascript程式碼
下面一段程式碼演示了難以閱讀/修改的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(function (window, namespace) { var $ = window.jQuery; window[namespace] = function (targetId, textId) { //一個嘗試複用的獲取位置的"元件" var $target = $('#' + targetId),//按鈕 $text = $('#' + textId);//顯示文字 $target.on('click', function () { $text.html('獲取中'); var data = '北京市';//balabala很多邏輯,虛擬碼,獲取得到位置中 if (data) { $text.html(data); } else $text.html('獲取失敗'); }); } })(window, 'linkFly'); |
這一段程式碼,我們暫且認可它已經構成一個”元件”。
上面的程式碼就是典型的一個方法搞定所有事情,一旦填充上內部的邏輯就會變得生活不能自理,而一旦增加需求,例如獲取位置返回的資料格式需要加工,那麼就要去裡面尋找處理資料的程式碼然後修改。
我們分離一下邏輯,得到程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
(function (window, namespace) { var $ = window.jQuery, $target, $text, states= ['獲取中', '獲取失敗']; function done(address) {//獲取位置成功 $text.html(address); } function fail() { $text.html(states[1]); } function checkData(data) { //檢查位置資訊是否正確 return !!data; } function loadPosition() { var data = '北京市';//獲取位置中 if (checkData(data)) { done(data); } else fail(); } var init = function () { $target.on('click', function () { $text.html(states[0]); loadPosition(); }); }; window[namespace] = function (targetId, textId) { $target = $('#' + targetId); $text = $('#' + textId); initData(); setData(); } })(window, 'linkFly'); |
函式不應該過分依賴外部環境
上面的程式碼中,我們已經把整個元件,切割成了各種函式(注意這裡我說的是函式,不是方法),這裡常出現一個新的問題:函式過分依賴不可控的變數。
變數$target和$text身為環境中的全域性變數,從元件初始化便賦值,而我們切割後的程式碼大多數的操作方法都依賴$text,尤其是$text和done()、fail()之間曖昧的關係,一旦$text相關的結構、邏輯改變,那麼我們的程式碼將會進行不小的改動。
和頁面/DOM相關的都是不可信賴的(例如$target和$text),一旦頁面結構發生改變,它的行為很大程度上也會隨之改變。而函式也不應該依賴外部的環境。
在不可控的變數上,我們應該解開函式和依賴變數上的關係,讓函式變得更加專注自己區域的邏輯,更加的純粹。簡單的說:函式所依賴的外部變數,都應該通過引數傳遞到函式內部。
新的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
(function (window, namespace) { var $ = window.jQuery; //檢查位置資訊是否正確 function checkData(data) { return !!data; } //獲取位置中 function loadPosition(done, fail) { var data = '北京市';//獲取位置中 if (checkData(data)) { done(data); } else fail(); } window[namespace] = function (targetId, textId) { var $target = $('#' + targetId), $text = $('#' + textId); var states = ['獲取中', '獲取失敗']; $target.on('click', function () { $text.html(states[0]); loadPosition(function (address) {//獲取位置成功 $text.html(address); }, function () {//獲取位置失敗 $text.html(states[1]); }); }); } })(window, 'linkFly'); |
語義化和複用
變數states是一個陣列,它描述的行為難以閱讀,每次看到states[0]都有一種分分鐘想捏死原作者的衝動,因為我們總是要記住變數states的值,在程式碼上,我們應該儘可能讓它可以很好的被閱讀。
另外,上面的程式碼中$text.html就是典型的程式碼重複,我們再一次的修改程式碼,請注意這一次修改的程式碼中,我們所抽離的changeStateText()的程式碼位置,它並沒有被提升到上一層環境中(也就是整個大閉包的環境)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
(function (window, namespace) { var $ = window.jQuery; function checkData(data) { return !!data; } function loadPosition(done, fail) { var data = '北京市';//獲取位置中 if (checkData(data)) { done(data); } else fail(); } window[namespace] = function (targetId, textId) { var $target = $('#' + targetId), $text = $('#' + textId), changeEnum = { LOADING: '獲取中', FAIL: '獲取失敗' }, changeStateText = function (text) { $text.html(text); }; $target.on('click', function () { changeStateText(changeEnum.LOADING); loadPosition(function (address) { changeStateText(address); }, function () { changeStateText(changeEnum.FAIL); }); }); } })(window, 'linkFly'); |
提及語義化,我們必須要知道當前整個程式碼的邏輯和語義:
在這整個元件中,所有的函式模組可以分為:工具和工具提供者。
上一層環境(整個大閉包)在我們的業務中扮演著工具的身份,它的任務是締造一套和獲取位置邏輯相關的工具,而在window[namespace])函式中,則是工具提供者的身份,它是唯一的入口,負責提供元件完整的業務給工具的使用者。
這裡的$text.html()在邏輯上並不屬於工具,而是屬於工具提供者使用工具後所得到的反饋,所以changeStateText()函式置於工具提供者window[namespace]()中。
元件應該關注邏輯,行為只是封裝
到此為止,我們分離了函式,並讓這個元件擁有了良好的語義。但這時候來了新的需求:當沒有獲取到位置的時候,需要進行一些其他的操作。這時候會發現,我們需要window[namespace]()上加上新的引數。
當我們加上新的引數之後,又被告知新的需求:當獲取位置失敗了之後,需要修改一些資訊,然後再次嘗試獲取位置資訊。
不過幸好,我們的程式碼已經把大部分的邏輯抽離到了工具提供者中了,對整個工具的邏輯影響並不大。
同時我們再看看程式碼就會發現我們的元件除了工具提供者之外,沒有方法(依賴在物件上的函式)。也就是說,我們的元件並沒有物件。
我見過很多人的程式碼總是喜歡打造工具提供者,而忽略了工具的本質。迎合上面的增加的需求,那麼我們的工具提供者將會變得越來越重,這時候我們應該思考到:是不是應該把工具提供出去?
讓我們回到最初的需求——僅僅只是一個獲取位置的元件,沒錯,它的核心業務就是獲取位置——它不應該被元件化。它的本質應該是個工具物件,而不應該和頁面相關,我們從一開始就不應該關注頁面上的變化,讓我們重構程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(function (window, namespace) { var Gps = { load: function (fone, fail) { var data = '北京市';//獲取位置虛擬碼 this.check(data) ? done(data, Gps.state.OK) : fail(Gps.state.FAIL); }, check: function (data) { return !!data; }, state: { OK: 1, FAIL: 0 } }; window[namespace] = Gps; })(window, 'Gps'); |
在這裡,我們直接捏死了工具提供者,我們直接將工具提供給外面的工具使用者,讓工具使用者直接使用我們的工具,這裡的程式碼無關狀態、無關頁面。
至此,重構完成。
形成自己風格的程式碼
之所以講這個是因為大家都有自己的程式設計風格。有些人的程式設計風格就是開篇那種程式碼的…
我覺得形成自己的程式設計風格,是建立在良好程式碼的和結構/語義上的。否則只會讓你的程式碼變得越來越難讀,越來越難寫。
****
單var和多var
我個人是喜歡單var風格的,不過我覺得程式碼還是儘可能在使用某一方法/函式使用前進行var,有時候甚至於為了單var而變得喪心病狂:由於我又過分的喜愛函式表示式宣告,函式表示式宣告並不會在var語句中執行,於是偶爾會出現這種邊宣告邊執行的程式碼,為了不教壞小朋友就不貼程式碼了(我不會告訴你們其實是我找不到了)。
物件屬性的遮蔽
下面的程式碼演示了兩種物件的構建,後一種通過閉包把內部屬性隱藏,同樣,兩種方法都實現了無new化,我個人…是不喜歡看見很多this的..但還是推薦前者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
(function () { //第一種,曝露了_name屬性 var Demo = function () { if (!(this instanceof Demo)) return new Demo(); this._name = 'linkFly'; }; Demo.prototype.getName = function () { return this._name; } //第二種,多一層閉包意味記憶體消耗更大,但是遮蔽了_name屬性 var Demo = function () { var name = 'linkFly'; return { getName: function () { return name; } } } }); |
巧用變數置頂[hoisting]
巧用函式宣告的變數置頂特性意味著處女座心態的你放棄單var,但卻可以讓你的函式在程式碼結構上十分清晰,例如下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
(function () { var names = []; return function (name) { addName(name); } function addName(name) { if (!~names.indexOf(name))//如果存在則不新增 names.push(name); console.log(names);// ["linkFly"] } }())('linkFly'); |
if和&&
這種程式碼,在幾個群裡都見過討論:
1 2 3 4 5 6 7 8 9 10 |
(function () { var key = 'linkFly', cache = { 'linkFly': 'http://www.cnblogs.com/silin6/' }, value; //&&到底 key && cache && cache[key] && (value = cache[key]); //來個if if (key && cache && cache[key]) value = cache[key]; })(); |
大概就想到這麼些了,我突然發現我不太推薦的程式碼,都是我寫的程式碼,囧。如果各位也還有更多有趣的程式碼,希望各位看官能掏出來讓小弟見識見識。