原文地址:http://www.monring.com/front_end/javascript-refactor-composing-methods.html
JavaScript代码重构系列中,最重要的要算這節了: 重新组织你的函数
-
提炼函数
-
将函数内联化
-
用查询取代临时变量
-
以临时变量取代高消耗的查询
-
将临时变量内联化
-
引入解释性变量
-
剖解临时变量
-
移除对参数的赋值动作
-
以函数对象取代函数
-
替换你的算法
提炼函数
信號:你有一段代码可以被组织在一起并独立出来。
操作:将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
當你有一個很長的函數,這也是一個非常直接的重構信號,但真正能讓你用【提煉函數】方法重構代碼時候,必須發現有一段代碼可以被組織在一起,也就是說這段代碼能夠看作一個獨立的工作模塊。
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
36
37
38
39
40
41
42
43
44
|
/* * 原函數 */ composingMethodsDemo.printOwing = function (order) { //print header document.writeln( '===================' ); document.writeln( '== print amount ===' ); document.writeln( '===================' ); var _order = order, outstanding = 0; for ( var i = 0, len = _order.records.length; i < len; i++) { outstanding = outstanding + _order.records[i].expenditure; } //print detail document.writeln( 'name: ' + _order.name); document.writeln( 'amount: ' + outstanding); }; /* * 重構後函數 */ composingMethodsDemo.newPrintOwing = function (order) { var outstanding = this .getOutstanding(order); this .extPrintHeader(); this .extPrintDetail(order.name, outstanding); }; // 无局部变量,直接提出 composingMethodsDemo.extPrintHeader = function () { document.writeln( '===================' ); document.writeln( '== print amount ===' ); document.writeln( '===================' ); }; // 有局部变量, 以參數傳遞形式輸入 composingMethodsDemo.extPrintDetail = function (name, outstanding) { document.writeln( 'name: ' + name); document.writeln( 'amount: ' + outstanding); }; // 对局部变量再赋值 composingMethodsDemo.getOutstanding = function (order) { var outstanding = 0; for ( var i = 0, len = order.records.length; i < len; i++) { outstanding = outstanding + order.records[i].expenditure; } return outstanding; }; |
将函数内联化
信號:一个函数,它本体应该与其名称同样清楚易懂。
操作:在函数调用点插入函数本体,然后移除该函数。
這條重構方法跟【提煉函數】是相對立的,如果你發現你【提煉函數】的函數它的內容跟它名字一樣清晰易懂,那麼還是把它內聯回去,讓它看上去更直接。
也有可能【提煉函數】重構完後,長函數(主函數)中添加了一些代碼,這時候被提取出來的函數寫到長函數中更直接,那我們也需要先內聯回去,然後再看它和其他代碼一起組織成新函數是否能再利用【提煉函數】。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/* * 原函數 */ composingMethodsDemo.getRating = function (numberOfLateDeliveries) { return ( this .moreThanFiveLateDeliveries(numberOfLateDeliveries)) ? 2 : 1; }; composingMethodsDemo.moreThanFiveLateDeliveries = function (numberOfLateDeliveries) { return numberOfLateDeliveries > 5; }; /* * 重構後 */ composingMethodsDemo.getRating = function (numberOfLateDeliveries) { return (numberOfLateDeliveries > 5) ? 2 : 1; }; |
用查询取代临时变量
信號:你的程序以一个临时变量保存某一表达式的运算结果。
操作:将这个表达式提炼到一个独立函数中。将这个临时变量的所有被引用点替换为对新函数的调用。新函数可被其他函数使用。
臨時變量一般在函數內部實用,這樣就促使你不得不讓你的函數變得更長,這樣才能使你的代碼訪問到你的臨時變量。這時候,如果將臨時變量換成一個查詢式,那麼代碼就更清晰,更簡潔了,但這個需要將高消耗的查詢除外。
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
|
// 用查询取代临时变量 var composingMethodsDemo = { _quantity: 30, _itemPrice: 12 } /* * 原函數 */ composingMethodsDemo.getTotalPrice = function () { var basePrice = this ._quantity * this ._itemPrice; if (basePrice > 1000) { return basePrice * 0.95; } else { return basePrice * 0.98; } }; /* * 重構後 */ composingMethodsDemo.getTotalPrice = function () { if ( this .getBasePrice() > 1000) { return this .getBasePrice() * 0.95; } else { return this .getBasePrice() * 0.98; } }; composingMethodsDemo.getBasePrice = function () { return this ._quantity * this ._itemPrice; }; |
用临时变量取代高消耗的查询
信號:多次調用的查詢式是一個高消耗的函數
操作:用臨時變量儲存該查詢,將該查詢式調用處用臨時變量代替
該方法與【用查询取代临时变量】是對立的,但出發點不同,本方法出發點最重要的為了提升代碼性能,一些高消耗的方法如果在函數體中多次調用,我們需要用臨時變量緩存起來。最常見的是DOM查詢式,高循環運算的函數查詢式。
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
|
// 用临时变量取代高消耗的查询 composingMethodsDemo.getTotalPrice = function () { if ( this .getBasePrice() > 1000) { return this .getBasePrice() * 0.95; } else { return this .getBasePrice() * 0.98; } }; composingMethodsDemo.getBasePrice = function () { return parseFloat(document.getElementById( 'quantity' ).value) * parseFloat(document.getElementById( 'itemPrice' ).value); }; /* * 重構後 */ composingMethodsDemo.getTotalPrice = function () { var basePrice = this .getBasePrice(); if (basePrice > 1000) { return basePrice * 0.95; } else { return basePrice * 0.98; } }; composingMethodsDemo.getBasePrice = function () { return parseFloat(document.getElementById( 'quantity' ).value) * parseFloat(document.getElementById( 'itemPrice' ).value); }; |
将临时变量内联化
信號:你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
操作:将所有对该变量的引用动作,替换为对它赋值的那个表达式本身。
本方法多半是作为【用查询取代临时变量】的一部分来使用,所以真正的动机出现在后者那儿。惟一单独使用本方法的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,你可以放心地把它留在那儿。但如果这个临时变量妨碍了其他的重构方法(例如【提取函數】),你就应该将它內聯化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 将临时变量内联化 var composingMethodsDemo = { _quantity: 30, _itemPrice: 12 }; /* * 原函數 */ composingMethodsDemo.isOverMaxPrice = function () { var basePrice = this .getBasePrice(); return basePrice > 1000; }; composingMethodsDemo.getBasePrice = function () { return this ._quantity * this ._itemPrice; }; /* * 重構後 */ composingMethodsDemo.isOverMaxPrice = function () { return this .getBasePrice() > 1000; }; composingMethodsDemo.getBasePrice = function () { return this ._quantity * this ._itemPrice; }; |
引入解释性变量
信號:你有一个复杂的表达式。
操作:将该表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
【引入解釋性變量】能使方法體看上去更加易懂。特別是當表達式特別複雜的情況下,更能體現該方法的優勢。該方法非常有用,但很多時候我們都實用【提取函數】的方法來重構,畢竟臨時變量只對當前函數體有效,所以只有當局部變量令【提取函數】難以進行,我們才實用【引入解釋性變量】方法來處理重構。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 引入解释性变量 /* * 原函數 */ composingMethodsDemo.getMacIEVersion = function (platform, browser) { if ((platform.toUpperCase().indexOf( "MAC" ) > -1) && (browser.toUpperCase().indexOf( "IE" ) > -1)) { return browser.version; } return null ; }; /* * 重構後 */ composingMethodsDemo.getMacIEVersion = function (platform, browser) { var isMac = platform.toUpperCase().indexOf( "MAC" ) > -1, isIE = browser.toUpperCase().indexOf( "IE" ) > -1; if (isMac && isIE) { return browser.version; } return null ; }; |
剖解临时变量
信號:你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不是一个集用临时变量(collecting temporary variable)。
操作:针对每次赋值,创造一个独立的、对应的临时变量。
除了循環變量和集用臨時變量這兩種臨時變量被多次賦值的情況,其他被多次賦值的臨時變量都在敲醒我們重構的警鐘。每個臨時變量應該只承擔一個單獨的責任,例如【用临时变量取代高消耗的查询】中生成的臨時變量,如果賦值多次,就應該重構以多個臨時代替。
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
|
// 剖解临时变量 composingMethodsDemo.getActivedUsers = function (users) { var names = [], // 集用临时变量 count = users.length; for ( var i = 0; i < count; i++) { // i就是循环变量 if (users[i].actived) { names.push(users[i].name); } } return names; }; /* * 原函數 */ composingMethodsDemo.printRectangleInfo = function (height, width) { var temp = 2 * (height + width); document.writeln(temp); temp = height * width; document.writeln(temp); }; /* * 重構後 */ composingMethodsDemo.printRectangleInfo = function (height, width) { var perimeter = 2 * (height + width), area = height * width; document.writeln(perimeter); document.writeln(area); }; |
移除对参数的赋值动作
信號:你的代码对一个参数进行赋值动作。
操作:以一个临时变量取代该参数的位置,以函數返回值在原函數改變對象。
在JavaScript中非常特別的一個地方,但一個對象作為參數傳遞時,傳遞的往往是一個原始的引用,甚至都不是副本。這樣出現的問題就是,參數被修改後會引起原函數中的對象會相應被修改,引起很多混雜不清晰的現象,甚至會是錯誤。所以如果參數是一個值類型,可以以一个临时变量取代该参数的位置。如果參數為一個引用類型,這時就需要避免這種情況出現。
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// 移除对参数的赋值动作 /* * 原函數(值傳遞) */ composingMethodsDemo.printAreaSize = function (height, width, padding) { height = height + 2 * padding; width = width + 2 * padding; document.writeln(height * width); }; /* * 重構後(值傳遞) */ composingMethodsDemo.printAreaSize = function (height, width, padding) { var outHeight = height + 2 * padding; outWidth = width + 2 * padding; document.writeln(outHeight * outWidth); }; /* * 原函數(引用傳遞) */ composingMethodsDemo.printRectangleInfo = function (height, width) { var rectang = { height: 12, width: 15, padding: 5 }; this .printArea(rectang); // 這裡正確 this .printPerimeter(rectang); // 由於rectang引用在printArea中被修改,這裏的輸出結構就錯了 }; composingMethodsDemo.printArea = function (rectang) { //這裡需要避免修改,如需更改參數對象,請在原函數進行修改 rectang.height = rectang.height + 2 * rectang.padding; rectang.width = rectang.width + 2 * rectang.padding; document.writeln(rectang.height * rectang.width); }; composingMethodsDemo.printPerimeter = function (rectang) { rectang.height = rectang.height + 2 * rectang.padding; rectang.width = rectang.width + 2 * rectang.padding; document.writeln(2 * (rectang.height + rectang.width)); }; /* * 重構後(引用傳遞) */ composingMethodsDemo.printRectangleInfo = function (height, width) { var rectang = { height: 12, width: 15, padding: 5 }; this .printArea(rectang); // 這裡正確, 但rectang被更改 this .printPerimeter(rectang); // 輸出錯誤,rectang再次被更改 }; composingMethodsDemo.printArea = function (rectang) { var height = rectang.height + 2 * rectang.padding, width = rectang.width + 2 * rectang.padding; document.writeln(height * width); }; composingMethodsDemo.printPerimeter = function (rectang) { var height = rectang.height + 2 * rectang.padding, width = rectang.width + 2 * rectang.padding; document.writeln(2 * (height + width)); }; |
以函数对象取代函数
信號:你有一个大型函数,其中对局部变量的使用,使你无法釆用【提取函數】。
操作:将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的值域(field) 然后你可以在同一个对象中将这个大型函数分解为数个小型函数。
如果一個大型函數,功能比較單一,且局部變量很多,這時候我們沒辦法採用單純的【提取函數】方法來重構,這個時候我們就可以把大型函數轉換成一個函數對象來處理,然後將函數體分解成小函數。
替换你的算法
信號:把某个算法替换为另一个更清晰的算法。
操作:将函数本体(method body)替换为另一个算法。
算法過於複雜,或則循環過於深入都是影響代碼閱讀的因素,這裡我們需要了解循环复杂度。可以用【提取函數】或替換算法來解決這類複雜問題。