JavaScript代碼風格要素

原文地址javascript

本文已在前端早讀課公衆號首發:【第952期】JavaScript代碼風格要素前端

譯者:墨白 校對:野草java


1920年,由威廉·斯特倫克(William Strunk jr .)撰寫的《英語寫做手冊:風格的要素(The Elements of Style)》出版了,這本書列舉了7條英文寫做的準則,過了一個世紀,這些準則並無過期。對於工程師來講,你能夠在本身的編碼風格中應用相似的建議來指導平常的編碼,提升本身的編碼水平。es6

須要注意的是,這些準則不是一成不變的法則。若是違背它們,可以讓代碼可讀性更高,那麼便沒有問題,但請特別當心並時刻反思。這些準繩是經受住了時間考驗的,有充分的理由說明:它們一般是正確的。若是要違背這些規則,必定要有充足的理由,而不要單憑一時的興趣或者我的的風格偏好。web

書中的寫做準則以下:編程

  • 以段落爲基本單位:一段文字,一個主題。數組

  • 刪減無用的語句。promise

  • 使用主動語態。瀏覽器

  • 避免一連串鬆散的句子。服務器

  • 相關的內容寫在一塊兒。

  • 從正面利用確定語句去發表陳述。

  • 不一樣的概念採用不一樣的結構去闡述。

咱們能夠應用類似的理念到代碼編寫上面:

  1. 一個function只作一件事,讓function成爲代碼組合的最小單元。

  2. 刪除沒必要要的代碼。

  3. 使用主動語態。

  4. 避免一連串結構鬆散的,不知所云的代碼。

  5. 將相關的代碼寫在一塊兒。

  6. 利用判斷true值的方式來編寫代碼。

  7. 不一樣的技術方案利用不一樣的代碼組織結構來實現。

1.一個function只作一件事,讓function成爲代碼組合的最小單元

軟件開發的本質是「組合」。 咱們經過組合模塊,函數和數據結構來構建軟件。理解若是編寫以及組合方法是軟件開發人員的基本技能。

模塊是一個或多個function和數據結構的簡單集合,咱們用數據結構來表示程序狀態,只有在函數執行以後,程序狀態纔會發生一些有趣的變化。

JavaScript中,能夠將函數分爲3種:

  • I/O 型函數 (Communicating Functions):函數用來執行I/O。

  • 過程型函數 (Procedural Functions):對一系列的指令序列進行分組。

  • 映射型函數 (Mapping Functions):給定一些輸入,返回對應的輸出。

有效的應用程序都須要I/O,而且不少程序都遵循必定的程序執行順序,這種狀況下,程序中的大部分函數都會是映射型函數:給定一些輸入,返回相應的輸出。

每一個函數只作一件事情:若是你的函數主要用於I/O,就不要在其中混入映射型代碼,反之亦然。嚴格根據定義來講,過程型函數違反了這一指導準則,同時也違反了另外一個指導準則:避免一連串結構鬆散,不知所云的代碼。

理想中的函數是一個簡單的、明確的純函數:

  • 一樣的輸入,老是返回一樣的輸出。

  • 無反作用。

也能夠查看,「什麼是純函數?」

2. 刪除沒必要要的代碼

簡潔的代碼對於軟件而言相當重要。更多的代碼意味更多的bug隱藏空間。更少的代碼 = 更少的bug隱藏空間 = 更少的bug

簡潔的代碼讀起來更清晰,由於它擁有更高的「信噪比」:閱讀代碼時更容易從較少的語法噪音中篩選出真正有意義的部分。能夠說,更少的代碼 = 更少的語法噪聲 = 更強的代碼含義信息傳達

借用《風格的元素》這本書裏面的一句話就是:簡潔的代碼更健壯。

function secret (message) {
  return function () {
    return message;
  }
};

能夠簡化成:

const secret = msg => () => msg;

對於那些熟悉簡潔箭頭函數寫法的開發來講,可讀性更好。它省略了沒必要要的語法:大括號,function關鍵字以及return語句。

而簡化前的代碼包含的語法要素對於傳達代碼意義自己做用並不大。它存在的惟一意義只是讓那些不熟悉ES6語法的開發者更好的理解代碼。

ES6自2015年已經成爲語言標準,是時候去學習它了。

刪除沒必要要的代碼

有時候,咱們試圖爲沒必要要的事物命名。問題是人類的大腦在工做中可用的記憶資源有限,每一個名稱都必須做爲一個單獨的變量存儲,佔據工做記憶的存儲空間。

