前言
畢業到入職騰訊已經差很少一年的時光了,接觸了不少項目,也積累了不少實踐經驗,在處理問題的方式方法上有很大的提高。隨着時間的增長,越發發現基礎知識的重要性,不少開發過程當中遇到的問題都是由最基礎的知識點遺忘形成,基礎不牢,地動山搖。因此,就再次迴歸基礎知識,從新學習JavaScript相關內容,加深對JavaScript語言本質的理解。日知其所亡,身爲有追求的程序員,理應不斷學習,不斷拓展本身的知識邊界。本系列文章是在此階段產生的積累,以記錄下以往沒有關注的核心知識點,供後續查閱之用。
2017
04/10
事實上,程序中如今運行的部分和未來運行的部分之間的關係就是異步編程的核心。 到底何時控制檯 I/O 會延遲,甚至是否可以被觀察到,這都是遊移不定的。若是在調 試的過程當中遇到對象在 console.log(..) 語句以後被修改,可你卻看到了意料以外的結果, 要意識到這多是這種 I/O 的異步化形成的。若是遇到這種少見的狀況,最好的選擇是在 JavaScript 調試器中使用斷點, 而不要依賴控制檯輸出。次優的方案是把對象序列化到一個字符串中,以強制執行一次「快照」,好比經過 JSON.stringify(..)。
04/11
多線程編程是很是複雜的。由於若是不經過特殊的步驟來防止這種中斷和交錯運行的話,可能會獲得出乎意料的、不肯定的行爲,一般這很讓人頭疼。
JavaScript 從不跨線程共享數據。
var res = []; // response(..)從Ajax調用中取得結果數組
function response(data) {
// 一次處理1000個
var chunk = data.splice(0, 1000);
// 添加到已有的res組
res = res.concat(
// 建立一個新的數組把chunk中全部值加倍
chunk.map(function(val) {
return val * 2;
}));
// 還有剩下的須要處理嗎?
if (data.length > 0) {
// 異步調度下一次批處理
setTimeout(function() {
response(data);
}, 0);
}
}
// ajax(..)是某個庫中提供的某個Ajax函數
ajax("http://some.url.1", response);
ajax("http://some.url.2", response);
咱們把數據集合放在最多包含 1000 條項目的塊中。這樣,咱們就確保了「進程」運行時 間會很短,即便這意味着須要更多的後續「進程」,由於事件循環隊列的交替運行會提升 站點 /App 的響應(性能)。
05/10
讓咱們來簡單總結一下使鏈式流程控制可行的 Promise 固有特性。
- 調用 Promise 的 then(..) 會自動建立一個新的 Promise 從調用返回。
- 在完成或拒絕處理函數內部,若是返回一個值或拋出一個異常,新返回的(可連接的) Promise 就相應地決議。
- 若是完成或拒絕處理函數返回一個 Promise,它將會被展開,這樣一來,無論它的決議 值是什麼,都會成爲當前 then(..) 返回的連接 Promise 的決議值。
05/11
Promise 侷限性 :
順序錯誤處理 : Promise 的設計侷限性(具體來講,就 是它們連接的方式)形成了一個讓人很容易中招的陷阱,即 Promise 鏈中的錯誤很容易被 無心中默默忽略掉。 關於 Promise 錯誤,還有其餘須要考慮的地方。因爲一個 Promise 鏈僅僅是鏈接到一塊兒的成員 Promise,沒有把整個鏈標識爲一個個體的實體,這意味着沒有外部方法能夠用於觀 察可能發生的錯誤。 遺憾的是,不少時候並無爲 Promise 鏈序列的中間步驟保留的引用。所以,沒有這樣的引用,你就沒法關聯錯誤處理函數來可靠地檢查錯誤。
單一值 :根據定義,Promise 只能有一個完成值或一個拒絕理由。在簡單的例子中,這不是什麼問題,可是在更復雜的場景中,你可能就會發現這是一種侷限了。 通常的建議是構造一個值封裝(好比一個對象或數組)來保持這樣的多個信息。這個解決 方案能夠起做用,但要在 Promise 鏈中的每一步都進行封裝和解封,就十分醜陋和笨重了。
單決議:Promise 最本質的一個特徵是:Promise 只能被決議一次(完成或拒絕)。在許多異步狀況中,你只會獲取一個值一次,因此這能夠工做良好。 可是,還有不少異步的狀況適合另外一種模式——一種相似於事件和 / 或數據流的模式。
慣性:Promise 提供了一種不一樣的範式,所以,編碼方式的改變程度從某處的個別差別到某種情 況下的大相徑庭都有可能。你須要刻意的改變,由於 Promise 不會從目前的編碼方式中自 然而然地衍生出來。
// polyfill安全的guard檢查
if (!Promise.wrap) {
Promise.wrap = function(fn) {
return function() {
var args = [].slice.call(arguments);
return new Promise(function(resolve, reject) {
fn.apply(null, args.concat(function(err, v) {
if (err) {
reject(err);
} else {
resolve(v);
}
}));
});
};
};
}
沒法取消的 Promise:一旦建立了一個 Promise 併爲其註冊了完成和 / 或拒絕處理函數,若是出現某種狀況使得 這個任務懸而未決的話,你也沒有辦法從外部中止它的進程。
Promise 性能:更多的工做,更多的保護。這些意味着 Promise 與不可信任的裸回調相比會更慢一些。這是顯而易見的,也很容易理解。
05/12
Promise 很是好,請使用。它們解決了咱們因只用回調的代碼而備受困擾的控制反轉問題。 它們並無擯棄回調,只是把回調的安排轉交給了一個位於咱們和其餘工具之間的可信任的中介機制。 Promise 鏈也開始提供(儘管並不完美)以順序的方式表達異步流的一個更好的方法,這有助於咱們的大腦更好地計劃和維護異步 JavaScript 代碼。
05/14
並無向第一個 next() 調用發送值,這是有意爲之。只有暫停的 yield 才能接受這樣一個經過 next(..) 傳遞的值,而在生成器的起始處咱們調用 第一個 next() 時,尚未暫停的 yield 來接受這樣一個值。規範和全部兼容瀏覽器都會默默丟棄傳遞給第一個 next() 的任何東西。傳值過去仍然不 是一個好思路,由於你建立了沉默的無效代碼,這會讓人迷惑。所以,啓動 生成器時必定要用不帶參數的 next()。
若是你的生成器中沒有 return 的話——在生成器中和在普通函數中同樣,return 固然不是必需的——總有一個假定的 / 隱式的 return;(也就是 return undefined;)。
05/15
若是你只是想要迭代一個對象的全部屬性的話(不須要保證特定的順序),能夠經過 Object.keys(..) 返回一個 array,相似於 for (var k of Object. keys(obj)) { .. 這樣使用。這樣在一個對象的鍵值上使用 for..of 循環與 for..in 循環相似,除了 Object.keys(..) 並不包含來自於 [[Prototype]] 鏈上的屬性,而 for..in 則包含。
一般在實際的 JavaScript 程序中使用 while..true 循環是很是糟糕的主意,至 少若是其中沒有 break 或 return 的話是這樣,由於它有可能會同步地無限循 環,並阻塞和鎖住瀏覽器 UI。可是,若是在生成器中有 yield 的話,使用這 樣的循環就徹底沒有問題。由於生成器會在每次迭代中暫停,經過 yield 返 回到主程序或事件循環隊列中。簡單地說就是:「生成器把 while..true 帶回 了 JavaScript 編程的世界!」
05/16
生成器 yield 暫停的特性意味着咱們不只可以從異步函數調用獲得看似同步的返回值,還 能夠同步捕獲來自這些異步函數調用的錯誤!
ES6 中最完美的世界就是生成器(看似同步的異步代碼)和 Promise(可信任可組合)的結合。
得到 Promise 和生成器最大效用的最天然的方法就 是 yield 出來一個 Promise,而後經過這個 Promise 來控制生成器的迭代器。
05/17
生成器是 ES6 的一個新的函數類型,它並不像普通函數那樣老是運行到結束。取而代之的是,生成器能夠在運行當中(徹底保持其狀態)暫停,而且未來再從暫停的地方恢復運行。 這種交替的暫停和恢復是合做性的而不是搶佔式的,這意味着生成器具備獨一無二的能力來暫停自身,這是經過關鍵字 yield 實現的。不過,只有控制生成器的迭代器具備恢復生 成器的能力(經過 next(..))。
yield/next(..) 這一對不僅是一種控制機制,實際上也是一種雙向消息傳遞機制。yield .. 表達式本質上是暫停下來等待某個值,接下來的 next(..) 調用會向被暫停的 yield 表達式傳回 一個值(或者是隱式的 undefined)。 在異步控制流程方面,生成器的關鍵優勢是:生成器內部的代碼是以天然的同步 / 順序方式表達任務的一系列步驟。其技巧在於,咱們把可能的異步隱藏在了關鍵字 yield 的後面, 把異步移動到控制生成器的迭代器的代碼部分。 換句話說,生成器爲異步代碼保持了順序、同步、阻塞的代碼模式,這使得大腦能夠更自 然地追蹤代碼,解決了基於回調的異步的兩個關鍵缺陷之一。
05/18
在 Worker 內部是沒法訪問主程序的任何資源的。這意味着你不能訪問它的任何全局變量, 也不能訪問頁面的 DOM 或者其餘資源。記住,這是一個徹底獨立的線程。
Web Worker 一般應用於哪些方面呢?
- 處理密集型數學計算
- 大數據集排序
- 數據處理(壓縮、音頻分析、圖像處理等)
- 高流量網絡通訊
Transferable 對象(http:// updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast)。這時發生的是對象所 有權的轉移,數據自己並無移動。一旦你把對象傳遞到一個 Worker 中,在原來的位置 上,它就變爲空的或者是不可訪問的,這樣就消除了多線程編程做用域共享帶來的混亂。 固然,全部權傳遞是能夠雙向進行的。
異步編碼模式使咱們可以編寫更高效的代碼,通 常可以帶來很是大的改進。可是,異步特性只能讓你走這麼遠,由於它本質上仍是綁定在 一個單事件循環線程上。
SIMD 打算把 CPU 級的並行數學運算映射到 JavaScript API,以得到高性能的數據並行運 算,好比在大數據集上的數字處理。
05/19
Benchmark.js :任何有意義且可靠的性能測試都應該基於統計學上合理的實踐。
對於微小運算的絕大多數測試結果,好比 ++x 對比 x++ 的迷思,像出於性能考慮應該用 X 代替 Y 這樣的結論都是不成立的。 這能夠歸結爲一點,測試不真實的代碼只能得出不真實的結論。若是有實際可能的話,你 應該測試實際的而非可有可無的代碼,測試條件與你指望的真實狀況越接近越好。只有這 樣得出的結果纔有可能接近事實。 像 ++x 對比 x++ 這樣的微觀性能測試結果爲虛假的可能性至關高,可能咱們最好就假定它們是假的。
jsPerf.com (http://jsperf.com):它使用 Benchmark.js 庫來運行統計上精確可靠的測試,並把測試結果放在一個公開 可得的 URL 上,你能夠把這個 URL 轉發給別人。
在考慮對代碼進行性能測試時,你應該習慣的第一件事情就是你所寫的代碼並不老是引擎真正運行的代碼。不要試圖和 JavaScript 引擎比誰聰明。對性能優化來講,你極可能會輸。
「沒有比臨時 hack 更持久的了」。頗有可能你如今編寫的用來繞過一些性能 bug 的代碼可能 比瀏覽器的性能問題自己存在得更長久。
咱們應該關注優化的大局,而不是擔憂這些微觀性能的細微差異。
高德納——計算訪談 6(1974 年 12 月) :程序員們浪費了大量的時間用於思考,或擔憂他們程序中非關鍵部分的速度,這 些針對效率的努力在調試和維護方面帶來了強烈的負面效果。咱們應該在,好比 說 97% 的時間裏,忘掉小處的效率:過早優化是萬惡之源。但咱們不該該錯過 關鍵的 3% 中的機會。
05/21
尾調用優化 :ES6 包含了一個性能領域的特殊要求-尾調用優化(Tail Call Optimization,TCO)。
簡單地說,尾調用就是一個出如今另外一個函數「結尾」處的函數調用。這個調用結束後就 沒有其他事情要作了(除了可能要返回結果值)。
調用一個新的函數須要額外的一塊預留內存來管理調用棧,稱爲棧幀。然而,若是支持 TCO 的引擎可以意識到一個函數調用位於尾部,這意味着外部函數基本上已經完成了,那麼在調用 函數時,它就不須要建立一個新的棧幀,而是能夠重用已有的棧幀。這樣不只速度更快,也更節省內存。
遞歸是 JavaScript 中一個紛繁複雜的主題。由於若是沒有 TCO 的話,引擎須要實現一 個隨意(還彼此不一樣!)的限制來界定遞歸棧的深度,達到了就得中止,以防止內存耗 盡。有了 TCO,尾調用的遞歸函數本質上就能夠任意運行,由於不再須要使用額外的內存! ES6 之因此要求引擎實現 TCO 而不是將其留給引擎自由決定,一個緣由是缺少 TCO 會導 致一些 JavaScript 算法由於懼怕調用棧限制而下降了經過遞歸實現的機率。