大多數程式設計挑戰都會教你解決難題。 LeetCode 的 30 天 JavaScript 學習計畫做了一些不同的事情:它向您展示了拼圖如何變成磚塊,準備好建立現實世界的專案。
這種區別很重要。當您解決典型的演算法問題時,您正在訓練您的思維進行抽象思考。但是,當您實現去抖1函數或建構事件發射器2時,您正在學習真正的軟體是如何運作的。
我自己在應對挑戰時發現了這一點。這種體驗不太像解決腦筋急轉彎問題,而更像考古學——揭示特定的現代 JavaScript 概念。每個部分都重點介紹 JS 的另一個現代功能。
這個學習計畫的奇特之處在於它不會教你 JavaScript。事實上,我相信您需要相當了解 JavaScript 才能從中受益。相反,它教導的是如何使用 JavaScript 來解決實際的工程問題。
考慮 Memoize3 挑戰。從表面上看,它是關於快取函數結果的。但你真正學到的是為什麼像 React 這樣的函式庫需要記憶來有效地處理元件渲染。或以 Debounce1 問題為例 - 這不僅僅是實現延遲;它還涉及延遲。它可以幫助您直接理解為什麼每個現代前端框架、電梯以及基本上任何具有互動式 UI 的系統都需要這種模式。
這種對實用模式而不是語言基礎知識的關注創造了一個有趣的限制;您需要處於以下兩個位置之一才能受益:
學習電腦科學和實踐軟體工程之間會發生一些奇怪的事情。這種轉變感覺就像學習了多年的國際象棋理論,卻發現自己在玩一種完全不同的遊戲 - 規則不斷變化,而且大多數動作都不在任何書本中。
在電腦科學中,您將學習二元樹的工作原理。在軟體工程中,您需要花費數小時來除錯 API,試圖了解回應快取不起作用的原因。從遠處看,這些世界之間的重疊可能看起來比實際上大得多。其中存在差距,這常常會讓電腦科學畢業生在開始職業生涯時感到震驚。不幸的是,大多數教育資源都無法彌補這一點。它們要么純粹是理論性的(「這是快速排序的工作原理」),要么是純粹的實用性(「這是如何部署 React 應用程式」)。
這個 JavaScript 學習計畫的有趣之處並不是它設計得特別好 - 而是它在這些世界之間創造了連結。以記憶問題為例:2623。記憶3。用計算機科學術語來說,它是關於快取計算值的。但實作它會迫使您應對 JavaScript 在物件參考、函數上下文和記憶體管理方面的特殊性。突然,
你不只是在學習演算法 - 你開始理解為什麼像 Redis 這樣的東西存在。
這種風格在整個挑戰中不斷重複。 Event Emitter2 實作不僅僅是教科書觀察者模式 - 您可以將其視為將 V8 引擎從瀏覽器中取出並圍繞它構建 Node.js 的原因,這實際上是有意義的。 Promise Pool4 解決並行執行問題,也就是資料庫需要連線限制的原因。
本學習計畫中的問題順序不是隨機的。它正在逐層建構現代 JavaScript 的心理模型。
它從閉包開始。並不是因為閉包是最簡單的概念 - 它們非常令人困惑 - 而是因為它們是 JavaScript 管理狀態的基礎。
function createCounter(init) { let count = init; return function() { return count++; } } const counter1 = createCounter(10); console.log(counter1()); // 10 console.log(counter1()); // 11 console.log(counter1()); // 12 // const counter1 = createCounter(10); // when this^ line executes: // - createCounter(10) creates a new execution context // - local variable count is initialized to 10 // - a new function is created and returned // - this returned function maintains access // to the count variable in its outer scope // - this entire bundle // (function (the inner one) + its access to count) // is what we call a closure
這種模式是 JavaScript 中所有狀態管理的種子。一旦你了解了這個計數器的工作原理,你就了解了 React 的 useState 在幕後是如何運作的。您了解為什麼模組模式會出現在 ES6 之前的 JavaScript 中。
然後計畫轉向功能轉換。這些教你函數裝飾——函數包裝其他函數以修改它們的行為。這不僅僅是一個技術技巧;這就是 Express 中間件的工作方式,React 高階組件的工作方式,
以及 TypeScript 裝飾器的工作原理。
當您遇到非同步挑戰時,您不僅僅是在學習 Promise,您還會發現 JavaScript 最初需要它們的原因。 Promise Pool4 問題不是在教你一個創新的、古怪的 JS 概念;而是在教你一個創新的、古怪的 JS 概念。它向您展示了為什麼每個資料庫引擎中都存在連接池。
以下是學習計畫各部分與現實世界軟體工程概念的粗略映射:
讓我們剖析一些問題,以展示該學習計劃的真正價值。
考慮 Memoize 挑戰。我喜歡它的原因是(我能想出的)最好的解決方案
非常簡單,就好像程式碼本身在溫和地告訴您它的作用(不過,我添加了一些註釋)。
無論如何,這並不會讓 #2623 成為一個簡單的問題。我之前需要兩次迭代才能讓它變得如此乾淨:
function createCounter(init) { let count = init; return function() { return count++; } } const counter1 = createCounter(10); console.log(counter1()); // 10 console.log(counter1()); // 11 console.log(counter1()); // 12 // const counter1 = createCounter(10); // when this^ line executes: // - createCounter(10) creates a new execution context // - local variable count is initialized to 10 // - a new function is created and returned // - this returned function maintains access // to the count variable in its outer scope // - this entire bundle // (function (the inner one) + its access to count) // is what we call a closure
想像一下你在電梯裡,有人瘋狂地重複按下「關門」按鈕。
按 按 按 按 按
按按
沒有去抖:電梯會在每按一次門時嘗試關門,導致門機構工作效率低下,甚至可能損壞。
防手震:電梯會等待,直到人停止按下一定時間(假設 0.5 秒),然後才真正嘗試關門。這樣效率就高多了。
這是另一個情況:
/** * @param {Function} fn * @return {Function} */ function memoize(fn) { // Create a Map to store our results const cache = new Map(); return function(...args) { // Create a key from the arguments const key = JSON.stringify(args); // If we've seen these arguments before, return cached result if (cache.has(key)) { return cache.get(key); } // Otherwise, calculate result and store it const result = fn.apply(this, args); cache.set(key, result); return result; } } const memoizedFn = memoize((a, b) => { console.log("computing..."); return a + b; }); console.log(memoizedFn(2, 3)); // logs "computing..." and returns 5 console.log(memoizedFn(2, 3)); // just returns 5, no calculation console.log(memoizedFn(3, 4)); // logs "computing..." and returns 7 // Explanantion: // It's as if our code had access to an external database // Cache creation // const cache = new Map(); // - this^ uses a closure to maintain the cache between function calls // - Map is perfect for key-value storage // Key creation // const key = JSON.stringify(args); // - this^ converts arguments array into a string // - [1,2] becomes "[1,2]" // - we are now able to use the arguments as a Map key // Cache check // if (cache.has(key)) { // return cache.get(key); // } // - if we've seen these arguments before, return cached result; // no need to recalculate
沒有去抖動:
// typing "javascript" 'j' -> API call 'ja' -> API call 'jav' -> API call 'java' -> API call 'javas' -> API call 'javasc' -> API call 'javascr' -> API call 'javascri' -> API call 'javascrip' -> API call 'javascript' -> API call
帶去抖動(300 毫秒延遲):
// typing "javascript" 'j' 'ja' 'jav' 'java' 'javas' 'javasc' 'javascr' 'javascri' 'javascrip' 'javascript' -> API call (only one call, 300ms after user stops typing)
這是 LeetCode #2627 的解決方案:
出了什麼問題
我希望,從這篇文章整體正面的基調來看,我對JS 30天的看法現在已經清晰了。但是沒有任何教育資源是完美的,而且,當涉及到局限性時,誠實是有價值的。這個學習計畫有幾個盲點值得檢視。
首先,學習計畫假設有一定程度的先驗知識。
如果您還不太熟悉 JavaScript,那麼有些挑戰可能會令人難以承受。這可能會讓初學者感到沮喪,因為他們可能對學習計劃有其他期望。
其次,挑戰是以孤立的方式呈現的。
這在一開始是有道理的,但隨著計劃的進展,你可能會感到失望。現實世界的問題通常需要結合多種模式和技術。研究計劃可以受益於更全面的挑戰,這些挑戰需要一起使用多個概念(例外:我們在整個計劃中都使用了閉包)。這些很適合放在獎勵部分(已經為高級用戶保留)。
最後,這群挑戰的主要弱點在於其概念解釋。來自競爭性節目,
我習慣在問題陳述中明確新術語和概念的定義。然而,LeetCode 的描述通常過於複雜——理解他們對去抖函數的解釋比實現實際的解決方案更難。
儘管有缺點,該學習計劃仍然是理解現代 JavaScript 的寶貴資源。
理解這些模式只是個開始。
真正的挑戰是識別何時以及如何將它們應用到生產代碼中。這是我在野外遇到這些模式後發現的。
首先,這些模式很少是單獨出現。真實的程式碼庫以挑戰無法探索的方式將它們組合起來。考慮一個從頭開始實現的搜尋功能。您可能會發現自己使用:
所有這些模式相互作用,創造了複雜性,沒有任何單一的挑戰能讓您做好準備。但是,在自己實現了每個部分之後,您將大致了解整個實現的運作方式。
與直覺相反,您將獲得的最有價值的技能不是實現這些模式 - 而是在其他人的程式碼中識別它們。
完成本學習計畫後,程式設計面試並不是您認識這些模式的唯一地方。
您會在開源程式碼、同事的拉取請求中發現它們,並且可能會開始在您過去的專案中註意到它們。您以前可能已經實現了它們,甚至沒有意識到。最重要的是,您將會明白它們為何存在。
最初的解謎轉變為對現代 JavaScript 生態系統的更深入理解。
這就是本學習計畫填補的空白:將理論知識與實務工程智慧連結起來。
2627。 Debounce(承諾與時間)↩
2694。事件發射器(類)↩
2623。 Memoize(函數轉換)↩
2636。承諾池(獎金)↩
以上是LeetCode 的 JavaScript 時代實際上填補了空白的詳細內容。更多資訊請關注PHP中文網其他相關文章!