原文地址: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)替换为另一个算法。
算法過於複雜,或則循環過於深入都是影響代碼閱讀的因素,這裡我們需要了解循环复杂度。可以用【提取函數】或替換算法來解決這類複雜問題。