<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Alicia Lan</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://believed-breadfruit.top/</id>
  <link href="https://believed-breadfruit.top/" rel="alternate"/>
  <link href="https://believed-breadfruit.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Alicia Lan</rights>
  <subtitle>Professional Dumbass · System 2 Thinker · Inner Stoic</subtitle>
  <title>Atypical</title>
  <updated>2026-06-04T17:03:35.324Z</updated>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="AI" scheme="https://believed-breadfruit.top/tags/AI/"/>
    <category term="工程实践" scheme="https://believed-breadfruit.top/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<p>最近看到一篇综述兼评论风格的文章（<a href="https://mp.weixin.qq.com/s/bk3-aSyTGsx_hEo-vt5KfQ">原文链接</a>）。起因是 Anthropic 新发布了 Dynamic Workflows，作者借此回顾了从 2022 年到 2025&#x2F;2026 年这几年间 Agent 技术的研究脉络。</p><p>文章的核心观点可以总结为一句话：Agent 能力的提升，并不只是靠模型本身更强，而是把那些不适合塞进上下文窗口的东西，挪到了 runtime、代码、技能库和工作流脚本里。</p><p>这篇文章让我对工程实践产生了三点新的思考。</p><p>一、工程的高可用，来自确定性</p><p>什么是确定性？相同的输入，系统总能产生相同的输出。</p><p>在计算机工程里，确定性有：计算确定（数学运算、排序、解析）和逻辑确定（if&#x2F;else、布尔判断）。</p><p>所以在 Agent 技术里，代码做了确定的事情（计算、执行、可重复运行），大模型则做”模糊”的事（制定计划、解释概念、拆解意图）；人做”决策”的事（定目标、做取舍）。</p><p>二、厉害的技术，未必新颖</p><p>还有另一个感悟，是关于 Agent 这一路的发展历程。</p><p>提示词工程、思维链（CoT）、ReAct 推理、Function Calling、工作流……回头看，这些概念在底层原理上其实都谈不上多么”高明”。</p><p>这让我意识到一个长期存在的误区：我们总觉得，真正厉害的东西一定得足够新颖。</p><p>所以，当一项技术的底层实现并不新颖时，很多人会下意识地说：”这不就跟那个谁谁差不多吗？”——这种评价背后的潜台词是：一项技术要够牛，就必须够新。</p><p>但现实恰恰相反。技术演进路上的每一步，其提出的观念往往本身都算不上惊艳。它们只是”恰好有用”，并且被足够多的人验证过、确实能跑出好结果而已。</p><p>三、为什么技术总是”沿着第一条路”走下去</p><p>再延伸一点。假设技术演进是一条 A1 → A2 → A3 → A4 不断向前的路径。</p><p>但其实在 A1 这个节点上，原本可能同时存在三条岔路：A1、B1、C1。</p><p>只是因为 A1 最先出现、最先被大多数人验证，又恰好反响热烈，于是大家会迅速围绕 A1 去做周边迭代、做配套基建。结果就是——整个技术生态都顺着 A1 这条路一路长了下去，而不是从 B1 或 C1 出发。</p><p>所以技术的演进方向，很多时候并不是因为 A1 一定最优，而是因为它”先到”，先吃到了验证和生态的红利。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/06/05/2026-06-05-%E5%AF%B9%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5%E6%9C%89%E4%BA%86%E6%96%B0%E8%AE%A4%E8%AF%86/</id>
    <link href="https://believed-breadfruit.top/2026/06/05/2026-06-05-%E5%AF%B9%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5%E6%9C%89%E4%BA%86%E6%96%B0%E8%AE%A4%E8%AF%86/"/>
    <published>2026-06-04T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>最近看到一篇综述兼评论风格的文章（<a href="https://mp.weixin.qq.com/s/bk3-aSyTGsx_hEo-vt5KfQ">原文链接</a>）。起因是 Anthropic 新发布了 Dynamic Workflows，作者借此回顾了从 2022]]>
    </summary>
    <title>对工程实践有了新认识</title>
    <updated>2026-06-04T17:03:35.324Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="AI" scheme="https://believed-breadfruit.top/tags/AI/"/>
    <category term="技术洞察" scheme="https://believed-breadfruit.top/tags/%E6%8A%80%E6%9C%AF%E6%B4%9E%E5%AF%9F/"/>
    <content>
      <![CDATA[<p><a href="https://aiweekly.co/learning-ai/artificial-intelligence/ai-weekly-index">AI Weekly Index</a>，它用一份报表呈现了 2015–2026 年的 AI 话语趋势。</p><p>其中有一个观察很有意思：这两年，扩散模型、RAG、Transformer 这些概念几乎从讨论中消失了。但它们消失，并不是因为失败，而是因为它们已经沉淀成了一种”不可见的基础设施”。</p><p>这句话可以从三个层面来理解。</p><p>一、为什么”消失”不等于”失败”</p><p>我们之所以会反复谈论一项新技术，往往是因为它还停留在”需要被解释、需要被证明”的阶段。</p><p>Transformer 在 2022 年被高频提及（0.44 次&#x2F;期），正是因为人们还在争论它的潜力；而到了 2026 年，提及次数归零，恰恰是因为它已经成为所有主流大模型的底层架构。它不再需要被讨论，因为它已经被默认了。</p><p>这就像今天没有人会特意说”我用互联网来发邮件”：互联网早已融入一切，反而不再作为一个独立话题存在。扩散模型和 RAG 走的是同一条路：从”前沿概念”变成”标准工具”，于是话语自然向更上层迁移。</p><p>二、”不可见的基础设施”意味着什么</p><p>报表里的数据把这个模式呈现得很清楚：机器学习在 2021 年达到峰值（3.72 次&#x2F;期），到 2026 年降为 0；深度学习晚两年，但走出了同样的曲线；扩散模型只”存活”了两年（2022–2023）便淡出了讨论。</p><p>但它们并没有退出舞台，恰恰相反，它们渗透进了每一个产品、每一篇论文、每一次对话的底层。</p><p>当一项技术不再需要被特意命名，就说明它已经从”创新”变成了”常识”，从”需要被选择”变成了”无需选择”。这正是技术走向成熟的最强信号。</p><p>三、”消失得最快 &#x3D; 最成功”的深层逻辑</p><p>这句话真正精妙的地方，在于它翻转了我们判断”成功”的标准。</p><p>我们通常用热度、讨论量、搜索指数来衡量一项技术的影响力。但这份数据揭示了相反的规律：真正改变行业的技术，往往是最快从话语中退场的那一批。原因很简单：当所有人都已经在用它，就没人再需要谈论它了。</p><p>反过来看，那些长期停留在讨论阶段的概念（比如”伦理”在 2019–2022 年持续高热），往往意味着行业还没有形成共识、找到解法，问题仍然悬而未决。</p><p>到这里，这让我想起 skills 刚出来的时候，很多文章把它和 MCP 拿来对比，断言”MCP 被推翻了”。</p><p>我当时专门调研过，发现 MCP 不仅没有退场，反而沉到了更底层，渗透进了 agent 技术的基础之中。</p><p>skills 和 MCP 本就是两个不同维度的东西，很多人把它们放在一起比较，其实是不了解二者的本质：skills 是一套稳定的工作流，MCP 是一种通信协议。</p><p>它们唯一可比的角度，是对上下文的影响：skills 按需加载，对上下文友好；MCP 偏向全量加载，更容易让上下文膨胀。除此之外，二者并不在同一条赛道上。</p><p>每当一个新概念出现，人们总是习惯性地把它和最近一个”看起来相关”的旧概念做替代式对比，而不是去理解它们各自在技术栈中所处的位置。</p><p>身处技术栈内部的人，能感知到各层之间的相对位置和依赖关系；而身处外部的人，只能看到话语层的表面，哪个词上了热搜，哪个词消失了，就据此判断谁赢谁输。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/06/03/2026-06-03-%E6%8A%80%E6%9C%AF%E6%B6%88%E5%A4%B1%E7%9A%84%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/</id>
    <link href="https://believed-breadfruit.top/2026/06/03/2026-06-03-%E6%8A%80%E6%9C%AF%E6%B6%88%E5%A4%B1%E7%9A%84%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD/"/>
    <published>2026-06-02T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p><a href="https://aiweekly.co/learning-ai/artificial-intelligence/ai-weekly-index">AI Weekly Index</a>，它用一份报表呈现了 2015–2026 年的 AI 话语趋势。</p>]]>
    </summary>
    <title>这两年扩散模型、RAG、Transformer 这些概念几乎从讨论中消失了</title>
    <updated>2026-06-04T16:24:08.223Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="技术" scheme="https://believed-breadfruit.top/tags/%E6%8A%80%E6%9C%AF/"/>
    <category term="Claude Code" scheme="https://believed-breadfruit.top/tags/Claude-Code/"/>
    <content>
      <![CDATA[<p>Claude Code 的过程观测有几个层次，从轻量到重型都有，下面分层介绍。</p><hr><h2 id="一、交互模式下的即时查看"><a href="#一、交互模式下的即时查看" class="headerlink" title="一、交互模式下的即时查看"></a>一、交互模式下的即时查看</h2><p><strong><code>--debug</code> 标志</strong> — 最直接的方式</p><p>启动时加上 <code>--debug</code> 可以看到 API 调用、工具调用、执行耗时等详细信息，输出到 stderr：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">claude --debug                          <span class="comment"># 完整 debug 输出</span></span><br><span class="line">claude --debug <span class="string">&quot;api,hooks&quot;</span>              <span class="comment"># 只看特定类别</span></span><br><span class="line">claude --debug-file /tmp/claude-debug.log  <span class="comment"># 写入文件，保持终端整洁</span></span><br></pre></td></tr></table></figure><p><strong><code>--verbose</code></strong> 则是更轻量的开关，可在配置里持久化，或启动时传入。</p><hr><h2 id="二、读取本地-JSONL-日志（离线回溯）"><a href="#二、读取本地-JSONL-日志（离线回溯）" class="headerlink" title="二、读取本地 JSONL 日志（离线回溯）"></a>二、读取本地 JSONL 日志（离线回溯）</h2><p>Claude Code 的 session transcript 以 JSONL 格式存储在 <code>~/.claude/projects/</code> 目录下，里面记录了每次 tool call、thinking step、token 用量等内容。原始文件可读性较差（是 escaped JSON），但可以直接 <code>cat</code> 或用 <code>jq</code> 解析。</p><p>社区工具 <strong><a href="https://claude-dev.tools/">claude-devtools</a></strong> 可以解析这些文件，它会重建完整的 session transcript —— 每个 tool call 展开、thinking step 可见、token 用量按 turn 分解，subagent 执行以树状结构渲染。</p><hr><h2 id="三、通过-Hooks-实时拦截（适合自定义观测）"><a href="#三、通过-Hooks-实时拦截（适合自定义观测）" class="headerlink" title="三、通过 Hooks 实时拦截（适合自定义观测）"></a>三、通过 Hooks 实时拦截（适合自定义观测）</h2><p>Hooks 是 Claude Code 的扩展点，可以在 <code>PreToolUse</code>、<code>PostToolUse</code> 等生命周期事件上挂载脚本。这是目前<strong>最灵活</strong>的方案——你可以在 hook 脚本里把 tool call 的输入&#x2F;输出写到自己的日志系统，或触发告警。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// .claude/settings.json</span><br><span class="line">&#123;</span><br><span class="line">  &quot;hooks&quot;: &#123;</span><br><span class="line">    &quot;PostToolUse&quot;: [&#123;</span><br><span class="line">      &quot;matcher&quot;: &quot;*&quot;,</span><br><span class="line">      &quot;hooks&quot;: [&#123;</span><br><span class="line">        &quot;type&quot;: &quot;command&quot;,</span><br><span class="line">        &quot;command&quot;: &quot;python3 /path/to/logger.py&quot;</span><br><span class="line">      &#125;]</span><br><span class="line">    &#125;]</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Hook 脚本通过 stdin 收到 JSON 事件，包含 tool 名称、参数、结果等。</p><hr><h2 id="四、OpenTelemetry-导出（生产-团队级别）"><a href="#四、OpenTelemetry-导出（生产-团队级别）" class="headerlink" title="四、OpenTelemetry 导出（生产&#x2F;团队级别）"></a>四、OpenTelemetry 导出（生产&#x2F;团队级别）</h2><p>Claude Code CLI 内置了 OpenTelemetry instrumentation：它会在每次 model request 和 tool execution 上记录 span，发出 token&#x2F;cost 计数的 metrics，并为 prompt 和 tool result 发出结构化 log event。</p><p>开启方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> CLAUDE_CODE_ENABLE_TELEMETRY=1</span><br><span class="line"><span class="built_in">export</span> CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1   <span class="comment"># 开启 Traces（beta）</span></span><br><span class="line"><span class="built_in">export</span> OTEL_TRACES_EXPORTER=otlp</span><br><span class="line"><span class="built_in">export</span> OTEL_LOGS_EXPORTER=otlp</span><br><span class="line"><span class="built_in">export</span> OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318</span><br></pre></td></tr></table></figure><p>支持导出到任何接受 OTLP 协议的后端，比如 Honeycomb、Datadog、Grafana、Langfuse，或自托管的 collector。</p><hr><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><table><thead><tr><th>需求</th><th>方案</th></tr></thead><tbody><tr><td>临时调试某次执行</td><td><code>claude --debug</code></td></tr><tr><td>事后回溯 session</td><td>读 <code>~/.claude/projects/*.jsonl</code> 或用 claude-devtools</td></tr><tr><td>自定义日志&#x2F;告警</td><td>Hooks + 脚本</td></tr><tr><td>团队监控 &#x2F; 成本追踪</td><td>OpenTelemetry 导出</td></tr></tbody></table>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/06/01/2026-06-01-claude-code%E8%BF%87%E7%A8%8B%E8%A7%82%E6%B5%8B/</id>
    <link href="https://believed-breadfruit.top/2026/06/01/2026-06-01-claude-code%E8%BF%87%E7%A8%8B%E8%A7%82%E6%B5%8B/"/>
    <published>2026-06-01T00:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Claude Code 的过程观测有几个层次，从轻量到重型都有，下面分层介绍。</p>
<hr>
<h2 id="一、交互模式下的即时查看"><a href="#一、交互模式下的即时查看" class="headerlink" title="一、交互模式下的即时查看"></]]>
    </summary>
    <title>Claude Code 过程观测的几种方式</title>
    <updated>2026-05-31T16:16:24.100Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="哲学" scheme="https://believed-breadfruit.top/tags/%E5%93%B2%E5%AD%A6/"/>
    <category term="思考" scheme="https://believed-breadfruit.top/tags/%E6%80%9D%E8%80%83/"/>
    <content>
      <![CDATA[<p>斯多葛哲学（Stoicism）起源于古希腊、在古罗马发展成熟，代表人物包括 Zeno of Citium、Epictetus、Seneca 和 Marcus Aurelius。很多人以为斯多葛就是”忍耐””压抑情绪”，但那只是很小的一部分——它本质上是一套关于如何生活的哲学体系。浓缩成几个核心思想，大致如下。</p><ol><li>区分可控与不可控</li></ol><p>Epictetus 在《手册》开篇就指出：”有些事情取决于我们，有些事情不取决于我们。”取决于我们的，是判断、选择、价值观和行动；不取决于我们的，则是天气、他人评价、财富、身体健康的很多方面、社会环境，以及最终结果。</p><p>斯多葛认为，痛苦往往来自于试图控制无法控制的东西——希望所有人喜欢自己、希望永远不失败、希望事情完全按计划发展，这些目标本身就与现实冲突。因此他们主张把精力集中于行动而非结果，这与现代心理学中的”过程导向”很接近。</p><ol start="2"><li>德性是唯一真正的善</li></ol><p>这是斯多葛最容易被现代人忽略的部分。他们认为财富、地位、健康、名声都不是人生最高目标，真正重要的是 智慧（Wisdom）、勇气（Courage）、正义（Justice）、节制（Temperance）。一个人贫穷但正直，依然是成功的人；一个人富有却卑劣，依然是失败的人。幸福建立在品格上，而不是建立在运气上。</p><ol start="3"><li>情绪来自判断</li></ol><p>斯多葛并不认为外界直接产生情绪，而是 事件 → 判断 → 情绪。同样是”失业”，解释成”人生完了”会带来焦虑和绝望，解释成”一次新的机会”则带来平静和积极行动。事件相同、情绪不同，区别完全来自认知解释——这也是后来 CBT（认知行为疗法）的重要思想来源。</p><ol start="4"><li>接受现实（Amor Fati）</li></ol><p>斯多葛强调不要与已经发生的现实对抗，Nietzsche 后来把这种思想概括为 Amor Fati（热爱命运）。这不是消极认命，而是承认现实已经发生、与现实争吵毫无意义，应当把精力放在”接下来我能做什么”上。面对考试失败、项目延期、被人拒绝，斯多葛不会否认痛苦的存在，只是认为抱怨无法改变事实，行动才可能改变未来。</p><ol start="5"><li>死亡意识（Memento Mori）</li></ol><p>斯多葛经常提醒自己”人终将死亡”，这听起来悲观，目的却恰恰相反：当意识到生命有限时，网络争吵、面子问题、一时得失这些烦恼都会缩小。他们借助死亡意识来校准人生优先级。</p><ol start="6"><li>负面预演（Premeditatio Malorum）</li></ol><p>主动去想象失业、失败、被拒绝、贫穷，目的不是吓自己，而是降低对不确定性的恐惧。很多焦虑来自”我无法接受最坏情况”，但当真正面对最坏情况时，人往往发现自己能够承受，这会显著提高心理韧性。</p><ol start="7"><li>人是共同体动物</li></ol><p>斯多葛并不主张独来独往。Marcus Aurelius 写道：”人生来就是为了彼此合作。”他们认为每个人都是更大整体的一部分，因此应当善待他人、履行责任、贡献社会——这与纯粹的个人主义并不相同。</p><hr><p>一句话总结：接受无法改变的现实，专注于能够控制的行动，并通过智慧、勇气、正义和节制塑造自己的品格。</p><p>斯多葛哲学最有价值的部分可能不是”坚强”，而是那个持续追问：<strong>“这件事究竟是现实本身让我痛苦，还是我对现实的解释让我痛苦？”</strong> 这个问题，正是斯多葛哲学和认知行为疗法的交汇点。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/05/30/2026-05-30-%E6%96%AF%E5%A4%9A%E8%91%9B%E5%93%B2%E5%AD%A6%E5%BE%88%E5%A4%9A%E7%90%86%E5%BF%B5%E5%92%8C%E6%88%91%E7%B1%BB%E4%BC%BC/</id>
    <link href="https://believed-breadfruit.top/2026/05/30/2026-05-30-%E6%96%AF%E5%A4%9A%E8%91%9B%E5%93%B2%E5%AD%A6%E5%BE%88%E5%A4%9A%E7%90%86%E5%BF%B5%E5%92%8C%E6%88%91%E7%B1%BB%E4%BC%BC/"/>
    <published>2026-05-29T16:00:00.000Z</published>
    <summary>
      <![CDATA[<p>斯多葛哲学（Stoicism）起源于古希腊、在古罗马发展成熟，代表人物包括 Zeno of Citium、Epictetus、Seneca 和 Marcus Aurelius。很多人以为斯多葛就是”忍耐””压抑情绪”，但那只是很小的一部分——它本质上是一套关于如何生活的哲学]]>
    </summary>
    <title>斯多葛哲学很多理念和我类似</title>
    <updated>2026-06-04T17:05:28.774Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Thinking" scheme="https://believed-breadfruit.top/categories/Thinking/"/>
    <category term="自我认知" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E6%88%91%E8%AE%A4%E7%9F%A5/"/>
    <category term="自律" scheme="https://believed-breadfruit.top/tags/%E8%87%AA%E5%BE%8B/"/>
    <category term="精力管理" scheme="https://believed-breadfruit.top/tags/%E7%B2%BE%E5%8A%9B%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<p>这周上班用脑过度，周五下班时已经累到连吃饭都觉得是负担，那天晚上很早就睡了，其他事一概不想干。周六索性休息了一整天；到了周天，整个人才重新「get my shit together」——有精力健身，也愿意坐下来复盘了。</p><p>这个小小的循环让我意识到一件事：能不能做事，本质上取决于精力，而不是意志力。休息好了，思考自然涌出来、想法自然冒出来，做事的欲望也跟着回来；可一旦精力被榨干，再有道理的事也推不动。顺着这个观察，我对「自律」又有了一些新的理解。</p><hr><p>去年我得出过一个结论：所谓自律，其实并不需要刻意强迫自己。当一件事变成你当下「最有可能、也最合适」做的选项，去做它就是顺理成章的事，根本谈不上什么自律。那时候我的方法论也很简单：不依赖意志力，而是设计环境，让目标行为成为大脑唯一可选项。顺着本能走，而不是顶着本能上。</p><p>今年我发现，「自然而然做事」这种状态其实不是凭空发生的。它有两个前提：身体已经恢复，以及饮食、休息、睡眠都到位。只有这些条件齐备，人才会自动滑入下一种状态；如果地基不在，再去硬撑着做事——确实也能做——那大概就是大家口中真正的「自律」吧。</p><hr><p>小时候被灌输的「自律」是另一套叙事：克服意志力、突破生理极限，运动员带伤跑完比赛、学生周末还在埋头做题。后来我发现，那些被鼓励去「自律」的事——学习、思考、锻炼——在生活足够单调、选项有限的时候，本来就会自然发生，根本不需要咬牙坚持。</p><p>这是去年的我看到的一面。</p><p>今年又看到了另一面：工作日精力被榨干之后，这些事真的推不动；可一旦周末休息够了，它们又会自发回来。这至少说明两点：</p><ol><li><p>学习、思考、锻炼本身就是高消耗的事。你不想做，不一定是懒、不一定是不自律，更可能是身体资源已经见底。</p></li><li><p>身体就像一块电池。低电量时确实可以再逼自己一把，但这种透支不可持续。与其执着于「再坚持一次」，不如想清楚：这一次硬撑能换来什么？如果回报有限，那休息才是更划算的选择。</p></li></ol><hr><p>这些东西表面上是个人感受，但其实背后都能找到一些更底层的逻辑。</p><p><strong>第一件事是，意志力本身就是一种有限的生理资源。</strong> 心理学里有个说法叫「自我损耗」（Ego Depletion）——Roy Baumeister 那一批研究的结论是，自控力像肌肉一样，高强度的脑力劳动、决策、情绪控制，都在持续抽取同一个能量池。能量耗尽后，人会自然倾向于阻力最小的路径，此时再去迫自己做难事，失败率高得离谱。所以「精力被榨干之后什么也不想做」，不是性格问题，是生理问题。</p><p><strong>第二件事是，真正高绩效的人管理的不是时间，而是精力。</strong> 小时候作文里那个「带伤跑完比赛的运动员」，在现代职业体育里是被严厉禁止的——那种「坚持」会毁掉运动员的整个职业生涯。</p><p>《哈佛商业评论》那篇被反复引用的 <em>The Making of a Corporate Athlete</em>，Jim Loehr 和 Tony Schwartz 说得很直白：时间是固定的，精力才是可以扩展、可以恢复的资源。顶级运动员的训练模式从来不是「持续发力」，而是压力与恢复有节奏地交替；职场上所谓的「企业运动员」也一样——不是撑着硬干，而是刻意留出恢复周期。长期只消耗不恢复，表面上还在运转，实际上决策质量、创造力和情绪稳定性都在悄悄滑坡。</p><p>所以所谓的「咬牙坚持」，本质上是在透支核心储备。如果不计投资回报，仅仅为了「感动自己」或者「看起来比别人自律」而硬撑，那不叫自律，叫自我消耗。</p><p><strong>第三件事更伤感一点，是我们被教会的「坚持」叙事，本来就是为了学校这个场景设计的。</strong> 哲学家 James P. Carse 把世间的游戏分成两种：有限游戏和无限游戏。学校是典型的有限游戏——规则固定、玩家固定（同班同学）、目标唯一（分数），且常常是零和博弈。为了在这场有限游戏里胜出，「吃苦」和「盲目坚持」自然就被括刷成了筛选机制。但社会是一场无限游戏，那套逻辑拿出来就完全不成立了。</p><p>如果社会是一场多维度的无限游戏，那么真正值得问的不是「我周末要不要逼自己学习」，而是：<strong>我该如何利用周末和精力，构建适合自己的「多维竞争优势」？</strong></p><hr><p>顺着这个问题，我比较认可三条思路。</p><p><strong>1. 从「一万小时」转向「技能叠加」</strong></p><p>学校要求你在单一学科上做到前 1%，但在社会这场无限游戏里，更聪明的策略是在两到三个领域都做到前 25%，再把它们组合起来。Marc Andreessen 说过一句话，我记很清楚：除了极少数天才，普通人获得超额回报最可靠的方式，就是同时掌握两种「组合起来很值钱」的技能。</p><p>这意味着，精力充沛的周末，不该再去死磕本职工作中边际收益递减的技能。可以挑一个完全不同、但能与现有能力发生化学反应的领域去探索——比如程序员去学设计心理学。</p><p><strong>2. 用「杠铃策略」分配精力</strong></p><p>既然身体是一块电池，那周末的精力分配就不该平均用力。Nassim Taleb 在《反脆弱》里讲过一个我觉得很好用的思路：把资源推向两个极端，避开中间地带。落到周末上是这样的：</p><p>约 <strong>80%</strong> 的精力投入「绝对恢复」。周五晚上的彻底摆烂、充足睡眠、毫无压力的散步、优质饮食。这一端守住基本盘。</p><p>约 <strong>20%</strong> 的精力投入「激进探索」。在周六下午或周日上午精力顶峰的 2–3 小时，去做有长期复利的事：写一篇东西、研究一个新工具、做一次深度训练。</p><p>砍掉中间地带。那些既不能让你彻底放松、也不能让你真正成长的活动（漫无目的刷短视频、无效社交），是最不划算的投入。</p><p><strong>3. 重新看待「休闲」</strong></p><p>休闲不是生产力的对立面。Cal Newport 讲「深度游戏」（Deep Play）时说得很准：高质量的休闲并不是对生产力的浪费，反而是维持高水平深度工作的前提。</p><p>对我来说，这意味着周末除了「纯休息」，还可以主动去找那种让人愿意投入、甚至能带来能量回流的活动：需要专注的运动（球类）、一门手艺（做饭）、沉浸式的阅读。它们也消耗体力，但不榨干心智带宽，反而能清理工作日积下的认知垃圾。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/05/24/2026-05-24-%E6%88%91%E7%9A%84%E8%87%AA%E5%BE%8B%E8%A7%82-2-0/</id>
    <link href="https://believed-breadfruit.top/2026/05/24/2026-05-24-%E6%88%91%E7%9A%84%E8%87%AA%E5%BE%8B%E8%A7%82-2-0/"/>
    <published>2026-05-24T08:30:00.000Z</published>
    <summary>
      <![CDATA[<p>这周上班用脑过度，周五下班时已经累到连吃饭都觉得是负担，那天晚上很早就睡了，其他事一概不想干。周六索性休息了一整天；到了周天，整个人才重新「get my shit together」——有精力健身，也愿意坐下来复盘了。</p>
<p>这个小小的循环让我意识到一件事：能不能做]]>
    </summary>
    <title>我的自律观 2.0</title>
    <updated>2026-06-04T17:08:35.374Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="Today I Learn" scheme="https://believed-breadfruit.top/categories/Today-I-Learn/"/>
    <category term="技术" scheme="https://believed-breadfruit.top/tags/%E6%8A%80%E6%9C%AF/"/>
    <category term="JavaScript" scheme="https://believed-breadfruit.top/tags/JavaScript/"/>
    <category term="Promise" scheme="https://believed-breadfruit.top/tags/Promise/"/>
    <category term="Deferred Pattern" scheme="https://believed-breadfruit.top/tags/Deferred-Pattern/"/>
    <category term="Vue" scheme="https://believed-breadfruit.top/tags/Vue/"/>
    <content>
      <![CDATA[<blockquote><p>本文从一段 Vue 项目中「防抖 + Promise resolve 外挂」的弹窗代码出发，分析它为什么别扭、背后想实现的 Deferred Pattern 究竟是什么，以及在现代 JavaScript 里更干净的写法。</p></blockquote><h2 id="一、问题代码"><a href="#一、问题代码" class="headerlink" title="一、问题代码"></a>一、问题代码</h2><p>先看引发讨论的这段代码：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> debounceOpenRecomfirmPopup = <span class="title function_">debounce</span>(</span><br><span class="line"><span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="comment">// 记录 Promise resolve，等待关闭回调传回 actionCode</span></span><br><span class="line">recomfirmPopupPromiseResolve.<span class="property">value</span> = resolve <span class="keyword">as</span> (<span class="attr">actionCode</span>: <span class="string">&#x27;confirm&#x27;</span> | <span class="string">&#x27;cancel&#x27;</span> | <span class="string">&#x27;close&#x27;</span>) =&gt; <span class="built_in">void</span></span><br><span class="line"><span class="comment">// 打开二次确认弹窗</span></span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line">&#125;,</span><br><span class="line"><span class="number">300</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 利息换超市卡二次确认弹窗：打开</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;<span class="string">&#x27;confirm&#x27;</span> | <span class="string">&#x27;cancel&#x27;</span> | <span class="string">&#x27;close&#x27;</span>&gt;(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="title function_">debounceOpenRecomfirmPopup</span>(resolve)</span><br><span class="line">&#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>第一眼看上去会有几个疑问：</p><ul><li>防抖的回调里再去记录 Promise 的 <code>resolve</code>，打开和完成是断开的，<strong>顺序怎么保证？</strong></li><li>这是某种「高级稳定」的写法吗？</li><li>我是不是应该照着学？</li></ul><p>直觉是对的——<strong>这不是值得照搬的高级模式，而是一个把两个机制硬耦合在一起的 workaround。</strong></p><hr><h2 id="二、为什么这段代码别扭"><a href="#二、为什么这段代码别扭" class="headerlink" title="二、为什么这段代码别扭"></a>二、为什么这段代码别扭</h2><h3 id="2-1-完整的执行链路"><a href="#2-1-完整的执行链路" class="headerlink" title="2.1 完整的执行链路"></a>2.1 完整的执行链路</h3><ol><li>调用 <code>openRecomfirmPopupAsync()</code>：创建 Promise，把 <code>resolve</code> 传给 <code>debounceOpenRecomfirmPopup</code>。</li><li>防抖 300ms 后才执行：把 <code>resolve</code> 存到 <code>recomfirmPopupPromiseResolve.value</code>，并打开弹窗。</li><li>弹窗关闭：<code>handleRecomfirmClose</code> 调用存起来的 <code>resolve(actionCode)</code>，Promise 才算完成。</li></ol><h3 id="2-2-顺序隐患：debounce-会吞掉-resolve"><a href="#2-2-顺序隐患：debounce-会吞掉-resolve" class="headerlink" title="2.2 顺序隐患：debounce 会吞掉 resolve"></a>2.2 顺序隐患：debounce 会吞掉 resolve</h3><p>如果用户快速连续触发两次 <code>openRecomfirmPopupAsync()</code>：</p><ul><li>第一次的 <code>resolve</code> 被防抖<strong>吞掉了</strong>（不执行），但对应的 Promise 已经挂起在等待。</li><li>防抖只执行最后一次回调，第二次的 <code>resolve</code> 覆盖了 <code>recomfirmPopupPromiseResolve.value</code>。</li><li><strong>第一次的 Promise 永远不会被 resolve —— 直接挂死。</strong></li></ul><blockquote><p><strong>核心问题</strong>：防抖的语义是「只保留最后一次调用」，而 Promise 的语义是「每次调用都必须有结果」。把 <code>resolve</code> 丢进 debounce，相当于允许「Promise 完成事件」被静默丢弃。</p></blockquote><h3 id="2-3-「resolve-外挂到-ref」本身也不稳"><a href="#2-3-「resolve-外挂到-ref」本身也不稳" class="headerlink" title="2.3 「resolve 外挂到 ref」本身也不稳"></a>2.3 「resolve 外挂到 ref」本身也不稳</h3><ul><li>依赖一个全局 ref 做中转，任何地方意外清空就会挂死。</li><li>和 Vue 响应式系统混用，调试困难。</li><li>弹窗如果走了非预期路径（组件销毁、路由跳转、异常），<code>resolve</code> 永远不会被调用，<strong>Promise 泄漏</strong>。</li></ul><hr><h2 id="三、它想做的事：Deferred-Pattern"><a href="#三、它想做的事：Deferred-Pattern" class="headerlink" title="三、它想做的事：Deferred Pattern"></a>三、它想做的事：Deferred Pattern</h2><p>抛开实现，这段代码的实质意图非常简单：</p><blockquote><p>弹窗打开 → 等用户操作 → 拿到结果继续。</p></blockquote><p>业务代码希望能这样写：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一气呵成，逻辑连贯</span></span><br><span class="line"><span class="keyword">const</span> actionCode = <span class="keyword">await</span> <span class="title function_">openRecomfirmPopupAsync</span>()</span><br><span class="line"><span class="keyword">if</span> (actionCode === <span class="string">&#x27;cancel&#x27;</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line"><span class="comment">// 继续下单...</span></span><br></pre></td></tr></table></figure><p>但弹窗本身是异步的：</p><ol><li><code>isShowRecomfirmPopup = true</code> 打开它；</li><li>用户看到弹窗，点击「确认 &#x2F; 取消」；</li><li>Vue 组件 emit <code>close</code> 事件，传回 <code>actionCode</code>。</li></ol><p><strong>步骤 2 是不可预测的时间间隔</strong>——你不可能在打开弹窗后同步拿到结果。这正是 Promise 的用武之地。</p><blockquote><p><strong>Promise 的本质</strong>：把「未来才会发生的结果」变成一个现在就能持有的对象。</p></blockquote><p>这种「把异步操作和它的完成控制权解耦」的模式，就是 <strong>Deferred Pattern（延迟对象模式）</strong>。</p><hr><h2 id="四、Deferred-Pattern-详解"><a href="#四、Deferred-Pattern-详解" class="headerlink" title="四、Deferred Pattern 详解"></a>四、Deferred Pattern 详解</h2><h3 id="4-1-Promise-vs-Deferred"><a href="#4-1-Promise-vs-Deferred" class="headerlink" title="4.1 Promise vs. Deferred"></a>4.1 Promise vs. Deferred</h3><table><thead><tr><th>对比维度</th><th>Promise</th><th>Deferred</th></tr></thead><tbody><tr><td>关注点</td><td>异步操作的<strong>结果</strong></td><td>异步操作<strong>本身及其控制权</strong></td></tr><tr><td>类比</td><td>取餐票，只能等出餐</td><td>厨师手里的底单，决定何时出餐</td></tr><tr><td>谁能完成它</td><td>构造函数内部的逻辑</td><td>外部任意代码（持有 resolve &#x2F; reject 的人）</td></tr></tbody></table><h3 id="4-2-基础实现"><a href="#4-2-基础实现" class="headerlink" title="4.2 基础实现"></a>4.2 基础实现</h3><p>在现代 JavaScript 里，可以用一个工具函数构造 Deferred：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> createDeferred&lt;T&gt;() &#123;</span><br><span class="line"><span class="keyword">let</span> resolve!: <span class="function">(<span class="params">value: T</span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"><span class="keyword">let</span> reject!: <span class="function">(<span class="params">reason?: <span class="built_in">unknown</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;T&gt;(<span class="function">(<span class="params">res, rej</span>) =&gt;</span> &#123;</span><br><span class="line">resolve = res</span><br><span class="line">reject = rej</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> &#123; promise, resolve, reject &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要点：</p><ul><li><code>resolve / reject</code> 在构造函数中被「偷出来」赋值给外部变量。</li><li>返回一个三件套：<code>promise</code> 给消费方 await，<code>resolve / reject</code> 给控制方调用。</li></ul><h3 id="4-3-适用场景"><a href="#4-3-适用场景" class="headerlink" title="4.3 适用场景"></a>4.3 适用场景</h3><ul><li><strong>解耦异步逻辑</strong>：开始和结束不在同一个作用域里（事件监听、Socket 消息、弹窗回调等）。</li><li><strong>封装回调式 API</strong>：旧接口不便直接塞进 <code>new Promise</code> 构造函数时，用 Deferred 当外挂。</li><li><strong>跨函数共享 resolve</strong>：多个回调函数协同决定一个 Promise 的命运。</li></ul><hr><h2 id="五、现代标准：Promise-withResolvers"><a href="#五、现代标准：Promise-withResolvers" class="headerlink" title="五、现代标准：Promise.withResolvers()"></a>五、现代标准：<code>Promise.withResolvers()</code></h2><p>ECMAScript 2024 (ES15) 已经把这个模式标准化了，直接内置：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; promise, resolve, reject &#125; = <span class="title class_">Promise</span>.<span class="property">withResolvers</span>&lt;<span class="built_in">string</span>&gt;()</span><br><span class="line"></span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line"><span class="title function_">resolve</span>(<span class="string">&#x27;任务完成！&#x27;</span>)</span><br><span class="line">&#125;, <span class="number">2000</span>)</span><br><span class="line"></span><br><span class="line">promise.<span class="title function_">then</span>(<span class="variable language_">console</span>.<span class="property">log</span>)</span><br></pre></td></tr></table></figure><p>以后写 Deferred 不需要再手撸 <code>createDeferred</code>，直接用原生 API 即可。</p><hr><h2 id="六、为什么-Promise-一开始是-pending"><a href="#六、为什么-Promise-一开始是-pending" class="headerlink" title="六、为什么 Promise 一开始是 pending"></a>六、为什么 Promise 一开始是 pending</h2><p>回到最初的疑问：</p><blockquote><p>点击下单要打开二次弹窗时，<code>openRecomfirmPopupAsync</code> 返回的 Promise 里也没立刻 <code>resolve</code> 啊，它一直在 pending —— 这是怎么工作的？</p></blockquote><p>答案是：<strong><code>resolve</code></strong> <strong>是一个普通函数对象，可以被传递、存储、延后调用。Promise 不需要在创建时就完成。</strong></p><h3 id="6-1-执行时间线"><a href="#6-1-执行时间线" class="headerlink" title="6.1 执行时间线"></a>6.1 执行时间线</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">时间 0ms ─── 业务代码调用 openRecomfirmPopupAsync()</span><br><span class="line">│</span><br><span class="line">│  new Promise(resolve =&gt; debounceOpenRecomfirmPopup(resolve))</span><br><span class="line">│  → resolve 被当作参数传进 debounce，但 debounce 不立即执行</span><br><span class="line">│  → 此时 Promise 状态：pending，resolve 函数&quot;悬空&quot;</span><br><span class="line">│  → await 暂停，业务代码停在这里等待</span><br><span class="line">│</span><br><span class="line">时间 300ms ─── debounce 延迟结束，执行回调</span><br><span class="line">│</span><br><span class="line">│  recomfirmPopupPromiseResolve.value = resolve   ← resolve 被存起来</span><br><span class="line">│  isShowRecomfirmPopup.value = true              ← 弹窗打开</span><br><span class="line">│</span><br><span class="line">│  → Promise 状态：仍然是 pending</span><br><span class="line">│  → 但现在 resolve 有了&quot;持有者&quot;（ref），未来可以被调用</span><br><span class="line">│</span><br><span class="line">时间 ?s ─── 用户在弹窗里点了确认 / 取消 / 关闭</span><br><span class="line">│</span><br><span class="line">│  handleRecomfirmClose(&#123; actionCode &#125;)</span><br><span class="line">│  recomfirmPopupPromiseResolve.value?.(actionCode) ← resolve 被调用</span><br><span class="line">│  recomfirmPopupPromiseResolve.value = null         ← 清理</span><br><span class="line">│</span><br><span class="line">│  → Promise 状态：pending → fulfilled，值 = actionCode</span><br><span class="line">│  → await 恢复执行，业务代码拿到 &#x27;confirm&#x27; / &#x27;cancel&#x27; / &#x27;close&#x27;</span><br></pre></td></tr></table></figure><h3 id="6-2-类比：事件监听"><a href="#6-2-类比：事件监听" class="headerlink" title="6.2 类比：事件监听"></a>6.2 类比：事件监听</h3><p>这就像你给 <code>addEventListener</code> 注册了一个回调：事件没触发时回调当然不会执行，但回调已经「挂上」了。Promise 的 <code>resolve</code> 也是同理：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="comment">// resolve 是一个函数，可以像任何变量一样被传递</span></span><br><span class="line"><span class="comment">// 传给 debounce、存到 ref、传给另一个函数——都可以</span></span><br><span class="line"><span class="comment">// 只要最终有人调用 resolve(值)，Promise 就会完成</span></span><br><span class="line"><span class="title function_">debounceOpenRecomfirmPopup</span>(resolve)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>Deferred Pattern 的核心</strong>就是：把「何时完成」的决定权从 Promise 内部移到了外部。</p><ul><li>普通 Promise 是<strong>内部自治</strong>的（网络请求成功就 resolve）。</li><li>Deferred 是<strong>外部控制</strong>的（弹窗关闭才 resolve）。</li></ul><hr><h2 id="七、更干净的写法"><a href="#七、更干净的写法" class="headerlink" title="七、更干净的写法"></a>七、更干净的写法</h2><p>回到最初那段代码，改造方向有两个原则：</p><ol><li><strong>防双击应该在 UI 层做（按钮点击节流），不要和 Promise 机制纠缠。</strong></li><li><strong><code>resolve</code></strong>** 不要散落在 ref 里，尽量保持在同一个闭包内管理。**</li></ol><h3 id="方案-A：用-Deferred-工具函数封装"><a href="#方案-A：用-Deferred-工具函数封装" class="headerlink" title="方案 A：用 Deferred 工具函数封装"></a>方案 A：用 Deferred 工具函数封装</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> createDeferred&lt;T&gt;() &#123;</span><br><span class="line"><span class="keyword">let</span> resolve!: <span class="function">(<span class="params">value: T</span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"><span class="keyword">let</span> reject!: <span class="function">(<span class="params">reason?: <span class="built_in">unknown</span></span>) =&gt;</span> <span class="built_in">void</span></span><br><span class="line"><span class="keyword">const</span> promise = <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;T&gt;(<span class="function">(<span class="params">r, j</span>) =&gt;</span> &#123; resolve = r; reject = j &#125;)</span><br><span class="line"><span class="keyword">return</span> &#123; promise, resolve, reject &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 弹窗使用</span></span><br><span class="line"><span class="keyword">let</span> <span class="attr">currentDeferred</span>: <span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> createDeferred&lt;<span class="title class_">ActionCode</span>&gt;&gt; | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">currentDeferred = createDeferred&lt;<span class="title class_">ActionCode</span>&gt;()</span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line"><span class="keyword">return</span> currentDeferred.<span class="property">promise</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleRecomfirmClose</span> = (<span class="params">actionCode: ActionCode</span>) =&gt; &#123;</span><br><span class="line">currentDeferred?.<span class="title function_">resolve</span>(actionCode)</span><br><span class="line">currentDeferred = <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方案-B：直接用-Promise-withResolvers"><a href="#方案-B：直接用-Promise-withResolvers" class="headerlink" title="方案 B：直接用 Promise.withResolvers()"></a>方案 B：直接用 <code>Promise.withResolvers()</code></h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="attr">pending</span>: <span class="title class_">ReturnType</span>&lt;<span class="keyword">typeof</span> <span class="title class_">Promise</span>.<span class="property">withResolvers</span>&lt;<span class="title class_">ActionCode</span>&gt;&gt; | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">pending = <span class="title class_">Promise</span>.<span class="property">withResolvers</span>&lt;<span class="title class_">ActionCode</span>&gt;()</span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line"><span class="keyword">return</span> pending.<span class="property">promise</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleRecomfirmClose</span> = (<span class="params">actionCode: ActionCode</span>) =&gt; &#123;</span><br><span class="line">pending?.<span class="title function_">resolve</span>(actionCode)</span><br><span class="line">pending = <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="方案-C：组件内部用-watchOnce-自动收尾"><a href="#方案-C：组件内部用-watchOnce-自动收尾" class="headerlink" title="方案 C：组件内部用 watchOnce 自动收尾"></a>方案 C：组件内部用 <code>watchOnce</code> 自动收尾</h3><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">openRecomfirmPopupAsync</span> = (<span class="params"></span>) =&gt; <span class="keyword">new</span> <span class="title class_">Promise</span>&lt;<span class="title class_">ActionCode</span>&gt;(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123;</span><br><span class="line">isShowRecomfirmPopup.<span class="property">value</span> = <span class="literal">true</span></span><br><span class="line"><span class="title function_">watchOnce</span>(isShowRecomfirmPopup, <span class="function">(<span class="params">val</span>) =&gt;</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (!val) <span class="title function_">resolve</span>(lastActionCode.<span class="property">value</span>)</span><br><span class="line">&#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>三种写法的共同点：<strong>逻辑是线性的，不会丢 resolve，也没有防抖和 Promise 的耦合。</strong></p><hr><h2 id="八、总结"><a href="#八、总结" class="headerlink" title="八、总结"></a>八、总结</h2><blockquote><p><strong>值得学的</strong>：Deferred Pattern 的思路 —— Promise 可以拆开，手动控制完成时机。这是处理异步 UI 交互、事件驱动逻辑的标准手段。</p></blockquote><blockquote><p><strong>不值得照搬的</strong>：在原代码里，把 <code>debounce</code> 和 Deferred 混用、把 <code>resolve</code> 外挂到 Vue ref —— 这是两个不相干机制的硬耦合，会带来顺序丢失和 Promise 泄漏的隐患。</p></blockquote><p>一句话总结：</p><blockquote><p>学到的是<strong>思路</strong>（Promise 可以手动控制完成时机），不是<strong>具体写法</strong>（debounce + ref 外挂 resolve）。</p></blockquote>]]>
    </content>
    <id>https://believed-breadfruit.top/2026/05/10/2026-05-10-Promise-Deferred-Pattern/</id>
    <link href="https://believed-breadfruit.top/2026/05/10/2026-05-10-Promise-Deferred-Pattern/"/>
    <published>2026-05-10T08:30:00.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>本文从一段 Vue 项目中「防抖 + Promise resolve 外挂」的弹窗代码出发，分析它为什么别扭、背后想实现的 Deferred Pattern 究竟是什么，以及在现代 JavaScript 里更干净的写法。</p>
</blockqu]]>
    </summary>
    <title>Promise Deferred Pattern：从一段别扭的弹窗代码说起</title>
    <updated>2026-05-10T15:40:22.889Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="运动与健康" scheme="https://believed-breadfruit.top/categories/%E8%BF%90%E5%8A%A8%E4%B8%8E%E5%81%A5%E5%BA%B7/"/>
    <category term="跑步" scheme="https://believed-breadfruit.top/tags/%E8%B7%91%E6%AD%A5/"/>
    <category term="运动" scheme="https://believed-breadfruit.top/tags/%E8%BF%90%E5%8A%A8/"/>
    <category term="健身" scheme="https://believed-breadfruit.top/tags/%E5%81%A5%E8%BA%AB/"/>
    <category term="训练计划" scheme="https://believed-breadfruit.top/tags/%E8%AE%AD%E7%BB%83%E8%AE%A1%E5%88%92/"/>
    <content>
      <![CDATA[<h2 id="衡量跑步性能的指标"><a href="#衡量跑步性能的指标" class="headerlink" title="衡量跑步性能的指标"></a>衡量跑步性能的指标</h2><p>作为有氧运动，跑步的性能衡量主要分为4个方面：</p><ul><li>持续性和坚持: 能够长期坚持跑步，养成稳定的锻炼习惯，例如每周跑步3-5次。</li><li>距离和耐力: 可以连续跑完一定的距离，如5公里、10公里，甚至半程或全程马拉松。</li><li>速度和配速: 在特定距离内达到较快的速度，例如5公里跑进25分钟。</li><li>心率: 心率是衡量心肺能力的重要指标。在跑步中能够长时间维持在有氧区间（Zone 2, 60%-70%的最大心率）是良好心肺健康的表现。</li></ul><h2 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h2><h3 id="近3个月跑步记录"><a href="#近3个月跑步记录" class="headerlink" title="近3个月跑步记录"></a>近3个月跑步记录</h3><table><thead><tr><th>Month</th><th>Consistency (Runs)</th><th>Speed (Pace)</th><th>Distance (Avg km)</th><th>Heart Rate (Avg bpm)</th></tr></thead><tbody><tr><td>11月</td><td>10</td><td>6.21 min&#x2F;km</td><td>7.11</td><td>149.9</td></tr><tr><td>10月</td><td>12</td><td>6.59 min&#x2F;km</td><td>4.47</td><td>137.2</td></tr><tr><td>9月</td><td>17</td><td>7.23 min&#x2F;km</td><td>5.37</td><td>145.35</td></tr></tbody></table><h3 id="近两周跑步记录"><a href="#近两周跑步记录" class="headerlink" title="近两周跑步记录"></a>近两周跑步记录</h3><table><thead><tr><th>Week</th><th>Consistency (Runs)</th><th>Speed (Pace)</th><th>Distance (Avg km)</th><th>Heart Rate (Avg bpm)</th></tr></thead><tbody><tr><td>11.24-12.01</td><td>3</td><td>5.59 min&#x2F;km</td><td>7.57</td><td>147.33</td></tr><tr><td>11.17-11.24</td><td>3</td><td>6.05 min&#x2F;km</td><td>7.86</td><td>152</td></tr></tbody></table><h3 id="数据趋势总结"><a href="#数据趋势总结" class="headerlink" title="数据趋势总结"></a>数据趋势总结</h3><ol><li>跑步频率（Consistency）：形成跑步习惯</li></ol><ul><li>近3个月：9月最多（17次），10月减少到12次，11月进一步下降到10次。</li><li>近两周：保持稳定，每周跑步3次，目前跑步频率趋于均衡，已经形成跑步习惯</li></ul><ol start="2"><li>配速（Speed &#x2F; Pace）：配速稳定提升，尤其是近两周已经显著超过前三个月的平均水平，跑步效率和速度都有所改善。</li></ol><ul><li>近3个月：配速持续提升：9月为 7.23 min&#x2F;km，10月为 6.59 min&#x2F;km，11月进一步提升到 6.21 min&#x2F;km。</li><li>近两周：11.24-12.01：5.59 min&#x2F;km，比前一周 6.05 min&#x2F;km 提升显著。</li></ul><ol start="3"><li>距离（Distance）：耐力逐步提升</li></ol><ul><li>近3个月：9月平均 5.37 km，10月减少到 4.47 km，但11月回升到 7.11 km。</li><li>近两周：11.17-11.24 跑步距离较高（7.86 km），11.24-12.01 略微下降到 7.57 km。</li></ul><ol start="4"><li>平均心率（Heart Rate）：心肺适应性有所改善，逐渐适应高强度训练</li></ol><ul><li>近3个月：9月心率为 145.35 bpm，10月下降到 137.2 bpm，但11月上升到 149.9 bpm。</li><li>近两周：11.24-12.01：147.33 bpm，略低于前一周的 152 bpm。</li><li>心率整体呈波动趋势：<ul><li>9月和10月的心率偏低，可能因为配速较慢。</li><li>11月心率回升至较高水平，与配速加快和训练强度提升相关。</li><li>近两周心率略有下降，表明跑步的心肺适应性有所改善。</li></ul></li></ul><h3 id="数据对标"><a href="#数据对标" class="headerlink" title="数据对标"></a>数据对标</h3><p>根据<a href="https://runninglevel.com/#results">running level 网站</a>的计算，我的5k, 10k的水平如下图所示：</p><ul><li><p>5k<br><img src="/../images/better_at_running/image-2.png" alt="alt text"></p></li><li><p>10k<br><img src="/../images/better_at_running/image-1.png" alt="alt text"></p></li></ul><p>可以看到，对标我年龄段的业余跑者来看，我处于入门水平。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>总体来讲，我很满意目前的跑步性能，看到了我未来的提升空间很大，看到了进步的趋势。最重要的是，每次跑完，感觉真的很爽，心情舒畅。</p><h2 id="回忆以前"><a href="#回忆以前" class="headerlink" title="回忆以前"></a>回忆以前</h2><p>我是今年5月份开始跑步，那段时间，我面临着两个不争的事实：</p><ol><li><p>我感觉自己达不到现在的水平。我的微信朋友圈有一个好友一直在坚持跑步，他的配速时5-6min&#x2F;km，距离是5k。有一次我直接向外发出了一个失败的感叹，“我永远也做不到像他这”，这句话我印象十分深刻。</p></li><li><p>第2公里很难坚持下去，每次往前跑一步，我都有疑问“自己为什么要迈出这步”。那个时候，我根本不知道自己为什么要跑步，只知道身体需要锻炼，我没有任何方法。</p></li></ol><p>今年上半年，我的跑步情况是这样的：</p><ul><li>1月跑了1次，2.67km, 配速9:16 min&#x2F;km</li><li>2月跑了1次，2.83km, 配速10:53 min&#x2F;km</li><li>3月没跑</li><li>4月跑了6次，平均2km, 配速9:12 min&#x2F;km</li><li>5月跑了10次，平均3.23km, 配速7:54 min&#x2F;km</li><li>6月跑了11次，平均3.23km, 配速8:48 min&#x2F;km</li></ul><p><img src="/../images/better_at_running/image-3.png" alt="alt text"></p><p>可以发现，这些数据真的很糟糕。</p><h2 id="跑步阶段回顾"><a href="#跑步阶段回顾" class="headerlink" title="跑步阶段回顾"></a>跑步阶段回顾</h2><p>从今年5月份开始决定跑步，到现在，我的跑步历程主要分为下面5个阶段，</p><ol><li>起步（5-7月份）。定一个简单的目标: 每周跑3次，随便找了个habit tracker工具，跑完记录一次。</li></ol><p><img src="/../images/better_at_running/habit.jpeg" alt="alt text"></p><p>这个阶段有两个问题：非常多断跑，跑的过程中很痛苦，不知道为啥要跑下一步。</p><ol start="2"><li>稳定跑（8月份）。主要是解决断跑+跑多少的问题。开了个keep会员，可以选一些跑步训练，比如3公里，跑的过程中还可以听节奏。</li></ol><p>这个阶段有点跑步的感觉了，但是感觉每次跑步都很累，如果跑步的情绪记忆是痛苦的，那肯定会影响跑步积极性。</p><p><img src="/../images/better_at_running/IMG_1374.jpg" alt="alt text"></p><ol start="3"><li><p>数据跑（9月份）。买了个新表garmin，忘了为什么要买，但是这表可以记录更多数据，从这个月开始，我开始了解跑步数据了，每次跑完就看看数据，还让ai给我生成一些跑步计划。</p></li><li><p>科学跑（10月份）。无意间了解到一个“二区跑”这个概念，然后在网上看了很多讲解，开始关注心率、有氧、耐力这些性能指标了。</p></li></ol><p><img src="/../images/better_at_running/image-4.png" alt="alt text"></p><ol start="5"><li>上瘾跑（11月份）。二区跑开始慢慢提速了，虽然心率跑到3、4区了，但是配速快了，而且跑完也不累，心情感觉也很爽，这个月跑步性能突然有了质的提升，数据给的正向反馈+多巴胺内啡肽的释放+习惯，各种buff叠加，发现跑步越来越上瘾了。</li></ol><h2 id="经验总结"><a href="#经验总结" class="headerlink" title="经验总结"></a>经验总结</h2><h3 id="阶段路径总结"><a href="#阶段路径总结" class="headerlink" title="阶段路径总结"></a>阶段路径总结</h3><p>每一个阶段，我都有一套统一的规则：目标，工具，反思。工具的选择其实并没有精心挑选过，大部分都是过程中体验，而且在好奇心的驱使下，无意间建立了很多认知。</p><h3 id="经验分析"><a href="#经验分析" class="headerlink" title="经验分析"></a>经验分析</h3><p>过程中确实有一些弯路，但是正是因为有这些弯路，才逐渐建立了对跑步的全局认知，这种认知我并不需要前置，不必要求一开始就全部知道。所以这样看，谈论规避弯路没有意义，但是确实两点可以前置。</p><ul><li>目标感加强。一开始动机不强不要紧，但是必须清楚地意识到，一件事情，要么做，要么不做，要是做了，就早点get going，缩短无效探索时间。</li><li>早点建立科学理念，在这个场景下，就是跑步理论，防止受伤。</li></ul><p>第二点对我来说不强烈，可能因为我这方面底子好，但是第一点是非常真实的感悟，通过这次跑步的经验总结，我似乎拥有了一种可以实现目标的超能力。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/12/01/2024-12-01-how-do-i-get-better-at-running/</id>
    <link href="https://believed-breadfruit.top/2024/12/01/2024-12-01-how-do-i-get-better-at-running/"/>
    <published>2024-12-01T03:05:01.562Z</published>
    <summary>
      <![CDATA[<h2 id="衡量跑步性能的指标"><a href="#衡量跑步性能的指标" class="headerlink" title="衡量跑步性能的指标"></a>衡量跑步性能的指标</h2><p>作为有氧运动，跑步的性能衡量主要分为4个方面：</p>
<ul>
<li>持续性和坚]]>
    </summary>
    <title>How do I get better at RUNNING</title>
    <updated>2024-12-22T06:29:08.041Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微前端" scheme="https://believed-breadfruit.top/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <content>
      <![CDATA[<p>微前端是现代前端开发的趋势，许多企业选择将微前端架构融入其技术战略中，有些是选择现有微前端框架如 <a href="https://single-spa.js.org/docs/getting-started-overview">Single-SPA</a>、<a href="https://qiankun.umijs.org/zh/guide">Qiankun</a>、<a href="https://micro-zoe.github.io/micro-app/docs.html#/">Micro-App</a> 或 <a href="https://wujie-micro.github.io/doc/guide/#%E6%97%A0%E7%95%8C%E6%96%B9%E6%A1%88">Wujie</a>，有些是根据自身独特的业务需求研发了定制的解决方案。</p><p>这一技术趋势的背后是前端技术的快速进化和单页面应用（SPA）开发模式的普及。</p><h3 id="多页应用"><a href="#多页应用" class="headerlink" title="多页应用"></a>多页应用</h3><p>在早期的前端开发中，网站通常采用多页面应用（MultiPage Application, MPA）的形式，每个页面都有自己独立的 HTML、JavaScript 和 CSS 文件。这种架构的一个显著特点是，当浏览器的 URL 发生变化时，浏览器会加载一个新的 HTML 页面，并随之加载相应的 JavaScript 和 CSS 资源。这导致页面不断刷新，虽然简单直接，但对于用户来说体验并不友好，尤其是在需要快速响应和交互的现代Web应用中。</p><h3 id="单页应用"><a href="#单页应用" class="headerlink" title="单页应用"></a>单页应用</h3><p>为了减少页面全局刷新，提升用户体验，单页面应用（SinglePage Web Application, SPA）应运而生。SPA 通过在一个单一的HTML页面加载所有JavaScript 和 CSS 资源，在用户交互过程中，网页不会重新加载或跳转，而是动态地重写当前页面与用户交互的部分，从而避免了页面的全面刷新。</p><p>SPA能实现局部渲染主要是基于两个技术实现：</p><ul><li><strong>AJAX异步</strong></li></ul><p>通过AJAX技术异步请求数据，客户端可以在不刷新整个页面的情况下，与服务器交换数据，并根据返回的数据更新页面的部分内容。这种方式允许用户在网页上进行快速交互，只加载或变更需要更新的部分。</p><ul><li><strong>JavaScript 路由</strong></li></ul><p>路由管理通常由JavaScript在前端处理，而不是服务器端。通过在客户端监听URL的变化，然后根据URL的不同展示相应的视图，而无需重新加载页面。单页面应用主要依赖两种路由模式来管理 URL 的变化：一种是基于 URL 的哈希（hash），另一种是利用 HTML5 的 History API。</p><h3 id="微前端应用"><a href="#微前端应用" class="headerlink" title="微前端应用"></a>微前端应用</h3><p>随着业务的增长和应用的扩展，单页面应用往往逐渐演变为庞大的单体应用，这不仅使得项目难以维护，同时由于前端技术栈的快速迭代，老旧技术的技术债务也日益增加。</p><p>在这样的背景下，传统的巨石单体应用已经难以应对快速迭代和灵活部署的要求。微前端架构应运而生，在微前端架构下，一个大型单一的前端应用被拆分成多个小型、独立的子应用，这些子应用负责应用的一部分功能，可以独立开发、部署、运行，并且可以使用不同的技术栈，从而带来更加灵活高效的项目开发和管理。</p><p><img src="/../images/2024-04/mfe.png" alt="Small Picture"></p><p>微前端架构本质上依然采用单页面应用的形式，即在一个单一的 HTML 文件中加载多个 JavaScript 和 CSS 资源。然而，它通过精巧的路由和应用生命周期管理，以及不可缺少的隔离和共享机制，能够将多个独立开发的子应用集成到一个协调一致的用户界面中。</p><p>从微前端概念和产生背景出发，基于对现有微前端框架的考查，可以总结出微前端架构的设计具有以下特征：</p><ul><li>子应用的独立性：独立开发、技术栈无关。</li><li>子应用的自治性（隔离）：应用运行期间有明显的边界，不受影响。</li><li>共享机制：复用性、统一体验、高效协调。</li></ul><p>这种模式不仅提高了开发效率，降低了维护难度，还使得旧系统的逐步迁移变得可行，无需重构整个应用。因此，微前端是现代Web开发中的一个重要趋势。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/04/22/%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%8A%80%E6%9C%AF%E7%BB%BC%E8%BF%B0/</id>
    <link href="https://believed-breadfruit.top/2024/04/22/%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%8A%80%E6%9C%AF%E7%BB%BC%E8%BF%B0/"/>
    <published>2024-04-22T13:15:58.000Z</published>
    <summary>
      <![CDATA[<p>微前端是现代前端开发的趋势，许多企业选择将微前端架构融入其技术战略中，有些是选择现有微前端框架如 <a href="https://single-spa.js.org/docs/getting-started-overview">Single-SPA</a>、<a href]]>
    </summary>
    <title>从多应用到微应用：前端架构的技术演变</title>
    <updated>2024-12-22T06:32:39.851Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="微前端" scheme="https://believed-breadfruit.top/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>微前端是微服务概念在前端的应用。在微前端架构下，一个大型单一的前端应用被拆分成多个小型、独立的子应用，这些子应用可以独立开发、独立部署、独立运行，从而带来更加灵活高效的项目开发和管理。</p><p>作为现代前端开发的趋势，许多企业的技术栈融入了微前端，有些是选择成熟的框架如 Qiankun、Micro-App 或 Single-SPA，有些是自研解决方案。</p><p>微前端最简单的实现形式是基于Iframe。本文主要从<a href="https://github.com/andregardi/micro-frontends-iframe">一个简单的微前端</a>案例入手，通过这个简单的Demo，让我们对微前端框架及其设计理念有个初步的理解。</p><h2 id="Demo源代码讲解"><a href="#Demo源代码讲解" class="headerlink" title="Demo源代码讲解"></a>Demo源代码讲解</h2><h3 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">iframe-demo</span><br><span class="line">├─ <span class="title class_">EventBus</span></span><br><span class="line">│  └─ <span class="title class_">EventBus</span>.<span class="property">js</span></span><br><span class="line">├─ cart</span><br><span class="line">│  ├─ index.<span class="property">html</span></span><br><span class="line">│  ├─ script.<span class="property">js</span></span><br><span class="line">│  └─ style.<span class="property">css</span></span><br><span class="line">├─ catalog</span><br><span class="line">│  ├─ index.<span class="property">html</span></span><br><span class="line">│  └─ script.<span class="property">js</span></span><br><span class="line">├─ composition.<span class="property">html</span></span><br><span class="line">├─ script.<span class="property">js</span></span><br><span class="line">└─ style.<span class="property">css</span></span><br></pre></td></tr></table></figure><p>这个Demo的目录结构非常简单，主要分为三部分：</p><ul><li>两个子应用cart、catalog</li><li>全局事件总线</li><li>主应用</li></ul><p>可以看到这个简单的项目已经体现了微前端的核心设计：应用管理、隔离和共享。</p><h3 id="应用管理"><a href="#应用管理" class="headerlink" title="应用管理"></a>应用管理</h3><p>从<code>composition.html</code>文件我们可以看出这个项目的应用管理非常简单，两个子应用通过<code>iframe</code>标签加载，共同存在于同一个页面。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;link rel=<span class="string">&quot;stylesheet&quot;</span> href=<span class="string">&quot;style.css&quot;</span> /&gt;</span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">iframe</span> <span class="attr">src</span>=<span class="string">&quot;./catalog/index.html&quot;</span> <span class="attr">id</span>=<span class="string">&quot;catalog&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">iframe</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">iframe</span> <span class="attr">src</span>=<span class="string">&quot;./cart/index.html&quot;</span> <span class="attr">id</span>=<span class="string">&quot;cart&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">iframe</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;script.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="隔离"><a href="#隔离" class="headerlink" title="隔离"></a>隔离</h2><p>由于使用的是iframe标签，两个子应用天然地存在CSS和JS隔离，不会有样式和运行时的冲突。</p><h3 id="微前端-why-iframe-not-div"><a href="#微前端-why-iframe-not-div" class="headerlink" title="微前端 why iframe not div?"></a><strong>微前端 why iframe not div?</strong></h3><p>为什么微前端使用<code>iframe</code>做隔离，而不是普通<code>div</code>。</p><p>主要是因为<code>iframe</code>在浏览器中具有天然的隔离环境，主要表现在：</p><ol><li><strong>JavaScript 隔离</strong>：<code>iframe</code> 内的 JavaScript 运行在独立的全局上下文中。这意味着，<code>iframe</code> 内的 JavaScript 变量和函数不会影响到主页面，反之亦然。这种特性对于确保应用间不会相互干扰是非常重要的。</li><li><strong>样式隔离</strong>：<code>iframe</code> 内的样式不会影响到外部页面。每个 <code>iframe</code> 有自己的文档流，所以里面的 CSS 只会作用于 <code>iframe</code>内部，不会泄露到外部，这保证了样式的独立性和一致性。</li><li><strong>DOM 隔离</strong>：每个 <code>iframe</code> 都有自己的 DOM 树，与主页面的 DOM 树完全隔离。这意味着，iframe 内的 DOM 操作不会影响到外部页面，减少了应用间的直接 DOM 冲突。</li></ol><p>这些隔离特性对于普通的<code>div</code>标签是没有的。</p><h2 id="共享-通信"><a href="#共享-通信" class="headerlink" title="共享(通信)"></a>共享(通信)</h2><p>虽然<code>iframe</code>能实现简单的微前端架构，但是通过代码中设计的全局事件总线（<code>EventBus.js</code>文件），我们可以看出基于<code>iframe</code>的微前端需要单独设计跨域通信方式。</p><p>本案例主要是通过<code>PostMessage</code>方法实现的。<code>PostMessage</code>  是一种安全地实现不同浏览器窗口（包括弹出窗口和<code>iframe</code>）间通信的方式。允许不同源（<code>origin</code>）的窗口进行数据交换，从而克服了同源策略的限制。适用于多种场景，如页面与弹出窗口、页面与嵌入的<code>iframe</code>、甚至是不同的web workers之间的通信。</p><p><strong>基本用法</strong><br>包括两部分：发送消息和接收消息。</p><p><strong>发送消息:</strong> <code>window.postMessage</code><br>这个方法接受两个参数：要发送的消息和消息接收方的源（<code>origin</code>）。但出于安全考虑，这通常不推荐。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">otherWindow.<span class="title function_">postMessage</span>(message, targetOrigin); <span class="comment">// otherWindow 是另一个窗口的引用，例如一个 iframe 或者通过 window.open 打开的窗口</span></span><br></pre></td></tr></table></figure><ul><li><code>message</code>：你想要发送的数据。</li><li><code>targetOrigin</code>：指定目标窗口的源，例如 “<code>https://example.com</code>“。这是一种安全机制，用来确保消息只被预期的接收者接收。如果你不关心目标窗口的源，也可以使用 “*” 作为通配符。</li></ul><p>**接收消息：**在目标窗口添加一个事件监听器来监听 <code>message</code> 事件</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&quot;message&quot;</span>, <span class="function">(<span class="params">event</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// 可以通过 event.origin 检查消息来源的源</span></span><br><span class="line">    <span class="keyword">if</span> (event.<span class="property">origin</span> !== <span class="string">&quot;&lt;http://expected-origin.com&gt;&quot;</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span>; <span class="comment">// 忽略来自不期望源的消息</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 处理接收到的数据</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(event.<span class="property">data</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在本项目中，<code>composition</code>主应用的<code>JS</code>脚本中注册了<code>message</code>事件监听，将基座应用接收到的消息转发给所有子应用。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">sendReceivedMessageToAllIframes</span> = (<span class="params">event</span>) =&gt; &#123;</span><br><span class="line">  <span class="variable language_">document</span></span><br><span class="line">    .<span class="title function_">querySelectorAll</span>(<span class="string">&#x27;iframe&#x27;</span>)</span><br><span class="line">    .<span class="title function_">forEach</span>(<span class="function">(<span class="params">iframe</span>) =&gt;</span> iframe.<span class="property">contentWindow</span>.<span class="title function_">postMessage</span>(event.<span class="property">data</span>, <span class="string">&#x27;*&#x27;</span>));</span><br><span class="line">&#125;;</span><br><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, sendReceivedMessageToAllIframes);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在<code>catalog</code>子应用中，注册了一个全局事件总线，并且给按钮注册了点击事件，当按钮点击后，会触发<code>productToCart</code>事件，在该事件中，会向主应用发送消息。</p><p>最终该消息会经过主应用，接着由<code>cart</code>子应用中的全局事件总线接收，执行<code>add</code>方法。</p><p>大致的交互示意图如下所示：</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/009cb022-bc62-4374-9163-d687c21e83ee/Untitled.png" alt="Untitled"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>一个 <code>iframe</code> 具备四层能力：文档的加载能力、HTML 的渲染能力、独立执行 JavaScript 的能力、隔离样式的能力。这些能力，其实也是微前端项目设计的核心要求。通过学习本项目，我们可以很好地初步学习微前端，为后来深入学习Single-SPA、qiankun等框架提供理论基础。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/04/01/%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84Demo%E4%BA%86%E8%A7%A3Iframe%E5%BE%AE%E5%89%8D%E7%AB%AF/</id>
    <link href="https://believed-breadfruit.top/2024/04/01/%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84Demo%E4%BA%86%E8%A7%A3Iframe%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    <published>2024-04-01T13:41:25.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>微前端是微服务概念在前端的应用。在微前端架构下，一个大型单一的前端应用被拆分成多个小型、独立的子应用，这些子应用可以独立开发、独立部署、独立]]>
    </summary>
    <title>一个简单的Demo了解Iframe微前端</title>
    <updated>2024-12-22T06:31:32.148Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="SVG" scheme="https://believed-breadfruit.top/tags/SVG/"/>
    <content>
      <![CDATA[<p>在SVG元素中，**<code>width</code><strong>和</strong><code>height</code><strong>属性以及</strong><code>viewBox</code>**属性中的宽度（width）和高度（height）都可以影响SVG的布局，前者宽高和普通元素的宽高类似，比较好理解，但是viewBox中的宽高不好理解，本文会将viewBox属性讲清楚。</p><h2 id="width-和-height-属性"><a href="#width-和-height-属性" class="headerlink" title="width 和 height 属性"></a><strong><code>width</code> 和 <code>height</code> 属性</strong></h2><p>我们先来理解简单的<code>width</code>和<code>height</code>属性，用法如下：</p><p><a href="https://codepen.io/janice143/pen/dyrxvjO">https://codepen.io/janice143/pen/dyrxvjO</a></p><p>在这段代码中，我们通过css的<code>background</code>属性建立了一个间距为10px的网格坐标系统，左上角起始点为原点（0,0)，向右和向下分别为横向和纵向的正方向。</p><p>所以svg元素的<code>width</code>和<code>height</code>属性很好理解，这两个属性确定了元素相对于整个画布的<strong>绝对大小</strong>，里面的<strong>元素肯定局限在这两个属性限定的范围内</strong>。</p><p>在这个限定范围内，我们要对svg内的元素进行缩放，或者移动内部元素怎么办？这个时候就要用到<code>viewBox</code>属性。</p><h2 id="viewBox属性"><a href="#viewBox属性" class="headerlink" title="viewBox属性"></a><code>viewBox</code>属性</h2><p>viewBox也是一个容器，有位置和大小。viewBox属性的用法如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">viewBox=<span class="string">&quot;x y width height&quot;</span></span><br><span class="line"></span><br><span class="line">or</span><br><span class="line"></span><br><span class="line">viewBox=<span class="string">&quot;x,y,width,height&quot;</span></span><br></pre></td></tr></table></figure><p>该属性内部有4个属性，可以用空格或者逗号进行分割。</p><ul><li><code>x</code>, <code>y</code> 表示viewBox这个容器的左上角位置坐标（相对于svg元素的绝对位置）</li><li><code>width</code>, <code>height</code>表示viewBox这个容器的宽高</li></ul><p>请看下面这里例子：</p><p><a href="https://codepen.io/janice143/pen/dyrxvjO">https://codepen.io/janice143/pen/dyrxvjO</a></p><p>在这里例子中，我们限定了一个200 <em>200 的区域，整个svg元素在这块区域内显示。svg内部的元素rect的宽高为100</em> 100。</p><p>可以看到左右两个设置了viewBox和没有设置的差异。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/d9fc530e-4cdf-42f6-8497-e50599d9c6bc/Untitled.png" alt="Untitled"></p><p>在这个图的下面，我们又以viewBox的视角，画出了viewBox这个容器中元素的展示</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/4f309c00-776c-4339-9093-1b117a7ce3cb/Untitled.png" alt="Untitled"></p><p>可以发现，在viewBox视角下，元素的展示和限定在<code>width</code>和<code>height</code>内的展示一模一样，唯一的差别是前者进行另一个缩放，需要完全限制在<code>width</code>和<code>height</code>内部。</p><p>这就是viewBox的作用。</p><h2 id="理解viewBox的SOP"><a href="#理解viewBox的SOP" class="headerlink" title="理解viewBox的SOP"></a>理解viewBox的SOP</h2><p>所以我们可以这样理解svg的viewBox属性：</p><ol><li>根据viewBox的四个值确定viewBox的大小</li><li>根据svg内部元素的宽高确定其在viewBox中的位置</li><li>最后把上面二者结合的图像缩放到svg元素的宽高内</li></ol><h2 id="举一反三"><a href="#举一反三" class="headerlink" title="举一反三"></a>举一反三</h2><p>我们在举一些例子来继续理解viewBox。</p><h3 id="1-案例1"><a href="#1-案例1" class="headerlink" title="1. 案例1"></a>1. 案例1</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;100&quot;</span> </span><br><span class="line">  height=<span class="string">&quot;100&quot;</span> </span><br><span class="line">  viewBox=<span class="string">&quot;0 0 200 200&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;100&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;100&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><h3 id="2-案例2"><a href="#2-案例2" class="headerlink" title="2. 案例2"></a>2. 案例2</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  height=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  viewBox=<span class="string">&quot;0 0 200 200&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;100&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;100&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><h3 id="3-案例3"><a href="#3-案例3" class="headerlink" title="3. 案例3"></a>3. 案例3</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  height=<span class="string">&quot;200&quot;</span> </span><br><span class="line">  viewBox=<span class="string">&quot;0 0 100 100&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;100&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;100&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><h3 id="4-案例4"><a href="#4-案例4" class="headerlink" title="4. 案例4"></a>4. 案例4</h3><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&lt;svg </span><br><span class="line">  width=<span class="string">&quot;200&quot;</span></span><br><span class="line">  height=<span class="string">&quot;200&quot;</span></span><br><span class="line">  viewBox=<span class="string">&quot;-100 -100 200 200&quot;</span></span><br><span class="line">&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">circle</span> <span class="attr">cx</span>=<span class="string">&quot;0&quot;</span> <span class="attr">cy</span>=<span class="string">&quot;0&quot;</span> <span class="attr">r</span>=<span class="string">&quot;50&quot;</span> /&gt;</span></span></span><br><span class="line">&lt;/svg&gt;</span><br></pre></td></tr></table></figure><p><a href="https://codepen.io/janice143/pen/dyrxvjO?editors=1100">https://codepen.io/janice143/pen/dyrxvjO?editors=1100</a></p><p>上面四个案例中，我们使用<code>**蓝色底+紫色元素**</code>画出了viewBox容器中的元素呈现，可以看到这个相对元素展示就是元素在绝对位置上的展示。</p><p>按照上述给出的理解步骤，我们很容易还原svg的图像展示。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文通过一些案例说明了svg元素中width、height 和viewBox属性的理解，其中，**<code>width</code><strong>和</strong><code>height</code><strong>属性控制SVG的物理尺寸，而</strong><code>viewBox</code>**属性则定义了一个内部的视图框架，允许内容在保持比例的前提下缩放以适应不同的物理尺寸。</p><ul><li><strong>无<code>viewBox</code>时</strong>：SVG的显示大小严格遵循**<code>width</code><strong>和</strong><code>height</code>**属性。图形的大小不会自动适应容器的大小变化。</li><li><strong>有<code>viewBox</code>时</strong>：SVG内部的图形（在这个例子中是蓝色的正方形）会被缩放以适应指定的物理尺寸（200x200像素），而不失去比例或变形。这是因为**<code>viewBox</code>**提供了一种机制，允许SVG内容在不同尺寸的容器中灵活显示，同时保持其内部图形的正确比例和定位。</li></ul>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/02/26/Understanding-ViewBox-in-SVG/</id>
    <link href="https://believed-breadfruit.top/2024/02/26/Understanding-ViewBox-in-SVG/"/>
    <published>2024-02-26T13:42:09.000Z</published>
    <summary>
      <![CDATA[<p>在SVG元素中，**<code>width</code><strong>和</strong><code>height</code><strong>属性以及</strong><code>viewBox</code>**属性中的宽度（width）和高度（height）都可以影响]]>
    </summary>
    <title>Understanding ViewBox in SVG</title>
    <updated>2024-12-22T06:31:11.653Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="数据结构与算法" scheme="https://believed-breadfruit.top/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"/>
    <category term="算法" scheme="https://believed-breadfruit.top/tags/%E7%AE%97%E6%B3%95/"/>
    <content>
      <![CDATA[<h2 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h2><p>如题：</p><p>给定两个由小写字母组成的字符串 <code>s1</code> 和 <code>s2</code>，请编写一个程序，确定其中一个字符串的字符重新排列后，能否变成另一个字符串。</p><p><strong>示例 1：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入:s1 = &quot;abc&quot;,s2 = &quot;bca&quot;</span><br><span class="line">输出: true</span><br></pre></td></tr></table></figure><p><strong>示例 2：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入:s1 = &quot;abc&quot;,s2 = &quot;bad&quot;</span><br><span class="line">输出: false</span><br></pre></td></tr></table></figure><p>该题的解题思路很多，如数组计数、哈希集合、排序等。本文讲解一种<strong>巧用数学公式</strong>的解法。</p><h2 id="有趣的推理"><a href="#有趣的推理" class="headerlink" title="有趣的推理"></a>有趣的推理</h2><p>首先，我们来推导一个公式，已知以下两个等式：</p><p>$$<br>A+B&#x3D;X+Y\<br>A<em>B&#x3D;X</em>Y<br>$$</p><p>其中A,B, X, Y不为0。我们的目标是证明：A，B和X，Y两两相等，即$A&#x3D;X,B&#x3D;Y$ 或者$A&#x3D;Y,B&#x3D;X$。</p><p>推导过程如下：</p><p>对已知的第一个等式进行移向，可以得到</p><p>$$<br>B&#x3D;Y−A+X<br>$$</p><p>将B带入到第二个等式，可得</p><p>$$<br>A(Y−A+X) &#x3D; XY<br>$$</p><p>经过代数运算，可得</p><p>$$<br>AY-A^2+AX &#x3D; XY\<br>A(X-A) &#x3D; (X-A)Y\<br>$$</p><p>所以可得</p><p>$$<br>X&#x3D;A, Y&#x3D;B<br>$$</p><p>或者</p><p>$$<br>Y&#x3D;A, X&#x3D;B<br>$$</p><p>证明完毕。</p><p>$A&#x3D;X,B&#x3D;Y$ 或者$A&#x3D;Y,B&#x3D;X$，这意味着A,B 和X,Y这两对数字是全排列组合，数字都是相同的，只是打乱的顺序。</p><p>有了这个推理，根据加法和乘法的交换律和结合律，我们可以继续增加变量，得出这么一个结论：</p><p>若A1 + A2 + A3 + … +An &#x3D; B1 + B2+ B3 + … + Bn，那么A1到An和B1到Bn肯定一一对应，即是全排列组合。</p><h2 id="字符串排列组合"><a href="#字符串排列组合" class="headerlink" title="字符串排列组合"></a>字符串排列组合</h2><p>知道了上述的推理，和我们的题目有什么关系呢？</p><p>题目是要我们证明两个字符串是否是全排列组合，我们是否可以利用刚才得到的全排列性质来解题呢？</p><p>答案是可以的，思路是这样的：<strong>如果我们把每个字符都看成一个不重合的数字，通过遍历字符串，对字符映射成的数字进行A+B和A*B的计算，只要两个字符串得出的结果一样，就说明这两个字符串互为全排列组合，即其中一个字符串的字符重新排列后是另一个字符串</strong>。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s1</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s2</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> &#123;<span class="type">boolean</span>&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> <span class="title class_">CheckPermutation</span> = <span class="keyword">function</span>(<span class="params">s1, s2</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span>(s1.<span class="property">length</span> !== s2.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    <span class="keyword">const</span> [s11, s22] =[s1.<span class="title function_">toLowerCase</span>(),s2.<span class="title function_">toLowerCase</span>()]</span><br><span class="line">    <span class="keyword">let</span> sum1 = <span class="number">0</span>, sum2 = <span class="number">0</span>, multi1 = <span class="number">1</span>, multi2 = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>;i&lt;s11.<span class="property">length</span>;i++)&#123;</span><br><span class="line">        sum1 += s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line">        sum2 += s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line"></span><br><span class="line">        multi1 *= s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line">        multi2 *= s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)+<span class="number">1</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> sum1 === sum2 &amp;&amp; multi1 === multi2</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// case1: s1=&quot;abc&quot;, s2=&quot;bca&quot;, 输出true，预期true</span></span><br><span class="line"><span class="comment">// case2: s1=&quot;abc&quot;, s2=&quot;bad&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case3: s1=&quot;aac&quot;, s2=&quot;bbb&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case4: s1=&quot;abbcdde&quot;, s2=&quot;abcccde&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case5: s1=&quot;bkhfhqlayvlhdqmxvnkqvtkojouugfsnwmyoywkilsnubnkvhdbrltuxvoblurpfinpigajttcvkcxlylblcaocsjmwdvwepvnfr&quot;, </span></span><br><span class="line"><span class="comment">// s2=&quot;mtycyvobjldulmhsuqvtrhqnisjkuxhvaxqkvpbllnkvvakxjbolefpyrtiivvwctunasbbocldflkcknmwgofngorduwlwhyfnp&quot; false</span></span><br><span class="line"><span class="comment">// 输出false，预期true</span></span><br></pre></td></tr></table></figure><p>可以看到，这个算法在执行第5个case的时候出错了，这是因为字符串的长度很长，我们的字符映射是从1-26，相乘很容易遇到溢出的问题，导致计算不准确。</p><p>但是好消息是上述解法证明我们的推理没有问题，思路没有问题，接下来我们思考如何解决计算溢出导致失去精度问题。</p><h2 id="位运算：解决溢出"><a href="#位运算：解决溢出" class="headerlink" title="位运算：解决溢出"></a>位运算：解决溢出</h2><p>我们引入位向量，什么是位向量呢？</p><p><strong>位向量（bit vector）</strong>，是一个仅包含0和1的数组或者序列，其中每一位都代表某个元素的状态或属性。例如，1表示存在，0表示不存在。<strong>位向量</strong>表示方法非常高效，可以在固定大小的数组中存储大量的信息，而且位向量可以适合进行位运算，可以提高计算效率。</p><p>例如我们可以用以下位向量表示0-5这几个数字：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">0</span> --&gt; <span class="number">1</span></span><br><span class="line"><span class="number">1</span> --&gt; <span class="number">10</span></span><br><span class="line"><span class="number">2</span> --&gt; <span class="number">100</span></span><br><span class="line"><span class="number">3</span> --&gt; <span class="number">1000</span></span><br><span class="line"><span class="number">4</span> --&gt; <span class="number">10000</span></span><br><span class="line"><span class="number">5</span> --&gt; <span class="number">100000</span></span><br></pre></td></tr></table></figure><p>数字和位向量的关系为：数字表示位向量中为1的位置（从右往左看）。值得注意的是**，位向量的创建需要自己赋予含义，并不是固定的。**</p><p>由于数字和字母之间可以通过ASCII码联系起来，例如a对应97，b对应98，c对应99，所以我们也可以约定一些位向量来表示字母，例如</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">a --&gt; <span class="number">0</span> --&gt; <span class="number">1</span></span><br><span class="line">b --&gt; <span class="number">1</span> --&gt; <span class="number">10</span></span><br><span class="line">c --&gt; <span class="number">2</span> --&gt; <span class="number">100</span></span><br><span class="line">...</span><br><span class="line">y --&gt; <span class="number">24</span> --&gt; <span class="number">1000000000000000000000000</span></span><br><span class="line">z --&gt; <span class="number">25</span> --&gt; <span class="number">10000000000000000000000000</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以发现此处我们做了相对处理，把a看成0，而不是97，这样可以<strong>节省空间</strong>，只需要26位就可以表示26个字母。</p><p>利用**左移运算，**可以创建上述位向量：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span> &lt;&lt; <span class="number">0</span> = <span class="number">1</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; <span class="number">1</span> = <span class="number">10</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; <span class="number">2</span> = <span class="number">100</span></span><br><span class="line"><span class="number">1</span>(十进制） &lt;&lt; <span class="number">3</span>(十进制） = <span class="number">1000</span>(二进制）</span><br><span class="line"></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">1</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;b&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">10</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;c&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">100</span></span><br><span class="line"><span class="number">1</span> &lt;&lt; (<span class="string">&#x27;d&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>)) = <span class="number">1000</span></span><br></pre></td></tr></table></figure><p>加法和乘法有这样的规律：</p><ul><li>交换律</li><li>任何数字和0 相加，等与自身</li><li>任何数字和1相乘，等于自身</li></ul><p>在位运算中，和加法和乘法规律非常相似的运算是位运算的**加法和异或。**加法比较简单，任何进制的加法都是一样的规律，而且相等，也就是二进制的x肯定等于n进制的y。我们重点来看看异或。</p><p>异或的运算逻辑如下：</p><p>1 ^ 1 &#x3D; 0</p><p>1 ^ 0 &#x3D; 1</p><p>0 ^ 1 &#x3D; 1</p><p>0 ^ 0 &#x3D; 0</p><p>简单来说，异或的特性是，两个值相同，结果为0，不同则结果为1。异或运算具有一定定律：</p><ol><li>任何值与自身异或，结果为0</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ x = 0</span><br></pre></td></tr></table></figure><ol><li>任何值与0异或，结果为其自身</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ 0 = x</span><br></pre></td></tr></table></figure><ol><li>交换律</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ y ^ z = x ^ z ^ y</span><br></pre></td></tr></table></figure><ol><li>结合律</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">x ^ y ^ z = x ^ (y ^ z)</span><br></pre></td></tr></table></figure><p>除了第一条，其他都和乘法非常类似。所以我们用位运算来改进我们上述算法。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s1</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> &#123;<span class="type">string</span>&#125; <span class="variable">s2</span></span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> &#123;<span class="type">boolean</span>&#125;</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">var</span> <span class="title class_">CheckPermutation</span> = <span class="keyword">function</span>(<span class="params">s1, s2</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span>(s1.<span class="property">length</span> !== s2.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    <span class="keyword">const</span> [s11, s22] =[s1.<span class="title function_">toLowerCase</span>(),s2.<span class="title function_">toLowerCase</span>()]</span><br><span class="line">  <span class="keyword">let</span> [bitSum1, bitSum2] = [<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line">  <span class="keyword">let</span> [bitMulti1, bitMulti2] = [<span class="number">0</span>,<span class="number">0</span>]</span><br><span class="line">  <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>;i&lt;s11.<span class="property">length</span>;i++)&#123;</span><br><span class="line">    bitSum1 += <span class="number">1</span> &lt;&lt; (s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line">    bitSum2 += <span class="number">1</span> &lt;&lt; (s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 异或的两条规律：交换率和任何值与自身异或，结果为0</span></span><br><span class="line">    bitMulti1 ^= <span class="number">1</span> &lt;&lt; (s11[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line">    bitMulti2 ^= <span class="number">1</span> &lt;&lt; (s22[i].<span class="title function_">charCodeAt</span>(<span class="number">0</span>)-<span class="string">&#x27;a&#x27;</span>.<span class="title function_">charCodeAt</span>(<span class="number">0</span>))</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> bitSum1 === bitSum2 &amp;&amp; bitMulti1 === bitMulti2</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// case1: s1=&quot;abc&quot;, s2=&quot;bca&quot;, 输出true，预期true</span></span><br><span class="line"><span class="comment">// case2: s1=&quot;abc&quot;, s2=&quot;bad&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case3: s1=&quot;aac&quot;, s2=&quot;bbb&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case4: s1=&quot;abbcdde&quot;, s2=&quot;abcccde&quot;, 输出false，预期false</span></span><br><span class="line"><span class="comment">// case5: s1=&quot;bkhfhqlayvlhdqmxvnkqvtkojouugfsnwmyoywkilsnubnkvhdbrltuxvoblurpfinpigajttcvkcxlylblcaocsjmwdvwepvnfr&quot;, </span></span><br><span class="line"><span class="comment">// s2=&quot;mtycyvobjldulmhsuqvtrhqnisjkuxhvaxqkvpbllnkvvakxjbolefpyrtiivvwctunasbbocldflkcknmwgofngorduwlwhyfnp&quot; false</span></span><br><span class="line"><span class="comment">// 输出true，预期true</span></span><br></pre></td></tr></table></figure><h2 id="算法复杂度分析"><a href="#算法复杂度分析" class="headerlink" title="算法复杂度分析"></a>算法复杂度分析</h2><h3 id="时间复杂度分析"><a href="#时间复杂度分析" class="headerlink" title="时间复杂度分析"></a>时间复杂度分析</h3><ol><li>字符串长度为 <em>n</em>，因此循环的次数是 <em>O</em>(<em>n</em>)。</li><li>在循环内部，主要执行了一些位运算和常数时间的操作，这些操作的时间复杂度可以视为 <em>O</em>(1)。</li><li>因此，整个算法的时间复杂度为 <em>O</em>(<em>n</em>)。</li></ol><h3 id="空间复杂度分析"><a href="#空间复杂度分析" class="headerlink" title="空间复杂度分析"></a>空间复杂度分析</h3><ol><li>使用了一些常量空间变量（例如，<code>bitSum1</code>、<code>bitSum2</code>、<code>bitMulti1</code>、<code>bitMulti2</code>）和循环变量（例如，<code>i</code>）。</li><li>不随输入规模 <em><code>n</code></em> 的增长而变化，因此是 <em>O</em>(1) 的空间复杂度。</li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ul><li>时间复杂度：<em>O</em>(<em>n</em>)</li><li>空间复杂度：<em>O</em>(1)</li></ul><p>总的来讲，这个算法基于一则数学推理，采用了一种巧妙的位运算方法来判断两个字符串是否为排列。相比于其他解题思路，例如排序，暴力解法，等，本文章的解题思路更加高效。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2024/01/15/%E5%88%A9%E7%94%A8%E2%80%9D%E8%8B%A5A-B-X-Y-A-B-X-Y%EF%BC%8C%E9%82%A3%E4%B9%88A-X-B-Y%E6%88%96A-Y-B-X%E2%80%9C%E5%88%A4%E6%96%AD%E4%B8%A4%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF%E5%90%A6%E4%BA%92%E4%B8%BA%E6%8E%92%E5%88%97%E7%BB%84%E5%90%88/</id>
    <link href="https://believed-breadfruit.top/2024/01/15/%E5%88%A9%E7%94%A8%E2%80%9D%E8%8B%A5A-B-X-Y-A-B-X-Y%EF%BC%8C%E9%82%A3%E4%B9%88A-X-B-Y%E6%88%96A-Y-B-X%E2%80%9C%E5%88%A4%E6%96%AD%E4%B8%A4%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF%E5%90%A6%E4%BA%92%E4%B8%BA%E6%8E%92%E5%88%97%E7%BB%84%E5%90%88/"/>
    <published>2024-01-15T13:43:04.000Z</published>
    <summary>
      <![CDATA[<h2 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h2><p>如题：</p>
<p>给定两个由小写字母组成的字符串 <code>s1</code> 和 <code>s2</code>，请编写一个程序，确]]>
    </summary>
    <title>利用”若A+B=X+Y, A*B=X*Y，那么A=X, B=Y或A=Y,B=X“判断两字符串是否互为排列组合</title>
    <updated>2024-04-22T17:22:13.405Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Git" scheme="https://believed-breadfruit.top/tags/Git/"/>
    <category term="Husky" scheme="https://believed-breadfruit.top/tags/Husky/"/>
    <content>
      <![CDATA[<p>在了解husky之前，先了解一下Git hooks。</p><h2 id="GitHooks"><a href="#GitHooks" class="headerlink" title="GitHooks"></a>GitHooks</h2><p>官方文档：<a href="https://git-scm.com/docs/githooks">https://git-scm.com/docs/githooks</a></p><h3 id="是什么"><a href="#是什么" class="headerlink" title="是什么"></a>是什么</h3><p>git hooks 是git提供的hooks，类似Vue的生命周期钩子函数一样，Git也会在它运行周期里面的某些时间点，提供一些让用户添加和执行自定义的函数。</p><p>默认情况下，hooks 目录是 <code>$GIT_DIR/hooks</code> ，但可以通过 <code>core.hooksPath</code> 配置变量进行更改。打开之后能看到很多<code>Hooks</code>的<code>sample</code>。</p><p><a href="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f80e778aa94147f2acbaefa8b5fb5bce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp">https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f80e778aa94147f2acbaefa8b5fb5bce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp</a></p><p><code>Simple</code>文件的内容如下：</p><p><a href="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55484a649af74601b493eeffa8a31bd0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp">https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55484a649af74601b493eeffa8a31bd0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp</a></p><p>可以发现，Githooks文件其实是一个<code>shell</code>脚本，<strong>可以通过修改<code>shell</code>脚本来实现自定义功能</strong>。</p><h3 id="常用的hooks"><a href="#常用的hooks" class="headerlink" title="常用的hooks"></a>常用的hooks</h3><p><strong>pre-commit 预提交</strong></p><p>该钩子由 <code>git-commit</code>调用，可以使用 <code>--no-verify</code> 选项绕过。它不带任何参数，在提交之前调用。从此脚本中<strong>以非零状态退出</strong>会导致 <code>git commit</code> 命令在创建提交前中止。</p><p><strong>prepare-commit-msg 准备-提交-消息</strong></p><p>在准备好默认日志消息之后、启动编辑器之前，git-commit[1] 会立即调用此挂钩。</p><p>它需要一到三个参数。</p><ul><li>第一个是包含提交日志消息的文件的名称。</li><li>第二个是提交消息的来源，可以是： <code>message</code> （如果给出了 <code>-m</code> 或 <code>-F</code> 选项）； <code>template</code> （如果给出了 <code>-t</code> 选项或设置了配置选项 <code>commit.template</code> ）； <code>merge</code> （如果提交是合并或存在 <code>.git/MERGE_MSG</code> 文件）； <code>squash</code> （如果 <code>.git/SQUASH_MSG</code> 文件存在）；或 <code>commit</code> ，后跟提交对象名称（如果给出了 <code>-c</code> 、 <code>-C</code> 或 <code>--amend</code> 选项）。</li></ul><p>如果退出状态非零， <code>git commit</code> 将中止。</p><h3 id="Githooks能解决什么"><a href="#Githooks能解决什么" class="headerlink" title="Githooks能解决什么"></a>Githooks能解决什么</h3><p>回到项目中的实际问题</p><ul><li>问题：远程仓库的代码中存在语法错误</li><li>原因：有成员没有仔细检查，误把有语法错误的代码上传了</li><li>解决方法：在<code>commit之前</code>检查在暂存区的代码，看是否存在语法错误 显然，如果只靠当前成员去检查自己添加到暂存区的代码有没有错误，是很不可靠且低效的行为，那么这里我们可以用到<code>pre-commit</code>这一个hook了，它可以在git commit之前执行我们自定义的语句，当<code>执行错误</code>或者<code>返回非0状态</code>的时候阻止commit行为。</li></ul><p>除了这个Hook以外，还有哪些就不一一介绍了，一般的Git操作，例如commit、push等会提供操作前或操作后的Hook，有兴趣的可以查看<a href="https://link.juejin.cn/?target=https://git-scm.com/docs/Githooks">Githooks</a>去了解。</p><h1 id="Husky"><a href="#Husky" class="headerlink" title="Husky"></a>Husky</h1><p>git hooks和husky有什么关系呢？用一句话概括，那就是husky能够简化上述创建或者修改Githooks的操作流程。</p><h3 id="husky工作原理"><a href="#husky工作原理" class="headerlink" title="husky工作原理"></a>husky工作原理</h3><ul><li>初始化：先检查该项目是否通过Git来托管代码的</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="title function_">spawnSync</span>(<span class="string">&#x27;git&#x27;</span>, [<span class="string">&#x27;rev-parse&#x27;</span>]).<span class="property">status</span> !== <span class="number">0</span>) &#123;</span><br><span class="line">  <span class="title function_">l</span>(<span class="string">&#x27;not a Git repository, skipping hooks installation&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>创建<code>.husky</code>文件夹：创建<code>.husky</code>文件夹用来存放<code>Githooks</code>和<code>husky</code>的配置</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建 .husky/_</span></span><br><span class="line"><span class="title function_">mkdirSync</span>(<span class="title function_">join</span>(dir, <span class="string">&#x27;_&#x27;</span>), &#123; <span class="attr">recursive</span>: <span class="literal">true</span> &#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 创建 .husky/.gitignore</span></span><br><span class="line"><span class="title function_">writeFileSync</span>(<span class="title function_">join</span>(dir, <span class="string">&#x27;.gitignore&#x27;</span>), <span class="string">&#x27;_\n&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 将husky.sh 复制到 .husky/_/husky.sh</span></span><br><span class="line"><span class="title function_">copyFileSync</span>(</span><br><span class="line">  <span class="title function_">fileURLToPath</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">&#x27;./husky.sh&#x27;</span>, <span class="keyword">import</span>.<span class="property">meta</span>.<span class="property">url</span>)),</span><br><span class="line">  <span class="title function_">join</span>(dir, <span class="string">&#x27;_/husky.sh&#x27;</span>)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul><li>修改<code>git hooks</code>路径：通过<code>core.hooksPath</code>使项目<code>Githooks</code>的路径，指向新创建的<code>.husky</code>文件夹</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; error &#125; = <span class="title function_">spawnSync</span>(<span class="string">&#x27;git&#x27;</span>, [<span class="string">&#x27;config&#x27;</span>, <span class="string">&#x27;core.hooksPath&#x27;</span>, dir]);</span><br><span class="line"><span class="keyword">if</span> (error) &#123;</span><br><span class="line">  <span class="keyword">throw</span> error;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>初始化之后，可以通过<code>add</code>命令来进行创建或者往文件后面增加语句</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="title function_">existsSync</span>(file)) &#123;</span><br><span class="line">  <span class="title function_">appendFileSync</span>(file, <span class="string">`<span class="subst">$&#123;cmd&#125;</span>\n`</span>);</span><br><span class="line">  <span class="title function_">l</span>(<span class="string">`updated <span class="subst">$&#123;file&#125;</span>`</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="title function_">set</span>(file, cmd);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="如何使用Husky"><a href="#如何使用Husky" class="headerlink" title="如何使用Husky"></a>如何使用Husky</h3><p>为了更好的演示这个功能，我们来做一个小Demo。</p><p>需求如下：</p><p>在代码commit之前，检查暂存区的代码的语法错误。</p><p>解决方案：</p><p><code>pre-commit hook</code>+<code>husky</code>。它可以在git commit之前执行我们自定义的语句，当<code>执行错误</code>或者<code>返回非0状态</code>的时候阻止<code>commit</code>行为。</p><ul><li><code>npm</code>初始化：安装<code>eslint</code>、<code>lint-staged</code>和<code>husky</code></li></ul><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install <span class="attr">--save-dev</span> eslint lint-staged husky</span><br></pre></td></tr></table></figure><ul><li><code>eslint</code>初始化</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm init @eslint/config</span><br></pre></td></tr></table></figure><ul><li><code>husky</code>初始化</li></ul><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky install</span><br></pre></td></tr></table></figure><p>这时候我们就看到了.husky的文件夹</p><ul><li>添加<code>lintstagedrc.js</code>：用来配置暂存区文件相对应的<code>lint</code></li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="string">&#x27;*.js&#x27;</span>: [<span class="string">&#x27;eslint&#x27;</span>]</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>使用husky add添加一条检查命令：</li></ul><p>创建一个<code>pre-commit hook</code>，用来对暂存区的文件进行检查</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky <span class="keyword">add</span> .husky<span class="operator">/</span>pre<span class="operator">-</span><span class="keyword">commit</span> &quot;npx lint-staged -c .husky/lintstagedrc.js&quot;</span><br></pre></td></tr></table></figure><ul><li>在项目中新增一个js，故意写上语法错误</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">const testA = &#123;</span><br><span class="line">    console.log(testA);</span><br></pre></td></tr></table></figure><ul><li>尝试提交代码，得到以下报错</li></ul><p><a href="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3facc870d17141838aec9bbd2a55cc7d~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp">https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3facc870d17141838aec9bbd2a55cc7d~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp</a></p><ul><li>把语法错误修复之后再<code>commit</code>，这时候就可以顺利通过检测且commit了，这样我们的目的就达成了</li></ul>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/12/19/%E4%B8%80%E6%96%87%E4%BA%86%E8%A7%A3Husky/</id>
    <link href="https://believed-breadfruit.top/2023/12/19/%E4%B8%80%E6%96%87%E4%BA%86%E8%A7%A3Husky/"/>
    <published>2023-12-19T13:44:10.000Z</published>
    <summary>
      <![CDATA[<p>在了解husky之前，先了解一下Git hooks。</p>
<h2 id="GitHooks"><a href="#GitHooks" class="headerlink" title="GitHooks"></a>GitHooks</h2><p>官方文档：<a href]]>
    </summary>
    <title>一文了解Husky</title>
    <updated>2024-12-22T06:31:36.092Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="Canvas" scheme="https://believed-breadfruit.top/tags/Canvas/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>最近在业务上遇到了一个问题是要将页面打印pdf文件，产品的预期是希望点击一个按钮，就能够将页面数据写在一个pdf上，并下载下来，需要保证pdf的内容具有很好的可读性。</p><p>评估下来这个需求的本质是要实现一个能够将HTML页面转为PDF并实现下载的功能。通过技术调研，我最终的方案是采购<code>html2canvas + jspdf</code>这两个库来实现，通过使用html2canvas将使用canvas将页面转为base64图片流，然后将其插入jspdf插件中，实现保存并下载pdf。</p><p>这个方案其实也是目前比较常用的一个方案，但是在实践过程中，遇到的最大问题就是分页截断的问题。当html页面超过一页A4纸的时候，页面就是出现被截断，影响了pdf的可读性。</p><p>由于网上关于分页截断的解决思路比较少，所以特意将此次的解决方案记录下来，也作为一次个人的技术成长实践。</p><h2 id="使用-JSPDF-和-html2canvas-创建简单的-PDF文件"><a href="#使用-JSPDF-和-html2canvas-创建简单的-PDF文件" class="headerlink" title="使用 JSPDF 和 html2canvas 创建简单的 PDF文件"></a><strong>使用 JSPDF 和 html2canvas 创建简单的 PDF文件</strong></h2><p>首先，我们开始使用 JSPDF 和 html2canvas 创建一个简单的 PDF。</p><h3 id="创建一个-JSPDF-实例"><a href="#创建一个-JSPDF-实例" class="headerlink" title="创建一个 JSPDF 实例"></a>创建一个 JSPDF 实例</h3><p>创建一个 JSPDF 实例，设置页面的大小、方向和其他参数。参考官网可以写一个很简单的实例：<a href="https://artskydj.github.io/jsPDF/docs/index.html">https://artskydj.github.io/jsPDF/docs/index.html</a></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> doc = <span class="keyword">new</span> <span class="title function_">jsPDF</span>(&#123;</span><br><span class="line">  <span class="attr">orientation</span>: <span class="string">&#x27;landscape&#x27;</span>,</span><br><span class="line">  <span class="attr">unit</span>: <span class="string">&#x27;in&#x27;</span>,</span><br><span class="line">  <span class="attr">format</span>: [<span class="number">4</span>, <span class="number">2</span>]</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">doc.<span class="title function_">text</span>(<span class="string">&#x27;Hello world!&#x27;</span>, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">doc.<span class="title function_">save</span>(<span class="string">&#x27;two-by-four.pdf&#x27;</span>)</span><br></pre></td></tr></table></figure><p>生成一个pdf文件，并且在文件中写入一定内容，其实<strong>JSPDF</strong> 这个库就能做到。</p><p>但是很多场景下，我们的目标pdf中的内容会更复杂，而且还要考虑排版，所以最好的方式是引入<strong>html2canvas</strong>这个库，将页面元素转换成base64数据，然后贴在pdf中(使用<a href="https://artskydj.github.io/jsPDF/docs/module-addImage.html">addImage方法</a>），这样就能保证页面的内容。引入了<strong>html2canvas</strong>库后，我们更多关注就是利用现成组件库、或者原生html和css实现更复杂的页面内容。</p><h3 id="引入-html2canvas"><a href="#引入-html2canvas" class="headerlink" title="引入 html2canvas"></a>引入 html2canvas</h3><p>官方文档：<a href="https://html2canvas.hertzen.com/getting-started">https://html2canvas.hertzen.com/getting-started</a></p><p> 使用 html2canvas 捕捉 HTML 内容或特定的 HTML 元素，并将其转换为 Canvas。下面是一个简单的demo, 可以可以看到html2canvas能够将dom元素转化为一张base64图片，尝试选择页面元素可以感受到图片和文字的不同。</p><p><a href="https://codepen.io/janice143/pen/RwvpQbZ">https://codepen.io/janice143/pen/RwvpQbZ</a></p><h3 id="将html2canvas转化的图片放到pdf中"><a href="#将html2canvas转化的图片放到pdf中" class="headerlink" title="将html2canvas转化的图片放到pdf中"></a>将html2canvas转化的图片放到pdf中</h3><p>这一步我们需要使用JSPDF 的addImage方法，其语法如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">addImage</span>(imageData, format, x, y, width, height, alias, compression)</span><br></pre></td></tr></table></figure><ul><li>imageData - 要添加的图像数据。可以是图像的 URL、图像的 base64 编码字符串或图像的二进制数据</li><li>format - 图像的格式。可以是 “JPEG”、”PNG” 或 “TIFF”。</li><li>x - 图像在 PDF 文档中的 x 坐标。</li><li>y - 图像在 PDF 文档中的 y 坐标。</li><li>width - 图像的宽度。</li><li>height - 图像的高度。</li><li>alias - 图像的别名。此别名可用于在 PDF 文档中引用图像。</li><li>compression - 图像的压缩级别。可以是 “NONE”、”FAST” 或 “SLOW”。</li></ul><p>下面是一串示例代码：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> doc = <span class="keyword">new</span> <span class="title function_">jsPDF</span>();</span><br><span class="line">doc.<span class="title function_">addImage</span>(<span class="string">&#x27;image.jpg&#x27;</span>, <span class="string">&#x27;JPEG&#x27;</span>, <span class="number">10</span>, <span class="number">10</span>, <span class="number">100</span>, <span class="number">100</span>);</span><br><span class="line">doc.<span class="title function_">save</span>(<span class="string">&#x27;output.pdf&#x27;</span>);</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>此示例将在 PDF 文档的 (10, 10) 处添加一个名为 “image.jpg” 的 JPEG 图像。图像的宽度和高度将分别为 100 和 100 像素。</p><h3 id="JSPDF-和-html2canvas结合起来用"><a href="#JSPDF-和-html2canvas结合起来用" class="headerlink" title="JSPDF 和 html2canvas结合起来用"></a><strong>JSPDF 和 html2canvas结合起来用</strong></h3><p>知道上面的关键三个点，接下来我们将这三个步骤串联起来，实现一个基本的html→pdf的方案。大致步骤如下：</p><ol><li>写一个基本html页面</li><li>创建jspdf实例</li><li>获取页面的dom节点，使用html2canvas将其转化为base64数据流</li><li>将base64数据流装载到jspdf的addImage方法中</li><li>保存pdf</li></ol><p>基于这5个步骤，可以实现基本的单页打印。</p><h2 id="多页：比例缩放-循环移位"><a href="#多页：比例缩放-循环移位" class="headerlink" title="多页：比例缩放+循环移位"></a>多页：比例缩放+循环移位</h2><p>但是，在你实践中，你会发现2个问题：</p><ul><li>页面的内容超出了pdf页面</li><li>打印的pdf只有一页，没有展示全部的html信息</li></ul><p>这篇博客给了一个非常好的解决方案，其原理是：</p><ul><li>通过比例缩放，实现等比展示在pdf中</li><li>通过循环迭代，不停移动base64的图片位置，也就是调整X,Y参数（x - 图像在 PDF 文档中的 x 坐标；y - 图像在 PDF 文档中的 y 坐标），移动使用jspdf的addPage方法新加一个pdf。</li></ul><p><a href="https://juejin.cn/post/7090368199291568165#heading-4">Vue前端实现HTML转PDF并导出 - 掘金</a></p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 当内容未超过pdf一页显示的范围，无需分页</span></span><br><span class="line"><span class="keyword">if</span> (leftHeight &lt; pageHeight) &#123;</span><br><span class="line">  <span class="comment">// addImage(pageData, &#x27;JPEG&#x27;, 左，上，宽度，高度)设置</span></span><br><span class="line">  <span class="variable constant_">PDF</span>.<span class="title function_">addImage</span>(pageData, <span class="string">&#x27;JPEG&#x27;</span>, <span class="number">0</span>, <span class="number">0</span>, imgWidth, imgHeight)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  <span class="comment">// 超过一页时，分页打印（每页高度841.89）</span></span><br><span class="line">  <span class="keyword">while</span> (leftHeight &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="variable constant_">PDF</span>.<span class="title function_">addImage</span>(pageData, <span class="string">&#x27;JPEG&#x27;</span>, <span class="number">0</span>, position, imgWidth, imgHeight)</span><br><span class="line">    leftHeight -= pageHeight</span><br><span class="line">    position -= <span class="number">841.89</span></span><br><span class="line">    <span class="keyword">if</span> (leftHeight &gt; <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="variable constant_">PDF</span>.<span class="title function_">addPage</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="分页截断的挑战"><a href="#分页截断的挑战" class="headerlink" title="分页截断的挑战"></a><strong>分页截断的挑战</strong></h2><p>尽管 JSPDF 和 html2canvas 是功能强大的工具，但是 他们也有很多槽点，比如得手动分页，手动处理分页截断的问题。等你实践到这一步，就开始面临分页截断的问题，类似的问题也有网友在Github上提出，但是底下依然没有很好的解决思路：<a href="https://github.com/parallax/jsPDF/issues/1517">https://github.com/parallax/jsPDF/issues/1517</a></p><p>好在掘金上有人分享了一个不错的方法：</p><p><a href="https://juejin.cn/post/7138370283739545613">jsPDF + html2canvas A4分页截断 完美解决方案（含代码 + 案例） - 掘金</a></p><p>概括一下，其处理分页截断的原理就是在使用addImage之前，将html进行分页，通过维护一个高度位置数据，来记录每次循环迭代addImage的位置。</p><blockquote><p><strong>从高到低</strong>遍历维护一个<strong>分页数组pages</strong>，该数组记录每一页的起始位置，如：<strong>pages[0]</strong> 对应 <strong>第一页起始位置</strong>， <strong>pages[1]</strong> 对应 <strong>第二页起始位置</strong></p></blockquote><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/d9dfea7c-1249-485f-9df8-fc89a94b113b/Untitled.png" alt="Untitled"></p><p>接下来我们重点讨论如何将html进行切割，然后生成<code>Pages</code>这个数组。</p><p>假设html高度是1500，pdf宽高是[500, 900]，如果不用处理分页截断的问题，我们可以想到第一页（0-900）是用来承载html从高度为0到900的信息；第二页（900-1800）是用来承载html从高度900到1500的，所以Pages数组为[0, 900]。</p><p>如果要处理分页截断呢，这时候就需要计算html元素的距离打印起始位置的高度<code>h1</code>，以及该元素的内部高度<code>h2</code>，通过这两个高度来判断这个元素要不要放在下一页，防止截断。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/fd747bbb-7960-4f94-af50-5f8a93643edf/Untitled.png" alt="Untitled"></p><p>如果<code>h1 + h2 &gt; 页面高度</code>， 这时候说明这个元素不处理的就会被分页截断，所以应该要把这个元素放到第二页去渲染，这就以为这<code>Pages</code>记录的数据要变化，示意图如下，可以看到<code>Pages[1]</code>我们往上调整了，比第二页pdf的起始位置更高。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/aeb35600-1bce-4640-b423-a7db8e27d1ba/Untitled.png" alt="Untitled"></p><p>说明渲染第二页pdf的时候，要从<code>h1</code>开始渲染，pages数组为<code>[0, h1]。</code>解释为第一页pdf渲染html高度区域为0-900, 第二页pdf渲染html高度区域为h1-1500。注意到第一页渲染的时候到尾部的时候，<strong>会有部分内容和第二页头部内容重合。</strong></p><p>为了解决这个问题，我们可以使用<code>jspdf</code>的<code>rect</code>和<code>setFillColor</code>方法，把重合的区域遮百处理。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pdf.<span class="title function_">setFillColor</span>(<span class="number">255</span>, <span class="number">255</span>, <span class="number">255</span>);</span><br><span class="line">pdf.<span class="title function_">rect</span>(x, y, <span class="title class_">Math</span>.<span class="title function_">ceil</span>(_width), <span class="title class_">Math</span>.<span class="title function_">ceil</span>(_height), <span class="string">&#x27;F&#x27;</span>);</span><br></pre></td></tr></table></figure><h3 id="如何获得h1和h2"><a href="#如何获得h1和h2" class="headerlink" title="如何获得h1和h2"></a>如何获得h1和h2</h3><p>上面我们谈到了<code>h1</code>和<code>h2</code>，其中<code>h1</code>是元素盒子的上边距到打印区域的高度（比例缩放后的高度），<code>h2</code>是元素盒子的内部高度。</p><p>计算<code>h1</code>: <code>[getBoundingClientRect</code>方法](<a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect</a>)</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> rect = contentElement.<span class="title function_">getBoundingClientRect</span>() || &#123;&#125;;</span><br><span class="line"><span class="keyword">const</span> topDistance = rect.<span class="property">top</span>;</span><br><span class="line"><span class="keyword">return</span> topDistance;</span><br></pre></td></tr></table></figure><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/c75e3d32-abcd-4d60-b4ab-45a8b2451d2c/Untitled.png" alt="Untitled"></p><p>计算<code>h2</code>： <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight">offsetHeight方法</a></p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/b57228e1-d395-4d78-b281-ce63576ab6a4/Untitled.png" alt="Untitled"></p><p>值得注意的是，因为打印区域的html元素不一定是从窗口顶部开始，所以为了计算实际的<code>h1</code>(元素到打印区域的顶部距离），可以采用这样的方法：</p><ul><li>用<code>getBoundingClientRect</code>方法计算元素到窗口顶部的距离</li><li>循环打印之前将<code>pages</code>信息针对第一个元素进行一个高度校准。</li></ul><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对pages进行一个值的修正，因为pages生成是根据根元素来的，根元素并不是我们实际要打印的元素，而是element，</span></span><br><span class="line">  <span class="comment">// 所以要把它修正，让其值是以真实的打印元素顶部节点为准</span></span><br><span class="line">  <span class="keyword">const</span> newPages = pages.<span class="title function_">map</span>(<span class="function">(<span class="params">item</span>) =&gt;</span> item - pages[<span class="number">0</span>]);</span><br></pre></td></tr></table></figure><h2 id="源代码"><a href="#源代码" class="headerlink" title="源代码"></a>源代码</h2><p> 文章的最后，我基于<a href="https://juejin.cn/post/7138370283739545613">这篇博客</a>的思路，另写了一份demo，源代码如下：<a href="https://github.com/janice143/pdf-demo%E3%80%82%E4%B8%8E%E7%8E%B0%E6%9C%89%E6%96%87%E7%AB%A0%E4%B8%8D%E5%90%8C%E7%9A%84%E6%98%AF%EF%BC%8C%E6%9C%AC%E4%BB%93%E5%BA%93%E7%9A%84%E4%BB%A3%E7%A0%81%E7%89%B9%E7%82%B9%E5%9C%A8%E4%BA%8E%EF%BC%9A">https://github.com/janice143/pdf-demo。与现有文章不同的是，本仓库的代码特点在于：</a></p><ul><li>支持设置pdf打印的方向，比如横向</li><li>修正了高度计算问题，解决了多出一个空白页问题。掘金那篇文章计算元素高度时候没有减去容器距离顶部高度，所以导致很多新手使用那份代码的时候，会发现自己的页面顶部被裁剪到了，原因就是这个</li><li>支持自定义页眉页脚</li><li>支持扩展自定义分页方法，如果遇到复杂的组件，可以自定扩展逻辑计算高度</li></ul><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/9aef8349-a9a1-48a7-9cf7-381c53878bb8/Untitled.png" alt="Untitled"></p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/d0ecb1ba-e017-4a75-8be3-1c1dc49d73b1/Untitled.png" alt="Untitled"></p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/11/08/JSPDF-html2canvas-A4%E5%88%86%E9%A1%B5%E6%88%AA%E6%96%AD/</id>
    <link href="https://believed-breadfruit.top/2023/11/08/JSPDF-html2canvas-A4%E5%88%86%E9%A1%B5%E6%88%AA%E6%96%AD/"/>
    <published>2023-11-08T13:45:04.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>最近在业务上遇到了一个问题是要将页面打印pdf文件，产品的预期是希望点击一个按钮，就能够将页面数据写在一个pdf上，并下载下来，需要保证pd]]>
    </summary>
    <title>JSPDF + html2canvas A4分页截断</title>
    <updated>2024-12-22T06:30:35.615Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <content>
      <![CDATA[<p>作为一个初学者，第一次接触单测的时候，我其实有很多疑问，单测是什么，为什么要做单测，如何做单测，最佳实践是什么。</p><p>怎么罗列测试用例，如何调试代码</p><p>本文就是一篇浅浅的入门指南。主要围绕上面这些问题做一些解答和记录。</p><h2 id="单测是什么"><a href="#单测是什么" class="headerlink" title="单测是什么"></a>单测是什么</h2><p>单测就是单元测试，属于软件开发中的一个测试方法。正如其名字一样，单元测试关注的是代码中比较小的单元，可能是一个函数，一个方法，甚至一个属性。只要在逻辑上可以独立存在的，都是单元测试可进行的对象。</p><h2 id="Why"><a href="#Why" class="headerlink" title="Why"></a>Why</h2><p>减少上线bug，可以保证我们写的代码按照预想的执行。同时，对于代码维护也可得带来更高效率。</p><h2 id="How"><a href="#How" class="headerlink" title="How"></a>How</h2><p>主要围绕如何进行javascript代码的单元测试讲解。（单元测试是软件工程里的概念，不应该只局限与前端，由于网上的资源看起来很繁杂，我暂时没有建立起一个完整的单测认识，所以这篇文章暂时只局限于unit test with javascript）</p><h3 id="选择一个测试框架JavaScript-unit-testing-framework"><a href="#选择一个测试框架JavaScript-unit-testing-framework" class="headerlink" title="选择一个测试框架JavaScript unit testing framework"></a>选择一个测试框架JavaScript unit testing framework</h3><p>关于js的单测框架，目前使用最多的是Jest 和mocha，下面这张图给出了2016年到2021年框架使用比例图。（主要是js的发展很快，所以基于js的工具也要不断迭代和更新）</p><p><img src="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5038ccd6-04f5-4373-9086-6180b6292a03/Untitled.png" alt="Untitled"></p><p>如果第一次接触单测，肯定要选择使用最多的，因为参考资料多，不管是学习好还是开发过程中遇到问题，都能很容易找到相关的资料。还有一个好处是，使用多意味着这个工具被维护的可能性更高且更持久。</p><h3 id="Jest测试框架"><a href="#Jest测试框架" class="headerlink" title="Jest测试框架"></a>Jest测试框架</h3><p>有两个网站我推荐看，我相信会有很多收获。</p><p><a href="https://jestjs.io/docs/getting-started">Getting Started · Jest</a></p><p><a href="https://www.softwaretestinghelp.com/jest-testing-tutorial/">Jest Tutorial - JavaScript Unit Testing Using Jest Framework</a></p><p><a href="https://www.lambdatest.com/jest">Jest Tutorial: Complete Guide to Jest Testing</a></p><p>第三个文档我只简易看目录。具体内容我觉得他写的很简陋，所以不简易看。</p><p>首先根据官网的文档，可以着手写一个简单的单元测试。测试总的来说就是输入-预测输出-判断结果的过程（<strong>input - expected output - assert the result）</strong></p><p>我认为一个技术官网上的东西初学的时候不需要全部看完，技术官网是一个工具网站，我们可以把他看成是一个字典，遇到不认识的字的时候再使用它。对于一个技术网站，首先就是要快速了解这个技术最基本的几个东西是什么，比如说jest，最基本的就是如何输入命令使用、积累一些常用的matchers。如何判断哪些最基本，你可以根据js的基本知识来学习，在学js的时候，逻辑运算、字符串什么都是是常用的，所以学习jest的时候你需要了解一些对应的匹配器；页面上的交互经常涉及到一些click,change事件，这是jest有一些mock函数需要了解。等等。</p><h3 id="测什么"><a href="#测什么" class="headerlink" title="测什么"></a>测什么</h3><p>How do I know what to test?当你对jest有个初步的实操之后，下一个关注的问题是测什么？</p><p>如果是写一个页面的话，页面上每个组件、每个交互理论上都需要测试。如果自己罗列，难免有遗漏的地方，所以在jest中有个coverage的概念（<strong><strong>Code coverage in Jest）</strong></strong>，可以把它理解为Jest的一个命令或者工具，可以告诉我们代码中哪些需要测的地方。执行完这个命令后，甚至有一个可视化的Html页面可以直观地看到测试覆盖率。</p><h3 id="test-React-with-Jest"><a href="#test-React-with-Jest" class="headerlink" title="test React with Jest"></a><strong><strong>test React with Jest</strong></strong></h3><p>了解到这里，我们知道了简单的基于js的单元测试，使用的单测框架是jest。为了最佳实践，下一步我们要把单测和前端框架结合，以react框架为例，还是因为react使用的最多。</p><p>针对react组件库的测试，需要用到另一个有用工具</p><p><a href="https://testing-library.com/docs/react-testing-library/intro">React Testing Library | Testing Library</a></p><p>另外推荐阅读这两篇文章</p><p><a href="https://www.softwaretestinghelp.com/testing-react-apps-using-jest/">Jest React Tutorial - How To Test React Apps Using Jest Framework</a></p><p><a href="https://www.valentinog.com/blog/testing-react/">Testing React Components with react-test-renderer, and the Act API</a></p><p>我目前还是有疑问，snapshot到底在什么时候推荐使用。这个问题我还在思考中。尽管我搜索了不少答案，甚至问了chatgpt，我还是没有一个具体答案。</p><h3 id="如何调试"><a href="#如何调试" class="headerlink" title="如何调试"></a>如何调试</h3><p>不重要但是必不可少的是我们得知道如何调试代码。</p><p><a href="https://www.softwaretestinghelp.com/jest-configuration-and-debugging/">Jest Configuration And Debugging Jest Based Tests</a></p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a><strong>Conclusion</strong></h2><p>have fun！这篇文章并没有给出具体的命令，也没有代码实例，只是给出了一些初步的理解，以及如何找到最佳实践，其中给了一些参考博客。我觉得对于一个有基础的前端开发基础，但是缺乏软件工程完整体系，且想了解单侧的同学来说，这篇文章会有一点点帮助。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/11/06/%E3%80%90%E5%8D%95%E6%B5%8B%E3%80%91%E5%8D%95%E6%B5%8B%E5%85%A5%E9%97%A8/</id>
    <link href="https://believed-breadfruit.top/2023/11/06/%E3%80%90%E5%8D%95%E6%B5%8B%E3%80%91%E5%8D%95%E6%B5%8B%E5%85%A5%E9%97%A8/"/>
    <published>2023-11-05T16:20:45.000Z</published>
    <summary>
      <![CDATA[<p>作为一个初学者，第一次接触单测的时候，我其实有很多疑问，单测是什么，为什么要做单测，如何做单测，最佳实践是什么。</p>
<p>怎么罗列测试用例，如何调试代码</p>
<p>本文就是一篇浅浅的入门指南。主要围绕上面这些问题做一些解答和记录。</p>
<h2 id="单测是什]]>
    </summary>
    <title>【单测】单测入门 A Beginner's Guide to Writing Unit Tests for React Components with Jest and React Testing Library</title>
    <updated>2024-12-22T06:28:13.270Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://believed-breadfruit.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript 的类型系统已经从基本的类型注释，发展成为一种大型且复杂的编程语言。在上篇文章中，我们打开了类型编程的潘多拉魔盒，我们知道TS的强大之处，不仅在于其类型检查系统，还在于其具有图灵完备特性，可以实现强大的类型级编程能力。</p><p>在学习一般的编程语言的时候，我们首先会学习变量声明和数据结构，其次是基本的代数运算、条件分支和循环，更高阶地，我们会接触到递归，通过递归，我们可以了解到排序、动态规划等各种算法。掌握这些特性可以让我们快速入门一门编程语言，并且编写出更复杂的应用。</p><p>TS类型编程也有类似的特性，本文继续延续类型编程这个话题，从类型编程角度，重点探讨TS中的递归，我们特别以实现计算斐波那契序列为例，展现出不同的实现算法，来感受TS类型编程的魅力。</p><p>应该指出的是，本文话题仅仅是一次有意思的学术探索，内容深度已经远远超出了我们日常对TS的普通使用，给出的代码不建议在生产环境中使用。</p><h2 id="类型编程的思维"><a href="#类型编程的思维" class="headerlink" title="类型编程的思维"></a>类型编程的思维</h2><p>TS编程语言可以看成两部分组成：</p><ul><li>普通编程：在运行时体现，使用值和函数</li><li>类型编程：在编译时体型，使用类型和泛型</li></ul><p>对于普通编程，我们经常涉及到的元素是</p><ul><li>数据结构</li><li>函数</li><li>控制流</li><li>循环</li><li>基本代数运算（加减乘除等）</li></ul><p>有了这些特性，大部分的编程语言都是图灵完备的，可以实现任何算法的编写，实现各种各样的复杂运算。TS的类型编程也是图灵完备的，这说明这些概念同样在类型编程的世界里也有体现。</p><ul><li>数据结构 → 对象和元祖</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 数组 -&gt; 元组类型</span></span><br><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"><span class="comment">// 获取下标为1的元素，arr[1]</span></span><br><span class="line"><span class="keyword">const</span> v = arr[<span class="number">1</span>] <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Arr</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>]</span><br><span class="line"><span class="comment">// 通过下标索引类型获取某个类型元素，Arr[1]</span></span><br><span class="line"><span class="keyword">type</span> t = arr[<span class="number">1</span>] <span class="comment">// 2</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 对象 -&gt; 对象类型</span></span><br><span class="line"><span class="keyword">const</span> obj = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;John&quot;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">23</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> name = obj.<span class="property">name</span>; <span class="comment">// &#x27;John&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Obj</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name</span> = <span class="title class_">Obj</span>[<span class="string">&quot;name&quot;</span>]; <span class="comment">// string</span></span><br></pre></td></tr></table></figure><ul><li>函数 → 泛型类型</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">f</span> = (<span class="params">num</span>) =&gt; num;</span><br><span class="line"><span class="keyword">const</span> v1 = <span class="title function_">f</span>(<span class="number">1</span>)(); <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">const</span> v2 = <span class="title function_">f</span>(<span class="string">&quot;str&quot;</span>)(); <span class="comment">// &#x27;str&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> F&lt;T&gt; = T;</span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">T1</span> = F&lt;<span class="built_in">number</span>&gt;; <span class="comment">// number</span></span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">T2</span> = F&lt;<span class="built_in">string</span>&gt;; <span class="comment">// string</span></span><br></pre></td></tr></table></figure><ul><li>控制流 → 条件类型<code>extends</code></li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> ans = <span class="number">2</span> === <span class="number">1</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line"><span class="comment">// 如果2等于1，进入true分支，否则进入false分支</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Ans</span> = <span class="number">2</span> <span class="keyword">extends</span> <span class="number">1</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line"><span class="comment">// 如果2（字面量类型）是1类型的子集，进入true分支，否则进入false分支</span></span><br></pre></td></tr></table></figure><ul><li>循环 → 函数式编程的递归</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">reverse</span> = (<span class="params">arr</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> n = arr.<span class="property">length</span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; n / <span class="number">2</span>; i++) &#123;</span><br><span class="line">    [arr[i], arr[n - i - <span class="number">1</span>]] = [arr[n - i - <span class="number">1</span>], arr[i]];</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> arr;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"><span class="keyword">const</span> res = <span class="title function_">reverse</span>(arr); <span class="comment">// [5,4,3,2,1]</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Reverse</span>&lt;</span><br><span class="line">  T <span class="keyword">extends</span> <span class="built_in">number</span>[],</span><br><span class="line">  N = T[<span class="string">&quot;length&quot;</span>],</span><br><span class="line">  U <span class="keyword">extends</span> <span class="built_in">number</span>[] = []</span><br><span class="line">&gt; = U[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> N ? U : <span class="title class_">Reverse</span>&lt;T, N, [T[U[<span class="string">&quot;length&quot;</span>]], ...U]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Target</span> = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Res1</span> = <span class="title class_">Reverse</span>&lt;<span class="title class_">Target</span>&gt;; <span class="comment">// [5,4,3,2,1]</span></span><br></pre></td></tr></table></figure><ul><li>基本代数运算（加减乘除等） → 通过访问元祖长度实现加减乘除</li></ul><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 类型编程的加减乘除运算</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">BuildArr</span>&lt;</span><br><span class="line">  <span class="title class_">Num</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Arr</span> <span class="keyword">extends</span> <span class="built_in">unknown</span>[] = []</span><br><span class="line">&gt; = <span class="title class_">Arr</span>[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> <span class="title class_">Num</span> ? <span class="title class_">Arr</span> : <span class="title class_">BuildArr</span>&lt;<span class="title class_">Num</span>, [<span class="number">1</span>, ...<span class="title class_">Arr</span>]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 减法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Sub</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt; <span class="keyword">extends</span> [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: infer <span class="title class_">Rest</span></span><br><span class="line">]</span><br><span class="line">  ? <span class="title class_">Rest</span>[<span class="string">&quot;length&quot;</span>]</span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 加法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Add</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;</span><br><span class="line">][<span class="string">&quot;length&quot;</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 乘法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Mul</span>&lt;</span><br><span class="line">  <span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Res</span> <span class="keyword">extends</span> <span class="built_in">number</span> = <span class="number">0</span></span><br><span class="line">&gt; = <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="title class_">Res</span></span><br><span class="line">  : <span class="title class_">Mul</span>&lt;<span class="variable constant_">T1</span>, <span class="title class_">Sub</span>&lt;<span class="variable constant_">T2</span>, <span class="number">1</span>&gt;, <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="variable constant_">T1</span>&gt; <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="variable constant_">T1</span>&gt; : <span class="built_in">never</span>&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 除法</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Div</span>&lt;</span><br><span class="line">  <span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Res</span> <span class="keyword">extends</span> <span class="built_in">number</span> = <span class="number">0</span></span><br><span class="line">&gt; = <span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="title class_">Res</span></span><br><span class="line">  : <span class="title class_">Div</span>&lt;<span class="title class_">Sub</span>&lt;<span class="variable constant_">T1</span>, <span class="variable constant_">T2</span>&gt;, <span class="variable constant_">T2</span>, <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="number">1</span>&gt; <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="title class_">Add</span>&lt;<span class="title class_">Res</span>, <span class="number">1</span>&gt; : <span class="built_in">never</span>&gt;;</span><br></pre></td></tr></table></figure><p>可以看出，正是TS类型有条件分支和递归能力，才有了实现强大类型编程的基础。</p><h2 id="斐波那契序列的递归实现"><a href="#斐波那契序列的递归实现" class="headerlink" title="斐波那契序列的递归实现"></a>斐波那契序列的递归实现</h2><p>在学习一门新的编程语言时，我们通常以打印“Hello World”作为入门案例。同样，通过递归实现斐波那契数列，也可以看作是学习递归算法的一个“Hello World”级别的案例。</p><p>作为数学中一个重要的序列，斐波那契序列在理论和实践应用中都有重要价值。深入理解它可以帮助我们更全面地掌握数字模式和算法思路。</p><p>斐波那契序列具有这样的特点：除了前两个数字0和1之外，任意一个数字都等于前两个数字之和。数列的展开式为：0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987…。也就是说:</p><p>!<a href="https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230827/image.454v4zqwsaq0.png">https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230827/image.454v4zqwsaq0.png</a></p><p>实现斐波那契序列最简单的算法就是递归，下面是用JS编写的递归算法：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">fib</span> = (<span class="params">n</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">1</span>) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">fib</span>(n - <span class="number">1</span>) + <span class="title function_">fib</span>(n - <span class="number">2</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">fib</span>(<span class="number">10</span>)); <span class="comment">// 55</span></span><br></pre></td></tr></table></figure><p>可以看到递归最重要的两个特性：</p><ul><li>base case：逃出递归的条件</li><li>调用自身</li></ul><p>TS类型编程中的递归和JS的递归其实没什么太大区别，也具有这两个特性，只不过语法不一样。下面的代码是TS的纯类型编程版本：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Fib1</span>&lt;N <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = N <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="number">0</span></span><br><span class="line">  : N <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? <span class="number">1</span></span><br><span class="line">  : <span class="title class_">Fib1</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">1</span>&gt;&gt; <span class="keyword">extends</span> infer t1 ? <span class="title class_">Add</span>&lt;t1 <span class="keyword">extends</span> <span class="built_in">number</span> ? t1 : <span class="built_in">never</span>, <span class="title class_">Fib1</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">2</span>&gt;&gt; <span class="keyword">extends</span> infer t2 ? t2 <span class="keyword">extends</span> <span class="built_in">number</span> ? t2 : <span class="built_in">never</span> : <span class="built_in">never</span>&gt; : <span class="built_in">never</span></span><br></pre></td></tr></table></figure><p>可以看到我们用两个<code>extends</code>条件类型给出了两种base case, 第三种情况是不断调用自身。在进行类型编程的时候，我们思路其实就是按照算法来编写，然后按照TS提供的特性来实现。因为TS类型编程不能修改类型的状态，而且只用”表达式”，不用”语句”，这两个特点和<strong>函数式编程</strong>一样，所以进行类型编程的时候，我们采用的是函数式编程思想。</p><ul><li>不能修改类型状态</li></ul><p>TS可以用<code>Infer</code>来存储一个类型，但是该类型不能被修改，保存新的类型值。普通编程语言一般都有<code>for</code>循环，这是因为他们定义的变量的状态可以改变，通过可变的状态来作为计数器实现循环，但是TS的类型编程没有<code>for</code>循环，要实现遍历则需要使用递归，通过递归函数的参数来保存或更新状态。</p><ul><li>只用”表达式”，不用”语句”</li></ul><p>TS的类型编程基本上只有一个表达式，例如<code>X extends Y ? A : B</code>，这个表达式是一个单纯的运算过程，总有返回值。从斐波那契递归算法的例子可以看到，我们写的类型编程其实也就是一个比较长的表达式，而且表达式里可能有别的表达式。</p><h2 id="动态规划和矩阵快速幂运算"><a href="#动态规划和矩阵快速幂运算" class="headerlink" title="动态规划和矩阵快速幂运算"></a>动态规划和矩阵快速幂运算</h2><p>递归方法具有很高的时间复杂度，因为有大量的重复计算。为了减少时间复杂度，斐波那契序列的计算也有很多优化算法，例如动态规划和矩阵快速幂运算。本节也用TS类型编程来实践一下。</p><h3 id="动态规划"><a href="#动态规划" class="headerlink" title="动态规划"></a>动态规划</h3><p>先讲动态规划，动态规划分两种：带有备忘录的自上而下和自下而上。下面给出了JS实现代码：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// top down</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">fibMemo</span> = (<span class="params">n, memo = [<span class="number">0</span>, <span class="number">1</span>]</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (memo[n] !== <span class="literal">undefined</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> memo[n];</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  memo[n] = <span class="title function_">fibMemo</span>(n - <span class="number">1</span>, memo) + <span class="title function_">fibMemo</span>(n - <span class="number">2</span>, memo);</span><br><span class="line">  <span class="keyword">return</span> memo[n];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// bottom up</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">fib</span> = (<span class="params">n, arr = [<span class="number">0</span>, <span class="number">1</span>]</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">2</span>; i &lt;= n; i++) &#123;</span><br><span class="line">    arr[i] = arr[i - <span class="number">1</span>] + arr[i - <span class="number">2</span>];</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> arr[n];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>因为TS类型编程无法修改备忘录的状态，所以目前无法实现带有备忘录机制的纯类型编程。但是可以实现自下而上的动态规划，代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">FibBottomUp</span>&lt;N <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="title class_">Arr</span> <span class="keyword">extends</span> <span class="built_in">any</span>[]=[<span class="number">0</span>,<span class="number">1</span>]&gt; = </span><br><span class="line">  <span class="title class_">Arr</span>[N] <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">    ? <span class="title class_">Arr</span>[N]</span><br><span class="line">    : <span class="title class_">FibBottomUp</span>&lt;N, [ ...<span class="title class_">Arr</span>, ...[ <span class="title class_">Add</span>&lt;<span class="title class_">Arr</span>[<span class="title class_">Sub</span>&lt;<span class="title class_">Arr</span>[<span class="string">&#x27;length&#x27;</span>], <span class="number">1</span>&gt;], <span class="title class_">Arr</span>[<span class="title class_">Sub</span>&lt;<span class="title class_">Arr</span>[<span class="string">&#x27;length&#x27;</span>], <span class="number">2</span>&gt;]&gt; ]]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测试</span></span><br><span class="line"><span class="keyword">type</span> res2 = <span class="title class_">FibBottomUp</span>&lt;<span class="number">10</span>&gt;; <span class="comment">// 55</span></span><br></pre></td></tr></table></figure><p>可以看到我们通过<code>Arr</code>的长度来实现循环迭代，元组<code>Arr</code>的每个下标<code>index</code>对应着<code>fib(index)</code>结果。每次计算计算<code>fib(n)</code>的时候，都会先计算<code>fib(n-1)</code>和<code>fib(n-2)</code>的值，这种思路就是自底向上的动态规划，求解最终问题之前，会从问题的子问题开始，逐步构建到最终问题。</p><h3 id="矩阵快速幂"><a href="#矩阵快速幂" class="headerlink" title="矩阵快速幂"></a>矩阵快速幂</h3><p>动态规划算法由于每个节点只会计算一次，所以时间复杂度是O(n)。矩阵快速幂比动态规划的时间复杂度更低。那么矩阵快速幂是怎么推导来的呢？</p><p>我们知道，斐波那契数列有递推公式：</p><p>$$<br>F_{n+2}&#x3D;F_{n+1}+F_n, n ∈ 0,1,2..<br>$$</p><p>把这个公式改写成矩阵运算，可以得到下面的式子：</p><p>$$<br>\begin{bmatrix}<br>F_{n+2}\<br>F_{n+1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}*\begin{bmatrix}<br>F_{n+1}\<br>F_{n}<br>\end{bmatrix}<br>$$</p><p>也就是说，当n&#x3D;0, 1, 2的时候，我们又下列公式，</p><p>$$<br>\begin{bmatrix}<br>F_{2}\<br>F_{1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}*\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>$$<br>\begin{bmatrix}<br>F_{3}\<br>F_{2}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}<em>\begin{bmatrix}<br>F_{2}\<br>F_{1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}^2</em>\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>$$<br>\begin{bmatrix}<br>F_{4}\<br>F_{3}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}<em>\begin{bmatrix}<br>F_{3}\<br>F_{2}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}^3</em>\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>所以对于<code>n</code>，我们有</p><p>$$<br>\begin{bmatrix}<br>F_{n}\<br>F_{n-1}<br>\end{bmatrix}&#x3D;\begin{bmatrix}<br>1&amp; 1\<br>1&amp; 0<br>\end{bmatrix}^{n-1}*\begin{bmatrix}<br>F_{1}\<br>F_{0}<br>\end{bmatrix}<br>$$</p><p>所以只要求解出幂次部分就可以求解<code>Fn</code>，而幂次部分求解可以使用快速幂公式，如下</p><p>$$<br>T^{n} &#x3D; T^{n&#x2F;2}*T^{n&#x2F;2}, n为偶数，\<br>T^{n} &#x3D; T^{n&#x2F;2}*T^{n&#x2F;2}*T, n为奇数，<br>$$</p><p>这说明要求解$T^n$只需求解$T^{n&#x2F;2}$即可，而求解$T^{n&#x2F;2}$，只需求解$T^{n&#x2F;4}$，依次递推，我们可以发现整个幂运算其实就是不断二分，时间复杂度就是二分法的时间复杂度，为<code>O(lgn)</code>.</p><p>知道了什么是矩阵快速幂，我们可以很容易得写出对应的类型代码：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Matrix</span> = [<span class="built_in">number</span>, <span class="built_in">number</span>, <span class="built_in">number</span>, <span class="built_in">number</span>]; <span class="comment">// [a, b, c, d] represents the matrix [[a, b], [c, d]]</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">MatrixMultiply</span>&lt;A <span class="keyword">extends</span> <span class="title class_">Matrix</span>, B <span class="keyword">extends</span> <span class="title class_">Matrix</span>&gt; = [</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">0</span>], B[<span class="number">0</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">1</span>], B[<span class="number">2</span>]&gt;&gt;,</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">0</span>], B[<span class="number">1</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">1</span>], B[<span class="number">3</span>]&gt;&gt;,</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">2</span>], B[<span class="number">0</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">3</span>], B[<span class="number">2</span>]&gt;&gt;,</span><br><span class="line">  <span class="title class_">Add</span>&lt;<span class="title class_">Mul</span>&lt;A[<span class="number">2</span>], B[<span class="number">1</span>]&gt;, <span class="title class_">Mul</span>&lt;A[<span class="number">3</span>], B[<span class="number">3</span>]&gt;&gt;</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">MatrixPower</span>&lt;M <span class="keyword">extends</span> <span class="title class_">Matrix</span>, N <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = N <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? M</span><br><span class="line">  : <span class="title class_">IsOdd</span>&lt;N&gt; <span class="keyword">extends</span> <span class="literal">true</span></span><br><span class="line">  ? <span class="title class_">MatrixMultiply</span>&lt;</span><br><span class="line">      <span class="title class_">Div</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">1</span>&gt;, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span>,</span><br><span class="line">      <span class="title class_">Div</span>&lt;<span class="title class_">Sub</span>&lt;N, <span class="number">1</span>&gt;, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span></span><br><span class="line">    &gt; <span class="keyword">extends</span> infer <span class="variable constant_">R2</span></span><br><span class="line">    ? <span class="variable constant_">R2</span> <span class="keyword">extends</span> <span class="title class_">Matrix</span></span><br><span class="line">      ? <span class="title class_">MatrixMultiply</span>&lt;M, <span class="variable constant_">R2</span>&gt;</span><br><span class="line">      : <span class="built_in">never</span></span><br><span class="line">    : <span class="built_in">never</span></span><br><span class="line">  : <span class="title class_">MatrixMultiply</span>&lt;</span><br><span class="line">      <span class="title class_">Div</span>&lt;N, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span>,</span><br><span class="line">      <span class="title class_">Div</span>&lt;N, <span class="number">2</span>&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">        ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="built_in">number</span></span><br><span class="line">          ? <span class="title class_">MatrixPower</span>&lt;M, <span class="variable constant_">R1</span>&gt;</span><br><span class="line">          : <span class="built_in">never</span></span><br><span class="line">        : <span class="built_in">never</span></span><br><span class="line">    &gt; <span class="keyword">extends</span> infer <span class="variable constant_">R2</span></span><br><span class="line">  ? <span class="variable constant_">R2</span> <span class="keyword">extends</span> <span class="title class_">Matrix</span></span><br><span class="line">    ? <span class="variable constant_">R2</span></span><br><span class="line">    : <span class="built_in">never</span></span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">FibMatrix</span>&lt;N <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = N <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="number">0</span></span><br><span class="line">  : N <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? <span class="number">1</span></span><br><span class="line">  : <span class="title class_">MatrixPower</span>&lt;[<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">0</span>], N&gt; <span class="keyword">extends</span> infer <span class="variable constant_">R1</span></span><br><span class="line">  ? <span class="variable constant_">R1</span> <span class="keyword">extends</span> <span class="title class_">Matrix</span></span><br><span class="line">    ? <span class="variable constant_">R1</span>[<span class="number">1</span>]</span><br><span class="line">    : <span class="built_in">never</span></span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Test</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">MatrixResult</span> = <span class="title class_">FibMatrix</span>&lt;<span class="number">15</span>&gt;; <span class="comment">// 610</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文给出了三种斐波那契序列算法的类型编程实现方式，最终的代码可以在查看这个<a href="https://www.typescriptlang.org/play#code/PTAEBcGdG8fRo9UNH9DQXoPO1ANzoBNNBHNoYoTCKmodO9BVmwChiRRBO00IEZAsf-AHsAHUAEwYHcA7UQK8DAoOUAwAYGg5QCFugCqVAgAaARyMBISsXABPJgFNQAMQCWAI2oAeAHKgVAD3AqurSKC4BXALbaVAJwB8oALygjp85esADMSgoAD8oEEhAFzexmYWVqDUwWFJKTFaunoAyrbahgA0Sa7uvgnWmlwAZi4Q1KkAgqyseuD1Zf42Dk7OqW2gMVwqAG4uRZn6ufkGRQBMJXF+iZU1veCzfRsdiXaOteHrAzYjtYMnbkdDo84p5ID+8oAUrqBNLRM5eYXFi+WgK7Wa5ns9XCrnGOkmHxmoHmC0AWPKAUqNAJip9GUakqkHAAEMuOBNJjcQweJprKYAMYqSCQTSjAA2ijYKhULGxrFATAYlJ0dN+1UqAJUACJjM5nAxnAA6UhKVSgZwUrxvagAVnc5AAbNQgmQwFRZrReMJBPwJDJ5NK1JkAEIMcCMewAVSYhm+nV2PSKDRFLsS2MUAG0ALoeQPuLwpT3OP0GAPe6xulwpELhCNRgOJo5Wm12x2fP2gcUFiNFAvigAUKYA5DSLABzcAACwrMe2gVAAB8kqk-ZXq1w642AzGYnmXnoU1MxyK-VXaw2m0VqK4Ax6pxOe7OB3Ml+4AwBKAOuADcUtRGh01ttDAdTFmzpbXT2zhXvXvvsDwYCBWoB88oHDU+jWMHx6dNkwAtMQmiM9tAvbMb1zfNCxFYsCxHZpJ0jNcpxnPs52XYp8PHD511wzdoW3UBByPE8ZTlSANgVc8syvR1b01I9QHIJUlVIbVKEIABmWhAEv3QBWNMAa-1AHwEwAhPQUU8AFl8WcTQTF-P14yfYCxi0zSNIDQ9OLAP1MSKbQilJIpWBjOUmDoiwoAges1HsJSVNAP1jNM-C-QsthBzkmVFPAZSTHk2waVxJg6THICgpCopLVi1yTFDdzw3QsKaTHP0Anwy0coPIpMuy78Er9WYD1BdKWmKhoCrK79QVAWq-VK0B8oEyqCmqvQWoqsrcqalrOrKiqSm6kJRz6vLWsK5rwuykb2r9TqSmIfTSHIQBYOUeQAYuUAMCVCFEMTpMAQuj8FEKhACDNYRaD481muSgAFTgXF6pLgpUoofHiV1uhcVKfqWaxknIJNmr4qDSzilSXo4N75KKCcoUXUpfuWapagAJWBEIcY+kLUhh0Lwsi6LEdAHH3DOa5LnOXdjz4sSpNkh7ibhhGCa+2J7w0wGgOScH5PSUAAElIAAeXQgw0eBiBnFsFQUnCYnMrJxQ9HTEIABFqXeaYFya+YgL+Xoca1yDwnx3n-puSD7ctx7PpMDnnF6ooqYt+2aYTB3IJ9p8Ld14Z9c+RctxNzGzcFv3Umt9G41tr3HfZ163Ypz3Y-945rmTgP01ln5Tcp2ZQJLrmTAtlXkrVzQoo1jP5gt-PvZz32jlV0m6+ioO9ahY372L82s6t9oE505OheetP3cpxdJ7p3Os4DiaHeDz4B-HoeY790egI0hfq+d13Z8zrPF-bv2W8gwvOiH0vwaxrZx+Jsun-Ta+A8ZgKLR0Ym73HnzX8QMfiRFSGAmIIDOgx3CDHGIqd4Zu1aguFBER8Iy0jqsOeys54VzftQWaH8252wvs4Rm5AAAqFJwA-ydiFLGFJSa-kyP-ZUHF1Sal4uQQAX+qABpzQAx3KAAdTQAC-H4EAKHxgBKpUANs2sA4C0InBQsecsNJFAoc-ZRttUqWlsJoGkrAIx6EUbfRIfoUglkxCKagMRtG6P0SKQx8xV7mJFLMGIQ9qHrRwQwjEfoBS9n7AKCCpDGYPVHIo-ettVHqJ+EArwpiQjOOcFY9qOi9EGKMU4gsFjnCuJSbY9JTcAzTn8XhEJCkFrhJto+KJETqmUwpLUnoHgAipTUUBAIccKTwIqdQJGHw1FfiaqObxBQjGNP2M8dCIyxlRCuADUAZSZTrxSJUwBkSVnRL+tUlI3jxm9C8EEVpSjQFeIpCLdeCjemgDUU1AZkyWgjK+FUnojQpkUkNqQ6iIQaJqBsWk+xKQDAOD2avCMQFbBcAANZcE4FwQMak0ypRTH4jcgSgJAvsI0L01jUl2LdhiooyDELigjAeRZahxZSxaFAnYmjgECz6ArNQMRKXoWRgUGE+9zipCqJiGkkBmVi0lmyyEHK1oPXFgAUVGFwABGjHz83vB0g4TKjhSplaHGYnLebcvCLy-lgr1UWE1WK1wxAgA">playground</a>。</p><p>虽然TS的类型编程很强大，但是在编写的时候，我不得不指出一些体感不好的地方，比如很难debug，使用递归会经常遇到<code>”type instantiation is excessively deep and possibly infinite&quot; error.“</code>报错，这个时候就得通过<code>infer</code>来定义一个局部类型，再给这个类型加一个条件判断来解决，这会让代码更加复杂难懂。除此之外，我觉得类型编程的性能不高，而且难以衡量，比如当你想计算更大的<code>n</code>的时候，上述给出的三种方法可能会算不出来，返回一个<code>any</code>类型。如果想要看结果，只能通过把鼠标hover到这个类型上查看，没有一个console.log之类的方法打印出来。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/24/TS%E5%AE%9E%E7%8E%B0%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97%E7%AE%97%E6%B3%95/</id>
    <link href="https://believed-breadfruit.top/2023/10/24/TS%E5%AE%9E%E7%8E%B0%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97%E7%AE%97%E6%B3%95/"/>
    <published>2023-10-24T13:46:54.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript 的类型系统已经从基本的类型注释，发展成为一种大型且复杂的编程语言。在上篇文章中，我们打开了类型编程的潘多拉魔盒，我们知]]>
    </summary>
    <title>TS实现斐波那契序列算法</title>
    <updated>2024-12-22T06:31:01.977Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="FormData" scheme="https://believed-breadfruit.top/tags/FormData/"/>
    <content>
      <![CDATA[<p>文件上传是我们在业务中经常遇到的实战场景。本文从自顶向下的角度讲解文件上传的原理。</p><h2 id="表现层"><a href="#表现层" class="headerlink" title="表现层"></a>表现层</h2><p>我们简单写一个”上传文件demo“，来开始学习”文件上传“的知识点。</p><p>通过下面这个命令构建一个基本的nextjs应用。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx create-next-app@latest</span><br></pre></td></tr></table></figure><p>之后通过<code>npm install antd</code>安装<code>antd</code>组件库，因为我们会使用它的upload组件。</p><p>安装好后，修改pages.tsx文件，代码如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Button</span>, <span class="title class_">Upload</span>, <span class="title class_">UploadProps</span>, message &#125; <span class="keyword">from</span> <span class="string">&#x27;antd&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">FILE_PATH</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./constants&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> styles <span class="keyword">from</span> <span class="string">&#x27;./page.module.css&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Home</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">props</span>: <span class="title class_">UploadProps</span> = &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;file&#x27;</span>,</span><br><span class="line">    <span class="attr">action</span>: <span class="string">&#x27;/api/upload&#x27;</span>,</span><br><span class="line">    <span class="title function_">onChange</span>(<span class="params">info</span>) &#123;</span><br><span class="line">      <span class="keyword">if</span> (info.<span class="property">file</span>.<span class="property">status</span> !== <span class="string">&#x27;uploading&#x27;</span>) &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(info.<span class="property">file</span>, info.<span class="property">fileList</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (info.<span class="property">file</span>.<span class="property">status</span> === <span class="string">&#x27;done&#x27;</span>) &#123;</span><br><span class="line">        message.<span class="title function_">success</span>(</span><br><span class="line">          <span class="string">`<span class="subst">$&#123;info.file.name&#125;</span> 文件上传成功, 前往 <span class="subst">$&#123;FILE_PATH&#125;</span> 查看上传文件`</span></span><br><span class="line">        );</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (info.<span class="property">file</span>.<span class="property">status</span> === <span class="string">&#x27;error&#x27;</span>) &#123;</span><br><span class="line">        message.<span class="title function_">error</span>(<span class="string">`<span class="subst">$&#123;info.file.name&#125;</span> file upload failed.`</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">main</span> <span class="attr">className</span>=<span class="string">&#123;styles.main&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">div</span>&gt;</span>文件上传demo<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Upload</span> &#123;<span class="attr">...props</span>&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">Button</span>&gt;</span>上传文件<span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Upload</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">main</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>写法其实和react差不多，唯一不同的是在文件顶部，我们写了<code>&#39;use client&#39;;</code> 来声明这个文件是一个<a href="https://nextjs.org/docs/app/building-your-application/rendering/client-components">客户端组件</a>。</p><p>写好了客户端部分的代码，接下来我们开始写服务端的<code>/api/upload</code>接口，nextjs是一个全栈应用框架，通过<a href="https://nextjs.org/docs/app/api-reference/file-conventions/route">route handler</a>，我们可以快速写一个rest风格的接口。</p><p>我们在app文件夹下，新建一个upload文件夹，在upload文件夹中，我们新建一个route.ts文件，该文件的代码如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="variable constant_">FILE_PATH</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@/app/constants&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; writeFile &#125; <span class="keyword">from</span> <span class="string">&#x27;fs/promises&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">NextRequest</span>, <span class="title class_">NextResponse</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;next/server&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; join &#125; <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">POST</span>(<span class="params">request: NextRequest</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> request.<span class="title function_">formData</span>();</span><br><span class="line">  <span class="keyword">const</span> <span class="attr">file</span>: <span class="title class_">File</span> | <span class="literal">null</span> = data.<span class="title function_">get</span>(<span class="string">&#x27;file&#x27;</span>) <span class="keyword">as</span> unknown <span class="keyword">as</span> <span class="title class_">File</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!file) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">NextResponse</span>.<span class="title function_">json</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;请上传文一个件&#x27;</span>, <span class="attr">code</span>: <span class="number">400</span> &#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> bytes = <span class="keyword">await</span> file.<span class="title function_">arrayBuffer</span>();</span><br><span class="line">  <span class="keyword">const</span> buffer = <span class="title class_">Buffer</span>.<span class="title function_">from</span>(bytes);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> path = <span class="title function_">join</span>(<span class="variable constant_">FILE_PATH</span>, file.<span class="property">name</span>);</span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">writeFile</span>(path, buffer);</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`open <span class="subst">$&#123;path&#125;</span> to see the uploaded file`</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">NextResponse</span>.<span class="title function_">json</span>(&#123; <span class="attr">data</span>: <span class="literal">null</span>, <span class="attr">message</span>: <span class="string">&#x27;success&#x27;</span>, <span class="attr">code</span>: <span class="number">200</span> &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过运行<code>npm run dev</code> 命令，我们运行该项目。界面如下：</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/55b394b4-532e-4b78-9145-6944203609df/Untitled.png" alt="Untitled"></p><p>点击上传文件，选择一个文件，可以看到文件成功上传了，打开dev tool查看接口详情。我们可以看到一个在预期之内的upload请求，请求头的content-type如下：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Content</span>-<span class="title class_">Type</span>: multipart/form-data; boundary=----<span class="title class_">WebKitFormBoundaryiI4</span>jCWvT8XkA2pgg</span><br></pre></td></tr></table></figure><p>请求参数：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Form</span> <span class="title class_">Data</span></span><br><span class="line"><span class="attr">file</span>: (binary)</span><br></pre></td></tr></table></figure><p>以上是一个基本的上传文件实践。在这个过程中，我们观察到了antd的upload组件在调upload接口的时候，会将请求的content-type转化成<code>multipart/form-data</code>, 并且文件数据以form data的数据结构传递给服务器，服务器为了解析文件类型与类型，也以formdata对应的解析形式进行解析（在nextjs中，我们使用nextjs 内置的request.formData来解析表单数据）。</p><p>我们的demo代码已经放到github仓库中：<a href="https://github.com/janice143/file-upload-demo">https://github.com/janice143/file-upload-demo</a></p><h2 id="HTTP协议：客户端和服务器传输数据的规范"><a href="#HTTP协议：客户端和服务器传输数据的规范" class="headerlink" title="HTTP协议：客户端和服务器传输数据的规范"></a>HTTP协议：客户端和服务器传输数据的规范</h2><p>文件上传的流程其实就是客户端和服务器进行数据交换的过程，而客户端与服务器通信遵循HTTP协议，包括请求和响应的格式、状态码、header字段等。</p><h3 id="客户端"><a href="#客户端" class="headerlink" title="客户端"></a>客户端</h3><p>在文件上传的流程中，前端通过XMLHttpRequest请求，设置Content-Type这个请求头字段，并取值为<code>multipart/form-data</code>。**<code>Content-Type</code><strong>头部告诉服务器如何解析主体数据。例如，</strong><code>Content-Type: text/html</code><strong>表示主体数据是HTML文档，而</strong><code>Content-Type: application/json</code><strong>表示主体数据是JSON格式的数据。</strong><code>Content-Type:** multipart/form-data</code>在告诉服务器要用解析form data的形式解析才能获取文件。</p><h3 id="multipart-form-data"><a href="#multipart-form-data" class="headerlink" title="multipart/form-data"></a><code>multipart/form-data</code></h3><p><code>multipart/form-data</code> 是一种**MIME类型，<strong>浏览器使用MIME类型（而不是文件扩展符）来区分文件类型，通常由一个主类型（type）和一个子类型（subtype）组成，例如</strong><code>text/html</code><strong>表示HTML文档，</strong><code>image/jpeg</code>**表示JPEG图像。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">type/subtype</span><br></pre></td></tr></table></figure><p>对于type来讲，一般有分为两种：discrete and multipart**。**discrete类型表示单文件，常见的有application, audio, image, text等。multipart表示有多个文件，一个合成文件，这些部分可以有自己的 MIME 类型。分为两种: message, multipart。虽然multipart用于处理多文件的场景，但是在文件上传的时候，不管是单文件还是多文件，我们都是用<code>multipart/form-data</code>这个类型，这是为什么呢？</p><p>因为<code>multipart</code>可以进行传输原始的二进制文件，在这之前，文件上传最早是使用<code>application/x-www-form-urlencoded</code>，这就要求客户端在上传文件之前对文件进行 URL 编码。如果文件主要是 ASCII 文本，则 URL 编码是有效的，但如果文件主要是二进制数据，则必须对每个字节进行都 URL 转义，这是非常低效的。</p><p>所以在<a href="https://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>提案中，最早提出了<code>multipart/form-data</code>这一MIME来兼容文件上传的表示，它允许您在一个 HTTP 正文中发送多个文件，而无需对它们进行编码。</p><p>所以总结下来，调用上传接口的时候设置form-data请求头，是为了告诉服务器客户端要给你传输一个文件，而为什么要使用form-data这个content-type，这是http协议的一个规范，为了实现客户端和服务器之间高效的文件传输。</p><p>在dev tool中，我们的请求体其实还包含一个boundary字段，这是“边界定界符”，用来分割多个文件，这样服务器就能知道每个文件的边界。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Content</span>-<span class="title class_">Type</span>: multipart/form-data; boundary=----<span class="title class_">WebKitFormBoundaryiI4</span>jCWvT8XkA2pgg</span><br></pre></td></tr></table></figure><p>我们上述的代码是使用了antd封装的upload组件，文件上传的时候本质是发送了一个异步的AJAX请求。在这种技术之前，我们最早是利用form表单来实现文件上传。</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&lt;form method=<span class="string">&quot;POST&quot;</span> enctype=<span class="string">&quot;multipart/form-data&quot;</span>&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;file&quot;</span> <span class="attr">name</span>=<span class="string">&quot;file&quot;</span> <span class="attr">value</span>=<span class="string">&quot;请选择文件&quot;</span>&gt;</span><span class="tag">&lt;<span class="name">br</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;submit&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br></pre></td></tr></table></figure><p>可见，在form表单中，我们需要手动将表单数据编码格式设置为 <code>multipart/form-data</code> 类型，这个类型也是同样遵守http协议的规范。</p><p>为什么<code>XMLHttpRequest</code>无需手动设置这个请求头类型呢？</p><p>因为使用了浏览器提供的FormData构造函数。FormData对象是用于存储键值对的数据结构，是为了保存表单数据而设计的。formData的特殊用途在于进行网络请求（如<code>fetch</code>, <code>XMLHttpRequest</code>)，网络请求可以接受formData对象，并将其编码并用 <code>Content-Type: multipart/form-data</code> 发送给服务器。</p><p>所以在dev tool中，我们可以看到请求参数是form data, 其key为file, value是一个二进制文件。</p><p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/3c07946f-64a3-4eae-a269-738c94764b10/8d5d06a7-b6ec-4936-964e-db673e58daa3/Untitled.png" alt="Untitled"></p><h3 id="服务器端"><a href="#服务器端" class="headerlink" title="服务器端"></a>服务器端</h3><p>在服务器端，主要是接受和处理客户端发送的文件数据。</p><ol><li><strong>接收请求：</strong> 服务器接收来自客户端的HTTP POST请求，请求头部中包括**<code>Content-Type</code><strong>为</strong><code>multipart/form-data</code>**的标识。</li><li><strong>解析数据：</strong> 服务器使用**<code>multipart/form-data</code>**格式解析请求体中的数据。这个格式将数据划分为多部分，每个部分包含一个表单字段或一个文件。</li><li><strong>处理文件：</strong> 对于文件部分，服务器会将其保存到指定的位置，通常是文件系统中的某个目录。文件名和路径通常是根据服务器端的需求和配置而定。</li><li><strong>返回响应：</strong> 服务器处理完文件后，会向客户端发送响应，通常包括状态码（如200表示成功）和一些附加信息。</li></ol><p>服务器端的文件上传流程可以根据具体的后端框架和语言而有所不同，但基本原理是相似的。通常，服务器端会提供文件处理的API或路由，以接收和处理上传的文件。</p><h2 id="非Form-Data形式上传的文件"><a href="#非Form-Data形式上传的文件" class="headerlink" title="非Form Data形式上传的文件"></a>非Form Data形式上传的文件</h2><p>客户端和服务器之间上传文件并不限于使用FormData格式。FormData是一种常用的方法，但还有其他方式可以进行文件上传。</p><p>例如可以将文件内容转换为Base64编码，然后将其作为文本数据上传。服务器端可以将Base64解码为原始文件。</p><p>如果后端需要的数据是其他形式的二进制文件，如果blob, array buffer，客户端可以通过浏览器提供的Blob和File实例格式化数据，服务器为了能够识别这些二进制文件，可以才有适配的方法，例如使用Node.js（Express）接收Blob文件，可以使用中间件来处理文件上传。以下是一个基本示例：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">&#x27;express&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">express</span>();</span><br><span class="line"><span class="keyword">const</span> multer = <span class="built_in">require</span>(<span class="string">&#x27;multer&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 配置文件上传</span></span><br><span class="line"><span class="keyword">const</span> storage = multer.<span class="title function_">memoryStorage</span>();</span><br><span class="line"><span class="keyword">const</span> upload = <span class="title function_">multer</span>(&#123; storage &#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理文件上传</span></span><br><span class="line">app.<span class="title function_">post</span>(<span class="string">&#x27;/upload&#x27;</span>, upload.<span class="title function_">single</span>(<span class="string">&#x27;file&#x27;</span>), <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> blobData = req.<span class="property">file</span>.<span class="property">buffer</span>; <span class="comment">// Blob数据可以在req.file.buffer中找到</span></span><br><span class="line">  <span class="comment">// 在这里对blobData进行处理，保存到磁盘或数据库</span></span><br><span class="line">  res.<span class="title function_">status</span>(<span class="number">200</span>).<span class="title function_">json</span>(&#123; <span class="attr">message</span>: <span class="string">&#x27;File uploaded successfully&#x27;</span> &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">listen</span>(<span class="number">3000</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Server is running on port 3000&#x27;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>在这个示例中，**<code>multer</code><strong>中间件用于处理文件上传，</strong><code>req.file.buffer</code>**包含了Blob数据。</p><p>客户端和服务器进行文件传输的其他常见方式还有一种是<strong>基于WebSocket的文件上传</strong>，WebSocket协议允许双向通信，因此可以使用WebSocket来实现实时文件上传和流传输。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文主要介绍了基于form data形式的文件上传原理。从一个实例出发，解释了multipart&#x2F;form-data类型的文件传输协议，通过解释MIME type规范的意义，我们进一步延伸了非form data类型的文件上传。总之，文件上传本质上就是一个浏览器和服务器通信的问题，最重要的是把握的是二者之间需要遵守什么规范，才能保证客户端正确地发送文件，服务器正确地接受文件。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/23/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%EF%BC%9A%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E7%90%86%E8%A7%A3FormData%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/</id>
    <link href="https://believed-breadfruit.top/2023/10/23/%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%EF%BC%9A%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E7%90%86%E8%A7%A3FormData%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86/"/>
    <published>2023-10-23T13:46:08.000Z</published>
    <summary>
      <![CDATA[<p>文件上传是我们在业务中经常遇到的实战场景。本文从自顶向下的角度讲解文件上传的原理。</p>
<h2 id="表现层"><a href="#表现层" class="headerlink" title="表现层"></a>表现层</h2><p>我们简单写一个”上传文件demo“]]>
    </summary>
    <title>文件上传：自顶向下理解FormData文件上传的工作原理</title>
    <updated>2024-04-22T17:22:13.407Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://believed-breadfruit.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript（TS）的类型系统十分强大，除了定义了各种类型方便我们对变量、函数、类等进行类型约束，实现类型安全，而且还支持泛型，可以很大程度实现代码复用。另外，为了迎合JS语言的灵活特性，保证更安全的类型检查，TS还提供了很多类型操作符，可以帮助我们实现强大的类型编程，避免<code>any</code>类型的滥用。</p><p>本文主要讲解TS的类型编程。事实上，在编写TS的时候，我们很容易发现，除了要编写业务本身的复杂逻辑，有的时候类型逻辑写起来也很复杂。如何更好地理解TS类型编程，让TS的类型逻辑不会成为代码编写的负担，是我们很关心的一个问题。</p><p>本文的主要观点是将TS看成由两种语言组成：一种是普通的编程语言JS，另一种是类型语言。在JS中，我们的主要操作对象是值，通过声明变量对值进行引用和存储，并使用函数对值进行各种计算、处理和转换。</p><p>在类型语言中，我们的关注点从普通编程语言中对值的操作转移到了对类型的操作。通过类型注解和泛型，类型成为一等公民，可以实现类型的赋值、组合和转换，从而产生各种不同的类型。</p><p>当我们在编写TS的时候，我们其实是在这两种语言中反复转换：我们用普通的JS语言编写逻辑，处理各种值数据；我们用类型编程创建各种类型，并使用类型注释约束JS中的各种元素。</p><p>!<a href="https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230814/image.1aoi2cvm2u2o.png">https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230814/image.1aoi2cvm2u2o.png</a></p><h2 id="静态类型系统的泛型"><a href="#静态类型系统的泛型" class="headerlink" title="静态类型系统的泛型"></a>静态类型系统的泛型</h2><p>在静态类型系统中，泛型是一种参数化类型的机制，意思说我们可以通过将类型看成参数，用一个符号来表示多种类型，从而编写出可以复用的类型代码。</p><p>泛型的关键是将类型作为参数（类型参数），传递给代码单元，如函数、类、接口，在其内部，我们使用的是参数化的类型，而不是某一个具体类型。在类型理论中，泛型背后的理论是多态（<strong><a href="https://en.wikipedia.org/wiki/Polymorphism_(computer_science)#Row_polymorphism">Polymorphism</a>）</strong>，即通过一个单一的符号来表示多个不同类型，或者通过单一的接口描述不同的数据类型实体。泛型通过参数多态成为实现多态的方式之一。</p><p>在没有泛型之前，我们需要多次定义来满足不同类型的情况：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> t1 = <span class="function">(<span class="params">a: <span class="built_in">number</span></span>) =&gt;</span> <span class="built_in">number</span>;</span><br><span class="line"><span class="keyword">type</span> t2 = <span class="function">(<span class="params">a: <span class="built_in">string</span></span>) =&gt;</span> <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">f1</span>: t1 = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">f2</span>: t2 = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br></pre></td></tr></table></figure><p>有了泛型后，我们使用尖括号（<code>&lt;&gt;</code>）和类型参数来定义一个泛型：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 泛型函数1：类似普通函数的定义</span></span><br><span class="line"><span class="keyword">const</span> generics_f1 = &lt;T&gt;(<span class="attr">a</span>: T): <span class="function"><span class="params">T</span> =&gt;</span> a;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 泛型函数2：泛型类型</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">generics_f2</span>: &lt;T&gt;<span class="function">(<span class="params">a: T</span>) =&gt;</span> T = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 泛型函数3：对象字面量形式的泛型类型</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">generics_f3</span>: &#123; &lt;T&gt;(<span class="attr">a</span>: T): T &#125; = <span class="function">(<span class="params">a</span>) =&gt;</span> a;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> f3 = generics_f1&lt;<span class="built_in">number</span>&gt;;</span><br><span class="line"><span class="keyword">const</span> f4 = generics_f1&lt;<span class="built_in">string</span>&gt;;</span><br></pre></td></tr></table></figure><p>不仅TS的类型系统支持泛型，很多其他编程语言也支持，如Java, C#，GO等，所以对于计算机语言来讲，泛型并不是一个很新的概念。但是TS类型系统提供的能力远远超过一般的泛型，在TS类型编程中，我们可以基于一个类型构建另一个类型，限制类型的范围，甚至对传入的类型参数进行各种逻辑运算。</p><p>在上面这片代码中，我们定义的泛型很简单，输入是什么类型，输出就是什么类型，但是对于十分灵活的JS来讲，这种简单的泛型远远不够保证类型安全，因为大部分的场景是这样的：我们输入的类型T，最终需要通过某种逻辑运算，转化成类型U，我们用数学的函数表达式来描述T和U的关系<code>U=F(U)</code>.</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">generics_complex_f</span>: &lt;T&gt;<span class="function">(<span class="params">a: T</span>) =&gt;</span> U = <span class="function">(<span class="params">a</span>) =&gt;</span> b;</span><br><span class="line"><span class="comment">// U = F(T), b = f(a)</span></span><br></pre></td></tr></table></figure><p>TS的类型编程主要内容，就是按照TS给定的语言规则，编写<code>U=F(U)</code>的代码实现。</p><h2 id="类型编程与普通编程"><a href="#类型编程与普通编程" class="headerlink" title="类型编程与普通编程"></a>类型编程与普通编程</h2><p>普通编程语言中有的思想，在类型编程编程中可以找到类似的实现。</p><h3 id="类型注解和赋值"><a href="#类型注解和赋值" class="headerlink" title="类型注解和赋值"></a>类型注解和赋值</h3><p>在JS中，我们的数据是值，可以通过<code>=</code>将数据赋值给变量。在TS中，我们的数据是类型，通过类型注解<code>:</code>，可以为变量、函数参数、函数返回值等明确指定类型，从而约束其取值范围和操作。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">message</span>: <span class="built_in">string</span> = <span class="string">&quot;Hello, World!&quot;</span>;</span><br></pre></td></tr></table></figure><h3 id="变量声明"><a href="#变量声明" class="headerlink" title="变量声明"></a>变量声明</h3><p>JS 使用关键字 <code>var</code> 、 <code>const</code> 和 <code>let</code> 声明一个变量。在TS类型语言中，我们使用<code>type</code> 和 <code>interface</code> 声明类型变量。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> user = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;Bob&quot;</span>,</span><br><span class="line">  <span class="attr">login</span>: <span class="string">&quot;robert&quot;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">30</span>,</span><br><span class="line">  <span class="attr">comments</span>: [],</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">User</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">login</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">  <span class="attr">comments</span>: <span class="title class_">Comment</span>[];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>局部变量<code>infer</code></strong></p><p>JS的变量声明具有作用域，类型语言也有作用域，用<code>type</code>和<code>interface</code>创建的变量具有全局作用域，要创建局部作用域，可以使用<code>infer</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> A = <span class="string">&#x27;foo&#x27;</span>; <span class="comment">// 全局作用域</span></span><br><span class="line"><span class="keyword">type</span> B = A <span class="keyword">extends</span> infer C ? (</span><br><span class="line">    C <span class="keyword">extends</span> <span class="string">&#x27;foo&#x27;</span> ? <span class="literal">true</span> : <span class="literal">false</span> <span class="comment">// 局部作用域：C类型变量只在该表达式中有用</span></span><br><span class="line">) : <span class="built_in">never</span></span><br></pre></td></tr></table></figure><p>在上面这片代码中，类型B的结果可以看成两个过程：</p><ol><li>将 <code>A extends &#39;foo&#39;</code> 的结果，通过<code>infer</code>缓存在C中</li><li>根据<code>C ? true : false</code>将结果带入，计算类型B。</li></ol><h3 id="条件分支"><a href="#条件分支" class="headerlink" title="条件分支"></a>条件分支</h3><p>类型语言和JS语言都可以使用三元运算符：<code>condition ? trueExpression : falseExpression</code> ，如果条件成立，那么会进入<code>trueExpression</code>分支，否则进入<code>falseExpression</code>分支。在JS中，判断条件成立可以使用<code>==</code>、 <code>===</code>或者逻辑运算符。</p><p>在类型语言中，使用 <code>extends</code> 关键字来判断条件是否成立，<code>X extends Y</code> 可以理解为”<code>X</code>是<code>Y</code>的子集吗？“，如果是，进入<code>trueExpression</code>分支，否则进入<code>falseExpression</code>分支。</p><p>注：对于<code>X extends Y</code> ，如果<code>X</code>是联合类型，结果会按照分布规则进行计算，这一点可以参考<a href="https://juejin.cn/post/7264549320362934309">这篇文章</a>更好理解。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> R = <span class="string">&#x27;a&#x27;</span> <span class="keyword">extends</span> <span class="built_in">string</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> S = <span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span> <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> T = &#123; <span class="attr">a</span>: <span class="number">1</span>; <span class="attr">b</span>: <span class="number">2</span> &#125; <span class="keyword">extends</span> &#123; <span class="attr">a</span>: <span class="number">1</span> &#125; ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> U = &#123; <span class="attr">a</span>: <span class="number">1</span> &#125; <span class="keyword">extends</span> &#123; <span class="attr">a</span>: <span class="number">1</span>; <span class="attr">b</span>: <span class="number">2</span> &#125; ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br></pre></td></tr></table></figure><h3 id="索引：检索属性值和属性类型"><a href="#索引：检索属性值和属性类型" class="headerlink" title="索引：检索属性值和属性类型"></a>索引：检索属性值和属性类型</h3><p>在 JS 中，我们可以使用方括号访问对象属性值，例如 <code>obj[&#39;prop&#39;]</code> ，或者根据下标获取数组的值。TS的类型语言也提供了方括号运算符，可以用来获取对象类型的属性类型，或者对元祖和数组进行索引。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 对象</span></span><br><span class="line"><span class="keyword">const</span> person = &#123; <span class="attr">age</span>: <span class="number">12</span>; <span class="attr">name</span>: <span class="string">&#x27;John&#x27;</span>; <span class="attr">alive</span>: <span class="literal">true</span> &#125;;</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">Age</span> = person[<span class="string">&quot;age&quot;</span>]; <span class="comment">// 12</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Person</span> = &#123; <span class="attr">age</span>: <span class="built_in">number</span>; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">alive</span>: <span class="built_in">boolean</span> &#125;;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Age</span> = <span class="title class_">Person</span>[<span class="string">&quot;age&quot;</span>]; <span class="comment">// number</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 数组（元祖）</span></span><br><span class="line"><span class="keyword">const</span> names = [<span class="string">&#x27;John&#x27;</span>, <span class="string">&#x27;Bob&#x27;</span>];</span><br><span class="line"><span class="keyword">const</span> name1 = names[<span class="number">0</span>] <span class="comment">// John</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Names</span> = <span class="built_in">string</span>[]</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name</span> = <span class="title class_">Names</span>[<span class="built_in">number</span>] <span class="comment">// string</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Name1</span> = <span class="title class_">Names</span>[<span class="number">1</span>] <span class="comment">// string</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Tuple</span> = [<span class="built_in">string</span>, <span class="built_in">number</span>]</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Age</span> = <span class="title class_">Tuple</span>[<span class="number">1</span>] <span class="comment">// string</span></span><br></pre></td></tr></table></figure><p>对类型语言中，方括号中使用的索引是一个类型（索引类型），这意味着我们可以使用<code>union</code>, <code>keyof</code>等其他类型操作符对索引类型进行改造，从而获得更多类型。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Person</span> = &#123; <span class="attr">age</span>: <span class="built_in">number</span>; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">alive</span>: <span class="built_in">boolean</span> &#125;;</span><br><span class="line">     </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Age</span> = <span class="built_in">number</span></span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">I1</span> = <span class="title class_">Person</span>[<span class="string">&quot;age&quot;</span> | <span class="string">&quot;name&quot;</span>]; <span class="comment">// number | string</span></span><br><span class="line">      </span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">I2</span> = <span class="title class_">Person</span>[keyof <span class="title class_">Person</span>]; <span class="comment">// number | string | boolean</span></span><br><span class="line"></span><br><span class="line"> </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">AliveOrName</span> = <span class="string">&quot;alive&quot;</span> | <span class="string">&quot;name&quot;</span>;</span><br><span class="line"><span class="keyword">type</span> <span class="variable constant_">I3</span> = <span class="title class_">Person</span>[<span class="title class_">AliveOrName</span>]; <span class="comment">// string | boolean</span></span><br></pre></td></tr></table></figure><h3 id="泛型和函数"><a href="#泛型和函数" class="headerlink" title="泛型和函数"></a>泛型和函数</h3><p>函数是JS等编程语言的核心之一，具有入参和出参，通过传入一些特定值，会根据内部逻辑转化成新的值，并输出。在TS类型语言中，泛型是类型编程中非常重要的概念，它允许我们像JS编写函数一样，编写出能够适用于多种类型的代码。</p><p>我们以TS内置的<code>pick</code>方法为例，该方法接口两个参数，<code>T</code>和<code>K</code>，其中<code>T</code>一般是一个对象类型，<code>K</code>为该类型的某个属性，<code>pick&lt;T,K&gt;</code>可以获取T对象中，属性为K的对象转成的新类型：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">Todo</span> &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">description</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">completed</span>: <span class="built_in">boolean</span>;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">TodoPreview</span> = <span class="title class_">Pick</span>&lt;<span class="title class_">Todo</span>, <span class="string">&quot;title&quot;</span> | <span class="string">&quot;completed&quot;</span>&gt;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// type TodoPreview = &#123;</span></span><br><span class="line"><span class="comment">//     title: string;</span></span><br><span class="line"><span class="comment">//     completed: boolean;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure><p>在JS中，尝试写一个<code>pick</code>函数，入参也是两个，第一个是对象，第二个是对象的属性列表，函数返回值是具有这些属性的新对象，该函数可以写成如下这样：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> todo = &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="string">&quot;1&quot;</span>,</span><br><span class="line">  <span class="attr">description</span>: <span class="string">&quot;1&quot;</span>,</span><br><span class="line">  <span class="attr">completed</span>: <span class="literal">true</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">pick</span> = (<span class="params">obj, keys</span>) =&gt; keys.<span class="title function_">map</span>(<span class="function">(<span class="params">key</span>) =&gt;</span> (&#123; [key]: obj[key] &#125;));</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> res = <span class="title function_">pick</span>(todo, [<span class="string">&quot;title&quot;</span>, <span class="string">&quot;completed&quot;</span>]);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(res); <span class="comment">// [ &#123; title: &#x27;1&#x27; &#125;, &#123; completed: true &#125; ]</span></span><br></pre></td></tr></table></figure><p>在类型编程中，需要把所有值看成类型，类型的遍历用<code>in keyof</code>操作符，用索引类型获取对应属性的类型，通过<code>extends</code>实现属性名的限制，所以类型编程版本的 <code>pick</code>方法的实现如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Pick</span>&lt;T, K <span class="keyword">extends</span> keyof T&gt; = &#123;</span><br><span class="line">  [P <span class="keyword">in</span> K]: T[P];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><strong>默认参数</strong></p><p>和JS函数参数有默认值一样，TS的类型编程也可以定义一个默认参数：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> getName&lt;T=<span class="built_in">string</span>&gt;(<span class="attr">name</span>: T): T &#123;</span><br><span class="line">  <span class="keyword">return</span> name;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> name1 = getName&lt;<span class="built_in">string</span>&gt;(<span class="string">&quot;myString&quot;</span>); <span class="comment">// myString</span></span><br><span class="line"><span class="keyword">let</span> name2 = <span class="title function_">getName</span>(<span class="string">&quot;myString&quot;</span>); <span class="comment">//myString</span></span><br><span class="line"><span class="keyword">let</span> name3 = getName&lt;<span class="built_in">number</span>&gt;(<span class="number">1</span>); <span class="comment">// 1</span></span><br></pre></td></tr></table></figure><h3 id="遍历和过滤"><a href="#遍历和过滤" class="headerlink" title="遍历和过滤"></a>遍历和过滤</h3><p>在JS中，我们可以通过<code>for in</code> 遍历对象。在遍历的过程中，我们可以修改原数组，例如</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> obj = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&quot;John&quot;</span>,</span><br><span class="line">  <span class="attr">gender</span>: <span class="string">&quot;male&quot;</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="number">23</span>,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> key <span class="keyword">in</span> obj) &#123;</span><br><span class="line">  obj[key + <span class="string">&quot;_suffix&quot;</span>] = obj[key];</span><br><span class="line">  <span class="keyword">delete</span> obj[key];</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(obj); <span class="comment">// &#123; name_suffix: &#x27;John&#x27;, gender_suffix: &#x27;male&#x27;, age_suffix: 23 &#125;</span></span><br></pre></td></tr></table></figure><p>TS的类型编程则可以通过 <code>[K in keyof T]</code> 实现对象类型的遍历，并通过<code>as</code> 将遍历后的类型转化为新类型：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Obj</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>,</span><br><span class="line">  <span class="attr">gender</span>: <span class="built_in">string</span>,</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">NewObj</span>&lt;T&gt; =&#123;</span><br><span class="line">  [T <span class="keyword">in</span> keyof <span class="title class_">Obj</span> <span class="keyword">as</span> <span class="string">`<span class="subst">$&#123;T&#125;</span>_suffix`</span>]: <span class="title class_">Obj</span>[T];</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// type NewObj&lt;T&gt; = &#123;</span></span><br><span class="line"><span class="comment">//     name_suffix: string;</span></span><br><span class="line"><span class="comment">//     gender_suffix: string;</span></span><br><span class="line"><span class="comment">//     age_suffix: number;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如果要过滤某些属性，JS可以使用<code>delete</code>方法，而在类型编程中，则通过<code>as</code>将类型转为<code>never</code>实现剔除某些属性。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">NewObj1</span>&lt;T&gt; =&#123;</span><br><span class="line">  [T <span class="keyword">in</span> keyof <span class="title class_">Obj</span> <span class="keyword">as</span> <span class="title class_">Obj</span>[T] <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="built_in">never</span> : T ]: <span class="title class_">Obj</span>[T];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// type NewObj1&lt;T&gt; = &#123;</span></span><br><span class="line"><span class="comment">//     name: string;</span></span><br><span class="line"><span class="comment">//     gender: string;</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure><h3 id="模式匹配"><a href="#模式匹配" class="headerlink" title="模式匹配"></a>模式匹配</h3><p>在JS的正则表达式中，括号具有一个很重要的作用，就是用来进行模式提取，可以通过<code>$1</code><em>、</em><code>$2</code><em>、</em><code>$3</code> **指代相应的分组，例如下面代码中，<code>$1</code>表示第一个分组<code>\d&#123;4&#125;</code>，<code>$2</code>表示第一个分组<code>\d&#123;2&#125;</code>，<code>$3</code>表示第一个分组<code>\d&#123;2&#125;</code>。通过字符串的<code>replace</code>方法，我们可以实现字符串的位置替换。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> regex = <span class="regexp">/(\d&#123;4&#125;)-(\d&#123;2&#125;)-(\d&#123;2&#125;)/</span>;</span><br><span class="line"><span class="keyword">const</span> <span class="built_in">string</span> = <span class="string">&quot;2023-08-13&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> result = <span class="built_in">string</span>.<span class="title function_">replace</span>(regex, <span class="string">&quot;$2/$3/$1&quot;</span>);</span><br></pre></td></tr></table></figure><p>在TS类型语言中，主要通过<code>extends</code>,<code>infer</code>以及扩展运算符等组合使用实现模式匹配：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Str</span> = <span class="string">&#x27;foo-bar&#x27;</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Bar</span> = <span class="title class_">Str</span> <span class="keyword">extends</span> <span class="string">`foo-<span class="subst">$&#123;infer rest&#125;</span>`</span> ? rest : <span class="built_in">never</span> <span class="comment">// &#x27;bar&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Arr</span> = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>,<span class="number">4</span>,<span class="number">5</span>]</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">First</span> = <span class="title class_">Arr</span> <span class="keyword">extends</span> [infer F, ...infer <span class="title class_">Rest</span>] ? F : <span class="built_in">never</span> <span class="comment">// 1</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Last</span> = <span class="title class_">Arr</span> <span class="keyword">extends</span> [...infer <span class="title class_">Rest</span>, infer L] ? L : <span class="built_in">never</span> <span class="comment">// 5</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Mid</span> = <span class="title class_">Arr</span> <span class="keyword">extends</span> [infer F, ...infer <span class="title class_">Rest</span>, infer L] ? <span class="title class_">Rest</span> : <span class="built_in">never</span> <span class="comment">// [2,3,4]</span></span><br></pre></td></tr></table></figure><p>通过类型匹配，我们使用TS类型编程来模拟JS中的各种数组和字符串方法。具体举例可以参考<a href="https://juejin.cn/book/7047524421182947366?scrollMenuIndex=1"><strong>神光</strong>的掘金小册</a>，里面有一个章节是讲模式提取。</p><h3 id="函数递归"><a href="#函数递归" class="headerlink" title="函数递归"></a>函数递归</h3><p>像很多纯函数式编程语言一样，TS的类型语言虽然没有<code>for</code>循环实现数据迭代，但是有递归，递归可以代替了循环迭代。</p><p>如果用JS，使用递归来实现一个数组的翻转，代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> target = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">reverse</span> = (<span class="params">target, n = target.length, arr = []</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (arr.<span class="property">length</span> === n) &#123;</span><br><span class="line">    <span class="keyword">return</span> arr;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> pre = arr;</span><br><span class="line">  <span class="keyword">const</span> cur = target[arr.<span class="property">length</span>];</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">reverse</span>(target, n, [cur, ...pre]);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> res = <span class="title function_">reverse</span>(target);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(res); <span class="comment">// [5,4,3,2,1]</span></span><br></pre></td></tr></table></figure><p>类比到类型编程中，我们使用条件分支、默认参数、递归来实现成等价的类型编码，代码如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> reverse&lt;</span><br><span class="line">  T <span class="keyword">extends</span> <span class="built_in">number</span>[],</span><br><span class="line">  N = T[<span class="string">&quot;length&quot;</span>],</span><br><span class="line">  U <span class="keyword">extends</span> <span class="built_in">number</span>[] = []</span><br><span class="line">&gt; = U[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> N ? U : reverse&lt;T, N, [T[U[<span class="string">&quot;length&quot;</span>]], ...U]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> target = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"><span class="keyword">type</span> res1 = reverse&lt;target&gt; <span class="comment">// [5,4,3,2,1]</span></span><br></pre></td></tr></table></figure><p>看到这里是不是很惊讶，原来TS的类型编码和JS编码其实一模一样，只不是换了一套语法规则！让我们再用斐波那契递归来体验一把。</p><p>一个简单的斐波那契数列，其用JS实现的算法如下：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">fib</span> = (<span class="params">n</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">if</span> (n === <span class="number">1</span>) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">fib</span>(n - <span class="number">1</span>) + <span class="title function_">fib</span>(n - <span class="number">2</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">fib</span>(<span class="number">6</span>)); <span class="comment">// 8</span></span><br></pre></td></tr></table></figure><p>对应的TS类型编码如下：可以看到类型编码的实现比JS更加复杂一点，因为要实现加减操作，我们需要定义额外的类型实现。在TS的类型编码中，我们可以根据数组类型的<code>length</code>获取到数字，然后通过<code>模式匹配</code>以及<code>扩展运算符</code>实现数组长度的加减，进行实现基本的代数运算。</p><p>试试这个 👉👉 <a href="https://www.typescriptlang.org/play#code/C4TwDgpgBAYglgIwDwBUoQB7AgOwCYDOUOArgLYIQBOAfFALxRqbb5EAMAUFFAPxRceALibosuQlACM3PtNkiAgnjxJ4yAMolkKADTSaNfeqRad+gEyGaAbk6dQkKGdRSxrSaQrV9KC+4kiL0paBigAIRI4ABs8RSoqVzoWQKgAbVkAOmyAQwSpEUiYuITUK10s3ISLETgcADNqKAAlCAJgTgBdWX5W9rSAImjcAHNgAAsB7uFiCAA3ajsHcGhlVRQ3FLZichDffy3PXeo6RgyebMy8qgKIqNj4xI2jSqvqwvuSp6sutIByYY4MbjP6dJaOaBFB6lWQAOXIAW2wR8skeiMkJBwAGscAB7ADuODSnTCxM4pygj0GgOBU3RRHhZDkaI+xUeSEZ+jSUn0l0enVs9ghUCobUYJgAbHQAPTSqAADiAA">playground</a> 👈👈</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Fib</span>&lt;T <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = T <span class="keyword">extends</span> <span class="number">0</span></span><br><span class="line">  ? <span class="number">0</span></span><br><span class="line">  : T <span class="keyword">extends</span> <span class="number">1</span></span><br><span class="line">  ? <span class="number">1</span></span><br><span class="line">  : <span class="title class_">Add</span>&lt;<span class="title class_">Fib</span>&lt;<span class="title class_">Sub</span>&lt;T, <span class="number">1</span>&gt;&gt;, <span class="title class_">Fib</span>&lt;<span class="title class_">Sub</span>&lt;T, <span class="number">2</span>&gt;&gt;&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Sub</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt; <span class="keyword">extends</span> [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: infer <span class="title class_">Rest</span></span><br><span class="line">]</span><br><span class="line">  ? <span class="title class_">Rest</span>[<span class="string">&quot;length&quot;</span>]</span><br><span class="line">  : <span class="built_in">never</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Add</span>&lt;<span class="variable constant_">T1</span> <span class="keyword">extends</span> <span class="built_in">number</span>, <span class="variable constant_">T2</span> <span class="keyword">extends</span> <span class="built_in">number</span>&gt; = [</span><br><span class="line">  ...<span class="attr">arr1</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T1</span>&gt;,</span><br><span class="line">  ...<span class="attr">arr2</span>: <span class="title class_">BuildArr</span>&lt;<span class="variable constant_">T2</span>&gt;</span><br><span class="line">][<span class="string">&#x27;length&#x27;</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">BuildArr</span>&lt;</span><br><span class="line">  <span class="title class_">Num</span> <span class="keyword">extends</span> <span class="built_in">number</span>,</span><br><span class="line">  <span class="title class_">Arr</span> <span class="keyword">extends</span> <span class="built_in">unknown</span>[] = []</span><br><span class="line">&gt; = <span class="title class_">Arr</span>[<span class="string">&quot;length&quot;</span>] <span class="keyword">extends</span> <span class="title class_">Num</span> ? <span class="title class_">Arr</span> : <span class="title class_">BuildArr</span>&lt;<span class="title class_">Num</span>, [<span class="number">1</span>, ...<span class="title class_">Arr</span>]&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> res= <span class="title class_">Fib</span>&lt;<span class="number">6</span>&gt; <span class="comment">// 8</span></span><br></pre></td></tr></table></figure><p>写到这里，我们见证了TS的类型系统强大的类型编程能力，事实上，TS的潜力远不止上述这些主要的类型操作，目前已经有不少文章证明了TS的类型系统具有<a href="https://itnext.io/typescript-and-turing-completeness-ba8ded8f3de3">图灵完备</a>（Turing completeness) 特性，一个系统具有图灵完备特性，意味着它可以表达各种算法，实现各种逻辑运算。</p><p>所以我们可以理解，TS的类型编程其实已经远远超出了简单的、用一个已知的类型（或者<code>union</code>, <code>intersection</code>, <code>record</code>后的类型)给JS变量进行注解的范围，用TS实现的类型编码确实可以变得异常复杂（但是这种复杂大部分生产环境用不到）。</p><p>如果想更近一步探索TS极限类型编程，我觉得<a href="https://www.learningtypescript.com/articles/extreme-explorations-of-typescripts-type-system">这篇文章</a>很不错，其中列举了用纯TS类型编程实现的应用案例，如模拟一个4比特的虚拟机、SQL数据库引擎、国际象棋等等。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文主要讲解了TS的泛型和类型编程。通过类比JS这类普通编程语言中的基本特性，我们在TS的类型编程中找到了对应的实现方式。</p><p>尽管类型编程比普通编程更复杂，甚至被很戏谑为类型体操，但是这复杂的原因，其实是这种技术JSer们很新鲜，而且不常用。我们接触的大部分语言的类型系统，支持基本的泛型其实就足够了。但是为了契合高度灵活的JS，TS还需要更加灵活的类型编程系统。</p><p>另外，使用类型进行编程，比起传统的利用各种API和操作符来实现对数据值的处理，也是一个需要适应的思维转换。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/19/TS%E7%B1%BB%E5%9E%8B%E7%BC%96%E7%A8%8B%EF%BC%9A%E7%B1%BB%E5%9E%8B%E6%98%AF%E4%B8%80%E7%AD%89%E5%85%AC%E6%B0%91/</id>
    <link href="https://believed-breadfruit.top/2023/10/19/TS%E7%B1%BB%E5%9E%8B%E7%BC%96%E7%A8%8B%EF%BC%9A%E7%B1%BB%E5%9E%8B%E6%98%AF%E4%B8%80%E7%AD%89%E5%85%AC%E6%B0%91/"/>
    <published>2023-10-19T13:47:36.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>TypeScript（TS）的类型系统十分强大，除了定义了各种类型方便我们对变量、函数、类等进行类型约束，实现类型安全，而且还支持泛型，可以]]>
    </summary>
    <title>TS类型编程：类型是一等公民</title>
    <updated>2024-12-22T06:31:08.046Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://believed-breadfruit.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<blockquote><p>In TypeScript, it’s better to think of a type as a <em>set of values</em> that share something in common. Because types are just sets, a particular value can belong to <em>many</em> sets at the same time.  —— <a href="https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html#types-as-sets">TypeScript官方文档</a></p></blockquote><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>我们都知道，计算机是用二进制来保存所有数据的，计算机的本质是对二进制数据进行各种逻辑运算，但是对于我们人类来讲，使用0101这样的语言直接和计算机沟通十分困难，所以在此基础上，我们逐渐发展出了人能看懂的高级语言。</p><p>在高级语言中，我们对0101的具体数据抽象出了变量、类型与值等概念。值是计算机直接进行逻辑运算的实际数据，变量是一个存储值的容器，类型则定义了一个变量可以保存、或者一个值所属的数据类型。不同的数据类型具有不同的运算和操作，如JS的number类型可以进行加减乘除四则运算，object类型可以进行索引访问每一个元素。对object类型的数据进行加减乘除肯定会报错，对number类型的数据进行下标索引肯定也会报错。所以，对于编程语言来讲，都需要一套类型系统，可以在编译的时候实现类型检查，及时对类型不匹配的问题进行报错，或者在运行的时候进行隐式转换，防止程序发生错误。</p><p>JavaScript (JS)是一种弱类型、动态的语言，其内部规定了八种数据类型： <code>number</code>、<code>boolean</code>、<code>string</code>、<code>object</code>、<code>bigint</code>、<code>symbol</code>、<code>undefined</code>、<code>null</code>。变量在定义的时候不会直接和某一特定的类型联系，可以保存任何类型的数据，具体的类型会在运行时计算出来。</p><p>TypeScript (TS)是JS的超集，而且是在编译时做类型检查的静态类型系统。为了兼容JS的所有语法、运行时类型检查以及实现自己独有的静态类型检查，TS的类型系统除了包含了JS的所有类型，还多加了一些类型，如元组（Tuple）、接口（Interface）、枚举（Enum），而且还支持泛型，提供了各种类型运算，可以实现强大的类型编程。</p><p>TS的类型系统比JS复杂得多，如何理解TS的各种类型和类型运算，是我们熟练使用TS的前提。本文将数学上的集合观点代入到TS的类型体系中，帮助大家更好地使用TS进行编程。</p><h2 id="高中数学里的集合"><a href="#高中数学里的集合" class="headerlink" title="高中数学里的集合"></a>高中数学里的集合</h2><p>集合是<a href="http://www.tup.com.cn/upload/books/yz/075970-01.pdf">集合论</a>中最基本的概念。在高中数学里，我们学会了集合的概念。我们可以把方程$x^2-1&#x3D;0$的实数解看成一个集合，全部的自然数看成一个集合，某高校的全体学生看成一个集合，平面上所有点看成一个集合。</p><p>对于集合的表示，通常有三种。</p><p>第一种是使用<code>&#123;&#125;</code>以及<code>,</code>把一个集合的所有元素列举出来描述，例如<code>A=&#123;1,2,3,4&#125;</code>，<code>B=&#123;a,b,c,d&#125;</code>，<code>C=&#123;true, false&#125;</code>。</p><p>如果<code>&#123;&#125;</code>中的元素有无限个，无法列举，或者具有某种规律，则可以用第二种方法来描述，也就是将里面的元素用一些变量或者符号来描述，${x \in S | p(x)}$，表示使<code>p(x)</code>为真的所有<code>x</code>的集合，且<code>x</code>满足属于<code>S</code>集合中。</p><p>第三种集合的表示则是画图法，如下图所示，用一个圈表示一个集合，集合内部有一些元素，集合和集合具有一定关系。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.59izbucgi600.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.59izbucgi600.webp</a></p><p>对于一些常见的集合，在数学上，规定了一些字符来描述。例如全体实数集合<code>R</code>，全体自然数集合<code>N</code>，有理数集合<code>Q</code>，不包含任何元素的空集$\emptyset$。</p><p>在了解完集合的概念后，我们开始学习集合间的关系。如子集，表示集合之间的包含关系，如果A是B的子集，即$A \subset B$，表示A集合中的所有元素都属于B集合。与子集相对立的是超集，如果A是B的子集，那么B则为A的超集。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.2hy7a91ygpu0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.2hy7a91ygpu0.webp</a></p><p>除此之外，两个集合之间的关系可能还存在相等、有交集、没有交集的情况，为了更好的描述，我们又多出了“交集”、“并集“和”补集“的概念。</p><p>交集：$c \in A, c \in B$，那么所有c元素组成的集合是A和B集合的交集，记作$C &#x3D; A \cap B$；</p><p>并集：A和B两个集合的所有元素合并在一起组成的新集合C，记作$C &#x3D; A \cup B$；</p><p>补集：在一个大集合中，所有属于C集合而不属于A集合的元素，记作$C&#x3D;\bar A$或者C &#x3D; A‘。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.26pbglv7t4gw.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.26pbglv7t4gw.webp</a></p><p>集合运算同其他代数运算一样, 都遵循一定的运算律. 下面列出了一些常用的主要运算律：</p><ul><li>幂等律： $A \cup A &#x3D; A,A \cap A &#x3D; A$</li><li>交换律：$A \cap B &#x3D; B \cap A,A \cup B &#x3D; B \cup A$</li><li>结合律：$(A \cup B) \cup C &#x3D; A \cup (B \cup C),(A \cap B) \cap C &#x3D; A \cap (B \cap C)$</li><li>分配律：$A \cup (B \cap C) &#x3D; (A \cup B) \cap (A \cup C),A \cap (B \cup C) &#x3D; (A \cap B) \cup (A \cap C)$</li></ul><h2 id="TS概念映射到集合中"><a href="#TS概念映射到集合中" class="headerlink" title="TS概念映射到集合中"></a>TS概念映射到集合中</h2><p>回顾了高中学习的集合知识，现在我们来把它用到TS类型中。</p><h3 id="理解类型与值"><a href="#理解类型与值" class="headerlink" title="理解类型与值"></a>理解类型与值</h3><p>在集合论中，我们把类型看成是一个具有若干值的集合，这些值就是集合里的元素，集合与集合之间具有一定关系，可以帮助我们理解TS类型与类型之间的关系。</p><p>在TS中，类型与值的关系，就好比是数学里的集合与元素的关系。例如<code>number</code>类型，可以看成这个集合里包含了TS中所有的数字值；<code>string</code>类型可以看成一个包含了所有可能字符串值的集合；<code>boolean</code>集合只有两个元素：<code>true</code>和<code>false</code>，<code>undefined</code>和<code>null</code>类型分别只有一个元素：<code>undefined</code>和<code>null</code>。<code>Object</code>类型包含了所有的（类）对象数据结构，如函数，日期，正则，对象等。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.630pvgaooks0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.630pvgaooks0.webp</a></p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.5gzanbc2n9w0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.5gzanbc2n9w0.webp</a></p><p>和大部分的编程语言一样，TS也有一个常见的赋值表达式：在这串代码中，<code>name</code>和<code>age</code>是一个变量，分别定义了<code>string</code> 和<code>number</code>类型，被赋值了一个相同值”<code>Alice</code>”。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="attr">name</span>: <span class="built_in">string</span> = <span class="string">&quot;Alice&quot;</span>; <span class="comment">// 合法</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">age</span>: <span class="built_in">number</span> =  <span class="string">&quot;Alice&quot;</span>; <span class="comment">// 不合法：Type &#x27;string&#x27; is not assignable to type &#x27;number&#x27;</span></span><br></pre></td></tr></table></figure><p>我们用集合的观点来解读上面这片代码的类型定义，是这样的：</p><ul><li><code>Alice</code>这个元素（值）属于<code>string</code>集合（类型），所以把它赋值给被<code>string</code>类型约束的<code>name</code>变量，是合法的，可以通过TS的类型检验；</li><li>但是<code>Alice</code>这个元素不在<code>number</code>集合中，所以把它赋值给被<code>number</code>类型约束的<code>age</code>变量，TS会报错</li></ul><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.4kzd5jhc2tq0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.4kzd5jhc2tq0.webp</a></p><p>在TS中，一个值直接赋值给一个变量，我们可以通过元素与集合之间的关系来很容易来解读。那么如果是一个变量赋值给另一个变量呢，这种解释还能成立吗？我们看看下面这片代码：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> num = <span class="number">1</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">num1</span>:<span class="built_in">number</span> = num;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">num2</span>:&#123;&#125; = num1;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">num3</span>:<span class="built_in">number</span> = num2; <span class="comment">// error: Type &#x27;&#123;&#125;&#x27; is not assignable to type &#x27;number&#x27;.</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在第4行代码中，<code>num2</code>的值是1，但是赋值给被<code>number</code>类型约束的<code>num3</code>变量却报错。错误的原因是<code>num2</code>的值虽然是1，但是其类型是<code>&#123;&#125;</code>，类型<code>&#123;&#125;</code>的值无法赋值给<code>number</code>类型的变量。这说明用元素和集合的关系来判断TS类型的赋值问题非常局限，我们需要新的解释模型。</p><p>让我们来试试用集合之间关系来解释上述代码。<code>num</code>类型是1，是TS的<a href="https://www.typescriptlang.org/docs/handbook/literal-types.html">字面量类型</a>，可以赋值给<code>number</code>类型的<code>num1</code>，说明字面量类型1是<code>number</code>类型的子集，所以TS允许赋值；<code>num2</code>类型是<code>&#123;&#125;</code>，是一个没有属性的<code>interface</code>类型，不能赋值给<code>number</code>类型，但是可以被<code>number</code>类型赋值，说明<code>number</code>类型是<code>&#123;&#125;</code>的真子集，如下图所示。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.cxcvd96pwmo.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.cxcvd96pwmo.webp</a></p><p>所以，TS中的类型赋值问题，可以看成是不同类型之间的集合关系。A是B的子集，所以A类型可以赋值给B类型。</p><p>对于TS中的<code>string</code>, <code>number</code>, and <code>boolean</code>等基础类型，其子集不仅包含这个类型本身，还有这个类型包含的所有值元素表示的字面量类型。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.79xwwz8r14w0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.79xwwz8r14w0.webp</a></p><h3 id="理解never和unknown"><a href="#理解never和unknown" class="headerlink" title="理解never和unknown"></a><strong>理解<code>never</code>和<code>unknown</code></strong></h3><p>TS提供了<code>any</code>, <code>never</code>, <code>unknow</code>, <code>void</code>类型，这四个类型JS都没有，但是肯定有它存在的道理。</p><p>TS的<code>never</code>表示的是永不存在的值的类型，什么情况下会有不可能存在的值呢？如果一个值既是0又是1，这个值肯定不存在，那么这个值就是<code>never</code>类型。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> a = A &amp; <span class="built_in">never</span>; <span class="comment">// never </span></span><br><span class="line"><span class="keyword">type</span> b =  A | <span class="built_in">never</span>; <span class="comment">// A </span></span><br><span class="line"><span class="keyword">type</span> c = <span class="title class_">Exclude</span>&lt;<span class="number">0</span>, <span class="number">0</span>&gt;; <span class="comment">// never</span></span><br><span class="line"><span class="keyword">type</span> d =  <span class="number">0</span> &amp; <span class="number">1</span>; <span class="comment">// never</span></span><br></pre></td></tr></table></figure><p>联想到集合，<code>never</code>类型其实就是空集，表示一个不存在任何元素的集合，而且空集是任何集合的子集，所以<code>never</code>类型可以赋值给任何类型，但是任何类型都不能赋值给<code>never</code>类型。</p><p>下表给出了<a href="https://www.typescriptlang.org/docs/handbook/type-compatibility.html#any-unknown-object-void-undefined-null-and-never-assignability">TS官方文档</a>中给出的常见类型的赋值特性（assignability）。</p><table><thead><tr><th></th><th>any</th><th>unknown</th><th>object</th><th>void</th><th>undefined</th><th>null</th><th>never</th></tr></thead><tbody><tr><td>any</td><td></td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td>✕</td></tr><tr><td>unknown</td><td>✓</td><td></td><td>✕</td><td>✕</td><td>✕</td><td>✕</td><td>✕</td></tr><tr><td>object</td><td>✓</td><td>✓</td><td></td><td>✕</td><td>✕</td><td>✕</td><td>✕</td></tr><tr><td>void</td><td>✓</td><td>✓</td><td>✕</td><td></td><td>✕</td><td>✕</td><td>✕</td></tr><tr><td>undefined</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td></td><td>✓</td><td>✕</td></tr><tr><td>null</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td></td><td>✕</td></tr><tr><td>never</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td>✓</td><td></td></tr></tbody></table><p>从上面这张表中，我们还可以看出<code>unknown</code>类型可以被所有类型赋值，但是除了<code>any</code>外，不能被任何类型赋值，所以<code>unknown</code>类型是一个全集，与数学上的所有自然数集合<code>N</code>类似。此处引用一下<a href="https://juejin.cn/post/6844904182843965453?searchId=202308011624346198C61171204F01C9B0#heading-10"><strong>1.2W字 | 了不起的 TypeScript 入门教程</strong></a>的代码用例：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="attr">value</span>: <span class="built_in">unknown</span>;</span><br><span class="line"></span><br><span class="line">value = <span class="literal">true</span>; <span class="comment">// OK</span></span><br><span class="line">value = <span class="number">42</span>; <span class="comment">// OK</span></span><br><span class="line">value = <span class="string">&quot;Hello World&quot;</span>; <span class="comment">// OK</span></span><br><span class="line">value = []; <span class="comment">// OK</span></span><br><span class="line">value = &#123;&#125;; <span class="comment">// OK</span></span><br><span class="line">value = <span class="title class_">Math</span>.<span class="property">random</span>; <span class="comment">// OK</span></span><br><span class="line">value = <span class="literal">null</span>; <span class="comment">// OK</span></span><br><span class="line">value = <span class="literal">undefined</span>; <span class="comment">// OK</span></span><br><span class="line">value = <span class="keyword">new</span> <span class="title class_">TypeError</span>(); <span class="comment">// OK</span></span><br><span class="line">value = <span class="title class_">Symbol</span>(<span class="string">&quot;type&quot;</span>); <span class="comment">// OK</span></span><br></pre></td></tr></table></figure><h3 id="理解any"><a href="#理解any" class="headerlink" title="理解any"></a><strong>理解<code>any</code></strong></h3><p>看到这里大家可能觉得很奇怪，对着上面这张表，我们可以看到所有类型都可以赋值给<code>any</code>类型，为什么<code>any</code>类型不是全集呢？</p><p>因为从集合论来看，全集类型只能是<code>unknown</code>类型，<code>any</code>类型可以赋值给任何类型，已经不符合全集的概念。<code>any</code>类型可以看成是<code>never</code>和<code>unknow</code>类型的混合类型，既有<code>never</code>类型的特性，可以赋值给任何类型；又有<code>unknown</code>类型的特性，任何类型都可以赋值给它。</p><p>所以严格来说<code>any</code>不能看成是集合。那TS为什么设计<code>any</code>类型呢？一个很重要的原因，我觉得和TS的定位有关，TS是JS的超集，TS必然需要提供一种能力可以回退为JS，在TS中将所有变量都定义为<code>any</code>类型，相当于是禁用TS的静态类型检查功能，让TS退化成动态弱类型的JS。</p><h3 id="理解void"><a href="#理解void" class="headerlink" title="理解void"></a><strong>理解<code>void</code></strong></h3><p>现在我们来想想<code>void</code>类型，很多资料里说<code>void</code>表示没有任何类型，这句话讲了好像没讲一样，让我们来用集合的观点理解<code>void</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// The inferred return type is void</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">noop</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有了集合的概念后，我们有两个思考角度，第一，<code>void</code>集合里能放什么值元素？只能放<code>undefined</code>。第二，<code>void</code>集合里能放什么类型元素？有三个：<code>never</code>, <code>void</code>, <code>undefined</code>。</p><p>试试这个 👉👉 <a href="https://www.typescriptlang.org/play?ssl=22&ssc=51&pln=14&pc=1#code/FAGwpgLgBAbghiArmAXLA9gSwCYG5jxJhQC8UEATsrlAPS1QAqAngA7EDkARuuuHADsOUTAGcoA9NDijRmAOYC4XcOXTk2nGFmwcCCZKSgAWAEw16TTVA4DEAWy5gKwsRKlQZcxctUR1ENYc2jh6hIZkAEQAEmAgIOoA6ugUINiRFgws7DailJgC8q7iktKyCkoqxP4aOcE6HAB0+kRGANoAuplWdQJgMM6dxe5l3pV+AUEhui0RUADeAL7d2ZxLw6We5T5VarVaDbPEZACycBAAFo0Ugtjo9itBABQAlKQAfBIOTi4iJR5eCq+aqTOrTMIGY5feKPXqIeIbAHbcYg-Y2cFHIyIATYMAAMwKYDwdAYAHkANKYsh9ADuPTAAFEKBQUq9YZxVkyWb83JtATsJmj6qEqVAAMrMRx8J6RQLsSIvdm5SU8ECI0ZA3Y1OUHEXAYA68gARiMfQGFCgYAAHhAwDjxNMoAB+chUYhoPEIURgbqUZAG6wQUxYnH4wnYS02u3YB06Z2uwwer0+kkJsABnIQADMRkd1tt9owOHjfvdUE9IG9vrd+sNEGMIYA1pIaQJIwWY0WIy7S1Ak5WU5YK96M9UAKym+EgdvR2PFntuvvl5PdYfpusANiMJvzs67JcX-arqbXo-IAHYjEsZ4XHQvE8uB6vk0A">playground</a> 👈👈</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="attr">value</span>: <span class="built_in">void</span>;</span><br><span class="line">value = <span class="literal">true</span>; <span class="comment">// Type &#x27;boolean&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = <span class="number">42</span>; <span class="comment">// Type &#x27;number&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = <span class="string">&quot;Hello World&quot;</span>; <span class="comment">// Type &#x27;string&#x27; is not assignable to type &#x27;void&#x27;.</span></span><br><span class="line">value = []; <span class="comment">// Type &#x27;never[]&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = &#123;&#125;; <span class="comment">// Type &#x27;&#123;&#125;&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = <span class="title class_">Math</span>.<span class="property">random</span>; <span class="comment">// Type &#x27;() =&gt; number&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = <span class="literal">null</span>; <span class="comment">// Type &#x27;null&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = <span class="literal">undefined</span>; <span class="comment">// OK</span></span><br><span class="line">value = <span class="keyword">new</span> <span class="title class_">TypeError</span>(); <span class="comment">// Type &#x27;TypeError&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line">value = <span class="title class_">Symbol</span>(<span class="string">&quot;type&quot;</span>); <span class="comment">// Type &#x27;symbol&#x27; is not assignable to type &#x27;void&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> t1 = <span class="built_in">never</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> t2 = <span class="literal">undefined</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> t3 = <span class="built_in">void</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> t4 = <span class="built_in">unknown</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"><span class="keyword">type</span> t5 = <span class="literal">null</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"><span class="keyword">type</span> t6 = <span class="number">1</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"><span class="keyword">type</span> t7 = &#123;&#125; <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br></pre></td></tr></table></figure><p>知道了这两个思考点，我们很直观地看出<code>void</code>类型的本质，以及理解容易和<code>void</code>类型混淆的<code>undefined</code>类型的区别，如下图所示。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.s299iag04sw.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.s299iag04sw.webp</a></p><p>从元素上来看，<code>void</code>和<code>undefined</code>都只能设置<code>undefined</code>这个值，所以尽管在<code>void</code>的使用场景中，函数不<code>return</code>任何数，这个返回值其实还是<code>undefined</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// The inferred return type is void</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">noop</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="title function_">noop</span>() + <span class="string">&#x27;aaa&#x27;</span>) <span class="comment">// &quot;undefinedaaa&quot;</span></span><br></pre></td></tr></table></figure><p>从集合间的关系来看，<code>undefined</code>类型不包含<code>void</code>类型，说明<code>void</code>类型有其独特的存在性，不能用<code>undefined</code>来代替。那TS为什么设置void类型？</p><p>因为TS的设定就是要成为JS的超集，要兼容JS的所有语法，只是在JS的基础上加一套更严格的静态检查机制，而<code>undefined</code>类型被TS设定为像<code>number</code>, <code>string</code>, <code>boolean</code>等基础类型一样，只能被对应的数据类型赋值。<code>void</code>类型被创造出来是为了兼容JS的一些原型方法，使其可以经过TS的类型检查，而不被认定为类型错误。比如原生的<code>Array.prototype.forEach</code>方法。</p><p>试试这个 👉👉 <a href="https://www.typescriptlang.org/play#code/CYUwxgNghgTiAEAzArgOzAFwJYHtVJxgFEowALARgB4AVAPgApYYAueGgbQF0AaeMKBAgAjUgGs2DEBDY0AlPAC8deADccWYHLbrNAbgBQEEBngZYAcxMU2qZAFthIGNyXxuhxIRLkKDDhR8AEx8AMy88NJKKuYwVhgUAHQADsgAzmRSEHJyevAA9PnwAPIA0gYVoJCwCCjo2HgExKRkQbSMzLLcfAJComAS8FmyCsrwaKCIWKggWjoawIbGprHxQbYOTi5cbh4GXs3kQf6B8CHw4XxRY6smQSnpmdI5eYXsAJ7JCADkdo7O33gWDS8FQOFMUDSaSwFlQUGExjMODMnx+ExAUxmwG+iSAA">playground</a> 👈👈</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">declare</span> <span class="keyword">function</span> forEach1&lt;T&gt;(<span class="attr">arr</span>: T[], <span class="attr">callback</span>: <span class="function">(<span class="params">el: T</span>) =&gt;</span> <span class="built_in">void</span>): <span class="built_in">void</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="attr">target1</span>: <span class="built_in">number</span>[] = [];</span><br><span class="line"><span class="title function_">forEach1</span>([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>], <span class="function"><span class="params">el</span> =&gt;</span> target1.<span class="title function_">push</span>(el)); <span class="comment">// OK</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">declare</span> <span class="keyword">function</span> forEach2&lt;T&gt;(<span class="attr">arr</span>: T[], <span class="attr">callback</span>: <span class="function">(<span class="params">el: T</span>) =&gt;</span> <span class="literal">undefined</span>): <span class="built_in">void</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="attr">target2</span>: <span class="built_in">number</span>[] = [];</span><br><span class="line"><span class="title function_">forEach2</span>([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>], <span class="function"><span class="params">el</span> =&gt;</span> target2.<span class="title function_">push</span>(el)); <span class="comment">// Type &#x27;number&#x27; is not assignable to type &#x27;undefined&#x27;.</span></span><br></pre></td></tr></table></figure><h3 id="理解结构式类型匹配"><a href="#理解结构式类型匹配" class="headerlink" title="理解结构式类型匹配"></a>理解结构式类型匹配</h3><p>在TS中，定义一个有具体属性的对象类型有三种写法：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 接口</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">IPerson</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 类型别名</span></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">TPerson</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对象类型</span></span><br><span class="line"><span class="keyword">let</span> <span class="attr">person</span>:&#123; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">age</span>: <span class="built_in">number</span>&#125;;</span><br></pre></td></tr></table></figure><p>这三种写法本质都一样。如果是用一个字面量的值赋值，该值所具有的属性不能多不能少，否则TS会报错。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">IPerson</span> &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">type</span> <span class="title class_">TPerson</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="built_in">string</span>;</span><br><span class="line">  <span class="attr">age</span>: <span class="built_in">number</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">person1</span>:&#123; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">age</span>: <span class="built_in">number</span> &#125; = &#123; <span class="attr">name</span>: <span class="string">&#x27;1&#x27;</span>,<span class="attr">age</span>:<span class="number">1</span>, <span class="attr">gender</span>: <span class="string">&#x27;male&#x27;</span> &#125;; <span class="comment">// error: &#x27;gender&#x27; does not exist in type &#x27;&#123; name: string; age: number; &#125;&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">person2</span>:<span class="title class_">IPerson</span> =  &#123; <span class="attr">name</span>: <span class="string">&#x27;1&#x27;</span>,<span class="attr">age</span>:<span class="number">1</span>, <span class="attr">gender</span>: <span class="string">&#x27;male&#x27;</span> &#125; <span class="comment">// error: &#x27;gender&#x27; does not exist in type &#x27;IPerson&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">person3</span>:<span class="title class_">TPerson</span> =  &#123; <span class="attr">name</span>: <span class="string">&#x27;1&#x27;</span>,<span class="attr">age</span>:<span class="number">1</span>, <span class="attr">gender</span>: <span class="string">&#x27;male&#x27;</span> &#125; <span class="comment">// error: &#x27;gender&#x27; does not exist in type &#x27;TPerson&#x27;</span></span><br></pre></td></tr></table></figure><p>但是如果用一个变量赋值，该变量的值就不再遵循“不多不少”的原则，而是”只多不少”。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 结构式类型匹配 shape</span></span><br><span class="line"><span class="keyword">const</span> more_person = &#123; <span class="attr">name</span>: <span class="string">&#x27;1&#x27;</span>,<span class="attr">age</span>:<span class="number">1</span>, <span class="attr">gender</span>: <span class="string">&#x27;male&#x27;</span> &#125;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">m_person1</span>:<span class="title class_">IPerson</span> = more_person;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">m_person2</span>:<span class="title class_">TPerson</span> = more_person;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">m_person3</span>:&#123; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">age</span>: <span class="built_in">number</span> &#125; = more_person;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> less_person = &#123; <span class="attr">name</span>: <span class="string">&#x27;1&#x27;</span> &#125;</span><br><span class="line"><span class="keyword">const</span> <span class="attr">l_person1</span>:<span class="title class_">IPerson</span> = less_person; <span class="comment">// Property &#x27;age&#x27; is missing in type &#x27;&#123; name: string; &#125;&#x27; but required in type &#x27;IPerson&#x27;.</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">l_person2</span>:<span class="title class_">TPerson</span> = less_person; <span class="comment">// Property &#x27;age&#x27; is missing in type &#x27;&#123; name: string; &#125;&#x27; but required in type &#x27;TPerson&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">l_person3</span>:&#123; <span class="attr">name</span>: <span class="built_in">string</span>; <span class="attr">age</span>: <span class="built_in">number</span> &#125; = less_person; <span class="comment">// Property &#x27;age&#x27; is missing in type &#x27;&#123; name: string; &#125;&#x27; but required in type &#x27;&#123; name: string; age: number; &#125;</span></span><br></pre></td></tr></table></figure><p>这种现象是与TS核心特性有关，TS的类型系统做的是<a href="https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html#structural-type-system">结构式匹配</a>，这意味着做类型检查的时候，只要有相同的<code>shape</code>，就能匹配成功。这说的是啥，光一句定义以及简单的几个示例真的很难理解TS的结构式匹配，让我们来用集合论理解试试。</p><p>我们来分析上面代码中蕴含的集合间的关系。<code>more_person</code>的类型是<code>&#123;name: string; age: number; gender: string;&#125;</code>，该类型是<code>IPerson</code>、<code>TPerson</code>以及<code>&#123; name: string; age: number &#125;</code> 的子集，所以<code>more_person</code>可以赋值给<code>m_person1</code>，<code>m_person2</code>，<code>m_person3</code>。</p><p><code>less_person</code>的类型是<code>&#123;name: string;&#125;</code>，该类型是<code>IPerson</code>、<code>TPerson</code>以及<code>&#123; name: string; age: number &#125;</code> 的超集，所以<code>less_person</code>无法赋值给<code>l_person1</code>，<code>l_person2</code>，<code>l_person3</code>。</p><p>为什么每增加一个属性，类型集合的空间就缩小，可赋值的元素就越少？</p><p>这是因为每增加一个属性，该类型可以匹配的值必然得包含这个属性，这样就使得符合该shape的值越来越少。我们用极限的思想可以进一步理解，如果一个接口类型没有任何属性，也就是<code>&#123;&#125;</code>类型，根据“只多不少”原则，所以的对象结构都可以赋值给这个类型；如果一个接口类型包含了所有可能的属性，那可以和这个类型匹配的对象，必然要包含所有这些可能的属性，不能少一个，无法多一个。可以看到，这两个极限条件下对应的集合，前者包含了后者，说明在可比的情况下，属性越少，集合越大，元素越多。</p><h3 id="理解TS类型运算"><a href="#理解TS类型运算" class="headerlink" title="理解TS类型运算"></a>理解TS类型运算</h3><p>有了对TS各种类型的理解基础，下面我们来讨论TS类型运算。</p><p><strong>理解联合类型和交叉类型</strong></p><p>在集合运算中，两个集合的并运算和交运算可以产生新的集合。例如$C&#x3D;A \cup B$，A和B集合的并运算产生了范围更大的C集合，A、B集合中的所有元素都属于C集合；同理，$D &#x3D; A \cap B$，A和B集合的交运算产生了范围更小的D集合，D中的所有元素都属于A，B集合。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.3xagxctz6qe0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.3xagxctz6qe0.webp</a></p><p>TS也有并运算和交运算，分别是联合类型(Union)和交叉类型(Intersection)。在TS中，联合类型可以将多个类型组合在一个，合并成一个范围更大的类型，多种类型之间使用 <code>|</code> 分隔，例如下面代码中的<code>StringOrNumber</code>类型，用集合的观点可以解读为：<code>StringOrNumber</code>集合 &#x3D; <code>string</code> 集合和<code>number</code>集合的并集，所以<code>StringOrNumber</code>类型可以同时被<code>string</code>类型和<code>Number</code>类型赋值。</p><p>交叉类型可以将多个类型变成一个范围更小的类型，使用<code>&amp;</code>分割，下面代码中定义的<code>StringAndNumber</code>类型，可以看成是对应的集合是<code>string</code>集合和<code>number</code>集合的交集，因为这俩集合没有重合，所以交集是空集，所以可以看到<code>StringAndNumber</code>类型是<code>never类型</code>（空集）。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">StringOrNumber</span> = <span class="built_in">string</span> | <span class="built_in">number</span>;</span><br><span class="line"><span class="comment">// string | number → both string and number are admissible</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">StringAndNumber</span> = <span class="built_in">string</span> &amp; <span class="built_in">number</span>;</span><br><span class="line"><span class="comment">// never → no type is ever admissible</span></span><br></pre></td></tr></table></figure><p>原始类型符合集合的交、并运算，对象类型也同样符合。在下图中，我们用交叉类型创造了范围更小的<code>t1</code>类型，范围更小，意味着属性越多，所以<code>t1</code>包含了<code>IPerson1</code>和<code>IPerson2</code>两个类型都有的属性；我们用联合类型创造了范围更大的t2类型，范围更大，意味着属性越少，所以<code>t2</code>包含了<code>IPerson1</code>和<code>IPerson2</code>两个类型共同有的属性<code>id</code>。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.5gk8b38oyxc0.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.5gk8b38oyxc0.webp</a></p><p>由于TS的<code>|</code>和<code>&amp;</code>很像JS的逻辑运算<code>or运算||</code>和<code>and运算&amp;&amp;</code>，如果将C&#x3D;A | B理解为C类型是A类型或者B类型，D &#x3D; A &amp; B理解为D类型既满足是A类型，又满足B类型，这种理解方式对于基础类型是适用的，但是无法解释接口类型，因为这种理解方式会给我们相反的结果，例如上图的中的<code>persion1</code>，根据合运算和或运算，会得出<code>person1</code>的属性为<code>id</code>，<code>person2</code>的属性为<code>id, name, age</code>。</p><p>用集合的交集和并集，可以让我们正确地理解联合类型和交叉类型。TS官方文档对<a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#working-with-union-types">union类型的解读</a>也正好印证了这一点。</p><blockquote><p>It might be confusing that a <em>union</em> of types appears to have the <em>intersection</em> of those types’ properties. This is not an accident - the name <em>union</em> comes from type theory. The <em>union</em> <code>number | string</code> is composed by taking the union <em>of the values</em> from each type. Notice that given two sets with corresponding facts about each set, only the <em>intersection</em> of those facts applies to the <em>union</em> of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about <em>every</em> person is that they must be wearing a hat.</p></blockquote><p><strong>理解条件类型</strong></p><p>TS的条件类型是通过<code>extends</code>关键字实现的，可以做条件判断和条件限制。</p><p>条件判断是<code>extends ? :</code>。很像JS的三元运算符。如果条件为<code>true</code>则返回<code>?</code>后的内容，否则返回<code>:</code>后的内容。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> R = <span class="string">&#x27;a&#x27;</span> <span class="keyword">extends</span> <span class="built_in">string</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> S = <span class="string">&#x27;a&#x27;</span> | <span class="string">&#x27;b&#x27;</span> <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> T = &#123; <span class="attr">a</span>: <span class="number">1</span>; <span class="attr">b</span>: <span class="number">2</span> &#125; <span class="keyword">extends</span> &#123; <span class="attr">a</span>: <span class="number">1</span> &#125; ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> U = &#123; <span class="attr">a</span>: <span class="number">1</span> &#125; <span class="keyword">extends</span> &#123; <span class="attr">a</span>: <span class="number">1</span>; <span class="attr">b</span>: <span class="number">2</span> &#125; ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br></pre></td></tr></table></figure><p>用集合来看，条件判断很简单，这个条件可以理解为前者是否是后者的子集，例如<code>A extends B ？C ： D</code>解读为A是B的子集吗？如果是那么取C， 否则取D。</p><p>条件类型的强大之处不是做静态计算，TS支持泛型以及复杂的类型编程，本质上是限制输入类型和变换输出类型。所以条件类型最重要的用法是和泛型一起使用。</p><p>如果传入的泛型是一个具体的类型（可以看成一个单独的集合）时，条件类型的结果我们用子集的概念一眼就能看出，如下面这片代码。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> isTwo&lt;T&gt; = T <span class="keyword">extends</span> <span class="number">2</span> ? <span class="attr">true</span>: <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> res = isTwo&lt;<span class="number">1</span>&gt;; <span class="comment">// false</span></span><br><span class="line"><span class="keyword">type</span> res2 = isTwo&lt;<span class="number">2</span>&gt;; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> res3 = isTwo&lt;<span class="number">1</span> | <span class="number">2</span>&gt;; <span class="comment">// boolean</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// res3类型等同于下面这种写法：</span></span><br><span class="line"><span class="keyword">type</span> res3 = (<span class="number">1</span> <span class="keyword">extends</span> <span class="number">2</span> ? <span class="literal">true</span> : <span class="literal">false</span>) | (<span class="number">2</span> <span class="keyword">extends</span> <span class="number">2</span> ? <span class="literal">true</span> : <span class="literal">false</span>) </span><br><span class="line">          = <span class="literal">true</span> | <span class="literal">false</span> </span><br><span class="line">          = <span class="built_in">boolean</span>;</span><br></pre></td></tr></table></figure><p>但是如果传入的是一个联合类型，情况变得复杂了一点，因为TS默认的运算规则是会把联合类型拆分判断（<a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#conditional-type-constraints">Distributive Conditional Types</a>）。即 <code>isTwo&lt;1 |2&gt;</code> 会被拆分为 <code>1 extends 2</code>、<code>2 extends 2，</code>最后的结果是拆分后结果的联系类型，所以在下面代码中，<code>isTwo&lt;1 | 2&gt;</code>的结果是<code>boolean</code>。</p><p>TS的这种分配条件规则，可以用集合的分配率来很好地解释：$(B \cup C) \cap A &#x3D; (B \cap A) \cup (C \cap A)$。我们可以把传入的联合类型看成是公式中的$(B \cup C)$ ，A为<code>extends</code>后面的类型，根据分配率扩展后，最终的结果是<code>B extends A ？X : Y | C extends A ？X : Y</code>。</p><p>如果想要打破TS这种默认分配规则，可以使用元组类型讲泛型看成一个整体，也就是单独一个集合。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> isTwo&lt;T&gt; = T <span class="keyword">extends</span> <span class="number">2</span> ? <span class="attr">true</span>: <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> res3 = isTwo&lt;[<span class="number">1</span> | <span class="number">2</span>]&gt;; <span class="comment">// false</span></span><br></pre></td></tr></table></figure><p>理解了集合分配率在TS中的使用，可以思考一下为啥下面的结果是这样：</p><p>试试这个 👉👉 <a href="https://www.typescriptlang.org/play#code/FAFwngDgpgBCCMMC8MCGA7MMoA8RXQBMBnGdAVwFsAjKAJxgH447zYAuGAM1QBtiooSLBAAmZGkzY8BEjAD21AFZQAxiCYs2MTj36Dw0OAGYJGLLnxFSAN3kBLQppCsO3PgKFGQAFjNTLWVJiF3t0AHNNAG9UThC6MPCAGhhqONCIgF8dGCi0mHjEzOBgVXl0EIVldl8AbmBFJQA6YCA">playground</a> 👈👈</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> t1 = <span class="built_in">any</span> <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// boolean</span></span><br><span class="line"><span class="keyword">type</span> t2 = <span class="built_in">any</span> <span class="keyword">extends</span> <span class="built_in">object</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// boolean</span></span><br><span class="line"><span class="keyword">type</span> t3 = <span class="built_in">any</span> <span class="keyword">extends</span> <span class="built_in">void</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// boolean</span></span><br><span class="line"><span class="keyword">type</span> t4 = <span class="built_in">any</span> <span class="keyword">extends</span> <span class="built_in">string</span> ? &#123;<span class="attr">a</span>: <span class="built_in">string</span>, <span class="attr">b</span>: <span class="built_in">string</span>&#125; : &#123;<span class="attr">b</span>: <span class="built_in">string</span>&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// t4的类型是 &#123;</span></span><br><span class="line"><span class="comment">//     a: string;</span></span><br><span class="line"><span class="comment">//     b: string;</span></span><br><span class="line"><span class="comment">//     &#125;</span></span><br><span class="line"><span class="comment">// | &#123;</span></span><br><span class="line"><span class="comment">//     b: string;</span></span><br><span class="line"><span class="comment">//     &#125;;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="attr">obj</span>:t4;</span><br><span class="line">obj. <span class="comment">// 可以取b类型</span></span><br></pre></td></tr></table></figure><p>因为我们前面说过<code>any</code>类型可以看成<code>never</code>和<code>unknown</code>的混合体，也就是空集和全集的并集，因为<code>never</code>是任何类型的子集，任何类型是<code>unknown</code>的子集，根据集合的分配率，<code>any extends [A] ? B: C</code>的结果是<code>B | C</code>, <code>A</code>为任何类型。</p><p>所以我们可以定义自己的union运算方法：</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> myUion&lt;A, B&gt; = <span class="built_in">any</span> <span class="keyword">extends</span> [<span class="built_in">never</span>] ? A : B; <span class="comment">// never类型是随意写的，这里可以替换成任何类型</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> t1 = myUion&lt;<span class="built_in">string</span>, <span class="built_in">number</span>&gt;; <span class="comment">// string | number</span></span><br></pre></td></tr></table></figure><p><code>extends</code>的另一种用法是限制输入类型（条件限制），下面代码限制了输入类型T必须是<code>&#123; message: unknown &#125;</code>的子集，所以必须得包含<code>message</code>这个属性。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">MessageOf</span>&lt;T <span class="keyword">extends</span> &#123; <span class="attr">message</span>: <span class="built_in">unknown</span> &#125;&gt; = T[<span class="string">&quot;message&quot;</span>];</span><br><span class="line"> </span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Email</span> &#123;</span><br><span class="line">  <span class="attr">message1</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">EmailMessageContents</span> = <span class="title class_">MessageOf</span>&lt;<span class="title class_">Email</span>&gt;; <span class="comment">// 报错：Property &#x27;message&#x27; is missing in type &#x27;Email&#x27; but required in type &#x27;&#123; message: unknown; &#125;</span></span><br></pre></td></tr></table></figure><h3 id="理解TS类型断言"><a href="#理解TS类型断言" class="headerlink" title="理解TS类型断言"></a>理解TS类型断言</h3><p>此处引用一下<a href="https://juejin.cn/post/6844904182843965453?searchId=202308011624346198C61171204F01C9B0#heading-10"><strong>1.2W字 | 了不起的 TypeScript 入门教程</strong></a>的类型断言解释：</p><blockquote><p>有时候你会遇到这样的情况，你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。</p><p>通过类型断言这种方式可以告诉编译器，“相信我，我知道自己在干什么”。类型断言好比其他语言里的类型转换，但是不进行特殊的数据检查和解构。它没有运行时的影响，只是在编译阶段起作用。</p></blockquote><p>学会了集合论的观点看TS后，我们可以猜测类型断言相当于把一个变量的类型手动转化为<code>as</code>之后的新类型，之后这个变量会按照新类型的规则来检查。</p><p>!<a href="https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.783g8nvt0p80.webp">https://jsd.cdn.zzko.cn/gh/janice143/picx-images-hosting@master/20230731/image.783g8nvt0p80.webp</a></p><p>以下代码可以验证我们的想法：</p><p>从值集合来看，<code>v1</code>的类型已经变为<code>never</code>，所以这个变量只能被<code>never</code>类型的值赋值，所以<code>v1</code>变量不能被赋予任何值；</p><p>从类型集合的角度来看，<code>1</code>本来是<code>number</code>类型，但是类型断言成<code>never</code>后，因为<code>never</code>是空集，是所有类型的子集，所以<code>t1</code>是<code>true</code>, <code>t2</code>是<code>false</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> v1 = <span class="number">1</span> <span class="keyword">as</span> <span class="built_in">never</span>; <span class="comment">// never</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// value view</span></span><br><span class="line"><span class="keyword">const</span> v2 = v1; <span class="comment">//v2的类型是never</span></span><br><span class="line">v1 = <span class="literal">true</span>; <span class="comment">// Type &#x27;boolean&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line">v1 = <span class="number">42</span>; <span class="comment">// Type &#x27;number&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line">v1 = <span class="string">&quot;Hello World&quot;</span>; <span class="comment">// Type &#x27;string&#x27; is not assignable to type &#x27;never&#x27;.</span></span><br><span class="line">v1 = []; <span class="comment">// Type &#x27;never[]&#x27; is not assignable to type &#x27;never&#x27;.</span></span><br><span class="line">v1 = &#123;&#125;; <span class="comment">// Type &#x27;&#123;&#125;&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line">v1 = <span class="title class_">Math</span>.<span class="property">random</span>; <span class="comment">// Type &#x27;() =&gt; number&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line">v1 = <span class="literal">null</span>; <span class="comment">// Type &#x27;null&#x27; is not assignable to type &#x27;never&#x27;.</span></span><br><span class="line">v1 = <span class="literal">undefined</span>; <span class="comment">// Type &#x27;undefined&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line">v1 = <span class="keyword">new</span> <span class="title class_">TypeError</span>(); <span class="comment">// Type &#x27;TypeError&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line">v1 = <span class="title class_">Symbol</span>(<span class="string">&quot;type&quot;</span>); <span class="comment">// Type &#x27;symbol&#x27; is not assignable to type &#x27;never&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// type view</span></span><br><span class="line"><span class="keyword">type</span> t1 = <span class="number">1</span> <span class="keyword">extends</span> <span class="built_in">string</span> ? <span class="literal">true</span> : <span class="literal">false</span>; <span class="comment">// false</span></span><br><span class="line"><span class="keyword">type</span> t2 = <span class="keyword">typeof</span> v1 <span class="keyword">extends</span> <span class="built_in">string</span> ? <span class="attr">true</span>: <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> t3 = <span class="keyword">typeof</span> v1 <span class="keyword">extends</span> <span class="built_in">number</span> ? <span class="attr">true</span>: <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> t4 = <span class="keyword">typeof</span> v1 <span class="keyword">extends</span> <span class="literal">null</span> ? <span class="attr">true</span>: <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> t5 = <span class="keyword">typeof</span> v1 <span class="keyword">extends</span> <span class="literal">undefined</span> ? <span class="attr">true</span>: <span class="literal">false</span>; <span class="comment">// true</span></span><br><span class="line"><span class="keyword">type</span> t6 = <span class="keyword">typeof</span> v1 <span class="keyword">extends</span> <span class="built_in">unknown</span> ? <span class="attr">true</span>: <span class="literal">false</span>; <span class="comment">// true</span></span><br></pre></td></tr></table></figure><h3 id="理解TS类型体操"><a href="#理解TS类型体操" class="headerlink" title="理解TS类型体操"></a>理解TS类型体操</h3><p>掌握了集合在TS中的映射，TS类型体操看起来也不是很难理解了。</p><p><strong>Exclude</strong></p><p><code>Exclude&lt;T, U&gt;</code>排除T类型中与U类型的交集部分，生成新的类型。其源码如下所示。可以看到，如果<code>T</code>和<code>U</code>是一个单个集合类型时（不是联合类型），如果<code>T</code>不是<code>U</code>的子集，那么返回<code>T</code>，否则返回<code>never</code>。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">Exclude</span> &lt;T, U&gt; = T <span class="keyword">extends</span> U ? <span class="built_in">never</span> : T;</span><br><span class="line"><span class="keyword">type</span> t1 = <span class="title class_">Exclude</span>&lt;<span class="string">&#x27;a&#x27;</span>|<span class="string">&#x27;b&#x27;</span>|<span class="string">&#x27;c&#x27;</span>,<span class="string">&#x27;a&#x27;</span>|<span class="string">&#x27;b&#x27;</span> &gt;; <span class="comment">// &#x27;c&#x27;</span></span><br><span class="line"><span class="keyword">type</span> t2 = <span class="title class_">Exclude</span>&lt;<span class="built_in">number</span>,&#123;&#125;&gt;; <span class="comment">// never</span></span><br><span class="line"><span class="keyword">type</span> t3 = <span class="title class_">Exclude</span>&lt;&#123;<span class="attr">a</span>: <span class="built_in">string</span>&#125;,&#123;<span class="attr">a</span>: <span class="built_in">string</span>; <span class="attr">b</span>: <span class="built_in">string</span>&#125;&gt;; <span class="comment">// &#123;a: string;&#125;</span></span><br></pre></td></tr></table></figure><p>如果T是联合类型，需要用分配率来解释。对于t1，我们把U看成一个整体，用A代替，可以用这个公式$(A\cup B) \cap A &#x3D; (A \cap A) \cup (B \cap A)$ ，因为任何集合是其本身集合的子集，所以根据<code>exclude</code>的规则联合左边的结果为<code>never</code>，在联合右边，因为B(<code>c</code>)不是A(<code>a|b</code>)的子集，根据<code>exclude</code>的规则，取<code>B</code>，所以最终<code>t1</code>的结果是<code>c</code>。</p><p><strong>IsNever</strong></p><p>判断是否是never类型</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">IsNever</span>&lt;T&gt; = [T] <span class="keyword">extends</span> [<span class="built_in">never</span>] ? <span class="literal">true</span> : <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>never是空集，所以除了本身之外，不可能是任何类型的子集。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文使用了大量实例探讨了 TS 类型系统中的集合思想，通过类比和映射，我们发现集合论的很多概念与 TS 的类型系统一一对应，这对我们深刻理解和使用TS有很大的帮助，更进一步地，通过揭示TS背后的集合论，我们还能发现计算机科学、数学和逻辑学之间的紧密联系。</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/18/Think-in-Set-%EF%BC%8C%E4%BB%8E%E9%9B%86%E5%90%88%E8%AE%BA%E7%9A%84%E8%A7%92%E5%BA%A6%E7%90%86%E8%A7%A3TypeScript/</id>
    <link href="https://believed-breadfruit.top/2023/10/18/Think-in-Set-%EF%BC%8C%E4%BB%8E%E9%9B%86%E5%90%88%E8%AE%BA%E7%9A%84%E8%A7%92%E5%BA%A6%E7%90%86%E8%A7%A3TypeScript/"/>
    <published>2023-10-18T13:48:29.000Z</published>
    <summary>
      <![CDATA[<blockquote>
<p>In TypeScript, it’s better to think of a type as a <em>set of values</em> that share something in common. Because types are]]>
    </summary>
    <title>Think in {Set}，从集合论的角度理解TypeScript</title>
    <updated>2024-12-22T06:30:48.419Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="前端" scheme="https://believed-breadfruit.top/categories/%E5%89%8D%E7%AB%AF/"/>
    <category term="TypeScript" scheme="https://believed-breadfruit.top/tags/TypeScript/"/>
    <content>
      <![CDATA[<p>在正式讲TS之前，我想用一个章节来讲TS的前置知识。</p><h3 id="TS的介绍"><a href="#TS的介绍" class="headerlink" title="TS的介绍"></a>TS的介绍</h3><p>先看维基百科的介绍：</p><p><strong>TypeScript</strong> is a <a href="https://en.wikipedia.org/wiki/Free_and_open-source">free and open-source</a> <a href="https://en.wikipedia.org/wiki/High-level_programming_language">high-level</a> <a href="https://en.wikipedia.org/wiki/Programming_language">programming language</a> developed by <a href="https://en.wikipedia.org/wiki/Microsoft">Microsoft</a> that adds <a href="https://en.wikipedia.org/wiki/Static_typing">static typing</a> with optional type <a href="https://en.wikipedia.org/wiki/Annotation">annotations</a> to <a href="https://en.wikipedia.org/wiki/JavaScript">JavaScript</a>. It is designed for the development of large applications and <a href="https://en.wikipedia.org/wiki/Source-to-source_compiler">transpiles</a> to JavaScript.<a href="https://en.wikipedia.org/wiki/TypeScript#cite_note-5">[5]</a> Because TypeScript is a <a href="https://en.wikipedia.org/wiki/Superset">superset</a> of JavaScript, all JavaScript programs are <a href="https://en.wikipedia.org/wiki/Syntax_(programming_languages)">syntactically</a> valid TypeScript, but they can fail to <a href="https://en.wikipedia.org/wiki/Type_system#Type_checking">type-check</a> for <a href="https://en.wikipedia.org/wiki/Type_safety">safety</a> reasons.</p><p>国内社区普遍达成共识的介绍：</p><p><a href="https://link.juejin.cn/?target=https://www.typescriptlang.org/">TypeScript</a> 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集，在JS的基础上添加了可选的静态类型，能够在编译阶段检测出语法错误的编程语言。</p><p>无论是国内外，社区对TS的理解总是离不开类型检查、JS的超集、静态类型等概念。很多初学者会对这些定义一览而过，直奔文章后面的TS特性讲解。而我觉得理解TS的定义是学习TS的关键所在。</p><h3 id="类型系统简介"><a href="#类型系统简介" class="headerlink" title="类型系统简介"></a>类型系统简介</h3><p>很显然，根据TS的定义，TS能给JS做类型检查，而且是静态的类型检查。这是什么意思呢？要了解这个需要先知道这背后的“类型系统”这一基础理论。</p><p><strong>类型</strong></p><p>我们都知道计算机是0101的世界，编程语言中的程序逻辑和数据在内存空间里都是不同01的序列，在这个维度上，二者没有区别。所以当计算机误把数据当成逻辑，或者逻辑当成数据，就很容易发生错误，导致系统崩溃。举个例子来讲，JS中的eval函数，可以传一个字符串作为参数，当字符串是一串可以运行的代码，eval可以正常运行，但是如果是普通字符串，程序就会发生错误。但是这两个字符串到底是可执行的代码，还是普通字符串，对于计算机来说是无法区分的。</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">eval</span>(<span class="string">&#x27;console.log(\&#x27;Test Me\&#x27;)&#x27;</span>) <span class="comment">// &quot;Test Me&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">eval</span>(<span class="string">&#x27;Test Me&#x27;</span>) <span class="comment">// Uncaught SyntaxError: Unexpected identifier &#x27;Me&#x27;</span></span><br></pre></td></tr></table></figure><p>这也就为什么编程语言需要类型的一个原因，因为类型给了0101数据含义，以及一组逻辑规则。这些规则确保程序表达式使用了合理的类型数据。正如上述举的eval的例子，我们需要一套类型规则来判断其参数是否赋值合理，以便及时暴露出代码问题。</p><p><del>通过将一类具有相同特征的数据分为同一个类型，定义其含义和逻辑规则，这些规则对程序语言中的变量、函数等具有限制作用，通过类型检查机制对源代码暴露出类型错误，以便实现程序稳定安全运行。</del></p><p><strong>类型系统</strong></p><p>给类型下完定义后，<a href="https://en.wikipedia.org/wiki/Type_system">类型系统</a>的理论不断完善和发展。大部分的编程语言都有自己的类型系统，JS有自己的类型系统，TS也有一套类型系统，Java，c#, Python等都有一套类型系统。</p><p>类型系统可以理解为编程语言里用来定义变量、函数、表达式和模块等属于何种类型的一套规则，对于变量、函数等这些源代码元素，一旦被赋值了某种类型，这意味只有这么一类的数据的全部值来可以赋值给这个变量。例如JS中的number类型，定义了变量只能赋值一个<a href="https://en.wikipedia.org/wiki/Double-precision_floating-point_format">双精度64位的浮点数</a>，或者Java的int类型，定义了变量的值只能是一个32位的整数，最小值为$-2^{32}$，最大值为$2^{32}-1$。</p><p>值得一提的是刚才我们解释类型系统用了“只有这么一类的数据的全部值来可以赋值给这个变量”这个描述，是不是会让你想起高中数学里的集合概念，一个教室的全体学生是一个集合，北京、上海等这些城市是一个集合，全部的自然数01234等也属于一个集合，而且这个集合是无限的。</p><p>集合是因为我们想要把所有具有共性的元素聚集在一起而创造的概念。光造出集合的概念可不够，我们还有子集、真子集、有限集、无限集、空集、非空集、超集等等。等等，”超集“？是不是想起什么来了，回到TS的定义，它是 JavaScript 的一个”超集“，此”超集“是彼”超集“吗？</p><p>答案是肯定的，后面的章节我会给大家解释TS背后的集合论，以及利用这个集合思想可以让我们有多么容易理解TS的各种特性。</p><p><img src="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f0c5373b-de03-44ba-9930-d39202d9f460/Untitled.png" alt="Untitled"></p><p><strong>类型检查</strong></p><p>编程语言都有一套类型体系，而且各有差异。现存的编程语言数不胜数，为了方便我们学习，聪明的计算机先驱们又充分发挥了自己十分擅长的分类、归纳、定义、总结等手段给我们带来了”类型检查“这一概念和背后的理论。</p><p>我们用高中学习的集合论来理解什么是类型检查。在我印象中，无论是小学、中学还是大学，班里总是有几个体型比较胖的同学，思考这么一个找胖同学的场景：一个固定的班级里，我们想找出一个没有胖同学的集合，班里的同学体型不一，这个集合里很容易混入胖同学，那么我们怎么把这个胖同学找出来呢？类型检查就可以。那么怎么检查呢？我们想到的第一个方法就是让班里同学都把自己体重报出来，我心里有个评判标准，我觉得超过80kg就是个大胖子，所以只要我听到有同学体重超过80kg，说明他不属于这个集合，我们可以用这个方法把最终的不含胖子的集合确定下来。我们管这个方法叫做静态检查。我们还想到可以让这群学生都去操场上跑，胖同学一般体力耐力不够，跑的过程中会很快坚持不下来，我们管这个检查方法叫动态检查。</p><p>虽然上述例子不严谨，而且含有对体型大的朋友们的刻板影响，但是一切都是为了让我们更好理解计算机里的类型检查机制。不管怎么说，感谢例子中的胖子们。</p><p>所以总结起来，类型检查就是编程语言中，按照类型体系给出的一组规则践行的判断方法，把异常的类型暴露出来。</p><p><strong>静态类型和动态类型</strong></p><p>对于类型定义，经常会看到或者听到强类型、弱类型、静态类型和动态类型等词汇。这些词汇其实是为了区分不同的类型系统而衍生的概念。不同的类型系统有不同的类型检查机制，从类型检查的时机来看，类型系统可以分为静态类型系统和动态类型系统，前者是在源代码编译的时候进行类型检查，后者是在运行时进行检查。可见，我们上述的找胖子案例还是有合理之处的。</p><p>图？展示了一般的解释型编程语言从源代码解析成抽象语法树，然后编译成字节码，最后在运行时环境上运行的过程。如果一门编程语言的类型系统是在编译的时候做类型检查，那么就是静态类型系统；如果是在运行时进行类型检查就是动态系统。JS是动态类型语言，意味着它的类型体系只在运行时做检查的，在编译的时候则不做校验，所以JS允许我们为已经赋值的变量再次分配为任何其他类型的值。TS是静态类型语言，也就是说每次编译的时候都会暴露出类型错误。</p><p><img src="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/52073ce4-30a5-4b00-b66c-d0aefd459302/Untitled.png" alt="图？解释型编程语言解析、编译过程（图片[来源](https:&#x2F;&#x2F;blog.bhekani.com&#x2F;typescript-under-the-hood)）"></p><p>图？解释型编程语言解析、编译过程（图片<a href="https://blog.bhekani.com/typescript-under-the-hood">来源</a>）</p><p>既然说到TS，作为本文重点研究的对象，此处我们再延伸讲一下TS的原理，与图？不同的是，TS被解析成抽象语法树后，会被ts的编译器转为成JS代码，如图？所示。之后JS的解析和编译又回到了图？。</p><p><img src="https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b1e368ec-491f-420c-960f-e3b40af16e2a/Untitled.png" alt="Untitled"></p><p><strong>强类型和弱类型</strong></p><p>从类型检查的严格程度角度看，类型系统分为强类型系统和弱类型系统。对于弱类型系统，其内部会隐式地进行一定的类型转化，比如JS中的<code>==</code>运算符，当等式两边分别是1和<code>’1’</code>时，JS内部会做转化让结果为<code>true</code>。</p><p>但是值得注意的是，目前并没有对强类型和弱类型的严格定义，因为一个类型系统是静态还是动态的，这种根据类型检查的时机分类是很明显的，根据时间的单向线性或者维度的正交性，静态和动态没有重合的可能。虽然也有一些编程语言结合了静态类型检查和动态类型检查，但这并没有让我们对静态类型和动态类型分类持有怀疑态度。</p><p>但是类型强弱却一直饱受争议。因为强弱是对相同事物（在此场景下为强度）的不同程度区分，不同程度是一个非常模糊的概念，如果一个编程语言是49%弱类型检查特性，那我们就把它称为弱类型语言吗？继续用我们的找胖子案例来讲，我的评价标准是认为超过80kg是胖子，因为这个标准是我自己定的，和别人的标准可能不统一，别人可能认为超过100kg才是胖子，那我们用不同的标准试图下一个非0即1的结论，是不是很容易意见不一致。</p><p>保持严谨的态度是我们技术人该有的品质，但是日常口语中，人们还是习惯性地叫我这种类型检查方式叫做弱胖子检查，管100kg标准的叫强胖子检查。</p><p>因为类型系统强度在一个范围内，所以评价一个编程语言是强类型还是弱类型很容易产生歧义，因此便有了一个更严谨地描述：类型安全，描述了一个类型系统能够防止类型错误的性质。具有类型安全的编程语言不会产生类型错误。</p><h3 id="类型怎么用"><a href="#类型怎么用" class="headerlink" title="类型怎么用"></a><strong>类型怎么用</strong></h3><p>前面我们花了很多篇幅让大家理解类型系统中的基础概念。我们知道在类型系统的底层理论中，很重要的一部分是是给出类型的规则，实现类型规则的检验机制算法，这些只是在理论和算法层面对类型系统的完善。聪明的计算机科学家们把类型体系的理论和算法整出来了，实干的工程师们按照这些理论把具体的类型系统做出来了。现在我们不禁想问一个问题：怎么用？</p>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/10/17/TS%E7%9A%84%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86/</id>
    <link href="https://believed-breadfruit.top/2023/10/17/TS%E7%9A%84%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86/"/>
    <published>2023-10-17T13:49:22.000Z</published>
    <summary>
      <![CDATA[<p>在正式讲TS之前，我想用一个章节来讲TS的前置知识。</p>
<h3 id="TS的介绍"><a href="#TS的介绍" class="headerlink" title="TS的介绍"></a>TS的介绍</h3><p>先看维基百科的介绍：</p>
<p><stron]]>
    </summary>
    <title>TS的前置知识</title>
    <updated>2024-12-22T06:31:04.925Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alicia Lan</name>
    </author>
    <category term="数据结构与算法" scheme="https://believed-breadfruit.top/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"/>
    <category term="算法" scheme="https://believed-breadfruit.top/tags/%E7%AE%97%E6%B3%95/"/>
    <content>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>贪心算法的核心思想是通过每一步的局部最优解来达到全局最优解。本文将介绍贪心算法的原理、时间复杂度、思维框架、示例以及与动态规划的区别。</p><h2 id="贪心算法原理"><a href="#贪心算法原理" class="headerlink" title="贪心算法原理"></a>贪心算法原理</h2><p>关键字：贪心策略、局部最优。</p><p>贪心算法是一种基于贪心策略的算法，也就是说，在每一步选择时，都采取当前状态下的最优策略，即局部最优解。然而，局部最优解并不一定是全局最优解。思考下面这个问题：如何找到下面这个图的最长路径？</p><p>如果用贪心策略来讲，</p><ol><li>第一步我们遍历了根节点20，在这个位置上，我们最好的选择是3，因为3 &gt; 2。</li><li>所以第二步我们选择了3，在这个位置上，我们最好的选择是1。该节点没有子节点，所以路径遍历完毕。</li><li>但是这条路径[20, 3, 1]显然没有路径[20, 2, 10]长。</li></ol><p>所以贪心算法的局部最优解并不一定是全局最优解。</p><p><img src="https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230707/Untitled.783qvgohjfo0.webp" alt="graph"></p><p>贪心算法的适用条件是，问题的最优解可以通过局部最优解来推导出来，并且问题具有无后效性，也就是说，某个状态以后的过程不会影响以前的状态。</p><h3 id="贪心算法的思维框架"><a href="#贪心算法的思维框架" class="headerlink" title="贪心算法的思维框架"></a>贪心算法的思维框架</h3><p>贪心算法的思想在于每一步都是最优的选择，从而达到全局最优解。在实际应用中，贪心算法通常需要具备以下三个条件：</p><p>(1)  问题的最优解可以通过局部最优解来推导出来。</p><p>(2) 问题具有无后效性，后面的状态不会影响前面的状态（不可撤回）</p><p>(3) 贪心策略的选择只看眼前，不能依赖于后面的策略。</p><p><img src="https://cdn.staticaly.com/gh/janice143/picx-images-hosting@master/20230707/download.4aucxnedy2g0.webp" alt="贪心算法流程图"></p><h3 id="贪心算法和动态规划的区别"><a href="#贪心算法和动态规划的区别" class="headerlink" title="贪心算法和动态规划的区别"></a>贪心算法和动态规划的区别</h3><p>动态规划的思维框架是确认状态和状态转移方程，求解过程中需要保存中间结果来降低时间复杂度。贪心算法不需要保存结果，而是通过从底向上的每一步最优达到全局最优。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ol><li><a href="https://www.geeksforgeeks.org/greedy-algorithms/">https://www.geeksforgeeks.org/greedy-algorithms/</a></li><li><a href="https://www.freecodecamp.org/news/greedy-algorithms/">https://www.freecodecamp.org/news/greedy-algorithms/</a></li></ol>]]>
    </content>
    <id>https://believed-breadfruit.top/2023/07/07/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E8%AE%B2%E8%A7%A3/</id>
    <link href="https://believed-breadfruit.top/2023/07/07/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E8%AE%B2%E8%A7%A3/"/>
    <published>2023-07-07T14:57:34.000Z</published>
    <summary>
      <![CDATA[<h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>贪心算法的核心思想是通过每一步的局部最优解来达到全局最优解。本文将介绍贪心算法的原理、时间复杂度、思维框架、示例以及与动态规划的区别。</p]]>
    </summary>
    <title>贪心算法讲解</title>
    <updated>2024-04-22T17:22:13.408Z</updated>
  </entry>
</feed>
