JavaScript編碼指南

出其不意javascript

1920年,William Strunk Jr的《英文寫做指南》出版了,這本書給英語的風格定下了一個規範,並且已經沿用至今。代碼其實也可使用類似的方法加以改進。html

本文接下來的部分是一些指導方針,不是一成不變的法律。若是可以清晰解釋代碼含義,固然有不少的理由不這樣作,可是,請保持警戒和自覺。他們能通過時間的檢驗也是有理由的:由於他們一般都是對的。偏離指南應該有好的理由,並不能簡單由於突發奇想或者我的偏好就那麼作。前端

基本上寫做的基本準則的每一部分都能應用在代碼上:java

  • 讓段落成爲文章的基本結構:每一段對應一個主題。web

  • 去掉無用的單詞。 .編程

  • 使用主動語態。
  • 避免一連串鬆散的句子。api

  • 將相關的詞語放在一塊兒。數組

  • 陳述句用主動語態。promise

  • 平行的概念用平行的結構。服務器

這些均可以用在咱們的代碼風格上。

  1. 讓函數成爲代碼的基本單元。每一個函數作一件事。
  2. 去掉無用的代碼
  3. 使用主動語態
  4. 避免一連串鬆散結構的代碼
  5. 把相關的代碼放在一塊兒。
  6. 表達式和陳述語句中使用主動語態。
  7. 用並行的代碼表達並行的概念。

###

  1. 讓函數成爲代碼的基本單元。每一個函數作一件事。

軟件開發的本質就是寫做。咱們把模塊、函數、數據結構組合在一塊兒,就有了一個軟件程序。

理解如何編寫函數並如何構建它們,是軟件開發者的基本技能。

模塊是一個或多個函數或數據結構的簡單集合,數據結構是咱們如何表示程序的狀態,但在沒有應用函數,數據結構自身不會發生什麼有趣的事情。

JavaScript有三種類型的函數:

  • 交流型函數:執行I/O的函數
  • 功能型函數:一系列指令的合集
  • 映射型函數:給一些輸入,返回相應的輸出

全部有用的程序都須要I / O,而且許多程序遵循一些程序順序,但大多數函數應該像映射函數:給定一些輸入,該函數將返回一些相應的輸出。

一個函數作一件事:若是你的函數是I/O敏感,那麼就不要把I/O和映射(計算)混雜在一塊兒。若是你的函數是爲了映射,那麼就不要加入I/O。功能性的函數就違背了這條準則。功能性的函數還違背了另外一條準則:避免把鬆散的句子寫在一塊兒。

理想的函數應該是一個簡單的,肯定的,純粹功能函數。

  • 給定相同的輸入,返回相同的輸出
  • 沒有反作用

參見《什麼是純粹的函數》

2. 去掉無用代碼

剛健的文字是簡練的。一句話應該不包含無用的詞語,一段話沒有無用的句子,正如做畫不該該有多餘的線條,一個機器沒有多餘的零件。這就要求做者儘可能用短句子,避免羅列全部細節,在大綱裏就列出主題,而不是什麼都說。William Strunk,Jr.,《英文寫做指南》

簡練的代碼在軟件中也很重要,這是由於更多的代碼讓bug有了藏匿的空間。更少的代碼=更少的含有bug的空間=更少bug。

簡練的代碼更清晰,是由於它有更高的信噪比:讀者能夠減小對的語法理解更多的瞭解它的意義。更少的代碼=更少的語法噪音=更多信息的傳遞。

借用《英文寫做指南》的一個詞:簡練的代碼更有力

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

上面一段代碼能夠簡化爲:

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

對於熟悉箭頭函數(ES 2015年加入的新特性)的人來講,這段代碼可讀性加強了。它去掉了多餘的語法:括號,function關鍵詞,以及return返回值語句。

第一個版本包含了沒必要要的語法。對於熟悉箭頭語法的人來講,括號,function關鍵詞,和return語句都沒有任何意義。它們存在只是由於還有不少人對ES6的新特性不熟悉。

ES6從2015年就是語言標準了。你應該熟悉它了

去掉無用的變量

有時候咱們傾向給一些實際不須要命名的變量命名。緣由是人腦在可用的容量內只能存儲有限的資源,而且每一個變量都必須做爲離散量子存儲,佔據了咱們可用的很少的記憶空間。

由於這個緣由,有經驗的開發者都傾向減小不須要的變量命名。

好比,在大多數狀況下,你應該去掉變量,只給建立一個返回值的變量。函數名應該可以提供足夠多的信息以顯示它的返回值。看下面的例子:

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

以及:

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

開發者經常用來減小變量的另外一個作法是:利用函數組合以及Point-free 的風格。

Point-free 風格是指:定義函數時無需引用對其操做的參數。經常使用的point-free風格方式主要是curry和函數組合。

看一個使用curry的例子:

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關鍵詞,或者=>語法。沒有參數列表,由於這個函數內部並無使用參數列表。相反的,它返回的是如何處理參數的一個函數。

下面咱們看一下使用函數組合的例子。函數組合是把一個函數結果應用到另外一個函數的處理流程。你可能沒有意識到,你其實一直都在用函數組合。當你調用.map()或者promise.then()函數的時候,你就在使用它了。例如,它的大部分時候的基本形態,其實都像這樣:f(g(x)).在代數中,這樣的組合被寫成:f ∘ g, 被稱做「g後f」或者「f組合g」。

當你把兩個函數組合在一塊兒時,你就去掉了須要存儲的中間返回值的變量。咱們看一下下面這個能夠更簡單的代碼:

const g = n => n + 1;
const f = n => n * 2;

// With points:
const incThenDoublePoints = n => {
  const incremented = g(n);
  return f(incremented);
};

incThenDoublePoints(20); // 42

// compose2 - Take two functions and return their composition
const compose2 = (f, g) => x => f(g(x));

// Point-free:
const incThenDoublePointFree = compose2(f, g);

incThenDoublePointFree(20); // 42

使用仿函數也能實現相似的效果。使用仿函數也能實現相似的效果。下面這段代碼就是使用仿函數的一個例子:

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()。當我在Lodash裏使用它們時,通常都這樣引入:

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.使用主動語態

主動語態比被動語態更加直接、有力。 -- William Strunk,Jr. 《英文寫做指南》

命名越直接越好。

  • 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)
  • component.onDragStart(handleDragStart)優於component.startDrag(handleDragStart)。

這個例子裏兩種命名方法的第二種,看上去更像是咱們嘗試觸發一件事,而不是對這個事件做出響應。

生命週期函數

假設有一個組件,有這樣一個生命週期函數,在它更新以前要調用一個事件處理的函數,有如下幾種命名方式:

  • componentWillBeUpdated(doSomething)
  • componentWillUpdate(doSomething)
  • componentWillUpdate(doSomething)

第一種命名使用被動語態。這種方式有點繞口,不如其餘方式直觀。

第二種方式稍好,可是給人的意思是這個生命週期方法要調用一個函數。componentWillUpdate(handler)讀起來就像是這個組件要更新一個事件處理程序,這就偏離了本意。咱們的原意是:"在組件更新前,調用事件處理"beforeComponentUpdate()這樣命名更爲恰當清晰。

還能更精簡。既然這些都是方法,那麼主語(也就是組件自己)其實已經肯定了。調用這個方法時再帶有主語就重複了。想象一下看到這段代碼時,你會看到component.componentWillUpdate()。這就像是在說「吉米,吉米中午要吃牛排」。你其實不須要聽到重複的名字。

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

Functional mixins 是把屬性和方法添加到Object對象上的一種方法。函數一個接一個的組合添加在一塊兒,就像是管道流同樣,或者像組裝線同樣。每一個functional mixin的函數都有一個instance做爲輸入,把一些額外的東西附加上去,而後再傳遞給下一個函數,就像組裝流水線同樣。

我傾向用形容詞命名mixin 函數。你也可使用「ing」或者「able」之類的後綴來表示形容詞的含義。例如:

  • const duck = composeMixins(flying,quacking);
  • const box = composeMixins(iterable,mappable);

避免一連串鬆散的語句

一連串的句子很快就會無聊冗長了。William Strunk,Jr.,《英文寫做指南》

開發者其實經常講一連串的事件鏈接成一整個處理過程:一系列鬆散的語句原本就爲了一個接一個而設計存在的。但過分使用這樣的流程會致使代碼像意大利麪同樣錯綜複雜。

這種序列經常被重複,儘管會有些許的不一樣,有時還會出乎意料的偏離正規。例如,一個用戶界面可能會和另外的用戶界面共享了一樣的組件代碼。這樣的代價就是代碼可能被分到不一樣的生命週期裏而且一個組件可能由多個不一樣的代碼塊進行管理。

參考下面這個例子:

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

這段代碼作了三件事:加載數據,計算相關狀態,而後渲染內容。

在現代的前端應用框架中,這三件事是相互分離的。經過分離,每件事均可以獲得比較好的組合或者擴展。

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

另外一個問題是你沒法簡單的計算要顯示的數據而且若是沒有第一次加載數據就沒法生成顯示頁面。假如你已經加載了數據呢?那麼你的計算邏輯就在接下來的調用中變的多餘了。

分離也使得各個部件獨立可測。我喜歡給本身的應用加不少單元測試,而且把測試結果顯示出來,這樣我有任何改動的時候都能看到。可是,若是我要嘗試測試加載數據並渲染的功能,那我就不能只用一些假數據測試渲染部分。正在保存……

我沒法經過單元測試馬上得到結果。函數分離卻可讓咱們可以進行獨立的測試。

這個例子就已經說明,分離函數可讓咱們可以參與到應用的不一樣生命週期中去。能夠在應用加載組件後,觸發數據的加載功能。計算和渲染能夠在視圖發生變化的時候進行。

這樣的結果就是更清楚地描述了軟件的責任:能夠重用組件相同的結構以及生命週期的回調函數,性能也更好;在後面工做流程中,咱們也節省了沒必要要的勞動。

5.把相關的代碼放在一塊兒。

不少框架或者樣板程序都預設了一種程序的組織方法,那就是按照文件類型劃分。若是你作一個小的計算器或者To Do的應用,這樣作沒問題;可是若是是大型項目,更好的辦法是按功能對文件進行分組。

下面以一個To Do 應用爲例,有兩種文件組織結構。

按照文件類型分類

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

按照文件功能分類

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

按照功能組織文件,能夠有效避免在文件夾視圖中不斷的上下滾動,直接去到功能文件夾就能夠找到要編輯的文件了。

把文件按照功能進行組織。

6.陳述句和表達式使用主動語態。

「作出明確的斷言。避免無聊、不出彩、猶豫、不可置否的語氣。使用「not」時應該表達否認或者對立面的意思,而不要用來做爲逃避的手段。」William Strunk,Jr., 《英文寫做指南》。

  • isFlying優於isNotFlying
  • late優於notOnTime

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
}

儘可能選擇語氣強烈的否認句

有時候咱們只關係一個變量是否缺失,所以使用主動語法會讓咱們被迫加上一個!。在這些狀況下,不如使用語氣強烈的否認句式。「not」這個詞和!的語氣相對較弱。

  • if (missingValue)優於if (!hasValue)
  • if (anonymous)優於if (!user)
  • if (isEmpty(thing))優於if (notDefined(thing))

函數調用時避免使用null和undefined參數類型

不要使用undefined或者null的參數做爲函數的可選參數。儘可能使用可選的Object作參數。儘可能使用可選的Object作參數。

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

// later...

const birthdayParty = createEvent({
  title = 'Birthday Party',
  timeStamp = birthDay
});

優於

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

// later...

const birthdayParty = createEvent(
  'Birthday Party',
  undefined, // This was avoidable
  birthDay
);

使用平行結構

平行結構須要儘量類似的結構表達語義。格式上的形似使得讀者可以理解不一樣語句的意義也是類似的。- William Strunk,Jr., 《英文寫做指南》

實際應用中,還有一些額外的問題沒有解決。咱們可能會重複的作同一件事情。這樣的狀況出現時,就有了抽象的空間。把相同的部分找出來,並抽象成能夠在不一樣地方同時使用的公共部分。這其實就是不少框架或者功能庫作的事情。

以UI控件爲例來講。十幾年之前,使用jQuery寫出把組件、邏輯應用、網絡I/O混雜在一塊兒的代碼還還很常見。而後人們開始意識到,咱們能夠在web應用裏也使用MVC框架,因而人們逐漸開始把模型從UI更新的邏輯中分離出來。

最終的結構是:web應用使用了組件化模型的方法,這讓咱們能夠用JSX或者HTML模板來構建咱們的UI組件。

這就讓咱們可以經過相同的方式去控制不一樣組件的更新,而無需對每個組件的更新寫重複的代碼。

熟悉組件化的人能夠輕易的看到每一個組件的工做原理:有一些代碼是表示UI元素的聲明性標記,也有一些用於事件處理程序和用在生命週期上的回調函數,這些回調函數在須要的時候會被執行。

當咱們爲類似的問題找到一種模式後,任何熟悉這個模式的人都能很快的理解這樣的代碼。

結論:代碼要簡潔,但不是簡單化。

剛健的文字是簡練的。一句話應該不包含無用的詞語,一段話沒有無用的句子,正如做畫不該該有多餘的線條,一個機器沒有多餘的零件。這就要求做者儘可能用短句子,避免羅列全部細節,在大綱裏就列出主題,而不是什麼都說。-William Strunk,Jr.,《英文寫做指南》

ES6在2015年是標準化的,但在2017年,許多開發人員避免了簡潔的箭頭功能,隱式回報,休息和傳播操做等的功能。人們以編寫更容易閱讀的代碼爲藉口,但只是由於人們更熟悉舊的模式而已。這是個巨大的錯誤。熟悉來自於實踐,熟悉ES6中的簡潔功能明顯優於ES5的緣由顯而易見:相比厚重的語法功能的代碼,這樣的代碼更簡潔。

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

簡潔的代碼就是:

  • 更少的bug
  • 更加便於調試

bug一般是這樣的:

  • 修理起來耗時耗力
  • 可能引入更多的bug
  • 打亂正常的工做流程

因此簡潔的代碼應該要:

  • 易寫
  • 易讀
  • 易維護

讓開發者學會並使用新技術好比curry實際上是值得的。這樣作也是在讓讀者們熟悉新知識。若是咱們仍是依然用原來的作法,這也是對閱讀代碼人的不尊重,就好像在用成年人在和嬰兒講話時使用孩子的口吻同樣。

咱們能夠假設讀者不理解這段代碼的實現,但請不要假設閱讀代碼的人都很笨,或者假設他們連這門語言都不懂。

代碼應該簡潔,而但不要掉價。掉價纔是一種浪費和侮辱。要在實踐中練習,投入精力去熟悉、學習一種新的編程語法、一種更有活力的風格。

代碼應該簡潔,而非簡單化。

 

相關文章
相關標籤/搜索