首頁 > web前端 > js教程 > 主體

教你如何寫出小而清晰的JavaScript函數

怪我咯
發布: 2017-04-05 14:57:36
原創
968 人瀏覽過

本文以 JavaScript 為例,介紹了該如何最佳化函數,讓函數清晰易讀,且更有效率且穩定。

軟體的複雜度一直在持續成長。程式碼品質對於確保應用的可靠性、易擴展性非常重要。

然而,幾乎每一個開發者,包括我自己,在職業生涯中都見過低品質的程式碼。這東西就是個坑。低品質程式碼具備以下極具殺傷力的特點:

  • 函數超長,而且塞滿了各種亂七八糟的功能。

  • 函數通常會有一些副作用,不僅難以理解,甚至根本沒辦法除錯。

  • 含糊的函數、變數命名。

  • 脆弱的程式碼:一個小的變更,就有可能出乎意料的破壞其他應用元件。

  • 程式碼覆蓋率缺失。

它們聽起來基本上都是: “我根本無法理解這段程式碼是如何運作的”,“這段程式碼就是一堆亂麻”,“要修改這一段程式碼實在太難了」 等等。

我就曾經遇到過這樣的情況,我的一個同事由於無法繼續將一個基於Ruby 的 REST API 做下去,繼而離職。這個專案是他從之前的開發團隊接手的。

修復現有的 bug ,然後引入了新的 bug,添加新的特性,就增加了一連串新 bug,如此循環(所謂的脆弱程式碼)。客戶不希望以更好的設計重構整個應用,開發人員也做出明智的選擇-維持現狀。

教你如何寫出小而清晰的JavaScript函數

好吧,這種事兒常常發生,而且挺糟糕的。那我們能做點什麼呢?

首先,需要謹記於心:只是讓應用程式運轉起來,並且盡心保證程式碼品質是兩個完全不同的事。一方面,你需要實現產品需求。但另一方面,你應該花點時間,確保函數功能簡單、使用易讀的變數和函數命名,避免函數的副作用等等。

函數(包含物件方法)是讓應用程式運轉的齒輪。首先你應該將注意力集中在他們的結構和整體佈局上。這篇文章包括了一些非常好的範例,展示如何編寫清晰、易於理解和測試的函數。

1. 函數應當很小,非常小

避免使用包含大量的函數的大函數,應將其功能分割為若干較小的函數。大的黑盒子函數難於理解、修改,特別是很難測試。

假設這樣一個場景,需要實作一個函數,用於計算 array、map 或 普通 JavaScript 物件的權重。總權重可透過計算各成員權重來獲得:

  • null 或 未定義變數計 1 點。

  • 基本型別計 2 點。

  • 物件或函數計 4 點。

例如,陣列[null, 'Hello World', {}] 的權重這樣計算:1(null) + 2(string 是基本型別) + 4(物件) = 7 。

Step 0: 最初的大函數

我們從最糟的實例開始。所有的邏輯都被編碼在函數 getCollectionWeight() 中:

 

