怎樣編寫更好的 JavaScript 代碼

做者:Ryland Gjavascript

翻譯:瘋狂的技術宅前端

原文:dev.to/taillogs/pr…java

未經許可嚴禁轉載node

我看到沒有多少人談論改進 JavaScript 代碼的實用方法。如下是我用來編寫更好的 JS 的一些頂級方法。git

使用TypeScript

改進你 JS 代碼要作的第一件事就是不寫 JS。TypeScript(TS)是JS的「編譯」超集(全部能在 JS 中運行的東西都能在 TS 中運行)。 TS 在 vanilla JS 體驗之上增長了一個全面的可選類型系統。很長一段時間裏,整個 JS 生態系統對 TS 的支持不足以讓我以爲應該推薦它。但值得慶幸的是,那養的日子已通過去好久了,大多數框架都支持開箱即用的 TS。假設咱們都知道 TS 是什麼,如今讓咱們來談談爲何要使用它。github

TypeScript 強制執行「類型安全」。

類型安全描述了一個過程,其中編譯器驗證在整個代碼段中以「合法」方式使用全部類型。換句話說,若是你建立一個帶有 number 類型參數的函數 foo編程

function foo(someNum: number): number {
  return someNum + 5;
}
複製代碼

只應使給 foo 函數提供 number 類型的參數:後端

good前端工程化

console.log(foo(2)); // prints "7"
複製代碼

no good數組

console.log(foo("two")); // invalid TS code
複製代碼

除了向代碼添加類型的開銷以外,使用類型安全沒有任何缺點。額外的好處太大了而不容忽視。類型安全提供額外級別的保護,以防止出現常見的錯誤或bug,這是對像 JS 這樣沒法無天的語言的祝福。

沒法無天-主演:shia lebouf

電影:沒法無天,主演 shia lebouf

Typescript 類型,能夠重構更大的程序

重構大型 JS 程序是一場真正的噩夢。重構 JS 過程當中引發痛苦的大部分緣由是它沒有強制按照函數的原型執行。這意味着 JS 函數永遠不會被「誤用」。若是我有一個由 1000 種不一樣的服務使用的函數 myAPI

function myAPI(someNum, someString) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}
複製代碼

我稍微改變了函數的原型:

function myAPI(someString, someNum) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}
複製代碼

這時我必須 100% 肯定每一個使用此函數的位置(足足有1000個)都正確地更新了用法。哪怕我漏掉一個地方,函數也可能就會失效。這與使用 TS 的狀況相同:

以前

function myAPITS(someNum: number, someString: string) { ... }
複製代碼

以後

function myAPITS(someString: string, someNum: number) { ... }
複製代碼

正如你所看到的,我對 myAPITS 函數進行了與 JavaScript 對應的相同更改。可是這個代碼不是產生有效的 JavaScript,而是致使無效的 TypeScript,由於如今使用它的 1000 個位置提供了錯誤的類型。並且因爲咱們以前討論過的「類型安全」,這 1000 個問題將會阻止編譯,而且你的函數不會失效(這很是好)。

TypeScript使團隊架構溝通更容易。

正確設置 TS 後,若是事先沒有定義好接口和類,就很難編寫代碼。這也提供了一種簡潔的分享、交流架構方案的方法。在 TS 出現以前,也存在解決這個問題的其餘方案,可是沒有一個可以真正的解決它,而且還須要你作額外的工做。例如,若是我想爲本身的後端添加一個新的 Request 類型,我可使用 TS 將如下內容發送給一個隊友。

interface BasicRequest {
  body: Buffer;
  headers: { [header: string]: string | string[] | undefined; };
  secret: Shhh;
}
複製代碼

儘管我不得不編寫一些代碼,可是如今能夠分享本身的增量進度並得到反饋,而無需投入更多時間。我不知道 TS 本質上是否能比 JS 更少出現「錯誤」,不給我強烈認爲,迫使開發人員首先定義接口和 API,從而產生更好的代碼是頗有必要的。

總的來講,TS 已經發展成爲一種成熟且更可預測的 vanilla JS替代品。確定仍然須要 vanilla JS,可是我如今的大多數新項目都是從一開始就是 TS。

使用現代功能

JavaScript 是世界上最流行的編程語言之一。你可能會認爲,有大約數百萬人使用的 JS 如今已經有 20 多歲了,但事實偏偏相反。JS 已經作了不少改變和補充(是的我知道,從技術上說是 ECMAScript),從根本上改變了開發人員的體驗。做爲近兩年纔開始編寫 JS 的人,個人優點在於沒有偏見或指望。這致使了我關於要使用哪一種語言更加務實。

async 和 await

很長一段時間裏,異步、事件驅動的回調是 JS 開發中不可避免的一部分:

傳統的回調

makeHttpRequest('google.com', function (err, result) {
  if (err) {
    console.log('Oh boy, an error');
  } else {
    console.log(result);
  }
});
複製代碼

我不打算花時間來解釋上述問題(我之前寫過此類文章)。爲了解決回調問題,JS 中增長了一個新概念 「Promise」。 Promise 容許你編寫異步邏輯,同時避免之前基於回調的代碼嵌套問題的困擾。

Promises

makeHttpRequest('google.com').then(function (result) {
  console.log(result);
}).catch(function (err) {
  console.log('Oh boy, an error');
});
複製代碼

Promise 優於回調的最大優勢是可讀性和可連接性。

雖然 Promise 很棒,但它們仍然有待改進。到如今爲止,寫 Promise 仍然感受不到「原生」。爲了解決這個問題,ECMAScript 委員會決定添加一種利用 promise,asyncawait 的新方法:

async 和 await

try {
  const result = await makeHttpRequest('google.com');
  console.log(result);
} catch (err) {
  console.log('Oh boy, an error');
}
複製代碼

須要注意的是,你要 await 的任何東西都必須被聲明爲 async

在上一個例子中須要定義 makeHttpRequest

async function makeHttpRequest(url) {
  // ...
}
複製代碼

也能夠直接 await 一個 Promise,由於 async 函數實際上只是一個花哨的 Promise 包裝器。這也意味着,async/await 代碼和 Promise 代碼在功能上是等價的。因此隨意使用 async/await 並不會讓你感到不安。

let 和 const

對於大多數 JS 只有一個變量限定符 varvar 在處理方面有一些很是獨特且有趣的規則。 var 的做用域行爲是不一致並且使人困惑的,在 JS 的整個生命週期中致使了意外行爲和錯誤。可是從 ES6 開始有了 var 的替代品:constlet。幾乎沒有必要再使用 var 了。使用 var 的任何邏輯均可以轉換爲等效的 constlet 代碼。

至於什麼時候使用 constlet,我老是優先使用 constconst 是更嚴格的限制和 「永固的」,一般會產生更好的代碼。我僅有 1/20 的變量用 let 聲明,其他的都是 const

我之因此說 const 是 「永固的」 是由於它與 C/C++ 中的 const 的工做方式不一樣。 const 對 JavaScript 運行時的意義在於對 const 變量的引用永遠不會改變。這並不意味着存儲在該引用中的內容永遠不會改變。對於原始類型(數字,布爾等),const 確實轉化爲不變性(由於它是單個內存地址)。但對於全部對象(類,數組,dicts),const 並不能保證不變性。

箭頭函數 =>

箭頭函數是在 JS 中聲明匿名函數的簡明方法。匿名函數即描述未明確命名的函數。一般匿名函數做爲回調或事件鉤子傳遞。

vanilla 匿名函數

someMethod(1, function () { // has no name
  console.log('called');
});
複製代碼

在大多數狀況下,這種風格沒有任何「錯誤」。 Vanilla 匿名函數在做用域方面表現得「有趣」,這可能致使許多意外錯誤。有了箭頭函數,咱們就沒必要再擔憂了。如下是使用箭頭函數實現的相同代碼:

匿名箭頭函數

someMethod(1, () => { // has no name
  console.log('called');
});
複製代碼

除了更簡潔以外,箭頭函數還具備更實用的做用域行爲。箭頭函數從它們定義的做用域繼承 this

在某些狀況下,箭頭函數能夠更簡潔:

const added = [0, 1, 2, 3, 4].map((item) => item + 1);
console.log(added) // prints "[1, 2, 3, 4, 5]"
複製代碼

第 1 行的箭頭函數包含一個隱式的 return 聲明。不須要具備單線箭頭功能的括號或分號。

在這裏我想說清楚,這和 var 不同,對於 vanilla 匿名函數(特別是類方法)仍有效。話雖這麼說,但若是你老是默認使用箭頭函數而不是vanilla匿名函數的話,最終你debug的時間會更少。

像以往同樣,Mozilla 文檔是最好的資源

展開操做符

提取一個對象的鍵值對,並將它們做爲另外一個對象的子對象添加,是一種很常見的狀況。有幾種方法能夠實現這一目標,但它們都很是笨重:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
const merged = Object.assign({}, obj1, obj2);
console.log(merged) // prints { dog: 'woof', cat: 'meow' }
複製代碼

這種模式很是廣泛,但也很乏味。感謝「展開操做符」,不再須要這樣了:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }
複製代碼

最重要的是,這也能夠與數組無縫協做:

const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4]
複製代碼

它可能不是最重要的 JS 功能,但它是我最喜歡的功能之一。

文字模板(字符串模板)

字符串是最多見的編程結構之一。這就是爲何它如此使人尷尬,以致於本地聲明字符串在許多語言中仍然得不到很好的支持的緣由。在很長一段時間裏,JS 都處於「糟糕的字符串」系列中。可是文字模板的添加使 JS 成爲它本身的一個類別。本地文字模板,方便地解決了編寫字符串,添加動態內容和編寫橋接多行的兩個最大問題:

const name = 'Ryland';
const helloString =
`Hello ${name}`;
複製代碼

我認爲代碼說明了一切。多麼使人讚歎。

對象解構

對象解構是一種從數據集合(對象,數組等)中提取值的方法,無需對數據進行迭代或顯的式訪問它的 key:

舊方法

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict.dog, myDict.cat);
複製代碼

解構

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

const { dog, cat } = myDict;
animalParty(dog, cat);
複製代碼

不過還有更多方式。你還能夠在函數的簽名中定義解構:

解構2

function animalParty({ dog, cat }) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict);
複製代碼

它也適用於數組:

解構3

[a, b] = [10, 20];

console.log(a); // prints 10
複製代碼

還有不少你應該使用現代功能。如下是我認爲值得推薦的:

始終假設你的系統是分佈式的

編寫並行化程序時,你的目標是優化你一次性可以完成的工做量。若是你有 4 個可用的 CPU 核心,而且你的代碼只能使用單個核心,則會浪費 75% 的算力。這意味着,阻塞、同步操做是並行計算的最終敵人。但考慮到 JS 是單線程語言,不會在多個核心上運行。那這有什麼意義呢?

儘管 JS 是單線程的,它仍然是能夠併發執行的。發送 HTTP 請求可能須要幾秒甚至幾分鐘,在這期間若是 JS 中止執行代碼,直到響應返回以前,語言將沒法使用。

JavaScript 經過事件循環解決了這個問題。事件循環,即循環註冊事件並基於內部調度或優先級邏輯去執行它們。這使得可以「同時」發送1000個 HTTP 請求或從磁盤讀取多個文件。這是一個問題,若是你想要使用相似的功能,JavaScript 只能這樣作。最簡單的例子是 for 循環:

let sum = 0;
const myArray = [1, 2, 3, 4, 5, ... 99, 100];
for (let i = 0; i < myArray.length; i += 1) {
  sum += myArray[i];
}
複製代碼

for 循環是編程中存在的最不併發的構造之一。在上一份工做中,我帶領一個團隊花了幾個月的時間嘗試將 R 語言中的 for-loops 轉換爲自動並行代碼。這基本上是一個不可能的任務,只有經過等待深度學習技術的改善才能解決。並行化 for 循環的難度來自一些有問題的模式。用 for 循環進行順序執行的狀況是比較罕見的,但它們沒法保證循環的可分離性:

let runningTotal = 0;
for (let i = 0; i < myArray.length; i += 1) {
  if (i === 50 && runningTotal > 50) {
    runningTotal = 0;
  }
  runningTotal += Math.random() + runningTotal;
}
複製代碼

若是按順序執行迭代,此代碼僅生成預期結果。若是你嘗試執行屢次迭代,則處理器可能會根據不許確的值進入錯誤地分支,從而使結果無效。若是這是 C 代碼,咱們將會進行不一樣的討論,由於使用狀況不一樣,編譯器可使用循環實現至關多的技巧。在 JavaScript 中,只有絕對必要時才應使用傳統的 for 循環。不然使用如下構造:

map

// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url) => makHttpRequest(url));
const results = await Promise.all(resultingPromises);
複製代碼

帶索引的 map

// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url, index) => makHttpRequest(url, index));
const results = await Promise.all(resultingPromises);
複製代碼

for-each

const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
// note this is non blocking
urls.forEach(async (url) => {
  try {
    await makHttpRequest(url);
  } catch (err) {
    console.log(`${err} bad practice`);
  }
});
複製代碼

下面我將解釋爲何這是對傳統 for 循環的改進:不是按順序執行每一個「迭代」,而是構造諸如 map 之類的全部元素,並將它們做爲單獨的事件提交給用戶定義的映射函數。這將直接與運行時通訊,各個「迭代」彼此之間沒有鏈接或依賴,因此可以容許它們同時運行。我認爲如今應該拋棄一些循環,應該去使用定義良好的 API。這樣對任何將來數據訪問模式實現的改進都將使你的代碼受益。 for 循環過於通用,沒法對同一模式進行有意義的優化。

map 和 forEach 以外還有其餘有效的異步選擇,例如 for-await-of。

Lint 你的代碼並強制使用一致的風格

沒有一致風格的代碼難以閱讀和理解。所以,用任何語言編寫高端代碼的一個關鍵就是具備一致和合理的風格。因爲 JS 生態系統的廣度,有許多針對 linter 和樣式細節的選項。我不能強調的是,你使用一個 linter 並強制執行同一個樣式(隨便哪一個)比你專門選擇的 linter 或風格更重要。最終沒人可以準確地編寫代碼,因此優化它是一個不切實際的目標。

有不少人問他們是否應該用 eslintprettier。對我來講,它們的目的是有很大區別的,所以應該結合使用。 Eslint 是一種傳統的 「linter」,大多數狀況下,它會識別代碼中與樣式關係不大的問題,更多的是與正確性有關。例如,我使用eslint與 AirBNB 規則。若是用了這個配置,如下代碼將會強制 linter 失敗:

var fooVar = 3; // airbnb rules forebid "var"
複製代碼

很明顯,eslint 爲你的開發週期增長價值。從本質上講,它確保你遵循關於「is」和「isn't」良好實踐的規則。所以 linters 本質上是執拗的,只要你的代碼不符合規則,linter 可能就會報錯。

Prettier 是一個代碼格式化程序。它不太關心「正確性」,更關注一致性。 Prettier 不會對使用 var 提出異議,但會自動對齊代碼中的全部括號。在個人開發過程當中,在將代碼推送到 Git 以前,老是處理得很​​漂亮。不少時候讓 Prettier 在每次提交到 repo 時自動運行是很是有意義的。這確保了進入源碼控制系統的全部代碼都有一致的樣式和結構。

測試你的代碼

編寫測試是一種間接改進你代碼但很是有效的方法。我建議你熟悉各類測試工具。你的測試需求會有所不一樣,沒有哪種工具能夠處理全部的問題。 JS 生態系統中有大量完善的測試工具,所以選擇哪一種工具主要歸結爲我的偏好。一如既往,要爲你本身考慮。

Test Driver - Ava

測試驅動 — Ava

AvaJS on Github

測試驅動只是簡單的框架,能夠提供很是高級別的結構和工具。它們一般與其餘特定測試工具結合使用,這些工​​具根據你的實際需求而有所不一樣。

Ava 是表達力和簡潔性的完美平衡。 Ava 的並行和獨立的架構是個人最愛。快速運行的測試能夠節省開發人員的時間和公司的資金。Ava 擁有許多不錯的功能,例如內置斷言等。

替代品:Jest,Mocha,Jasmine

Spies 和 Stubs — Sinon

Sinon on Githubgithub.com/sinonjs/sin…

Spies 爲咱們提供了「功能分析」,例如調用函數的次數,調用了哪些函數以及其餘有用的數據。

Sinon 是一個能夠作不少事的庫,但只有少數的事情作得超級好。具體來講,當涉及到 Spies 和 Stubs 時,sinon很是擅長。功能集豐富並且語法簡潔。這對於 Stubs 尤爲重要,由於它們爲了節省空間而只是部分存在。

替代方案:testdouble

模擬 — Nock

Nock on Githubgithub.com/nock/nock?s…

HTTP 模擬是僞造 http 請求中某些部分的過程,所以測試人員能夠注入自定義邏輯來模擬服務器行爲。

http 模擬多是一種真正的痛苦,nock 使它不那麼痛苦。 Nock 直接覆蓋 nodejs 內置的 request 並攔截傳出的 http 請求。這使你能夠徹底控制 http 響應。

替代方案:我真的不知道 :(

網絡自動化 - Selenium

Selenium on Githubgithub.com/SeleniumHQ/…

我對推薦 Selenium 有着一種複雜的態度。因爲它是 Web 自動化最受歡迎的選擇,所以它擁有龐大的社區和在線資源集。不幸的是學習曲線至關陡峭,而且它依賴許多外部庫。儘管如此,它是惟一真正的免費選項,因此除非你作一些企業級的網絡自動化,不然仍是 Selenium 最適合這個工做。

歡迎關注前端公衆號:前端先鋒,領取前端工程化實用工具包。

相關文章
相關標籤/搜索