<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Tsukistar - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Tsukistar - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 19 May 2026 10:03:01 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/tsukistar/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 代码整洁实践：敏捷软件开发中的代码优化指南 ]]>
                </title>
                <description>
                    <![CDATA[ 构建可扩展的软件应用程序需要编写干净的代码，这种代码简单到任何开发人员都能理解。 在这篇文章中，我将解释并演示什么是干净的代码。然后，我将分享我最喜欢的代码整洁模式，用于构建现代敏捷应用程序。 我不会使用复杂的术语。我会用简单明了的 JavaScript 示例来展示核心概念。开门见山，这就是我的风格。 让我们开始吧。 目录  1. 坏代码的成本            2. 清洁编码者 vs. 混乱编码者            3. 如果你的代码一团糟，AI 也救不了你 🗑️           ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-clean-code-handbook/</link>
                <guid isPermaLink="false">69630570d350d0045e97cbae</guid>
                
                    <category>
                        <![CDATA[ 代码质量 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Sun, 11 Jan 2026 02:21:24 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2026/01/----_20260111101926_191_113.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/the-clean-code-handbook/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The Clean Code Handbook: How to Write Better Code for Agile Software Development</a>
      </p><!--kg-card-begin: markdown--><p>构建可扩展的软件应用程序需要编写干净的代码，这种代码简单到任何开发人员都能理解。</p>
<p>在这篇文章中，我将解释并演示什么是干净的代码。然后，我将分享我最喜欢的代码整洁模式，用于构建现代敏捷应用程序。</p>
<p>我不会使用复杂的术语。我会用简单明了的 JavaScript 示例来展示核心概念。开门见山，这就是我的风格。</p>
<p>让我们开始吧。</p>
<h2 id="">目录</h2>
<ol>
<li>
<p><a href="#heading-the-cost-of-bad-code">坏代码的成本</a></p>
</li>
<li>
<p><a href="#heading-clean-coder-vs-messy-coder">清洁编码者 vs. 混乱编码者</a></p>
</li>
<li>
<p><a href="#heading-ai-cant-save-you-if-your-code-is-a-mess">如果你的代码一团糟，AI 也救不了你 🗑️</a></p>
</li>
<li>
<p><a href="#heading-12-clean-code-design-patterns-for-building-agile-applications">用于构建敏捷应用的 12 个整洁代码设计模式 ⚖️</a></p>
<ul>
<li>
<p><a href="#heading-use-names-that-mean-something">🌿 使用有意义的名称</a></p>
</li>
<li>
<p><a href="#heading-keep-functions-laser-focused-srp">🔨 保持函数的专注性 (SRP)</a></p>
</li>
<li>
<p><a href="#heading-use-comments-thoughtfully">🚪 谨慎使用注释</a></p>
</li>
<li>
<p><a href="#heading-best-practices-for-writing-good-comments">⚡ 编写优秀注释的最佳实践</a></p>
</li>
<li>
<p><a href="#heading-make-your-code-readable">🧩 让你的代码可读</a></p>
</li>
<li>
<p><a href="#heading-test-everything-you-write">🏌️ 测试你写的所有内容</a></p>
</li>
<li>
<p><a href="#heading-use-dependency-injection">💉 使用依赖注入</a></p>
</li>
<li>
<p><a href="#heading-clean-project-structures">📂 清理项目结构</a></p>
</li>
<li>
<p><a href="#heading-be-consistent-with-formatting">🤹‍♂️ 格式保持一致</a></p>
</li>
<li>
<p><a href="#heading-stop-hardcoding-values">✋ 停止硬编码值</a></p>
</li>
<li>
<p><a href="#heading-keep-functions-short">🤏 保持函数简短</a></p>
</li>
<li>
<p><a href="#heading-follow-the-boy-scout-rule">⛺ 遵循童子军原则</a></p>
</li>
<li>
<p><a href="#heading-follow-the-openclosed-principle">🏟️ 遵循开放/封闭原则</a></p>
</li>
</ul>
</li>
<li>
<p><a href="#heading-modern-best-practices-to-help-you-write-clean-code-a-summary">帮助你编写整洁代码的现代最佳实践：总结 🥷</a></p>
</li>
<li>
<p><a href="#heading-automated-tools-for-maintaining-clean-code">保持代码整洁的自动化工具 ⚓</a></p>
<ul>
<li>
<p><a href="#heading-1-static-analysis">1️⃣ 静态分析</a></p>
</li>
<li>
<p><a href="#heading-2-automated-code-formatting">2️⃣ 自动代码格式化</a></p>
</li>
<li>
<p><a href="#heading-3-continuous-integration-ci-testing">3️⃣ 持续集成 (CI) 测试</a></p>
</li>
<li>
<p><a href="#heading-4-cicd-pipelines">4️⃣ CI/CD 流水线</a></p>
</li>
</ul>
</li>
<li>
<p><a href="#heading-the-role-of-documentation-in-agile-software-development">文档在敏捷软件开发中的角色 🚣</a></p>
</li>
<li>
<p><a href="#heading-conclusion">结论 🏁</a></p>
</li>
<li>
<p><a href="#heading-frequently-asked-questions-about-clean-code">关于代码整洁的常见问题 🧯</a></p>
</li>
</ol>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xh3j6ccn1hc3euc3lfyl.png" alt="敏捷软件开发梗图" width="3125" height="1250" loading="lazy"></p>
<p>在敏捷中，变化是唯一不变的，干净的代码是你的盔甲。它使你具有适应性、敏捷性，最重要的是，它使你能够掌控局面。</p>
<p>事实是：如果你想在软件开发行业中生存，编写整洁代码绝非可有可无，而是必须掌握的能力。幸运的是，我们人类通过努力和练习终能掌握整洁代码的精髓。</p>
<h2 id="heading-the-cost-of-bad-code">坏代码的成本</h2>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wdai6npb55j71sguj6kl.png" alt="混乱代码与整洁代码成本对比图表 by shahan" width="500" height="600" loading="lazy"></p>
<p>为了解释这个堆叠条形图，在初始开发阶段，坏代码比整洁代码的变更成本<strong>稍微</strong>高一些。</p>
<p>但是当我们进入维护和重构阶段时，这个差距显著扩大，坏代码的成本几乎是整洁代码的两倍。</p>
<p>到了代码变成遗留代码时，坏代码的变更成本达到100%——现在升级它非常昂贵，而整洁代码仍然更易于管理，仅为45%。</p>
<p>目前，美国关于软件低质量成本的最新分析仍是信息与软件质量联盟（Consortium for Information and Software Quality，CISQ，网站：cisq.org）发布的2022年报告。在这份报告中，经估计，2022年软件低质量给美国经济造成了至少2.41万亿美元的损失，其中技术债务（technical debt）约占1.52万亿美元。</p>
<p>你可以<a href="https://www.it-cisq.org/the-cost-of-poor-quality-software-in-the-us-a-2022-report/">在这里阅读更多内容</a>。</p>
<p>近期的讨论继续强调技术债务对软件质量和业务绩效的显著影响。</p>
<p>例如，<a href="https://vfunction.com/blog/how-to-manage-technical-debt">2024年的一项调查</a>指出，超过50%的公司认为技术债务占其IT总预算的四分之一以上。如果不加以解决，这确实会阻碍创新。</p>
<p>如你所见，毫无疑问，在软件开发中，坏代码是一个代价高昂的问题。</p>
<h2 id="heading-clean-coder-vs-messy-coder">清洁编码者 vs. 混乱编码者</h2>
<p>这是一个展示<strong>两种</strong>编码者历程的图表：</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6ubf77uwipf4gtucw8q.png" alt="整洁代码 vs 坏代码图表" width="604" height="340" loading="lazy"></p>
<ul>
<li>
<p><strong>⚠️ 混乱编码者（红线）：</strong> 起步快但坠落得更惨。他们写的行数越多，问题也越多。</p>
</li>
<li>
<p><strong>⚡ 清洁编码者（蓝线）：</strong> 开始慢但保持一致。增长不会停止——它会加速。</p>
</li>
</ul>
<p>🫵 现在，你可以决定想走哪条线。</p>
<h2 id="heading-ai-cant-save-you-if-your-code-is-a-mess">如果你的代码一团糟，AI 也救不了你 🗑️</h2>
<p>当你陷入写代码的困境时，你可能会求助于 AI。但让我告诉你：如果你的代码一团糟，AI 也救不了你。</p>
<p>这就像是在沙子上建房子。没错，它会暂时站立，但一旦有强风或大浪，就会倒塌。</p>
<p>请记住：AI 只是一个工具。如果你不知道如何编写清晰、可扩展的应用程序，就是在为失败埋下伏笔。</p>
<p>我反复看到了这样的情况：那些熟悉五种编程语言的开发者，他们能构建应用程序、网站、软件，他们对算法和数据结构了如指掌。</p>
<p>但当面对大型项目或别人的混乱代码时，他们崩溃了。</p>
<p>他们就像一位能设计和建造自己飞机的航天工程师，但却不知道如何驾驶它们。他们在自己的代码中坠毁。</p>
<p>这曾经是我...... 从前的我。我会写上数千行代码，但却发现自己连上周写的东西都无法理解。对我来说，那是一片混乱。</p>
<p>然后我恍然大悟 —— 每个开发者都在为此苦苦挣扎。问题不在于我知道多少，而在于我如何组织和结构化我所知道的东西。换句话说，这取决于对编程艺术本身的理解。</p>
<p>我决定摆脱这个陷阱。经过五个月的密集工作——每天写作、设计和研究四到五个小时——我创造了当初学编程时梦寐以求的工具，一本完整的初学者指南：<strong>《整洁代码从零到一》</strong>。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737731329839/c4c862d9-7fdc-460a-ae2e-18b19468b6ec.png" alt="《整洁代码从零到一》封面图片：从混乱代码到杰作" width="1920" height="1080" loading="lazy"></p>
<p>如果你想了解更多关于这本书的信息，我会在本教程的结尾告诉你所有的细节。所以继续阅读以了解更多关于编写整洁代码的内容。</p>
<h2 id="heading-12-clean-code-design-patterns-for-building-agile-applications">构建敏捷应用程序的 12 种整洁代码设计模式 ⚖️</h2>
<p>如果你的代码不遵循这些现代整洁代码设计模式，你可能正在制造一个定时炸弹。这些模式是你的工具。掌握它们并享受项目成功的乐趣。让我一一展示给你看。</p>
<h3 id=""><strong>🌿 使用有意义的名称</strong></h3>
<p>将你的变量或函数命名为 b 或 x 并没有帮助。将它们称为何物以便于理解。以下是一个坏变量名与好变量名的例子：</p>
<pre><code>// 弱且模糊
let b = 5;

// 强且清晰
let numberOfUsers = 5;
</code></pre>
<p>那些写出不清晰名字的人不愿为自己的错误负责。不要成为那样的人。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736165724746/37b2edc3-3c68-47a8-ab6f-f131a2239a01.png" alt="Shahan创作的关于不良变量名和良好变量名的漫画" width="2000" height="800" loading="lazy"></p>
<h3 id="srp"><strong>🔨 保持函数的专注性 (SRP)</strong></h3>
<p>一个函数应该做到<strong>一件事</strong>——并做到完美。这就是单一职责原则（<strong>SRP</strong>）。</p>
<p>好的代码就像一把锤子。它只打一根钉子，不会打十根。例如，如果你招聘一个人来处理公司里的所有事情——财务、销售、市场营销、清洁工作等等——他们很可能会因为无法专注而惨败。同样的道理适用于你代码中的类。</p>
<p>🚧 当一个类或函数做超过一件事时，就会变成一团乱麻。调试它就像在倒着拼拼图。比如，如果你的类既要处理用户输入又要操作数据库，这不是多任务并行，而是逻辑混乱。将其拆分，遵循一个方法只做一件事的原则。</p>
<p><strong>🔥 我的法则：</strong> 你的代码为你工作。保持它精炼、专注且可控，否则它会控制你。以下是实现这一目标的方法：</p>
<pre><code>// 整洁代码：专注于单一任务
function calculateTotal(a, b) {
    return a + b;
}

function logTotal(user, total) {
    console.log(`User: ${user}, Total: ${total}`);
}

// 混乱代码：尝试做所有事情
function calculateAndLogTotal(a, b, user) {
    let total = a + b;
    console.log(`User: ${user}, Total: ${total}`);
}
</code></pre>
<p>🪧 任务混在一起，混乱也就随之而来。就这么简单。</p>
<h3 id=""><strong>🚪 谨慎使用注释</strong></h3>
<p>职业开发者中有一句名言：</p>
<blockquote>
<p>“代码自有其解释。”</p>
</blockquote>
<p>当有人走进房间时，你不会每次都解释门是做什么用的，对吧？你的代码也该如此。</p>
<p>加注释没错，但如果代码不加注释就看不懂，那代码本身可能就有毛病。</p>
<p>🪧 好的注释要讲“为什么”，而不是“怎么做”或“是什么”。如果开发者连代码是怎么工作的都看不懂，那他们更不可能理解背后的“为什么”。</p>
<p>以下是一些好的注释与坏的注释的简短例子。我还将向你展示一个编写清洁注释的实际项目。</p>
<p><strong>例子1：糟糕的注释 👎</strong></p>
<pre><code>// 将价格乘以数量以计算总数
const total = price * quantity;
</code></pre>
<p>这是一个<strong>糟糕的注释</strong>，因为它只是重复了代码已经说明的内容。代码 <code>price * quantity</code> 本身已经很清晰，因此这个注释没有增加任何有用的信息。</p>
<p><strong>好注释：👍</strong></p>
<p>如果代码清晰简明，<strong>你不需要注释。</strong></p>
<pre><code>const total = price * quantity;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736165891398/6a942ad7-5b09-4990-9c7f-95358dafcbf3.png" alt="Shahan创作的说明不必要注释与“无声注释”的图像" width="2000" height="800" loading="lazy"></p>
<p><strong>例子2：糟糕的注释 👎</strong></p>
<pre><code>// 检查用户是否登录
function isUserLoggedIn(session) {
    return !!session.user;
}
</code></pre>
<p>这个注释不好，因为它没有解释 <code>isUserLoggin()</code> 的存在原因。它只是解释了正在发生的事情。但我们已经知道这是一个身份验证函数。这个注释是浪费时间。</p>
<p><strong>好例子 👍</strong></p>
<pre><code>// 用户在访问受保护的资源之前已经通过认证
function isUserLoggedIn(session) {
    return !!session.user;
}
</code></pre>
<p>这是一个<strong>好的注释</strong>，因为它解释了代码存在的<strong>原因</strong>。它告诉我们函数在允许访问应用程序的敏感部分之前检查用户是否已认证。它关注的是更大的图景。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736166143011/b3ddae3d-41cf-4534-8f1a-af710579922c.png" alt="之前：“检查用户是否已登录”。之后：“用户在访问受保护的资源之前已经通过认证。”作者：Shahan。" width="2000" height="800" loading="lazy"></p>
<h3 id=""><strong>⚡ 编写优秀注释的最佳实践</strong></h3>
<ol>
<li>
<p><strong>解释“为什么”，而不是“什么”：</strong><br>
写注释是为了解释代码的目的或背景，而不是代码正在做什么。</p>
</li>
<li>
<p><strong>避免明显的注释：</strong><br>
不要为代码已经清晰的部分写注释。</p>
</li>
<li>
<p><strong>保持简短和精确：</strong><br>
写简洁的注释，方便阅读，并直接解释目的。</p>
</li>
<li>
<p><strong>定期更新注释：</strong><br>
过时的注释可能误导开发者，所以在代码更改时务必更新注释。</p>
</li>
</ol>
<p><strong>现实世界中的例子（使用好的注释） 🛒</strong></p>
<p>让我们把这些实践应用到一个真实项目中：一个大型电子商务应用程序。一个函数根据订单详情计算运输成本。下面是完整的代码，我将在下面解释每个注释：</p>
<pre><code>// 运输规则：
// - 订单超过 $100 免费运输
// - 低于 $100 的订单标准运输（$10）
// - 国际订单额外 $5

function calculateShipping(order) {
    let shippingCost = 0;

    // 检查订单是否符合免费运输条件
    if (order.total &gt;= 100) {
        shippingCost = 0; // 免费运输
    } else {
        shippingCost = 10; // 标准运输费用
    }

    // 为国际订单增加额外费用
    if (order.isInternational) {
        shippingCost += 5;
    }

    return shippingCost;
}

// 示例使用
const order1 = { total: 120, isInternational: false };
const order2 = { total: 80, isInternational: true };

console.log(calculateShipping(order1)); // 输出：0
console.log(calculateShipping(order2)); // 输出：15
</code></pre>
<p>在函数的开始，我们包含了一个注释来解释运输费用的规则。这使读者在不需要阅读完整代码的情况下获得逻辑概览。</p>
<pre><code>// 运输规则：
// - 订单超过 $100 免费运输
// - 低于 $100 的订单标准运输（$10）
// - 国际订单额外 $5
</code></pre>
<p>然后，第一个条件检查订单总额是否大于或等于 $100。这里的注释明确了为什么会应用免费运输。</p>
<pre><code>// 检查订单是否符合免费运输条件
if (order.total &gt;= 100) {
    shippingCost = 0; // 免费运输
}
</code></pre>
<p>第二个条件为国际运输应用附加收费。注释解释了为何增加额外费用。</p>
<pre><code>// 为国际订单增加额外费用
if (order.isInternational) {
    shippingCost += 5;
}
</code></pre>
<p><strong>为什么这些注释很好？</strong></p>
<p>想象一下你正身处一个 20 人的开发团队。六个月后，有人读到了 <code>calculateShipping</code> 这个函数。如果没有这些注释，他们可能得浪费大把时间去猜测为什么国际订单会有一笔额外费用。好的注释能阐明背后的原因，避免让人陷入数小时的烦躁和困惑中。</p>
<h3 id=""><strong>🧩 让你的代码可读</strong></h3>
<p>如果别人看你的代码感觉像是在“破译密码”，那你已经在给团队“埋坑”了。不信请看：</p>
<pre><code>// 干净：读起来像看故事一样
if (isLoggedIn) {
    console.log("Welcome!");
} else {
    console.log("Please log in.");
}

// 杂乱：感到困惑
if(isLoggedIn){console.log("Welcome!");}else{console.log("Please log in.");}
</code></pre>
<p>如果你的代码杂乱且难以阅读，它会让他人甚至是你自己感到困惑！想象六个月后回到自己写的代码却感觉像在读一种外语。可读性高的代码节省时间，减少错误，使每个人的生活更轻松。</p>
<p><strong>🍵 为什么可读性很重要？</strong></p>
<ol>
<li>
<p><strong>对于你自己：</strong> 当你在几周或几个月后重新访问代码时，整洁的代码能帮助你在不浪费时间弄清楚自己做了什么的情况下继续进行接下来的工作。</p>
</li>
<li>
<p><strong>对于你的团队：</strong> 如果其他人阅读你的代码，他们不应该像是在解谜题。整洁的代码让团队合作更顺畅，防止沟通不畅。</p>
</li>
<li>
<p><strong>更少的错误：</strong> 清晰的代码更易于调试，因为你能快速发现错误。</p>
</li>
</ol>
<p><strong>🧙‍♂️ 如何编写可读的代码</strong></p>
<p>让我们构建一个简单的程序来管理图书馆中的书籍。我们将使其整洁和可读，接下来我将分解这段代码：</p>
<pre><code>// 一个表示书籍的类
class Book {
    constructor(title, author, isAvailable) {
        this.title = title;
        this.author = author;
        this.isAvailable = isAvailable;
    }

    borrow() {
        if (this.isAvailable) {
            this.isAvailable = false;
            console.log(`You borrowed "${this.title}".`);
        } else {
            console.log(`Sorry, "${this.title}" is not available.`);
        }
    }

    returnBook() {
        this.isAvailable = true;
        console.log(`You returned "${this.title}".`);
    }
}

// 一个用于展示可用书籍的函数
function displayAvailableBooks(books) {
    console.log("Available books:");
    books.forEach((book) =&gt; {
        if (book.isAvailable) {
            console.log(`- ${book.title} by ${book.author}`);
        }
    });
}

// 示例使用
const book1 = new Book("The Clean Coder", "Robert Martin", true);
const book2 = new Book("You Don’t Know JS", "Kyle Simpson", false);
const book3 = new Book("Eloquent JavaScript", "Marijn Haverbeke", true);

const library = [book1, book2, book3];

displayAvailableBooks(library); // 显示可用的书籍
book1.borrow(); // 借阅一本书
displayAvailableBooks(library); // 再次显示可用的书籍
book1.returnBook(); // 归还这本书
displayAvailableBooks(library); // 最终列表
</code></pre>
<p>我们创建了一个 <code>Book</code> 类来表示每本书。它具有像 <code>title</code>、<code>author</code> 和 <code>isAvailable</code> 这样的属性来追踪其状态。</p>
<ul>
<li><code>borrow</code> 方法检查书籍是否可用。如果是，则将其标记为不可用并打印一条消息。</li>
<li><code>returnBook</code> 方法使书籍再次可用。</li>
<li><code>displayAvailableBooks</code> 函数循环遍历图书馆，只打印可用的书籍。</li>
<li>我们创建了三本书（<code>book1</code>、<code>book2</code>、<code>book3</code>）并将它们存储在一个 <code>library</code> 数组中。</li>
<li>我们借阅和归还书籍，展示了可用书籍列表如何变化。</li>
</ul>
<p>如你所见，可读代码不仅仅关乎风格。它能节省时间、防止错误，并确保你的代码在未来多年仍能发挥作用。</p>
<h3 id=""><strong>🏌️ 测试你写的所有内容</strong></h3>
<p>如果你不花时间编写测试，那么代码出故障时就别感到意外。如果你打算编写测试，请遵循这套单元测试策略，以便及早发现问题。</p>
<p><strong>什么是单元测试？</strong></p>
<p>具体来说，单元测试检查代码的各个部分（如函数或类）以确保其正常工作。就像在建造房子的墙之前检查每一块砖是否完好一样。</p>
<p>让我给你一个单元测试工作原理的例子：</p>
<pre><code class="language-javascript">class Calculator {
    add(a, b) { return a + b; }
    subtract(a, b) { return a - b; }
}

// 测试它（单元测试）
const calculator = new Calculator();
console.assert(calculator.add(2, 3) === 5, "加法失败");
console.assert(calculator.subtract(5, 3) === 2, "减法失败");
</code></pre>
<p>让我们来看看这段代码是怎么运行的：</p>
<p>首先，我们要先构建一个计算器类：</p>
<pre><code class="language-javascript">class Calculator {
    add(a, b) { return a + b; }
    subtract(a, b) { return a - b; }
}
</code></pre>
<p><code>Calculator</code> 类有两个方法：<code>add</code> 和 <code>subtract</code>。</p>
<ul>
<li><code>add(a, b)</code> 接受两个数字并返回它们的和。</li>
<li><code>subtract(a, b)</code> 接受两个数字并返回它们的差。</li>
</ul>
<p>接下来，我们设置测试：</p>
<pre><code class="language-javascript">const calculator = new Calculator();
</code></pre>
<p>在这里，我们创建一个 <code>Calculator</code> 类的实例以测试其方法。</p>
<p>然后我们编写测试用例：</p>
<pre><code class="language-javascript">console.assert(calculator.add(2, 3) === 5, "加法失败");
console.assert(calculator.subtract(5, 3) === 2, "减法失败");
</code></pre>
<p><code>console.assert(condition, message)</code> 检查条件是否为 <code>true</code>。如果是 <code>false</code>，则消息（“加法失败”或“减法失败”）将显示在控制台中。</p>
<ul>
<li>
<p><strong>第一次测试</strong>：<code>calculator.add(2, 3) === 5</code></p>
<ul>
<li>调用 <code>add</code> 方法，给出 <code>2</code> 和 <code>3</code>。</li>
<li>检查结果是否为 <code>5</code>。</li>
</ul>
</li>
<li>
<p><strong>第二次测试</strong>：<code>calculator.subtract(5, 3) === 2</code></p>
<ul>
<li>调用 <code>subtract</code> 方法，给出 <code>5</code> 和 <code>3</code>。</li>
<li>检查结果是否为 <code>2</code>。</li>
</ul>
</li>
</ul>
<p>那么如果出错了会发生什么呢？在这里解决出现的问题是相当简单的。在这种情况下，如果 <code>add</code> 或 <code>subtract</code> 方法不能正常工作，测试将失败。例如：</p>
<pre><code class="language-javascript">console.assert(calculator.add(2, 3) === 6, "加法失败");
</code></pre>
<ul>
<li>条件 <code>calculator.add(2, 3) === 6</code> 是 <code>false</code>。</li>
<li>控制台将显示：“加法失败”。</li>
</ul>
<p><strong>实际例子：测试一个登录系统 👥</strong></p>
<p>让我们测试一个简单的登录系统，看看单元测试在实际场景中如何工作。</p>
<pre><code class="language-javascript">class Auth {
    login(username, password) {
        return username === "admin" &amp;&amp; password === "1234";
    }
}

// 测试 Auth 类
const auth = new Auth();
console.assert(auth.login("admin", "et5t45#@") === true, "有效凭证登录失败");
console.assert(auth.login("user", "wrongpassword") === false, "无效凭证登录成功");
</code></pre>
<p>首先，创建 <code>Auth</code> 类：</p>
<pre><code class="language-javascript">class Auth {
    login(username, password) {
        return username === "admin" &amp;&amp; password === "1234";
    }
}
</code></pre>
<p><code>login</code> 方法检查用户名是否为 <code>"admin"</code> 且密码是否为 <code>"1234"</code>。如果两者都匹配，则返回 <code>true</code>，否则返回 <code>false</code>。</p>
<p>接下来，设置测试：</p>
<pre><code class="language-javascript">const auth = new Auth();
</code></pre>
<p>创建一个 <code>Auth</code> 类的实例。然后编写测试用例：</p>
<pre><code class="language-javascript">console.assert(auth.login("admin", "1234") === true, "有效凭证登录失败");
console.assert(auth.login("user", "wrongpassword") === false, "无效凭证登录成功");
</code></pre>
<ul>
<li><strong>第一次测试</strong>：检查有效凭证（<code>"admin"</code>，<code>"1234"</code>）是否成功。如果不成功，就会显示 “有效凭证登录失败”。</li>
<li><strong>第二次测试</strong>：检查无效凭证（<code>"user"</code>，<code>"wrongpassword"</code>）是否失败。如果没有失败，就会显示 “无效凭证登录成功”。</li>
</ul>
<p><strong>🌱 为什么测试会导致干净的代码：</strong></p>
<ol>
<li>
<p>为了提高代码的可测试性，你会自然而然地编写更短小、更专注的函数。</p>
</li>
<li>
<p>测试能验证你的代码在不同场景下的表现是否符合预期。</p>
</li>
<li>
<p>有了测试作为保障，你可以放心大胆地更新代码，因为你知道任何错误都会被测试及时发现。</p>
</li>
</ol>
<h3 id=""><strong>💉 使用依赖注入</strong></h3>
<p>将依赖硬编码就像在额头上纹上某人的名字——它是永久性的，可能是磨蚀性的，并且限制了你的选择。</p>
<p>那么，依赖注入做了什么？它通过将依赖关系作为参数传递来管理代码的关系。它是灵活、可适应且易于维护的。</p>
<p>为了演示其工作原理，我这里使用 Nodemailer 依赖项向用户发送电子邮件：</p>
<pre><code>// 依赖：使用 Nodemailer 发送电子邮件
const nodemailer = require('nodemailer');
function sendEmail(to, subject, message) {
    const transporter = nodemailer.createTransport({ /* 配置 */ });
    return transporter.sendMail({ from: "programmingwithshahan@gmail.com", to, subject, text: message });
}
</code></pre>
<p>⚠️ 为了避免风险，请确保避免将依赖关系<strong>硬编码</strong>。使用抽象或配置文件进行安全维护。</p>
<p>这只是一个例子。作为开发人员，你可能会使用数百个库或依赖项。</p>
<p>我并不是说你绝对不应该依赖依赖项/库，因为如今很难避免它们。但在将它们安装到你的编码项目中之前，你应该非常小心。</p>
<p>你应该检查组织的软件系统的安全性、性能、质量或功能。因为它们有时可能包含会毁掉你整个项目的风险。</p>
<p>🚧 永远要控制你的工具，不要让它们控制你。</p>
<h3 id=""><strong>📂 清理项目结构</strong></h3>
<p>一个组织良好的项目就像一座高端<strong>精品店</strong>与一堆<strong>垃圾堆</strong>的区别。</p>
<p>以下是每个文件夹的组织方式：</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xwyg9iqqcybz21lsgxz.png" alt="Shahan 的整洁代码项目结构图" width="1563" height="1250" loading="lazy"></p>
<p>如果你的代码库像个杂物抽屉，那你已经给未来的自己制造了麻烦。</p>
<p>让我们浏览上面的干净项目结构，以更好地理解它：</p>
<p><strong>1.</strong> <code>myProjet/src</code></p>
<p>这是整个应用程序的主容器。应用所需的一切都存储在这个文件夹中。它有子文件夹，可以保持整洁并在一个地方管理。</p>
<p><strong>2.</strong> <code>components</code></p>
<p>这是存放应用所有可重用部分的地方。你可以在多个地方使用这些组件，而无需再次构建它们。</p>
<p><strong>3.</strong> <code>services</code></p>
<p>这是应用的“大脑”。它在前端和后端为幕后工作提供支持。<code>emailService.js</code>、<code>userService.js</code> 和 <code>productService.js</code> 是 <code>services</code> 文件夹的一些示例文件。</p>
<p><strong>4.</strong> <code>utils</code></p>
<p>这包含了所有运行应用程序所需的小工具，使你的生活更轻松。例如，<code>formatedate.js</code>、<code>validateEmail.js</code> 和 <code>generateId.js</code> 是一些常见的 utils 文件，用于为整个项目制作可重用的组件。</p>
<p><strong>5.</strong> <code>tests</code></p>
<p>根据惯例，测试文件通常位于项目根级别的 <code>src</code> 文件夹<strong>之外</strong>。这样可以将生产代码（<code>src</code>）与测试代码（<code>tests</code>）分隔开，使其更清晰易于管理。查看下面结构：</p>
<pre><code>myProject/
├── src/              # 生产代码
│   ├── components/
│   ├── services/
│   └── utils/
├── tests/            # 测试文件
│   ├── components/
│   ├── services/
│   └── utils/
├── package.json      # 项目配置
└── README.md         # 文档
</code></pre>
<p>一些开发人员可能更喜欢在 <code>test</code> 文件夹中创建一个测试文件来测试所有内容。遗憾的是，一开始它看起来很整洁，但随着项目的发展，你将不得不寻找和搜索特定代码块。这样显得杂乱无章，并可能产生意外的测试结果。因此，强烈建议在 <code>tests</code> 文件夹中将它们分解成多个测试文件。</p>
<p><strong>一个实际的例子 📧</strong></p>
<p>让我为你创建一个干净、耐用的项目结构，以便在任何将来可能从事的项目中应用。不用说，干净的项目结构是构建可维护项目的基础。</p>
<p>根据我们之前的发送电子邮件应用程序的示例，我们将为该应用程序编写一个干净的项目结构。我们希望构建一个向用户发送电子邮件的应用程序。此应用的干净项目结构应如下所示：</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6v6rlc5qiplgxz1h4dps.png" alt="Shahan 的邮箱应用整洁代码项目结构图" width="1563" height="1250" loading="lazy"></p>
<p>如你所见，我将每个子文件夹和文件都打包在应用的主容器 <code>src</code> 文件夹中。在 <code>src</code> 文件夹中，我们创建了 <code>components</code>、<code>services</code>、<code>utiles</code>。最后，我们在 <code>src</code> 文件夹外有一个可管理的 <code>test</code> 文件夹，以独立测试每个组件。这个测试文件夹与位于 <code>src</code> 文件夹中的生产代码没有任何关系。</p>
<h3 id=""><strong>🤹‍♂️ 保持格式一致性</strong></h3>
<p>别让你的代码看起来像是出自不同的十个人一样。请务必保持格式风格的一致性。</p>
<p>使用 <a href="https://prettier.io/">Prettier</a> 或 <a href="https://eslint.org/">ESLint</a> 等工具强制执行一致的风格。如果每个文件都看起来不同，你只是在制造没人愿意解决的混乱。</p>
<p>我认为格式一致性是编写整洁代码的最重要方面之一。</p>
<pre><code>// 始终使用2个空格进行缩进
function calculateArea(width, height) {
  if (width &lt;= 0 || height &lt;= 0) {
    throw new Error("维度必须是正数。");
  }
  return width * height;
}

// 添加有意义的空白以提高可读性
const rectangle = {
  width: 10,
  height: 20,
};

// 清晰的逻辑分离
try {
  const area = calculateArea(rectangle.width, rectangle.height);
  console.log(`面积: ${area}`);
} catch (error) {
  console.error(error.message);
}
</code></pre>
<p>让我们来分析一下，这段代码在哪些方面体现了“整洁之道”：</p>
<h4 id="1">1️⃣ 一致的缩进</h4>
<p>为什么用2个或4个空格？这很简洁、最小化，并且被许多JavaScript风格指南普遍接受。它不会让眼睛感到负担，并且代码结构清晰。如果你在这里用2个空格，那里用4个空格，就会让人感到困惑——而困惑时容易犯错。</p>
<h4 id="2">2️⃣ 有意义的空白：给代码留出呼吸空间</h4>
<p>在<code>rectangle</code>定义和<code>try</code>块之间的额外换行就像句子中的停顿——它让读者有时间去处理信息。</p>
<h4 id="3">3️⃣ 清晰的逻辑分离：模块化思维</h4>
<pre><code>try {
  const area = calculateArea(rectangle.width, rectangle.height);
  console.log(`Area: ${area}`);
} catch (error) {
  console.error(error.message);
}
</code></pre>
<p>看看代码逻辑是如何被分成清晰的部分：</p>
<ul>
<li>
<p>首先是计算（<code>calculateArea</code>函数）。</p>
</li>
<li>
<p>然后是输出（<code>console.log</code>）。</p>
</li>
<li>
<p>最后是错误处理（<code>catch</code>块）。</p>
</li>
</ul>
<p>每个任务都有其独立的空间和明确的职责。</p>
<h4 id="4">4️⃣ 可读的错误处理</h4>
<p>当你抛出错误或记录消息时，请整洁地格式化它们。不要使用模糊或隐晦的信息。看到这样的代码，开发者立刻就能知道出了什么问题。</p>
<pre><code>throw new Error("维度必须是正数。");
</code></pre>
<p><strong>🐦‍⬛ 常规的格式一致性建议：</strong></p>
<ul>
<li>
<p>在整个代码库中一致地使用2个或4个空格进行缩进。避免使用制表符，以维护在不同编辑器间的一致性。</p>
</li>
<li>
<p>将每行的字符数限制在最多100-120个以内，以防止水平滚动并提高可读性。</p>
</li>
<li>
<p>将相关逻辑集中在一起，用空行分隔代码块以突出强调其用途。</p>
</li>
<li>
<p>最后，避免过度对齐代码。相反，让缩进自然引导逻辑流程。</p>
</li>
</ul>
<h3 id=""><strong>✋ 停止硬编码值</strong></h3>
<p>硬编码值是一种懒惰的编码方式。以下是证明：</p>
<pre><code>// 糟糕：硬编码且僵化
function createUser() {
    const maxUsers = 100;
    if (currentUsers &gt;= maxUsers) throw "用户太多！";
}

// 简洁：动态且灵活
const MAX_USERS = 100;
function createUser() {
    if (currentUsers &gt;= MAX_USERS) throw "用户太多！";
}
</code></pre>
<p>你看，改变这个变量将来不会让你感到意外。你确切地知道在哪里可以找到它来改变不确定的值。</p>
<p>最好将固定值存储在全局配置（配置）文件中。</p>
<p>🪧 因此，无论如何避免硬编码。硬编码看似捷径，却可能让未来的自己（或他人）抓狂。</p>
<h3 id=""><strong>🤏 保持函数简洁</strong></h3>
<p>如果你的函数超过20行，可能它试图做得太多了。</p>
<p>短小精悍的函数总是能命中目标。</p>
<p>冗长的函数又乱又难读，而短小的函数则清晰且集中。以下是你应如何将大型函数拆解：</p>
<pre><code>function updateCart(cart, item) {
    addItemToCart(cart, item);
    let total = calculateTotal(cart);
    logTransaction(item, total);
    return total;
}

function addItemToCart(cart, item) {
    cart.items.push(item);
}
</code></pre>
<p>让我解释一下这段代码，以便你理解为何将大型函数拆分是一个明智的策略。</p>
<ol>
<li>
<p><strong>主函数：</strong> <code>updateCart()</code> 调用较小的辅助函数来处理特定的任务，如：</p>
<ul>
<li>
<p>将物品添加到购物车。</p>
</li>
<li>
<p>计算总价格。</p>
</li>
<li>
<p>记录交易细节。</p>
</li>
<li>
<p>最后，返回总价格。</p>
</li>
</ul>
</li>
</ol>
<p>这并不是一个试图做所有事情的长块代码，而是将任务委派给辅助函数。</p>
<ol start="2">
<li><strong>辅助函数：</strong> <code>addItemToCart()</code> 这个函数<strong>只</strong>负责将物品添加到购物车。如果你需要更改添加物品的方式（例如，检查重复项）。你只需编辑这个小函数，而不必在<code>updateCart</code>中一大块代码中寻找更改。这就是编写整洁代码函数的方式，令人愉悦和易于维护。</li>
</ol>
<p><strong>函数过长会怎样？ 💤</strong></p>
<p>假设你没有拆分<code>updateCart</code>函数。它可能看起来像这样：</p>
<pre><code>function updateCart(cart, item) {
    cart.items.push(item);
    let total = 0;
    for (let i = 0; i &lt; cart.items.length; i++) {
        total += cart.items[i].price;
    }
    console.log(`添加了${item.name}。总价现在是$${total}。`);
    return total;
}
</code></pre>
<p>这里的问题是什么？</p>
<ul>
<li>
<p>它试图做所有事情。</p>
</li>
<li>
<p>特别是当它变得更庞大时，难以阅读。</p>
</li>
<li>
<p>如果出了问题，你将浪费时间去找出问题的所在部分。</p>
</li>
</ul>
<p>现在，选择权在你手中：是继续坚持那种混乱的“全能一把抓”写法，还是开始实践“一个函数只做一件事”的整洁心态？</p>
<h3 id=""><strong>⛺ 遵循童子军规则</strong></h3>
<blockquote>
<p>离开营地时，要比你发现它时更整洁。</p>
</blockquote>
<p>让我来详细解释一下：你不能只顾着自己使用，却留下一个比之前更烂的摊子。那是极不负责任的行为。真正的专业人士总会想办法让事物变得比原来更好。</p>
<p>在编程中，这意味着 <strong>每当你触碰代码库，都要让它变得更好一点。</strong> 去清理冗余、重构混乱的部分、提升可读性。如果你不这么做，你只是在不断堆积垃圾，而这些“屎山”最终会崩塌并砸到你自己头上。</p>
<p>来看这个例子。在这里，我们没有选择优化，而是仅仅增加了一层又一层的复杂度：</p>
<pre><code>// 原始代码：难以阅读，变量命名不好
function calc(a, b) {
  let x = a + b;
  let y = x * 0.2;
  return y;
}

// 我们只是不断添加而未进行清理
function calcDiscount(a, b, discountRate) {
  let total = calc(a, b);
  let final = total - discountRate;
  return final;
}
</code></pre>
<p>之后：每次都有所改善。这是一个有纪律的程序员的工作方式——他们在改进中前行：</p>
<pre><code>// 改进后的代码：清晰的命名，重构以提高清晰度
function calculateSubtotal(price, quantity) {
  return price * quantity;
}

function calculateDiscountedTotal(price, quantity, discountRate) {
  const subtotal = calculateSubtotal(price, quantity);
  const discount = subtotal * discountRate;
  return subtotal - discount;
}
</code></pre>
<p>现在，任何人一目了然地看出发生了什么。因为我们将代码拆解为更小、更集中的函数。因此，增加新功能不会破坏现有功能。🏕️</p>
<h3 id=""><strong>🏟️ 遵循开放/封闭原则</strong></h3>
<p>这项设计原则建议：代码应当设计成允许扩展，而不必改变现有的基础。</p>
<p>你肯定希望是“增加功能”，而不是每升级一次就把代码“拆了重建”。为了适配新需求而去修改旧代码，简直就像每次买新家具都要把房子推倒重盖一样。这显然是不可持续的。</p>
<p>让我们来看看如何构建更聪明、更具扩展性的代码，让你在增加功能的同时，不会把其他部分搞坏。</p>
<h4 id="">重构前：违反原则的写法</h4>
<p>你写了一个处理支付的类——挺简单的，起初它只支持信用卡支付。</p>
<p>结果你老板突然冒出来说：“嘿，我们现在得接个 PayPal。”</p>
<p>因为你之前没心思学什么代码整洁之道，你的代码现在看起来就像一个从 1995 年过时企业级系统里爬出来的“意面怪物”。来，欣赏一下你亲手打造的这件“杰作”：</p>
<pre><code>class PaymentProcessor {
  processPayment(paymentType, amount) {
    if (paymentType === "creditCard") {
      console.log(`Processing credit card payment of $${amount}`);
    } else if (paymentType === "paypal") {
      console.log(`Processing PayPal payment of $${amount}`);
    } else {
      throw new Error("Unsupported payment type");
    }
  }
}

const paymentProcessor = new PaymentProcessor();
paymentProcessor.processPayment("creditCard", 100);
paymentProcessor.processPayment("paypal", 200);
</code></pre>
<p>唉！每新增一种支付方式（例如 Apple Pay、Google Pay 等），都需要修改 <code>processPayment</code> 方法。不用说，你在添加新功能时有可能破坏现有功能。如果你学过这个原则，你就不会陷入这个困境。</p>
<p>不要担心：我会帮你解决这个问题。首先，我们需要重构代码。我们将通过使用<a href="https://stackify.com/oop-concept-polymorphism/">多态</a>来扩展其功能，而不是修改现有的类：</p>
<pre><code>javascriptCopy code// 基础类
class PaymentProcessor {
  processPayment(amount) {
    throw new Error("processPayment() must be implemented");
  }
}

// 信用卡支付
class CreditCardPayment extends PaymentProcessor {
  processPayment(amount) {
    console.log(`Processing credit card payment of $${amount}`);
  }
}

// PayPal 支付
class PayPalPayment extends PaymentProcessor {
  processPayment(amount) {
    console.log(`Processing PayPal payment of $${amount}`);
  }
}

// 添加新的支付类型？只需扩展类！
class ApplePayPayment extends PaymentProcessor {
  processPayment(amount) {
    console.log(`Processing Apple Pay payment of $${amount}`);
  }
}

// 使用方法
const payments = [
  new CreditCardPayment(),
  new PayPalPayment(),
  new ApplePayPayment(),
];

payments.forEach((payment) =&gt; payment.processPayment(100));
</code></pre>
<p>现在，添加新的支付方式不再需要修改现有的 <code>PaymentProcessor</code> 类了，你只需要创建一个新的子类。这样一来，原有代码保持原封不动，意味着完全没有破坏现有功能的风险。</p>
<p>每种支付类型都有独立的类。比如添加 PayPal 支持，完全不会影响到老代码。现在你可以自信地回复老板：“没问题，我 5 分钟就能把这功能加上。” 升职加薪的机会正等着你呢！</p>
<p>我在我的书 <a href="https://codewithshahan.gumroad.com/l/cleancode-zero-to-one">从零到一的整洁代码</a> 中分享了更多的技巧。</p>
<h2 id="heading-modern-best-practices-to-help-you-write-clean-code-a-summary">帮助你编写整洁代码的现代最佳实践：总结 🥷</h2>
<p>现在，让我为你展示最佳实践，并总结这 12 条整洁代码设计原则，助你在敏捷应用开发中游刃有余。</p>
<h3 id="">🔎 常见代码异味及其修复方法</h3>
<ul>
<li>
<p>💊 重复代码： 如果你在复制粘贴代码，那你是在给自己挖坑。把代码抽离成函数，一次性把事做对。</p>
</li>
<li>
<p>🛤️ 过长方法： 如果一个方法长到需要滚动条，那它承载的逻辑就太多了。拆分它，让功能保持专注。</p>
</li>
<li>
<p>👑 万能对象： 没有任何一个类应该包揽所有活儿。简化职责，否则你的代码库迟早会变成一团乱麻。</p>
</li>
</ul>
<h3 id="">💬 高效注释实践</h3>
<ul>
<li>
<p>💭 何时注释： 只有在代码逻辑不够清晰时才写注释。如果代码已经很直观了，注释就是噪音。</p>
</li>
<li>
<p>🫗 清晰度： 注释应该解释 <strong>“为什么”</strong> 而不是“是什么”。如果你的代码非得靠注释才能读懂，那可能它写得太复杂了。</p>
</li>
<li>
<p>🌴 避免冗余： 别给显而易见的代码写注释。如果函数名是 <code>addNumbers</code>，就别再注释说它是用来“加数”的。</p>
</li>
</ul>
<h3 id="">🧼 整洁代码的重构技巧</h3>
<ul>
<li>
<p>🏭 提取方法：方法太臃肿？拆掉它。这不仅仅是为了整洁，更是为了掌控感。</p>
</li>
<li>
<p>🫕 重命名变量：如果变量名不能一眼看出用途，就改掉它。命名的精准度反映了思维的严谨度。</p>
</li>
<li>
<p>🍃 如果你的判断语句写得像代数题一样复杂，简化它。如果是 if (a == true)，直接写成 if (a)。</p>
</li>
</ul>
<h3 id="">🧪 测试与清洁代码</h3>
<ul>
<li>
<p>🧙 单元测试：像审讯嫌疑人一样测试每一行代码。不放过任何死角。</p>
</li>
<li>
<p>🏇 TDD（测试驱动开发）：先写测试。这不只是为了抓 Bug，更是为了在动笔写代码前，就搞清楚它到底该干什么。</p>
</li>
<li>
<p>🧽 清洁测试：测试代码也要写得和业务代码一样整洁。如果测试代码本身就乱七八糟，它就毫无参考价值。</p>
</li>
</ul>
<h3 id="">🐛 错误处理与整洁代码</h3>
<ul>
<li>
<p>⁉️ 异常处理：大胆使用异常。它们不仅能处理错误，还能让你的主逻辑免受错误处理代码的干扰。</p>
</li>
<li>
<p>🖍️ 快速失败：一旦发现异常，立即停止。别让错误雪球越滚越大，当场解决。</p>
</li>
<li>
<p>🚨 日志记录：记录日志要像记录犯罪现场一样。清晰、精准，且只记录必要的信息。</p>
</li>
</ul>
<h3 id="">🌱 代码审查及整洁代码</h3>
<ul>
<li>
<p>🚢 流程化：建立一套系统。拒绝“牛仔式”野路子开发。审查、评判、改进。</p>
</li>
<li>
<p>🔪 工具化：利用工具让审查变得轻松。工具不仅是为了纠错，更是为了培养规范性。</p>
</li>
<li>
<p>🧦 文化建设：营造一种“反馈是金”的文化。帮助团队学会如何给出和接受有建设性的批评。</p>
</li>
</ul>
<h2 id="heading-automated-tools-for-maintaining-clean-code">保持代码整洁的自动化工具 ⚓</h2>
<p>工具和自动化技术对于编写清洁代码非常有帮助。如果你没有使用合适的工具和自动化来节省时间，那就错过了机会。</p>
<p>你认为可以"凭眼力"判断代码质量？再想想吧。没有自动化，这会发生：</p>
<ol>
<li>
<p>👎 因为“太忙”而错过显而易见的错误。</p>
</li>
<li>
<p>🤕 你的代码在每个文件中看起来都不同，协作起来头疼。</p>
</li>
<li>
<p>🪦 部署失败，因为你跳过了关键测试。</p>
</li>
</ol>
<p>成功的开发者使用合适的工具来自动化代码和完成任务。以下是使用现代工具维护清洁代码的四种策略。</p>
<h3 id="1"><strong>1️⃣ 静态分析</strong></h3>
<p>静态分析实际上是一个代码检查器，它在早期找出潜在问题。最棒的是，它在<strong>运行前</strong>工作，捕获可能导致崩溃、停机或令人尴尬的错误。</p>
<h4 id=""><strong>它是如何工作的？</strong></h4>
<ol>
<li>
<p><strong>语法检查</strong>：检查代码中编写的语法是否正确。如果拼写错误或遗漏了闭合括号，它会立即指出。</p>
</li>
<li>
<p><strong>代码质量规则</strong>：像 ESLint 这样的工具强制执行规则，如一致的缩进、避免未使用的变量，以及遵循最佳实践。</p>
</li>
<li>
<p><strong>错误预防</strong>：识别逻辑错误，比如使用未定义的变量或进行无意义的比较。</p>
</li>
</ol>
<p>以下是静态分析的实际操作：</p>
<h4 id="">🚨 在静态分析之前：</h4>
<pre><code class="language-javascript">let sum = (a, b) =&gt; { return a + b; }
console.log(sume(2, 3)); // Typo, unnoticed until runtime
</code></pre>
<ul>
<li><strong>问题</strong>：<code>sume</code>中的拼写错误只有在运行代码时才会导致错误，这可能会导致令人沮丧的调试会话，或者更糟糕的是，在生产环境中中断应用程序。</li>
</ul>
<h4 id="eslint">🚑 使用 ESLint 之后的静态分析：</h4>
<pre><code>codeError: 'sume' is not defined.
</code></pre>
<ul>
<li><strong>解决方案</strong>：ESLint 立即标记拼写错误，你甚至在运行代码之前就发现了问题。早期捕获错误，节省了时间和麻烦。</li>
</ul>
<h3 id="2"><strong>2️⃣ 自动代码格式化</strong></h3>
<p>格式化前：</p>
<pre><code class="language-javascript">function calculate ( x , y ){ return x+ y;}
console.log( calculate (2,3 ) )
</code></pre>
<ul>
<li><strong>问题</strong>：不一致的空格和格式化使代码难以阅读。</li>
</ul>
<h4 id="prettier">使用 Prettier 之后：</h4>
<pre><code class="language-javascript">function calculate(x, y) {
  return x + y;
}
console.log(calculate(2, 3));
</code></pre>
<ul>
<li><strong>解决方案</strong>：自动应用干净、一致、专业的格式。不再挑剔空格或对齐问题。</li>
</ul>
<p>当然了，这些都是基本操作。我讲这些是以防万一，比如你得用记事本手撕代码，或者是在面试这种没有 IDE 助力的环境。</p>
<h3 id="3ci"><strong>3️⃣ 持续集成（CI）测试</strong></h3>
<p>CI 测试确保你对代码的每一次更改都得到自动验证。它就像一个安全网，可以捕捉开发过程中引入的错误。CI 工具在每次推送代码时运行测试，因此在部署后没有任何中断。</p>
<h4 id="ci"><strong>CI 测试如何工作？</strong></h4>
<ol>
<li>
<p><strong>变化触发</strong>：每次提交代码时，CI 工具（如 GitHub Actions、Jenkins）会运行自动测试。</p>
</li>
<li>
<p><strong>反馈</strong>：如果出现问题，它会立即提供反馈。</p>
</li>
<li>
<p><strong>防止代码破坏</strong>：只有干净且工作的代码会被合并到主分支。</p>
</li>
</ol>
<h3 id="4cicd">4️⃣ CI/CD 流水线</h3>
<p>我们也使用 CI/CD 流水线作为一个持续的过程，其中包括代码构建、测试和部署，而 CI 测试是这个过程中专注于自动化代码变更测试的部分。</p>
<p><strong>CI/CD 流水线与 CI 测试的区别：</strong></p>
<ul>
<li>
<p><strong>CI/CD 流水线：</strong> CI/CD 流水线将代码构建、测试和部署整合为一个单一过程。这个过程确保对主分支代码的所有更改都可以发布到生产环境。CI/CD 流水线可以减少部署时间、降低成本并改善团队协作。</p>
</li>
<li>
<p><strong>CI 测试：</strong> CI 测试是自动测试集成到中央代码库中的代码变更的过程。CI 测试专注于确保代码库的稳定性以及解决集成问题。CI 测试帮助开发者构建稳定的、无错误的软件并满足功能需求。</p>
</li>
</ul>
<p>🚧 这些就是 CI 测试和 CI/CD 流水线概念的真正含义。并不像想象中那么复杂。所以让我详细讲解一下使用 GitHub Actions 进行的 CI 测试，因为我们现在通常通过自动化工具运行测试。</p>
<h3 id="githubactionsci"><strong>⚡ 使用 GitHub Actions 进行持续集成（CI）测试</strong></h3>
<p>正如我之前所说，CI 工具会在每次您推送代码或打开拉取请求时运行自动化测试。这确保只有正常工作的、无错误的代码才会被合并到主分支。</p>
<h4 id="githubactionsci">如何使用 GitHub Actions 设置 CI 测试</h4>
<p><strong>步骤 1：创建您的代码库</strong></p>
<p>为您的项目设置一个 GitHub 代码库。然后，使用以下命令将代码推送到 GitHub：</p>
<pre><code>git init
git add .
git commit -m "Initial commit for CI Testing"
git branch -M main
git remote add origin https://github.com/codewithshahan/codewithshahan.git
git push -u origin main
</code></pre>
<p>或者您可以在不使用命令的情况下从 GitHub 账户创建一个新的代码库。只需登录到您的 GitHub 账户并访问仪表板。您会发现一个“New”按钮来创建一个全新的代码库：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737618697327/dcef8be8-0d08-45d7-8000-34c4c65df425.png" alt="在 GitHub 上创建新代码库的图片由 Shahan 提供" width="1371" height="553" loading="lazy"></p>
<p><strong>步骤 2：添加一个 GitHub Actions 工作流</strong></p>
<p>导航到您代码库的 <strong>Actions</strong> 选项卡。要执行此操作，首先您必须访问 GitHub 上的代码库（创建代码库后您会找到链接）。在此示例中，我创建了一个名为“codewithshahan”的新代码库。在这里，看一下导航栏右侧的 <strong>Actions</strong> 选项卡。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737618879398/7c5aa37a-72be-4701-a8f8-9ea9e05c0d5d.png" alt="GitHub Actions 导航选项卡的图片由 Shahan 提供" width="1885" height="724" loading="lazy"></p>
<p>进入 Actions 选项卡后，向下滚动一点，您会找到 <strong>continuous integration</strong> 部分：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737619002674/60003e57-f2b2-48f1-bef8-9bde39149faf.png" alt="GitHub Actions 页面上的 CI（持续集成）测试图片由 Shahan 提供" width="1496" height="508" loading="lazy"></p>
<p>选择一个适合您的工作流设置。我将在这个项目中使用 Node.js。</p>
<p>点击配置按钮后，会自动创建一个 <code>node.js.yml</code> 文件，您可以根据目标调整代码。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737619475568/74da6d46-c105-42c8-8662-fc72e9410bda.png" alt="GitHub 自动测试工作流代码段的图片由 Shahan 提供" width="1879" height="854" loading="lazy"></p>
<p>我不会详细说明如何修改 <code>.yml</code> 文件。这取决于您的项目目标和个人偏好。此外，这是一个完全不同的更广泛的话题，鉴于这篇文章已经相当长，我将在以后的文章中再做解释。目前，请坚持这些基础知识。</p>
<p>这个 CI 测试工作流非常适合现代应用程序开发。您的应用程序在包含关键功能（例如：暗模式）、在 GitHub 代码库中直接构建和部署应用程序时保持稳定。通过这种方式，您可以自信地推送代码，确保您的代码始终清晰并准备好投入生产。</p>
<h2 id="heading-the-role-of-documentation-in-agile-software-development">文档在敏捷软件开发中的角色 🚣</h2>
<p>如果您希望您的编码能力达到顶级水准，您需要了解如何编写好的文档。如果您认为文档只是匆忙写下代码如何工作，那么您错了。这是关于解释<strong>为什么</strong>它可以工作，而不仅仅是如何工作。这是大多数人忽视的一点。</p>
<h3 id="1">1. 🚡 创建<strong>有用的文档（解释为什么，而不仅仅是如何）</strong></h3>
<p>当您编写文档时，您不仅仅是写下一些关于如何使用代码的说明。您是在告诉下一个人（或者未来的自己）为什么这个代码段最初存在。这就是好文档和差文档的区别。</p>
<p>糟糕的文档会让人困惑。它们太模糊、太简单，并且没有回答重要的问题。如果您的文档不清晰，这可能意味着您的思考不清晰。您基本上是在说，“我不在乎你是否理解这个，它管用，直接用就行。”这没有帮助。</p>
<p>优秀的文档回答了棘手的问题：</p>
<ul>
<li>
<p>✅ 为什么您选择了这种方法而不是其他方法？</p>
</li>
<li>
<p>✅ 为什么这个函数存在？它解决了什么问题？</p>
</li>
<li>
<p>✅ 为什么您这样编写代码？</p>
</li>
</ul>
<h3 id="2">2. ⏳ <strong>保持文档更新（过时的文档比没有文档更糟）</strong></h3>
<p>过时的文档是最糟糕的。事实上，它可能比没有文档更糟。当你的文档与代码不同步时，你是在给未来的自己（或下一个必须处理它的人）带来巨大的麻烦。</p>
<p>每当代码发生变化，你的文档也必须随之更新。它必须能够反映代码的当前状态。千万不要留下那些过时的信息去误导未来的开发者（甚至是未来的你自己），那只会让他们感到困惑并浪费时间。如果某些内容不再相关，直接删掉它。过时的文档就像凌乱的大脑——它只会拖你的后腿。</p>
<p>要养成定期检查和更新文档的习惯。代码改动的那一刻，文档也必须同步更新。就这么简单。</p>
<h3 id="3">3. 🚆 <strong>整合注释（代码中的良好注释是文档的一部分）</strong></h3>
<p>听好了——代码里的注释应当与文档融为一体。优秀的注释不该是开发者的“拐杖”，仅仅因为他们在别处解释不清楚代码才拿来凑合。注释的作用是升华文档，而不是取而代之。</p>
<p>注释是文档的补充。你应该编写那种清晰易懂、几乎不需要解释的高质量代码；但当某些地方无法做到“一眼看透”时，再加入注释。记住注释的准则：解释 <strong>“为什么（Why）”</strong>，而不是 <strong>“怎么做（How）”</strong>。文档也是同理。别说废话，让代码自己“说话”。注释应当服务于文档的宏观大局，而不是给烂代码打补丁。</p>
<p>🪧 优秀的代码应该是自解释的。先去优化代码逻辑，如果仍有必要，再添加注释进行澄清。保持注释整洁、简短、直击要害。</p>
<p>如果你想写出整洁、高效且易维护的代码，文档就是关键。别再把文档当成事后才补的作业，或者填补空间的废话。它是代码的延伸——是你进行清晰高效沟通的方式。它是留给后人的路线图，更是你思维过程的体现。</p>
<h2 id="heading-conclusion">结论 🏁</h2>
<p>整洁代码并非可有可无的“加分项”，而是那些志在成为领导者的人的“必选项”。它关乎掌控力、效率，以及长期的持续改进。归根结底，它将助你在敏捷软件开发的博弈中脱颖而出。</p>
<p>🪧 如果你想真正精进自己的技艺，请编写整洁的代码，让效率自己证明一切。</p>
<h2 id="heading-frequently-asked-questions-about-clean-code">关于整洁代码的常见问题 🧯</h2>
<ol>
<li>
<p><strong>什么是干净的代码？</strong> 是指不会让你想把电脑扔出窗外的代码。</p>
</li>
<li>
<p><strong>为什么干净的代码在敏捷中很重要？</strong> 因为敏捷注重速度和变化，凌乱的环境中你不可能快速行动。</p>
</li>
<li>
<p><strong>什么是代码异味？</strong> 是你即将失去对代码库控制的迹象。</p>
</li>
<li>
<p><strong>我该如何改善注释？</strong> 只对必要的部分进行注释，并确保每个注释都增加价值而不是噪音。</p>
</li>
</ol>
<p>感谢您的陪伴。您可以访问我的 <a href="https://x.com/shahancd">Twitter 账号</a> 或 <a href="https://www.codewithshahan.com">我的网站</a> 阅读更多关于整洁代码和敏捷应用程序开发的文章。下次再见……继续改进你的代码库。</p>
<p>如果你认真想掌握整洁代码并提升你的编程职业生涯，我的书就是为你而写的：<a href="https://codewithshahan.gumroad.com/l/cleancode-zero-to-one"><strong>从零到一的整洁代码</strong></a>。这本书是你从零到一掌握整洁代码的完整指南，从杂乱到杰作。我正在提供50%的折扣，使用代码“earlybird”——仅限前50本。此外，还有30天的退款保证——无风险，纯奖励。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 学习 TypeScript：开发者手册 ]]>
                </title>
                <description>
                    <![CDATA[ 这本手册将教你 TypeScript 的基础知识，包括它是什么、它为什么非常有用，以及它提供的关键功能。 TypeScript 是由微软的著名软件工程师 Anders Hejlsberg 创建的，他同时也因为在 C# 和 Delphi 上的贡献而广为人知。TypeScript 的设计目的是通过添加静态类型来增强 JavaScript，使构建和维护大规模应用程序变得更容易。 我们将通过“使用 Vite 将 TypeScript 整合进 React 项目”这件事开始。然后你会学习到诸如类型注释、类型推论、如何处理对象和数组等关键概念。 之后，我们将探讨一些高级的主题，例如联合类型和 any 类型、只读属性、具有特定参数和返回类型的函数、用于灵活和可重用代码的泛型，以及类型别名和接口的不同角色。 我将通过这本手册提供一些详细的示例和解释，帮助你全面理解 TypeScript 的特性是如何改善 JavaScript 开发的。 前提条件 我假设你已经熟悉 JavaScript 和 React 的基础知识。因此在这本手册中，我不会深入解释某些概念，例如在搭建项目时的文件结构。 目录   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-typescript-with-react-handbook/</link>
                <guid isPermaLink="false">681742a21084d10483d33a71</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Sun, 04 May 2025 11:16:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/05/----_20250504202035.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/learn-typescript-with-react-handbook/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learn TypeScript – A Handbook for Developers</a>
      </p><!--kg-card-begin: markdown--><p>这本手册将教你 TypeScript 的基础知识，包括它是什么、它为什么非常有用，以及它提供的关键功能。</p>
<p>TypeScript 是由微软的著名软件工程师 Anders Hejlsberg 创建的，他同时也因为在 C# 和 Delphi 上的贡献而广为人知。TypeScript 的设计目的是通过添加静态类型来增强 JavaScript，使构建和维护大规模应用程序变得更容易。</p>
<p>我们将通过“使用 Vite 将 TypeScript 整合进 React 项目”这件事开始。然后你会学习到诸如类型注释、类型推论、如何处理对象和数组等关键概念。</p>
<p>之后，我们将探讨一些高级的主题，例如联合类型和 any 类型、只读属性、具有特定参数和返回类型的函数、用于灵活和可重用代码的泛型，以及类型别名和接口的不同角色。</p>
<p>我将通过这本手册提供一些详细的示例和解释，帮助你全面理解 TypeScript 的特性是如何改善 JavaScript 开发的。</p>
<h3 id="">前提条件</h3>
<p>我假设你已经熟悉 JavaScript 和 React 的基础知识。因此在这本手册中，我不会深入解释某些概念，例如在搭建项目时的文件结构。</p>
<h2 id="">目录</h2>
<ol>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-what-is-typescript">什么是 TypeScript？</a></p>
</li>
<li>
<p><a href="#heading-setting-up-the-project">项目设置</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-type-annotations-and-type-inference">类型注释和类型推论</a></p>
<ul>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-commonly-used-type-annotations">常用类型注释</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-type-inference">类型推论</a></p>
</li>
</ul>
</li>
<li>
<p><a href="#heading-the-union-and-any-types">联合类型和 Any 类型</a></p>
<ul>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-be-careful-when-using-any-in-typescript">在 TypeScript 中使用 any 时要小心</a></p>
</li>
<li>
<p><a href="#heading-unknown-as-a-safer-alternative-to-any-in-typescript">在 TypeScript 中使用 unknown 作为 any 的更安全替代方案</a></p>
</li>
</ul>
</li>
<li>
<p><a href="#heading-objects-in-typescript">TypeScript 中的对象</a></p>
<ul>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-the-problem-of-mutability">可变性问题</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-readonly-on-object-properties">对象上的只读属性声明</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-readonly-arrays">只读数组</a></p>
</li>
</ul>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-function-params-and-function-returns">函数参数和函数返回值</a></p>
<ul>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-the-risks-of-using-any">使用 any 的风险</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-use-explicit-types-for-parameters-and-return-values">为参数和返回值使用显式类型</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-using-unknown-as-a-safer-alternative-to-any-in-typescript">在 TypeScript 中使用 unknown 作为 any 的更安全替代方案</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-handling-optional-default-in-typescript">在 TypeScript 中处理可选和默认值</a></p>
</li>
</ul>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-rest-parameters">剩余参数</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-objects-as-parameters-in-typescript">TypeScript 中作为参数的对象</a></p>
</li>
<li>
<p><a href="#heading-type-aliases-in-typescript">TypeScript 中的类型别名</a></p>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-what-is-an-intersection-type-in-typescript">在 TypeScript 中的交叉类型是什么？</a></li>
</ul>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-interfaces-in-typescript">TypeScript 中的接口</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-tuples-and-enums">元组和枚举</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-type-assertion-type-unknown-and-type-never-in-typescript">TypeScript 中的类型断言、类型 Unknown 和类型 Never</a></p>
</li>
<li>
<p><a href="#heading-generics-in-typescript">TypeScript 中的泛型</a></p>
</li>
<li>
<p><a href="https://chinese.freecodecamp.org/news/learn-typescript-with-react-handbook/heading-conclusion">写在最后</a></p>
</li>
</ol>
<h2 id="typescript">什么是 TypeScript？</h2>
<p>在深入了解 TypeScript 是什么之前，理解它为什么被创造出来是很重要的。JavaScript 是一种<strong>弱类型的语言</strong>，这意味着变量是在运行时被定义，变量的类型是在运行时被决定的。这种灵活性会导致意想不到的行为，尤其是在较大的项目中。</p>
<p>例如，你可能会意外地将一个错误类型的值分配给一个变量，这会引发一些你只有在执行代码时才会发现的错误。</p>
<p>下面是一个展示此问题的 JavaScript 的示例：</p>
<pre><code class="language-javascript">let userName = "Alice";
userName = 42; // 在赋值时没有错误，但这可能会破坏之后的代码。

function greetUser(name) {
  console.log("Hello, " + name.toUpperCase()); // 如果 `name` 不是字符串，会在运行时报错。
}

greetUser(userName); // 抛出异常，因为 `userName` 是数字而不是字符串，没有toUpperCase方法。
</code></pre>
<p>这类错误对于调试来说是一个巨大的挑战，因为它只在运行时出现。这样的错误使得大型项目更难维护且更容易出现 bug。</p>
<p>这就是 TypeScript 大显身手的地方。TypeScript 是一种通过添加<strong>静态类型</strong>构建在 JavaScript 之上的编程语言。静态类型意味着你可以显式指定变量、函数参数、返回值等的类型。与在运行时确定类型的<strong>动态类型</strong>不同，静态类型允许 TypeScript 在开发过程中提前捕获与类型相关的错误，提高代码质量并减少 bug 。</p>
<p>例如，下面是用 TypeScript 编写的同一代码：</p>
<pre><code class="language-typescript">let userName: string = "Alice";
// userName = 42; // 错误：类型“number”不能分配给类型“string”。

function greetUser(name: string): void {
  console.log("Hello, " + name.toUpperCase());
}

greetUser(userName); // 完美运行，因为 `userName` 类型正确。
</code></pre>
<h2 id="">项目设置</h2>
<p>我们将使用 <a href="https://vite.dev/guide/">Vite</a> 来设置我们的 TypeScript 项目。Vite 是一个旨在为 Web 项目提供更快速和更精简开发体验的现代构建工具。</p>
<p>第一步，运行以下命令创建一个支持 TypeScript 的新 Vite 项目：</p>
<pre><code class="language-shell">npm create vite@latest
</code></pre>
<p>然后为你的项目输入一个名称（你可以选择任何你喜欢的名称）。在接下来的步骤中，请仔细按照说明操作。</p>
<p>选择项目模板时，从可用选项中选择 “React”。我们将在本项目的开发中使用带有 TypeScript 的 React。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736769912180/e94dc70c-32e2-4f9f-89cc-d70d35e3a86e.png" alt="运行 create vite@latest 时的项目模板" width="632" height="311" loading="lazy"></p>
<p>当提示选择变体时，从可用选项中选择 “TypeScript”。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736770059262/d605726e-8d4f-4e73-8fb7-3854ce0b4e72.png" alt="在 create vite@latest 模板中的 TypeScript 变体选择" width="470" height="225" loading="lazy"></p>
<p>完成这些步骤后，系统会提示您切换到项目目录并运行 <code>npm install</code>。您可以选择任何代码编辑器。在本例中，我将使用 VS Code。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736771043869/e3f81f8b-19b7-4fb6-a439-2f24e3f55df5.png" alt="e3f81f8b-19b7-4fb6-a439-2f24e3f55df5" width="577" height="416" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736771426441/4c524149-4557-40bf-b50a-79400c6c3c91.png" alt="在 vscode 中查看项目概况并运行 npm install 以安装项目依赖" width="1366" height="725" loading="lazy"></p>
<p>运行 <code>npm install</code> 后，运行 <code>npm run dev</code> 以在本地服务器上启动项目。一旦该项目启动并运行，我们就可以准备开始学习 TypeScript 概念。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736772238962/36f9523c-d316-43e3-ae05-e1ebfa9398f1.png" alt="运行项目中的 npm run dev 后的登录页面" width="1364" height="699" loading="lazy"></p>
<p>首先，让我们创建第一个 TypeScript 文件，<code>test.ts</code>（您可以选择使用 <code>.ts</code> 或 <code>.tsx</code>）。在项目的 <code>src</code> 文件夹内创建 <code>test.ts</code> 文件，并添加以下代码将测试消息在控制台中打印为日志：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">console.log('Testing our first TypeScript file');
</code></pre>
<p>要在控制台中查看此内容，请将 <code>test.ts</code> 文件导入到位于 <code>src</code> 文件夹中的 <code>main.tsx</code> 文件中。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736773745661/8492e586-7bc0-44a8-ac54-fb576119cdea.png" alt="突出显示 main.tsx 和 test.tsx 文件" width="1366" height="768" loading="lazy"></p>
<p><code>main.tsx</code></p>
<pre><code class="language-typescript">import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import "./test.ts";

createRoot(document.getElementById("root")!).render(
  &lt;StrictMode&gt;
    &lt;App /&gt;
  &lt;/StrictMode&gt;
);
</code></pre>
<p>要在控制台中查看日志，请确保将 <code>test.ts</code> 文件导入到位于 <code>src</code> 文件夹中的 <code>main.tsx</code> 文件中。之后，检查在本地服务器上运行的项目的控制台，您应该会看到显示的日志消息。</p>
<p><strong>Voilà!</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736774231199/9a270631-0639-40e0-84de-513143b4478d.png" alt="在 console.log 中的结果" width="1364" height="649" loading="lazy"></p>
<p>现在，让我们正式开始学习 TypeScript。</p>
<h2 id="">类型注解和类型推论</h2>
<h3 id="">什么是类型注解？</h3>
<p>TypeScript 中的类型注解使您能够显式地指定变量的类型。这确保了变量仅被赋予指定类型的值，增强了类型安全性并使您的代码更易于维护。</p>
<p>要在 TypeScript 中定义类型注解，只需在变量名称后加上一个冒号 <code>:</code>，然后是所需的类型。这允许您指定变量将持有的类型，为您的代码添加一个清晰和精确的层次。例如，让我们在 <code>test.ts</code> 文件中指定一个类型为 <code>string</code> 的变量，以确保只分配字符串值：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let name: string = 'Stephen';
</code></pre>
<p>在这个例子中，我们声明了一个变量 <code>name</code> 并指定它的类型必须是 <code>string</code>。TypeScript 现在将确保只能将字符串值分配给 <code>name</code>。</p>
<p>**📄 注意：**所有代码片段都在一个名为 <code>test.ts</code> 的文件中用于演示目的。您可以根据需要重命名文件或将片段复制到您的 TypeScript 项目中。我在本文中没有遵循一致的文件命名。</p>
<h3 id="">常用的类型注解</h3>
<p>以下是 TypeScript 中一些常用的类型注解：</p>
<ul>
<li>
<p><code>string</code>: 表示文本值。</p>
</li>
<li>
<p><code>number</code>: 表示数值（包括整数和浮点数）。</p>
</li>
<li>
<p><code>boolean</code>: 表示值为 <code>true</code> 或 <code>false</code>。</p>
</li>
<li>
<p><code>any</code>: 一种允许将任何值分配给变量的回退类型，禁用类型检查。</p>
</li>
<li>
<p><code>void</code>: 通常用于不返回值的函数。</p>
</li>
<li>
<p><code>null</code> 和 <code>undefined</code>: 用于表示没有值。</p>
</li>
</ul>
<p>一旦您定义了带有类型注解的变量，TypeScript 会确保它只能持有指定类型的值。您还可以访问与该类型相关的方法。例如，如果您声明了一个字符串变量，TypeScript 就会提供所有字符串方法的访问支持，如 <code>.toUpperCase()</code>。</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let name: string = 'Stephen';  // 类型显式设置为字符串
name = 'John';  // 这没有问题，因为它仍然是一个字符串

// 访问字符串方法
console.log(name.toUpperCase());  // 输出: JOHN
</code></pre>
<p>在这里，变量 <code>name</code> 被重新分配了一个新字符串值 <code>'John'</code>。由于类型仍然是 <code>string</code>，您可以毫无问题地使用字符串方法，比如 <code>.toUpperCase()</code>。</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let numbers: number[] = [1, 2, 3];  // 类型显式设置为数字数组
numbers.push(4);  // 可以的，因为 4 是一个数字

// 访问数组方法
console.log(numbers.length);  // 输出: 4
</code></pre>
<p>在这个例子中，<code>numbers</code> 是一个数字数组。你可以安全地使用数组方法，比如 <code>.push()</code> 和 <code>.length</code>，这些都是对数字数组有效的操作。</p>
<p>如果你尝试将变量重新赋值为不兼容类型的值，TypeScript 会在开发时立即捕获错误，甚至在代码运行之前。</p>
<p>例如：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let name: string = 'Stephen';
name = 2;  // 错误: 类型 '2' 不能赋值给类型 'string'
</code></pre>
<p>在这里，你试图将一个数字 (<code>2</code>) 赋给一个先前声明为字符串的变量。TypeScript 会立即抛出错误，指出不能将一个数字赋给字符串变量。</p>
<p>同样地，对于一个数组：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let numbers: number[] = [1, 2, 3];
numbers = 'Hello';  // 错误: 类型 'string' 不能赋值给类型 'number[]'
</code></pre>
<p>在这里，你试图将一个字符串 (<code>'Hello'</code>) 赋给一个先前声明为数字数组的变量。TypeScript 会捕捉到这个错误并强调类型不匹配。</p>
<p>尝试不同的类型来体验 TypeScript 如何执行类型安全。例如，在你的数组和变量中尝试使用布尔值、数字或其他类型。</p>
<p>既然你已经了解了类型注释如何与字符串和数组配合工作，现在是时候尝试其他类型了。TypeScript 允许你定义具有各种类型的数组和变量，从而确保代码的类型安全性。尝试创建具有其他数据类型的数组，例如 <code>boolean</code>、<code>number</code>。</p>
<h4 id="">示例: 布尔数组</h4>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let booleanArray: Array&lt;boolean&gt; = [true, false, true];

// 访问数组方法
console.log(booleanArray.length);  // 输出: 3
</code></pre>
<p>在这个例子中，数组 <code>booleanArray</code> 明确声明为仅包含 <code>boolean</code> 值。尝试添加 <code>string</code> 或 <code>number</code> 元素，看看 TypeScript 如何捕获类型错误。</p>
<h4 id="">示例: 数字数组</h4>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let numberArray: Array&lt;number&gt; = [1, 2, 3];

// 访问数组方法
console.log(numberArray[0] * 2);  // 输出: 2
</code></pre>
<p>欢迎你试验这些例子，并观察 TypeScript 如何提供强大的类型安全性和实时捕获错误。探索得越多，你就越能理解如何利用 TypeScript 的类型系统编写更整洁、更可靠的代码。</p>
<h3 id="">什么是类型推论？</h3>
<p>TypeScript 中的类型推论是一种强大的功能，它允许 TypeScript 编译器根据赋给变量的值自动确定变量的类型。TypeScript 被设计得足够智能，以在许多情况下推断类型，从而减少显式类型注释的需求。这增强了代码的简洁性，同时保持了类型安全性的优点。</p>
<p>通过类型推论，TypeScript 可以通过分析赋给变量的值来预测变量的类型，确保即使不手动指定类型，也能获得类型检查的所有优势。</p>
<h5 id="1"><strong>示例 1</strong>：推断的字符串类型</h5>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let message = "Hello, TypeScript!";  // TypeScript 推断 'message' 的类型为字符串
console.log(message.toUpperCase());  // 输出: HELLO, TYPESCRIPT!
</code></pre>
<p>在这个例子中，TypeScript 自动推断 <code>message</code> 的类型为 <code>string</code>，因为赋值给它的值是一个字符串。</p>
<h5 id="2"><strong>示例 2</strong>：推断的数字类型</h5>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let count = 42;  // TypeScript 推断 'count' 的类型为数字
console.log(count + 8);  // 输出: 50
</code></pre>
<p>这里，TypeScript 根据值 <code>42</code> 推断 <code>count</code> 的类型为 <code>number</code>，你可以在它上面执行算术操作而不会出现类型错误。</p>
<h5 id="3"><strong>示例 3:</strong> 推断的数组类型</h5>
<p><code>test.ts</code></p>
<pre><code>let numbers = [1, 2, 3];  // TypeScript 推断 'numbers' 为数字数组 (number[])
console.log(numbers.length);  // 输出: 3
</code></pre>
<p>在这种情况下，TypeScript 推断 <code>numbers</code> 为类型 <code>number[]</code> 的数组，因为数组包含数字。</p>
<h4 id=""><strong>不正确的示例:</strong></h4>
<h5 id="4"><strong>示例 4</strong>：类型不匹配的赋值</h5>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let count = 42;  // TypeScript 推断 'count' 的类型为数字
count = "Not a number";  // 错误: 类型 'string' 不能赋值给类型 'number'
</code></pre>
<p>尽管 TypeScript 推断 <code>count</code> 是一个数字，但尝试将一个 <code>string</code> 赋给它会导致一个错误。TypeScript 抓住了这是一个类型不匹配，因为 <code>count</code> 最初被推断为 <code>number</code>。</p>
<h5 id="5"><strong>示例 5:</strong> 推断为混合类型数组</h5>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let mixedArray = [1, "apple", true];  // TypeScript 推断 'mixedArray' 为 (string | number | boolean)[]
console.log(mixedArray[0].toFixed(2));  // 错误: 属性 'toFixed' 不存在于类型 'string | boolean' 上。
</code></pre>
<p>在这个例子中，TypeScript 推断 <code>mixedArray</code> 是一个包含多种类型的数组 (<code>string | number | boolean</code>)。虽然这是允许的，但对元素使用类似 <code>.toFixed()</code> 的方法可能会导致错误，因为并非所有数组元素都支持该方法（例如，<code>boolean</code> 和 <code>string</code> 没有 <code>.toFixed()</code>）。</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let price = 99.99;  // TypeScript 推断 'price' 为一个数字
price = "Free";  // 错误: 不能将类型 'string' 分配给类型 'number'
</code></pre>
<p>在这里，TypeScript 推断 <code>price</code> 是一个 <code>number</code>，但是尝试将其重新赋值为一个 <code>string</code> 会导致类型错误，从而确保变量维持其推断的类型。</p>
<h2 id="any">联合类型和 Any 类型</h2>
<p>在之前的例子中，我们使用了混合类型。现在，让我们正确定义这些概念，并通过各种例子来扩展它们：</p>
<h3 id=""><strong>什么是联合类型？</strong></h3>
<p>联合类型允许变量或参数拥有多种特定类型，提供灵活性的同时保持类型安全性。你可以使用管道符 (<code>|</code>) 来定义一个联合类型。</p>
<p><strong>简单联合类型：</strong></p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let value: string | number;

value = "Hello";  // ✅ 正确
console.log(value.toUpperCase());  // 输出: HELLO

value = 42;  // ✅ 正确
console.log(value + 8);  // 输出: 50

value = true;  // ❌ 错误: 不能将类型 'boolean' 分配给类型 'string | number'.
</code></pre>
<p>在这个例子中，<code>value</code> 可以是字符串或数字。任何其他类型的赋值都会导致类型错误。</p>
<p><strong>函数参数中的联合类型：</strong></p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">function printId(id: string | number): void {
  console.log(`Your ID is: ${id}`);
}

printId(12345);      // ✅ 正确
printId("abc123");   // ✅ 正确
printId(true);       // ❌ 错误: 不能将类型 'boolean' 分配给类型 'string | number'.
</code></pre>
<p>在这里，<code>id</code> 参数只能接受 <code>string</code> 或 <code>number</code>，确保了类型安全性并同时提供了灵活性。</p>
<p><strong>自定义联合类型：</strong></p>
<p>你可以使用 <code>type</code> 关键字创建自定义类型，以提高可读性和可重用性。</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">type ID = string | number;

function getUser(id: ID): void {
  console.log(`Fetching user with ID: ${id}`);
}

getUser(12345);      // ✅ 正确
getUser("abc123");   // ✅ 正确
getUser(true);       // ❌ 错误: 不能将类型 'boolean' 分配给类型 'string | number'.
</code></pre>
<h3 id="any"><strong>什么是</strong> <code>any</code> 类型？</h3>
<p><code>any</code> 类型是 TypeScript 中最灵活的类型。它允许变量持有任何类型的值，并对该变量禁用类型检查。</p>
<p><code>any</code> 类型牺牲了类型安全以获取最大灵活性。在你不确定类型或者处理动态数据时，这非常有用。</p>
<h5 id="1any"><strong>例子 1</strong>：Any 类型数组</h5>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let mixedArray: any[] = [1, "apple", true];

console.log(mixedArray[0]);  // 输出: 1
console.log(mixedArray[1].toUpperCase());  // 输出: APPLE
console.log(mixedArray[2]);  // 输出: true
</code></pre>
<p>在这里，<code>mixedArray</code> 可以包含任何类型的元素而不会触发类型错误。</p>
<h4 id="any"><strong>何时使用联合类型与</strong> <code>any</code>？</h4>
<ul>
<li>
<p><strong>联合类型</strong>：当可能的值已知或限制为几种特定类型时使用联合类型。它提供类型安全并避免了运行时错误。</p>
</li>
<li>
<p><code>any</code> <strong>类型</strong>：作为最后的手段在类型未知或动态时使用 <code>any</code>。</p>
</li>
</ul>
<p>但请记住，过度使用 <code>any</code> 会使 TypeScript 的类型系统的优势丧失。通过谨慎地在联合类型和 <code>any</code> 之间进行选择，你可以编写既灵活又类型安全的 TypeScript 代码。</p>
<h3 id="typescriptany"><strong>在 TypeScript 中使用</strong> <code>any</code> <strong>时要小心</strong></h3>
<p>TypeScript 中的 <code>any</code> 类型是一个强大而又冒险的特性。虽然这种灵活性有时会很有用，但它往往会导致 TypeScript 无法在编译时捕获的意外行为或错误。</p>
<p>让我们通过一个例子来理解这种潜在的陷阱。</p>
<p>这是一个展示滥用any类型带来风险的函数：</p>
<pre><code class="language-typescript">function combineValues(value: any) {
  let anotherValue: number = 10;

  return value + anotherValue;
}

const result = combineValues(5); // 这里没有错误。
const anotherResult = result;

// 试图调用 `anotherResult` 上的方法
anotherResult.someUndefinedMethod(); // 没有编译时错误！
</code></pre>
<p>运行这段代码的时候发生了什么？</p>
<p>首先，我们没有使用 <code>any</code> 时的类型检查。参数 <code>value</code> 是 <code>any</code> 类型，这意味着它可以持有任何值：字符串、数字、对象等等。TypeScript 会跳过对 <code>value</code> 的类型检查。</p>
<p>其次，返回值假设为 <code>any</code>。由于 <code>value</code> 是 <code>any</code>，因此返回类型也被推论为 <code>any</code>。</p>
<p>第三，调用未定义方法时没有错误。函数调用后，<code>anotherResult</code> 也被视为 <code>any</code>。TypeScript 允许在 <code>any</code> 类型的变量上调用任何方法（甚至是不存在的方法）而不报错。在这种情况下，<code>someUndefinedMethod</code> 并不存在，但 TypeScript 不会发出警告。</p>
<h4 id="any"><strong>使用</strong> <code>any</code> <strong>的风险</strong></h4>
<ol>
<li>
<p><strong>类型安全的丧失</strong>：你会失去 TypeScript 类型系统的好处，比如编译时错误检查。潜在的运行时错误在开发过程中可能会被忽视。</p>
</li>
<li>
<p><strong>意外的行为</strong>：函数可能会接受意外的输入（例如：字符串、数组或对象），导致结果不正确或崩溃。</p>
</li>
<li>
<p><strong>调试复杂性</strong>：由于类型未被强制执行，由不正确类型引发的问题的调试变得更加复杂。</p>
</li>
</ol>
<h3 id=""><strong>如何解决</strong></h3>
<h4 id=""><strong>对参数和返回值使用显式类型</strong></h4>
<p>这是一个具有正确类型注释的改进版本：</p>
<pre><code class="language-typescript">function combineValues(value: number): number {
  let anotherValue: number = 10;
  return value + anotherValue;
}
const result = combineValues(5);
// result.someUndefinedMethod(); // 错误：类型 “number” 上不存在属性 “someUndefinedMethod”。
</code></pre>
<ol>
<li>
<p><strong>参数类型</strong>: 该函数现在明确要求 <code>value</code> 参数为 <code>number</code> 类型。</p>
</li>
<li>
<p><strong>返回类型</strong>: 返回类型被声明为 <code>number</code>，确保只返回数字。</p>
</li>
</ol>
<p>这确保了如果您尝试传递无效类型或调用不存在的方法，TypeScript 将会抛出错误。</p>
<h4 id=""><strong>要点</strong></h4>
<ul>
<li>
<p><code>any</code> 类型禁用了 TypeScript 的类型检查，使您的代码容易出现运行时错误。</p>
</li>
<li>
<p>尽可能避免使用 <code>any</code>。在类型无法预先确定的情况下，应当改用显式类型声明或更严格的替代方案（例如 <code>unknown</code> 类型）。</p>
</li>
<li>
<p>明确的类型通过利用 TypeScript 的编译时检查，增强了代码的清晰度、可维护性和可靠性。</p>
</li>
</ul>
<p>如果您因为不清楚类型而有使用 <code>any</code> 的冲动，考虑重构您的代码或结合使用类型守卫与 <code>unknown</code> 以获得更好的安全性。</p>
<h3 id="typescriptunknown">在 TypeScript 中使用 <code>unknown</code> 作为更安全的替代方案</h3>
<p>TypeScript 中的 <code>unknown</code> 类型是 <code>any</code> 的更严格且更安全的替代方案。虽然 <code>any</code> 和 <code>unknown</code> 都可以容纳任意类型的值，但 <code>unknown</code> 要求您在使用值之前执行类型检查。这在提供灵活性的同时确保了更高的类型安全性。</p>
<pre><code class="language-typescript">function processValue(input: unknown): string {
  if (typeof input === 'string') {
    return `这个值是一个字符串：${input}`;
  } else if (typeof input === 'number') {
    return `这个值是一个数字：${input}`;
  } else {
    return '这个值是未知类型';
  }
}

console.log(processValue('Hello, TypeScript!')); // 这个值是一个字符串：Hello, TypeScript!
console.log(processValue(42)); // 这个值是一个数字：42
console.log(processValue(true)); // 这个值是未知类型
</code></pre>
<p>使用 <code>unknown</code> 而不是 <code>any</code> 有几个好处：</p>
<ol>
<li>
<p><strong>类型安全处理</strong>: 与 <code>any</code> 不同，<code>unknown</code> 迫使您在使用变量的值之前进行类型检查。这防止了在意外类型上执行无效操作所引发的运行时错误。</p>
</li>
<li>
<p><strong>显式类型缩小</strong>: TypeScript 要求您先通过类型守卫（<code>typeof</code>、<code>instanceof</code> 等）将 <code>unknown</code> 缩小到特定类型（如 <code>string</code>、<code>number</code>）后，才能访问其属性或方法。</p>
</li>
<li>
<p><strong>增强代码清晰度</strong>: 通过使用 <code>unknown</code>，您向其他开发人员表明类型是不确定的，必须在使用之前检查。</p>
</li>
</ol>
<h3 id="anyvsunknown"><strong>主要区别:</strong> <code>any</code> vs. <code>unknown</code></h3>
<p>| <strong>特性</strong> | <code>any</code> | <code>unknown</code> |<br>
| 类型检查 | 无类型检查 | 使用前需类型检查 |<br>
| 灵活性 | 可直接使用 | 必须先缩小类型 |<br>
| 常见用例 | 快速修复（不推荐） | 安全处理不确定类型 |</p>
<p>总结一下，每当您处理不确定类型的值时，请使用 <code>unknown</code> 而不是 <code>any</code>。它有助于维护类型安全并减少错误风险。同时尽量避免使用 <code>any</code>，因为它会绕过 TypeScript 的安全特性。</p>
<h2 id="typescript">TypeScript 中的对象</h2>
<p>在 TypeScript 中，对象是属性的集合，每个属性都有一个名称（键）和一个值。TypeScript 允许我们为这些属性定义类型，以确保对象符合特定结构。</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let car = { car: 'Toyota', brand: 2024 };
console.log(car);
</code></pre>
<p>这段代码可以正常工作，因为 TypeScript 根据提供的值自动推断出 <code>car</code> 和 <code>brand</code> 的类型。</p>
<h3 id=""><strong>显式对象类型</strong></h3>
<p>当我们需要显式定义对象的结构时，可以使用内联类型注解。这种方式能明确指定每个属性应有的类型，例如：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let carOne: { car: string; brand: number } = { car: 'Evil Spirit', brand: 2025 };
console.log(carOne);
</code></pre>
<p>这确保 <code>carOne</code> 始终拥有一个类型为 <code>string</code> 的 <code>car</code> 属性和一个类型为 <code>number</code> 的 <code>brand</code> 属性。</p>
<p>假设我们想为 <code>carOne</code> 添加一个 <code>color</code> 属性：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">let carOne: { car: string; brand: number } = { car: 'Evil Spirit', brand: 2025, color: 'Black' };
</code></pre>
<p>上面的代码会显示红线，因为 <code>color</code> 不是已定义类型 <code>{ car: string; brand: number }</code> 的一部分。错误可能会像这样：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736933755272/8a3d48dd-3ae0-4769-9e13-fa1f6ca37331.png" alt="8a3d48dd-3ae0-4769-9e13-fa1f6ca37331" width="757" height="40" loading="lazy"></p>
<blockquote>
<p>类型 '{ car: string; brand: number; color: string; }' 无法分配给类型 '{ car: string; brand: number; }'。对象字面量只能指定已知属性，且 'color' 不存在于类型 '{ car: string; brand: number; }' 中。</p>
</blockquote>
<p>类似地，如果您试图将 <code>brand</code> 的类型更改为 <code>string</code>：</p>
<p><code>test.ts</code></p>
<pre><code class="language-typescript">carOne.brand = "2026";
</code></pre>
<p>您将收到另一个错误：</p>
<blockquote>
<p>类型 'string' 无法分配给类型 'number'。</p>
</blockquote>
<p>每次编写完整的对象类型可能会显得重复，尤其是对于拥有许多属性或在多个地方使用相同结构的对象。但不用担心——我会很快介绍<strong>类型别名</strong>，这将使定义和重用对象类型更简单。之后您将了解到如何使用类型别名简化对象类型并使您的代码更简洁。接下来，我们将探索如何在 React 中应用这些概念。</p>
<p>现在，我们要专注于理解基础知识以及 TypeScript 如何强制执行这种处理对象的模式。这就像是掀开引擎盖，窥探 TypeScript 幕后是如何运作的。</p>
<h3 id=""><strong>对象与数组</strong></h3>
<p>在 TypeScript 中，我们经常处理对象数组，其中每个对象都有特定的结构。TypeScript 可以帮助确保数组中的每个对象都符合预期的类型。</p>
<p>想象你在经营一家杂货店，并想要记录你的蔬菜库存。可以这样开始：</p>
<pre><code class="language-typescript">let tomato = { name: 'Tomato', price: 2 };
let potato = { name: 'Potato', price: 1 };
let carrot = { name: 'Carrot' };

let vegetables: { name: string; price: number }[] = [tomato, potato, carrot];
</code></pre>
<p>当 TypeScript 检查这段代码时，它会抛出一个错误，因为 <code>carrot</code> 没有 <code>price</code> 属性。<code>vegetables</code> 数组的每一项的预期类型是 <code>{ name: string; price: number }</code>。由于 <code>carrot</code> 缺少 <code>price</code>，TypeScript 会将其标记为错误。</p>
<blockquote>
<p>类型 '{ name: string; }' 不能赋值给类型 '{ name: string; price: number; }'。类型 '{ name: string; }' 中缺少属性 'price'，但在类型 '{ name: string; price: number; }' 中该属性是必需的。</p>
</blockquote>
<p>如果 <code>price</code> 并不总是已知或需要（例如，也许胡萝卜的价格正在谈判中），可以使 <code>price</code> 属性为可选。可以通过在属性名后添加一个 <code>?</code> 来实现这一点：</p>
<pre><code class="language-typescript">let vegetables: { name: string; price?: number }[] = [tomato, potato, carrot];
</code></pre>
<p>现在，TypeScript 知道 <code>price</code> 属性是可选的。这意味着在 <code>vegetables</code> 数组中的对象可以包含 <code>price</code> 或不包含 <code>price</code> 而不产生错误。</p>
<p>当一个属性是可选的时，TypeScript 允许它：</p>
<ol>
<li>
<p>以指定的类型存在。</p>
</li>
<li>
<p>完全不存在。</p>
</li>
</ol>
<p>这种灵活性消除了对于像 <code>carrot</code> 这样缺少 <code>price</code> 属性的对象的错误。</p>
<h3 id="readonly"><strong><code>readonly</code> 修饰符</strong></h3>
<p>在 TypeScript 中，<code>readonly</code> 修饰符是确保某些属性或整个对象保持不变的好方法。这在你想防止数据的意外更改时特别有用。</p>
<p>让我们继续用蔬菜商店的例子来看看 <code>readonly</code> 是如何工作的。</p>
<h4 id=""><strong>可变性的问题</strong></h4>
<p>假设我们有这样的设置：</p>
<pre><code class="language-typescript">let tomato = { name: 'Tomato', price: 2 };
let potato = { name: 'Potato', price: 1 };
let carrot = { name: 'Carrot' };

let vegetables: { name: string; price?: number }[] = [tomato, potato, carrot];
</code></pre>
<p>如果有人不小心尝试更改 <code>tomato</code> 对象的 <code>name</code> 或从 <code>vegetables</code> 数组中删除 <code>carrot</code> 对象，TypeScript 不会报错：</p>
<pre><code class="language-typescript">vegetables[0].name = 'Cucumber'; // 没有错误，但这可能是无意的！
vegetables.pop(); // 移除最后一个蔬菜，没有警告。
</code></pre>
<p>我们可以使用 <code>readonly</code> 来使这些对象和数组不可变，以确保它们的原始状态不能被更改。</p>
<h3 id="readonly"><strong>对象属性上的 Readonly</strong></h3>
<p>要使每个蔬菜的属性不可变，可以这样做：</p>
<pre><code class="language-typescript">let vegetables: { readonly name: string; readonly price?: number }[] = [
  { name: 'Tomato', price: 2 },
  { name: 'Potato', price: 1 },
  { name: 'Carrot' },
];
</code></pre>
<p>现在，如果你尝试更改任何蔬菜的 <code>name</code> 或 <code>price</code>，TypeScript 会抛出错误：</p>
<pre><code class="language-typescript">vegetables[0].name = 'Cucumber'; // 错误：不能分配给 'name'，因为它是一个只读属性。
</code></pre>
<h3 id="readonly"><strong>Readonly 数组</strong></h3>
<p>你还可以通过将整个 <code>vegetables</code> 数组声明为 <code>readonly</code> 来使其不可变：</p>
<pre><code class="language-typescript">let vegetables: readonly { name: string; price?: number }[] = [
  { name: 'Tomato', price: 2 },
  { name: 'Potato', price: 1 },
  { name: 'Carrot' },
];
</code></pre>
<p>这会阻止修改数组本身的操作，例如 <code>push</code>、<code>pop</code> 或 <code>splice</code>：</p>
<pre><code class="language-typescript">vegetables.push({ name: 'Onion', price: 3 }); // 错误：属性 'push' 在类型 'readonly { name: string; price?: number; }[]' 上不存在。
vegetables.pop(); // 错误：属性 'pop' 在类型 'readonly { name: string; price?: number; }[]' 上不存在。
</code></pre>
<h3 id="readonly"><strong>使用 <code>readonly</code> 的时机</strong></h3>
<ol>
<li>
<p><strong>不可变数据</strong>：当你希望对对象或数组实现不可变性时使用 <code>readonly</code>，特别是在数据应该保持不变的情况下（例如，配置、初始状态、常量）。</p>
</li>
<li>
<p><strong>防止错误</strong>：保护你的数据不被代码的其他部分无意中更改。</p>
</li>
</ol>
<h3 id=""><strong>完整示例</strong></h3>
<p>这是一个带有 <code>readonly</code> 的更新示例：</p>
<pre><code class="language-typescript">let vegetables: readonly { readonly name: string; readonly price?: number }[] = [
  { name: 'Tomato', price: 2 },
  { name: 'Potato', price: 1 },
  { name: 'Carrot' },
];

// 尝试修改数据
vegetables[0].name = 'Cucumber'; // 错误：不能分配给 'name'，因为它是一个只读属性。
vegetables.pop(); // 错误：方法 'pop' 在类型 'readonly { readonly name: string; readonly price?: number; }[]' 上不存在。

console.log(vegetables);
</code></pre>
<p>以下是对 readonly 的总结：</p>
<ul>
<li>
<p>属性上的 <code>readonly</code> 确保对象的各个字段不能被更改。</p>
</li>
<li>
<p>数组上的 <code>readonly</code> 使数组本身不可变，阻止诸如 <code>push</code> 和 <code>pop</code> 之类的操作。</p>
</li>
<li>
<p>将两者结合可以为数组中的对象提供完全的不可变性。</p>
</li>
</ul>
<p>通过使用 <code>readonly</code>，你可以创建更安全、更可预测的代码，减少由于无意更改导致的错误。</p>
<h2 id=""><strong>函数参数与函数返回值</strong></h2>
<p>在 TypeScript 中，函数允许您显式定义<strong>参数</strong>和<strong>返回类型</strong>。这确保函数按预期运行并避免运行时错误。让我们通过一个简单的例子来详细说明。</p>
<h3 id=""><strong>推断的返回类型</strong></h3>
<pre><code class="language-typescript">function arithmeticOp(price: number) {
  return price * 9;
}

const FP = arithmeticOp(2); // 结果是 18。
</code></pre>
<ol>
<li>
<p>参数 <code>price</code> 被显式定义为 <code>number</code>。</p>
</li>
<li>
<p>返回类型没有被显式声明，但 TypeScript <strong>推断</strong>它为 <code>number</code>，因为函数返回的是 <code>price * 9</code>，这是一个数值运算。</p>
</li>
</ol>
<p>TypeScript 足够聪明，可以根据返回语句推断函数的返回类型。在这种情况下，它正确地推断 <code>arithmeticOp</code> 返回一个 <code>number</code>。</p>
<h3 id=""><strong>显式返回类型</strong></h3>
<pre><code class="language-typescript">function arithmeticOp(price: number): number {
  return price * 9;
}

const FP = arithmeticOp(2); // 结果仍然是 18。
</code></pre>
<ol>
<li>
<p>函数通过语法 <code>functionName(parameters): returnType</code> 显式声明返回类型为 <code>number</code>。</p>
</li>
<li>
<p>这不会改变结果，但使函数声明更加清晰。</p>
</li>
</ol>
<p>那么为什么要使用显式返回类型呢？首先，这提高了代码的可读性，并确保未来的更改不会意外改变返回类型。其次，它相当于为其他开发者提供了文档说明。</p>
<h3 id=""><strong>返回类型不匹配</strong></h3>
<pre><code class="language-typescript">function arithmeticOp(price: number): number {
  if (hasDiscount) {
    return 'discount'; // 这里出错了！
  }
  return price * 9;
}

const FP = arithmeticOp(2);
</code></pre>
<p>在上面的代码中，返回类型被显式声明为 <code>number</code>。但函数尝试在某些情况下返回一个 <code>string</code>（<code>'discount'</code>）。这导致 TypeScript 抛出错误：</p>
<blockquote>
<p>类型 'string' 不能赋值给类型 'number'。</p>
</blockquote>
<p>这是因为 TypeScript 强制执行声明的返回类型。如果您声明一个函数返回 <code>number</code>，它<strong>必须始终</strong>返回一个 <code>number</code>，无论函数内的逻辑如何。</p>
<p>如果您希望函数返回多种类型（例如，<code>number</code> 或 <code>string</code>），请使用<strong>联合类型</strong>：</p>
<pre><code class="language-typescript">function arithmeticOp(price: number): number | string {
  if (hasDiscount) {
    return 'discount'; // 现在有效！
  }
  return price * 9;
}

const FP = arithmeticOp(2);
</code></pre>
<p>返回类型 <code>number | string</code> 告诉 TypeScript 函数可以返回 <code>number</code> 或 <code>string</code>。这解决了类型不匹配错误。</p>
<h4 id="">关键要点：</h4>
<ol>
<li>
<p>TypeScript 在未显式定义时<strong>推断</strong>返回类型，但为了清晰和可维护性，鼓励使用显式返回类型。</p>
</li>
<li>
<p>声明的返回类型确保函数仅返回指定类型的值。</p>
</li>
<li>
<p>类型不匹配，例如从预期返回 <code>number</code> 的函数返回 <code>string</code>，会导致 TypeScript 错误。</p>
</li>
<li>
<p>联合类型（<code>type1 | type2</code>）允许函数在需要时返回多种类型。</p>
</li>
</ol>
<h3 id="typescript"><strong>在 TypeScript 中处理可选参数和默认值</strong></h3>
<p>在使用 TypeScript 的函数时，指定参数的行为对于灵活性和防止运行时错误至关重要。让我们通过实际的例子来探讨如何有效处理可选参数和默认参数。</p>
<h3 id="1">示例 1：理解缺少参数的问题</h3>
<p>思考下面这个函数：</p>
<pre><code class="language-typescript">function calculateFinalScore(baseScore: number, deductions: number): number {
  return baseScore - deductions;
}

let scoreWithDeductions = calculateFinalScore(50, 10);
let scoreWithoutDeductions = calculateFinalScore(50); // 错误
</code></pre>
<p>对 <code>calculateFinalScore</code> 的第一次调用完全正常。但第二次调用会抛出 TypeScript 错误：</p>
<pre><code class="language-typescript">⚠ Error (TS2554) | 预期有2个参数，但只传入了1个。
Tutorial.ts(7, 47): 参数 'deductions' 没有提供。
</code></pre>
<p>这是因为 TypeScript 期望同时提供 <code>baseScore</code> 和 <code>deductions</code>，因为它们都是必需参数。如果省略 <code>deductions</code> 值，TypeScript 将不允许函数调用。</p>
<h3 id="2">示例 2：使用默认参数解决问题</h3>
<p>为了解决这个问题，我们可以为 <code>deductions</code> 参数定义一个默认值。默认参数在没有传递参数时提供回退值。</p>
<pre><code class="language-typescript">function calculateFinalScore(baseScore: number, deductions: number = 0): number {
  return baseScore - deductions;
}

let scoreWithDeductions = calculateFinalScore(50, 10); // 40
let scoreWithoutDeductions = calculateFinalScore(50);  // 50
</code></pre>
<p>在这个更新的例子中：</p>
<ul>
<li>
<p>如果没有显式提供 <code>deductions</code> 参数，其默认值将设为 <code>0</code>。</p>
</li>
<li>
<p>两次调用现在都可以正常执行且没有错误。</p>
</li>
</ul>
<h3 id="">为什么这种解决方案有效</h3>
<p>通过将 <code>deductions</code> 定义为默认参数，TypeScript 确保函数在调用时拥有执行所需的所有参数，即使某些参数在调用中被省略。这种方法增加了函数的灵活性，同时保持了类型安全。</p>
<p>当一个参数值是函数正常工作所必需的，请使用默认参数，这能确保它在被忽略时可以安全地有回退值。这种方法提高了代码清晰度并减少了运行时错误的可能性。</p>
<p>TypeScript中的剩余参数可以让你在不知道将会收到多少个参数的情况下处理多个参数。你可以传递任意多的参数，TypeScript将处理它们。对于输入数量不固定的情况，它们非常完美。</p>
<p>要使用剩余参数，你需要在参数名称前加上三个点（<code>...</code>），这些点会将所有额外的参数收集到一个数组中。</p>
<p>假设你想将多个单词组合成一个句子：</p>
<pre><code class="language-typescript">function joinWords(...words: string[]): string {
  return words.join(" ");
}

let sentence = joinWords("TypeScript", "makes", "coding", "fun");
console.log(sentence); // "TypeScript makes coding fun"
</code></pre>
<ul>
<li>
<p><code>...words</code> 将所有参数收集到一个数组中（<code>["TypeScript", "makes", "coding", "fun"]</code>）。</p>
</li>
<li>
<p><code>join</code> 方法将它们组合成一个用空格分隔的字符串。</p>
</li>
</ul>
<h3 id="">数字的剩余参数</h3>
<p>现在，假设你想累加多个数字：</p>
<pre><code class="language-typescript">function sumNumbers(...numbers: number[]): number {
  return numbers.reduce((total, num) =&gt; total + num, 0);
}

let total = sumNumbers(10, 20, 30);
console.log(total); // 60
</code></pre>
<ul>
<li>
<p><code>...numbers</code> 将所有数字收集到一个数组中（<code>[10, 20, 30]</code>）。</p>
</li>
<li>
<p><code>reduce</code> 方法将它们相加得到总和。</p>
</li>
</ul>
<p>我们也可以使用剩余参数将多个数组合并为一个：</p>
<pre><code class="language-typescript">function mergeArrays(...arrays: number[][]): number[] {
  return arrays.flat();
}

let combined = mergeArrays([1, 2], [3, 4], [5, 6]);
console.log(combined); // [1, 2, 3, 4, 5, 6]
</code></pre>
<ul>
<li>
<p><code>...arrays</code> 将每个参数作为数组收集到一个数组的数组中（<code>[[1, 2], [3, 4], [5, 6]]</code>）。</p>
</li>
<li>
<p><code>flat</code> 方法将它们合并为一个数组。</p>
</li>
</ul>
<p>剩余参数必须始终放在参数列表的最后。例如：</p>
<pre><code class="language-typescript">function example(a: string, ...others: number[]): void {
  console.log(a, others);
}
</code></pre>
<p>这确保所有剩余的参数都进入剩余参数。</p>
<h2 id="typescript">TypeScript中的对象作为参数</h2>
<p>在TypeScript中，函数可以接受对象作为参数。这在处理多个相关值时特别有用。</p>
<h3 id="">使用具有特定属性的对象</h3>
<p>这是一个接受具有<code>id</code>属性的对象并返回一个新对象的函数：</p>
<pre><code class="language-typescript">function createEmployee({ id }: { id: number }): { id: number; isActive: boolean } {
  return { id, isActive: id % 2 === 0 };
}

const firstEmployee = createEmployee({ id: 1 });
console.log(firstEmployee); // { id: 1, isActive: false }

const secondEmployee = createEmployee({ id: 2 });
console.log(secondEmployee); // { id: 2, isActive: true }
</code></pre>
<p>函数 <code>createEmployee</code>：</p>
<ul>
<li>
<p>接受具有单个属性<code>id</code>的对象作为参数。</p>
</li>
<li>
<p>返回一个具有两个属性的对象：<code>id</code> 和 <code>isActive</code>。</p>
</li>
</ul>
<p><code>isActive</code> 属性的值是通过检查 <code>id</code> 是否为偶数来确定的（<code>id % 2 === 0</code>）。</p>
<p>参数中使用了 <strong>解构</strong> 的语法（参见 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring">解构</a>）：</p>
<ul>
<li>返回值中的 <code>id</code> 属性直接提取自输入对象 <code>{ id }</code> 中的 <code>id</code> 属性。</li>
</ul>
<h3 id="">接受更复杂的对象</h3>
<p>现在，让我们看看接受具有多属性对象的函数：</p>
<pre><code class="language-typescript">function createStudent(student: { id: number; name: string }): void {
  console.log(`Welcome to the course, ${student.name}!`);
}

const newStudent = { id: 1, name: "John" };
createStudent(newStudent); // "Welcome to the course, John!"
</code></pre>
<p>函数 <code>createStudent</code>：</p>
<ul>
<li>
<p>接受一个具有两个属性的对象：<code>id</code> 和 <code>name</code>。</p>
</li>
<li>
<p>使用 <code>name</code> 属性记录欢迎信息。</p>
</li>
</ul>
<p><code>newStudent</code> 对象与函数期望的结构匹配，因此可以直接传递。</p>
<h3 id="">为什么使用对象作为参数？</h3>
<p>首先，使用对象作为参数的函数更易于阅读，尤其是在处理多个相关值时。此外，利用解构，你可以从对象中提取所需的属性，使代码更加简洁。最后，对象可以在多个函数之间复用，而无需每次都创建新对象。</p>
<h3 id="typescript">TypeScript中的多余属性检查</h3>
<p>在TypeScript中，多余属性检查有助于确保传递给函数的对象只能包含定义在函数参数类型中的属性。如果有额外的属性，TypeScript将引发错误。让我们通过简单的例子来看看它是如何工作的。</p>
<h4 id="1">1. 额外属性错误</h4>
<p>这是一个接受具有 <code>id</code> 和 <code>name</code> 对象的函数，但没有额外属性：</p>
<pre><code class="language-typescript">function createStudent(student: { id: number; name: string }): void {
  console.log(`Welcome, ${student.name}!`);
}

const newStudent = { id: 1, name: "John", age: 20 }; // 多余属性 'age'

createStudent(newStudent); // 错误: 'age' 是不被期望的
</code></pre>
<p>TypeScript 会报错，因为 <code>age</code> 属性不属于预期的对象结构。</p>
<h4 id="2">2. 修复错误</h4>
<p>要避免此错误，只需删除任何额外的属性：</p>
<pre><code class="language-typescript">const validStudent = { id: 1, name: "John" };
createStudent(validStudent); // 没有问题
</code></pre>
<p>这可行是因为对象只有预期的属性：<code>id</code> 和 <code>name</code>。</p>
<h4 id="3">3. 使用类型断言（不推荐）</h4>
<p>如果你确实需要传递一个带有额外属性的对象，可以使用<strong>类型断言</strong> 告诉 TypeScript 忽略多余的属性：</p>
<pre><code class="language-typescript">const studentWithExtras = { id: 1, name: "John", age: 20 };
createStudent(studentWithExtras as { id: number; name: string }); // 绕过该错误
</code></pre>
<p>虽然这样做可以工作，但最好是匹配预期的结构，而不是使用类型断言。</p>
<ul>
<li>
<p>TypeScript 期望对象与参数类型的精确结构匹配。</p>
</li>
<li>
<p>多余的属性会导致错误，以确保结构的正确性。</p>
</li>
<li>
<p>如果需要额外的属性，请修复对象或（谨慎）使用类型断言。</p>
</li>
</ul>
<p>针对作为函数参数的对象属性中多余属性的检查可以帮助保持代码安全，并确保只有正确的数据传递到函数中。</p>
<h2 id="typescript">TypeScript 中的类型别名</h2>
<p>TypeScript 中的<strong>类型别名</strong>本质上是现有类型的<strong>缩写</strong>或<strong>替代名称</strong>。它允许您为在代码中可能会使用的复杂类型或反复使用的类型定义一个更简单或更易读的名称。</p>
<p>使用类型别名不会创建新类型，而是为现有类型提供一个新的标识符。使用类型别名时，代码的原有功能逻辑不会改变——它只是使代码更具可读性和可重用性。</p>
<p>下面是使用类型别名前的示例：</p>
<pre><code class="language-typescript">function getUserInfo(user: { name: string; age: number; address: string }) {
  console.log(`User Info: 
    Name: ${user.name}, 
    Age: ${user.age}, 
    Address: ${user.address}`);
}

const user: UserInfo = { name: 'Alice', age: 30, address: '123 Main St' };

getUserInfo(user);
</code></pre>
<p>现在，让我们对函数参数使用类型别名，使代码更具可读性：</p>
<pre><code class="language-typescript">// 使用类型别名
type UserInfo = { name: string, age: number, address: string };

function getUserInfo(user: UserInfo) {
  console.log(`User Info: 
    Name: ${user.name}, 
    Age: ${user.age}, 
    Address: ${user.address}`);
}

const user: UserInfo = { name: 'Alice', age: 30, address: '123 Main St' };

getUserInfo(user);
</code></pre>
<p>在上面的示例中：</p>
<ul>
<li>
<p>使用类型别名前，我们在函数内分别定义了参数。</p>
</li>
<li>
<p>定义类型别名（<code>UserInfo</code>）后，我们在函数参数中使用它，使函数签名更简单，更具可读性。</p>
</li>
</ul>
<p>使用类型别名<strong>不会改变代码的功能</strong>。它只是通过使用别名，使处理代码变得更容易。别名作为复杂类型的可重用引用，如果 <code>UserInfo</code> 的结构发生变化，我们只需在一个地方更新它，从而使代码更易维护。</p>
<h3 id="">如何使用类型别名</h3>
<p>类型别名允许您为一种类型定义一个新名称。这个新名称可以表示基本类型、对象结构，甚至是类型的联合。主要好处是使您的代码更具可读性、可重用性，并防止错误。</p>
<p>您可以使用 <code>type</code> 关键字定义类型别名，后跟一个名称和类型结构。</p>
<pre><code class="language-typescript">type TypeName = TypeStructure;
</code></pre>
<p>例如，让我们为一个用户对象创建一个类型别名：</p>
<pre><code class="language-typescript">type User = {
  name: string;
  age: number;
}
</code></pre>
<p>这意味着 <code>User</code> 是一个期待具有两个属性的对象的类型：</p>
<ul>
<li>
<p><code>name</code> 应该是一个字符串。</p>
</li>
<li>
<p><code>age</code> 应该是一个数字。</p>
</li>
</ul>
<h3 id="">为什么使用类型别名？</h3>
<p>使用类型别名有几个原因。首先，类型别名显式定义了对象的结构，因此任何阅读代码的人都确切知道会得到什么。其次，您可以在代码中的任何地方重用 <code>User</code> 类型，而无需重复其结构。最后，TypeScript 将检查分配给 <code>User</code> 类型的任何对象是否具有必需属性以及正确的类型。</p>
<h4 id="">使用类型别名：</h4>
<pre><code class="language-typescript">type User = {
  name: string;
  age: number;
};

function getUserDetails(user: User): string {
  return `${user.name} (${user.age} years old)`;
}

const user: User = { name: "Alice", age: 30 };
console.log(getUserDetails(user)); // "Alice (30 years old)"
</code></pre>
<p>在这个示例中，我们定义了 <code>User</code> 类型别名，指定 <code>user</code> 对象必须有一个 <code>name</code> 类型为 <code>string</code> 和 <code>age</code> 类型为 <code>number</code>。</p>
<p>如果您试图分配一个不匹配此结构的对象，TypeScript 会捕获错误，如下所示：</p>
<pre><code class="language-typescript">// 这将导致 TypeScript 错误：
const invalidUser: User = { name: "Alice" }; // 缺少 'age' 属性
</code></pre>
<h3 id="typescript">什么是 TypeScript 中的<strong>交叉类型</strong>？</h3>
<p><strong>交叉类型</strong>是 TypeScript 中一个强大的功能，允许您将多种类型组合成一个。当您创建交叉类型时，生成的类型必须同时具备每个交叉类型的<strong>所有属性</strong>。</p>
<p>您可以组合任意数量的类型，生成的类型必须满足所有原始类型的每一种条件。</p>
<h4 id="">交叉类型的语法</h4>
<p>要定义一个交叉类型，您可以使用 <code>&amp;</code> 运算符来组合两个或更多的类型。</p>
<pre><code class="language-typescript">type TypeA &amp; TypeB;
</code></pre>
<h4 id="">交叉类型的示例</h4>
<p>假设您想为 <code>User</code> 类型扩展包含用户的地址。您可以使用<strong>交叉类型</strong>组合 <code>User</code> 和 <code>Address</code>，而不是修改原始 <code>User</code> 类型。</p>
<pre><code class="language-typescript">type Address = {
  city: string;
  country: string;
};

type UserWithAddress = User &amp; Address; // User 和 Address 的交叉
</code></pre>
<p>现在，<code>UserWithAddress</code> 将需要同时具备 <code>User</code> 和 <code>Address</code> 的属性。</p>
<h4 id="">在函数中使用交叉类型的示例</h4>
<p>以下是如何在函数中使用此方法：</p>
<pre><code class="language-typescript">type User = {
  name: string;
  age: number;
};

type Address = {
  city: string;
  country: string;
};

type UserWithAddress = User &amp; Address;

function getUserDetails(user: UserWithAddress): string {
  return `${user.name} (${user.age} years old), lives in ${user.city}, ${user.country}`;
}

const user: UserWithAddress = {
  name: "Alice",
  age: 30,
  city: "New York",
  country: "USA"
};

console.log(getUserDetails(user));
// 输出: "Alice (30 years old), lives in New York, USA"
</code></pre>
<p>在此示例中：</p>
<ul>
<li>
<p><code>UserWithAddress</code> 是一个交叉类型，这意味着 <code>user</code> 对象必须同时拥有 <code>User</code> 和 <code>Address</code> 的属性。</p>
</li>
<li>
<p>TypeScript 会检查对象中是否存在 <code>name</code> 和 <code>age</code>（来自 <code>User</code>），以及 <code>city</code> 和 <code>country</code>（来自 <code>Address</code>）。</p>
</li>
</ul>
<p>如果遗漏了这些属性中的任意一个，TypeScript 将显示错误。</p>
<pre><code>// 这会导致 TypeScript 错误：
const incompleteUser: UserWithAddress = {
  name: "Alice",
  age: 30,
  city: "New York"
}; // 缺少 'country'
</code></pre>
<h3 id="">为什么使用<strong>交叉类型</strong>？</h3>
<p>交叉类型在多个场景下非常有用。首先，它们允许你在不修改原有类型的情况下扩展其功能，从而让代码更具模块化和灵活性。当需要将多个不同的结构合并为一个时，例如将 <code>User</code> 与 <code>Address</code> 或 <code>OrderDetails</code> 合并，使用交叉类型也是非常有用的。而使用交叉类型时，你可以轻松地看到对象必须具有的所有必需属性。</p>
<h3 id="vs">类型别名 vs 交叉类型：</h3>
<p>| 特性 | 类型别名 | 交叉类型 |<br>
| <strong>定义</strong> | 定义单一类型。 | 将多个类型合并为一个类型。 |<br>
| <strong>使用场景</strong> | 为对象或原始数据创造可重用类型。 | 合并多个类型，并要求所有属性存在。 |<br>
| <strong>合并类型</strong> | 不用于合并类型。 | 用于合并多种类型。 |<br>
| <strong>示例</strong> | <code>type User = { name: string, age: number };</code> | <code>type UserWithAddress = User &amp; Address;</code> |</p>
<h3 id="">何时使用类型别名或交叉类型</h3>
<ul>
<li>
<p>当需要为对象、函数或其他数据结构定义<strong>单一类型</strong>时，使用类型别名。它们有助于提升代码的清晰度、重用性和类型安全。</p>
</li>
<li>
<p>当需要将多个类型<strong>合并为一个</strong>时，使用交叉类型。在对象需要同时满足多种合同时理想，比如合并不同的类型或扩展现有类型的功能。</p>
</li>
</ul>
<p>通过在 TypeScript 中运用类型别名和交集类型，你的代码将变得更易于理解、更安全且更易于维护。这些功能为你的数据提供了结构，帮助尽早捕获错误。</p>
<h2 id="typescript">TypeScript 中的接口</h2>
<p>在 TypeScript 中，<strong>接口</strong>是一种定义对象结构、描述其属性及其类型的方法。接口用于在代码中强制执行类型检查，确保对象遵循特定的结构。类似于类型别名，接口使你的代码更具可读性、可重用性和可维护性。</p>
<h3 id="">什么是接口？</h3>
<p>接口是对象的蓝图，定义了它应有的属性和方法。接口可用于为对象、函数或类定义自定义类型。</p>
<p>这是一个基本示例：</p>
<pre><code class="language-typescript">interface User {
  name: string;
  age: number;
  address: string;
}

function getUserInfo(user: User): string {
  return `${user.name} (${user.age} years old) lives at ${user.address}`;
}

const user: User = {
  name: "Alice",
  age: 30,
  address: "123 Main St",
};

console.log(getUserInfo(user)); // 输出: Alice (30 years old) lives at 123 Main St
</code></pre>
<p>在这个示例中：</p>
<ul>
<li>
<p><code>User</code> 接口定义了对象的结构。</p>
</li>
<li>
<p>任何 <code>User</code> 类型的对象必须具有 <code>name</code>，<code>age</code> 和 <code>address</code> 属性且符合指定类型。</p>
</li>
<li>
<p><code>getUserInfo</code> 函数确保 <code>user</code> 参数遵循 <code>User</code> 接口。</p>
</li>
</ul>
<h3 id="">接口和类型别名的相似之处</h3>
<ul>
<li>
<p>接口和类型别名都可以定义对象的结构。</p>
</li>
<li>
<p>两者都可以扩展，尽管语法不同。</p>
</li>
<li>
<p>两者都提高了代码的可读性和可重用性。</p>
</li>
<li>
<p>在大多数情况下，可以互换使用接口或类型别名来定义对象类型。</p>
</li>
</ul>
<p>使用类型别名的示例：</p>
<pre><code class="language-typescript">type User = {
  name: string;
  age: number;
  address: string;
};

const user: User = {
  name: "Bob",
  age: 25,
  address: "456 Elm St",
};
</code></pre>
<p>在这种情况下，<code>type</code> 和 <code>interface</code> 实现了同样的结果。</p>
<h3 id="">接口和类型别名的区别</h3>
<p>让我们总结一下它们的主要区别：</p>
<table>
<thead>
<tr>
<th>特性</th>
<th>接口</th>
<th>类型别名</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>语法</strong></td>
<td>使用 <code>interface</code> 关键字。</td>
<td>使用 <code>type</code> 关键字。</td>
</tr>
<tr>
<td><strong>扩展性</strong></td>
<td>可以使用 <code>extends</code> 扩展。</td>
<td>可以使用交集（<code>&amp;</code>）扩展。</td>
</tr>
<tr>
<td><strong>声明合并</strong></td>
<td>支持在多个声明中合并。</td>
<td>不支持声明合并。</td>
</tr>
<tr>
<td><strong>联合类型</strong></td>
<td>不能定义联合类型。</td>
<td>可以定义联合类型。</td>
</tr>
</tbody>
</table>
<h3 id="">使用接口和类型别名进行扩展</h3>
<p><strong>扩展接口：</strong></p>
<pre><code class="language-typescript">interface Address {
  city: string;
  country: string;
}

interface User extends Address {
  name: string;
  age: number;
}

const user: User = {
  name: "Alice",
  age: 30,
  city: "New York",
  country: "USA",
};
</code></pre>
<pre><code class="language-typescript">type Address = {
  city: string;
  country: string;
};

type User = {
  name: string;
  age: number;
} &amp; Address;

const user: User = {
  name: "Alice",
  age: 30,
  city: "New York",
  country: "USA",
};
</code></pre>
<p>两种方法得到相同的结果，但语法不同。</p>
<h3 id="">使用接口的高级概念</h3>
<p><strong>1. 可选属性:</strong></p>
<p>接口可以使用 <code>?</code> 符号定义属性为可选：</p>
<pre><code class="language-typescript">interface User {
  name: string;
  age?: number; // 可选
}

const user1: User = { name: "Alice" };
const user2: User = { name: "Bob", age: 25 };
</code></pre>
<p><strong>2. 只读属性:</strong></p>
<p>使用 <code>readonly</code> 修饰符使属性不可变：</p>
<pre><code class="language-typescript">interface User {
  readonly id: number;
  name: string;
}

const user: User = { id: 1, name: "Alice" };
// user.id = 2; // 错误: 不能分配给 'id' 因为它是只读属性。
</code></pre>
<p><strong>3. 函数类型:</strong></p>
<p>接口可以定义函数签名：</p>
<pre><code class="language-typescript">interface Add {
  (a: number, b: number): number;
}

const add: Add = (a, b) =&gt; a + b;
console.log(add(5, 3)); // 输出: 8
</code></pre>
<p><strong>4. 索引签名:</strong></p>
<p>接口可以定义动态属性名：</p>
<pre><code class="language-typescript">interface StringDictionary {
  [key: string]: string;
}

const dictionary: StringDictionary = {
  hello: "world",
  name: "Alice",
};
</code></pre>
<p><strong>5. 扩展多个接口:</strong></p>
<p>一个接口可以扩展多个接口：</p>
<pre><code class="language-typescript">interface A {
  propA: string;
}

interface B {
  propB: number;
}

interface C extends A, B {
  propC: boolean;
}

const obj: C = {
  propA: "Hello",
  propB: 42,
  propC: true,
};
</code></pre>
<h3 id="">接口与类型别名的使用时机</h3>
<ul>
<li>
<p>当你需要定义对象形状，尤其是当你计划扩展它们时，使用 <strong>接口</strong> 。如果你需要声明合并，也使用接口，因为类型别名不支持它。</p>
</li>
<li>
<p><strong>类型别名</strong> 常用于更复杂的类型，例如联合或交叉。</p>
</li>
</ul>
<h2 id="">元组和枚举</h2>
<p>在 TypeScript 中， <strong>元组</strong> 是一种特殊类型的数组，它具有固定数量的元素，其中每个元素可以有不同的类型。元组确保值的顺序和类型保持一致。</p>
<pre><code class="language-typescript">// 一个包含字符串和数字的元组
let user: [string, number] = ["Alice", 25];

console.log(user[0]); // 输出: Alice
console.log(user[1]); // 输出: 25
</code></pre>
<p>在这个例子中，元组 <code>user</code> 包含一个字符串（名字）和一个数字（年龄）。顺序和类型必须按定义遵循。</p>
<h4 id=""><strong>包含可选元素的元组:</strong></h4>
<pre><code class="language-typescript">let person: [string, number, boolean?] = ["Bob", 30];

console.log(person); // 输出: ["Bob", 30]
</code></pre>
<p>这里，第三个元素（boolean）是可选的。</p>
<h4 id=""><strong>只读属性的元组:</strong></h4>
<pre><code class="language-typescript">const coordinates: readonly [number, number] = [10, 20];

// coordinates[0] = 50; // 错误: 不能分配给 '0' 因为它是一个只读元组
</code></pre>
<p><code>readonly</code> 关键字防止修改元组的值。</p>
<h3 id=""><strong>枚举</strong></h3>
<p>在 TypeScript 中， <strong>枚举</strong> 是一种定义一组命名常量的方法。枚举使代码更易读并帮助管理一组固定的值。</p>
<h4 id=""><strong>数值枚举（默认）:</strong></h4>
<pre><code class="language-typescript">enum Status {
  Pending,   // 0
  InProgress, // 1
  Completed,  // 2
}

console.log(Status.Pending);   // 输出: 0
console.log(Status.Completed); // 输出: 2
</code></pre>
<p>默认情况下，TypeScript 从 <code>0</code> 开始分配数值。</p>
<h4 id=""><strong>自定义枚举数值:</strong></h4>
<pre><code class="language-typescript">enum OrderStatus {
  Pending = 1,
  Shipped = 5,
  Delivered = 10,
}

console.log(OrderStatus.Shipped); // 输出: 5
</code></pre>
<p>这里为每个状态分配了自定义值。</p>
<h4 id=""><strong>字符串枚举:</strong></h4>
<pre><code class="language-typescript">enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

console.log(Direction.Up); // 输出: "UP"
</code></pre>
<p>字符串枚举存储固定的文本值而不是数字。</p>
<h4 id=""><strong>在函数中使用枚举:</strong></h4>
<pre><code class="language-typescript">function getStatusText(status: Status): string {
  switch (status) {
    case Status.Pending:
      return "Order is pending.";
    case Status.InProgress:
      return "Order is in progress.";
    case Status.Completed:
      return "Order is completed.";
    default:
      return "Unknown status.";
  }
}

console.log(getStatusText(Status.InProgress)); // 输出: "Order is in progress."
</code></pre>
<p>这个函数接受一个枚举值并根据状态返回一个信息。</p>
<p>元组定义了具有不同数据类型的固定长度数组，而枚举为更好的可读性提供命名常量，使代码更具结构性和类型安全。</p>
<h2 id="typescriptunknownnever"><strong>TypeScript 中的类型断言、unknown类型和never类型</strong></h2>
<h3 id=""><strong>类型断言</strong></h3>
<p>类型断言告诉 TypeScript 将某个值视为特定类型。它不会改变值，但帮助编译器理解类型。</p>
<pre><code class="language-typescript">let value: unknown = "Hello, TypeScript!";

// 使用类型断言将 'value' 视为字符串
let strLength: number = (value as string).length;

console.log(strLength); // 输出: 18
</code></pre>
<p>这里，<code>value</code> 最初是 <code>unknown</code> 类型，但类型断言 (<code>as string</code>) 允许将其视为字符串。</p>
<p>这里还有另一种编写类型断言的方式：</p>
<pre><code class="language-typescript">let num = &lt;number&gt;(10);
console.log(num); // 输出: 10
</code></pre>
<p><code>&lt;number&gt;</code> 语法也执行类型断言。</p>
<h3 id="unknown"><strong>unknown类型</strong></h3>
<p>现在让我们简单回顾一下 <code>unknown</code> 类型。记住，它是一个比 <code>any</code> 更安全的选择，并且可以保存任何值——但是TypeScript在使用它之前需要进行类型检查。</p>
<pre><code class="language-typescript">let data: unknown;

data = "Hello";
data = 42;
data = true;

// 使用值之前进行类型检查
if (typeof data === "string") {
  console.log(data.toUpperCase()); // 仅当数据是字符串时有效
}
</code></pre>
<p>由于 <code>data</code> 是 <code>unknown</code> 类型，TypeScript 不允许在不先检查其类型的情况下直接操作。</p>
<h3 id="never"><strong>never 类型</strong></h3>
<p><code>never</code> 类型表示永远不会发生的值。它通常用于永不返回或总是抛出错误的函数。</p>
<pre><code class="language-typescript">function throwError(message: string): never {
  throw new Error(message);
}

// throwError("Something went wrong!"); // 此函数永远不返回
</code></pre>
<p>在这里，<code>throwError</code> 不会返回任何东西，因为它总是抛出一个错误。</p>
<h4 id="switchnever"><strong>Switch 案例中的 Never 类型示例：</strong></h4>
<pre><code class="language-typescript">type Status = "success" | "failure";

function checkStatus(status: Status): void {
  switch (status) {
    case "success":
      console.log("Operation was successful.");
      break;
    case "failure":
      console.log("Operation failed.");
      break;
    default:
      const unexpected: never = status; // 确保所有情况都被处理
  }
}
</code></pre>
<p>这确保了 <code>Status</code> 的所有可能值都得到了处理，从而防止了意料之外的行为。</p>
<p>下面是这些不同方法的快速比较：</p>
<p>| <strong>功能</strong> | <strong>描述</strong> |<br>
| <strong>类型断言</strong> | 告诉 TypeScript 将某个值视为特定类型。 |<br>
| <strong>Unknown 类型</strong> | 允许存储任何值，但在使用前需要进行类型检查。 |<br>
| <strong>Never 类型</strong> | 表示永远不会发生的值，用于函未返回的数。 |</p>
<h2 id="typescript">TypeScript 中的泛型</h2>
<p>泛型允许编写灵活、可重用且类型安全的代码。泛型让函数、类或接口在不指定特定类型的情况下工作，同时保持类型安全。</p>
<h3 id=""><strong>基本泛型</strong></h3>
<p>一个泛型函数可以与任何类型一起工作，同时保持类型安全。</p>
<pre><code class="language-typescript">function identity&lt;T&gt;(value: T): T {
  return value;
}

console.log(identity&lt;string&gt;("Hello")); // 输出: "Hello"
console.log(identity&lt;number&gt;(42));      // 输出: 42
</code></pre>
<p>这里，<code>&lt;T&gt;</code> 是一个<strong>泛型类型参数</strong>，允许 <code>identity</code> 使用任何类型。</p>
<h3 id=""><strong>数组中的泛型</strong></h3>
<p>泛型有助于在数组中强制执行类型安全。</p>
<p>以下是使用泛型反转数组的示例：</p>
<pre><code class="language-typescript">function reverseArray&lt;T&gt;(arr: T[]): T[] {
  return arr.reverse();
}

console.log(reverseArray&lt;number&gt;([1, 2, 3]));  // 输出: [3, 2, 1]
console.log(reverseArray&lt;string&gt;(["A", "B", "C"])); // 输出: ["C", "B", "A"]
</code></pre>
<p>这确保了函数始终返回与接收到的相同类型的数组。</p>
<h3 id=""><strong>接口中的泛型</strong></h3>
<p>泛型可以在接口中用来定义灵活的对象结构。</p>
<pre><code class="language-typescript">interface StorageBox&lt;T&gt; {
  content: T;
}

let numberBox: StorageBox&lt;number&gt; = { content: 100 };
let stringBox: StorageBox&lt;string&gt; = { content: "TypeScript" };

console.log(numberBox.content); // 输出: 100
console.log(stringBox.content); // 输出: "TypeScript"
</code></pre>
<p>在这里，<code>StorageBox&lt;T&gt;</code> 允许存储不同类型的内容，同时确保一致性。</p>
<h3 id=""><strong>类中的泛型</strong></h3>
<p>泛型在类中同样有效，使其更具可重用性。</p>
<p>这是一个泛型队列类的示例：</p>
<pre><code class="language-typescript">class Queue&lt;T&gt; {
  private items: T[] = [];

  enqueue(item: T): void {
    this.items.push(item);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }
}

let numberQueue = new Queue&lt;number&gt;();
numberQueue.enqueue(10);
numberQueue.enqueue(20);
console.log(numberQueue.dequeue()); // 输出: 10

let stringQueue = new Queue&lt;string&gt;();
stringQueue.enqueue("Hello");
stringQueue.enqueue("World");
console.log(stringQueue.dequeue()); // 输出: "Hello"
</code></pre>
<p>此类适用于任何类型，同时保持类型安全。</p>
<h3 id=""><strong>具有多个类型参数的泛型</strong></h3>
<p>函数或类可以接受多个泛型类型。</p>
<p>以下是交换两个值的函数示例：</p>
<pre><code class="language-typescript">function swap&lt;T, U&gt;(first: T, second: U): [U, T] {
  return [second, first];
}

console.log(swap&lt;string, number&gt;("Age", 25)); // 输出: [25, "Age"]
console.log(swap&lt;boolean, string&gt;(true, "Yes")); // 输出: ["Yes", true]
</code></pre>
<p>在这里，<code>&lt;T, U&gt;</code> 允许函数同时处理不同的类型。</p>
<h3 id=""><strong>具有约束的泛型</strong></h3>
<p>有时候，泛型类型应遵循某些规则。<strong>约束</strong>确保某个类型具有特定属性。</p>
<p>以下是确保类型具有 <code>length</code> 属性的示例：</p>
<pre><code class="language-typescript">function getLength&lt;T extends { length: number }&gt;(item: T): number {
  return item.length;
}

console.log(getLength("Hello"));   // 输出: 5
console.log(getLength([1, 2, 3])); // 输出: 3
</code></pre>
<p>在这里，<code>T extends { length: number }</code> 确保 <code>T</code> 具有 <code>length</code> 属性。</p>
<h3 id="keyof"><strong>高级：使用</strong> <code>keyof</code> <strong>操作符的泛型</strong></h3>
<p><code>keyof</code> 操作符可用于确保有效的属性名称。</p>
<p>以下是按名称获取属性值的示例：</p>
<pre><code class="language-typescript">function getProperty&lt;T, K extends keyof T&gt;(obj: T, key: K): T[K] {
  return obj[key];
}

let user = { name: "Alice", age: 30 };

console.log(getProperty(user, "name")); // 输出: "Alice"
console.log(getProperty(user, "age"));  // 输出: 30
</code></pre>
<h2 id="">结语</h2>
<p>在本手册中，您深入了解了如何在 React 中使用 TypeScript 的基础知识。我们讨论了诸如类型注解、类型推论以及对象和数组管理等重要概念，展示了 TypeScript 如何提高代码的稳定性和可维护性。</p>
<p>我们还涵盖了一些高级主题，如联合类型和 any 类型、只读属性的使用，以及泛型、类型别名和接口的应用。希望这些示例能帮助您理解 TypeScript 如何增强您的 JavaScript 开发，使 TS 成为构建健壮的大型应用程序的宝贵工具。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 什么是语义化版本和约定式提交 ]]>
                </title>
                <description>
                    <![CDATA[ 最近写了不少新奇的小东西开源到了 GitHub 上，比如浏览器扩展或者一些运行起来的网页应用。在写浏览器扩展的过程中，我在生成 release 这一步时用 Github Action 创建了一条流水线，用于监听 manifest.json 中 version 属性值的变化，每次变化就根据 version 的值生成一个新版本的 release。也就在这时，我开始思考一个问题：我应该如何规范我的版本号？ 因为我此前并未接触过相关的规范，此前参与过的项目也没有明确的提交守则与版本号命名规范，对于相关的规范我只在一些开源项目中看到过，以及偶然听人提起过，并没有一个较为完整的认识，所以我决定查询一些资料去学习一下如何规范一个项目的版本号。 所幸我在让 AI 帮我规范提交信息时，它提到了“约定式提交”这个概念，而“约定式提交”与“语义化版本”有着密切的关系，所以我先分别了解了两者的概念，然后了解了它们之间是如何相互对应的。 语义化版本 软件的版本号是一个软件的重要标识，而许多软件的版本号命名都遵循着一套规范，“语义化版本”就是其中的一种。 什么是语义化版本 首先来看语义化版本的概念： > ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/semantic-versioning-and-conventional-commits/</link>
                <guid isPermaLink="false">67f3c36e11498004ee937050</guid>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Wed, 09 Apr 2025 01:07:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/04/----_20250421114125.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近写了不少新奇的小东西开源到了 GitHub 上，比如浏览器扩展或者一些运行起来的网页应用。在写浏览器扩展的过程中，我在生成 release 这一步时用 Github Action 创建了一条流水线，用于监听 <code>manifest.json</code> 中 <strong>version</strong> 属性值的变化，每次变化就根据 version 的值生成一个新版本的 release。也就在这时，我开始思考一个问题：我应该如何规范我的版本号？</p>
<p>因为我此前并未接触过相关的规范，此前参与过的项目也没有明确的提交守则与版本号命名规范，对于相关的规范我只在一些开源项目中看到过，以及偶然听人提起过，并没有一个较为完整的认识，所以我决定查询一些资料去学习一下如何规范一个项目的版本号。</p>
<p>所幸我在让 AI 帮我规范提交信息时，它提到了“约定式提交”这个概念，而“约定式提交”与“语义化版本”有着密切的关系，所以我先分别了解了两者的概念，然后了解了它们之间是如何相互对应的。</p>
<h2 id="">语义化版本</h2>
<p>软件的版本号是一个软件的重要标识，而许多软件的版本号命名都遵循着一套规范，“语义化版本”就是其中的一种。</p>
<h3 id="">什么是语义化版本</h3>
<p>首先来看语义化版本的概念：</p>
<blockquote>
<p>语义化版本是一种用于描述软件版本的方式，它使用一组规则来描述版本的变化。语义化版本的规则如下：</p>
<ul>
<li>版本号由三个部分组成：主版本号、次版本号、修订号，格式为：<code>主版本号.次版本号.修订号</code></li>
<li>版本号的递增规则如下：
<ul>
<li>主版本号：当软件发生了不兼容的 API 更改时，主版本号递增，次版本号和修订号重置为 0</li>
<li>次版本号：当软件添加了新功能，但向后兼容时，次版本号递增，修订号重置为 0</li>
<li>修订号：当软件进行了 bug 修复，向后兼容时，修订号递增</li>
</ul>
</li>
<li>先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面，作为延伸。</li>
</ul>
</blockquote>
<p>（PS：向后兼容指的是，用户 import 你的软件包，用了里面的函数，这个软件包更新时，用户无需做任何修改，预期结果也不受影响）</p>
<h3 id="">举个例子</h3>
<p>让我们以一个例子来理解这种规则：</p>
<p>假设我们有一个版本号为 <code>1.0.0</code> 的软件包供外部软件调用，这个软件包中有一个函数<code>example(a: string, b: number): string {}</code>，以下三种情况分别对应了修订号、次版本号、主版本号的修改：</p>
<ul>
<li>example 函数体存在bug，我们在确保修复了这个 bug 的情况下未在软件包中引入新的 API，该函数依旧能够实现预期的功能，返回预期的结果。这时仅进行了 bug 修复，未添加新功能，未发生不兼容的API修改，版本号变为 <code>1.0.1</code></li>
<li>软件包中新增了一种 example 函数的函数重载，它的实现函数变成<code>example(a: string, b: number, c?: boolean): string {}</code>，用户原有的函数调用依旧返回预期结果，但是当用户调用 example 函数传入 <code>c</code>参数时，返回值会发生变化，这时我们新增了一个函数重载，未发生不兼容的API修改，版本号变为 <code>1.1.0</code></li>
<li>example 函数新增了一个参数，变成<code>example(a: string, b: number, c: string): string {}</code>，用户原有的函数调用会报错提示缺少参数。这时我们在 example 函数中新增了一个参数，发生了不兼容的API修改，版本号变为 <code>2.0.0</code></li>
</ul>
<p>可以看到，语义化版本的规则是非常清晰的，它规定了版本号的递增规则，以及当软件发生不兼容的 API 修改时，版本号的变化。这也是语义化版本的最主要的作用，它可以确保用户在使用某一主版本的软件包时，已编写的代码在该软件包仅发生次版本号会修订版本号更新时，不会出现错误。</p>
<h3 id="">开发初始阶段的版本控制</h3>
<p>软件的主版本号为零（0.y.z）是一种特殊状态，这意味着该软件处于开发初始阶段，一切都可能随时被改变。因此在初始开发阶段，进行版本控制最简单的做法是以 0.1.0 作为初始化开发版本，并在后续的每次发行时递增次版本号。</p>
<h3 id="100">如何判断发布 1.0.0 版本的时机</h3>
<p>当软件被部署到生产环境中开始被正式使用，它应该已经达到了 1.0.0 版。此时的软件已经有了一系列稳定的API在被使用者依赖，并开始需要考虑向后兼容的问题。</p>
<h3 id="">版本的优先级</h3>
<p>对于不同版本号的软件，如何排序决定它们的优先级呢？在判断优先级时，我们必须把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较（版本编译信息不在这份比较的列表中）。随后，按照这三条规则进行判断：</p>
<ul>
<li>对于两个正式版本，需要依据它们的主版本号、次版本号、修订号依次进行比较，直到找到第一个不同的数字，该数字较大的版本号优先级较高（例如1.0.0 &lt; 1.0.1 &lt; 1.1.0 &lt; 1.1.1 &lt; 2.0.0）</li>
<li>对于具有相同主版本号、次版本号、修订号的正式版本和先行版本，正式版本的优先级总是高于先行版本（例如1.0.0-alpha &lt; 1.0.0）</li>
<li>对于两个先行版本，如果它们的主版本号、次版本号、修订号完全相同，其优先级必须通过由左到右的每个被句点分隔的标识符来比较，直到找到一个差异值后，按照如下规则决定：
<ul>
<li>只有数字的标识符以数值高低比较。（例如1.0.0-alpha.1&lt;1.0.0-alpha.5）</li>
<li>有字母或连接号时则逐字以 ASCII 的排序来比较。（例如1.0.0-alpha.beta&lt;1.0.0-alpha.rc）</li>
<li>数字的标识符比非数字的标识符优先级低。（例如1.0.0-0&lt;1.0.0-alpha）</li>
<li>若开头的标识符都相同时，栏位比较多的先行版本号优先级比较高。（例如1.0.0-alpha.1&lt;1.0.0-alpha.1.0）</li>
</ul>
</li>
</ul>
<p>再举一个最直观的例子：<br>
1.0.0-alpha &lt; 1.0.0-alpha.1 &lt; 1.0.0-alpha.beta &lt; 1.0.0-beta &lt; 1.0.0-beta.2 &lt; 1.0.0-beta.11 &lt; 1.0.0-rc.1 &lt; 1.0.0 &lt; 1.0.1 &lt; 1.1.0 &lt; 1.1.1 &lt; 2.0.0<br>
你可以按照上述的三条规则，一一它们的优先级排列，相信你会对版本的优先级有更加深刻的认识。</p>
<h2 id="">约定式提交</h2>
<p>最开始时我提到过，“约定式提交”与“语义化版本”有着密切的关系，这是因为在进行源代码管理时，我们通常会使用版本控制工具，如 Git，在使用<code>git commit</code>提交代码时，如果我们按照“约定式提交”的规范填写提交信息，我们可以方便地将一组特定的提交发布为一个新版本，而这个新版本可以按照对应的提交类型确定版本号如何更新。</p>
<p>在许多开源项目的 contribute 指南中，都有约定式提交的规范，比如在 <a href="https://github.com/arco-design/arco-design/blob/main/CONTRIBUTING.zh-CN.md" target="_blank" rel="noopener">arco-design的CONTRIBUTING.zh-CN.md</a> 的 Commit 指南章节，就要求 Commit messages 需要遵循约定式提交的标准，并列出了 commit 的类型列表。</p>
<p>因此，让我们首先了解什么是约定式提交。</p>
<h3 id="">什么是约定式提交</h3>
<p>首先来看约定式提交的概念：</p>
<blockquote>
<p>约定式提交是一种用于描述提交信息的方式，它使用一组规则来描述提交的内容。约定式提交的规则如下：</p>
<ul>
<li>提交信息必须以一个描述性的标题开头，标题的第一个单词必须是提交类型，提交类型必须是以下之一：
<ul>
<li>feat：新功能（feature），类型 为 feat 的提交表示在代码库中新增了一个功能（这和语义化版本中的 次版本号 相对应）。</li>
<li>fix：bug 修复，类型 为 fix 的提交表示在代码库中修复了一个 bug（这和语义化版本中的 修订号 相对应）。</li>
<li>BREAKING CHANGE：破坏性变更，在脚注中包含 BREAKING CHANGE: 或 &lt;类型&gt;(范围) 后面有一个 ! 的提交，表示引入了破坏性 API 变更（这和语义化版本中的 MAJOR 相对应）。</li>
<li>其他提交类型：例如 docs、chore、style、refactor、perf、test、build、ci、revert 等</li>
</ul>
</li>
<li>提交信息的结构如下所示：<pre><code>&lt;类型&gt;[可选 范围]: &lt;描述&gt;
[可选 正文]
[可选 脚注]
</code></pre>
</li>
</ul>
</blockquote>
<p>让我们通过一些例子了解如何完成一个约定式提交：</p>
<ul>
<li>包含了描述、范围并且脚注中有破坏性变更的提交说明：<pre><code>feat(api): 新增了一个新功能
这是一个新功能的详细描述。
BREAKING CHANGE: 这个提交引入了一个破坏性变更。
</code></pre>
</li>
<li>包含了 ! 字符以提醒注意破坏性变更的提交说明：<pre><code>feat(api)!: 新增了一个新功能
这是一个新功能的详细描述。
</code></pre>
</li>
<li>不包含正文的提交说明：<pre><code>docs: correct spelling of CHANGELOG
</code></pre>
</li>
</ul>
<h3 id="">约定式提交与语义化版本的对应关系</h3>
<table>
<thead>
<tr>
<th>提交类型</th>
<th>语义化版本</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>feat</td>
<td>次版本号</td>
<td>新功能</td>
</tr>
<tr>
<td>fix</td>
<td>修订号</td>
<td>bug 修复</td>
</tr>
<tr>
<td>BREAKING CHANGE</td>
<td>主版本号</td>
<td>破坏性变更</td>
</tr>
<tr>
<td>其他提交类型</td>
<td>无</td>
<td>其他类型的提交</td>
</tr>
</tbody>
</table>
<h4 id="">举个例子</h4>
<p>让我们以一个例子来理解这种规则：</p>
<p>对于一个当前版本为 1.0.0 的项目，我们有以下几个提交：</p>
<ul>
<li>feat(api): 新增了一个新功能</li>
<li>fix(api): 修复了一个 bug</li>
<li>docs: 修正了文档中的错误</li>
</ul>
<p>我们现在想要发布一个新版本，由于新的提交包括了新功能的增加，但是没有引入破坏性的变更，所以我们可以按照语义化版本的规则，将次版本号递增，版本号变为 1.1.0。</p>
<p>但是如果我们有以下几个提交：</p>
<ul>
<li>feat(api)!: 新增了一个新功能, 引入了破坏性变更</li>
<li>fix(api): 修复了一个 bug</li>
<li>docs: 修正了文档中的错误</li>
</ul>
<p>由于引入了破坏性变更，如果我们此时想要发布新版本，我们需要将主版本号递增，版本号变为 2.0.0。</p>
<h3 id="">使用约定式提交的好处</h3>
<p>使用约定式提交有以下几个好处：</p>
<ul>
<li>便于建立流水线自动化生成 CHANGELOG 、自动化触发对应的构建与部署流程，从而方便地进行版本发布。</li>
<li>便于基于提交的类型，自动决定语义化的版本变更，从而方便地进行版本管理。</li>
<li>便于形成结构化的提交历史，向同事、公众与其他利益关系者传达变化的性质，降低贡献者了解项目发展历程的难度。</li>
</ul>
<h2 id="">后记</h2>
<p>语义化版本和约定式提交是两个相互关联的概念，它们都可以帮助我们更好地管理软件版本。语义化版本规定了版本号的递增规则，以及当软件发生不兼容的 API 修改时版本号的变化。约定式提交规定了提交信息的结构，以及如何根据提交信息自动决定版本号的变化。</p>
<p>在实际的项目开发与项目管理过程中，建立和遵守这些规范可以提高软件的质量和可维护性，同时也可以方便地进行版本管理和发布，提高开发效率与协作效率。</p>
<h2 id="">参考文章</h2>
<ul>
<li><a href="https://semver.org/lang/zh-CN/">语义化版本 2.0.0 | Semantic Versioning</a></li>
<li><a href="https://www.conventionalcommits.org/en/v1.0.0/">约定式提交</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 学习 HTTP 方法：GET、POST 和 DELETE —— 带有代码示例的手册 ]]>
                </title>
                <description>
                    <![CDATA[ 当你与网站或应用进行交互时，潜藏在那些交互后面的过程非常复杂。这些过程的核心之一就是浏览器或应用是如何与服务器通信的。HTTP 方法定义了需要执行的操作——它可能是获取数据、发送信息或更改现有内容。 每种方法都有特定的用途，以保持网络通信的清晰、安全和有序。 在本文中，我们将分解最常见的 HTTP 方法，并解释它们如何运行以实现顺畅的在线互动。 目录  1.  GET 方法              2.  POST 方法              3.  PUT 方法     ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-http-methods-like-get-post-and-delete-a-handbook-with-code-examples/</link>
                <guid isPermaLink="false">67ee8c16a2b8e3047d17aee1</guid>
                
                    <category>
                        <![CDATA[ HTTP ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Thu, 03 Apr 2025 10:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/04/1743687125353-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/learn-http-methods-like-get-post-and-delete-a-handbook-with-code-examples/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learn HTTP Methods like GET, POST, and DELETE – a Handbook with Code Examples</a>
      </p><!--kg-card-begin: markdown--><p>当你与网站或应用进行交互时，潜藏在那些交互后面的过程非常复杂。这些过程的核心之一就是浏览器或应用是如何与服务器通信的。HTTP 方法定义了需要执行的操作——它可能是获取数据、发送信息或更改现有内容。</p>
<p>每种方法都有特定的用途，以保持网络通信的清晰、安全和有序。</p>
<p>在本文中，我们将分解最常见的 HTTP 方法，并解释它们如何运行以实现顺畅的在线互动。</p>
<h3 id="">目录</h3>
<ol>
<li>
<p><a href="#heading-get-method">GET 方法</a></p>
</li>
<li>
<p><a href="#heading-post-method">POST 方法</a></p>
</li>
<li>
<p><a href="#heading-put-method">PUT 方法</a></p>
</li>
<li>
<p><a href="#heading-patch-method">PATCH 方法</a></p>
</li>
<li>
<p><a href="#heading-delete-method">DELETE 方法</a></p>
</li>
<li>
<p><a href="#heading-head-method">HEAD 方法</a></p>
</li>
<li>
<p><a href="#heading-options-method">OPTIONS 方法</a></p>
</li>
<li>
<p><a href="#heading-trace-method">TRACE 方法</a></p>
</li>
<li>
<p><a href="#heading-connect-method">CONNECT 方法</a></p>
</li>
<li>
<p><a href="#heading-conclusion">总结</a></p>
</li>
</ol>
<h2 id="get">GET 方法</h2>
<p>GET 方法是最常见的 HTTP 方法之一，用于从服务器请求数据。可以把它看作是索取信息而不做任何改变。</p>
<p>当你访问网页时，浏览器发送一个 GET 请求给服务器，要求页面的内容。然后服务器响应数据（如 HTML、图像或其他文件），浏览器进行显示。</p>
<p>关于 GET 的一个重点是它不会对数据进行任何更改。它只是“读取”或检索信息。例如，当你浏览社交媒体或在线搜索产品时，应用或网站使用 GET 来显示数据而不更改它。</p>
<p>另一个关键点是 GET 请求的参数通过URL传递，这意味着请求的数据会显示在浏览器的地址栏中。例如，如果你在网店搜索产品，搜索词会包含在 URL 中。</p>
<h3 id="get">GET 请求示例</h3>
<p>以下是使用 Fetch API 的 JavaScript 中一个简单的 GET 请求示例：</p>
<pre><code class="language-javascript">fetch('https://api.example.com/products?category=shoes')
  .then(response =&gt; response.json())
  .then(data =&gt; console.log(data))
  .catch(error =&gt; console.error('Error:', error));
</code></pre>
<p>在这个例子中，GET 请求被发送到 URL <a href="https://api.example.com/products"><code>https://api.example.com/products</code></a> ，带有查询参数 <code>category=shoes</code>，请求服务器返回鞋子类别的产品。</p>
<h3 id="get">GET 方法的使用场景</h3>
<p>GET 主要用于获取信息，以下是一些常见的应用场景：</p>
<ol>
<li>
<p><strong>加载网页</strong>：每当你在浏览器中输入 URL 或点击链接时，就是在进行 GET 请求。浏览器向服务器请求网页，服务器返回显示内容。</p>
<ul>
<li>示例：<code>GET /index.html HTTP/1.1</code></li>
</ul>
</li>
<li>
<p><strong>从 API 获取数据</strong>：开发者常使用 API（应用程序接口）从外部服务器获取数据。例如，天气应用使用 GET 请求从天气 API 获取当前温度。</p>
<ul>
<li>示例：</li>
</ul>
<pre><code class="language-javascript">fetch('https://api.weather.com/current?city=Lagos')
   .then(response =&gt; response.json())
   .then(data =&gt; console.log(data));
</code></pre>
</li>
<li>
<p><strong>搜索查询</strong>：当你在 Google 或其他搜索引擎中进行搜索时会发出 GET 请求。你输入的搜索词包含在 URL 中，服务器返回匹配结果的列表。</p>
<ul>
<li>示例：<code>GET /search?q=JavaScript</code></li>
</ul>
</li>
<li>
<p><strong>检索文件</strong>：无论你是下载图片、查看 PDF 或播放视频，GET 都用于从服务器获取这些文件。</p>
<ul>
<li>示例：<code>GET /files/image.jpg</code></li>
</ul>
</li>
</ol>
<h3 id="get">GET 请求的最佳实践</h3>
<p>要有效地使用 GET 请求，遵循一些良好的实践以确保数据处理的顺畅和安全是很重要的：</p>
<ol>
<li>
<p><strong>仅用 GET 来检索数据</strong>：GET 请求用于获取数据，而不是发送如密码或个人数据等敏感信息。因为 GET 请求中的参数包含在 URL 中，任何人都可以看到它们。例如，如果你网站登录时，不应该使用 GET 发送密码，因为它会出现在 URL 中。</p>
<ul>
<li>不该做的示例：</li>
</ul>
<pre><code class="language-javascript">fetch('https://example.com/login?username=john&amp;password=secret');
</code></pre>
</li>
<li>
<p><strong>保持 URL 简洁</strong>：由于 GET 请求的数据包含在 URL 中，过长的 URL 会造成问题。浏览器和服务器对于 GET 请求 URL 的数据量也有限制，所以避免放入太多信息。如果需要发送大量数据，考虑改用 POST 请求。</p>
</li>
<li>
<p><strong>为性能启用缓存</strong>：GET 请求通常会被浏览器缓存，即浏览器可以存储响应并无需再次联系服务器就重复使用。这提高了性能，尤其是对于不经常更改的静态内容，如图像或样式表。为此，确保服务器发送适当的 cache-control 头，以便可频繁请求的数据可以更快加载。</p>
<ul>
<li>设置缓存头的示例：</li>
</ul>
</li>
</ol>
<pre><code>    Cache-Control: max-age=3600
</code></pre>
<ol start="4">
<li>
<p><strong>避免使用 GET 请求执行会修改数据的操作</strong>：由于 GET 是一种“安全”的方法，它仅应用于不会修改数据的操作。如果你想创建、更新或删除数据，应使用像 POST、PUT 或 DELETE 这样的方法。例如，如果你不小心使用 GET 来删除资源，可能会有人通过点击链接或刷新页面来删除它，这样做是不安全的。</p>
<ul>
<li><strong>不要</strong>使用 GET 来进行删除操作的示例：</li>
</ul>
</li>
</ol>
<pre><code>    GET /delete/user/123
</code></pre>
<ol start="5">
<li><strong>谨慎处理敏感数据</strong>：由于 GET 请求是 URL 的一部分，它们可能被记录或者保存在浏览器的历史记录中。避免在 GET 请求中发送敏感信息，如密码、信用卡详细信息或隐私数据。处理此类信息时应始终使用 POST 方法，以确保信息保持隐藏。</li>
</ol>
<h2 id="post">POST 方法</h2>
<p>POST 方法用于向服务器发送数据。与仅用于获取数据的 GET 方法不同，POST 方法可以用来提交服务器可以处理或存储的信息。POST 方法通常用于用户输入数据的表单，如用户名、密码或联系信息。</p>
<p>当发出 POST 请求时，数据通过请求体（而非 URL）发送的。这样 POST 方法就成了发送更大或更敏感的信息（如密码）的理想选择，因为数据是隐藏的，不会出现在浏览器的地址栏中。</p>
<p>例如，当你注册一个网站或在博客上提交评论时，使用 POST 方法将你的信息发送到服务器，后者处理并将其存储在数据库中。</p>
<h3 id="post">POST 请求示例</h3>
<p>以下是使用 Fetch API 将表单数据发送到服务器的 POST 请求示例：</p>
<pre><code class="language-javascript">const formData = {
  username: 'john_doe',
  password: 'mypassword123'
};

fetch('https://example.com/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(formData)
})
.then(response =&gt; response.json())
.then(data =&gt; console.log('Success:', data))
.catch(error =&gt; console.error('Error:', error));
</code></pre>
<p>在这个例子中，POST 请求以 JSON 数据的形式在请求体中发送 <code>username</code> 和 <code>password</code>，这是一种安全地处理像密码这样的敏感信息的方法。</p>
<h3 id="getpost">GET 和 POST 的差异</h3>
<p>虽然 GET 和 POST 都用于与服务器通信，但它们的目的不同，处理数据的方式也不同：</p>
<h4 id="">数据传输：</h4>
<ul>
<li>
<p><strong>GET</strong>：数据包含在 URL 中，使其在地址栏中可见。这限制了可发送的数据量。</p>
</li>
<li>
<p><strong>POST</strong>：数据是在请求体中发送的，允许发送更多的信息。这也使得敏感信息不在 URL 中暴露。</p>
</li>
</ul>
<h4 id="">目的：</h4>
<ul>
<li>
<p><strong>GET</strong>：用于获取数据。不会在服务器上改变或修改任何内容。</p>
</li>
<li>
<p><strong>POST</strong>：用于发送可能会改变或增加服务器资源的数据，比如向数据库添加新用户或提交表单。</p>
</li>
</ul>
<h4 id="">缓存：</h4>
<ul>
<li>
<p><strong>GET</strong>：GET 请求可以缓存。因此浏览器可能会保存响应，从而加快后续请求。</p>
</li>
<li>
<p><strong>POST</strong>：POST 请求不会被缓存，因为它们通常涉及新的或更新的数据，这些数据不应重复使用。</p>
</li>
</ul>
<h4 id="">幂等性：</h4>
<ul>
<li>
<p><strong>GET</strong>：多次发送相同的 GET 请求不会改变结果。每次都会返回相同的数据。</p>
</li>
<li>
<p><strong>POST</strong>：多次发送相同的 POST 请求可能会导致不同的结果。例如，提交表单两次可能会创建重复的条目。</p>
</li>
</ul>
<h3 id="post">POST 的常见使用场景</h3>
<p>POST 是在需要向服务器发送数据时的理想选择，通常用于处理或存储。以下是一些常见的使用案例：</p>
<ol>
<li>
<p><strong>提交表单</strong>：每当你在线填写并提交表单时，如注册新闻简讯或在注册表单中输入你的详细信息，POST 方法用于将该信息发送到服务器。服务器会处理数据，将其存储或根据需要执行其他操作。</p>
<ul>
<li>示例：</li>
</ul>
</li>
</ol>
<pre><code class="language-html">    &lt;form action="https://example.com/register" method="POST"&gt;
      &lt;input type="text" name="username" /&gt;
      &lt;input type="password" name="password" /&gt;
      &lt;button type="submit"&gt;Sign Up&lt;/button&gt;
    &lt;/form&gt;
</code></pre>
<ol start="2">
<li>
<p><strong>用户身份验证</strong>：当你使用用户名和密码登录网站时，通常使用 POST 将你的凭据安全地发送到服务器。服务器检查信息，如果凭据匹配，则授权访问你的账户。</p>
</li>
<li>
<p><strong>上传文件</strong>：POST 也用于上传文件，如图像、文档或视频。由于 POST 方法允许发送大量数据，它非常适合上传需要被处理或存储在服务器上的文件。</p>
<ul>
<li>使用表单上传文件的示例：</li>
</ul>
</li>
</ol>
<pre><code class="language-html">    &lt;form action="https://example.com/upload" method="POST" enctype="multipart/form-data"&gt;
      &lt;input type="file" name="file" /&gt;
      &lt;button type="submit"&gt;Upload File&lt;/button&gt;
    &lt;/form&gt;
</code></pre>
<ol start="4">
<li>
<p><strong>创建新资源</strong>：POST 经常在 API 中用于创建新资源。例如，当你向在线商店添加新产品时，POST 方法会被用来将产品详细信息发送到服务器，后者会将产品添加到商店的数据库中。</p>
<ul>
<li>发送产品数据的示例：</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const product = {
      name: 'New Sneakers',
      price: 59.99,
      category: 'Footwear'
    };
</code></pre>
<ol start="5">
<li>
<p><strong>向 API 发送数据</strong>：当需要发送将被处理或存储的数据时，POST 在 API 中被广泛使用。例如，一个记录你的健身进度的应用可能会使用 POST 将你的锻炼细节发送到服务器，在那里它会被存储和分析。</p>
</li>
<li>
<p><strong>在线购物</strong>：当你进行在线购物时，POST 用于将支付详情发送到服务器进行处理。服务器处理交易并用你的订单信息更新系统。</p>
</li>
</ol>
<h2 id="put">PUT 方法</h2>
<p><strong>PUT</strong> 方法用于更新或替换服务器上的现有资源。它将数据发送到服务器，并指示创建一个新的资源（如果不存在）或替换当前的资源。PUT 的关键思想是你告诉服务器资源应该是什么样的。</p>
<p>例如，想象一个网站上的用户资料。如果你使用 PUT 更新你的资料，服务器将用你提供的新数据替换整个资料。资料的每个部分都会完全匹配你发送的内容，因此如果某些信息缺失，它们将被新数据覆盖。</p>
<h3 id="put">PUT 请求示例</h3>
<p>下面是一个使用 Fetch API 更新用户数据的 PUT 请求示例：</p>
<pre><code class="language-javascript">const updatedProfile = {
  username: 'john_doe_updated',
  email: 'john_updated@example.com',
  age: 30
};

fetch('https://example.com/users/123', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(updatedProfile)
})
.then(response =&gt; response.json())
.then(data =&gt; console.log('Updated:', data))
.catch(error =&gt; console.error('Error:', error));
</code></pre>
<p>在此示例中，PUT 请求使用新数据更新用户资料。资料将被 <code>username</code>、<code>email</code> 和 <code>age</code> 的值替换。如果某些数据缺失，例如 <code>phoneNumber</code>，它将从资料中删除。</p>
<h3 id="put">何时使用 PUT</h3>
<p>PUT 主要用于当你希望使用特定的完整数据更新或替换资源时。以下是一些使用 PUT 的常见情形：</p>
<ol>
<li>
<p><strong>更新资源</strong>：当你需要对现有资源进行更改时，PUT 用于发送整个资源的新版本。例如，更新博客文章、产品详情或用户信息需要使用 PUT 完整替换资源。</p>
<ul>
<li>示例：</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const updatedPost = {
      title: 'New Title for My Blog',
      content: 'Updated blog content here...',
      author: 'John Doe'
    };

    fetch('https://example.com/blog/45', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(updatedPost)
    });
</code></pre>
<ol start="2">
<li>
<p><strong>创建资源（如果不存在）</strong>：如果你发送一个 PUT 请求到一个尚无资源的特定 URL，服务器将使用你提供的数据创建一个。这在需要预先完全定义的资源时非常有用。</p>
<ul>
<li>如果产品不存在，创建产品的示例：</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const newProduct = {
      id: 101,
      name: 'New Sneakers',
      price: 59.99,
      category: 'Footwear'
    };

    fetch('https://example.com/products/101', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newProduct)
    });
</code></pre>
<ol start="3">
<li>
<p><strong>与 API 协作</strong>：在与 API 交互时，当你需要更新资源如用户资料、产品详情或任何其他结构化数据时，通常使用 PUT。例如，一个待办事项应用可能允许你使用 PUT 更新现有任务的新信息。</p>
<ul>
<li>更新任务的示例：</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const updatedTask = {
      title: 'Updated Task Title',
      completed: true
    };

    fetch('https://example.com/tasks/67', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(updatedTask)
    });
</code></pre>
<h3 id="putvspost">PUT vs. POST：关键区别</h3>
<p>虽然 PUT 和 POST 都可以向服务器发送数据，但它们有不同的目的和行为：</p>
<h4 id="">目的</h4>
<ul>
<li>
<p><strong>PUT</strong>：主要用于更新或替换现有资源。如果资源不存在，PUT 也可以创建它。</p>
</li>
<li>
<p><strong>POST</strong>：主要用于创建新资源或提交需要处理的数据。POST 不替换现有资源，而是添加新的。</p>
</li>
</ul>
<h4 id="">数据处理</h4>
<ul>
<li>
<p><strong>PUT</strong>：用新数据替换整个资源。如果请求中缺少部分资源，该部分将被删除或替换。</p>
</li>
<li>
<p><strong>POST</strong>：添加或更新资源而不替换整个资源。例如，提交表单时，POST 向服务器添加新数据而不删除已有内容。</p>
</li>
</ul>
<h4 id="">幂等性</h4>
<ul>
<li>
<p><strong>PUT</strong>：是幂等的，因此多次发送相同的 PUT 请求将始终产生相同的结果。无论你使用 PUT 更新资源多少次，结果都是一样的。</p>
</li>
<li>
<p><strong>POST</strong>：不是幂等的，因此多次提交相同的 POST 请求可能会创建重复的资源或产生不同的结果。</p>
</li>
<li>
<p><strong>PUT</strong>: 最适用于资源的更新和完全替换。例如，如果您要更新在线商店中的产品详情，PUT 确保您发送的所有细节都被新的替换。</p>
</li>
<li>
<p><strong>POST</strong>: 用于创建新条目或发送需要处理的数据。例如，提交在线订单或填写联系表单时使用 POST。</p>
</li>
</ul>
<h2 id="patch">PATCH 方法</h2>
<p><strong>PATCH</strong> 方法用于对服务器上的资源进行部分更新。与完全替换整个资源的 PUT 方法不同，PATCH 允许您在不再发送完整数据的情况下更新资源的特定部分。这使得 PATCH 非常适合于只需调整某些细节而不影响资源的其他部分的场景。</p>
<p>例如，如果您有一个用户资料，只想更新电话号码，PATCH 使您可以仅发送新的电话号码，而其他部分资料保持不变。这种方式更高效，并减少了意外数据丢失的风险。</p>
<h3 id="patch">使用 PATCH 进行部分更新</h3>
<p>PATCH 旨在对资源进行有针对性的更改。其工作原理如下：</p>
<ul>
<li>
<p><strong>有针对性的更改</strong>：当您使用 PATCH 时，您只需指定要更新的字段。例如，如果用户更新他们的邮箱地址，您仅需发送一个包含新邮箱的 PATCH 请求，服务器上的所有其他信息将保持不变。</p>
</li>
<li>
<p><strong>效率</strong>：PATCH 比 PUT 更高效，因为它允许您仅发送正在更改的数据。对于只需修改一小部分的大型资源而言，这可以减少带宽使用。</p>
</li>
<li>
<p><strong>不覆盖</strong>：与 PUT 不同，PATCH 不会替换整个资源。它只更新请求中提供的字段，保证其他字段不受影响。</p>
</li>
</ul>
<h3 id="patch">PATCH 请求示例</h3>
<p>以下是一个使用 PATCH 方法更新特定字段的基本示例，例如更改用户的邮箱地址：</p>
<pre><code class="language-javascript">const updatedEmail = {
  email: 'new_email@example.com'
};

fetch('https://example.com/users/123', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(updatedEmail)
})
.then(response =&gt; response.json())
.then(data =&gt; console.log('Email updated:', data))
.catch(error =&gt; console.error('Error:', error));
</code></pre>
<p>在这个例子中，只有 <code>email</code> 字段被更新。用户资料的其他部分，比如用户名或地址则保持不变。</p>
<h3 id="patchput">何时使用 PATCH 而非 PUT</h3>
<p>在特定情况下，使用 PATCH 比 PUT 更合适：</p>
<ol>
<li>
<p><strong>更新特定字段</strong>：如果您只需要更新资源的一部分，比如更改用户的邮箱地址、为博客文章添加标签或修改单个属性，使用 PATCH 更好。它允许您仅发送需要更新的字段，使请求更高效。</p>
<ul>
<li>示例：更新用户的电话号码。</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const updatedPhone = { phoneNumber: '123-456-7890' };

    fetch('https://example.com/users/123', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updatedPhone)
    });
</code></pre>
<ol start="2">
<li>
<p><strong>避免意外的数据丢失</strong>：使用 PUT 时，若遗漏任何字段，可能导致服务器删除或覆盖这些字段。PATCH 通过仅更新提供的特定字段，避免了此风险，确保没有意外的数据丢失。</p>
<ul>
<li>示例：如果您只想更新用户的用户名，而不想覆盖其他字段（如地址或偏好），PATCH 可以确保仅更新用户名。</li>
</ul>
</li>
<li>
<p><strong>性能考量</strong>：PATCH 对于大型资源更为高效。例如，如果您管理一个拥有大量记录的数据库，并且需要更改其中的一小部分内容，PATCH 会减少发送到服务器的数据量，提高性能并加速过程。</p>
<ul>
<li>示例：更新大型订单的状态而不修改整个订单详情。</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const updatedStatus = { status: 'shipped' };

    fetch('https://example.com/orders/987', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updatedStatus)
    });
</code></pre>
<ol start="4">
<li>
<p><strong>频繁更新</strong>：在数据经常变化的应用程序中，PATCH 使得仅更新资源的特定部分更加容易，而不影响整个结构。例如，在电子商务平台中，用户可能会经常更新他们的送货地址或支付方式，PATCH 能够处理这些频繁的变化，而无需重新发送整个用户资料。</p>
<ul>
<li>示例：更新订单的送货地址。</li>
</ul>
</li>
</ol>
<pre><code class="language-javascript">    const updatedAddress = {
      shippingAddress: '123 New Street, New City, Country'
    };

    fetch('https://example.com/orders/987', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updatedAddress)
    });
</code></pre>
<h3 id="putpatch">PUT 和 PATCH 的关键区别</h3>
<p>以下是 PATCH 和 PUT 的快速对比，这些对比项可以明确每种方法什么时候使用更合适：</p>
<table>
<thead>
<tr>
<th><strong>特性</strong></th>
<th>PUT</th>
<th>PATCH</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>用途</strong></td>
<td>替换整个资源</td>
<td>部分更新资源</td>
</tr>
<tr>
<td><strong>数据处理</strong></td>
<td>要求发送整个资源</td>
<td>只发送需要更新的字段</td>
</tr>
<tr>
<td><strong>效率</strong></td>
<td>对于大型资源效率较低</td>
<td>对于小型的特定更新，效率更高</td>
</tr>
<tr>
<td><strong>幂等性</strong></td>
<td>幂等性（重复后结果相同）</td>
<td>不一定等效（取决于请求）</td>
</tr>
<tr>
<td><strong>数据丢失风险</strong></td>
<td>如果数据丢失，可以覆盖字段</td>
<td>除非指定，否则不会覆盖现有字段</td>
</tr>
</tbody>
</table>
<p><strong>PATCH</strong> 方法在您希望进行部分更新、避免覆盖其他数据并提高请求效率时特别有用。</p>
<h2 id="delete">DELETE 方法</h2>
<p>DELETE 方法用于从服务器中移除资源。当发出 DELETE 请求时，服务器会删除指定的资源，这意味着该资源将不再可访问或可用。此方法常用于删除用户账号、从在线商店移除产品或清除数据库中过期的数据等任务。</p>
<p>与 GET 或 POST 不同，DELETE 不需要在请求中发送 body——只需提供要删除资源的 URL 就足够了。例如，要删除一个特定的博客文章，可以向该文章的 URL 发送 DELETE 请求，服务器将负责将其删除。</p>
<h3 id="delete">DELETE 的工作原理</h3>
<p>要删除某个资源，通常只需提供要移除资源的 URL。不同于 POST 或 PUT 请求，DELETE 请求通常不需要 body。</p>
<h4 id="">示例</h4>
<p>如果您想删除特定的一篇博客文章，可以向其 URL 发送 DELETE 请求：</p>
<pre><code class="language-javascript">fetch('https://example.com/posts/123', {
  method: 'DELETE'
})
.then(response =&gt; response.json())
.then(data =&gt; console.log('Resource deleted:', data))
.catch(error =&gt; console.error('Error:', error));
</code></pre>
<p>这将告诉服务器移除 ID 为 <code>123</code> 的博客文章。</p>
<h3 id="delete">安全使用 DELETE</h3>
<p>DELETE 请求可能带来显著影响，因此需要谨慎使用以避免意外删除有价值的数据。以下是安全处理 DELETE 请求的一些关键考虑：</p>
<ul>
<li>
<p><strong>永久性操作</strong>：一旦 DELETE 请求被处理，资源通常就消失了。在某些情况下，系统可能实现“软删除”功能，其中资源被隐藏但并未完全移除。然而，大多数情况下使用的是“硬删除”，即完全擦除资源。软删除对恢复很有帮助，允许在需要时恢复数据。</p>
</li>
<li>
<p><strong>认证</strong>：DELETE 请求应仅限于授权用户。执行 DELETE 操作前，服务器应验证用户有权限删除资源。例如，只有用户账号的所有者才能删除账号，而不是其他用户。</p>
</li>
<li>
<p><strong>确认</strong>：许多应用程序在处理 DELETE 操作前会提示用户确认意图。此额外步骤可确保用户不会意外删除重要数据，尤其是诸如账户删除等不可逆的操作。</p>
</li>
</ul>
<h4 id="">确认步骤示例：</h4>
<pre><code class="language-javascript">if (confirm("Are you sure you want to delete this post?")) {
  fetch('https://example.com/posts/123', {
    method: 'DELETE'
  })
  .then(response =&gt; console.log('Post deleted'))
  .catch(error =&gt; console.error('Error:', error));
}
</code></pre>
<ul>
<li><strong>可逆性（软删除）</strong>：对于重要数据，通常使用<strong>软删除</strong>是比较好的，它不会完全移除数据，而是在数据库中标记为已删除。这使得在需要时数据可以恢复。例如，许多电子邮件系统会将已删除的邮件保留在“垃圾箱”文件夹中，直到它们被永久删除。</li>
</ul>
<h3 id="delete">处理 DELETE 请求的最佳实践</h3>
<ol>
<li>
<p><strong>需要认证</strong>：只有经过认证的用户才能执行 DELETE 操作。这能防止未经授权的用户删除他们不拥有的资源。例如，用户只能删除他们自己的数据，而不能删除他人的数据。</p>
<ul>
<li><strong>示例</strong>：在内容管理系统（CMS）中，确保只有文章的作者或管理员才能删除它。</li>
</ul>
</li>
<li>
<p><strong>使用确认步骤</strong>：对于关键操作，确认用户的意图后再继续。这对于无法撤销的操作尤为重要，比如删除账户或永久移除文件。</p>
<ul>
<li><strong>示例</strong>：显示一个提示，如“您确定要删除您的账户吗？此操作无法撤销。”</li>
</ul>
</li>
<li>
<p><strong>记录删除操作</strong>：保留 DELETE 请求的记录，包括谁发起了请求以及何时发生。记录对责任、故障排除和在意外删除时的数据恢复非常重要。</p>
<ul>
<li><strong>示例</strong>：在电商系统中，当产品从目录中移除时，记录发起请求的用户及删除时间等详细信息。</li>
</ul>
</li>
<li>
<p><strong>对关键数据使用软删除</strong>：为可能需要恢复的数据实现软删除机制。这在诸如用户账户等场景中特别有用，因为用户可能在删除后希望恢复他们的数据。</p>
<ul>
<li><strong>示例</strong>：当用户“删除”他们的账户时，将其标记为不活跃或隐藏，而不是完全擦除，允许用户在后续恢复。</li>
</ul>
</li>
<li>
<p><strong>优雅地处理错误</strong>：如果 DELETE 请求失败，服务器应返回适当的错误信息。例如，如果资源不存在或用户无权删除它，服务器应响应“资源未找到”或“未授权操作”等信息。</p>
<ul>
<li><strong>示例</strong>：对不存在用户的 DELETE 请求可能返回 <code>404 Not Found</code> 响应。</li>
</ul>
</li>
<li>
<p><strong>仔细检查 URL 目标</strong>：在发送 DELETE 请求之前，确保 URL 指向正确的资源。错误地指向错误的资源可能导致意外的数据丢失。</p>
<ul>
<li><strong>示例</strong>：如果您正在管理一个待办事项列表并想删除单个任务，确保 URL 特别指向该任务而不是整个列表。</li>
</ul>
</li>
<li>
<p><strong>将结果告知用户</strong>：在成功的 DELETE 请求之后，通知用户资源已被删除。可以通过消息或通知确认操作。</p>
<ul>
<li><strong>示例</strong>：在产品或文章从系统中移除后，显示“项目成功删除”之类的消息。</li>
</ul>
</li>
</ol>
<p>通常，一个成功的 DELETE 请求会返回以下状态代码之一：</p>
<ul>
<li>
<p><strong>200 OK</strong>: 表示删除成功，并包含响应主体（例如，确认删除的消息）。</p>
</li>
<li>
<p><strong>204 No Content</strong>: 请求成功，但响应主体中没有返回内容。这在资源删除后非常常见，因为没有内容需要返回。</p>
</li>
<li>
<p><strong>404 Not Found</strong>: 表示要删除的资源不存在。</p>
</li>
</ul>
<h3 id="delete">DELETE 请求响应示例</h3>
<p>如果 DELETE 请求成功且资源已移除，服务器可能会返回一个 <code>204 No Content</code> 状态：</p>
<pre><code class="language-http">HTTP/1.1 204 No Content
</code></pre>
<p>这个响应告诉客户端资源已成功删除，但不会返回任何额外数据。</p>
<h2 id="head">HEAD 方法</h2>
<p>HEAD 方法与 GET 方法类似，但有一个关键区别：它仅检索资源的头信息，而不是实际内容。</p>
<p>当你发送一个 HEAD 请求时，服务器会回应与 GET 请求相同的头信息，但不发送资源主体（如文本、图片或文件）。这使得 HEAD 很适合在不下载整个内容的情况下检查资源的信息，例如其大小或最后修改日期。</p>
<p>例如，如果你正在管理一个大文件并想在下载前检查其大小，可以使用 HEAD 请求从服务器获取此信息，而无需实际获取文件。</p>
<h3 id="headget">HEAD 与 GET 的比较</h3>
<ul>
<li>
<p><strong>相同的头信息，无内容</strong>: HEAD 请求提供与 GET 请求相同的头信息，如 <code>Content-Type</code>、<code>Content-Length</code>、<code>Last-Modified</code> 等。然而，响应不包含主体——仅包含元数据。</p>
</li>
<li>
<p><strong>请求更快</strong>: 由于不包含主体，HEAD 请求比 GET 请求更快并且消耗的带宽更少。这在你只对资源的细节而非内容感兴趣时特别有用。</p>
</li>
</ul>
<h3 id="head">HEAD 的使用场景</h3>
<ol>
<li>
<p><strong>检查资源可用性</strong>: 你可以使用 HEAD 请求检查资源（如网页或文件）是否存在而不获取内容。例如，如果 URL 返回状态代码为 <code>200 OK</code>，你就知道资源存在。<code>404 Not Found</code> 状态代码则表示它不可用。</p>
</li>
<li>
<p><strong>测试链接</strong>: 如果你管理的网站有大量外部链接，可以用 HEAD 请求测试这些链接是否仍然有效，而无需加载整个页面。如果 HEAD 请求返回错误代码，你就知道链接已断开。</p>
</li>
<li>
<p><strong>获取文件元数据</strong>: 如果你在处理大文件，可能需要在下载前检查它们的大小。HEAD 请求允许你收集元数据，如文件大小 (<code>Content-Length</code>) 和类型 (<code>Content-Type</code>)，而不提取整个文件。</p>
</li>
<li>
<p><strong>优化缓存</strong>: 浏览器和应用程序可以使用 HEAD 请求检查资源自缓存以来是否已更新。服务器返回如 <code>Last-Modified</code> 或 <code>ETag</code> 这类头信息，如果这些值未改变，则可以使用缓存版本，从而节省带宽和时间。</p>
</li>
<li>
<p><strong>API 效率</strong>: 当客户端需要验证数据是否存在而不下载整个响应时，HEAD 请求在 API 中很有用。例如，请求可以检查数据库中是否存在记录而无需获取完整细节。</p>
</li>
<li>
<p><strong>服务器健康监控</strong>: HEAD 请求可以用来测量服务器性能。通过测试响应速度而不下载内容，开发者可以监控服务器响应时间、检查问题或确定服务器是否正常运行。</p>
</li>
</ol>
<h3 id="head">使用 HEAD 的最佳实践</h3>
<ul>
<li>
<p><strong>高效测试</strong>: HEAD 非常适合验证资源或测试 API 端点，而不下载不必要的数据。</p>
</li>
<li>
<p><strong>缓存验证</strong>: HEAD 请求有助于缓存验证，确保资源是最新的，而不消耗带宽。</p>
</li>
<li>
<p><strong>无副作用</strong>: 像 GET 一样，HEAD 应该是安全且幂等的，即不应改变资源的状态。它纯粹用于检索信息。</p>
</li>
</ul>
<h2 id="options">OPTIONS 方法</h2>
<p>OPTIONS 方法用于查明对特定资源允许哪些操作。它允许客户端（如浏览器或 API）向服务器询问，“我可以对这个资源执行什么操作？” 服务器则会列出它对该资源支持的 HTTP 方法，如 GET、POST、PUT、DELETE 等。</p>
<p>OPTIONS 不会对资源本身执行任何操作。相反，它提供有关客户端可以执行什么操作的信息。这在你想检查允许哪些操作而不实际提出改变或检索数据的请求时很有用。</p>
<p>例如，如果你正在使用 API 并想查看它是否在特定端点支持 DELETE 方法，可以发送 OPTIONS 请求以获取该信息，而不影响资源。</p>
<h3 id="http">检索目标资源支持HTTP请求的方法</h3>
<ol>
<li>
<p><strong>发送 OPTIONS 请求</strong>：客户端向服务器发送 OPTIONS 请求，通常针对特定 URL。该请求用作关于允许对该端点上的资源执行什么操作的查询。</p>
</li>
<li>
<p><strong>服务器的响应</strong>：服务器通过 <code>Allow</code> 响应头返回该资源支持的 HTTP 方法。例如，可能返回 <code>Allow: GET, POST, DELETE</code>，表示可以使用这些方法。</p>
</li>
<li>
<p><strong>测试方法</strong>: 如果不确定服务器是否支持特定方法（如 PATCH 或 DELETE），可以先发送 OPTIONS 请求进行检查。这可以避免尝试服务器不支持的方法，从而避免错误。</p>
</li>
</ol>
<pre><code class="language-http">OPTIONS /api/resource HTTP/1.1
Host: example.com
</code></pre>
<p>服务器响应：</p>
<pre><code class="language-http">HTTP/1.1 200 OK
Allow: GET, POST, DELETE
</code></pre>
<h3 id="optionscors">OPTIONS 方法在跨域资源共享（CORS）中的使用</h3>
<p>OPTIONS 方法最常见的用途之一是在处理**跨域资源共享（CORS）**时。CORS 是一种安全特性，保证一个域名上的资源不会被另一个域名的网页不当访问。</p>
<h4 id="cors">CORS 和预检请求</h4>
<p>当浏览器需要进行跨域请求（例如，从<a href="http://domainA.com"><code>domainA.com</code></a>请求到<a href="http://api.domainB.com"><code>api.domainB.com</code></a>），浏览器首先会向目标服务器发送一个被称为<strong>预检请求</strong>的<strong>OPTIONS 请求</strong>。预检请求用于检查实际请求是否符合服务器的 CORS 策略。</p>
<ol>
<li>
<p><strong>预检请求</strong>：浏览器在实际请求（如 POST 或 PUT）前发送一个 OPTIONS 请求。该请求询问服务器允许哪些方法、哪些域名可以访问资源，以及是否允许特定的头信息或凭据。</p>
</li>
<li>
<p><strong>服务器响应</strong>：服务器用 CORS 头信息回应，例如 <code>Access-Control-Allow-Methods</code>、<code>Access-Control-Allow-Origin</code> 和 <code>Access-Control-Allow-Headers</code>。这些信息告知浏览器该请求是否可以继续、允许哪些方法或域名。</p>
<p>示例响应：</p>
<pre><code class="language-http">HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://domainA.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type
</code></pre>
</li>
<li>
<p><strong>确保安全性</strong>：CORS 和预检 OPTIONS 请求确保只有在服务器许可的情况下才允许跨域请求。如果没有此安全步骤，网站可能会对其他域名进行未经授权的请求。</p>
</li>
<li>
<p><strong>处理复杂请求</strong>：如果请求包含自定义头信息、使用非简单 HTTP 方法（如 GET 或 POST）或发送诸如 cookies 的凭据，浏览器会自动发送 OPTIONS 预检请求。如果服务器拒绝请求（即返回头信息不允许该操作），浏览器会拦截请求。</p>
</li>
</ol>
<h4 id="">简化的工作流程</h4>
<ul>
<li>
<p><strong>浏览器</strong>： "我可以向[<code>api.domainB.com</code>]请求吗？"</p>
</li>
<li>
<p><strong>服务器</strong>： "可以，你可以使用 <code>GET</code> 和 <code>POST</code>，但只能从[<code>domainA.com</code>]并且使用这些头信息。"</p>
</li>
<li>
<p><strong>浏览器</strong>： 如果响应允许，则继续实际请求。</p>
</li>
</ul>
<h3 id="options">OPTIONS 方法的使用场景</h3>
<ul>
<li>
<p><strong>发现可用方法</strong>：对于开发者来说有用，可以在进行操作之前检查一个资源支持哪些 HTTP 方法。</p>
</li>
<li>
<p><strong>CORS 预检</strong>：在网络安全中至关重要，确保跨域请求是经过适当授权的。</p>
</li>
<li>
<p><strong>提高 API 效率</strong>：API 可以通过 OPTIONS 公开一个资源支持的方法，使客户端更容易理解可以执行哪些操作。</p>
</li>
</ul>
<p>因此，OPTIONS 方法在管理请求权限和提高安全性方面对网络应用程序是必不可少的，特别是在跨域场景中。</p>
<h2 id="trace">TRACE 方法</h2>
<p>TRACE 方法用于调试网络应用程序和测试请求如何通过网络。当你发送一个 TRACE 请求时，会触发一个回环测试，服务器将原样返回它接收到的请求。这有助于开发者查看请求在通过不同系统（如防火墙或代理）到达服务器前是否被修改。</p>
<p>简单来说，TRACE 允许你追踪请求从客户端（如浏览器或 API 工具）到服务器再返回的路径。这种方法对于识别请求传输过程中的问题很有用。</p>
<h3 id="">理解回环测试</h3>
<p>回环测试指的是通过 TRACE 查看请求在跨网络时如何处理，以检查原始请求是否保持完整。具体如下：</p>
<ol>
<li>
<p><strong>发送 TRACE 请求</strong>：你向服务器发送一个 TRACE 请求。这个请求通常较小，包含基本信息如方法、URL 和头信息。它不像 POST 或 PUT 方法那样携带额外数据或负载。</p>
</li>
<li>
<p><strong>服务器响应</strong>：服务器不是以资源作为响应，而是逐字返回它接收到的请求。这包括 HTTP 方法、URL、头信息和原始请求中的其他内容。服务器不修改或处理请求，只是原样返回。</p>
</li>
<li>
<p><strong>追踪路径</strong>：当 TRACE 响应返回时，你可以看到请求经过的完整路径，包括请求头信息或内容中的任何变化。对以下问题的诊断特别有用：</p>
<ul>
<li>
<p><strong>代理服务器</strong>：如果你的请求在到达目的地之前经过一个或多个代理服务器，TRACE 可以显示这些代理是否更改了请求头信息或内容。</p>
</li>
<li>
<p><strong>网络防火墙</strong>：某些网络防火墙可能在请求经过时添加或修改头信息。TRACE 可以揭示这些修改。</p>
</li>
<li>
<p><strong>错误跟踪</strong>：如果请求未按预期运行，TRACE 可以帮助追踪传输过程中出现问题的地方。</p>
</li>
</ul>
</li>
<li>
<p><strong>有效调试</strong>：TRACE 在调试网络应用程序或 API 特别有帮助。如果你的应用程序因路由、代理或服务器配置引发错误，TRACE 让你看到未修改的请求，从而更容易定位问题。</p>
</li>
</ol>
<h3 id="trace">TRACE 的安全问题</h3>
<p>虽然 TRACE 在调试中很有用，但通常被视为安全风险，并且由于多个原因在大多数服务器上经常被禁用：</p>
<ol>
<li>
<p><strong>跨站脚本攻击 (XSS)</strong>：TRACE 方法可能会暴露请求头中的敏感信息，如 Cookie 或认证令牌。恶意行为者可能利用 TRACE 来捕获这些详细信息，导致安全漏洞，特别是在存在跨站脚本攻击 (XSS) 等漏洞时。这使 TRACE 成为攻击者试图窃取用户数据的潜在目标。</p>
</li>
<li>
<p><strong>请求修改暴露</strong>：由于 TRACE 显示了对请求所做的所有修改，它还可以揭示内部代理和防火墙如何处理请求。这可能让攻击者深入了解网络内部运作，为他们策划进一步攻击提供便利。</p>
</li>
<li>
<p><strong>出于安全目的禁用 TRACE</strong>：基于这些原因，TRACE 通常在大多数网络服务器上被禁用以防滥用。在许多现代 Web 应用程序中，存在更为安全的方法用于调试请求和追踪网络路径，因此在日常使用中 TRACE 很少必要。</p>
</li>
<li>
<p><strong>更安全的替代方案</strong>：开发人员可以使用现代 Web 框架和 API 内置的更安全的诊断工具和日志功能。这些替代方案能提供类似的见解而不会带来 TRACE 相关的安全风险。</p>
</li>
</ol>
<h2 id="connect">CONNECT 方法</h2>
<p>CONNECT 方法主要用于通过中介（通常是代理服务器）在客户端和服务器之间建立隧道。当客户端发送 CONNECT 请求时，服务器创建一个隧道，允许加密数据在客户端和目标服务器之间流动。这种方法对于保障连接安全至关重要，特别是当涉及 HTTPS 时。</p>
<p>CONNECT 本身并不处理任何实际数据。它的作用是建立一个安全通信路径，允许加密信息通过代理而不被修改或检查。</p>
<h3 id="connect">CONNECT 的工作原理</h3>
<ol>
<li>
<p><strong>发送 CONNECT 请求</strong>：客户端（如网络浏览器）向代理服务器发送 CONNECT 请求。该请求包含目标服务器的主机名和端口，通常是标准的 HTTPS 端口 (443)。例如，当访问 <a href="https://example.com"><code>https://example.com</code></a> 时，浏览器向代理服务器发送 CONNECT 请求，请求其连接到该域的端口 443。</p>
</li>
<li>
<p><strong>建立隧道</strong>：代理服务器在收到 CONNECT 请求后，建立一条到目标服务器的隧道。该隧道允许加密通信无干扰地通过。代理仅在客户端和目标之间转发流量，充当中继的角色。</p>
</li>
<li>
<p><strong>加密通信</strong>：隧道建立后，客户端和目标服务器可以使用安全加密协议（如 TLS，用于 HTTPS）直接通信。由于数据在客户端和服务器之间是加密的，代理无法解密或修改它。</p>
</li>
<li>
<p><strong>安全数据传输</strong>：通过 CONNECT 方法，敏感数据（如登录凭证、个人信息或金融交易）可以在客户端和服务器之间安全传输，即使是通过代理。加密隧道确保数据保持机密和完整。</p>
</li>
</ol>
<h3 id="connect">CONNECT 请求和响应示例</h3>
<ul>
<li>
<p><strong>CONNECT 请求</strong>：</p>
<pre><code class="language-http">  CONNECT example.com:443 HTTP/1.1
  Host: example.com
</code></pre>
</li>
<li>
<p><strong>代理响应</strong>（若隧道成功建立）：</p>
<pre><code class="language-http">  HTTP/1.1 200 Connection Established
</code></pre>
</li>
</ul>
<h3 id="connect">CONNECT 隧道</h3>
<p>在此环境中，<strong>隧道</strong>一词指的是通过代理在客户端和目标服务器之间创建直接、安全的链接。代理作为中间人存在，但不会干涉或访问通过隧道传输的加密数据。</p>
<h4 id="">隧道过程的步骤：</h4>
<ul>
<li>
<p><strong>发送 CONNECT 请求</strong>：客户端向代理发送 CONNECT 请求，指定目标服务器和端口（例如，HTTPS 的 443 端口）。</p>
</li>
<li>
<p><strong>代理设置隧道</strong>：代理服务器在客户端和目标服务器之间建立安全隧道，在两个端点之间转发流量。</p>
</li>
<li>
<p><strong>开始加密通信</strong>：客户端和目标服务器通过加密隧道直接通信，使用 HTTPS 或其他加密协议。代理转发加密流量但无法访问或修改。</p>
</li>
</ul>
<h3 id="connect">CONNECT 方法的典型使用案例</h3>
<ol>
<li>
<p><strong>通过代理的 HTTPS</strong>：CONNECT 方法最常见的用途之一是启用<strong>通过代理的 HTTPS 流量</strong>。在许多公司或网络环境中，互联网流量必须通过代理服务器。对于使用 HTTPS 的安全网站，CONNECT 方法允许代理服务器建立隧道，在不检查数据的情况下将加密流量从客户端转发到目标服务器。</p>
<ul>
<li><strong>示例</strong>：当您从公司网络访问一个安全的银行网站时，您的浏览器可能需要通过公司代理。CONNECT 方法用于在您的浏览器和银行网站之间建立加密隧道，确保敏感数据（如登录凭证）安全地通过代理。</li>
</ul>
</li>
<li>
<p><strong>VPN 和安全通道</strong>：**VPN（虚拟专用网络）**服务也依赖类似的隧道技术来安全地加密和路由互联网流量。一些 VPN 服务使用 CONNECT 创建安全隧道，确保用户与互联网之间传输的数据加密且不被窃听。</p>
</li>
<li>
<p><strong>访问被封锁的内容</strong>：在某些网站被封锁的环境中（例如，学校或办公室），CONNECT 有时可以用于通过代理建立隧道来绕过限制。虽然这种做法可能违反网络政策，但它说明了 CONNECT 如何用于建立安全、不受监控的访问。</p>
</li>
<li>
<p><strong>自定义代理</strong>：开发人员可能会设置<strong>自定义代理</strong>来为应用程序路由流量以提高性能或安全性。在这种情况下，CONNECT 允许 HTTPS 或其他安全流量通过代理传输，同时保持隐私和安全，因为代理服务器无法访问隧道内的加密数据。</p>
</li>
</ol>
<p>虽然 CONNECT 对于安全通信是必不可少的，但它也带来了一些安全挑战：</p>
<ul>
<li>
<p><strong>绕过内容过滤器</strong>：由于 CONNECT 创建了代理无法检查的加密隧道，它可以被用来绕过内容过滤系统。这使用户能够访问受限的网站或服务，可能违反组织政策。</p>
</li>
<li>
<p><strong>隧道传输恶意流量</strong>：恶意行为者可以利用 CONNECT 通过代理隧道传输有害或未经授权的流量。由于流量是加密的，防火墙和安全系统可能无法检测到恶意活动。</p>
</li>
<li>
<p><strong>缓解措施</strong>：许多组织通过密切监控和限制 CONNECT 方法的使用来解决这些风险。一些代理执行 <strong>SSL 截取</strong> 来解密和检查 HTTPS 流量，尽管这会引发隐私问题并可能影响用户安全。</p>
</li>
</ul>
<h2 id="">结论</h2>
<p>HTTP 方法对于实现 web 应用程序和服务器之间的通信至关重要。从 GET 到 CONNECT，每个方法都为特定任务设计，如发送数据、检索信息、更新资源或建立安全连接。为任务选择正确的方法可以提高应用程序的效率和安全性。</p>
<p>GET 适合检索数据，POST 和 PUT 帮助创建和更新，PATCH 处理部分更新，而 DELETE 用于移除资源。HEAD 检查响应头而不检索内容，OPTIONS 显示支持的方法，TRACE 和 CONNECT 有助于调试和安全通信。</p>
<p>使用适当的 HTTP 方法确保您的应用程序高效、安全地运行，为用户提供流畅的体验。</p>
<p>如果您有任何问题或建议，请随时在 <a href="https://ng.linkedin.com/in/joan-ayebola">LinkedIn</a> 上联系。如果您喜欢这篇内容，请考虑 <a href="https://www.buymeacoffee.com/joanayebola">buy me a coffee</a> 来支持我创作更多对开发者友好的内容。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何转行找到理想的技术工作 ]]>
                </title>
                <description>
                    <![CDATA[ 在这篇文章中，我将探讨一些能够帮助你在你的目标公司的面试中取得好成绩并最大限度地获得工作机会的专业建议。 自我介绍 — 从华尔街到谷歌总部 2019年3月31日，我被一家初创对冲基金公司裁员了。在过去的10多年里，我一直是金融行业的一名量化软件开发人员，但我不想再做一份同样的工作。我开始追寻自己热切想去做的事，在人工智能/机器学习/深度学习(AI/ML/DL)领域寻找我的下一份工作。 在接下来的六个月里，我为了这个目标付出了非常大的努力。到2019年9月时，我已经有了多个与机器学习相关的现场面试和offer，包括来自谷歌和Facebook的offer。 这篇文章讲述了我是如何做到这件事的，希望它能鼓励和帮助其他潜在的想要换工作的人完成这种转变。通过这种转变，你可能会获得能力上和经济上的双重回报。 Googleplex:谷歌的全球总部 文章概览 如果你时间不够，这里有一个对我很有效的五大秘诀：  * 展示: 做一个你感兴趣的项目，并在网上展示出来  * 学习: 集中练习算法与数据结构的程序设计问题和系统设计问题的解答方法  * 应用: 利用多种渠道获得面试机会  * 面试 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/career-switchers-guide-to-your-dream-tech-jo/</link>
                <guid isPermaLink="false">6672da4f5c4dc803d18ad2b2</guid>
                
                    <category>
                        <![CDATA[ 职业发展 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Wed, 19 Jun 2024 13:26:53 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/06/googleplex.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/career-switchers-guide-to-your-dream-tech-job/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">A Career Switcher’s Guide to Landing Your Dream Tech Job</a>
      </p><!--kg-card-begin: markdown--><p>在这篇文章中，我将探讨一些能够帮助你在你的目标公司的面试中取得好成绩并最大限度地获得工作机会的专业建议。</p>
<h1 id="">自我介绍 — 从华尔街到谷歌总部</h1>
<p>2019年3月31日，我被一家初创对冲基金公司裁员了。在过去的10多年里，我一直是金融行业的一名量化软件开发人员，但我不想再做一份同样的工作。我开始追寻自己热切想去做的事，在人工智能/机器学习/深度学习(AI/ML/DL)领域寻找我的下一份工作。</p>
<p>在接下来的六个月里，我为了这个目标付出了非常大的努力。到2019年9月时，我已经有了多个与机器学习相关的现场面试和offer，包括来自谷歌和Facebook的offer。</p>
<p>这篇文章讲述了我是如何做到这件事的，希望它能鼓励和帮助其他潜在的想要换工作的人完成这种转变。通过这种转变，你可能会获得能力上和经济上的双重回报。</p>
<p><img src="https://miro.medium.com/max/3264/1*nhcfyWkKLu_q4mBTetSHyw.jpeg" alt="1*nhcfyWkKLu_q4mBTetSHyw" width="600" height="400" loading="lazy"></p>
<p>Googleplex:谷歌的全球总部</p>
<h1 id="">文章概览</h1>
<p>如果你时间不够，这里有一个对我很有效的五大秘诀：</p>
<ul>
<li><strong>展示</strong>: 做一个你感兴趣的项目，并在网上展示出来</li>
<li><strong>学习</strong>: 集中练习算法与数据结构的程序设计问题和系统设计问题的解答方法</li>
<li><strong>应用</strong>: 利用多种渠道获得面试机会</li>
<li><strong>面试</strong>: 在电话面试和现场面试中保持冷静</li>
<li><strong>谈判</strong>: 在工作机会谈判与匹配团队时，知道自己想要什么</li>
</ul>
<p>如你所见，这是一个非常常见的五步秘诀，然而完成每一步都需要付出很多努力。如果想了解更多细节的话，就继续读下去吧。</p>
<h1 id=""><a href="https://www.linkedin.com/in/dctian">关于我</a></h1>
<p>这里有一些关于我的介绍，因为我认为这个指南对和我有相似背景的人最有帮助。也就是说，我是一名非科技行业经验丰富的软件工程师/开发人员，希望跳槽到西海岸(湾区或西雅图)的一家顶级科技公司。如果你没有软件工程的工作经验，上面的方法应该仍然适用于你，你只需要比那些有编程经验的人更加努力。</p>
<p>我毕业于加拿大滑铁卢大学，是一名计算机工程师。在我早期的职业生涯中，我曾在微软和甲骨文担任软件工程师。MBA毕业后，我进入了量化金融行业。</p>
<p>在过去的10多年里，我曾在美国银行、瑞银集团、Citadel和一些较小的公司担任量化软件开发人员/量化分析师。但当我在2016年第一次偶然发现机器学习/深度学习时，我就爱上了它。我知道我最终想要在AI/ML领域找到一份工作。</p>
<p>2019年4月，我开始为转行做全职准备。下周我将搬到旧金山湾区加入谷歌，它是人工智能和机器学习领域的领导者之一。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/image-4.png" alt="image-4" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.linkedin.com/in/dctian">https://www.linkedin.com/in/dctian</a></p>
<h1 id="">迈向成功的五个简单步骤</h1>
<h2 id="">第一步，展示: 做一个你感兴趣的项目，并在网上展示出来</h2>
<p>2019年2月，我最好的朋友Igor鼓励我创造一个很酷的项目并写些东西记录它。那时候我没有任何好的想法，同时我从未在网上发表过任何东西，也不知道怎么开始做这些事。</p>
<p>但是在四月初，当我在墨西哥和我的家庭一起度假时，某天晚上我突然有一个“我想造一辆自动驾驶汽车”的想法。考虑到我之前没有机器人技术或电子技术方面的经验，我决定先开始建造一辆可以在我的客厅里跑的<a href="https://towardsdatascience.com/tagged/deep-pi-car">深度学习/自动驾驶机器人汽车，DeepPiCar</a>。</p>
<p>一个月后，当我成功造出来了这辆自动驾驶汽车时，我开始在“<a href="https://towardsdatascience.com/">Towards Data Science</a>”上写一篇一共六个部分的博文来教其他人如何造一辆这样的车。我的博文在几周的时间内获得了超过10000次的阅读量，世界各地的读者对这篇文章的回应非常热烈。我感觉我两周的努力（一周用来造车，一周用来写博文）都是值得的。</p>
<p><img src="https://miro.medium.com/max/1776/0*PqGFISPKqUjwc_Ki.jpeg" alt="0*PqGFISPKqUjwc_Ki" width="600" height="400" loading="lazy"></p>
<p>在我的客厅里跑的DeepPiCar</p>
<p>然而，直到我参加面试和获得团队匹配的要求前，我一直没有意识到这件事对我的职业生涯产生了全面的影响。当面试官问我:“你为什么想从目前的行业转行?”时，我已经准备好了回答这个问题的话题!</p>
<p>我不仅能够告诉他们我对AI/ML的热情，而且当我从背包里拿出我的车(是的，真正的机器人车)并把它放在面试官手中时，我可以从他们的眼睛里看到兴奋!他们中的许多人甚至说，他们很想和自己的孩子一起造一辆这样的车。然后我告诉他们，我已经在网上发布了完整的步骤说明和源代码(我的简历中有链接)，供他们参考。我很确定他们不会把我的简历扔进回收箱。: -)</p>
<p>我的一些朋友们想让我推荐一些可以开展的好的项目，这是我给出的一些建议：</p>
<ul>
<li>如果你想转职去做前端开发，那就用流行的开源框架（例如<a href="https://reactjs.org/">React</a>、<a href="https://angular.io/">Angular</a>或<a href="https://vuejs.org/">Vue.js</a>等）搭建一个炫酷的网站。</li>
<li>如果你对服务端开发感兴趣，尝试使用分布式服务技术创建网络爬虫或搜索引擎，如<a href="http://cassandra.apache.org/">Cassandra</a>， <a href="https://zookeeper.apache.org/">ZooKeeper</a>， <a href="https://memcached.org/">Memcached</a>，和<a href="https://www.elastic.co/webinars/getting-started-elasticsearch">Elastic search</a>等。</li>
<li>如果你想做移动端开发，尝试创建并发布一个苹果/安卓应用。请注意，在苹果的App Store上发布应用比在谷歌的Play Store上发布应用要花费更长的时间。</li>
<li>如果你对人工智能/机器学习感兴趣，尝试使用现有的计算机视觉或自然语言处理深度学习模型之一，并尝试从中制作产品。例如，我使用了一个预训练的<a href="https://arxiv.org/abs/1512.02325">目标检测模型，SSD</a>，并将其改编为我的DeepPiCar的交通标志检测器。如果您是AI / ML新手，请务必阅读<a href="https://towardsdatascience.com/deeppicar-part-1-102e03c83f2c?source=---------5-----------------------">DeepPiCar的第一部分</a>，在那里我列出了几个ML课程和资源，以帮助您开始学习。</li>
<li>如果你对机器人技术感兴趣，试着用树莓派或英伟达Jetson Nano制造一些很酷的机器人。一定要使用新的<a href="https://www.raspberrypi.org/products/raspberry-pi-4-model-b/">Raspberry Pi 4</a>或<a href="https://developer.nvidia.com/embedded/jetson-nano-developer-kit">Jetson Nano</a>。树莓派更被制造商广泛使用，因此具有更好的技术支持，而Jetson Nano自带GPU，因此可能更适合<a href="https://www.sparkfun.com/products/15365">深度学习项目</a>。</li>
<li>如果你没有任何有关项目的想法，不要绝望。如果你对人工智能/机器学习感兴趣，你可以为上面提到的开源项目之一做出贡献，也可以为<a href="http://tensorflow.org/">TensorFlow</a>或<a href="http://pytorch.org/">PyTorch</a>做出贡献。你可能认为您需要成为一名专家才能为这些知名的开源项目做出贡献，但实际上并不需要。每个开源项目中都有相当多的小特性和简单的错误修复是专门为新手准备的，让新手可以先动手做。这样，你就可以在处理更复杂的问题之前熟悉代码库和提交流程。</li>
</ul>
<p><img src="https://miro.medium.com/max/1688/0*Ch5vkdlhxP1uRsbe.png" alt="0*Ch5vkdlhxP1uRsbe" width="600" height="400" loading="lazy"></p>
<p>顶尖的开源项目</p>
<p>一旦你完成了你的项目，你一定要让全世界都知道这件事！你以为只是在GitHub上发布完成的代码就足够了吗？不！因为大多数人更喜欢先阅读博客来了解代码的功能。</p>
<p>优秀的、精心设计的博文对于你的项目，更进一步来讲对于你来说，是一份很好的营销材料。是的，用漂亮的图片和图表写精致的博客需要很长时间。我花了整整一个五月写了6篇文章。我估计我总共花费了200个小时或每篇文章花费了大约30个小时。如果你是全职工作，也许可以写一篇写得很好的博文作为你项目的概述。</p>
<p><img src="https://miro.medium.com/max/1080/1*blXkdzZ0Z2T9cezJmmX96g.png" alt="1*blXkdzZ0Z2T9cezJmmX96g" width="600" height="400" loading="lazy"></p>
<p>DeepPiCar的博客系列</p>
<p>有一件事你需要牢记：你<strong>必须</strong>享受你正在做的事情，而不是仅仅为了找到一份工作而做这个项目。如果你真的对自己所做的事情充满激情，那么当你和面试官交谈时，这件事情就会成为一个亮点。</p>
<h2 id="">第二步：聪明并努力地学习</h2>
<h3 id="">算法与数据结构</h3>
<p>如果你需要掌握一项技术面试技能，那就是算法和数据结构（A&amp;DS）编码技能。无论你面试的是大公司还是小公司，他们都会详细询问你的编程技能，尽管大多数公司并不关心你使用哪种编程语言。</p>
<p>我最喜欢的编程语言是C++和Python，但对于编码面试，我推荐Python。C++是非常冗长的，而与C++相比它是如此简洁，你可以用这么少的行做这么多的事情。有很多资源可以让你为算法和数据结构（A&amp;DS）做准备。我使用了以下资源：</p>
<p><a href="https://amzn.to/2LLxmSm"><strong>Cracking the Coding Interview Book</strong> <strong>(CtCI)</strong></a>: 这本书大约有700页，并且只卖30美元。如果你需要复习A&amp;DS的概念，这是一个很好的起点。除了概念之外，它还提供了一系列简单到中等水平的练习题和完整的答案。我花了2 - 3周的时间，花了1-2天的时间在每个A&amp;DS相关章节上。我跳过了关于数学/脑筋急转弯/OOD/C++/Java/SQL的章节，因为它们与A&amp;DS无关。</p>
<p><img src="https://miro.medium.com/max/350/0*AGWwwLapmQvGVQ6o.jpg" alt="0*AGWwwLapmQvGVQ6o" width="600" height="400" loading="lazy"></p>
<p><a href="http://leetcode.com/"><strong>LeetCode.com</strong></a>：我也大量使用LeetCode (LC)，因为我觉得CtCI没有给我足够的练习。Leetcode可能是最全面和有组织的编程面试问题在线数据库，有1000多个简单，中等和困难的问题。每个问题都有一个唯一的数字(例如，LC#1是TwoSum)。许多网上帖子只是简单地把他们的面试问题称为LC# xxx，而不是把整个问题打出来。</p>
<p>我强烈建议每月花30美元注册LeetCode高级版，因为它可以让你访问所有的LC问题和解决方案。我觉得这很有帮助，我以99美元的价格注册了年度会员，因为我认为这是“我大脑的健身房会员费”。</p>
<p>我完成了100多个LeetCode问题(35个简单，60个中等，14个困难)。我实际面试中90%的问题，或者它们的细微变化，都可以在LeetCode上找到。在大多数情况下，我直到事后才知道它们来自Leetcode。但是由于我练习了足够多的LC中等难度的问题，所以我能够解决相同难度的问题。最后，我觉得我永远不能做足够的LC中等难度的问题，因为直到今天，我仍然与一些LC中等难度的问题斗争。</p>
<p><img src="https://miro.medium.com/max/1360/0*nS7N-kMs_oFwiTcG.png" alt="0*nS7N-kMs_oFwiTcG" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.youtube.com/watch?v=l3hda49XcDE&amp;list=PLrmLmBdmIlpuE5GEMDXWf0PWbBD9Ga1lO"><strong>Tushar Roy’s LeetCode Hard Solutions</strong></a>: Tushar实际上是苹果公司的工程师经理。他做这些视频的时候还是个软件工程师。我想说，看他的视频不仅教会了我这些算法，还教会了我如何清晰地呈现复杂的想法/算法。当我在现场面试时用白板写代码时，这给了我很大的帮助。</p>
<p><img src="https://miro.medium.com/max/1280/0*sTf-RyIfjStnTdf1.jpg" alt="0*sTf-RyIfjStnTdf1" width="600" height="400" loading="lazy"></p>
<p>请注意，大多数面试官会给LC中等水平的问题，因为要求某人在20-30分钟内解决中等水平的问题是相当公平的。我建议你在第一次电话面试前至少做20个简单的问题和30个中等的LC问题。我第一次电话面试是在我写完CtCI的书之后，在我开始写LC之前的时候，而我很快就被淘汰了。</p>
<p>我也会在每次做练习题的时候计时，就好像我在面试一样。但是我使用的是带有自动补全和调试器的IDE。你不会在实际的面试中得到这些，所以我在练习时通常会把时间限制得更紧，每道题大约30分钟，而不是45分钟。你可以选择只使用常规的文本编辑器，甚至是白板来模拟真实的考试环境。这取决于你的个人喜好。</p>
<h3 id="">系统设计</h3>
<p>你需要掌握的第二最重要的技能是系统设计。通常来说，一个现场面试环节会让你设计一个大型分布式系统，比如Netflix视频服务系统，或者WhatsApp即时通讯系统，或者Instagram故事等等。</p>
<p>这是一项同时测试你的硬实力与软实力的技能测试。对于硬实力的部分，你需要了解许多常用的分布式系统组件，例如分布式数据库、分布式内存缓存、分布式配置管理器、分布式文件存储和分布式搜索引擎等，以及何时使用它们是正确的。而对于软实力的部分，你需要能够画出清晰的图表来说明你的设计，并口头讨论和维护你的设计。有人告诉我，如果你通过了现场面试，系统设计面试会严重影响你获得的职级评定结果(也就是说，它可能决定你得到的是4级还是5级工程职位)。</p>
<p>下面是我在准备这一部分的过程中使用的一些资料:</p>
<ul>
<li><a href="https://amzn.to/2LLxmSm"><strong>Cracking the Coding Interview Book(CtCI)</strong></a>: 书中有一章讨论了系统设计，但这只包括了系统设计中最基础的部分。</li>
<li><a href="http://youtube.com/watch?v=quLrc3PbuIw&amp;list=PLMCXHnjXnTnvo6alSjVkgxV-VH6EPyvoX"><strong>Gaurav Sen 的系统设计 YouTube 频道</strong></a>: 在阅读了CtCI的系统设计章节后，我不满足于其中包含的知识。然后我偶然发现了 Gaurav Sen 的YouTube频道，里面有22个视频。我把他所有的视频都下载到我的笔记本电脑上，以便在长途飞机上观看。哦我的老天爷呐，这个兄弟的视频真的很棒，其中对于系统设计知识的讲解非常清楚！这些视频中大约有10个视频是对单个分布式系统组件的深入描述，其余的是关于如何构建NetFlix, Tinder或Facebook系统等的实际用例讨论。</li>
</ul>
<p><img src="https://miro.medium.com/max/1280/0*rIMPbQS57T_u5U-R.jpg" alt="0*rIMPbQS57T_u5U-R" width="600" height="400" loading="lazy"></p>
<ul>
<li><a href="https://www.youtube.com/watch?v=UzLMhqg3_Wc&amp;list=PLrmLmBdmIlps7GJJWW9I7N0P0rB0C3eY2"><strong>Tushar Roy 的系统设计视频</strong></a><strong>:</strong> 虽然Tushar非常擅长LeetCode上的难题，但他也有5个很棒的系统设计视频。我建议在Gaurav的视频之后再看这些视频，因为Gaurav为你提供了系统设计的坚实基础，而Tushar的视频则提供了一些进阶知识。</li>
</ul>
<p>就我个人而言，由于我之前从事过相当多的分布式系统，所以即使我没有使用上面视频中提到的一些新技术，我也能够很快地掌握总体思想。在看了足够多的视频后，我能够将大多数分布式系统概括为以下几层:</p>
<ul>
<li>客户端（PC端/移动端应用程序，浏览器）。</li>
<li>一个用来处理客户端请求的分布式负载均衡器。</li>
<li>基于位置的内容分发网络(CDN)或内存缓存，快速交付大型和相对静态的内容(视频和图像)到地理上更接近CDN/缓存的客户端。</li>
<li>一系列微服务来处理各种业务逻辑，如认证、服务/保存用户内容、在用户之间传递消息。微服务之间的通信是通过分布式消息系统发送的。</li>
<li>用于保存用户内容和消息的分布式数据库。你可以选择在数据库之前添加分布式缓存层，以提高读/写吞吐量。</li>
</ul>
<h3 id="">行为型问题</h3>
<p>我认为行为型问题是面试中最简单的部分。不幸的是，它们在面试评估中所占的比重也是最小的。科技公司当然知道，在行为面试中“表现得很好”和“说正确的话”是很容易的，但要训练自己成为一个优秀的程序员却要困难得多。</p>
<p>对于一个跨行找工作的人来说，最重要的问题当然是“你为什么想换行业？”希望你在步骤1中完成的项目可以帮助你通过展示你的项目和你对新行业以及你申请的公司的热情来完美回答这个问题。</p>
<p><img src="https://miro.medium.com/max/1280/0*7R7K7tRug6GBHdfT.jpg" alt="0*7R7K7tRug6GBHdfT" width="600" height="400" loading="lazy"></p>
<p>以下是一些其他需要准备的行为型问题：</p>
<ul>
<li>你为什么想为我的公司/我的团队工作？</li>
<li>举一个你达到目标的例子，并告诉我你是如何实现的。</li>
<li>举一个你未能达到目标的例子，并告诉我你是如何处理的。</li>
<li>描述一个你在工作中遇到的压力情况，以及你是如何处理的。</li>
<li>你是否曾遇到过没有足够工作可做的情况？</li>
<li>你是否曾犯过错误？你是如何处理的？</li>
<li>描述一个你与同事/老板/下属有分歧的情况，以及你是如何处理的？</li>
</ul>
<p>所有回答行为型问题的关键是以积极的态度结束。例如，即使你被问到与同事发生分歧或职业生涯中的失败，也要如实描述发生了什么，但更重要的是提到你从中学到了什么。谈谈当类似情况再次出现时，它如何帮助你成为一个更有效的团队成员/工程师。</p>
<h2 id="">第三步：通过多个渠道申请</h2>
<p>我在六月初（在我发布DeepPiCar博客之后的那段时间）开始密切关注湾区的机器学习工程师职位市场。同时，我开始了一个月的全职高强度编码准备（总计约200小时）。</p>
<p>到六月底，尽管我感觉自己还没完全准备好，但我开始向公司投递简历。原因是从我递交简历到电话面试之间需要有1-2周的准备时间。通过多个渠道，我联系了超过20家公司，并进行了超过30次电话面试。有时同一家公司会进行多次电话面试。</p>
<h3 id="">简历 —— 把相关的内容放在前面</h3>
<p>我不会详细讨论简历写作，因为已经有很多优秀的文章讨论过这个话题。我的重点是从跨行找工作的人的角度出发，这样招聘人员才能快速识别你是否适合新的行业/职位。</p>
<p>记住，我是从金融领域的量化开发人员转到科技领域的机器学习工程师——这是一个天大的困难，因为这个跨度既有角色的转变也有行业的转变。再次强调，我的项目帮了我大忙。我把它们作为“个人项目”列在我的工作经历的顶部。尽管它们不完全是“工作”，但它们是我的工作，并且与我想要的下一份工作高度相关。</p>
<p>你应该知道，大多数招聘人员不会阅读超过一页的简历。如果他们在10-20秒内找不到他们想要的关键词，你的简历就会被抛到一边。我还在简历的第一页上半部分列出了与新工作相关的技能（以及所有关键词）。见下图。</p>
<p><img src="https://miro.medium.com/max/937/1*a-pon57rs0aflAfiOj86lw.png" alt="1*a-pon57rs0aflAfiOj86lw" width="600" height="400" loading="lazy"></p>
<h3 id="linkedin">LinkedIn —— 让他们来找你！</h3>
<p>最好的工作申请是不需要你主动发出的。在我在LinkedIn上发布DeepPiCar博客文章后，我很高兴收到许多招聘人员的消息和电话，其中包括一些大型自动驾驶公司，问我是否对他们的机器学习工程师职位感兴趣。</p>
<p>我对Medium博客和LinkedIn帖子带来的影响力感到惊讶。但请确保在LinkedIn隐私设置中标记自己为“在找工作”，这样招聘人员可以轻松找到你。当招聘人员联系我时，我几乎总能得到电话面试。</p>
<p><img src="https://miro.medium.com/max/1176/1*wZtT-iVYzKI26gNohTXAhw.png" alt="1*wZtT-iVYzKI26gNohTXAhw" width="600" height="400" loading="lazy"></p>
<h3 id="">内部推荐 —— 有一个强有力的推荐人</h3>
<p><img src="https://miro.medium.com/max/700/0*07v0u9hdLQjmnQ00.jpg" alt="0*07v0u9hdLQjmnQ00" width="600" height="400" loading="lazy"></p>
<p>我认为第二好的工作申请是通过内部推荐提交的。我在Facebook和Google都有内部推荐。我第一次在Facebook的面试表现很差，但由于我的推荐人给了我强烈推荐，Facebook允许我再试一次。幸运的是，一个月后我通过了第二次电话面试。</p>
<p>在Google的情况更加令人惊讶。尽管我是远程候选人（顺便说一下，我住在芝加哥），他们直接跳过了电话面试，安排了现场面试，其中一部分是因为我有很棒的推荐人，一部分是因为我告诉他们我有其他现场面试。</p>
<p>所以我建议你找到在你想加入的科技公司工作的家庭成员/朋友/同学。如果他们没有在那工作，也许他们认识在那里工作的人。请你的朋友介绍你们认识，并让他们通过内部推荐提交你的简历。这样你获得电话面试的机会会高很多。</p>
<p>另一种方法是明智地使用你的LinkedIn网络。你可以搜索在特定公司工作的人，并请你的共同朋友介绍你们认识。如果没有，给那个人发送LinkedIn Mail。</p>
<p>注意，如果你发送很多LinkedIn Mail，你需要LinkedIn Premium。我订阅了大约3个月的LinkedIn Premium（大约每月30美元），并能直接与更多的人联系。有些联系导致了面试。附带的好处是，我收到的招聘人员信息比以前多得多，因为我相信LinkedIn的推荐算法将我排在了非付费用户之前。</p>
<h3 id="">第三方招聘机构</h3>
<p>总体而言，我觉得大多数第三方招聘机构在这次求职过程中并没有起到很大作用。作为参考，以前我寻找金融工作时，主要依赖高管猎头公司，并且这样做的效果非常好。但大多数科技巨头并不使用第三方招聘机构。对我来说，唯一有效的机构是 <a href="https://triplebyte.com/iv/Wzwz8pq/cp/header">TripleByte</a> 和 <a href="http://deeplearning.ai/">DeepLearning.ai</a>，我将在下面详细介绍。</p>
<h3 id="triplebyte"><a href="https://triplebyte.com/iv/Wzwz8pq/cp/header">TripleByte</a> — 免去电话面试</h3>
<p><img src="https://miro.medium.com/max/1067/1*N8n2DoEP57Wpmtb0xmKF6w.png" alt="1*N8n2DoEP57Wpmtb0xmKF6w" width="600" height="400" loading="lazy"></p>
<p>TripleByte 是一家独特的招聘机构。它对候选人的吸引力在于，一旦候选人通过了 TripleByte 综合的2小时现场技术筛选测试，就可以直接进入多家公司的现场面试。对公司来说，它的吸引力在于 TripleByte 为他们筛选掉了大多数不合格的候选人，节省了工程师大量的电话筛选时间。</p>
<p>当我通过 TripleByte 的测试后，可以选择约200家公司。这些公司主要是小型初创公司，但也有不少大公司，如 Apple、Adobe、American Express 等。我最终与 <a href="https://www.apple.com/siri/">Apple Siri 团队</a>（大公司）、<a href="https://zoox.com/">Zoox</a>（中型公司）和 <a href="https://determined.ai/">Determined.ai</a>（小公司）进行了现场面试。所有这些公司都在令人惊叹的工程中工作。</p>
<p>我强烈推荐通过 TripleByte 的流程，因为它识别到了我薄弱的地方，并为我节省了许多电话筛选和相关的麻烦。<a href="https://triplebyte.com/iv/Wzwz8pq/cp/header">点击这里参加 TripleByte 的在线测试</a>。</p>
<h3 id="deeplearningaiworkeraai"><a href="http://deeplearning.ai/">DeepLearning.ai</a> 和 <a href="http://workera.ai/">Workera.ai</a> — 数据科学家/机器学习工程师的福音</h3>
<p><img src="https://miro.medium.com/max/1171/1*I5gI_cSrZLdT3fFf4N_cYQ.png" alt="1*I5gI_cSrZLdT3fFf4N_cYQ" width="600" height="400" loading="lazy"></p>
<p>如果你是一个有志成为数据科学家或机器学习工程师的人，你一定听说过或参加过 Andrew Ng 在 Coursera/Deep Learning.ai 上提供的机器学习/深度学习课程。</p>
<p>实际上，DeepLearning.ai 有两部分：一部分广为流传的是教育板块；另一部分相对较少人知道的是招聘板块，即 <a href="http://workera.ai/">Workera.ai</a>。这是因为 Workera.ai 在2019年中期创立，成立时间很短。他们没有像 TripleByte 那样多的合作公司，但我相信他们正在迅速扩展这方面的努力。他们也会让你参加一个测试。</p>
<p>Workera.ai 的测试与 TripleByte 的测试有两点不同。首先，它是为数据科学家（DS）/机器学习工程师（ML）设计的测试，而 TripleByte 有为通用软件工程师以及 ML 工程师设计的测试——ML 工程师测试是全新的。</p>
<p>其次，Workera.ai 的测试是非约束性的，这意味着通过测试并不能让你直接进入现场面试。相反，Workera.ai 会将你推荐给其少数合作公司的 DS/ML 团队，并基本上让你排在前列。但你仍然需要经过完整的电话/现场面试过程。</p>
<p>我认为这仍然非常有价值，因为 Andrew Ng 在深度学习领域及其在湾区的网络非常广泛。我最终与 <a href="http://pinterest.com/">Pinterest</a> 和 <a href="http://scale.ai/">Scale.ai</a> 的 ML 团队进行了面试。如果我只是直接申请，我不认为我会得到这两家公司的面试机会。<a href="https://workera.ai/candidates/">点击这里申请 Workera.ai 的高级工程师 AI 项目</a>。</p>
<h3 id="">在线招聘平台——不要过于依赖它们</h3>
<p><img src="https://miro.medium.com/max/870/0*13Twl2pnw3RyDtFu.jpg" alt="0*13Twl2pnw3RyDtFu" width="600" height="400" loading="lazy"></p>
<p>为了扩大我的求职范围，我在一些在线招聘平台上设置了职位搜索提醒，例如 LinkedIn、GlassDoor、Indeed 和 ZipRecruiter，这样它们会在有符合我搜索条件的新职位发布时通知我。随着时间的推移，我在申请后确实收到了一些电话面试。</p>
<p>总体而言，我发现信噪比有点低，也就是说，我每天会收到很多邮件，但来自顶尖公司的好职位却很少。不过，不要完全忽视这个渠道。你需要在一开始就广撒网，或许你会通过这个渠道捕捉到一些机会。</p>
<h3 id="">直接申请——效果并不好</h3>
<p>过去，人们会附上一封漂亮的求职信和简历寄给公司，期待着人力资源部门的回复。但这次，这种方法对我来说完全不起作用！</p>
<p>我找了大约10家公司（主要是自动驾驶公司），并在它们的官网上的招聘页面直接申请了职位。让我惊讶的是，我没有收到任何一家的回复，连拒绝邮件都没有！</p>
<p>幸运的是，在线申请每家公司并没有花费太多时间。我认为，还是要申请你感兴趣的公司，但不要指望一定会收到回复。</p>
<h3 id="">分阶段申请工作</h3>
<p>尝试在第一阶段申请一些你不太感兴趣或者你认为更容易进入的公司。然后在后续阶段申请你的梦想工作/公司。你可以利用第一阶段来练习和提高，如果得到Offer，还可以用这些Offer来争取更好的梦想公司的Offer。</p>
<p>这似乎是种非常“功利”的方法，听起来可能有些争议，但想一想：许多人会先在一家不那么知名的公司工作，计划积累经验后再跳槽到更知名的公司。而且谁知道结果会怎么样呢，如果你没有得到梦想工作，至少你在第一阶段的公司中有一些选择。</p>
<h3 id="">将信息组织起来：持续更新一个日志本</h3>
<p>通过这么多渠道申请工作后，你很难跟踪你申请了哪些公司，以及对于每个公司你目前位于哪个阶段。</p>
<p>为了帮助解决这个问题，我维护了一个详细的面试日志本。它按面试阶段（已申请、面试中、已录取、被拒绝等）和公司进行分类。每家公司本质上是一页包含背景信息和按时间顺序排列的事件列表（例如电话和面试）的日志。这样，我可以看到我申请和面试过的公司，以便进行适当的跟进。</p>
<p><img src="https://miro.medium.com/max/528/1*LCR7hMBbjU3yzZGuze_sNw.png" alt="1*LCR7hMBbjU3yzZGuze_sNw" width="600" height="400" loading="lazy"></p>
<h1 id="">第四步：面试</h1>
<h2 id="41">4.1 电话面试</h2>
<h3 id="hr">初步与HR电话交流</h3>
<p>你与公司的第一次接触通常是通过与招聘人员的邮件往来。招聘人员会询问你的时间进而安排何时进行初步电话交流，这通常是一次“适配”电话，你们在电话中会讨论你的兴趣、背景以及为什么你适合这个职位。不必过于担心各种细节，这通电话通常很短，招聘人员会希望把你推进到下一个阶段，即技术电话面试。</p>
<h3 id="">技术电话面试前</h3>
<p><img src="https://miro.medium.com/max/485/0*vN0RSfOWzKkGulG7.jpg" alt="0*vN0RSfOWzKkGulG7" width="600" height="400" loading="lazy"></p>
<p>在技术电话面试前的一两天，请务必在 <a href="http://glassdoor.com/">Glassdoor</a> 和 <a href="https://leetcode.com/">Leetcode</a> 上查阅该公司以前的面试题目。对于谷歌和Facebook这样的大公司，这样做效果可能不会很好，因为有成百上千的面试题目。</p>
<p>但对于较小的公司，这种方法还是比较有效的。我会浏览Glassdoor上的许多帖子，记录下具体的技术问题，然后尝试解决所有这些问题。在电话面试中，遇到与网上发布的题目完全相同的情况非常少见，但做这些公司的过去问题能更好地为该公司的电话面试做好准备。</p>
<p>这类似于在参加同一教授的期末考试之前练习往年的期末题目——问题可能会有所不同，但问题的风格和类型会相似。</p>
<h3 id="">技术电话面试</h3>
<p><img src="https://miro.medium.com/max/1280/0*P4QVsG3vBNfY6UhY.jpg" alt="0*P4QVsG3vBNfY6UhY" width="600" height="400" loading="lazy"></p>
<p>技术电话面试是展示实力的关键阶段。这个阶段的目的是区分有能力的候选人和能力较弱的候选人。据说只有大约10%至20%的候选人能够通过顶级科技公司的技术电话面试。</p>
<p>评价的标准只有一个编码测试，而不是你的口才、当前工作的经验、掌握的编程语言数量，甚至不是你的个人项目。大多数面试官只想让你在你最熟悉的编程语言中完成一个编码练习。</p>
<p>个人而言，我并不认为这是发现最佳工程师的最佳方式。例如，我与许多非常有经验的软件工程师合作过。有些是GUI/应用开发专家，有些是C++专家，还有些是低级Linux专家，但他们中的许多人告诉我，他们会在限时的算法和数据结构（A&amp;DS）编码测试中表现得很差。</p>
<p>但既然这是唯一的途径，想要进入科技行业，<strong>你必须“破解编码面试”</strong>……</p>
<p><img src="https://miro.medium.com/max/940/0*ZPXWlO_1OlBQa5fw.jpg" alt="0*ZPXWlO_1OlBQa5fw" width="600" height="400" loading="lazy"></p>
<p>编码练习通常通过共享在线记事本进行，例如CoderPad或Google Docs，面试官和你可以同时在共享文档中进行输入。</p>
<p>虽然一些公司（比如Google和Facebook）只要求你写出正确的算法，不要求你运行代码，但许多公司期望你在45-60分钟的时间内完成可以正确运行的代码。</p>
<p>除了共享记事本，大多数公司采用语音通话进行面试，而一些公司则通过Zoom/Skype视频通话进行。</p>
<p>对我来说，电话面试是面试过程中最困难的部分。作为参考，我大约只有50%的技术电话面试通过率。它们困难的原因包括：</p>
<ul>
<li>45-60分钟的时间窗口通常对我来说很紧张，因为我打字速度不快。（无论你是否相信，我在白板编码时更快，原因将在现场面试部分讨论。）</li>
<li>你需要在整个通话过程中与面试官保持交流的同时编写代码。大多数人更喜欢先讨论设计/方法，然后在安静中编写代码和调试，包括我自己。但如果在实现阶段你有20-30分钟不说话，会显得很尴尬。</li>
<li>在线记事本是基于文本的，不是白板。因此很难画图或图解代码工作流程。</li>
<li>在线记事本不是集成开发环境（IDE）。虽然大多数在线记事本，如CoderPad，可以做不错的语法高亮和缩进，但它们不是全功能的IDE。例如，它们不能做自动补全，也不能在你输入时高亮明显的语法错误，更不能支持逐行调试。所以很多时候，我不得不依靠“老实可靠”的print语句进行调试，这非常慢且笨拙。</li>
<li>语音通话是非视觉的。当我试图在电话中解释我的方法时，我不能画图，也不能通过面试官的面部表情或肢体语言来判断我的表述是否正确。</li>
</ul>
<p>为了应对上述困难，我采取了以下措施：</p>
<ul>
<li><strong>使用一个好的电话耳机</strong>。这是显而易见的，因为你需要解放双手来编码！所以买一个好的蓝牙耳机，并确保在电话面试前充满电。</li>
<li><strong>把闲聊时间降到最低</strong>。在我的前几次面试中，我会试图给面试官留下好印象。所以我会花大约5-7分钟谈论我的背景、经验，然后是我的自动驾驶汽车。有时面试官会问我一些问题，这可能会拖延到前10分钟！我很快发现所有这些闲聊时间都占用了面试分配的45分钟，这意味着我有更少的时间来处理编码问题。事实上，大多数面试官只想评估你的编码技能，你的强大工作经验此时对他们来说并不重要。所以我学会了在开始时将闲聊时间缩短到2分钟以内，并将我的问题留到面试结束。如果我成功完成了编码问题，面试官通常更愿意和我多聊一会儿。相反，如果你在编码面试中表现不佳，聊天也不会改变面试结果。同样的建议也适用于现场面试。</li>
</ul>
<p><img src="https://miro.medium.com/max/600/1*HOQJzGbrCYQrXMZoIPl3UQ.png" alt="1*HOQJzGbrCYQrXMZoIPl3UQ" width="600" height="400" loading="lazy"></p>
<ul>
<li><strong>使用两个显示器</strong>。一个显示器用于共享记事本，另一个显示器用于搜索和IDE等。现在大多数笔记本电脑都有外接显示器的端口。确保在编写代码时将笔记本电脑连接到至少一个外接显示器，这样你就不必在窗口之间来回切换。</li>
<li><strong>使用外接鼠标</strong>。如果你使用笔记本电脑，务必购买一个外接鼠标。对我来说，使用外接鼠标显著提高了选择、复制和粘贴代码的能力。</li>
</ul>
<p><img src="https://miro.medium.com/max/650/0*_KphaQpx9VCemE2C.jpg" alt="0*_KphaQpx9VCemE2C" width="600" height="400" loading="lazy"></p>
<ul>
<li><strong>使用IDE</strong>。尽管你在共享记事本中编写代码，有时为了排除故障，最好将代码复制粘贴到IDE中，修复语法错误和错误，然后将可以正常运行的代码复制回共享记事本。大多数面试官不会介意，只要你能在规定时间内完成代码。他们也希望看到你成功！在电话面试前，确保设置一个空项目，方便快速粘贴代码并运行。你不想在宝贵的面试时间内创建项目和设置运行参数。打开IDE还意味着你可以在面试结束时直接粘贴代码，这样你可以保存问题和解决方案以供进一步分析。</li>
</ul>
<p><img src="https://miro.medium.com/max/2000/0*4QQMQfLgKT0Y7XLV.png" alt="0*4QQMQfLgKT0Y7XLV" width="600" height="400" loading="lazy"></p>
<ul>
<li><strong>使用节省打字的编程语言</strong>。这里的大多数技巧都是为了帮助你节省打字时间，以便你可以花更多时间思考解决方案。因此，尝试选择一种打字量少的语言。我的最佳语言是C++和Python。除非面试官特别要求C++，否则我在所有面试中都选择Python，因为与C++相比，它更加简洁。</li>
<li><strong>善用Google</strong>。这是电话面试相对于现场面试的一个优势，因为你可以搜索。如果你不太熟悉某个函数的API或某种语言的语法，随时可以搜索。大家都理解你可以在电话面试中使用Google。如果你的面试官懒得从LeetCode上直接拿问题，那么恭喜你！;-)</li>
<li><strong>在开始实现前与面试官彻底讨论你的方法</strong>。在我的前几次面试中，我会在大致了解该做什么后开始编码，想着可以在编码时细化细节。这不是一个好主意。尝试在与你的面试官的初步讨论中尽量细化细节。这实际上可以为你节省很多后续实施时间。面试官当然非常熟悉这个问题的所有优缺点。如果你提出一个次优或不正确的方法，他们可能会引导你走向更优的方向，并可能指出你忘记考虑的错误/角落案例。这可以为你节省大量的实现时间。但务必尽可能多地涵盖角落案例，因为如果面试官必须指出缺少的角落案例，你会被扣分。</li>
<li><strong>解决问题比不解决问题更好</strong>。当你在寻找最优解决方案时，这一点可能不太明显。有时，如果我无法想到最优解决方案，我会提出并完成一个次优解决方案。无论如何，提出某个工作方案（尽管次优）能给面试官提供一个不错的比较数据点。在某些情况下，面试官在事后告诉我，我认为可能的“最优”解决方案实际上并不存在！根据我的失败经验，如果没有提出工作方案，面试肯定会失败。</li>
</ul>
<p><img src="https://miro.medium.com/max/516/0*zYwn2rtIyq9npuS_.png" alt="0*zYwn2rtIyq9npuS_" width="600" height="400" loading="lazy"></p>
<p>记住，在这个阶段，你不应该专注于系统设计问题，因为这些问题只会在现场面试中提问，因为它需要白板来画图。</p>
<h3 id="">在线编程能力测试</h3>
<p><img src="https://miro.medium.com/max/1386/0*ZQjHTN8X0xZw0A1J.jpg" alt="0*ZQjHTN8X0xZw0A1J" width="600" height="400" loading="lazy"></p>
<p>我很少遇到还给在线编码测试（如 HackerRank 或 Codility）的科技公司，虽然很多金融公司仍然会给这些测试。这些测试的目的是节省招聘公司的人工成本。</p>
<p>通常，你会从公司的招聘人员那里收到一个在线测试的链接。在线测试通常有 3-5 道编码题，你需要在 2-3 小时内完成。在收到招聘人员的邮件后的 7-10 天内，你可以随时完成这个测试。在你进行测试时，没有人监视你，你需要通过大部分测试用例才能通过这一阶段。</p>
<p>以下是一些关于在线编码测试的建议：</p>
<ul>
<li><strong>使用 IDE</strong>: 你可以在你喜欢的 IDE 中完全编写和测试你的代码，然后将其粘贴到在线测试页面以运行正式的测试用例。</li>
<li><strong>在开始之前阅读所有问题</strong>: 有些在线测试的设计是让大多数人在规定时间内无法完成所有问题。因此，在开始做题之前，一定要先阅读所有问题，先从简单的问题开始，这样你可以尽可能多地完成问题。</li>
<li><strong>首先让代码运行起来</strong>: 对于大多数在线测试来说，时间是一种稀缺资源。所以目标是尽可能通过更多的测试用例，而不是所有的测试用例。如果你有一个可行的解决方案，它应该能通过大部分测试用例。如果你的解决方案不是时间优化最好的，你可能会在一些测试用例上超时，这没关系。继续解决下一个问题，如果有时间再回来。（注意：编写生产级别代码时不要这样做。对于生产级别代码，一定要花时间把算法搞对、整理干净，并添加足够的文档，以便你和其他人将来能维护你的代码。）</li>
<li><strong>保存问题和你的解决方案的副本</strong> 以便日后分析。不管是电话面试、在线面试还是现场面试的题目，都应养成保存所有面试题目的习惯。</li>
</ul>
<h3 id="">居家项目</h3>
<p>一些公司在技术电话面试之前或代替技术电话面试给求职者布置居家项目。我有两家公司给我布置了居家项目，这两个项目都是与机器学习相关的。我发现这些项目比技术电话面试更有趣，也更贴近我申请的职位。因此我希望更多公司能用居家项目代替技术电话面试。</p>
<p>但我也理解这样做可能不是那么有效或公平，因为</p>
<ol>
<li>公司不知道是你还是你的机器学习专家朋友完成了这个项目，</li>
<li>即使是你自己完成的，公司也不知道你花了多长时间。</li>
</ol>
<p><img src="https://miro.medium.com/max/750/0*2DB9c6E9xy1Hsu_e" alt="0*2DB9c6E9xy1Hsu_e" width="600" height="400" loading="lazy"></p>
<p>由于居家项目通常需要花费大量时间，请确保你在明智地使用你的时间。</p>
<p>对于你非常感兴趣的公司，居家项目确实需要花8-10小时好好编写代码并记录你的方法和设计决策。例如，我为 <a href="http://scale.ai/">Scale.ai</a> 的项目花了至少10小时，尽管说明中让我只花大约2-3小时。我这样做是因为我觉得很有趣，并且通过探索不同的机器学习方法学到了很多。</p>
<p>对于你不太感兴趣的公司，不要花费太多时间，把时间留给更多的 LeetCode 问题，这样你可以更好地准备编码面试。</p>
<h3 id="">面试安排</h3>
<p><img src="https://miro.medium.com/max/400/0*XF9LDZeQZ6_GVE0J" alt="0*XF9LDZeQZ6_GVE0J" width="600" height="400" loading="lazy"></p>
<p>当你完成了几次电话面试后，你应该会收到一些现场面试的邀请。在电话面试的初期阶段，你的成功率可能会比较低。我在前4-5次电话面试中都失败了。后来我意识到需要更多地练习动态规划和递归算法。</p>
<p>你的经历可能不同，但在面试过程中收到拒绝邮件时不要气馁。向招聘人员请求反馈并继续学习，还有很多公司等着你去尝试。</p>
<p>以下是一些面试安排的建议：</p>
<ul>
<li><strong>保持详细的面试日志</strong>: 这与第2步提到的日志相同。现在是时候开始记录每次面试的时间和讨论内容。</li>
<li><strong>将所有面试放入日历并设置提醒/警报</strong>。你不想因为忘记面试而错过任何一个面试。此外，确保确认面试的时区，为简便起见，我总是按招聘人员所在的时区（通常是太平洋时间）与他们沟通。</li>
<li><strong>安排某一时段的多场电话面试时至少间隔30分钟</strong>。这是因为有些面试官可能会晚打5-10分钟电话，有些面试官可能会允许面试超时5-10分钟。如果你把两个电话面试安排得太近，可能会不得不缩短一个或错过另一个。此外，你还需要5-10分钟的时间来清理思绪并记下面试笔记。</li>
<li><strong>请求第二次机会</strong>。这可能是一个鲜为人知的事实。如果你第一次电话面试失败，很多公司会允许你有第二次机会。大多数公司不会自动提供第二次机会，但如果你礼貌地请求，它们通常会同意。因此你每次都要尝试请求它们再给一次机会，但将它安排在第一次面试后的几周，这样你有足够的时间学习。除非你等6个月，否则通常不会有第三次机会。</li>
<li><strong>错开你的面试</strong>。我将面试分阶段进行。把认为较容易通过的公司安排在早期阶段，把较难通过且更知名的公司安排在2-3周后的后期阶段。这样，如果你发现自己在某些主题上较弱，还有2-3周的时间来学习。</li>
<li><strong>集中安排你的现场面试</strong>。因为我面试的大多数公司都在湾区，我尝试在1-2周内安排所有的现场面试，这样我只需要飞一次就能完成所有的现场面试。例如，我的湾区现场面试持续了整整两周，其间我面试了6家公司，每周3次面试。如果公司知道你会在城里进行其他面试，他们更愿意安排你进行现场面试。此外，他们不需要支付你的机票。不同公司支付了一些酒店费用，剩下的酒店费用由我自己承担。这样对我来说没问题，因为它为我节省了很多时间，我能够集中安排所有的现场面试，这样可以在同一时间段内收到所有的面试结果。</li>
</ul>
<h2 id="42">4.2 现场面试</h2>
<p><img src="https://miro.medium.com/max/696/0*C5eicXplRRZIxXk3.png" alt="0*C5eicXplRRZIxXk3" width="600" height="400" loading="lazy"></p>
<p>在现场面试前的2-3周，你需要开始专注于系统设计问题。许多公司允许你在通过电话面试后安排4-6周内的现场面试，这应该能给你足够的时间准备算法与数据结构（A&amp;DS）和系统设计问题。</p>
<p>通常每个公司的现场面试包含4-5个45分钟的面试环节——上午2个，午餐时间，然后下午2-3个。面试内容一般包括1个系统设计面试，1个行为面试和2-3个算法与数据结构（A&amp;DS）编码面试。很少有公司会问到数学或脑筋急转弯问题，所以我不会花太多时间准备这些。</p>
<h3 id="">系统设计面试</h3>
<p><img src="https://miro.medium.com/max/1280/0*Vnhf3KUToEKuUIGV.jpg" alt="0*Vnhf3KUToEKuUIGV" width="600" height="400" loading="lazy"></p>
<p>Gaurav Sen 正在设计 WhatsApp</p>
<p>这一阶段被包含在第二步（面试准备）中。如果你看了所有我推荐的所有系统设计YouTube视频，并能够将其概括为一个类似于我所概述的框架，说明你掌握得非常好。</p>
<h4 id="">明确需求规范</h4>
<p>在系统设计问题中，一个重要的技巧就是及早明确系统的功能和特点。你需要定义一组既不太简单也不太复杂的功能，以便在45分钟的时间内完成。</p>
<p>例如，在被要求设计一个即时通讯应用时，请确保提及以下基本功能：</p>
<ul>
<li>用户认证（这是大多数系统中应有的）</li>
<li>一对一消息</li>
<li>群组消息</li>
<li>用户在线状态</li>
<li>离线消息（如果有时间）</li>
</ul>
<p>对于一个45分钟的面试，我不会提及以下非基本功能：</p>
<ul>
<li>语音通话</li>
<li>视频通话</li>
<li>多人通话</li>
<li>个人时间线（如Facebook Stories）</li>
</ul>
<p>当然，如果你被要求设计Skype，你必须设计语音和视频通话，但我不会在设计的功能中包括共享计算机桌面，这样做是为了将已有功能限制在可管控的范围内。</p>
<h4 id="">注意意外事件</h4>
<p>在面试中，你需要注意面试官提出的一些奇怪的问题，并能做出聪明的回应。</p>
<p><img src="https://miro.medium.com/max/1000/0*FuUaWBV2H26hOdIy.jpg" alt="0*FuUaWBV2H26hOdIy" width="600" height="400" loading="lazy"></p>
<p>例如，在Facebook的面试中，我被要求设计一个简化版的Google搜索。我在白板上画了一个相当不错的高层设计。然后面试官抛出一个意外的问题：</p>
<p>面试官：“你认为需要多少台服务器？”</p>
<p>我：“嗯，这取决于有多少人在使用它以及服务器的性能如何……”</p>
<p>面试官：“你为什么需要知道服务器的性能？”</p>
<p>我：“我的想法是，实际使用情况就像分子，服务器硬件的能力就像分母，所需的服务器数量取决于这两个数字。”</p>
<p>面试官：“不，服务器的硬件规格是无关紧要的。”</p>
<p>我：“硬件规格当然很重要，如果我可以用10台高性能服务器托管一个服务，我可能需要100台笔记本电脑来托管相同的服务。”</p>
<p>面试官：“硬件规格无关紧要。”</p>
<p>此时，我感到困惑，因为我觉得他有点不讲理。在长时间的尴尬停顿后，我几乎要放弃了。</p>
<p>我：“所以你的意思是硬件规格不重要？”</p>
<p>面试官：“假装你不知道服务器的硬件规格，你怎么确定需要多少台服务器？”</p>
<p>哦，他的意思是这样！！他不是说硬件规格无关紧要，而是说我们不知道确切的硬件规格。</p>
<p>我：“哦，在这种情况下，我需要基准测试服务器的吞吐量，所需的服务器数量大致是最大使用量除以单台服务器的吞吐量。我这样说对吗？”</p>
<p>我看到他微微点头。</p>
<p>我讲这个故事的意思是，在面试中进行补充说明从而明确面试官的问题/陈述有多么重要。你可以通过用自己的话复述问题/陈述来做到这一点。如果你真的卡住了，问他们刚才说的是什么意思。也许他们会以不同的方式重述问题/陈述，以便你理解并继续。</p>
<p>记住，许多面试官不是母语为英语的人，所以<strong>他们说的</strong>和<strong>他们想说的</strong>可能不完全一致。同样，许多面试者（包括我在内）也不是母语为英语的人，所以<strong>他们听到的</strong>和<strong>他们以为听到的</strong>也可能不完全一致。</p>
<p>让面试官以不同的方式重复同样的话，可能会大大帮助面试者理解面试官的真正意思。</p>
<h3 id="">在白板上进行的算法和数据结构编码面试</h3>
<p><img src="https://miro.medium.com/max/1280/0*zrHrbhiBz6Mfv79F.jpg" alt="0*zrHrbhiBz6Mfv79F" width="600" height="400" loading="lazy"></p>
<p>Tushar Roy 讲解 Trie 数据结构</p>
<p>现场面试中的算法和数据结构（A&amp;DS）问题几乎都是在白板上完成的。包括我自己在内，许多人最初都对白板编码感到害怕，因为在实际工作环境中，谁会在白板上写详细的代码呢？</p>
<p>但在经历了所有这些现场面试之后，我发现白板编码在某种程度上比电话面试更容易。我将在下面介绍一些专门针对白板编码的技巧。</p>
<p><strong>用图表/表格来说明你的算法</strong>。有句老话说，“一张图胜过千行代码”，或者类似的说法。在写代码之前，尝试用图表/表格来说明你的代码如何工作。这会给面试官一个你代码可能是什么样子的路线图，并允许他们指出他们看到的任何潜在问题。</p>
<p>一旦你写完代码，尝试逐行检查代码，并在旁边放上图表，这样你就可以向面试官展示你的实现与你的意图是一致的。一定要观看<a href="https://www.youtube.com/playlist?list=PLrmLmBdmIlpv0b07yUFtY8eN5odlw1Pse">Tushar Roy的LeetCode高难度解决方案</a>的YouTube视频，学习如何在白板上展示想法。</p>
<p><strong>将白板分成几个部分</strong>。出于前一条提示提到的原因，我建议将白板至少分成2-3个垂直部分。一部分用于图表，其他部分用于代码。这样，你可以在编写代码的整个过程中随时参考图表。</p>
<p><strong>不要纠结于语法细节</strong>。如果允许你使用任何编程语言（这是常态），那么面试官不应该对你的代码语法太挑剔。例如，如果你漏掉了C++/Java中的分号，或Python中的冒号，只要你的缩进正确，就没关系。或者如果你拼错或缩短了内置函数的名称，只要面试官理解你的意图，也没关系。相反，在电话面试中你没有这样奢侈的宽容度。</p>
<p><strong>使用简化的变量/函数名称</strong>。因为用记号笔写长名称需要很长时间和很多空间，告诉面试官变量的全名和含义，然后在代码中使用简化的名称。这会节省你很多宝贵的时间和白板空间。</p>
<p><strong>不断寻求反馈</strong>。这是现场面试的一个重要好处，因为你可以很容易地判断面试官的反馈。当我展示我的设计或方法时，我会定期检查面试官的面部表情，并随口问，“我这样做是正确的吗？”或“看起来怎么样？”通常，面试官都很乐于提供一些反馈，或者至少点头或皱眉。在你偏离正确答案太远的时候，如果你问的话他们肯定会告诉你。</p>
<p>正如我几次惨痛的经历告诉我的那样，当我不寻求反馈时，一些面试官会先让我完成整个实现，然后在最后指出一些重大缺陷。结果，我惨败了所有那些面试。所以尽量在编码前尽量细化你的设计并寻求反馈。</p>
<p><img src="https://miro.medium.com/max/400/0*VOO-nVdsgXIbDUr5.png" alt="0*VOO-nVdsgXIbDUr5" width="600" height="400" loading="lazy"></p>
<h3 id="">其他的在线面试技巧</h3>
<p>其中一些非技术技巧可能是常识，但我将它们列出来作为提醒。</p>
<p><strong>在结尾提出好的问题</strong>。在每次面试结束时，你可能没有太多的时间来提问。所以这些问题要简短而有影响力。面试是一个双向的过程——公司在评估过程中试图从你身上提取信息，你也需要对这些公司做同样的事情，这样当你得到工作机会时，你就可以做出最明智的决定。</p>
<p><img src="https://miro.medium.com/max/998/0*rvbji0M2GsMoD0CW.png" alt="0*rvbji0M2GsMoD0CW" width="600" height="400" loading="lazy"></p>
<p>以下是我喜欢问的经典问题:</p>
<ul>
<li>你能告诉我你的工作背景和你现在的工作吗?</li>
<li>你能告诉我贵集团/公司的技术栈和开发方式吗?一旦面试官提到一些编程语言或工具，你可以回应说你也使用过这些技术。</li>
<li>谁是贵集团/公司的理想人选?一旦面试官提到了理想候选人的一些特征，你可以回应说你也有这些特征，并给出一个快速的例子来支持它。</li>
<li>推销一下自己。这可能并不适用于所有人。我还利用面试结束时的“提问”时间，向面试官展示了我的自动驾驶机器人汽车和几段关于这辆汽车的YouTube视频。我觉得效果出奇的好，因为我看到大多数面试官在拿着车的时候都非常惊喜。</li>
</ul>
<p><strong>保持充足的水分</strong>. 在面试过程中你会说很多话，你会(无意识地)出汗很多，所以你总是想要随时补充水分。这意味着每次面试结束后都要把杯子重新斟满，不在白板上写字的时候就喝一小口。我发现咖啡因(咖啡/茶/可乐)在让我保持最佳状态方面非常有效。</p>
<p><img src="https://miro.medium.com/max/2000/0*VsgMcMV6BniWLJgN.jpg" alt="0*VsgMcMV6BniWLJgN" width="600" height="400" loading="lazy"></p>
<p><strong>记笔记</strong>. 我总是详细记录我的面试。因为大多数面试都是连续进行的，所以你唯一能做笔记的时间就是上厕所的时候。带上你的手机，快速记下一些面试时收到的提问和你的解决方法，这样你就可以在完成一天的面试后让自己回想起来这些。这应该不超过1分钟。</p>
<p><strong>吃一顿清淡的午餐.</strong> 午餐时不吃得过多是很难的，因为许多顶级科技公司都有很棒的免费食物!Facebook甚至还有免费的冰淇淋!虽然你很想在那里品尝所有的免费食物，但你需要记住，<strong>你去那里不是只为了吃一次很棒的免费午餐</strong>，而是去面试，然后<strong>得到这份工作，这样你就可以每天都在那里吃饭了</strong>。</p>
<p>不要暴饮暴食的两个原因:第一，你需要在下午的会议上保持清醒，而不是感到昏昏欲睡。第二，你需要花大部分时间和你的午餐伙伴聊天。虽然在一些公司(谷歌/Facebook)，你的午餐伙伴的反馈不是决定offer的一部分，但很多时候，他们是公司的高层，所以他们的意见可能很重要，而你需要给他们留下好印象。</p>
<p><strong>上厕所</strong>。每次面试结束后，一定要去一下洗手间。不幸的是，这些45-60分钟的面试通常是连续的，中间没有休息时间。所以你一定要在下次面试开始前去一趟卫生间。你想要能够走路和伸展一下你的腿，更重要的是，洗干净你的手和脸，这样你就可以神清气爽地去参加下一次面试。不要休息太久，因为休息时间会占用你接下来的45分钟。</p>
<p><img src="https://miro.medium.com/max/1024/0*TBXEsZ8Nnx8eWzk-.jpg" alt="0*TBXEsZ8Nnx8eWzk-" width="600" height="400" loading="lazy"></p>
<h3 id="">带些零食</h3>
<p>与上一点相反，在我参加的一些现场面试中，不管是有意安排还是因为时间冲突，午餐时间有时会被安排在下午1:30-2:00，有时甚至会被直接跳过！那时我会饿到几乎撑不住了，所以我学会了随身带一根麦片棒和几块巧克力，这样可以在面试间隙快速吃几口。</p>
<h3 id="">带几份简历</h3>
<p>虽然大多数面试官确实会带上一份你的简历，但偶尔我会遇到没带简历的面试官，所以我会递给他们一份。对于技术面试，这可能并不是特别重要，但这是一个贴心的举动。</p>
<h3 id="">事后分析</h3>
<p>当天的面试结束后不要马上放松，你需要在回到酒店或家里后，趁着记忆还比较新，立刻尽可能详细地记录面试情况，包括面试官的姓名/背景、所有提问、你的应对方法、面试官对你提问的回答等。用电子方式记录（而不是纸质笔记），这样以后更容易搜索和存档。</p>
<p>之后，你需要再次回顾所有的面试问题，找到优化的解决方案，并思考如何在下次面试中提升自己的表现。如果在LeetCode上找到相应的面试问题，务必将问题标注上公司名称，以便其他人从你的经验中受益。</p>
<p><img src="https://miro.medium.com/max/2000/0*m4TbZx-yWlVl0HCy.jpg" alt="0*m4TbZx-yWlVl0HCy" width="600" height="400" loading="lazy"></p>
<p>以上只是一些小技巧，可以稍微增加你的胜算。你仍然需要大量练习算法和数据结构问题，学习快速识别每个问题的解决方法。</p>
<p>例如，了解在树/图中何时使用BFS或DFS，何时使用递归与迭代算法，何时在处理数据之前进行排序或建立索引等。并且一定要熟知你使用的算法的时间和空间复杂度。我从5月底开始准备面试，6月底进行了第一次电话面试，8月底完成了最后的现场面试——这是我生命中最紧张的三个月。</p>
<p>如果你们中有人问我遇到的具体面试问题，很遗憾，我不能在这里透露。但我已经在LeetCode上将所有相关的面试问题标注了公司名称，作为对在线社区的匿名回馈。我希望你们在面试后也能这样做。</p>
<h1 id="step5offer">Step 5. Offer谈判和团队匹配</h1>
<h2 id="51offer">5.1 Offer谈判</h2>
<p>如果你在阅读这部分，说明你可能已经收到至少一个 offer 了。哇，恭喜！(如果你还没有收到任何邀请，立即停止阅读，回去继续做更多的LeetCode练习和面试吧！😜)</p>
<p><img src="https://miro.medium.com/max/480/0*cU5XEYWCmu9FJg8u" alt="0*cU5XEYWCmu9FJg8u" width="600" height="400" loading="lazy"></p>
<h3 id="offer">Offer薪酬包的组成部分</h3>
<p><img src="https://miro.medium.com/max/624/0*mU6OkvJojAbaIaV5.png" alt="0*mU6OkvJojAbaIaV5" width="600" height="400" loading="lazy"></p>
<p>来源: levels.fyi</p>
<p>以下是科技公司Offer薪酬包的典型组成部分。我将举一个通常有3-5年的工作经验（YoE）的中级软件工程师（SWE）的例子。</p>
<ul>
<li>签约奖金: 这是你开始工作后立即得到的钱，而不是在你签署聘用合同时。例如，一次性支付$30,000。</li>
<li>基本工资: 这是每个月进入你银行账户的钱。例如，每年$150,000。</li>
<li>年度奖金: 这是你基本工资的一个百分比，通常为10-30%，每年发放一次。例如，20%，对于$150,000的基本工资来说就是$30,000。</li>
<li>股票期权或股票奖励: 大公司倾向于给予股票（称为受限制股票单位，RSUs），在授予时有一定价值。初创公司倾向于给予股票期权，在授予时价值较低但有很大潜力。例如，价值$300,000的股票奖励，分四年归属，每年归属$75,000。</li>
<li>搬迁补助（如果你需要搬迁）: 通常包括：
<ol>
<li>几周的公司住房供你和你的家庭使用</li>
<li>搬运你所有的物品</li>
<li>帮助买卖房屋，并且可选地</li>
<li>一些现金以帮助其他搬迁相关的费用。搬迁补助取决于你的家庭构成、位置和级别。通常是不可协商的。</li>
</ol>
</li>
<li>年度总薪酬（TC）: 这是使所有Offer薪酬包可以进行对比的一个数字。</li>
</ul>
<p>TC = 基本工资 + 年度奖金 + 年度股票奖励 + 分摊的签约奖金（假设你将签约奖金在3年内分摊）</p>
<p>我们中级SWE的TC = $150k + $30K + $75K + $30K / 3 = $265,000</p>
<p>注意，搬迁补助不被视为TC计算的一部分，因为在预期情况下你大概会花掉大部分（如果不是全部）搬迁补助。</p>
<h3 id="">知道你的技术水平所对应的等级</h3>
<p>即使你知道自己的求职目标，公司也可能会根据公司的具体情况对你进行面试。例如，一个4级工程师可能会选择谷歌的L3或L4级、Facebook的E3或E4级或者微软的59-62级的面试。</p>
<p><strong>询问招聘人员你面试的是哪个级别的岗位</strong>，但这可能不是你得到这份工作的水平。例如，你可能在Facebook的E4面试，最终，招聘委员会可能会认为你的表现不足以达到E4，但足以获得E3的offer。 所以<strong>再次询问招聘人员</strong>你的工作offer是什么级别。通过这一层，你可以找到TC的期望值和范围。(见下文)</p>
<p><img src="https://miro.medium.com/max/1025/1*iSvZ60-cnf0qTPfwEGDHCw.png" alt="1*iSvZ60-cnf0qTPfwEGDHCw" width="600" height="400" loading="lazy"></p>
<h3 id="">知道你自己在市场上的价值</h3>
<p>如果你不知道你的市场价值，你就无法有效和目标公司进行谈判。多年来，公司一直对薪酬信息保密，因此候选人在信息方面处于巨大的劣势。</p>
<p>幸运的是，近年来薪酬信息已经逐渐透明化，许多网站从许多候选人中收集到必要的信息，然后按级别、工作职能和地点呈现匿名的薪酬数据。一个著名的网站是<a href="http://levels.fyi/">levels.fyi</a>。</p>
<p><img src="https://miro.medium.com/max/917/1*LCu-2A8IXNYVfe-m9t5AFQ.png" alt="1*LCu-2A8IXNYVfe-m9t5AFQ" width="600" height="400" loading="lazy"></p>
<p>3-5年SWE工作经验的最高薪酬。来源:levels.fyi</p>
<p><img src="https://miro.medium.com/max/912/1*9pETieZ01nFFBNkjoz7MbA.png" alt="1*9pETieZ01nFFBNkjoz7MbA" width="600" height="400" loading="lazy"></p>
<p>5年以上SWE工作经验的最高薪酬。来源:levels.fyi</p>
<p>正如你在上面看到的，所有高薪工作都在旧金山湾区(Bay Area)。拥有3-5年工作经验的SWEs可以获得高达29万美元的TC，拥有5年以上工作经验的SWEs可以获得超过40万美元的TC。</p>
<p>对于任何非湾区的工程师来说，30万至40万美元的总薪酬可能令人瞠目结舌。但请记住，湾区是美国房价最高的区，因为很多人的工资要高得多。加州帕洛阿尔托的房价中位数为280万美元(资料来源:Zillow.com)，旧金山都会区(资料来源:<a href="https://censusreporter.org/profiles/31400US4186041884-san-francisco-redwood-city-south-san-francisco-ca-metro-division/">censusreporter.com</a>)为110万美元。</p>
<p>相比之下，纽约曼哈顿的房价中位数为120万美元(数据来源:Zillow.com)，而纽约/新泽西都会区的房价中位数仅为44万美元。(来源:[censusreporter.com] <a href="https://censusreporter.org/profiles/31000us35620-new-york-newark-jersey-city-ny-nj-pa-metro-area/">42</a>)</p>
<p>结合在levels.fyi上筛选出你的水平(从上一步获得)和公司名称，你可以清楚地看到offer的平均TC及其范围。例如，对于旧金山湾区的谷歌L4 SWE，offer的平均TC约为26万美元，范围从20万美元到32万美元。所以，如果你收到了只有20万美元的谷歌L4的offer，那么你就知道你还有讨价还价的空间。</p>
<p><img src="https://miro.medium.com/max/1055/1*de6XA2-BzEbzRDASwdWEXA.png" alt="1*de6XA2-BzEbzRDASwdWEXA" width="600" height="400" loading="lazy"></p>
<h3 id="">初创公司</h3>
<p><img src="https://miro.medium.com/max/285/0*m0izqd9brSBsnkdr" alt="0*m0izqd9brSBsnkdr" width="600" height="400" loading="lazy"></p>
<p>Levels.fyi 是一个很好的用来比较更成熟公司的offer（无论是上市公司还是私营公司）的工具，因为这些公司的薪酬中的股票部分可以更有把握地用美元来评估。</p>
<p>但是对于早期的初创公司，你在 Levels.fyi 上通常找不到可靠的数据，这往往是因为以下两种情况：</p>
<ol>
<li>一定比例的公司股票，或者</li>
<li>一定数量的股票期权。公司招聘人员或CEO可能会告诉你，虽然你现在的股权价值很低，但如果公司在3-5年内上市，它的价值可能会增长20-50倍。</li>
</ol>
<p>实际上，这些股权成分很难评估，极其缺乏流动性，多年来没有任何货币价值，实际上当<a href="https://www.forbes.com/sites/neilpatel/2015/01/16/90-of-startups-will-fail-heres-what-you-need-to-know-about-the-10/#35d103c06679">90%的初创公司失败时</a>（来源：福布斯），它们的价值确实会归零。</p>
<p>我收到了一些来自早期初创公司的offer，他们大多存在典型的薪酬包现金部分较少（即基本工资和奖金较低），而股权部分较重（很多期权，潜在回报巨大……或者一无所有）的特点。这很符合常理，因为大多数早期初创公司并不盈利，他们支付的任何现金工资/奖金都会消耗他们手头的现金，或者用风险投资（VC）的术语来说，烧光他们的资金。</p>
<p>虽然我收到的这些初创公司offer确实做着令人兴奋的工作，前景看起来非常有希望——其中一个公司还有谷歌风投的支持——但最终我选择了谷歌，因为我需要大公司的确定性和稳定性。毕竟，我的孩子即将上大学。我不能承担加入初创公司几年后看到它失败而我的股权部分被抹去的风险。</p>
<p>然而，如果我年轻且没有孩子，我肯定会去初创公司工作，尝试一鸣惊人。如果我失败了，我不会有养家的压力，还有足够的时间重新开始。如果我的妻子赚的钱足以支付我们全家的开销，那么我也可以去初创公司工作，追求其潜在的上升空间。在金融术语中，这称为<a href="https://www.investopedia.com/terms/d/diversification.asp">多元化</a>——通过将风险较高的股票与较安全的股票或债券结合起来，你实际上可以实现更高的<a href="https://www.investopedia.com/terms/r/riskadjustedreturn.asp">风险调整后收益</a>。</p>
<h3 id="offer">不要在第一次电话联系中接受Offer</h3>
<p><img src="https://miro.medium.com/max/740/0*dhK83JNgpbSsRcI-.jpg" alt="0*dhK83JNgpbSsRcI-" width="600" height="400" loading="lazy"></p>
<p>当你第一次通过面试被发放Offer时，招聘人员通常会通过电话告诉你 Offer 的详细信息（基本工资、奖金、股权奖励等）。这被称为“口头 Offer”。你在第一次电话中需要做的是感谢招聘人员，记下这个 Offer 所有的详细信息，重复一遍以确认，并请他们通过电子邮件发送这些详细信息。然后挂断电话。</p>
<p>记住，不要当场接受工作。（如果这是你的梦想工作，可以在挂断电话后跳起来庆祝。）有时候，招聘人员会拒绝以书面或电子邮件形式发送给你。如果是这样，那么请你将刚刚写下的 Offer 详细信息发送给他们，并询问你所听到的是否正确。他们通常会确认。</p>
<p>这很重要，因为一旦有书面记录，招聘人员就不容易收回他们在电话中告诉你的内容。</p>
<p>很多时候，招聘人员会催促你立即在电话中口头接受 Offer，或者给你一个 24 小时的接受期限。不要上钩！你应该总是尝试争取更好的 Offer。</p>
<h3 id="offeroffer">Offer 谈判——当你有多个Offer的时候</h3>
<p><img src="https://miro.medium.com/max/293/1*I3BYbnGWZCkQOJKaxwGSdA.png" alt="1*I3BYbnGWZCkQOJKaxwGSdA" width="600" height="400" loading="lazy"></p>
<p>Offer 谈判就像玩高赌注的扑克——毕竟，我们谈的是成千上万的美元——这既令人兴奋又有压力。对于像我这样的人来说，这也可能很让人害怕。不过有了 levels.fyi 的信息后，如果那个 Offer 太低的话，你可以揭穿招聘人员的虚张声势。</p>
<p>在 Offer 谈判过程中，有一个强有力的 Offer 或者说一副强有力的底牌是有帮助的。很多招聘人员很愿意尝试匹配竞争对手的 Offer。他们可以将竞争对手的 Offer 呈递给招聘/薪酬委员会，作为提出更好 Offer 的强有力理由。</p>
<p>在这种情况下，你可以尝试让这些公司竞标你的薪酬。竞标每天都在我们身边发生，比如在股票市场、eBay 和 Google Ads 中，当某种商品有需求时，这很正常。竞标过程确保商品以公平的市场价格展示它的价值。</p>
<h3 id="offeroffer">Offer 谈判——当你只有一个Offer</h3>
<p>即使你没有底牌——即只有一个 Offer 或者仅有比这个 Offer 更低的 Offer——你也可以告诉招聘人员你对这家公司非常感兴趣，但你的薪资期望实际上是 $XYZ。招聘人员可能会要求你证明这个数字，你可以说这是基于 levels.fyi 的数据，或者是你朋友/同事的相似年资和他们的总薪资是 $XYZ。</p>
<p>如果你表现得专业，很有可能最初的 Offer 会有所提高。公司很不可能因为你试图谈判就收回其原始 Offer。毕竟，公司投入了大量精力寻找合格的候选人，所以他们不想因为小幅增加而失去好候选人。所以你礼貌地要求更多薪酬是没有任何损失的。</p>
<h3 id="offer">Offer 谈判——可以不只是谈钱</h3>
<p>记住，<strong>不仅仅是薪酬可以谈判</strong>。公司可以提供的任何东西都在谈判桌上。</p>
<p>例如，你可以要求在不同的组工作（比如核心组而不是支持组）、不同的职称（比如高级软件工程师而不是软件工程师）、不同的工作职能（比如研究工程师而不是软件工程师）、更多的带薪休假天数、灵活的在家办公时间，或更好的搬迁打包。</p>
<p>当然，这在不同公司之间差异很大。大公司可能不那么灵活，但较小的公司可能无法匹配大公司的货币补偿，可能更愿意考虑这些替代要求。再次强调，如果你礼貌地提出要求，是没有坏处的。</p>
<h3 id="offer">Offer 谈判——不要太贪心</h3>
<p>经过几轮与招聘人员的来回交谈后，如果你感觉没有太多空间可以谈判了，就停止要求更多。大公司中的招聘人员每天都会见到这种情况，可能不会太介意。但对于较小的公司，招聘人员可能直接与你的招聘经理甚至是老板/创始人合作。如果你讨价还价太激烈，可能会被认为太贪婪。因此，即使他们雇用你，他们可能会对你产生负面看法。<strong>知道何时收手。</strong></p>
<h3 id="offer">Offer 拖延策略</h3>
<p><img src="https://miro.medium.com/max/650/0*m5gcnqcx9RKhAzZj.png" alt="0*m5gcnqcx9RKhAzZj" width="600" height="400" loading="lazy"></p>
<p>如果在谈判后你对任何 Offer 都不太满意，你应该尝试推迟接受这些 Offer。这可能发生在某些 Offer 的工作职能非常有吸引力，但薪酬太低，而其他 Offer 的薪酬很好，但你不喜欢工作职能的情况下。</p>
<p>推迟一个 Offer 可以让你有更多时间进行面试，获得那个完美的 Offer。但这确实有风险，因为职位可能随时被填满。但对于大科技公司，这种风险很小，因为他们总是在招聘。如果你需要推迟一个 Offer，以下是一些策略。（免责声明：我没有尝试过所有这些策略，有些是别人建议的。）</p>
<ul>
<li>与团队/同事见面：请求一次通话、一起喝咖啡或共进晚餐，以了解团队其他成员或同事的更多信息。这不仅是展示兴趣的好方法，也是了解公司/团队文化的有力信号。</li>
<li>请求产品演示：请求对方进行产品演示是展示兴趣的另一种好方法。这也是评估技术挑战、产品路线图以及公司产品导向程度的好方法。你通常可以将此与团队共进午餐结合起来。</li>
<li>询问各种股票/期权相关问题，例如公司的当前估值、最近的融资轮次、公司的退出计划/策略以及期权行权计划等。</li>
</ul>
<p>这些策略对大多数公司都很有效。对于一些大科技公司，一旦他们决定给你 Offer，通常这个 Offer 至少会有效几个月，因为你达到了他们的技术门槛，他们会随时愿意雇佣一个优秀的软件工程师。你的招聘人员可能不愿意告诉你这一点，因为他们希望尽快敲定你，但你可以通过其他渠道了解，例如 Google 搜索和 Blind App。</p>
<p>最后，记住所有招聘人员都希望你被公司录用。能够敲定一个候选人（即将合格的候选人转化为员工）是他们的绩效指标之一，与招聘人员的绩效奖金挂钩。所以<strong>把你的招聘人员视为你的支持者，而不是对手</strong>。</p>
<h2 id="52">5.2 团队匹配/选择</h2>
<p><strong>不要仅仅因为一份工作现在支付最高工资就选择它。</strong> 虽然在你决定是否接受offer的过程中薪酬是一个重要因素，但更重要的可能是这份工作对你职业生涯的影响。我将在下面讨论如何选择一家公司/团队，以最大化你职业生涯的潜力。</p>
<h3 id="">团队匹配过程</h3>
<p><img src="https://miro.medium.com/max/922/0*tD7uB2HX3Adlzhsm.png" alt="0*tD7uB2HX3Adlzhsm" width="600" height="400" loading="lazy"></p>
<p>对于许多公司（包括苹果、微软和许多其他公司），你将与几个特定的团队进行面试。当你获得这份工作时，你将只会参与其中一个团队的工作。</p>
<p>然而，对于一些公司，如谷歌和Facebook，他们会将大多数候选人作为通用的软件工程师（SWE）进行面试，并且只有在决定向该候选人发放offer后才尝试将其匹配到一个团队。由于我只经历过这两家公司的团队匹配过程，下面我将谈一谈我的经验。</p>
<p>Facebook会先发出一个正式的书面offer，然后再将你匹配到一个团队。团队匹配可以在你加入之前进行，例如你与招聘经理通话时，或者在你加入后，在前4-6周的培训/入职阶段进行。这一过程的优点是候选人知道他们的offer是确定的。缺点是你可能会面临在入职培训期间找不到喜欢的团队的风险。不过我在Facebook的朋友告诉我，这种情况极不可能发生。</p>
<p>谷歌的做法则非常不同。他们不像Facebook那样进行长时间的培训/入职流程。他们首先会给你一个口头offer，然后要求你在<strong>发出正式的书面offer之前</strong>匹配到一个团队。即使在你被匹配到一个团队之后，这个offer也可能会被招聘委员会拒绝或修改，甚至是最终的薪酬，也可能由薪酬委员会在最初的口头offer基础上进行调整。</p>
<p>我从许多帖子中了解到，这个过程既漫长又艰难。就我个人经历来看，团队匹配过程花了我1.5个月，这确实漫长且令人沮丧。有时候我真的认为我无法完成团队匹配过程，并想加入另一家公司。</p>
<p>如果谷歌像Facebook那样先给我一个正式的书面offer，这会为我减少很多压力，因为我会明确知道自己将要为谷歌工作，只需找到一个团队即可。幸运的是，事情最终对我来说很顺利，我终于与一个自己选择的小组匹配，之后不久offer也被各个委员会批准了。</p>
<h3 id="">如何选择一个团队</h3>
<p><img src="https://miro.medium.com/max/1540/0*QValU2nlepbet2yU" alt="0*QValU2nlepbet2yU" width="600" height="400" loading="lazy"></p>
<p>这里有一些我在面试期间或录用阶段问自己的一些问题。你不需要问面试官这些问题，但你需要从你的面试中，或者从与公司和团体的对话中提取出这些问题的答案。</p>
<ol>
<li><strong>我将加入哪个组?</strong> 我想加入公司的一个“核心”小组——公司为这个小组大量投入资源，这样的小组通常是它的旗舰产品集团或高增长集团。在这些团队中工作可以得到稳定性和成长。</li>
<li><strong>我将为公司或小组做什么类型的工作?</strong> 我寻找的是能在我以后的职业生涯中应用的工作经验。我更喜欢使用公开可用的工具和技术，而不是使用公司的专有技术。举个例子，如果我在做一个广告推荐机器学习系统，我相信我很容易就能找到另一家想要改进其广告/产品推荐系统的公司。但是，我对使用公司专有的脚本语言自动化一些内部流程不太感兴趣。</li>
<li><strong>我能在这家公司/小组里学到什么?</strong> 工作是双向选择，能为双方都带来价值的。我希望能够为我的公司做出很大贡献，与此同时，我想从工作中学到很多东西，这样当我找下一份工作的时候，我掌握的技能就能让我很受欢迎。我相信AI/ML、计算机视觉、云计算、自动驾驶汽车和AR/VR技能在未来会有很大的需求，所以我想在下一份工作中学习这些技能。</li>
<li><strong>我对这份工作有热情和激情吗?</strong> 如果你对自己的工作没有激情，那么不管这份工作给你多少薪水，你都不会快乐，也不会投入120%的精力。我对量化金融充满热情，因为看到数学和技术改变整个行业是很酷的。但在10多年后，我现在对AI/ML充满热情。你不需要告诉电脑该怎么做，这真是太棒了，太神奇了。你所要做的就是给它提供数据，它会找出比别人用代码做得更好的方法。</li>
<li><strong>我喜欢我的领导和同事吗?他们喜欢我吗?</strong> 想要一个欣赏你并在新工作中支持你的领导。你也想享受和队友在一起的时光，因为你每天和他们在一起的时间超过8个小时，甚至比和家人在一起的时间还要长!</li>
</ol>
<p>很多时候并不是所有这些问题都能得到让你满意的答案，然后你必须权衡利弊。但如果以上任何一项给了你一个危险的信号，我会认真考虑放弃那个职位。</p>
<p>我最终选择了谷歌Pixel Phone的相机组，因为它提供了我非常感兴趣的三样东西:计算机视觉、机器学习和硬件，这正是我的机器人自动驾驶汽车项目的全部内容!此外据我所知，谷歌现在正致力于其硬件和智能家居战略，Pixel手机将成为该生态系统的核心部分。</p>
<h2 id="">结语</h2>
<p><img src="https://miro.medium.com/max/445/1*ehMi2Z73NL4MxN6alUr1yw.png" alt="1*ehMi2Z73NL4MxN6alUr1yw" width="600" height="400" loading="lazy"></p>
<p>感谢你读到这里。在过去的6个月里，我经历了一段漫长的旅程。通过我的个人项目和密集的面试准备，我相信我现在是一个更好的工程师(或者至少是一个更好的面试者 ;-)。在这篇文章中，我讲述了自己的经验和教训。</p>
<p>我发现数据结构和算法是任何科技面试的核心，除了练习更多的LeetCode上的问题外，没有捷径可以做得更好。一旦你对数据结构与算法和系统设计问题有了良好的了解，我在这篇文章中提出的建议应该会帮助你做些更有利于你的事情。</p>
<p>我建议你把这篇文章收藏起来，因为在接下来的2-6个月里，随着你找工作的进展，你会需要回顾这篇文章。在你真正进入某一阶段之前，那之后的阶段中所需要的一些技巧并没有多大意义。例如，你可能需要进行几次电话面试来理解第四步中提出的建议，在你得到任何offer之前，你当然不应该考虑第五步的offer谈判。</p>
<p>如果你正在参加面试，请在下面留言，让我知道我的建议哪些有效，哪些不太有效，这样其他人也会受益。也欢迎在评论中提出新的建议，谢谢!</p>
<h2 id="">一些使用到的资源</h2>
<p>以下是我在求职过程中使用过的资源的附录(按字母顺序排列)。希望你也会发现它们对你有帮助。</p>
<p><a href="https://www.teamblind.com/"><strong>Blind</strong></a>: 这是一个允许不同公司(主要是科技公司)的员工匿名发布问题和答案的应用程序。人们通常会讨论职业决策、公司文化、面试和薪酬等问题。</p>
<p>我发现这个平台有好有坏。好的地方是你会得到更坦诚的回答/回应，有时还会得到内幕消息。坏的地方是有些人会因为匿名而写下冒犯性的回复，这样的环境有时可能是有害的。我已经警告过你了。</p>
<p><img src="https://miro.medium.com/max/730/0*r7S6egJR6P9O9WP7" alt="0*r7S6egJR6P9O9WP7" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.coursera.org/"><strong>Coursera</strong></a>: 学习你感兴趣的科目。它是免费的，或者只收取象征性的费用。我在那里学了ML和DL课程。</p>
<p><img src="https://miro.medium.com/max/512/0*EcegqF8UG1uE9qGO" alt="0*EcegqF8UG1uE9qGO" width="600" height="400" loading="lazy"></p>
<p><a href="http://deeplearning.ai/"><strong>DeepLearning.ai</strong></a> <strong>and</strong> <a href="https://www.freecodecamp.org/news/career-switchers-guide-to-your-dream-tech-job/Workera.ai"><strong>Workera.ai</strong></a></p>
<ul>
<li>如果你对计算机视觉(CV)或自然语言处理(NLP)中的深度学习技术感兴趣，参加5门课程的深度学习系列课程。</li>
<li>通过筛选测试，被推荐给顶尖科技公司ML团队的招聘经理</li>
</ul>
<p><a href="https://amzn.to/31O2JBe"><strong>Elements of Programming Interviews</strong></a> <strong>Book:</strong> 这是另一本优秀的面试准备书，类似于CtCI书。但它更适合有经验的工程师，而CtCI可以被没有多少算法背景的初学者阅读。</p>
<p><img src="https://miro.medium.com/max/350/0*5aU2C4jyITY0A8-D.jpg" alt="0*5aU2C4jyITY0A8-D" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.geeksforgeeks.org/"><strong>GeeksForGeeks</strong></a>: 这是另一个与LeetCode类似的有非常全面的A&amp;DS问题和解决方案的网站。这个网站是免费的，有时会包含在LeetCode中找不到的面试问题。</p>
<p><img src="https://miro.medium.com/max/667/0*ZTMjAXFNZJnxw5wM.png" alt="0*ZTMjAXFNZJnxw5wM" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.glassdoor.com/index.htm"><strong>Glassdoor</strong></a></p>
<p><img src="https://miro.medium.com/max/589/1*qVt2_-4lyAnBgBxpQ7JV5A.png" alt="1*qVt2_-4lyAnBgBxpQ7JV5A" width="600" height="400" loading="lazy"></p>
<ul>
<li>查看公司的员工评价</li>
<li>找到之前的面试问题</li>
<li>设置工作提醒以接收新的工作更新</li>
</ul>
<p><a href="https://www.google.com/"><strong>Google Search</strong></a>: 生活中<strong>任何</strong>问题的起点。我还需要多说吗?</p>
<p><img src="https://miro.medium.com/max/1280/0*foG6evRh08Khr-wZ.jpg" alt="0*foG6evRh08Khr-wZ" width="600" height="400" loading="lazy"></p>
<p>[<strong>Internal Employees:</strong>] 总是试着联系你的朋友或者朋友的朋友，如果他们在你想申请的公司工作。他们将是你最有效的线人和支持者。如果你适合这份工作，<a href="https://www.linkedin.com/in/dctian/">在领英上联系我</a>, 我很乐意把你推荐给谷歌，或者我在其他顶尖科技公司工作的朋友。</p>
<p><a href="http://leetcode.com/"><strong>LeetCode</strong></a></p>
<ul>
<li>练习A&amp;DS问题</li>
<li>使用LeetCode高级版(35美元/月或99美元/年带优惠券)查找给定公司过去的面试问题。我建议你付年费。就把它想象成你的编程大脑的健身房会员吧。</li>
</ul>
<p><a href="https://levels.fyi/"><strong>Level.fyi</strong></a>: 这个网站按公司、资历级别、地点和工作职能列出了众包工资信息，这样你就知道你是否从公司得到了一个公平的报价。当然，这只适用于大公司，因为大公司往往有更多的数据来源。</p>
<p><img src="https://miro.medium.com/max/560/0*y6YcevhJEHIeMO0-.jpg" alt="0*y6YcevhJEHIeMO0-" width="600" height="400" loading="lazy"></p>
<p><a href="http://linkedin.com/"><strong>LinkedIn</strong></a>:</p>
<ul>
<li>查找人员的专业和教育背景</li>
<li>打开“Open to new opportunities”设置，这样招聘人员就能找到你。</li>
<li>使用领英高级连接到谁感兴趣的人你</li>
<li>设置职位提醒以接收新的职位更新</li>
</ul>
<p><a href="https://medium.com/"><strong>Medium</strong>/<strong>Blog Sites</strong></a>:</p>
<ul>
<li>阅读教程和操作指南来帮助你的个人项目</li>
<li>在媒体或其他地方写博客，展示你自己的个人项目</li>
</ul>
<p><img src="https://miro.medium.com/max/590/1*YNzUC78SPulsAznKTeUyNw.png" alt="1*YNzUC78SPulsAznKTeUyNw" width="600" height="400" loading="lazy"></p>
<p><a href="https://triplebyte.com/iv/Wzwz8pq/cp/header"><strong>TripleByte</strong></a>: 如果你通过了他们的技术筛选，你就可以跳过电话面试，直接去现场面试。详情请参阅上面的详细说明。</p>
<p><a href="https://youtube.com/"><strong>YouTube</strong></a></p>
<p><img src="https://miro.medium.com/max/491/1*lVrmEfw_VMiq51OoZNKVkw.png" alt="1*lVrmEfw_VMiq51OoZNKVkw" width="600" height="400" loading="lazy"></p>
<ul>
<li>当你在做个人项目时，观看教程和操作指南</li>
<li>观看系统设计和算法视频</li>
<li>警告：不要在准备面试期间分心，看几个小时的视频摸鱼取乐！</li>
</ul>
<p>就是这些，这是一项艰巨的任务，但只要你付出时间和努力，你就能做到。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 帮助初级开发者提升自己的 9 个好习惯 ]]>
                </title>
                <description>
                    <![CDATA[ 你曾经有过坐下来一一列举你的习惯的时候吗？习惯造就了我们。好的习惯帮助你变成你想要的样子，而不好的习惯会慢慢地将你变成你最不想成为的样子。 在做了12年多的软件开发者之后，我养成了一些令我引以为傲的习惯，也养成了一些我希望改掉的习惯。大多数时候，我并没有意识到我的习惯，但回顾过去，我很清楚哪些习惯帮助我成长，哪些习惯阻碍了我。这促使我盘点并写下一些开发者的好习惯，希望能激励你也这么做。 如果你刚开始做开发人员，看看下面列出的习惯，问问自己这些习惯是否能帮助你成为你想成为的人。意识到你的习惯并积极培养它们，然后成为一名优秀的软件开发者。 自发做一些以前没有做过的事情 在你职业生涯的开始，你知道的事情不多。当你加入一个新项目时，你会觉得自己像是一个“冒牌开发人员”，因为即使你连他们在每场会议上抛出的缩略词、技术和框架的一半都不理解，他们也在向你支付报酬。而另一半你只知道一点点，因为你在谷歌上搜索过。 把“在你职业生涯的开始”换成“在任何新项目的开始”，你就对软件开发职业生涯有了一个很好的总结。 每个新项目，我们都需要重新开始。每一次都有新的人要认识，新的需求要理解，新的框架要学习 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/good-habits-for-junior-developers/</link>
                <guid isPermaLink="false">6615feb4415343040cce0cc7</guid>
                
                    <category>
                        <![CDATA[ 自我提升 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Wed, 10 Apr 2024 03:05:12 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/04/my-life-journal-WI30grRfBnE-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/good-habits-for-junior-developers/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">9 Habits I Wish I Had as a Junior Developer</a>
      </p><!--kg-card-begin: markdown--><p>你曾经有过坐下来一一列举你的习惯的时候吗？习惯造就了我们。好的习惯帮助你变成你想要的样子，而不好的习惯会慢慢地将你变成你最不想成为的样子。</p>
<p>在做了12年多的软件开发者之后，我养成了一些令我引以为傲的习惯，也养成了一些我希望改掉的习惯。大多数时候，我并没有意识到我的习惯，但回顾过去，我很清楚哪些习惯帮助我成长，哪些习惯阻碍了我。这促使我盘点并写下一些开发者的好习惯，希望能激励你也这么做。</p>
<p>如果你刚开始做开发人员，看看下面列出的习惯，问问自己这些习惯是否能帮助你成为你想成为的人。意识到你的习惯并积极培养它们，然后成为一名优秀的软件开发者。</p>
<h1 id="">自发做一些以前没有做过的事情</h1>
<p>在你职业生涯的开始，你知道的事情不多。当你加入一个新项目时，你会觉得自己像是一个“冒牌开发人员”，因为即使你连他们在每场会议上抛出的缩略词、技术和框架的一半都不理解，他们也在向你支付报酬。而另一半你只知道一点点，因为你在谷歌上搜索过。</p>
<p>把“在你职业生涯的开始”换成“在任何新项目的开始”，你就对软件开发职业生涯有了一个很好的总结。</p>
<p>每个新项目，我们都需要重新开始。每一次都有新的人要认识，新的需求要理解，新的框架要学习。这就是为什么学习新东西是很重要的事，如果你只是继续做你知道的事情，你永远不会有信心开始一个新项目，对未知的恐惧总是存在的。</p>
<p><strong>如果你养成了自愿做你一无所知的任务的习惯，你就会不断地学到新东西。</strong></p>
<p>如果一个构建需要被修复，而你从未使用过对应的构建系统，那就开始吧！你将学习到构建管理相关的知识。</p>
<p>如果JavaScript前端中有一个bug，而你到目前为止只做过Java后端相关的工作，那就修复它吧！你将学习新的JavaScript的语言特性。</p>
<p>做一些你没有信心的事情是一个很好的成长方式。不过，一定要管理好别人对你的期望。不要假装你是处理这类事情的高手，告诉他们你以前没有做过，但你想学着去做这件事。</p>
<h1 id="">请求结对编程</h1>
<p>如果你因为对任务的上下文不熟悉而陷入困境，无法开始任务，那么请找一个在该主题上有经验的人与你结对编程。</p>
<p><strong>结对编程是开始任务工作的绝佳方式。</strong> 与你的搭档讨论需求，直到你明白任务的期望产出是什么，然后一起讨论解决方案。</p>
<p>上下文是什么？你需要触及哪些代码库？代码库中有哪些显式和隐式的约定？</p>
<p>但你还可以进一步地进行结对编程。<strong>不仅仅是为了开始一个任务而结对，还要安排更多与你的搭档共同工作的时间</strong>。在启动任务后，你们需要一起开始工作，你推进任务进度，你的搭档提供建议，然后你和你的搭档进行角色对调。</p>
<p>通过这种方式，你甚至可以了解你的搭档是如何思考和解决问题的。你只会从中受益！即使只是学到了一个新的IDE快捷键。</p>
<p><strong>关于在家工作的说明：</strong> 在家工作的那段时间我遇到了以前不会成为问题的事情。我曾经犹豫是否要求队友与我结对编程。曾经在办公室里简单地轻拍队友的肩膀进行高效率的沟通，在远程工作和使用视频会议软件进行沟通时却成了一个很大的障碍。</p>
<p>如果这是你团队中的问题，与你的队友讨论一下（例如，在回顾会议中），之后的沟通会变得容易得多。不同场景下采用的沟通方式只是一个需要重新学习的习惯。</p>
<h1 id="">谈谈你在做的事（和你没有在做的事）</h1>
<p>我不记得我有多少次欣然接受一项任务后以为自己一天就能完成，但一周后我仍然在执行这个任务。</p>
<p>这种情况随着经验的积累变好了许多，但我仍然发现自己在开始一项任务前，对这项任务做出了过于乐观的估计。有太多的理由让我们做出乐观的评估:</p>
<ul>
<li>因为截止日期迫在眉睫，需要快速交付这个新功能的压力</li>
<li>在同伴中表现良好的压力</li>
<li>事情没有按照我的预期运行（这是最常让我困惑的一点，即使我有多年的经验，我也依然无法规避）</li>
<li>其他的理由......</li>
</ul>
<p>很有可能大多数情况下你对任务的评估最终都过于乐观了。面对这种情况，你能做些什么来改变这种现状呢？</p>
<p><strong>你可以在评估任务的过程中管理期望。</strong></p>
<p>不断地谈论你在做什么，是什么阻碍了你。我所说的“持续”并不是指你应该每隔15分钟向整个团队提供一次状态更新。但至少要确保相关人员在一天开始或结束时知道你的处境。</p>
<p>所以，如果你的经理/团队/项目经理/产品经理/利益相关者期待你的结果，每天给他们一个快速的更新:“这是我一直在做的事，这是我下一步要做的事，这是我面临的一个问题，这些都是我的选择。”</p>
<p>这会让每个人都知道你的进步。如果你碰壁了，没人会责怪你，只要你让他们知道。</p>
<p>这将使“为什么花了这么长时间?”之类的讨论成为过去。作为一个额外的好处，状态更新将引发有助于解决问题的讨论。</p>
<p>在最好的情况下，这种状态更新在团队中是仪式化的。它通常被称为“每日站立”，每个团队成员都会快速向团队其他成员更新他们的进度和问题。</p>
<p>但即使你有这样的日常习惯，也请花几分钟时间考虑一下是否应该向不参与“每日站立”的人更新信息。他们应该被包括在内吗？还是应该通过其他方式通知他们？</p>
<p><strong>养成定期向那些关注你的工作成果的人更新信息的习惯。</strong></p>
<h1 id="">写博客</h1>
<p>对你来说我可能不是第一个这样说的人，但我还是要说:写博客！</p>
<p>它甚至不必是公开的。它可以是公司wiki中的几页，也可以是带有示例代码和几行解释性文本的GitHub存储库的集合。</p>
<p>为什么呢？</p>
<p><strong>因为“带着教会他人的目的进行写作”是一个很棒的学习和成长的方式。</strong></p>
<p>写下你是如何解决一个棘手的问题的。写下如何使用你一直想尝试的新框架。或者把你每周做的事情写在博文里（这也有助于养成“谈论你正在做的事情”的习惯，因为你可以查看你一直在做的事情）。</p>
<p>我开过几次博客。一开始很难保持动力，因为没有人会读你的博客文章，在收不到任何反馈的情况下坚持写作会有一种非常奇怪的感觉，所以我停下来了。然后，我在三年前开始了<a href="https://reflectoring.io/">我现在的博客</a>，在没有读者的情况下写了半年。我才注意到我的“robots.txt”文件不允许搜索引擎索引我的博客！所以我修改了robots.txt文件，人们开始读我写的东西了。虽然不多，但它给了我继续下去的动力。所以，我坚持下来了，在这个过程中我不断调整我的写作技巧，我的博客业发展到每月浏览量超过20万。</p>
<p>这一切都是因为我开始写<em>我</em>想学习的框架和<em>我</em>已经解决的问题，这样<em>我</em>就可以在<em>我</em>需要的时候再次查阅我的文章。做这件事的初衷不是因为我想吸引大批观众。</p>
<p>写博客一开始是一件苦差事，但如果你坚持下去，它会变得非常有益。如果你这样做的目的是为了学习和教学，你不仅会学到很多东西，而且其他人最终会注意到你的博客，它将打开一个充满机会的世界。</p>
<h1 id="">拥有一个记笔记的本子和一套体系</h1>
<p>我最近才成为笔记本的忠实粉丝。我说的不是电脑上的笔记本，而是真正的纸质笔记本。我随身携带它（和一支笔！），这样我可以随时记录我认为重要的事情。</p>
<p>当我听讲座时，或者等公交车的时候，甚至是考虑这周我可以做什么晚餐时，我都会做笔记。</p>
<p>我还使用笔记本来维护列表：我想读的书、我想尝试的框架、我想为我的副业项目添加的功能。最重要的是，我用它在阅读书籍时做笔记，因为这样可以保留我从书中学到的知识。</p>
<p>我把所有压在我心头的事情都记下来。<strong>如果我不写下它，它会让我的大脑忙碌不休</strong>, 有时甚至会让我感到焦虑，导致我难以入睡。</p>
<p>我在没有笔记本的情况下感到焦虑的原因是我不信任我的记忆。如果你的记忆力很好，可以回忆起你一周前所思考的一切，那么你可能不需要笔记本。但如果你的记忆力像我一样断断续续，这将对你保持平静大有帮助。</p>
<p><strong>要建立对你笔记本的信任，你需要一套体系</strong>. 你需要说服自己任何你写入笔记本的内容都不会丢失。</p>
<p>在笔记本的前几页创建一个索引以便检索信息，然后养成定期复习和处理笔记的习惯。</p>
<p>例如，为了处理我在阅读书籍时做的笔记，我会在读完书后复习这些笔记，并在我的博客上写一篇书评。几乎没有人读这些书评，但写书评的过程使我学到的东西更加牢固。</p>
<h1 id="">持续记录你的成就</h1>
<p>拥有一个笔记本也可以帮助你养成下一个习惯：记录你的成就。</p>
<p>如我所说，我的记忆力在最佳状态下也是断断续续的。我通常能记住昨天我吃了什么午餐，但如果我全神贯注地解决一个复杂的问题，我的记忆效率会大大降低。</p>
<p>这就是为什么我喜欢在一天结束时记录我的成就。这通常不是大的成就，而是小小的胜利 - 比如解决了一个bug，或者完成了我正在开发的软件中新增功能的其中一个步骤。我也会记录个人的胜利，比如坚持早晨的锻炼计划。</p>
<p>我每天晚上在我的笔记本上创建一个要点清单，但如果你更喜欢比如电子表格或其他你最熟悉的工具等数字媒介，你也可以用它们进行记录——只要你能坚持下去。</p>
<p>随着时间的推移，这些成就会累积起来。你可能想标记那些对你来说最重要的成就，这样你以后就可以轻松找到它们。</p>
<p>然后，在像<strong>绩效评估</strong>这样的场合，你可以查看那个列表，<strong>找出与该场合相关的成就，并列出它们以作准备。</strong> 有准备的绩效评估总是更有效。</p>
<p>拥有一个你的成就列表也有助于在日常情况下讲述你做了什么（参见习惯“谈论你正在做的事情”）。</p>
<h1 id="">为重要任务预留时间</h1>
<p>每天结束时，我经常觉得自己什么都没做。虽然记录你的成就或者你做的事有作用，但你仍然需要真正地去完成那些事情。</p>
<p>你可能一直在开会，从一个会议到另一个会议，然后一天很快就结束了。会议结束后，你想继续在会议前开始的任务，但正当你进入状态时，下一个会议就开始了。会议结束后，你又得重新开始，因为你失去了上下文。</p>
<p><strong>频繁的上下文切换会降低生产力。</strong></p>
<p>如果有一件事让我学会提高生产力，那就是为你想完成的事情预留专门的时间段。如果你没有为某个任务预先规划时间段，那么你开始这个任务的机会就会很小。它会被日常工作或其他计划中的工作所占据。</p>
<p>实际上，没有一种单一的方法来实施时间管理习惯，说实话，我每隔几个月就会从一个生产力方法跳到另一个方法。但核心始终是相同的：在你的一天中为你最想完成的事情预留一些时间。</p>
<p>我在早上工作前预留一个小时的时间，为<a href="https://reflectoring.io/">我的博客</a>写文章（或者为其他博客，就像这篇文章一样）。大多数日子，我也会在晚上预留一个小时的时间，当孩子们上床睡觉时，我会用这个时间来做任何可能的副业项目。</p>
<p>目前我有一个Trello板，<a href="https://reflectoring.io/organizing-work/#my-system-for-organizing-work">每周都有一个专栏</a>，我会在那里放置我想要在早上和晚上做的任务。我每周会更新一次那个板子，列出我下周想要做的事情，这样我就不必浪费我宝贵的预留时间去考虑下一步要做什么。</p>
<p>我为我的软件开发工作也准备了一个非常相似的Trello板。每天早上，我会考虑我想做的事情，并将它们放入当天的专栏。</p>
<p>我还会在我的日历中预留至少2小时的专注时间，这样我的同事就不会尝试在那个时候安排任何会议。那时我会完成我的任务清单。</p>
<p>如何管理你的时间实际上并不重要，<strong>但重要的是要这样做并将其养成习惯</strong>。否则，你的日子会被那些对你不重要的事情所占据。</p>
<h1 id="">当遇到困难时，休息一下</h1>
<p>作为软件开发者，我们经常会遇到困难。当我陷入困境找不到解决问题的方法时，我真的会很生气。</p>
<p>当你遇到困难时，休息一下是一个显而易见的建议，但实施起来却很难。“我离解决这个问题非常近了，现在我不能休息！”</p>
<p>而且现在休息意味着稍后我需要重新回到思考这个问题的状态。上下文切换是浪费时间的首要原因，为什么我应该刻意地切换上下文呢？</p>
<p>当你陷入困境时，你的思维并不清晰。你会想到“自己陷入到这个问题中”是多么的愚蠢，你的团队成员可能会很容易地解决它，以及为什么他们总是得到简单的任务。但你并没有思考如何解决这个问题。</p>
<p>休息一下，转而做一些其他事情。或者更好的是，第二天再试。离开问题一段时间会让你发现之前想不到的解决方案。</p>
<p>如果你之前没有尝试过这种方法，你不会相信多少问题在第二天早上就“神奇地解决”了。而这主要是因为你发现了一个之前没有想到的解决方案。</p>
<p>“去休息”这件事说起来很容易，但是你如何判断自己当前处于“困境模式”，然后说服自己暂时放下问题不去工作呢？</p>
<p>说实话，我自己在这方面并不擅长，因为我通常希望尽快完成这个愚蠢的任务，这样我就可以证明我已经取得了一些成果！</p>
<p>但我发现对我有帮助的是<strong>将我的一天分成30分钟的时间段，并在每个时间段结束后进行简短的回顾。</strong> 这种技巧被称为番茄工作法，源于那些番茄形状的厨房计时器。</p>
<p>每个番茄时钟结束后，我会问自己我是否仍然处于“解决模式”，或者我是否陷入困境，应该暂时做些其他事情。</p>
<p>番茄工作法的一个好处是，你可以将一个单位时间的结束作为其他习惯的触发器。</p>
<p>例如，我用它作为站起来伸展一下肌肉和喝点水的触发器。这有时被称为“习惯叠加”，因为你在一个习惯上叠加另一个习惯，这是非常有效的。</p>
<p>如果你想更多地了解习惯，我强烈推荐詹姆斯·克利尔（James Clear）的书《原子习惯》（Atomic Habits）。</p>
<h1 id="">不要追求万金油</h1>
<p>我写了一本关于特定架构风格的<a href="https://reflectoring.io/book">书籍</a>，我经常收到这样的邮件：“我喜欢某种架构风格，我想在所有项目中都应用它！我该如何做呢？”</p>
<p>你能猜到我的答案是什么吗？</p>
<p>没有一种单一的架构风格适用于所有问题。</p>
<p>当项目规模较小时，你可以构建一个简单的CRUD API。当你有一个复杂的领域模型时，你会构建一个更复杂的六边形架构。当你在特定的上下文中构建微服务时，你可以使用上百种不同风格的架构。</p>
<p>同样地，没有一种单一的框架适用于每一个项目。也没有一种单一的最佳编程语言或编码风格。</p>
<p>不要追求万金油，它们并不存在。</p>
<p><strong>如果你的观点有充分的论据支持，那是好的。</strong> “这是最好的架构风格”或“我一直都这样做”并不是好的论据，人们会拆穿它们。</p>
<p>想象一下，你的团队中有一个开发者，对所有事情都有自己的看法，总是想按照自己的方式去做，“因为这是最好的方式”。你会很快厌倦这样的人。不要成为那样的人。</p>
<h1 id="">养成这些习惯！</h1>
<p>哇，这篇文章比我想象的要长。我希望这篇文章能给你一些启发，让你知道在发展软件开发者的职业生涯时应该考虑些什么。当然，我还没有完全掌握这些习惯，但我每天都在努力做得更好。</p>
<p>选择一个最能让你产生共鸣的习惯，并有意识地把它应用到日常工作中。</p>
<p>期待你在<a href="https://twitter.com/TomHombergs">Twitter</a>告诉我这些习惯是如何行之有效的！我很高兴能收到你的反馈。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JavaScript 中的正则表达式（RegEx）——初学者手册 ]]>
                </title>
                <description>
                    <![CDATA[ 正则表达式，也被称为 regex，是用于模式匹配和文本处理的强大工具。无论是验证用户输入、从字符串中提取数据，还是进行高级的文本处理任务，理解正则表达式对开发人员来说都是必不可少的。 这份全面的指南将带领你了解 JavaScript 中正则表达式的基础知识，包括如何创建和使用正则表达式、正则表达式中的特殊字符、标志以及一些实际的示例。 预备知识 虽然本教程旨在适合初学者，但具备基本的 JavaScript 基础知识将会有所帮助。熟悉 JavaScript 中的变量、数据类型、函数和字符串处理将有助于理解本教程涵盖的概念。 目录  1.  什么是正则表达式      – 如何编写一个正则表达式的模式  2.  如何在JavaScript中使用正则表达式      – JavaScript中的正则表达式模式      – 通过标志进行高级搜索  3.  正则表达式中的锚点      ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/regex-in-javascript/</link>
                <guid isPermaLink="false">6602b1876f02f80413b539fe</guid>
                
                    <category>
                        <![CDATA[ 正则表达式 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Tue, 26 Mar 2024 13:13:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/03/Regular-Expressions-in-JavaScript-Cover-2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/regex-in-javascript/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Regular Expressions (RegEx) in JavaScript – A Handbook for Beginners</a>
      </p><!--kg-card-begin: markdown--><p>正则表达式，也被称为 regex，是用于模式匹配和文本处理的强大工具。无论是验证用户输入、从字符串中提取数据，还是进行高级的文本处理任务，理解正则表达式对开发人员来说都是必不可少的。</p>
<p>这份全面的指南将带领你了解 JavaScript 中正则表达式的基础知识，包括如何创建和使用正则表达式、正则表达式中的特殊字符、标志以及一些实际的示例。</p>
<h3 id="">预备知识</h3>
<p>虽然本教程旨在适合初学者，但具备基本的 JavaScript 基础知识将会有所帮助。熟悉 JavaScript 中的变量、数据类型、函数和字符串处理将有助于理解本教程涵盖的概念。</p>
<h2 id="">目录</h2>
<ol>
<li><a href="#what-are-regex">什么是正则表达式</a><br>
– <a href="#how-to-write-regular-expression-patterns">如何编写一个正则表达式的模式</a></li>
<li><a href="#how-to-use-regular-expressions-in-javascript">如何在JavaScript中使用正则表达式</a><br>
– JavaScript中的正则表达式模式<br>
– <a href="#advanced-searching-with-flags">通过标志进行高级搜索</a></li>
<li><a href="#anchors-in-regex">正则表达式中的锚点</a><br>
– <a href="#multiline-mode-of-anchors-and-">锚点的多行模式</a><br>
– <a href="#word-boundaries-b-">单词边界（<code>\b</code>）</a></li>
<li><a href="#quantifiers-in-regex">正则表达式中的量词</a><br>
– <a href="#greedy-quantifiers-">贪婪量词</a><br>
– <a href="#non-greedy-quantifiers">非贪婪量词（懒惰模式）</a></li>
<li><a href="#sets-and-ranges-in-regex">正则表达式中的集合与区间</a><br>
– <a href="#sets-">集合</a><br>
– <a href="#ranges-">区间</a><br>
– <a href="#negating-excluding-ranges-">否定/排除区间</a><br>
– <a href="#predefined-character-classes-">预定义的字符类</a></li>
<li><a href="#special-characters-and-escaping-in-regex">正则表达式中的特殊字符与转义</a><br>
– <a href="#metacharacters-">元字符</a><br>
– <a href="#escape-special-characters-">转义特殊字符</a></li>
<li><a href="#groupings-in-regex">正则表达式中的分组</a><br>
– <a href="#capturing-groups-">捕获组</a><br>
– <a href="#non-capturing-groups-">非捕获组</a><br>
– <a href="#backreferences-">后向引用</a><br>
– <a href="#regex-alternation">正则表达式选择符号</a></li>
<li><a href="#lookahead-and-lookbehind-in-regex">正则表达式中的前瞻断言和后顾断言</a><br>
– <a href="#lookahead-">前瞻断言（?=）</a><br>
– <a href="#negative-lookaheads-">否定前瞻断言（?!）</a><br>
– <a href="#lookbehind-">后顾断言（?&lt;=）</a><br>
– <a href="#negative-lookbehind-">否定后顾断言（?&lt;!）</a></li>
<li><a href="#practical-examples-and-use-cases-of-regex">正则表达式的实际应用示例</a><br>
– <a href="#password-strength-checking-">密码强度检查</a><br>
– <a href="#email-validation-function-">电子邮件地址校验</a><br>
– <a href="#phone-number-formatting-function-">电话号码格式化函数</a></li>
<li><a href="#tips-and-best-practices-for-using-regular-expressions">使用正则表达式的技巧和最佳实践方式</a></li>
<li><a href="#conclusion">总结</a></li>
</ol>
<h2 id="what-are-regex">什么是正则表达式</h2>
<p>正则表达式是一种定义搜索模式的字符序列，通常缩写为“regex”。这种模式提供了一种强大的方式来搜索、替换和操作文本，它被用于在字符串中查找匹配项，帮助你识别特定的文本或字符模型。</p>
<p>在JavaScript中，你可以使用字面量或<code>RegExp</code>构造函数创建正则表达式：</p>
<ul>
<li><strong>使用正则表达式字面量</strong>：模式由斜杠（"/"）包围。</li>
</ul>
<pre><code class="language-javascript">const re = /pattern/;

// 示例
const re = /ab+c/;
</code></pre>
<ul>
<li><strong>使用构造函数：</strong><code>RegExp</code> 构造函数。这种方式允许对正则表达式进行运行时编译，并且在模式可能更改时非常有用。</li>
</ul>
<pre><code class="language-javascript">const re = new RegExp("pattern");

// 示例
const re = new RegExp("ab+c");
</code></pre>
<p>两种方法会产生相同的结果——选择哪一种取决于你的偏好。</p>
<h3 id="how-to-write-regular-expression-patterns">如何编写一个正则表达式的模式</h3>
<p>一个正则表达式模式是由简单的字符或者是简单和特殊字符的组合所构成的。</p>
<ol>
<li><strong>简单模式</strong>：它们匹配精确的字符序列。例如，模式 <code>/abc/</code> 匹配字符串中的序列"abc"。</li>
<li><strong>特殊字符</strong>：它们通过重复或匹配特定类型的字符等功能增强了模式匹配，从而实现了更灵活、更强大的模式匹配。例如，<code>*</code> 匹配前一项出现0次或多次。<code>/ab*c/</code> 匹配 "ac"、"abc"、"abbc" 等等诸如这类形式的字符串。</li>
</ol>
<h2 id="how-to-use-regular-expressions-in-javascript">如何在JavaScript中使用正则表达式</h2>
<p>你可以使用JavaScript中适用于 <code>RegExp</code> 和 <code>String</code> 对象的多种方法来使用正则表达式，例如<code>test()</code>和 <code>exec()</code>方法，以及具有如下语法的方法：</p>
<pre><code class="language-javascript">regex.methodname(string)

// 示例
string.test(string)
</code></pre>
<p>以及类似于 <code>match()</code> 和 <code>replace()</code> 方法，以及具有这种语法的方法：</p>
<pre><code class="language-javascript">string.methodname(regex)

// 示例
string.replace(regex, replacement)
</code></pre>
<p>在这里， <code>string</code> 是字符串， <code>regex</code> 是一种正则表达式模式。</p>
<p>让我们来探讨一下这些方法在实践中是如何使用的。</p>
<p><strong><code>test()</code> 方法</strong>：检查特定字符串是否与指定模式或正则表达式匹配。如果字符串中找到了该模式，则返回 <code>true</code>；否则返回 <code>false</code>。</p>
<pre><code class="language-javascript">let pattern = /hello/;
let str = "hello world";

let result = pattern.test(str);
console.log(result); // 输出为：true
</code></pre>
<p><strong><code>exec()</code> 方法</strong>：根据正则表达式模式的内容搜索字符串中的匹配项。它返回一个数组，其中包含匹配文本、匹配项在字符串中的索引以及输入字符串本身的详细信息。例如：</p>
<pre><code class="language-javascript">let pattern = /world/;
let str = "hello world";

let result = pattern.exec(str);
console.log(result); // 输出为：["world", index: 6, input: "hello world"]
</code></pre>
<p><strong><code>match()</code>方法</strong>: 根据正则表达式模式的内容，在字符串中搜索它的出现次数。它返回匹配的第一个元素。如果具有全局标志（<code>g</code>），则返回一个包含所有找到的匹配项的数组，如果没有找到匹配项，则返回 <code>null</code>。</p>
<pre><code class="language-javascript">let str = "The quick brown fox jumps over the lazy dog.";
let matches = str.match(/the/gi);

console.log(matches); // 输出为： ["The", "the"]
</code></pre>
<p><code>/the/gi</code> 在字符串中搜索所有出现的单词 "the"，不区分大小写。</p>
<p><strong><code>matchAll()</code>方法</strong>: 返回一个用于匹配字符串中正则表达式的所有结果的迭代器。迭代器的每个元素都是一个数组，包含有关匹配的详细信息，包括捕获的分组。</p>
<pre><code class="language-javascript">let str = "Hello world! This is a test string.";
let regex = /[a-zA-Z]+/g;

let matches = str.matchAll(regex);

for (let match of matches) {
    console.log(match);
}
</code></pre>
<p>当你需要获取字符串中所有匹配项的详细信息时，此方法非常有用。</p>
<p><strong><code>search()</code>方法</strong>: 在字符串中搜索指定的模式。它返回字符串中模式的第一个出现的索引，如果未找到模式，则返回 <code>-1</code>。</p>
<pre><code class="language-javascript">let str = "The quick brown fox jumps over the lazy dog";
let pattern = /brown/;

let result = str.search(pattern);
console.log(result); // 输出为： 10
</code></pre>
<p><strong><code>replace()</code>方法</strong>：用另一个子字符串或值替换字符串中指定模式的第一个实例。要替换所有实例，可以在正则表达式中使用全局标志（<code>g</code>）。</p>
<pre><code class="language-javascript">let str = "Hello, World!";
let newStr = str.replace(/o/g, "0");

console.log(newStr); // 输出为： "Hell0, W0rld!"
</code></pre>
<p><strong><code>replaceAll()</code>方法</strong>：替换指定子字符串或模式的所有实例为一个替代字符串。它与 <code>replace()</code> 的不同之处在于，它默认替换所有出现实例，无需使用全局标志（<code>g</code>）。</p>
<pre><code class="language-javascript">let str = "apple,banana,apple,grape";
let newStr = str.replaceAll("apple", "orange");
console.log(newStr); // 输出为： "orange,banana,orange,grape"
</code></pre>
<p>这种方法简化了在字符串中替换所有子字符串实例的过程。</p>
<p><strong><code>split()</code>方法</strong>: 虽然 <code>split()</code> 并不是专属于正则表达式的方法，但它可以接受一个正则表达式模式作为其参数，根据指定的模式或分隔符将字符串拆分为子字符串数组。例如：</p>
<pre><code class="language-javascript">let str = "apple,banana,grape";
let arr = str.split(/,/);
console.log(arr); // 输出为：["apple", "banana", "grape"]
</code></pre>
<p>这些方法根据你的需求提供不同的功能。例如，如果你只需要知道字符串中是否找到了模式，则 <code>test()</code> 或 <code>search()</code> 方法是有效的。如果你需要更多关于匹配的信息，则 <code>exec()</code> 或 <code>match()</code> 方法是合适的。</p>
<h2 id="advanced-searching-with-flags">通过标志进行高级搜索</h2>
<p>在JavaScript中，正则表达式支持模式标志，这些是可选参数，修改了模式匹配的行为。</p>
<p>让我们深入了解两个常见的标志：忽略标志（<code>i</code>）和全局标志（<code>g</code>）。</p>
<h3 id="i">忽略标志（<code>i</code>）</h3>
<p>忽略标志（<code>i</code>）可以使正则表达式在搜索匹配项时忽略大小写敏感性。例如：</p>
<pre><code class="language-javascript">let re = /hello/i;
let testString = "Hello, World!";
let result = re.test(testString);

console.log(result); // 输出为：true
</code></pre>
<p>在这种情况下，正则表达式 <code>/hello/i</code> 会匹配字符串 <code>"Hello"</code>（即使大小写不同），因为我们使用了忽略标志。</p>
<h3 id="g">全局标志（<code>g</code>）</h3>
<p>全局标志（<code>g</code>）允许正则表达式在字符串中找到所有匹配项，而不是在找到第一个匹配项后停止。例如：</p>
<pre><code class="language-javascript">let re = /hi/g;
let testString = "hi there, hi again!";
let result = testString.match(re);

console.log(result); // 输出为：["hi", "hi"]
</code></pre>
<p>在这个例子中，正则表达式 <code>/hi/g</code> 找到了字符串 <code>"hi there, hi again!"</code> 中的两个 <code>"hi"</code> 的实例。</p>
<h3 id="">组合标志</h3>
<p>你可以将标志进行组合以实现特定的匹配行为。例如，同时使用忽略标志（<code>i</code>）和全局标志（<code>g</code>）允许进行不区分大小写的匹配，直到找到模式的所有匹配项。</p>
<pre><code class="language-javascript">let re = /hi/gi;
let testString = "Hi there, HI again!";
let result = testString.match(re);

console.log(result); // 输出为：["Hi", "HI"]
</code></pre>
<p>在这个例子中，正则表达式 <code>/hi/gi</code> 匹配字符串 <code>"Hi there, HI again!"</code> 中的 <code>"Hi"</code> 和 <code>"HI"</code>。</p>
<h3 id="u"><code>u</code> 标志</h3>
<p>虽然不常用，但 <code>u</code> 标志可以正确处理 Unicode 字符，特别是代理项对。代理项对用于表示 UTF-16 编码中基本多文种平面（BMP）之外的字符。</p>
<p><strong>示例：</strong> 让我们考虑一个包含表情符号字符的字符串，并尝试分别使用包含<code>u</code>标志和不包含<code>u</code>标志的正则表达式来匹配它们。</p>
<pre><code class="language-javascript">// 不使用u标志
let result1 = 'Smile Please 😊'.match(/[😒😊🙄]/);
console.log(result1); // 输出为：["�"]

// 使用u标志
let result2 = 'Smile Please 😊'.match(/[😒😊🙄]/u);
console.log(result2); // 输出为：["😊"]
</code></pre>
<p>没有使用 <code>u</code> 标志时，正则表达式无法正确匹配表情符号，因为它们在 UTF-16 编码中表示为代理项对。但是，使用 <code>u</code> 标志时，它可以正确匹配表情符号 <code>'😊'</code>。</p>
<h2 id="anchors-in-regex">正则表达式中的锚点</h2>
<p>锚点是正则表达式中的特殊字符，它们不表示实际字符，而是用于检查字符在字符串中是否处于特定的位置。本文讲解两个主要的锚点：<code>^</code> 和 <code>$</code>。</p>
<p><strong>锚点 <code>^</code></strong>：锚点 <code>^</code> 匹配文本的开头。一般情况下，它检查字符串是否以特定字符或模式开头。</p>
<pre><code class="language-javascript">let str = 'Mountain';
console.log(/^S/.test(str)); // 输出为：false
</code></pre>
<p><strong>锚点 <code>$</code></strong>：锚点 <code>$</code> 匹配文本的结尾。它检查字符串是否以特定字符或模式结尾。</p>
<pre><code class="language-javascript">let str = 'Ocean';
console.log(/n$/.test(str)); // 输出为：true
</code></pre>
<p>你经常会一起使用 <code>^</code> 和 <code>$</code> 来检查字符串是否完全匹配某个模式。</p>
<pre><code class="language-javascript">let isValid = /^\d\d:\d\d$/.test('10:01');
console.log(isValid); // 输出为：true
</code></pre>
<p>这个示例检查输入字符串是否匹配时间格式，例如 "10:01"。</p>
<p>在上面的代码中，<code>^\d\d:\d\d$</code> 确保字符串包含两个数字，后跟一个冒号，然后是两个数字。</p>
<h3 id="multiline-mode-of-anchors-and-">锚点的多行模式（`^` 和 `$`）</h3>
<p>默认情况下，正则表达式中的 <code>^</code> 和 <code>$</code> 锚点以单行模式操作，意味着它们匹配整个字符串的开头和结尾。但在某些情况下，你可能希望匹配多行字符串中各行的开头和结尾，这就是多行模式的作用，可以通过 <code>m</code> 标志来指示。</p>
<p>由于单行模式是默认的，因此它只匹配字符串开头的第一个数字 "1"。</p>
<pre><code class="language-javascript">let str = `1st line
2nd line
3rd line`;

let re = /^\d/g; // "^\d" 匹配字符串开头的数字
let matches = str.match(re);

console.log(matches); // 输出为：["1"]
</code></pre>
<ul>
<li><strong>多行模式（m）</strong>：<code>/^\d/gm</code> 是启用了 <code>m</code> 标志的正则表达式模式。通过利用 <code>m</code> 标志，你可以确保 <code>^</code> 和 <code>$</code> 匹配多行字符串中各行的开头和结尾，而不仅仅是整个字符串本身。</li>
</ul>
<p>因此，它从第一行匹配到 "1"，从第二行匹配到 "2"，从第三行匹配到 "3"：</p>
<pre><code class="language-javascript">let str = `1st line
2nd line
3rd line`;

let re = /^\d/gm;
let matches = str.match(re);

console.log(matches); // 输出为：["1", "2", "3"]
</code></pre>
<p>这在处理包含多行或换行符的文本时特别有用。</p>
<h3 id="word-boundaries-b-">单词边界（`\b`）</h3>
<p><code>\b</code> 是正则表达式中的一个特殊字符，称为锚点，就像 <code>^</code> 和 <code>$</code> 一样。它用于匹配字符串中的位置，其中一个单词字符（如字母、数字或下划线）之前或之后不是另一个单词字符。例如：</p>
<ul>
<li><code>\bword\b</code> 匹配字符串中的单词 "word"，但不匹配子串如 "wording" 或 "swordfish"。</li>
</ul>
<pre><code class="language-javascript">let pattern = /\bword\b/;
let pattern2 = /word/;
console.log(pattern.test("This is a word.")); // 输出为：true
console.log(pattern.test("This is wording.")); // 输出为：false (没有匹配"wording")
console.log(pattern2.test("This is wording")); // 输出为：True
</code></pre>
<p><code>/word/</code> 在字符串中的任何位置匹配子串 "word"。它在 "This is wording." 中匹配 "word"，因为它不包括任何单词边界断言。</p>
<p>其他示例包括：</p>
<ul>
<li><code>\b\d+\b</code> 匹配字符串中的整数，但不包括数字字符相邻的非数字字符。</li>
<li><code>^\bword\b$</code> 匹配仅由单词 “word” 组成的字符串。</li>
</ul>
<h2 id="quantifiers-in-regex">正则表达式中的量词</h2>
<p>在正则表达式中，量词允许你指定你想要在字符串中匹配的字符或字符类的数量。它们是定义你要查找的字符或组的实例数量的符号或字符。</p>
<h3 id="n">精确数量量词 <code>{n}</code></h3>
<p>最简单的量词是 <code>{n}</code>，它指定了你想匹配的字符或字符类的精确的数量。（译者注：该量词的一般使用形式为<code>x{n}</code>，其中x为任意字符或字符类，n为正整数，该量词的含义为“与‘只重复出现n次的x’对应的部分匹配”。）假设我们有一个字符串 "Year: 2022"，我们想从中提取年份：</p>
<pre><code class="language-javascript">let str = 'Year: 2022';
let re = /\d{4}/; // 匹配一个四位数字；基本上是等同于\d\d\d\d的更简洁、更好的写法。

let result = str.match(re);

console.log(result); // 输出为：["2022"]
</code></pre>
<p>（译者注：在上述例子中，该量词对应的模式只会与四位数字匹配。对于小于四位数字的字符串，例如203，该模式不会匹配；对于大于四位数字的字符串，例如20356，该模式会匹配最前面四位数字“2035”。<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions/Quantifiers#%E7%B1%BB%E5%9E%8B">参考文档链接</a>）</p>
<h3 id="nm">区间量词 <code>{n,m}</code></h3>
<p>区间量词 <code>{n,m}</code> 匹配一个字符或字符类从 n 到 m 次，包括 n 和 m。例如：</p>
<pre><code class="language-javascript">let str = "The meeting is scheduled for 10:30 AM and ends at 2 PM";
let re = /\d{2,4}/g; // 匹配有2到4位数字的数

let result = str.match(re);
console.log(result); // 输出为：[ '10', '30' ]
</code></pre>
<p>/\d{2,4}/g 匹配连续有2到4位数字的数，即 '10'、'30'。<br>
（译者注：区间量词中，n的取值为0或一个正整数，m&gt;n且m为一个正整数。与精确数量量词<code>{n}</code>相似，例如对于形式为<code>a{2,4}</code>的量词，它不会匹配"candy"中的'a'，而对于"caaaaady"，它只会匹配其中的前四个'a'）</p>
<h3 id="n"><code>{n,}</code> 和简写形式</h3>
<p><code>{n,}</code> 量词匹配一个字符或字符类至少 n 次。此外，还有常见量词的简写表示法。例如：</p>
<pre><code class="language-javascript">let str = 'The price of the item is $2500';
let re = /\d{2,}/g; // 匹配至少有2位数字的数。

let result = str.match(re);
console.log(result); // 输出为：["2500"]
</code></pre>
<h3 id="">简写形式：<code>+</code>, <code>?</code>, <code>*</code></h3>
<p>量词 <code>+</code>、<code>?</code> 和 <code>*</code> 是常见用例的简写表示法。让我们使用简写 <code>+</code> 来匹配电话号码中的一个或多个数字：</p>
<pre><code class="language-javascript">let phone = "+1-(103)-777-0101";
let result = phone.match(/\d+/g); // 匹配一个或多个数字。

console.log(result); // 输出为：["1", "103", "777", "0101"]
</code></pre>
<p>/\d+/g 匹配电话号码中一个或多个连续数字。</p>
<h3 id="">量词：零或一次（<code>?</code>）</h3>
<p>正则表达式中的量词 <code>?</code> 表示前一个字符或组的零次或一次出现。它等同于 {0,1}。例如：</p>
<pre><code class="language-javascript">let str = 'The sky is blue in color, but the ocean is blue in colour';
let result = str.match(/colou?r/g); // 匹配"color"和"colour"

console.log(result); // 输出为：["color", "colour"]
</code></pre>
<p>在这个例子中，正则表达式 <code>/colou?r/g</code> 匹配给定字符串中的 "color" 和 "colour"，允许字母 "u" 出现零次或一次。</p>
<h3 id="">量词：零次或更多（<code>*</code>）</h3>
<p>在正则表达式中，量词 <code>*</code> 表示前一个字符或组的零次或更多次出现。它等同于 {0,}。例如：</p>
<pre><code class="language-javascript">let str = 'Computer science is fascinating, but computational engineering is equally interesting';
let re = /comput\w*/g; // 匹配"computer"和"computational"

let results = str.match(re);

console.log(results); // 输出为：["computer", "computational"]
</code></pre>
<h3 id="greedy-quantifiers-">贪婪量词</h3>
<p>在正则表达式中，量词决定了特定元素在匹配中可以出现的次数。</p>
<p>默认情况下，量词以所谓的“贪婪”模式运行。这意味着它们会尝试匹配尽可能多的前一个元素。例如：</p>
<pre><code class="language-javascript">let regexp = /".+"/g;
let str = 'The "Boy" and his "Friends" were here';
console.log( str.match(regexp) ); // "Boy" and his "Friends"
</code></pre>
<p>它找到一个包含两者（"Boy" and his "Friends"）的匹配，而不是找到两个单独的匹配（"Boy" 和 "Friends"）。</p>
<h4 id="">理解贪婪搜索</h4>
<p>为了理解为什么初始尝试失败，让我们深入了解正则表达式引擎是如何进行搜索的。</p>
<ol>
<li>引擎从字符串的开始处开始，并找到开头的引号。</li>
<li>它继续匹配跟在开头引号后面的字符。由于模式是 <code>".+"</code>，其中 <code>.</code> 匹配任何字符，<code>+</code> 使其匹配一次或多次，引擎会继续匹配字符，直到达到字符串的末尾。</li>
<li>然后，引擎回溯以找到结束引号 <code>"</code> 以完成匹配。它首先假设由 <code>".+"</code> 匹配的最大可能字符数量，并逐渐减少字符数量，直到找到有效的匹配。</li>
<li>最终，引擎找到了一个包含整个子字符串 "Boy" and his "Friends" 的匹配。</li>
</ol>
<p>这种贪婪地匹配尽可能多的字符的行为是正则表达式中量词的默认模式，不总是产生期望的结果。你可以在这个例子中看到这一点，它导致单一匹配，而不是对带引号的字符串进行多个独立的匹配。</p>
<h3 id="non-greedy-quantifiers">非贪婪量词（懒惰模式）</h3>
<p>为了解决贪婪模式的限制，正则表达式也支持量词的懒惰模式。在懒惰模式中，量词后的字符重复的次数是满足模式所必需的最小次数。</p>
<p>我们可以通过在量词后添加一个问号 ? 来启用懒惰模式。例如，*? 或 +? 表示懒惰重复。</p>
<pre><code class="language-javascript">let regexp = /".+?"/g;
let str = 'The "Boy" and his "Friends" were here';
console.log( str.match(regexp) ); // "Boy" "Friends"
</code></pre>
<p>在这个例子中，懒惰量词 <code>".+?"</code> 确保每个带引号的字符串都被单独匹配，通过最小化开头和结束引号之间匹配的字符数量。</p>
<p>让我们逐步跟踪搜索过程，以理解懒惰量词是如何工作的：</p>
<ul>
<li>引擎从字符串的开始处开始，并找到开头的引号。</li>
<li>与其贪婪地匹配直到字符串的末尾的所有字符，懒惰量词 <code>".+?"</code> 只匹配满足模式所需的字符。它一遇到结束引号 <code>"</code> 就停止。</li>
<li>引擎为文本中的每个带引号的字符串重复此过程，导致 "Boy" 和 "Friends" 分别被单独匹配。</li>
</ul>
<h2 id="sets-and-ranges-in-regex">正则表达式的集合与区间</h2>
<p>在正则表达式中，你可以使用集合和区间来匹配特定的字符或在给定模式内的一系列字符。</p>
<h3 id="sets-">集合</h3>
<p>一个集合使用方括号 <code>[...]</code> 来定义。它允许你匹配集合中的任何字符。例如，<code>[aeiou]</code> 匹配元音字母 'a', 'e', 'i', 'o', 或 'u' 中的任何一个。</p>
<p><strong>示例：</strong> 假设我们有一个字符串 <code>'The quick brown fox jumps over the lazy dog.'</code>。为了匹配这个字符串中的所有元音字母，我们可以使用正则表达式 <code>/[aeiou]/g</code>。</p>
<pre><code class="language-javascript">let str = 'The quick brown fox jumps over the lazy dog.';
let re = /[aeiou]/g;
let results = str.match(re);

console.log(results); // 输出为：['e', 'u', 'i', 'o', 'o', 'u', 'o', 'e', 'e', 'a', 'o']
</code></pre>
<p>这个正则表达式匹配字符串中所有元音字母的实例。</p>
<pre><code class="language-javascript">let str = 'The cat chased the rats in the backyard';;
let re = /[cr]at/g;
let results = str.match(re);

console.log(results); // 输出为：['cats', 'rats']
</code></pre>
<p>在这里，正则表达式 [cr]at 匹配以 'c' 或 'r' 开头，并跟着 'at' 的单词。</p>
<h3 id="ranges-">区间</h3>
<p>区间允许你在集合内指定一系列字符。例如，<code>[a-z]</code> 匹配从 'a' 到 'z' 的任何小写字母，而 <code>[0-9]</code> 匹配从 '0' 到 '9' 的任何数字。示例：</p>
<pre><code class="language-javascript">let str = 'Hello World!';
let re = /[a-z]/g;
let results = str.match(re);

console.log(results); // 输出为：['e', 'l', 'l', 'o', 'o', 'r', 'l', 'd']
</code></pre>
<p>在这里，正则表达式 <code>[a-z]</code> 匹配字符串中的所有小写字母。</p>
<h3 id="">否定/排除区间</h3>
<p>要从集合中排除某些字符，你可以在方括号内使用 <code>^</code> 符号。例如：</p>
<pre><code class="language-javascript">let str = 'The price is $19.99';
let re = /[^0-9]/g;
let results = str.match(re);

console.log(results); // 输出为：['T', 'h', 'e', ' ', 'p', 'r', 'i', 'c', 'e', ' ', 'i', 's', ' ', '$', '.'] 
</code></pre>
<p>在这里，<code>[^0-9]</code> 匹配字符串中不是数字的任何字符。同样地，<code>[^a-z]</code> 将匹配任何不是小写字母的字符：</p>
<pre><code class="language-javascript">let str = 'The price is $19.99';
let results2 = str.match(/[^a-z]/g);

console.log(results2); // 输出为：['T', ' ', ' ', ' ', '$', '1', '9', '.', '9', '9']
</code></pre>
<h3 id="predefined-character-classes-">预定义的字符类</h3>
<p>某些字符类具有预定义的简写符号，用于常见字符区间的匹配。</p>
<p><strong><code>\d</code>类</strong>：<code>\d</code>匹配任何数字字符，等价于区间 <code>[0-9]</code>。例如：</p>
<pre><code class="language-javascript">let phone = '+1-(103)-777-0101';
let re = /\d/g;
let numbers = phone.match(re);
let phoneNo = numbers.join('');
console.log(phoneNo); // 输出为：11037770101
</code></pre>
<p>我们使用 <code>match()</code> 和 <code>join()</code> 方法来格式化电话号码。这种方法简化了数据的处理和清理过程，使其适用于各种文本处理应用程序。</p>
<p>类似地，<code>\s</code> 匹配单个空白字符，包括空格、制表符和换行符，而 <code>\w</code> 匹配任何单词字符（字母数字字符或下划线），等价于区间 <code>[a-zA-Z0-9_]</code>。</p>
<p>结合这些类可以实现更灵活、更精确的模式匹配，从而实现各种文本处理任务。示例：</p>
<pre><code class="language-javascript">let str = 'O2 is oxygen';
let re = /\w\d/g;
console.log(str.match(re)); // 输出为：["O2"]
</code></pre>
<p>这些预定义的字符类为常用的字符区间提供了便捷途径。</p>
<p><strong>反向类</strong>，用大写字母表示（例如，<code>\D</code>），匹配任何不包含在相应小写类中的字符。这提供了一种方便的方式来匹配特定集合之外的字符，例如非数字字符、非空白字符或非单词字符。示例：</p>
<pre><code class="language-javascript">let phone = '+1-(103)-777-0101';
let re = /\D/g;
console.log(phone.replace(re,'')); // 输出为：11037770101
</code></pre>
<h2 id="special-characters-and-escaping-in-regex">正则表达式中的特殊字符与转义</h2>
<h3 id="">元字符</h3>
<p>元字符是在正则表达式中具有特殊含义的字符，用于构建用于匹配文本的模式。</p>
<p>锚点 (<code>^</code> 和 <code>$</code>)、交替 (<code>|</code>)、量词 (<code>+</code>, <code>?</code>, <code>{}</code>) 和预定义的字符类 (<code>\d</code>, <code>\w</code>, <code>\s</code>) 都被认为是元字符，每个都在模式定义中有不同的用途。我们还有一些其他的元字符，现在我们来详细介绍它们。</p>
<p><strong>点号 (<code>.</code>)</strong> 是一个具有特殊含义的元字符。它用于匹配除换行符 (<code>\n</code>) 外的任何单个字符。它起到通配符的作用，允许在确切字符未知或无关紧要时进行灵活的模式匹配。</p>
<p>如果你需要点号匹配换行符，你可以在 JavaScript 中使用 <code>/s</code> 标志，该标志启用了 "单行" 模式，使点号匹配任何字符，包括换行符。例如：</p>
<pre><code class="language-javascript">const regex = /a.b/; 

console.log(regex.test('acb')); // true
console.log(regex.test('aXb')); // true
console.log(regex.test('a\nb')); // false（未匹配到换行符）
console.log(regex.test('a\nb', 's')); // true（使用 's' 标志，匹配到换行符）
console.log(regex.test('ab')); // false（'a' 和 'b' 之间缺少字符）
</code></pre>
<p><code>/a.b/</code> 匹配以 'a' 开始，后跟任何单个字符（除换行符外），并以 'b' 结束的任何字符串。</p>
<p>点号 (<code>.</code>) 可以与其他正则表达式元素结合，形成更复杂的模式。例如，<code>/.at/</code> 匹配任何以 'at' 结尾的三个字符序列，如 'cat'、'bat' 或 'hat'。</p>
<h3 id="escape-special-characters-">转义特殊字符</h3>
<p>在正则表达式模式中，当你想要搜索或匹配这些字符，而不触发它们的特殊正则表达式含义，转义特殊字符是至关重要的。</p>
<p>要在正则表达式模式中字面匹配一个特殊字符，你需要通过在其前面加上反斜杠（\）来转义它。这告诉正则表达式引擎将特殊字符视为普通字符。例如：</p>
<pre><code class="language-javascript">let str = 'This ^ symbol is called Caret ';
let re = /[\^]/g;
let results = str.match(re);

console.log(results); // 输出为：['^']
</code></pre>
<p>如果没有<code>\</code>，<code>^</code> 将被解释为字面插入符号。</p>
<p>有趣的事实是，我们用来转义元字符的 <code>/</code> 本身也是一个元字符，可以用另一个反斜杠来转义成 <code>//</code>。</p>
<h2 id="groupings-in-regex">正则表达式中的分组</h2>
<h3 id="capturing-groups-">捕获组</h3>
<p>在JavaScript正则表达式中，捕获组用于提取匹配字符串的特定部分。试想你有一个类似于"resource/id"的路径，例如 "posts/123"。为了匹配这个路径，你可以使用正则表达式 <code>/\w+\/\d+/</code>。</p>
<ul>
<li><code>\w+</code> 匹配一个或多个单词字符。</li>
<li><code>\/</code> 匹配斜杠 <code>/</code>。</li>
<li><code>\d+</code> 匹配一个或多个数字。</li>
</ul>
<p>假设你有一个路径像 "posts/123"，你想捕获 <code>id</code> 部分（123）。我们可以使用捕获组来实现这一点。</p>
<p>要创建一个捕获组，你将想要捕获的正则表达式模式的部分放在括号中。例如，<code>(\d+)</code> 捕获一个或多个数字。</p>
<p>这是它的运行步骤：</p>
<pre><code class="language-javascript">const path = 'posts/123';
const pattern = /\w+\/(\d+)/;

const match = path.match(pattern);
console.log(match);
</code></pre>
<p>输出为：</p>
<pre><code class="language-bash">[ 'posts/123', '123', index: 0, input: 'posts/123', groups: undefined ]
</code></pre>
<p>在这里，<code>'123'</code> 被捕获组 <code>(\d+)</code> 捕获。</p>
<p><strong>使用多个捕获组</strong>：你可以在正则表达式模式中使用多个捕获组。例如，为了从路径 "posts/123" 中同时捕获资源（如 "posts"）和 id（如 "123"），你可以使用 <code>/(\w+)\/(\d+)/</code>。</p>
<pre><code class="language-javascript">const path = 'posts/123';
const pattern = /(\w+)\/(\d+)/;

const match = path.match(pattern);
console.log(match);
</code></pre>
<p>输出为：</p>
<pre><code class="language-bash">['posts/123', 'posts', '123', index: 0, input: 'posts/123', groups: undefined]
</code></pre>
<p>在这里，<code>'posts'</code> 和 <code>'123'</code> 分别由两个捕获组 <code>(\w+)</code> 和 <code>(\d+)</code> 捕获。</p>
<p><strong>命名捕获组</strong>允许你为捕获组指定名称，这样在后续的代码中引用它们会更容易。</p>
<p>命名捕获组的语法是 <code>(?&lt;name&gt;rule)</code>，其中：</p>
<ul>
<li><code>()</code> 表示一个捕获组。</li>
<li><code>?&lt;name&gt;</code> 指定捕获组的名称。</li>
<li><code>rule</code> 是模式中的一个规则。</li>
</ul>
<p>例如，假设我们想要使用命名捕获组从路径 "posts/123" 中捕获资源（如 "posts"）和 id（如 "123"）。</p>
<pre><code class="language-javascript">const path = 'posts/123';
const pattern = /(?&lt;resource&gt;\w+)\/(?&lt;id&gt;\d+)/;

const match = path.match(pattern);
console.log(match);
</code></pre>
<p>输出为：</p>
<pre><code class="language-javascript">[
  'posts/123',
  'posts',
  '123',
  index: 0,
  input: 'posts/123',
  groups: [Object: null prototype] { resource: 'posts', id: '10' }
]
</code></pre>
<p>在这里，<code>resource</code> 和 <code>id</code> 是分配给捕获组的名称。我们可以使用 <code>match.groups</code> 来访问它们。</p>
<p><strong>另一个例子</strong>: 假设我们有一个类似于"posts/2022/02/18"的路径，我们想要使用命名捕获组来捕获资源（如 "posts"）、年份（如 "2022"）、月份（如 "02"）和日期（如 "18"）。</p>
<p>该例子对应的正则表达式的模式为：</p>
<pre><code class="language-javascript">const path = 'posts/2024/02/22';
const pattern =
  /(?&lt;resource&gt;\w+)\/(?&lt;year&gt;\d{4})\/(?&lt;month&gt;\d{2})\/(?&lt;day&gt;\d{2})/;

const match = path.match(pattern);
console.log(match.groups);
</code></pre>
<p>输出为：</p>
<pre><code class="language-bash">{resource: 'posts', year: '2024', month: '02', day: '22'}
</code></pre>
<p>在这里，路径的每个部分都使用命名捕获组进行捕获，这样就可以轻松地通过它们各自的名称访问它们。</p>
<h3 id="">非捕获组</h3>
<p>在正则表达式中，非捕获组用于将模式的部分组合在一起，以便应用量词或交替，而不捕获匹配的子字符串。</p>
<p>要创建一个非捕获组，你需要在括号的开始处添加 <code>?:</code>。因此，<code>/(?:\d)+/</code> 是前一个示例的非捕获版本。<code>?:</code> 告诉正则表达式引擎不要捕获匹配的子字符串。</p>
<p>让我们通过一个例子来看捕获组和非捕获组之间的区别：</p>
<pre><code class="language-javascript">// capturing group
const regexWithCapture = /(\d{2})\/(\d{2})\/(\d{4})/;
const matchWithCapture = regexWithCapture.exec('02/26/2024');

console.log(matchWithCapture); // ["02/26/2024", "02", "26", "2024"]
</code></pre>
<pre><code class="language-javascript">// non-capturing group
const regexWithoutCapture = /(?:\d{2})\/(?:\d{2})\/(?:\d{4})/;
const matchWithoutCapture = regexWithoutCapture.exec('02/26/2024');

console.log(matchWithoutCapture); // ["02/26/2024"]
</code></pre>
<p>总结来说，非捕获组 <code>(?:pattern)</code> 在匹配模式方面的行为与常规捕获组 <code>()</code> 相同，但它们不会将匹配的文本存储在内存中以供以后检索。这使得它们在你不需要提取匹配文本的特定部分时非常有用。</p>
<h3 id="">后向引用</h3>
<p>后向引用允许你在正则表达式中引用先前捕获的组，将它们视为存储匹配模式的变量。</p>
<p>在JavaScript中，后向引用的语法是 <code>\N</code>，其中 <code>N</code> 是表示捕获组编号的整数。</p>
<p>例如，考虑一个包含重复单词 "Lion" 的字符串，我们希望删除重复的单词以得到 <code>'Lion is the King'</code>：</p>
<pre><code class="language-javascript">const s = 'Lion Lion is the King';
</code></pre>
<ul>
<li>首先，我们使用 <code>\w+\s+</code> 匹配一个单词。</li>
<li>然后，我们创建一个捕获组来捕获这个单词，使用 <code>(\w+)\s+</code>。</li>
<li>接下来，我们使用反向引用 (<code>\1</code>) 来引用第一个捕获组。</li>
<li>最后，我们使用 <code>String.replace()</code> 将整个匹配替换为第一个捕获组。</li>
</ul>
<pre><code class="language-javascript">const pattern = /(\w+)\s+\1/;
const result = s.replace(pattern, '$1');
console.log(result); // 输出为：'Lion is the King'
</code></pre>
<h3 id="regex-alternation">正则表达式选择符号</h3>
<p>正则表达式的选择符号是一种允许你在单个正则表达式中匹配不同的模式的功能。它的工作方式类似于逻辑运算符<code>OR</code>。正则表达式使用竖线符号 <code>|</code> 表示选择符号，你可以使用它来匹配 A 或 B。</p>
<pre><code>A | B // 这意味着你可以匹配模式A或模式B
</code></pre>
<p>现在，让我们探讨一些正则表达式选择符号的实际应用：</p>
<p><strong>匹配格式为hh:mm的时间字符串</strong>：假设我们想要匹配格式为 hh:mm 的时间字符串，其中 hh 表示小时，mm 表示分钟。一个基本的正则表达式来匹配这种格式就是 <code>/\d{2}:\d{2}/</code>。</p>
<p>然而，这个基本模式匹配了无效的时间，比如 "99:99"。为了确保我们匹配有效的时间（小时区间从00到23，分钟区间从00到59），我们需要使用选择符号来完善我们的正则表达式。</p>
<p>为了匹配有效的小时（00到23），我们可以使用以下模式：</p>
<ul>
<li><code>[01]\d</code> 匹配00到19的数字。</li>
<li><code>2[0-3]</code> 匹配20到23的数字。</li>
</ul>
<p>因此，小时的模式变为 <code>[01]\d|2[0-3]</code>。</p>
<p>我们可以使用模式 <code>[0-5]\d</code> 来匹配有效的分钟数(00 to 59)。</p>
<p>现在，我们可以使用选择符号将小时和分钟模式结合起来，得到最终的正则表达式模式：</p>
<p><code>/([01]\d|2[0-3]):[0-5]\d/g</code></p>
<p>在这个模式中:</p>
<ul>
<li><code>([01]\d|2[0-3])</code> 匹配有效的小时数。</li>
<li><code>:</code> 匹配冒号。</li>
<li><code>[0-5]\d</code> 匹配有效的分钟数。</li>
</ul>
<p>该正则表达式模式确保我们只匹配 <code>hh:mm</code> 格式的有效时间字符串。例如：</p>
<pre><code class="language-javascript">const timeString = '07:23 33:71 21:17 25:81';
const pattern = /([01]\d|2[0-3]):[0-5]\d/g;
const matches = timeString.match(pattern);

console.log(matches);
</code></pre>
<p><strong>期望输出</strong>:</p>
<pre><code>['07:23', '21:17']
</code></pre>
<h2 id="lookahead-and-lookbehind-in-regex">正则表达式中的前瞻断言和后顾断言</h2>
<h3 id="lookahead-">前瞻断言</h3>
<p>正则表达式中的前瞻允许仅当某个模式（X）后面紧跟着另一个特定模式（Y）时进行匹配。语法是 <code>X(?=Y)</code>，其中：</p>
<ul>
<li><strong>X</strong> 是你要匹配的模式。</li>
<li><strong>(?=Y)</strong> 是前瞻断言，指示 <code>X</code> 应该紧跟着 <code>Y</code>。</li>
</ul>
<p><strong>例如：</strong> 假设我们有一个描述各种距离的字符串，我们想要识别字符串中包含的单位为 "miles" 而不是 "kilometers" 的数字。我们可以在正则表达式模式中使用前瞻断言：</p>
<pre><code class="language-javascript">const dist = "He ran 5 miles, but not 10 kilometers.";

const regex = /\d+(?=\s*miles)/g;

console.log(dist.match(regex)); // 输出为：["5"]
</code></pre>
<p><strong>多重前瞻断言</strong>: 在正则表达式中可以使用语法 <code>X(?=Y)(?=Z)</code> 来使用多个前瞻断言，这能够让我们对匹配施加多个条件。</p>
<p><strong>例如:</strong> 假设我们想要匹配同时包含 "foo" 和 "bar" 的字符串，但它们可以以任意的顺序排列：</p>
<pre><code class="language-javascript">const regex = /(?=.*foo)(?=.*bar)/;

console.log(regex.test("foobar")); // true
console.log(regex.test("barfoo")); // true
console.log(regex.test("foo"));    // false
console.log(regex.test("bar"));    // false
</code></pre>
<h3 id="negative-lookaheads-">否定前瞻断言</h3>
<p>为了否定一个前瞻断言，可以使用否定前瞻断言，其语法为 <code>(?!Y)</code>。在这种情况下，正则表达式引擎只有在 X 后面不跟着 Y 的情况下才会匹配 X。</p>
<p><strong>例如</strong>： 假设我们想要匹配数字，但不希望它们后面跟着 "miles"：</p>
<pre><code class="language-javascript">const text = "He ran 5 miles, but not 10 kilometers.";

const regex = /\d+(?!\s*miles)/g;

console.log(text.match(regex)); // 输出为：["10"]
</code></pre>
<p><code>(?!\s*miles)</code> 是一个否定前瞻断言，它确保数字后面不是零个或多个空格加上单词 "miles"。</p>
<h3 id="lookbehind-">后顾断言</h3>
<p>后顾断言提供了一种根据其前面的内容来匹配模式的方式，如果某个特定元素在其前面，则匹配该元素。</p>
<p><strong>例如</strong>：假设我们有一个包含价格的字符串，并且我们想要匹配在货币符号 "$" 前面的数字，但不匹配在 "€" 前面的数字。我们可以在正则表达式模式中使用后顾断言。</p>
<pre><code class="language-javascript">const priceString = "The price is $100, but €200.";

const regex = /(?&lt;=\$)\d+/g;

console.log(priceString.match(regex)); // 输出为：["100"]
</code></pre>
<p><strong>说明</strong>：如果在当前位置之前有一个文字字符串 "$"，<code>(?&lt;=\$)</code> 就会匹配该元素。反斜杠 <code>\</code> 用于转义特殊字符 "$"，将其视为字面字符。</p>
<h3 id="negative-lookbehind-">否定后顾断言</h3>
<p>否定后顾断言允许你仅在模式之前不是特定模式的情况下匹配该模式。这对于根据前面的内容排除某些模式的匹配非常有用。</p>
<p>示例：假设我们有一个包含不同货币的各种价格的字符串，并且我们想要匹配不是以货币符号 "$" 开头的数字。我们可以在正则表达式模式中使用否定后顾断言：</p>
<pre><code class="language-javascript">const priceString = "The price is $50, but not €100.";

const regex = /(?&lt;!\$)\b\d+\b/g;

console.log(priceString.match(regex)); // 输出为： ["100"]
</code></pre>
<p><strong>说明</strong>： <code>(?&lt;!\$)</code> 是否定后顾断言语法，它只在当前位置之前不是字面字符串"$"时匹配后面的模式。</p>
<h2 id="practical-examples-and-use-cases-of-regexpractical-examples-and-use-cases-of-regex">正则表达式的实际应用示例</h2>
<p>现在，让我们探索一些在JavaScript应用程序中使用正则表达式来解决常见问题和执行文本操作任务的实际示例。</p>
<h3 id="password-strength-checking-">密码强度检验函数</h3>
<p>你可以使用正则表达式来强制执行密码强度要求，例如最小长度和特殊字符的存在。</p>
<pre><code class="language-javascript">function checkPasswordStrength(password) {
    let pattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&amp;*]).{8,}$/;
    return pattern.test(password);
}

console.log(checkPasswordStrength("Passw0rd!"));    // 输出为：true
console.log(checkPasswordStrength("weakpassword")); // 输出为：false
</code></pre>
<p>这里的正则表达式确保密码包含至少1个数字、1个小写字母、1个大写字母、1个特殊字符，并且密码长度至少为8个字符。</p>
<p>这个模式进行了如下操作:</p>
<ul>
<li><code>(?=.*\d)</code>: 要求至少一个数字。</li>
<li><code>(?=.*[a-z])</code>: 要求至少一个小写字母。</li>
<li><code>(?=.*[A-Z])</code>: 要求至少一个大写字母。</li>
<li><code>(?=.*[!@#$%^&amp;*])</code>: 要求至少一个特殊符号。</li>
<li><code>.{8,}</code>: 要求密码长度至少为8个字符。</li>
</ul>
<h3 id="email-validation-function-">电子邮箱地址校验函数</h3>
<p>电子邮件验证对于确保网络应用程序中的数据完整性和安全性至关重要。通过使用正则表达式，我们可以轻松实现强大的电子邮件验证机制。</p>
<pre><code class="language-javascript">function validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

console.log(validateEmail("example@email.com")); // true
console.log(validateEmail("invalid-email"));      // false
</code></pre>
<p>这个模式进行了如下操作：</p>
<ul>
<li><code>^</code>: 断言字符串的起始位置。</li>
<li><code>[^\s@]+</code>: 匹配一个或多个非空白字符或'@'字符。</li>
<li><code>@</code>: 匹配'@'符号。</li>
<li><code>[^\s@]+</code>: 匹配一个或多个非空白字符或'@'字符。</li>
<li><code>\.</code>: 匹配'.'符号 (因为'.'在正则表达式中具有特殊意义，所以需要转义。)。</li>
<li><code>[^\s@]+</code>: 匹配一个或多个非空白字符或'@'字符。</li>
<li><code>$</code>: 断言字符串的结束位置。</li>
</ul>
<h3 id="phone-number-formatting-function-">电话号码格式化函数</h3>
<p>在涉及电话号码输入和显示的应用程序中，电话号码格式化增强了用户体验和可读性。</p>
<p>通过定义一个匹配电话号码组件的regex模式，我们可以使用 <code>replace()</code> 方法轻松地将电话号码格式化为所需的模式。</p>
<pre><code class="language-javascript">function formatPhoneNumber(phoneNumber) {
    const phoneRegex = /(\d{3})(\d{3})(\d{4})/;
    return phoneNumber.replace(phoneRegex, "($1) $2-$3");
}

const formattedNumber = formatPhoneNumber("9876543210");
console.log(formattedNumber); // (987) 654-3210
</code></pre>
<p>这个函数接受一个电话号码字符串作为输入，并以标准的 <code>(XXX) XXX-XXXX</code> 格式返回。</p>
<p>在 <code>replace()</code> 方法中, <code>$1</code>, <code>$2</code>, 和 <code>$3</code> 表示以RegEx模式捕获的组，对应于电话号码中的三组数字。</p>
<h2 id="tips-and-best-practices-for-using-regular-expressions">使用正则表达式的技巧和最佳实践方式</h2>
<h4 id="1">1. 理解正则表达式语法</h4>
<p>了解正则表达式的语法和元字符，以便有效使用正则表达式。</p>
<h4 id="2">2. 测试正则表达式</h4>
<p>由于复杂的模式或特殊字符，正则表达式有时会表现出意外的行为。经常使用不同的输入字符串测试你的正则表达式，以确保它们在不同的场景中表现得像预期的那样。</p>
<h4 id="3">3. 优化性能</h4>
<p>考虑通过简化模式或尽可能使用更有效的替代方案来优化正则表达式的性能。</p>
<h4 id="4">4. 使用内置方法</h4>
<p>JavaScript提供了例如 <code>String.prototype.match()</code>, <code>String.prototype.replace()</code>, 和 <code>String.prototype.split()</code>等用于常见的字符串操作任务的内置方法。评估这些方法是否可以在不需要正则表达式的情况下完成任务。</p>
<h4 id="5">5. 为你的正则表达式添加注释</h4>
<p>使用 <code>(?#comment)</code> 语法为你的正则表达式添加注释来解释部分复杂的模式。例如:</p>
<pre><code class="language-javascript">const regex = /(\d{3})-(\d{3})-(\d{4})\s(?# Match a phone number in the format XXX-XXX-XXXX)/;
</code></pre>
<h4 id="6">6. 分解复杂模式</h4>
<p>如果你的正则表达式太过复杂而难以理解或维护，请考虑将其分解为更小、更易于管理的部分。使用变量来存储正则表达式模式的各个组件，并根据需要组合它们。</p>
<h4 id="7">7. 利用在线资源并坚持练习</h4>
<p>有许多在线资源和工具可以用来测试和学习正则表达式。<a href="https://regex101.com/">Regex101</a> 和 <a href="https://regexr.com/">RegExr</a> 等网站提供了测试和调试正则表达式的交互式平台。还可以利用在线教程和文档来学习 regex 概念。</p>
<p>MDN Web Docs 提供了关于正则表达式的有用指南<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions">39</a>。以下是 JavaScript 中正则表达式的快速入门指南：<a href="https://www.freecodecamp.org/news/a-quick-and-simple-guide-to-javascript-regular-expressions-48b46a68df29/">RegExp 教程</a>。</p>
<h2 id="conclusion">总结</h2>
<p>正则表达式是 JavaScript 中用于模式匹配和操作的通用工具。</p>
<p>通过理解正则表达式的方法、高级特性和对标志的使用，利用在线资源和调试工具，你可以有效地学习它们，并将它们应用于各种场景，从简单的模式匹配到复杂的文本处理任务。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JavaScript 中的 DOM 操作——面向初学者的全面指南 ]]>
                </title>
                <description>
                    <![CDATA[ 作为一种 Web 开发语言，JavaScript 赋予了开发者创建动态和交互式网页的能力，其中一个实现这种交互性的 JavaScript 关键特性是文档对象模型（DOM）操作。 DOM 操作允许开发者修改网页的结构、样式和内容。在本文中，我们将探讨 JavaScript 中 DOM 操作的基础知识，将复杂的概念分解成易于理解的代码片段。 什么是 DOM 文档对象模型（DOM）是一种用于 Web 文档的编程接口。它以对象树的形式表示文档的结构，其中每个对象对应于文档的一部分，例如元素、属性和文本。JavaScript 可以操作这个树状结构，允许开发者动态地改变网页的内容和外观。 如何访问 DOM 元素 为了操作 DOM，我们需要访问它的元素，这一步通常使用代表整个 HTML 文档的 document 对象来实现。让我们看一个简单的例子： // 使用一个元素的 ID 访问这个元素 const headerElement = document.getElementById('header'); // 使用类名访问元素 const paragraphs = document.getE ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/dom-manipulation-in-javascript/</link>
                <guid isPermaLink="false">65f10c90823801041102de9e</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DOM ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tsukistar ]]>
                </dc:creator>
                <pubDate>Wed, 13 Mar 2024 02:17:02 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/03/Beige-Aesthetic-Neutral-Thesis-Defense-Presentation-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/dom-manipulation-in-javascript/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">DOM Manipulation in JavaScript – A Comprehensive Guide for Beginners</a>
      </p><!--kg-card-begin: markdown--><p>作为一种 Web 开发语言，JavaScript 赋予了开发者创建动态和交互式网页的能力，其中一个实现这种交互性的 JavaScript 关键特性是文档对象模型（DOM）操作。</p>
<p>DOM 操作允许开发者修改网页的结构、样式和内容。在本文中，我们将探讨 JavaScript 中 DOM 操作的基础知识，将复杂的概念分解成易于理解的代码片段。</p>
<h2 id="dom">什么是 DOM</h2>
<p>文档对象模型（DOM）是一种用于 Web 文档的编程接口。它以对象树的形式表示文档的结构，其中每个对象对应于文档的一部分，例如元素、属性和文本。JavaScript 可以操作这个树状结构，允许开发者动态地改变网页的内容和外观。</p>
<h3 id="dom">如何访问 DOM 元素</h3>
<p>为了操作 DOM，我们需要访问它的元素，这一步通常使用代表整个 HTML 文档的 document 对象来实现。让我们看一个简单的例子：</p>
<pre><code class="language-javascript">// 使用一个元素的 ID 访问这个元素
const headerElement = document.getElementById('header');

// 使用类名访问元素
const paragraphs = document.getElementsByClassName('paragraph');

// 使用标签名访问元素
const images = document.getElementsByTagName('img');
</code></pre>
<p>在上面的代码片段中，我们使用 <code>getElementById</code> , <code>getElementsByClassName</code> , 和 <code>getElementsByTagName</code> 三个函数来检索特定的元素。这三个函数的返回值可以存储在变量中，以供开发者进行进一步的操作。</p>
<h3 id="">如何修改元素内容</h3>
<p>一旦我们已经访问了一个元素，我们就可以使用这个元素的<code>innerHTML</code>属性来修改它的内容：</p>
<pre><code class="language-javascript">// 修改一个元素的内容
headerElement.innerHTML = 'New Header Text';
</code></pre>
<p>在上面的例子中，我们将 <code>headerElement</code> 的内容更改为 <code>New Header Text</code> 。这是一种简单而行之有效的方法，可以更新元素中的文本内容。</p>
<h2 id="">事件和事件处理</h2>
<p>事件是在浏览器中发生的操作或事务，例如用户点击按钮或调整窗口大小。JavaScript 允许我们处理这些事件并执行相应的代码作为对事件的响应。事件处理是创建交互式网页的关键方面。</p>
<h3 id="">如何添加事件监听器</h3>
<p>为了响应事件，我们可以使用事件监听器，它们是用于“监听”特定元素上的特定事件的函数。让我们以一个按钮点击事件为例：</p>
<pre><code class="language-javascript">// 访问一个按钮元素
const myButton = document.getElementById('myButton');

// 添加点击事件监听器
myButton.addEventListener('click', function() {
    alert('Button Clicked!');
});
</code></pre>
<p>在这个例子中，当 ID 为 <code>myButton</code> 的按钮被点击时，浏览器将弹出一个内容为 <code>Button Clicked!</code> 的提示框。事件监听器提供了一种根据用户交互执行自定义代码的方式。</p>
<h2 id="">如何修改样式</h2>
<p>DOM 操作还包括修改元素的样式，使我们能够创建视觉上吸引人的、动态的网页。</p>
<h3 id="">如何动态地修改样式</h3>
<p>我们可以使用元素的 <code>style</code> 属性来改变其外观。让我们以点击按钮时改变段落颜色的例子来说明：</p>
<pre><code class="language-javascript">// 访问一个段落元素
const myParagraph = document.getElementById('myParagraph');

// 访问一个按钮元素
const colorButton = document.getElementById('colorButton');

// 为按钮添加一个点击事件
colorButton.addEventListener('click', function() {
    // 修改段落的颜色样式
    myParagraph.style.color = 'blue';
});
</code></pre>
<p>在这个例子中，当 ID 为 <code>colorButton</code> 的按钮被点击时，ID 为 <code>myParagraph</code> 的段落的文本颜色将被更改为蓝色。</p>
<h2 id="">如何创建和修改元素</h2>
<p>除了修改现有元素之外，JavaScript 还允许我们创建新元素并将它们添加到 DOM 中。</p>
<h3 id="">如何创建新的元素</h3>
<p>使用 <code>createElement</code> 方法来创建一个新的 HTML 元素。让我们创建一个新的段落元素，并将其追加（添加）到文档的 body 中：</p>
<pre><code class="language-javascript">// 新建一个段落元素
const newParagraph = document.createElement('p');

// 为新段落设置文本内容
newParagraph.textContent = 'This is a new paragraph.';

// 将新创建的段落追加到文档的 body 中
document.body.appendChild(newParagraph);
</code></pre>
<p>在这个例子中，我们创建了一个新的 <code>p</code>（段落）元素，设置了它的文本内容，然后将其追加到文档的 body 中。</p>
<h3 id="">如何修改属性</h3>
<p>我们还可以修改现有元素的属性。让我们思考如何动态更改图像的来源：</p>
<pre><code class="language-javascript">// 访问一个图像元素
const myImage = document.getElementById('myImage');

// 修改图像的来源属性
myImage.src = 'new-image.jpg';
</code></pre>
<p>这个例子中, 我们访问了 ID 为 <code>myImage</code> 的图像元素，并将其 <code>src</code> 属性更改为 <code>new-image.jpg</code>，动态更新显示的图像。</p>
<h3 id="">如何更新表单的输入值</h3>
<p>让我们思考这样一个情景：你希望根据用户的交互来更新文本输入框的值：</p>
<pre><code class="language-javascript">// 访问一个文本输入框元素
const myInput = document.getElementById('myInput');

// 添加一个输入事件监听器
myInput.addEventListener('input', function() {
    // 使用输入值更新一个段落的内容
    document.getElementById('inputValue').textContent = myInput.value;
});
</code></pre>
<p>在这个例子中，当用户在 ID 为 <code>myInput</code> 的文本输入框中输入内容时，ID 为 <code>inputValue</code> 的段落将根据输入值动态更新段落内容。</p>
<h3 id="">如何切换可见性</h3>
<p>你可以通过使用 <code>display</code> 样式属性来切换元素的可见性。让我们创建一个按钮用于切换段落的可见性：</p>
<pre><code class="language-javascript">// 访问一个按钮元素
const toggleButton = document.getElementById('toggleButton');

// 访问一个段落元素
const toggleParagraph = document.getElementById('toggleParagraph');

// 添加一个点击事件监听器
toggleButton.addEventListener('click', function() {
    // 切换段落的可见性
    toggleParagraph.style.display = toggleParagraph.style.display === 'none' ? 'block' : 'none';
});
</code></pre>
<p>在这里，ID 为 <code>toggleParagraph</code> 的段落最初是可见的，单击 ID 为 <code>toggleButton</code> 的按钮会切换其可见性。</p>
<h2 id="dom">DOM 操作中的常见陷阱</h2>
<p>虽然 DOM 操作是创建动态网页的强大工具，但初学者经常会遇到一些常见陷阱，这些陷阱可能导致意外行为或错误。让我们探讨一些这样的陷阱，并提供如何避免它们的建议。</p>
<h3 id="dom">在 DOM 渲染完成前操作它们</h3>
<p>有时我们可能会在 DOM 尚未完全加载之前尝试操作它，这可能导致 JavaScript 尝试访问尚未呈现的元素。为了避免这种情况，最重要的是在执行任何 JavaScript 代码之前都需要等待 DOM 完全加载：</p>
<pre><code class="language-javascript">document.addEventListener('DOMContentLoaded', function() {
    // DOM 操作代码放在这里
});
</code></pre>
<p>通过将执行 DOM 操作的代码包裹在 <code>DOMContentLoaded</code> 事件监听器内部，你就可以确保它仅在 DOM 准备就绪时运行。</p>
<h3 id="">没有确认一个元素是否存在</h3>
<p>在尝试使用诸如 <code>getElementById</code> 这样的方法访问元素时，我们可能会假设元素存在并继续进行操作。但是，如果元素不存在于页面上，则可能会导致错误。</p>
<p>在操作元素之前，我们始终应该先检查该元素是否存在：</p>
<pre><code class="language-javascript">const myElement = document.getElementById('myElement');

if (myElement) {
    // 在这里对这个元素进行操作
} else {
    console.error('Element not found!');
}
</code></pre>
<p>这个简单的检查可以防止在操作不确定是否存在的元素时出现的错误。</p>
<h3 id="">忘记阻止默认行为</h3>
<p>在处理事件时，忘记阻止默认行为可能会导致意外的页面行为。例如，如果在没有阻止默认行为的情况下提交表单，页面可能会重新加载，导致数据丢失：</p>
<pre><code class="language-javascript">const myForm = document.getElementById('myForm');

myForm.addEventListener('submit', function(event) {
    // 阻止默认的表单提交
    event.preventDefault();

    // 你的表单处理代码放在这里
});
</code></pre>
<p>通过调用<code>event.preventDefault()</code>，你可以阻止与表单提交事件关联的默认行为，从而完全控制事件的处理方式。</p>
<h3 id="dom">执行效率低下的 DOM 查询</h3>
<p>在循环中执行效率低下的 DOM 查询会降低性能。每次查询都涉及遍历 DOM，不必要的查询会减慢网页的加载速度。</p>
<p>与其重复查询 DOM，不如缓存元素的引用：</p>
<pre><code class="language-javascript">// 在循环中执行的效率低下的查询
for (let i = 0; i &lt; 10; i++) {
    const myElement = document.getElementById('myElement');
    // 操作 `myElement`
}

// 循环之外的高效的查询
const myElement = document.getElementById('myElement');
for (let i = 0; i &lt; 10; i++) {
    // 操作 `myElement`
}
</code></pre>
<p>通过一次查询 DOM 并重复使用该 DOM 的引用，可以优化你的代码。</p>
<h3 id="">无法处理跨浏览器的兼容性</h3>
<p>不同的浏览器可能会略有不同地解释 JavaScript 和 DOM 操作。如果没有考虑跨浏览器兼容性，可能会导致代码的行为不一致。</p>
<p>使用特性检测，并考虑使用像 jQuery 或现代框架这样的库来处理跨浏览器的不一致性：</p>
<pre><code class="language-javascript">// 使用特性检测检查浏览器对 `addEventListener` 方法的支持：
if (document.addEventListener) {
    // 使用 addEventListener
} else {
    // 回退到另一种受支持的方法
}
</code></pre>
<p>通过在使用一些功能特性之前检查它们，你可以确保您的代码在各种浏览器上正常工作。</p>
<h2 id="dom">如何在框架中使用 DOM 操作</h2>
<p>虽然 JavaScript 允许直接操作 DOM，但现代 Web 开发通常涉及使用诸如 React 或 Vue.js 等框架。这些框架提供了一种更加结构化的方式来构建和管理用户界面。</p>
<h3 id="react">React 示例</h3>
<pre><code class="language-javascript">// React 组件渲染一个按钮并处理其点击事件
class MyButton extends React.Component {
    handleClick() {
        alert('React Button Clicked!');
    }

    render() {
        return (
            &lt;button onClick={() =&gt; this.handleClick()}&gt;Click me&lt;/button&gt;
        );
    }
}

// 将 React 组件渲染到 DOM 中
ReactDOM.render(&lt;MyButton /&gt;, document.getElementById('reactRoot'));
</code></pre>
<p>这个 React 示例创建了一个组件来处理按钮点击事件，它演示了一种更加声明式的 UI 开发方法。</p>
<h3 id="vuejs">Vue.js 示例</h3>
<pre><code class="language-javascript">// 具有一个数据属性和一个方法的 Vue.js 实例
new Vue({
    el: '#vueRoot',
    data: {
        message: 'Vue.js Message'
    },
    methods: {
        showMessage: function () {
            alert(this.message);
        }
    }
});
</code></pre>
<p>这个例子创建了一个 Vue.js 实例来管理数据和方法，展示了 Vue.js 的响应性和基于组件的结构。</p>
<h2 id="">总结</h2>
<p>在这个全面的指南中，我们深入探讨了 JavaScript 中的 DOM 操作。从访问元素到处理事件，从修改样式到创建新元素，我们以简单直接的方式涵盖了 DOM 操作的基本概念。</p>
<p>请记住，DOM 操作是创建动态和交互式网页的核心。通过掌握这些基本技术，你将能够构建引人入胜且用户友好的 Web 应用程序。随着你继续进行你的 JavaScript 学习之旅，更多的练习与实践将加深你对这些概念的理解，为你成为一名成功的 Web 开发者铺平道路。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
