首页 >web前端 >js教程 >LeetCode 的 JavaScript 时代实际上填补了空白

LeetCode 的 JavaScript 时代实际上填补了空白

Patricia Arquette
Patricia Arquette原创
2024-12-16 16:34:10908浏览

The Gap That LeetCode

大多数编码挑战都会教你解决难题。 LeetCode 的 30 天 JavaScript 学习计划做了一些不同的事情:它向您展示了拼图如何变成砖块,准备好构建现实世界的项目。

这种区别很重要。当您解决典型的算法问题时,您正在训练您的思维进行抽象思考。但是,当您实现去抖1函数或构建事件发射器2时,您正在学习真正的软件是如何工作的。

我自己在应对挑战时发现了这一点。这种体验不太像解决脑筋急转弯问题,而更像考古学——揭示特定的现代 JavaScript 概念。每个部分都重点介绍 JS 的另一项现代功能。

这个学习计划的奇特之处在于它不会教你 JavaScript。事实上,我相信您需要相当了解 JavaScript 才能从中受益。相反,它教的是如何使用 JavaScript 来解决实际的工程问题。

考虑 Memoize3 挑战。从表面上看,它是关于缓存函数结果的。但你真正学到的是为什么像 React 这样的库需要记忆来有效地处理组件渲染。或者以 Debounce1 问题为例 - 这不仅仅是实现延迟;它还涉及延迟。它可以帮助您直接理解为什么每个现代前端框架、电梯以及基本上任何具有交互式 UI 的系统都需要这种模式。

这种对实用模式而不是语言基础知识的关注创造了一个有趣的限制;您需要处于以下两个位置之一才能受益:

  1. 您了解 CS 基础知识(尤其是数据结构和算法)并且熟悉 JavaScript
  2. 您的计算机科学理论很强,并且之前接触过一些 JavaScript

连接计算机科学和软件工程

学习计算机科学和实践软件工程之间会发生一些奇怪的事情。这种转变感觉就像学习了多年的国际象棋理论,却发现自己在玩一种完全不同的游戏 - 规则不断变化,而且大多数动作都不在任何书本中。

在计算机科学中,您将学习二叉树的工作原理。在软件工程中,您需要花费数小时调试 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 概念。它向您展示了为什么每个数据库引擎中都存在连接池。

以下是学习计划各部分与现实世界软件工程概念的粗略映射:

  • 关闭 → 状态管理
  • 基本数组变换 → 基本技能(辅助);实际示例:数据操作
  • 函数转换 → 中间件模式
  • 承诺与时间 ->异步控制流
  • JSON ->基础技能(辅助);实例:数据序列化、API通信
  • (特别是在事件发射器的上下文中)→消息传递系统
  • 奖金(高级锁定)->可能包含在上述部分中的更难的挑战的组合; Promise Pool4 是本节中我最喜欢的一个

模式识别,而不是解决问题

让我们剖析一些问题,以展示该学习计划的真正价值。

  1. 记忆 (#2623)

考虑 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
  1. 反跳 (#2627)

想象一下你在电梯里,有人疯狂地反复按“关门”按钮。

没有去抖:电梯会在每按一次门时尝试关门,导致门机构工作效率低下,甚至可能损坏。

防抖:电梯会等待,直到人停止按下一定时间(假设 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

这将进行 10 次 API 调用。其中大多数都没用,因为用户仍在打字。

带去抖动(300 毫秒延迟):

// 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

去抖就像告诉你的代码:“等到用户停止执行某些操作 X 毫秒后再实际运行此函数。”

这是 LeetCode #2627 的解决方案:

// typing "javascript"
'j'
'ja'
'jav'
'java'
'javas'
'javasc'
'javascr'
'javascri'
'javascrip'
'javascript' -> API call (only one call, 300ms after user stops typing)

现实世界中其他常见的去抖动用例(搜索栏除外):

  • 保存草稿(等待用户停止编辑)
  • 提交按钮(防止重复提交)

出了什么问题

我希望,从这篇文章整体积极的基调来看,我对JS 30天的看法现在已经清晰了。

但是没有任何教育资源是完美的,而且,当涉及到局限性时,诚实是有价值的。这个学习计划有几个盲点值得审视。

首先,学习计划假设有一定水平的先验知识。
如果您还不太熟悉 JavaScript,那么有些挑战可能会令人难以承受。这可能会让初学者感到沮丧,因为他们可能对学习计划有其他期望。

其次,挑战是以孤立的方式呈现的。
这在一开始是有道理的,但随着计划的进展,你可能会感到失望。现实世界的问题通常需要结合多种模式和技术。研究计划可以受益于更综合的挑战,这些挑战需要一起使用多个概念(例外:我们在整个计划中都使用了闭包)。这些很适合放在奖励部分(已经为高级用户保留)。

最后,这组挑战的主要弱点在于其概念解释。来自竞争性节目,
我习惯于在问题陈述中明确新术语和概念的定义。然而,LeetCode 的描述通常过于复杂——理解他们对去抖函数的解释比实现实际的解决方案更难。

尽管有缺点,该学习计划仍然是理解现代 JavaScript 的宝贵资源。

30 天之后

理解这些模式只是一个开始。
真正的挑战是识别何时以及如何将它们应用到生产代码中。这是我在野外遇到这些模式后发现的。

首先,这些模式很少单独出现。真实的代码库以挑战无法探索的方式将它们组合起来。考虑一个从头开始实现的搜索功能。您可能会发现自己使用:

  • 输入处理的反跳
  • 结果缓存的记忆
  • 承诺 API 调用超时
  • 用于搜索状态管理的事件发射器

所有这些模式相互作用,创造了复杂性,没有任何单一的挑战能让您做好准备。但是,在自己实现了每个部分之后,您就会大致了解整个实现的运作方式。

与直觉相反,您将获得的最有价值的技能不是实现这些模式 - 而是在其他人的代码中识别它们。

最后的想法

完成本学习计划后,编程面试并不是您认识这些模式的唯一地方。

您会在开源代码、同事的拉取请求中发现它们,并且可能会开始在您过去的项目中注意到它们。您以前可能已经实现了它们,甚至没有意识到。最重要的是,您会明白它们为何存在。

最初的解谜转变为对现代 JavaScript 生态系统的更深入理解。

这就是本学习计划填补的空白:将理论知识与实践工程智慧联系起来。



  1. 2627。 Debounce(承诺与时间)↩

  2. 2694。事件发射器(类)↩

  3. 2623。 Memoize(函数转换)↩

  4. 2636。承诺池(奖金)↩

以上是LeetCode 的 JavaScript 时代实际上填补了空白的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn