原文:stackoverflow.blog/2019/09/12/…javascript
做者:Ryland Goldstein前端
翻譯:奶爸碼農java
我是Ryland Goldstein,在Binaris進行Reshuffle的產品研發人員。這是我在Stack Overflow的第二篇文章。讓咱們深刻探討一下提高JS開發能力的實用方法!node
我沒有看到不少人談論提升JavaScript的方法,這是我用來更好的編寫JS的一些實踐方法。編程
這是你能夠經過不編寫JS來改善JS的第一件事。對於初學者來講,TypeScript(TS)是JS的「已編譯」超集(在JS中運行的全部內容均可以在TS中運行)。TS在原生JS體驗的基礎上增長了一個全面的可選類型檢查系統。長期以來,整個生態系統中對TS的支持都不夠好,以致於我不建議你們使用。幸運的是,那些日子已通過去好久了,大多數框架都開箱即用地支持TS。如今咱們都在討論了什麼是TS,下面咱們來談談爲何要使用它。後端
TypeScript能夠保證類型安全數組
類型安全性能夠確保在編譯階段驗證整個代碼段中全部類型是否都以合法方式使用。換句話說,若是您建立一個數字參數的函數foo:promise
function foo(someNum: number): number {
return someNum + 5;
}
複製代碼
函數foo只接受number類型的參數安全
正確的例子
console.log(foo(2)); // 打印 "7"
錯誤的例子
console.log(foo("two")); // 不正確的TS代碼
複製代碼
除了在代碼中添加類型檢查的開銷以外,類型安全性的負面影響幾乎爲零。另外一方面,好處太大了,類型安全性提供了針對常見錯誤的額外保護,這對於像JS這樣有缺陷的語言來講是一種幸運。bash
TypeScript讓重構大型項目成爲可能
重構大型JS應用程序多是一場噩夢。重構JS的最大苦惱是因爲它不強制檢查函數參數簽名。這意味着絕對不能濫用JS函數。例如,若是我有一個函數myAPI提供給1000個不一樣的調用方使用:
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%肯定使用此功能的每一個地方(成千上萬個地方),我都正確地更新了用法。若是我錯過一個,系統就會崩潰。
若是使用TS的話:
修改前
function myAPITS(someNum: number, someString: string) { ... }
修改後
function myAPITS(someString: string, someNum: number) { ... }
複製代碼
如您所見,myAPITS函數與JavaScript對應函數進行了相同的更改。可是,此代碼不是生成有效的JavaScript,而是致使生成無效的TypeScript,由於它所使用的成千上萬個位置如今提供了錯誤的提示。 並且因爲咱們前面討論的類型安全性,這數千種狀況將阻止編譯,這樣你能夠從容進行修改。
TypeScript使得團隊架構溝通更爲順暢
正確配置TS後,若是不先定義接口和類就很難編寫代碼。這提供了一種共享簡潔,可交流的體系結構建議的方法。在TS以前,存在解決此問題的其餘解決方案,可是沒有一個解決方案能夠在不使您作額外工做的狀況下自動解決。 例如,若是我想爲後端提出新的請求類型,則可使用TS將如下內容發送給隊友。
interface BasicRequest {
body: Buffer;
headers: { [header: string]: string | string[] | undefined; };
secret: Shhh;
}
複製代碼
我必須已經編寫代碼,可是如今我能夠共享個人增量進度並得到反饋,而無需花費更多時間。我不知道TS本質上是否比JS更不容易出錯。我堅信,強制開發人員首先定義接口和API會產生更好的代碼。整體而言,TS已發展成爲比原生JS的更爲成熟且更可預測的替代方案。開發人員絕對仍然須要熟練使用原生JS,可是我最近開始的大多數新項目從一開始就是TS。
JavaScript是世界上最流行(即便不是最多)的編程語言之一。您可能但願到如今爲止,大多數人已經知道了數以億計的人使用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中添加了一個新概念Promises。使用Promise,您能夠編寫異步邏輯,同時避免之前困擾基於回調的代碼的嵌套問題。
Promises
makeHttpRequest('google.com').then(function (result) {
console.log(result);
}).catch(function (err) {
console.log('Oh boy, an error');
});
複製代碼
與回調相比,Promises的最大優勢是可讀性和可鏈式調用。儘管Promise很棒,但他們仍有一些不足之處。對於許多人來講,Promise的體驗仍然讓人聯想到回調。具體來講,開發人員正在尋求Promise模型的替代方案。爲了解決這個問題,ECMAScript委員會決定添加一種新的利用promise,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) {
// ...
}
複製代碼
因爲異步功能實際上只是精美的Promise包裝器,所以也能夠直接等待Promise。這也意味着async/await代碼和Promise代碼在功能上是等效的。所以,能夠在不感到內疚的狀況下隨意使用async/await。
let和const
對於JS的大部分存在,只有一個可變範圍限定符:var。var在處理範圍方面有一些很是獨特/有趣的規則。 var的做用域範圍不一致且使人困惑,而且致使了意外的行爲,所以致使整個JS生命週期中的錯誤。可是從ES6開始,還有var的替代方法:const和let。幾乎再也不須要使用var了,任何使用var的邏輯均可以始終轉換爲等效的const和let的代碼。
至於什麼時候使用const和let,我老是從聲明全部const開始。const的限制要嚴格得多且「固定不變」,這一般會致使更好的代碼。沒有大量的「真實場景」須要使用let,我會說我用let聲明的1/20變量,其他的都是const。
我說const是「不變的」,由於它不能以與C / C++中的const相同的方式工做。const對JavaScript運行時的含義是,對該const變量的引用永遠不會改變。這並不意味着存儲在該引用上的內容將永遠不會改變。對於原始類型(數字,布爾值等),const確實會轉換爲不變性(由於它是單個內存地址)。可是對於全部對象(類,數組,字典),const不能保證不變性。
箭頭=>函數
箭頭函數是在JS中聲明匿名函數的簡潔方式,匿名函數描述未明確命名的函數。一般,匿名函數做爲回調或事件掛鉤傳遞。
原生匿名函數
someMethod(1, function () { // has no name
console.log('called');
});
複製代碼
在大多數狀況下,這種風格沒有任何「錯誤」。原生匿名函數在做用域方面表現出「有趣」,這可能會(或已經)致使許多意外錯誤。 藉助箭頭功能,咱們沒必要再爲此擔憂。這是使用箭頭功能實現的相同代碼:
someMethod(1, () => { // has no name
console.log('called');
});
複製代碼
除了更加簡潔以外,箭頭功能還具備更多實用的做用域行爲。箭頭函數從定義它們的範圍繼承做用域。 在某些狀況下,箭頭函數功能可能更簡潔:
const added = [0, 1, 2, 3, 4].map((item) => item + 1);
console.log(added) // prints "[1, 2, 3, 4, 5]"
複製代碼
我想說清楚,這不是可變的狀況;仍然存在原生匿名函數(特別是類方法)的有效用例。話雖這麼說,但我發現,若是始終使用默認箭頭功能,則與默認使用原始匿名函數相比,您進行的調試要少得多。
展開操做符 ...
提取一個對象的鍵/值對並將它們添加爲另外一個對象的子代是一種很是常見的事情。從歷史上看,有幾種方法能夠作到這一點,可是全部這些方法都至關笨拙:
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}`;
複製代碼
對象解構
對象解構是一種從數據集合(對象,數組等)中提取值的方法,而無需遍歷數據或顯式訪問其健值:
老方法
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);
複製代碼
你還能夠在函數參數簽名中進行解構:
function animalParty({ dog, cat }) {}
const myDict = {
dog: 'woof',
cat: 'meow',
};
animalParty(myDict);
複製代碼
解構也能和數組配合使用:
[a, b] = [10, 20];
console.log(a); // prints 10
複製代碼
編寫並行化的應用程序時,您的目標是一次優化您的工做量。若是您有四個可用的核心,而您的代碼只能使用一個核心,那麼您的潛力就浪費了75%。這意味着阻塞,同步操做是並行計算的最終敵人。可是考慮到JS是單線程語言,事情就不會在多個內核上運行。那有什麼意義呢?
JS是單線程的,但不是單文件的(如學校裏的代碼)。即便不是並行的,它仍然是併發的。發送HTTP請求可能須要幾秒鐘甚至幾分鐘,所以,若是JS中止執行代碼,直到請求返回響應,該語言將沒法使用。
JavaScript經過事件循環解決了這個問題。事件循環遍歷已註冊的事件,並根據內部調度/優先級邏輯執行它們。這使發送數千個同時的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 lang for循環轉換爲自動並行代碼。從根本上講,這是一個不可能解決的問題,只有等待深度學習的改善才能解決。並行化for循環的困難源於一些有問題的模式。順序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 with index
// 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之類的結構無需按順序執行每一個迭代,而是採用全部元素並將它們做爲單獨的事件提交給用戶定義的map函數。 在大多數狀況下,各個迭代之間沒有固有的聯繫或依賴性,所以它們能夠並行運行。 這並非說您沒法使用for循環來完成同一件事。 實際上,它看起來像這樣:
const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
async function testCall() {
// do async stuff here
}
for (let i = 0; i < 10; i += 1) {
testCall();
}
複製代碼
如您所見,for循環並不能阻止我以正確的方式進行操做,可是它確定也不會使其變得更容易。 與map版本比較:
const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
items.map(async (item) => {
// do async stuff here
});
複製代碼
如您所見,map就能夠了。 若是要在全部單個異步操做完成以前進行阻塞,則map的優點將變得更加明顯。 使用for循環代碼,您將須要本身管理一個數組。 這是map版本:
const items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const allResults = await Promise.all(items.map(async (item) => {
// do async stuff here
}));
複製代碼
真的很容易,在許多狀況下,與map或forEach相比,for循環的性能相同(或可能更高)。 我仍然認爲,如今丟失幾個週期值得使用定義良好的API的優勢。 這樣,未來對該數據訪問模式實現的任何改進都會使您的代碼受益。 for循環過於籠統,沒法對同一模式進行有意義的優化。
在map和forEach以外還有其餘有效的異步選項,例如for-await-of.
沒有一致風格(外觀)的代碼很難閱讀和理解。所以,以任何語言編寫高端代碼的一個關鍵方面是保持一致且明智的風格。因爲JS生態系統的廣度,對於lint風格和style細節有不少選擇。我要強調的是,與使用特製的lint/style相比,使用lint並強制使用一種style(其中的任何一種)更爲重要。歸根結底,沒有人會徹底按照個人意願來編寫代碼,所以對此進行優化是不切實際的目標。
我看到不少人問他們應該使用eslint仍是prettier。對我來講,它們的用途很是不一樣,所以應結合使用。ESlint大部分時間都是傳統的linter。它將肯定與style無關,而與正確性有關的代碼問題。例如,我將eslint與Airbnb規則結合使用。使用該配置,如下代碼將強制linter失敗:
var fooVar = 3; // airbnb rules forebid "var"
複製代碼
很明顯,eslint如何爲您的開發週期增長價值。 從本質上講,它能夠確保您遵循關於什麼是好習慣的準則。 所以,lint在本質上是強制執行的。 與全部意見同樣,將其與鹽一同食用。 Linter多是錯誤的。
Prettier是代碼格式化程序。 它較少關注正確性,而更擔憂均勻性和一致性。 Prettier不會抱怨使用var,可是它將自動對齊代碼中的全部方括號。 在個人我的開發過程當中,在將代碼推送到Git以前,我老是使用Prettier來美化代碼格式。 在許多狀況下,讓Prettier在每次對存儲庫的每次提交時自動運行甚至是有意義的。 這樣能夠確保全部進入源代碼管理的代碼都具備一致的樣式和結構。
編寫測試是改善您編寫的JS代碼的一種間接但很是有效的方法。 我建議你須要適應使用各類各樣的測試工具。 您的測試需求會有所不一樣,而且沒有一種工具能夠處理全部問題。 JS生態系統中有大量完善的測試工具,所以選擇工具主要取決於我的喜愛。與往常同樣,請本身考慮。
Test Driver – Ava
Test drivers是一種簡單的框架,它能夠提供高級別的結構和實用性。 它們一般與其餘特定的測試工具結合使用,具體取決於您的測試需求。
Ava是表達力和簡潔性的完美平衡。 Ava的並行,隔離的體系結構是我大部分愛的源泉。 運行速度更快的測試可節省開發人員時間和公司資金。 Ava擁有大量不錯的功能,例如內置斷言,同時設法將其保持在最小限度。
其餘選項: Jest, Mocha, Jasmine
Mocks – Nock
HTTP模擬是僞造http請求過程的一部分的過程,所以測試人員能夠注入自定義邏輯來模擬服務器行爲。
Http模擬多是一個真正的痛苦,可是Nock減輕了痛苦。 Nock直接覆蓋nodejs的內置請求,並攔截傳出的http請求。這又使您能夠徹底控制響應。
其餘選項: 我也不知道其餘 🙁
測試自動化 – Selenium
我對推薦Selenium感到喜憂參半。 因爲它是網絡自動化的最受歡迎的選項,所以它擁有龐大的社區和在線資源集。 不幸的是,學習曲線至關陡峭,而且它依賴於許多實際使用的外部庫。 話雖如此,它是惟一真正的免費選項,所以,除非您要進行企業級的網絡自動化,不然Selenium會是一個好選擇。
其餘選項: Cypress, PhantomJS
與大多數事情同樣,編寫更好的JavaScript是一個持續的過程。 代碼始終能夠變得更加簡潔,一直在添加新功能,而且永遠沒有足夠的測試。 看起來彷佛勢不可擋,可是因爲有不少潛在的方面須要改進,所以您能夠按照本身的步調真正進步。 一次接着一步,在不知不覺中,您將成爲JavaScript的王者。
該博客文章最初出如今Ryland的我的網站和Dev.to上。 在這兩個站點上均可以找到他的更多著做。 若是您想爲Stack Overflow博客撰寫文章,請發送電子郵件至pitches@stackoverflow.com。
『奶爸碼農』從事互聯網研發工做10+年,經歷IBM、SAP、陸金所、攜程等國內外IT公司,目前在美團負責餐飲相關大前端技術團隊,按期分享關於大前端技術、投資理財、我的成長的思考與總結。