因爲這個緣由,有經驗的開發者會盡量地刪除沒必要要的變量。

例如,大多數狀況下,你應該省略僅僅用來當作返回值的變量。你的函數名應該已經說明了關於函數返回值的信息。看看下面的:

const getFullName = ({firstName, lastName}) => {
  const fullName = firstName + ' ' + lastName;
  return fullName;
};

對比

const getFullName = ({firstName, lastName}) => (
  firstName + ' ' + lastName
);

另外一個開發者一般用來減小變量名的作法是,利用函數組合以及point-free-style

Point-free-style是一種定義函數方式,定義成一種與參數無關的合成運算。實現point-free風格經常使用的方式包括函數科裏化以及函數組合。

讓咱們來看一個函數科裏化的例子:

const add2 = a => b => a + b;
// Now we can define a point-free inc()
// that adds 1 to any number.
const inc = add2(1);
inc(3); // 4

看一下inc()函數的定義方式。注意,它並未使用function關鍵字,或者=>語句。add2也沒有列出一系列的參數,由於該函數不在其內部處理一系列的參數,相反,它返回了一個知道如何處理參數的新函數。

函數組合是將一個函數的輸出做爲另外一函數的輸入的過程。 也許你沒有意識到,你一直在使用函數組合。鏈式調用的代碼基本都是這個模式,好比數組操做時使用的.map(),Promise 操做時的promise.then()。函數組合在函數式語言中也被稱之爲高階函數,其基本形式爲:f(g(x))。

當兩個函數組合時,無須建立一個變量來保存兩個函數運行時的中間值。咱們來看看函數組合是怎麼減小代碼的:

const g = n => n + 1;
const f = n => n * 2;
// 須要操做參數、而且存儲中間結果
const incThenDoublePoints = n => {
  const incremented = g(n);
  return f(incremented);
};
incThenDoublePoints(20); // 42

// compose2 - 接受兩個函數做爲參數,直接返回組合
const compose2 = (f, g) => x => f(g(x));
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

你能夠利用函子(functor)來作一樣的事情。在函子中把參數封裝成可遍歷的數組。讓咱們利用函子來寫另外一個版本的compose2

const compose2 = (f, g) => x => [x].map(g).map(f).pop();
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

當每次使用promise鏈時,你就是在作這樣的事情。

幾乎每個函數式編程類庫都提供至少兩種函數組合方法:從右到左依次運行的compose();從左到右依次運行的pipe()

Lodash中的compose()以及flow()分別對應這兩個方法。下面是使用pipe的例子:

import pipe from 'lodash/fp/flow';
pipe(g, f)(20); // 42

下面的代碼也作着一樣的事情,但代碼量並未增長太多:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
pipe(g, f)(20); // 42

若是函數組合這個名詞聽起來很陌生,你不知道如何使用它,請仔細想想:

軟件開發的本質是組合,咱們經過組合較小的模塊,方法以及數據結構來構建應用程序。

不難推論,工程師理解函數和對象組合這一編程技巧就如同搞裝修須要理解鑽孔機以及氣槍同樣重要。

當你利用「命令式」代碼將功能以及中間變量拼湊在一塊兒時,就像瘋狂使用膠帶和膠水將這些部分胡亂粘貼起來同樣,而函數組合看上去更流暢。

記住:

  • 用更少的代碼。

  • 用更少的變量。

3. 使用主動語態

主動語態比被動語態更直接,跟有力量,儘可能多直接命名事物:

  • myFunction.wasCalled()優於myFunction.hasBeenCalled()

  • createUser優於User.create()

  • notify()優於Notifier.doNotification()

命名布爾返回值時最好直接反應其輸出的類型:

  • isActive(user)優於getActiveStatus(user)

  • isFirstRun = false;優於firstRun = false;

函數名採用動詞形式:

  • increment()優於plusOne()

  • unzip()優於filesFromZip()

  • filter(fn, array)優於matchingItemsFromArray(fn, array)

事件處理

事件處理以及生命週期函數因爲是限定符,比較特殊,就不適用動詞形式這一規則;相比於「作什麼」,它們主要用來表達「何時作」。對於它們,能夠「<何時去作>,<動做>」這樣命名,朗朗上口。

  • element.onClick(handleClick)優於element.click(handleClick)

  • element.onDragStart(handleDragStart)優於component.startDrag(handleDragStart)