#
function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = [...collection.values()];
  } else {
    collectionValues = Object.keys(collection).map(function (key) {
      return collection[key];
    });
  }
  return collectionValues.reduce(function(sum, item) {
    if (item == null) {
      return sum + 1;
    } 
    if (typeof item === 'object' || typeof item === 'function') {
      return sum + 4;
    }
    return sum + 2;
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登入後複製

問題顯而易見,getCollectionWeight() 函數超級長,而且看起來像一個裝滿「意外」的黑盒子。可能你也發現了,第一眼根本搞不明白它要做什麼。再試想一下,應用程式裡有大把這樣的函數。

在工作中遇到這樣的程式碼,就是在浪費你的時間和精力。反之,高品質的程式碼不會令人不適。在高品質程式碼中,那些精巧、自文檔極好的函數非常易於閱讀和理解。

教你如何寫出小而清晰的JavaScript函數

Step 1:依照型別計算權重,拋棄那些「迷之數字」。

現在,我們的目標是:把這個巨型函數,分割成較小的、獨立的、可重複使用的一組函數。第一步,將根據類型計算權重的程式碼提取出來。這個新的函數命名為 getWeight()。

我們再看看這幾個「迷之數字」: 1, 2, 4。在不知道整個故事背景的前提下,僅靠這幾個數字提供不了任何有用的信息。幸好 ES2015 允許定義靜態只讀引用,那你就能簡單的創造幾個常數,用有意義的名稱,替換掉那幾個「迷之數字」。 (我特別喜歡「迷之數字」這個說法:D)

我們來新建一個較小的函數 getWeightByType(),並用它來改進 getCollectionWeight():

 

#
// Code extracted into getWeightByType() function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED  = 1;
  const WEIGHT_PRIMITIVE      = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
} function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = [...collection.values()];
  } else {
    collectionValues = Object.keys(collection).map(function (key) {
      return collection[key];
    });
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登入後複製

看起來好多了,對吧? getWeightByType() 函數是一個獨立的元件,僅用於決定各類型的權重值。而且它是可重複使用的,你可以在其他任何函數中使用它。

getCollectionWeight() 稍微瘦了點身。

WEIGHT_NULL_UNDEFINED , WEIGHT_PRIMITIVE 也有 WEIGHT_OBJECT_FUNCTION 都是具備自文檔能力的常數,透過它們的名字就可以看出各類型的權重。你就不需要猜測 1、2、4 這些數字的意義。

Step 2: 繼續切分,使之具備擴展性

然而,這個升級版依然有不足的地方。假如你打算對一個 Set,甚至其他使用者自訂集合來實現權值計算。 getCollectionWeight() 會快速膨脹,因為它包含了一組獲得權值的具體邏輯。

讓我們將取得 maps 權重的程式碼提取到 getMapValues() ,將取得基本 JavaScript 物件權值的程式碼則放到 getPlainObjectValues() 中。看看改進後的版本吧。

 

function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED = 1;
  const WEIGHT_PRIMITIVE = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
} // Code extracted into getMapValues() function getMapValues(map) {  
  return [...map.values()];
} // Code extracted into getPlainObjectValues() function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
} function getCollectionWeight(collection) {  
  letcollectionValues;
  if (collectioninstanceof Array) {
    collectionValues = collection;
  } else if (collectioninstanceof Map) {
    collectionValues = getMapValues(collection);
  } else {
    collectionValues = getPlainObjectValues(collection);
  }
  return collectionValues.reduce(function(sum, item) {
    return sum + getWeightByType(item);
  }, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登入後複製

現在再來看 getCollectionWeight() 函數,你會發現已經比較容易明白它的機理,看起來就像一段有趣的故事。

每一個函數的簡單明了。你不需要花時間去挖掘程式碼,理解程式碼的工作。這就是清新版代碼該有的樣子。

Step 3: 優化永無止境

就算到了現在這種程度,依然有很大優化的空間!

你可以建立一個獨立的函數getCollectionValues() ,使用if/else 語句區分集合中的類型:

function getCollectionValues(collection) {  
  if (collectioninstanceof Array) {
    return collection;
  }
  if (collectioninstanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
}
登入後複製

那麼, getCollectionWeight() 應該會變得異常純粹,因為它唯一的工作:用 getCollectionValues() 取得集合中的值,然後依序呼叫求和累加器。

你也可以创建一个独立的累加器函数:

function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
}
登入後複製

理想情况下 getCollectionWeight() 函数中不应该定义函数。

最后,最初的巨型函数,已经被转换为如下一组小函数:

function getWeightByType(value) {  
  const WEIGHT_NULL_UNDEFINED = 1;
  const WEIGHT_PRIMITIVE = 2;
  const WEIGHT_OBJECT_FUNCTION = 4;
  if (value == null) {
    return WEIGHT_NULL_UNDEFINED;
  } 
  if (typeof value === 'object' || typeof value === 'function') {
    return WEIGHT_OBJECT_FUNCTION;
  }
  return WEIGHT_PRIMITIVE;
} function getMapValues(map) {  
  return [...map.values()];
} function getPlainObjectValues(object) {  
  return Object.keys(object).map(function (key) {
    return object[key];
  });
} function getCollectionValues(collection) {  
  if (collectioninstanceof Array) {
    return collection;
  }
  if (collectioninstanceof Map) {
    return getMapValues(collection);
  }
  return getPlainObjectValues(collection);
} function reduceWeightSum(sum, item) {  
  return sum + getWeightByType(item);
} function getCollectionWeight(collection) {  
  return getCollectionValues(collection).reduce(reduceWeightSum, 0);
}
letmyArray = [null, { }, 15];  
letmyMap = new Map([ ['functionKey', function() {}] ]);  
letmyObject = { 'stringKey': 'Hello world' };  
getCollectionWeight(myArray);  // => 7 (1 + 4 + 2)   getCollectionWeight(myMap);    // => 4   getCollectionWeight(myObject); // => 2
登入後複製

这就是编写简单精美函数的艺术!

除了这些代码质量上的优化之外,你也得到不少其他的好处:

  • 通过代码自文档,getCollectionWeight() 函数的可读性得到很大提升。

  • getCollectionWeight() 函数的长度大幅减少。

  • 如果你打算计算其他类型的权重值,getCollectionWeight() 的代码不会再剧烈膨胀了。

  • 这些拆分出来的函数都是低耦合、高可复用的组件,你的同事可能希望将他们导入其他项目中,而你可以轻而易举的实现这个要求。

  • 当函数偶发错误的时候,调用栈会更加详细,因为栈中包含函数的名称,甚至你可以立马发现出错的函数。

  • 这些小函数更简单、易测试,可以达到很高的代码覆盖率。与其穷尽各种场景来测试一个大函数,你可以进行结构化测试,分别测试每一个小函数。

  • 你可以参照 CommonJS 或 ES2015 模块格式,将拆分出的函数创建为独立的模块。这将使得你的项目文件更轻、更结构化。

这些建议可以帮助你,战胜应用的复杂性。

教你如何寫出小而清晰的JavaScript函數

原则上,你的函数不应当超过 20 行——越小越好。

现在,我觉得你可能会问我这样的问题:“我可不想将每一行代码都写为函数。有没有什么准则,告诉我何时应当停止拆分?”。这就是接下来的议题了。

2. 函数应当是简单的

让我们稍微放松一下,思考下应用的定义到底是什么?

每一个应用都需要实现一系列需求。开发人员的准则在于,将这些需求拆分为一些列较小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。

一个组件又由其他更小的组件构成。如果你希望编写一个组件,你只能从抽象层中低一级的组件中,选取需要的组件用于创建自己的组件。

换言之,你需要将一个函数分解为若干较小的步骤,并且保证这些步骤都在抽象上,处于同一级别,而且只向下抽象一级。这非常重要,因为这将使得函数变得简单,做到“做且只做好一件事”。

为什么这是必要的?因为简单的函数非常清晰。清晰就意味着易于理解和修改。

我们来举个例子。假设你需要实现一个函数,使数组仅保留 素数 (2, 3, 5, 7, 11 等等),移除非素数(1, 4, 6, 8 等等)。函数的调用方式如下:

getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
登入後複製

如何用低一级抽象的若干步骤实现 getOnlyPrime() 函数呢?我们这样做:

为了实现 getOnlyPrime() 函数, 我们用 isPrime() 函数来过滤数组中的数字。

非常简单,只需要对数字数组执行一个过滤函数 isPrime() 即可。

你需要在当前抽象层实现 isPrime() 的细节吗?不,因为 getOnlyPrime() 函数会在不同的抽象层实现一些列步骤。否则,getOnlyPrime() 会包含过多的功能。

在头脑中谨记简单函数的理念,我们来实现 getOnlyPrime() 函数的函数体:

function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
登入後複製

如你所见, getOnlyPrime() 非常简单,它仅仅包含低一级抽象层的步骤:数组的 .filter() 方法和 isPrime() 函数。

现在该进入下一级抽象。

数组的 .filter() 方法由 JavaScript 引擎提供,我们直接使用即可。当然,标准已经 准确描述 了它的行为。

现在你可以深入如何实现 isPrime() 的细节中了:

为了实现 isPrime() 函数检查一个数字 n 是否为素数,只需要检查 2 到 Math.sqrt(n) 之间的所有整数是否均不能整除n。

有了这个算法(不算高效,但是为了简单起见,就用这个吧),我们来为 isPrime() 函数编码:

function isPrime(number) {  
  if (number === 3 || number === 2) {
    return true;
  }
  if (number === 1) {
    return false;
  }
  for (letpisor = 2; pisor <= Math.sqrt(number); pisor++) {
    if (number % pisor === 0) {
      return false;
    }
  }
  return true;
} function getOnlyPrime(numbers) {  
  return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
登入後複製

getOnlyPrime() 很小也很清晰。它只从更低一级抽象中获得必要的一组步骤。

只要你按照这些规则,将函数变的简洁清晰,复杂函数的可读性将得到很大提升。将代码进行精确的抽象分级,可以避免出现大块的、难以维护的代码。

3. 使用简练的函数名称

函数名称应该非常简练:长短适中。理想情况下,名称应当清楚的概括函数的功用,而不需要读者深入了解函数的实现细节。

对于使用 骆驼风格 的函数名称,以小写字母开始:  addItem(), saveToStore() 或者  getFirstName() 之类。

由于函数都是某种操作,因此名称中至少应当包含一个动词。例如 deletePage(), verifyCredentials() 。需要 get 或 set 属性的时候,请使用 标准的  set 和 get 前缀: getLastName() 或  setLastName() 。

避免在生产代码中出现有误导性的名称,例如 foo(), bar(), a(), fun() 等等。这样的名称没有任何意义。

如果函数都短小清晰,命名简练:代码读起来就会像诗一样迷人。

4. 总结

当然了,这里假定的例子都非常简单。现实中的代码更加复杂。你可能要抱怨,编写清晰的函数,只在抽象上一级一级下降,实在太没劲了。但是如果从项目一开始就开始你的实践,就远没有想象中复杂。

如果应用中已经存在一些功能繁杂的函数,希望对它们进行重构,你可能会发现困难重重。而且在很多情况下,在合理的时间内是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出来。

當然,最正確的解決方案應該是,從專案一開始就以正確的方式實現應用。除了花一些時間在實現上,也應該花一些精力在組成合理的函數結構上:如我們所建議的——讓它們保持短小、清晰。

教你如何寫出小而清晰的JavaScript函數

ES2015 實作了一個非常棒的模組系統,它明確地建議,小函數是優秀的工程實踐。

記住,乾淨、組織良好的程式碼通常 需要投入大量時間 。你會發現這做起來有難度。可能需要很多嘗試,可能會迭代、修改一個函數很多次。

然而,沒有什麼比亂麻一樣的程式碼更讓人痛心的了,那麼這一切都是值得的!

以上是教你如何寫出小而清晰的JavaScript函數的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!