上面兩例的後半部分,它們讀起來更像是正在嘗試去觸發一個事件,而不是對其做出迴應。

生命週期函數

對於組件生命週期函數(組件更新以前調用的方法),考慮一下如下的命名:

  • componentWillBeUpdated(doSomething)

  • componentWillUpdate(doSomething)

  • beforeUpdate(doSomething)

第一個種咱們使用了被動語態(將要被更新而不是將要更新)。這種方式很口語化,但含義表達並無比其它兩種方式更清晰。

第二種就好多了,但生命週期函數的重點在於觸發處理事件。componentWillUpdate(handler)讀起來就好像它將當即觸發一個處理事件,但這不是咱們想要表達的。咱們想說,「在組件更新以前,觸發事件」。beforeComponentUpdate()能更清楚的表達這一想法。

進一步簡化,由於這些方法都是組件內置的。在方法名中加入component是多餘的。想想若是你直接調用這些方法時:component.componentWillUpdate()。這就好像在說,「吉米吉米在晚餐吃牛排。」你沒有必要聽到同一個對象的名字兩次。顯然,

  • component.beforeUpdate(doSomething)優於component.beforeComponentUpdate(doSomething)

函數混合是指將方法做爲屬性添加到一個對象上面,它們就像裝配流水線給傳進來的對象加上某些方法或者屬性。

我喜歡用形容詞來命名函數混合。你也能夠常用"ing"或者"able"後綴來找到有意義的形容詞。例如:

  • const duck = composeMixins(flying, quacking);

  • const box = composeMixins(iterable, mappable);

4.避免一連串結構鬆散的,不知所云的代碼

開發人員常常將一系列事件串聯在一個進程中:一組鬆散的、相關度不高的代碼被設計依次運行。從而很容易造成「意大利麪條」代碼。

這種寫法常常被重複調用,即便不是嚴格意義上的重複,也只有細微的差異。例如,界面不一樣組件之間幾乎共享相同的核心需求。 其關注點能夠分解成不一樣生命週期階段,並由單獨的函數方法進行管理。

考慮如下的代碼:

const drawUserProfile = ({ userId }) => {
  const userData = loadUserData(userId);
  const dataToDisplay = calculateDisplayData(userData);
  renderProfileData(dataToDisplay);
};

這個方法作了三件事:獲取數據,根據獲取的數據計算view的狀態,以及渲染。

在大部分現代前端應用中,這些關注點中的每個都應該考慮分拆開。經過分拆這些關注點,咱們能夠輕鬆地爲每一個問題提供不一樣的函數。

好比,咱們能夠徹底替換渲染器,它不會影響程序的其餘部分。例如,React的豐富的自定義渲染器:適用於原生iOS和Android應用程序的ReactNative,WebVR的AFrame,用於服務器端渲染的ReactDOM/Server 等等...

drawUserProfile的另外一個問題就是你不能在沒有數據的狀況下,簡單地計算要展現的數據並生成標籤。若是數據已經在其餘地方加載過了會怎麼樣,就會作不少重複和浪費的事情。

分拆關注點也使得它們更容易進行測試。我喜歡對個人應用程序進行單元測試,並在每次修改代碼時查看測試結果。可是,若是咱們將渲染代碼和數據加載代碼寫在一塊兒,我不能簡單地將一些假數據傳遞給渲染代碼進行測試。我必須從端到端測試整個組件。而這個過程當中,因爲瀏覽器加載,異步I/O請求等等會耗費時間。

上面的drawUserProfile代碼不能從單元測試測試中獲得即時反饋。而分拆功能點容許你進行單獨的單元測試,獲得測試結果。

上文已經已經分析出單獨的功能點,咱們能夠在應用程序中提供不一樣的生命週期鉤子給其調用。 當應用程序開始裝載組件時,能夠觸發數據加載。能夠根據響應視圖狀態更新來觸發計算和渲染。

這麼作的結果是軟件的職責進一步明確:每一個組件能夠複用相同的結構和生命週期鉤子,而且軟件性能更好。在後續開發中,咱們不須要重複相同的事。

5.功能相連的代碼寫在一塊兒

許多框架以及boilerplates規定了程序文件組織的方法,其中文件按照代碼類別分組。若是你正在構建一個小的計算器,獲取一個待辦事宜的app,這樣作是很好的。可是對於較大的項目,經過業務功能特性將文件分組在一塊兒是更好的方法。

按代碼類別分組:

.
├── components
│   ├── todos
│   └── user
├── reducers
│   ├── todos
│   └── user
└── tests
    ├── todos
    └── user

按業務功能特性分組:

.
├── todos
│   ├── component
│   ├── reducer
│   └── test
└── user
    ├── component
    ├── reducer
    └── test

當你經過功能特性來將文件分組,你能夠避免在文件列表上下滾動,查找編輯所須要的文件這種狀況。

6.利用判斷true值的方式來編寫代碼

要作出肯定的斷言,避免使用溫順、無色、猶豫的語句,必要時使用 not 來否認、拒絕。典型的

  • isFlying優於isNotFlying

  • late優於notOneTime

if語句

if (err) return reject(err);

// do something

優於

if (!err) {
  // ... do something
} else {
  return reject(err);
}

三元判斷語句

{
  [Symbol.iterator]: iterator ? iterator : defaultIterator
}

優於

{
  [Symbol.iterator]: (!iterator) ? defaultIterator : iterator 
}

恰當的使用否認

有時候咱們只關心一個變量是否缺失,若是經過判斷true值的方式來命名,咱們得用!操做符來否認它。這種狀況下使用 "not" 前綴和取反操做符不如使用否認語句直接。

  • if (missingValue)優於if (!hasValue)

  • if (anonymous)優於if (!user)

  • if (!isEmpty(thing))優於if (notDefined(thing))

函數調用時,避免用null以及undefined代替某一個參數

不要在函數調用時,傳入undefined或者null做爲某個參數的值。若是某些參數能夠缺失,更推薦傳入一個對象:

const createEvent = ({
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
}) => ({ title, description, timeStamp });

const birthdayParty = createEvent({
  title: 'Birthday Party',
  description: 'Best party ever!'
});

優於

const createEvent = (
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
) => ({ title, description, timeStamp });

const birthdayParty = createEvent(
  'Birthday Party',
  undefined, // This was avoidable
  'Best party ever!'  
);

不一樣的技術方案利用不一樣的代碼組織結構來實現

迄今爲止,應用程序中未解決的問題不多。最終,咱們都會一次又一次地作着一樣的事情。當這樣的場景發生時,意味着代碼重構的機會來啦。分辨出相似的部分,而後抽取出可以支持每一個不一樣部分的公共方法。這正是類庫以及框架爲咱們作的事情。

UI組件就是一個很好的例子。10 年前,使用 jQuery 寫出把界面更新、應用邏輯和數據加載混在一塊兒的代碼是再常見不過的。漸漸地,人們開始意識到咱們能夠將MVC應用到客戶端的網頁上面,隨後,人們開始將model與UI更新邏輯分拆。

最終,web應用普遍採用組件化這一方案,這使得咱們可使用JSX或HTML模板來聲明式的對組件進行建模。

最終,咱們就能用徹底相同的方式去表達全部組件的更新邏輯、生命週期,而不用再寫一堆命令式的代碼

對於熟悉組件的人,很容易看懂每一個組件的原理:利用標籤來表示UI元素,事件處理器用來觸發行爲,以及用於添加回調的生命週期鉤子函數,這些鉤子函數將在必要時運行。

當咱們對於相似的問題採用相似的模式解決時,熟悉這個解決模式的人很快就能理解代碼是用來作什麼的。

結論:代碼應該簡單而不是過於簡單化

儘管在2015,ES6已經標準化,但在2017,不少開發者仍然拒絕使用ES6特性,例如箭頭函數,隱式return,rest以及spread操做符等等。利用本身熟悉的方式編寫代碼實際上是一個幌子,這個說法是錯誤的。只有不斷嘗試,纔可以漸漸熟悉,熟悉以後,你會發現簡潔的ES6特性明顯優於ES5:與語法結構偏重的ES5相比,簡潔的es6的代碼很簡單。

代碼應該簡單,而不是過於簡單化。

簡潔的代碼有如下優點:

  • 更少的bug可能性

  • 更容易去debug

但也有以下弊端:

  • 修復bug的成本更高

  • 有可能引用更多的bug

  • 打斷了正常開發的流程

簡潔的代碼一樣:

  • 更易寫

  • 更易讀

  • 更好去維護

清楚本身的目標,不要毫無頭緒。毫無頭緒只會浪費時間以及精力。投入精力去訓練,讓本身熟悉,去學習更好的編程方式,以及更有更有活力的代碼風格。

代碼應該簡單,而不是簡單化。

相關文章
相關標籤/搜索