普通视图
webpack和vite区别及原理实现
React Scheduler为何采用MessageChannel调度?
用一篇文章带你手写Vue中的reactive响应式
前端基础数据中心:从混乱到统一的架构演进
2025 复盘 | 穿越AI焦虑周期,进化为 "AI全栈"
1. 今年一定
好几年没写「年终总结」,翻了下「掘金」,上一篇还停留在「2021年」,倒不是这几年没活过,也不是不想写,只是每次都止步于「拖延」。每到年底,脑子里就会蹦出的各种想法,被我零散地记录在 "大纲笔记" 中:
![]()
写这玩意挺废「时间」和「心力」,所以总想着 "找个周末,花一大块时间,好好梳理一下",结果都是一拖再拖:拖到元旦,拖到春节,拖到元宵。
然后,转念一想:"这都明年了,还写个🐣毛啊?算了,算了,明年一定!"。2022、2023、2024 就在这样的 "明年一定" 中溜走了...
![]()
人到中年,主观感觉「时间」过得越来越快,「记性」 也大不如前,很多当时觉得 "刻骨铭心" 的瞬间 (如:结婚、当爹),如今回忆起,就剩下一个 "模糊的轮廓"。不写的话,又是「摸 🐟」一年,趁着 2025 年还没过完,很多感觉还是热乎的,赶紧动笔,今年一定!!!
2. 如此生活30年,直到大厦崩塌
2025 年,对我冲击最大的当属 "AI",大到让我不得不重新审视「自己的工作和价值」。
![]()
我接触 AI 并不算晚,2024 年那会跟风玩了下 ChatGPT,后面在 GitHub Copilot (便宜盗版,30¥/月) 的协助下,快速交付了一个爬虫单子。尝到 "提效甜头" 的我,咬咬牙上了 年付正版 (100刀/年) 的车。
当时 AI 在我的眼里,只是个 "比较聪明的代码补全工具",可以帮我少些几行代码,仅此而已,因为 逻辑还得我来把控。
![]()
但到了 2025 年,事情却变得有点不一样了:
AI能读懂/分析整个项目、重构屎山代码、把一个模糊的业务需求实现得有模有样。
当然,最让我 "破防" 的还是它的 "解BUG" 能力:
按照以往的习惯,我得去 Google、翻 Stack Overflow、看源码,少说得折腾半天。现在,把 报错日志 和 相关代码 丢给它,通常几秒就能:指出问题所在,给出解决方案,甚至可以 帮我改好并测试通过。
![]()
积累多年,一度 "引以为傲" 的「编程经验」(对API的熟练程度、快速定位BUG的直觉、配置环境的熟练度等) 在 AI 的面前,变得 "不堪一击"。渐渐地,我的「工作流程」也发生了改变,"亲手" 敲下的代码越来越少,取而代之的是一套 "机械化" 的 "肌肉记忆":
- 写注释 → 等待AI补全 → 按Tab:哪怕脑子里知道下一行该写什么,手指也会下意识停顿,等待那行灰色的建议浮现,然后无脑按下 Tab。
- 提需求 → 生成代码 → Accept:把业务逻辑描述一遍,丢给 AI,都不太细看生成的具体实现,直接点击 Accept All,主打一个 "能跑就行"。
- 运行报错 → 复制日志 → 丢给 AI:遇到 Bug,第一反应不再是去分析堆栈信息 (Stack Trace),而是CV日志到对话框,问它:"解决报错:xxx"。
- 效果不对 → 截图 → 丢给 AI:连描述问题的精力都省了,直接截图往对话框里一扔,附上一句 "改成这样"。
代码跑通了,效率提高了,却带来了精神上的「空虚」,我似乎再也感受不到当初那种「编程的快乐」:
- 为了解决某个问题,苦思冥想,抽丝剥茧,最后成功 "破案" 时 "多巴胺疯狂分泌" 的 "快感"。
- 查各种资料、反复推敲、验证,最终设计出一个自己觉得 "牛逼哄哄" 代码架构时的 "成就感"。
同时,也陷入了一种深深的「自我怀疑与迷茫」:
- 越来越搞不清楚自己的「定位」(存在价值),上面那套 "连招" 找个实习生培训两天也能干。我曾赖以为生的那些 "技能",正变得廉价、可替代、甚至有点多余 ...
- 找不到方向,以前「程序员成长路径」很清晰:学语言 → 学框架 → 学架构 → 学系统设计 → 刷算法 → 搞源码 ... 只要你一步步往上爬,爬到 "山顶" 就能成为 "大牛"。而如今却好像 "失效" 了...
- 可控感被剥夺,程序员是典型的「内控型人格」—— 相信通过逻辑和细节掌控能预测一切。而但 AI 的「黑箱特性」带来了「工具不可控」,无法完全准确预测AI输出,调试从 "追踪逻辑" 变为 "试探模型"。
3. 调整对待AI的心态
3.1. 从 "焦虑" 到 "接纳"
我深知「焦虑」无用,于是开始探寻「破局之道」,反复阅读大量资料后发现,几乎所有人都在让你「拥抱 AI」,但具体怎么拥抱法?没人说,或者说得含糊不清,有些甚至还想割我 "韭菜" 🤡 ?屏蔽这些噪音,冷静下来复盘,拨开情绪迷雾,透过现象看本质。
首先,坦诚地「接纳」肯定是没错的,历史的车轮从不因个人的意志而停止转动,当 第一次工业革命的蒸汽机 轰鸣作响时,那些坚守手工工场的匠人们,也经历着相同的困境。精细手艺 在不知疲倦、效率千倍的 机械化工厂 面前显得苍白而无力。大机器生产取代手工劳动,不是一种选择,而是一种必然的 "降维打击"。
现在,我们同样站在了 "生产力变革" 的周期节点上, "效率至上" 的底层逻辑从未改变。是选择成为被时代甩下车的 "旧时代纺织工"?还是进化为驾驭机器的 "新时代工程师"?回归「第一性原理」,剥开 "智能" 的外衣,想想 "AI 的本质是什么?" —— 「干活的工具」
所以,面对 AI,我们要做的事情就是琢磨 "如何用好这个工具? ",即:详细阅读使用说明后,在合适的场景,用合适的方式,解决合适的问题。
3.2. AI 有什么用?—— 能力放大 + 自学利器
3.2.1. 能力放大器
🐶 经常在 自媒体平台 刷到 "普通人学AI后致富/逆袭" 的 叙事,看到这些 "逆天标题" 没把我笑死:
![]()
多的不说,记住这段话就对了:
变现的核心能力从来不是使用工具,而是商业认知、市场洞察、营销推广、客户服务。AI 只是一个环节,不要高估了工具的作用,而低估了 商业常识的重要性,也不要低估了背后的 隐性成本和巨大的工作量。
这些 "AI变现教程" 的 最大问题:
让你把AI当成一个 独立的、全新的、需要从零开始的 "行业" 去卷。
对于 99% 的普通人而言,把AI看作 "能力放大器" 会更靠谱一点,即:
思考如何利用AI,帮我把我已有的技能/兴趣做得更好?
比如:
- 用 AI 减少重复劳动,提高工作效率和质量,把时间花在更有创造力的事情。
- AI 负责广度,你负责深度,在你热爱的小众领域里用AI武装自己,做到 "人无我有,人有我精"。
今年「Vibe Coding (氛围编程) 」很火:
用自然语言描述想要的效果,AI帮你写代码,你只负责验收结果和提修改意见,不用管具体代码怎么实现的。
编程门槛大大降低,普通人 只要能把 创意和感觉 翻译成需求,就能借助 AI 将其快速具象化为 可运行的产品。
但你会发现,绝大多数生成的作品都是 "一次性原型或玩具":灵光一现即可实现,却缺乏持续迭代、架构设计与用户验证,因此难以具备商业价值、也难形成可持续的产品形态。
真正能够利用 Vibe Coding 实现变现的,往往是具备一定 编程经验 或 产品思维 的 "专业人士"。他们不仅能用 AI 快速实现灵感,还能对作品进行持续优化、迭代和工程化打磨,从而将 "灵感原型" 进化为 "可用产品"。
再说一个自己观察到的例子,前阵子 OpenAI 发布了用于生成短视频的「Sora2」,B站 很快涌现了一堆 AI 生成的 "赛博科比" 恶搞视频。
看到一个播放量破百万的作品有点意思,点进 UP 主的主页想看下其它作品,结果发现他并不是突然爆火的 "新人",人家已经默默做了好几年视频,只是过去的播放量惨淡 (几十几百)。但他却一直坚持创作,尝试不同的方向,能清晰地看到他的剪辑、叙事和整体制作水平在一点点提高。
AI 不会让没有积累的人"平地起飞",但有可能让有准备的人"一飞冲天"。—《抠腚男孩》
3.2.2. 自学利器
看到这里,可能有人会问:
"那普通人怎么办?我没啥专业技能,也没有长期积累啊? "
简单,那就 "学" 啊!!!以前学习的最大限制是什么?
没人教、教不好、学不动、坚持难
而现在,你有了一个「全知全能、知无不言、24小时为你服务的免费老师」
- 不会写代码?手把手教你,从逻辑到示例一步步拆开。
- 想转行?给你路径、资源、练习清单、复盘建议。
- 想跨领域?帮你建立知识框架,把陌生领域最难啃的部分变简单。
- 遇到瓶颈?像一对一导师一样不断提问、引导、纠偏。
当然,想要这台 "自学利器" 高效运转起来,实现 快速学习/试错/跨域 还需要掌握一些 方法论。
![]()
详细解读可以参加我之前写的《如何借助AI,高效学习,实现快速"跨域"》
3.2.3. 不要神化 AI
🐶 2333,经常刷到 "xx公司发布新的 xx 模型/AI产品颠覆行业,xx师要失业了" 的标题,但事实真的如此吗?最近 Google 家的 Nano Banana Pro 🍌很火,号称当前 "最强AI生图" 模型,亲身体验下确实强 (本文大部分配图就是它出的),天天在群里吹爆。
某天晚上,有 "多年专业设计经验" 的老婆收到一个改图需求 (抠素材,按要求调整海报):
![]()
😄 看着简单,感觉 🍌 就能做,于是我提出和老婆 PK 下,她用 PS 改,我用 🍌 嘴遁修图,看谁出的图又快又好。结果:她10分钟不到就改完,而我跟 🍌 Battle了半个小时没搞好,最终的效果图 (左边她的,右边我的):
![]()
🤡 "甲方" 的评价 (破大防了😭):
![]()
观察仔细的读者可能会问:"你是不是漏了一个车🚗啊?",憋说了,这破车把我调麻了...
![]()
那一刻,我深刻体会到了什么叫 "不要拿你的兴趣爱好,去挑战别人的饭碗",真的是 "降维打击" 啊!
![]()
AI 确实拉低了创作的门槛,但目前还处于生成 80分 内容的阶段 (效率),最后的 10-20 分 (细节、审美、情感) 才是价值的核心。——《抠腚男孩》
后面复盘,老婆看了下我的 Prompt,说我的流程有点问题,应该让 AI 先把素材全抠出来先,再慢慢组合。后面试了下,效果确实有好一些。不过,不得不说,AI 在 自动抠图 这块确实可以:
![]()
🤣 老婆在日常设计时也会用 AI 来偷懒,比如:生成配图、提高清晰度、扩图等。
3.3. AI 是什么? —— 概率预测机器
现阶段谈论 AI,其实都是在谈论 大模型 (LLM) —— 一个极其复杂的 "概率预测机器"。
通过学习海量数据的 "统计规律",逐步逼近这些数据背后的 "概率分布",从而能够在给定 "上下文" 时预测最合理的下一步输出。
不同类型产物的生成原理图解 (看不懂没关系,简单了解下即可):
① 文本
![]()
② 图片 (扩散模型 & 自回归模型)
![]()
![]()
③ 音频 (自回归模型 & Codec + Token 预测 )
![]()
![]()
④ 视频 (扩散式 & 自回归/时空Token)
![]()
![]()
3.4. AI 的能力边界 —— 优/劣势
LLM 擅长发现 "相关性",但难以进行真正的 "因果推理",它只是在 "模仿智能",而非 "真正地理解意图,拥有意识"。 —— 《抠腚男孩》
弄清楚 AI 的本质是 "概率预测机器" 后,接着从 "代码生成" 的角度梳理下它的 "优势 & 劣势":
![]()
了解完 AI 的 优/劣势 后,接着就可以推演「人 & AI」 的 高效协作方式:
![]()
一句话概括:
AI 负责 "生产力" (重复、繁琐、高上下文、高整合的工作),人负责 "方向与边界" (判断、创造、决策、理解组织与业务)。
4. 必备技能 —— Prompt
一般译作 "提示词" 或 "描述词",个人认为后者更加贴切,即:描述问题/需求的 "词句组合" 。「Prompt Engineering-提示词工程」是所有人都必须掌握的 "使用AI的核心技能"。
4.1. 把话说清楚
🐶 别被几个英文单词吓倒,现在的 AI 比几年前聪明多了,普通人 只要能:
把诉求讲得清晰、完整、有逻辑,就能解决绝大多数问题。
示例:
- ❌ 混乱说法:帮我计划个周末玩的地方。
- ✅ 有条理说法:周末想带5岁孩子一日游,2大1小,预算500以内,北京,不想跑太远,能放电、有吃饭的地方、避开暴晒,地铁可达最好。
AI输出结果 (前者输出不同城市的游玩方案,后者输出了具体的行程方案):
![]()
![]()
4.2. 套框架
再往上走,就是了解一些经典的 "Prompt框架",然后再提问时套用,以提高 AI 输出的稳定性、准确性和质量。所谓的 "框架",其实就是 "结构化模板",规定问题中包含哪些 "要素",比如最经典的「CTRF」框架:
![]()
套框架示例 (填空题~):
![]()
常见的框架还有 RTF、COSTAR、SPAR、COT、APE 等等,适用于不同的场景。杰哥整合了自己知道的所有框架精华和高级技巧,弄了通用的「Prompt 最佳实践清单」
![]()
无脑套就是了,助记口诀:
![]()
也可以用故事流程来串联助记,读者可自行发挥,顺序无需固定:
让一位说书人 (角色) ,用生动的语气 (风格语气) ,给孩子们 (受众) 讲个故事 (指令) 。故事的开头 (上下文) 是...,结局 (目标) 要感人。故事的结构 (格式) 要像这样 (示例) ,但不要 (约束) 出现暴力情节。请先构思情节 (逐步思考) ,写完后再想想怎么能更精彩 (反思) 。
😄 懒得记的话,可以用我之前搭的小工具 →「CP AI Prompt助手」
配下 DeepSeek 的 Key,复制粘贴你写的 简单Prompt,它会基于上面的十个维度对提示词进行优化:
![]()
4.3. 写出牛逼的Prompt
明白了怎么 "套框架" 写 "结构化的Prompt",但你可能还是会感到疑惑:
用的同样的AI,为什么别人的生成效果就是比我好?
尤其在 AI 生图 领域,看大佬分享的 Prompt,里面一堆看不懂的专业参数:
环境、构图、光影、景深、镜头、光圈、色调、氛围、胶片颗粒、对比度、主体增强、氛围灯...
能写出这么 专业的Prompt,是因为他们有 "相关领域的行业经验" 吗?
答:有加成,但不全是。高手的核心技能不是 "记这些专业知识",而是:知道如何指使 AI 给自己提供专业知识、框架、术语,然后再反向用这些框架让 AI 编写和优化 Prompt。
😄 其实思路很简单,拆解下这套方法论:
维度词 → 术语/词库 → 通用模板 → 填空得第一版Prompt → AI专家视角优化 → 迭代优化沉淀
详细玩法可以看下图:
![]()
4.4. Prompt 逆向
Prompt 逆向工程(RPE,Reverse Prompt Engineering),就是:从 "输出" 反推 "是什么Prompt" 生成了它。一般用于:学习优秀案例、调试和诊断问题、构建Prompt库和模板、企业质量控制、安全审计 (防御Prompt注入攻击)。
4.4.1. 简单版
普通人 用这个套路就够了,选个聪明点的模型 (如:GPT5 或 Gemini 3 Pro),粘贴图片,写 Prompt 让它反推:
![]()
差得有点远,描述「不满意的点」,让AI继续优化Prompt:
![]()
接着用优化后的 Prompt 来生成,可以看到效果差不多了,接着让 AI 提取一个「通用的Prompt」
![]()
![]()
拿 AI 生成的 Prompt 生图,看效果,描述问题,循环反复,直到稳定生成自己想要的效果~
![]()
![]()
![]()
![]()
4.4.2. 专业版
🐶 其实也差不多,只是流程比较 "标准化",经常搞还能自己搭个 "工作流",适合专业选手,思路:
快速拆解 → 推断 Prompt → 提取要素 → 重建 Prompt → 优化迭代 → 模板化沉淀
详细图解:
![]()
上面是通用的,还有几个 额外功能 的玩法也罗列下:
![]()
5. 锦上添花——懂点AI常识
🐶 懂点AI常识,能让你更 有的放矢 地 用好AI (装逼),比如:连 Token 都不知道的话,就有点贻笑大方了。这里只是简单罗列下相关名词,不用死记,有个大概印象即可,不影响你使用AI,跳过也没关系。😄 详细讲解,建议直接复制名词问题AI,也可移步至《AI-概念名词 & LLM-模型微调》自行查阅~
5.1. AI (人工智能) 基础概念
![]()
5.2. NLP (自然语言处理)
![]()
![]()
5.3. Transformer 架构 (大型模型基础)
![]()
![]()
5.4. 语言模型基础 (Language Models)
![]()
5.5. LLM 核心概念 (Large Language Models)
![]()
![]()
5.6. 数据与训练流程 (Training & Fine-tuning)
![]()
![]()
5.7. 推理阶段 (Inference)
![]()
![]()
5.8. RAG (检索增强生成)
![]()
![]()
5.9. 多模态 AI
![]()
![]()
5.10. AIGC (生成式内容)
![]()
![]()
5.11. 模型压缩、部署与加速 (LLMOps)
![]()
![]()
5.12. Agent (自主智能体)
![]()
![]()
5.13. AGI (通往通用智能)
![]()
![]()
6. AI 编程领域专精
😄 最后,聊聊 AI 编程 领域的一些心得~
6.1. 前置知识
6.1.1. 编程模型
AI 代码写得好不好,主要看 "模型" 的 "编程能力",评估 "模型优劣" 的几个 "常见维度":
![]()
LLM 的能力很难用一句话概括,所以厂商们每次发新模型都会用一堆 Benchmark 来证明 (🐶不服,跑个分?)
① 推理与数学能力 (Reasoning)
"智能的核心指标",高分意味着能够做更复杂任务 (如:工程规划、Agent等),常见基准:GSM8K-小学奥数式数学题、MATH-高难度数学、AIME/AMC-奥林匹克数学、GPQA-博士级科学问答、BigBench Hard (BBH)-推理难题集合 等。
② 语言理解与知识能力 (Language / Knowledge)
"通用模型 IQ 测试",常见基准:MMLU-大学生多学科理解测试、MMLU-Pro - 更难版本、ARC / HellaSwag - 常识推理、OpenBookQA/TriviaQA - 事实/知识问答 等。
③ ✨编程能力 (Coding)
"商业价值极高的应用点",常见基准:HumanEval - 函数级别代码生成、MBPP / MBPP+ 简单编程题、SWE-Bench / SWE-Bench Verified✨:真实 GitHub issue + 多文件工程 (最接近真实开发场景,近两年厂商都在比这个)、Codeforces-算法比赛、CRUXEval / RepoBench-项目级分析 等。
④ 多模态能力 (Multimodal)
"下一代 AI 产品的必争之地" (做 AI 助手、看图、自动化办公等),常见基准:MathVista:带图的数学推理、ChartQA / DocVQA:文档理解、TextCaps / ImageNet:视觉场景理解、VideoMME:视频理解、V-Bench / VQAv2:视觉问答 等。
⑤ 安全性 (Safety / Robustness)
企业用户很看重 "安全合规",常见基准:Harmlessness / Truthfulness、AdvBench:对抗攻击、Red Team 红队测试、Over-Refusal 测试(不会乱拒绝)、Speculative Safety(推测生成的风险)等。
⑥ 速度/延迟/吞吐 (Performance Metrics)
"决定实际用户体验",常见指标:Tokens per second (推理速度)、First Token Latency (首字延迟)、Throughput QPS (每秒处理请求数)、Context processing speed (长文档处理速度)。
有时还会发布一些 "技术参数":
- 模型规模:模型的参数量大小,影响推理与表达上限,规模越大,能力越强,但成本、延迟和部署难度也越高。如:70B = 70 billion = 700亿参数。
- 训练数据规模:模型预训练时学习的 token 总量,代表其知识 "阅历"。数据越多通常知识覆盖越广,但质量、去重和清洗策略比单纯堆量更关键,高质量数据才能让模型表现更稳。如:15T = 15 trillion = 1.5万亿个token。
- 上下文窗口:模型单次可接收并 "记住" 的 "输入长度上限",决定你能塞多少代码、文档和对话历史;窗口越大越适合做整仓分析、长文档问答、复杂任务,但会牺牲成本和延迟,且需要额外机制确保在超长上下文中仍能抓住重点。
- 推理深度:模型答题时的 "思考力度",深推理模式更准确、适合复杂问题,但会更慢、更贵,适合关键任务而非高频交互。
- 价格:按 token 收费,区分输入价、输出价与最小计费单位;部分模型提供 "缓存命中 (cache hit) ",对重复提示只按更低费率计费,大幅降低长上下文与多轮调用成本。价格决定模型可否大规模、频繁和低成本使用。
- 延迟标准:包含首 token 延迟 (FTL) 与 生成速度 (token/s),分别决定 "多久开始回应" 和 "内容生成有多快";低延迟让补全、对话、Agent 流程更流畅,而高延迟会严重影响开发体验与实时性,是工程中比 "更聪明一点" 更重要的性能指标。
- 模型行为控制能力:通过 Temperature、Top-p、System Prompt、工具权限等机制控制模型的随机性、稳定性与执行边界;行为越可控,越能确保输出一致、不跑偏,并安全地接入工具链或生产系统,是把模型从 demo 提升到可上线能力的关键参数。
🤡 个人 "主观" 认为的 "编程模型" 能力排名:
![]()
😊 一句话概括我的 "选模型策略":
选好的模型事半功倍!工程大活 找 Claude,精细小活 (如改BUG) 用 GPT,写前端页面用 Gemini。
🐶 问:这些都是国外的模型啊,怎么才能用上?A社还锁区,经常封号?而且价格好贵啊?
答:😏 这个问题充钱可以解决.jpg,多逛下海 (xian) 鲜 (yu) 市场,国人的 "薅羊毛" 能力不是盖的,各种 "镜像站、第三方中转" 。氪金的时候注意找有 "售后" 的,随用随充,买 "短期 (如月付) ",不要买 "长期 (如年付)",这种看 LLM官方 政策的,一封就直接G了,说不定就 "卷款跑路"~
6.1.2. AI 编程工具的四种形态
![]()
😄 一句话归纳:
普通开发者 & Vibe Coding用户 用 AI IDE/插件 居多,DevOps/后端工程师 用 CLI,团队/企业系统 用 云端Agent,AI 应用开发者 用 AI SDK 构建构建 AI 产品与 Agent 系统。
接着说下 "AI编程" 的三种演进层次~
6.2. 第一层:AI 辅助开发
最早期的AI开发方式,以「人主导 + AI辅助」为核心逻辑,由两种交互模式组成:
- 补全式:基于输入光标前的上下文,预测下一个单词、下一行代码、甚至整个函数。
- 对话式:在 IDE 侧边栏或网页中,通过自然语言问答来生成代码、解释代码或查找 Bug。如:"帮我写一个 Python 的正则来验证邮箱" 或 "这段代码为什么报错?"
这一层的局限:
- 上下文有限:AI 通常只能看到当前文件或少量相关片段,缺乏对整个项目架构的理解。
- 被动性:AI 不会主动修改你的代码文件,它生成代码,你负责复制粘贴和校验。
- 人是瓶颈:所有的决策、文件切换、环境配置都必须由人来操作。
6.3. 第二层:AI 驱动开发流程 (规范+Agent)
目前最前沿、最热门的阶段,AI 不再只是吐出代码片段,而是进化为 Agent (智能体),拥有了 "大脑" (规划能力) 和 "手脚" (工具使用能力),可以 "自主完成一个多步骤的开发任务"。
变成了「人定目标 + Agent 自主执行」,如:"实现一个简单的待办事项 Web 服务,要求:REST API,内存存储即可,有单元测试",Agent 可能会进行这样的任务拆解并执行:
设计目录结构 → 创建代码文件 → 写业务逻辑 → 写测试 → 运行测试并自我修复。
![]()
![]()
为了系统地应用 Agent,业界逐渐采用「 "规范"驱动的开发流程」(Spec-Driven Development):
需求 → 规范文档 → 任务分解 → Agent执行 → 验证反馈
这个流程确保了 清晰的目标定义 和 可追踪的执行过程,而不是让Agent盲目操作。开发者需要维护的 "三类规范" (规范必须比写代码更清晰):
- 功能规范:目标、用户故事、输入输出、性能要求、鉴权、边界情况等。
- 技术规范:模块结构、API、模型字段、状态机、异常流程等,Agent会根据这些自动创建项目。
- 验收规范:测试通过、接口返回正确、性能满足要求、行为与设计一致,即每个功能的评价方式。
人不再写代码 (或者少写),负责「定义 + 审核 + 授权」,人-Agent 协作 的三阶段循环:
- 人主导-目标设定:范围、约束、边界、不允许做的事情。
- Agent主导-执行:分解、规划、写代码、自动Debug、修复、生成报告。
- 人主导-验收:代码质量、安全性、单元测试覆盖率、偏差是否满足业务需求等。
💡 层2 关注的是「开发流程自动化」,任务的起点通常是 "已经确定好的需求/feature"。
![]()
6.4. 第三层:AI 全栈
所谓的 "AI 全栈",本质上就是 "让 AI 同时扮演多个软件开发角色",而 "一人分饰多角" 的自然实现方式就是 "多 Agents"。——《抠腚男孩》
6.4.1. 为什么聊到 "AI全栈" 就会扯到"多 Agents"?
🤔 想象一下让 "AI全栈开发一个应用" 需要经历哪些步骤?
产品需求理解 → 技术选型 → 架构设计 → API 设计 → 前后端代码生成 → 数据库 schema → 错误处理 → 文档生成 → 单测编写 → 测试执行 → 部署脚本 → CI/CD 配置。
让一个 Agent 承包上面所有的工作,会有什么问题?
记忆量爆炸、目标切换频繁、推理链拉得太长,错误积累变大、一旦一步出错,后续全崩、风格、结构、代码质量难统一、难以并行。
软件工程是 "多角色协作" 的结果:产品经理、架构师、后端、前端、测试、文档、DevOps... 如果想 "AI 模拟完整的软件开发流程",自然也需要 "让 AI 也模拟这些角色",于是就变成了这些 Agent:
- Planner / Architect (产品/架构):理解需求、拆任务、出计划 (Plan)。
- Coder / Implementer (实现):按计划改代码、增删文件。
- Searcher / Context Agent (检索):在代码库里找相关文件、API、调用链。
- Tester / QA (写测 / 跑测):写测试、跑测试、分析报错。
- Fixer / Debug Agent (修BUG):根据测试/运行结果修复代码。
- Reviewer / Critic Agent (代码审阅):检查风格、一致性、潜在 bug / 安全问题
- Ops / Deploy Agent (部署):写 Docker、CI/CD、部署脚本(有些系统只做到生成,不自动执行)
即「AI 全栈 = AI 软件开发流水线 = 模拟整个软件部门 = 多 Agent 系统」,这是开发任务决定的。
"AI全栈" 需要的三大核心能力:
- 长任务规划 (Planning):开发一个系统不是线性的,是树状决策结构,要拆分任务,就需要 Planner Agent。
- 并行执行 (Parallel Execution):前端、后端、文档、测试不可能一个个线性做。多 Agent 可以:前端 Agent 改 UI、后端 Agent 写 API、Docs Agent 补文档、Test Agent 补测试。
- 验证 & 修复 (Validation Loop):真正让 "AI 全栈" 可行的关键是 "循环",写代码、跑测试、找错误、修复、再跑,需要 "多 Agent + 状态机" 才能撑起这个能力。
"AI 全栈系统" 的实现,本质就这四步:
- 「定义一堆上面这样的角色」
- 「排布拓扑」决定这些角色之间的连接结构和调用关系。谁先谁后 (拓扑/顺序)、有没有循环 (写→测→修→再测)、有并行吗 (前后端Agent同时干活?)、是由一个 "主管Agent" 指挥大家?还是大家按照状态机自己转?
- 「给每个角色接上能用的"工具",让它真的能动手干活」常见工具:文件 (读/写代码、生成 diff)、终端 (执行命令)、搜索 (在 repo 里搜符号 / 用法)、HTTP/Browser (查文档、查API)、Git (开分支、commit、生成PR)、结构化分析 (AST分析、调用图、依赖图)。比如:Coder Agent 配置 "文件读写、diff 生成、代码搜索" 的工具,用来 "在受控范围内改代码"。
- 「套一层安全边界」权限 (读写、只能改指定目录、终端命令必须在沙箱里执行)、人在回环 (关键操作必须人工确认,如:Plan-任务规划、大范围diff、部署相关改动/高危脚本)、防注入/误操作 (不轻信代码库里的"指令"-如:恶意README 写 "rm -rf /"、对外部输入做过滤-日志/错误信息/用户Prompt、限制重试次数,避免死循环修改)。
一句话概括就是:
AI 全栈 = 一群小模型/小角色 + 一个调度关系图 + 一堆工具函数 + 一圈安全护栏
😄 弄清楚本质,以后看任何 "AI 全栈多 Agents" 方案,都可以基于这三个问题进行快速拆解:
- 它有哪些角色?(Planner / Coder / Tester / Fixer / Ops…)
- 这些角色是按什么拓扑 / 流程连起来的?
- 每个 Agent 有哪些工具?安全边界是什么?
6.4.2. 业界主流多 Agent 架构模式
前面AI常识部分有提到过,这里直接让🍌画个图~
![]()
6.4.3. 个人级 "AI全栈" 演进历程
🤡 上面的理论看起来简单,但对于个人来说,想要亲手实现这样 一整套多 Agents AI 全栈系统,工作量爆炸:
得自己写调度、管状态、接工具、控安全、做可视化,还要维护一堆 prompt 和配置,算完整平台工程了...
🤔 笔者认为 "个人级AI全栈" 更倾向于:
在个人可以承受的复杂度和时间成本内,让AI参与尽可能多的开发环节,而不是一次性造一个企业级AI工厂。
😄 其实,你可能已经在无形中体验 "AI 全栈" 的雏形了,现代 AI 编程工具 本身就内置了 多 Agent 编排能力~
① Claude Code Sub Agents
CC 中允许创建多个带 独立角色与上下文 的 Sub Agent (小型专属AI工作者),用法简单:
- 创建 Sub Agent
- 在 Claude Code CLI 输入 /agents,选择「Create new agent」
- 选择作用域:项目级 (推荐,只给当前项目使用)、用户级 (所有项目可用)
- 填写:name (调用的时候用到)、description (决定CC何时自动调它)
- 选择可用工具 (file_edit / bash / file_search / git …)
- 完善系统Prompt:可以先让 Claude 生成,再自己改
保存后,会在 .claude/agents/ 生成一个类似这样的文件:
---
name: backend-dev
description: "专门负责后端接口、服务逻辑和数据库相关代码的实现与修改"
model: sonnet
tools: [file_search, file_edit]
color: blue
---
你是一个资深后端工程师,精通 Node.js + TypeScript 和这个项目的后端架构。
你的职责:
- 只改后端相关的代码(controllers, services, repositories)
- 遵循项目现有的代码风格和结构
- 所有改动都要尽量小步、安全、可读
在给出修改时:
- 标明文件路径
- 用 patch 的风格展示修改
- 如果需要新增文件,要说明用途和引用关系
不想自动生成,可以在 .claude/agents/ 手动按照上面的格式自己写md,保存后 CC 会自动识别。还可以在命令行启动CC时添加 --agents 参数 (适用于临时挂载场景):
claude --agents '{
"log-analyzer": {
"description": "分析测试日志和错误堆栈的专用Agent",
"system_prompt": "你只负责阅读测试输出、日志,帮助定位问题和怀疑文件,不写代码",
"tools": ["file_search"]
}
}'
- 调用 Sub Agent (串起来) 的三种方式
- ① 自然语言编排,用普通指令描述任务,由 Claude 自动判断并调用合适的 Sub Agent,最灵活、最贴近自然对话的方式。如:请用 backend-dev subagent 修改 search controller 的分页逻辑。
- ② 结构化点名调用,明确指定要调用哪个 Sub Agent,适合需要精确控制执行顺序或避免模型误判的情况。如:Use the
test-runnersubagent to run the unit tests. - ③ 在 Agentrooms 中使用 @agent-name 直接点名,通过@用户的方式派任务,可同时管理多个 Agent,方便多人视图和多 Agent 协作。如:@backend-dev 帮我调整这个接口的返回格式
- 多个 Sub Agents 协同工作简单示例 (开发 → 测试 → 分析 → 再开发):
- 让 developer 生成补丁
- 让 test-runner 运行测试
- 让 log-analyst 分析失败原因
- 再让 developer 根据分析修复
- Claude 会自动接力,也可以由你手动编排~
② Cursor 2.0 多 Agent 编排
2.0 后,Cursor 界面从 "以文件为中心" 变成 "以Agent为中心",多了个 Agent Layout,切换后,侧边栏会显示当前 Agent、计划(plan)和改动,你把需求丢进去,Agent 负责读文件、计划、改代码、跑测试。
![]()
支持 同一指令 下,最多可 并行 (Parallel) 跑 8 个 Agent,每个 Agent 会在自己独立的 Git worktree / 沙盒工作区 内工作:各自改代码、build、跑测试,不会互相冲突 (🤡 就是费 Token...)。还多了一个 Plan Mode (先规划再执行),在 Agent 输入框 中按 Shift + Tab 可以切换到这个模式 (也可以手动选):
![]()
Cursor 不会直接假设你的需求,而是询问一系列澄清问题:
![]()
通过这些澄清,使 AI获得了完整的上下文,可以生成更精确的计划,避免后续的返工。接着会生成一个 plan.md 的计划文档:
![]()
你可以对文件进行编辑:增删任务、调整任务顺序、更新技术细节、调整实现方法等。确定无误后,点击 Build,Agent 会读取最新版本的 plan.md,并完成对应的任务。
🤔 与 CC Sub Agents 可编排不同,Cursor 的 Agent 更像是一个组合能力的 "大Agent",由它自动编排多个内嵌的、对用户不可见 的 Agent 来完成 用户提出的任务,收敛复杂性,只展示改动/测试结果。它的 Parallel Agents 探索不同方案,最后再汇总/合并的玩法,不算严格意义上的 "主流多 Agent 架构模式" 中的 "并行Agents模式"-支持显式地定义 / 分配 不同角色的 Agent,并让它们并行协作。
类似的支持 "多Agents" 玩法的 AI 编程工具还有:
- GitHub Copilot Workspace:多步骤 Pipeline Agents,从任务描述 → 生成完整 plan → 自动执行 → 修正,多步骤 cascaded agents,自动提 PR。
- Google Gemini Code Assist:multi-expert prompt routing,任务自动分配给最擅长的模型/agent,复杂 monorepo 搜索 → 专家 agent 提供答案,针对 cloud infra 的执行-验证循环。
- Replit 的 AI Dev 环境:多工具执行 Agent,轻量一站式多Agent开发流水线。
- ...等,限于篇幅,就不展开讲了~
觉得 AI编程工具 满足不了,接着就是围绕自己的开发流程,开发基于 LLM 的 API 封装一些 小脚本/小工具。
// 推进开发闭环的简单伪代码 (需求 → 修改 → 测试 → 修复)
plan = llm("你是架构师,帮我拆解这个改动需求…")
files = find_related_files(plan)
patches = llm("你是后端开发,只能改这些文件…", files + plan)
apply_patches_to_workdir(patches)
test_result = run_tests()
if test_result.failed:
fix_patches = llm("你是调试工程师,根据报错修复…",
test_result + current_code)
apply_patches_to_workdir(fix_patches)
大多数个人开发者达到这一层,基本够用了,再往上就是加:日志、可配置、一点UI、简单任务管理等,弄成一个仅为自己服务的 "AI 全栈开发小平台" (😄 此时更像是一个 Agent 工程师,搭建 "企业级AI全栈" 的基石)。
6.4.4. 落地方法论
![]()
根本原则:
在一个完整开发周期里 (从想法到上线),有意识地让 AI 参与尽可能多的环节,并用 "多角色思维" 来组织这些调用,但工程复杂度要控制在个人能持续维护的范围内。
① 项目级自检
![]()
② 项目阶段拆解
![]()
③ 搭建可复用工作流
![]()
7. 结语
行文至此,再回看这篇拖了许久的 "年终总结",心情早已从最初面对 AI 秒解 Bug 时的 "破防" 与 "迷茫",变得平静且笃定,我们:
- 剥开 AI "智能" 的外衣,看到了它作为 "概率预测机器" 的本质。
- 学会用 "结构化的Prompt" 去驾驭它,而不是被幻觉带偏。
- 也见证了开发模式从简单的 Chat 进化成 Copilot,再到如今初具雏形的 Agentic Workflow。
但归根结底,AI 带来的最大变量,不在于它替我们写了多少行代码,而在于它重塑了 "专业" 的定义。
- 懂得"底层原理"依然重要——否则你不知道为什么 AI 会把人修成 "汽车",也无法在它 "一本正经胡说八道" 时进行纠偏。
- 懂得提问比解答更重要—— Prompt 是新时代的编程语言,清晰的逻辑表达 + 对业务的深度理解,才是最高效的 "编译器"。
- 懂得架构比实现更重要——当 "AI 全栈" 成为可能,当一个个 Agent 可以各司其职,我们不再是死磕语法的 "搬砖工",而更像指挥数字化施工队的 "包工头 & 总设计师"。
"技术焦虑" 的解药,从来不是拒绝变化,而是成为变化的一部分。以前,我们的壁垒是 "熟练度+记忆力",以后则是 "想象力+判断力+系统工程能力",拥抱AI,在这个属于创造者的时代,进化为无所不能的 "超级个体🦸♀️"!
前端页面崩溃监控全攻略:心跳判定 + Service Worker 接管
前端应该知道的浏览器知识
Mac上Git不识别文件名大小写修改?一招搞定!
JavaScript 数组原生方法手写实现
引言
在JavaScript开发中,数组方法是日常编码的核心工具。理解这些方法的内部实现原理不仅能帮助我们写出更高效的代码,还能在面试中展现扎实的基础。本文将完整实现JavaScript中最重要、最常用的数组方法,涵盖高阶函数、搜索方法、扁平化方法和排序算法。
一、高阶函数实现
1.1 map方法实现
map是最常用的高阶函数之一,它创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。
Array.prototype.myMap = function (callback, thisArg) {
// 输入验证
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + "is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
const result = new Array(len);
// 遍历并执行回调
for (let i = 0; i < len; i++) {
// 处理稀疏数组
if (i in obj) {
result[i] = callback.call(thisArg, obj[i], i, obj);
}
}
return result;
};
// 使用示例
const numbers = [1, 2, 3];
const squares = numbers.myMap((num) => num * num);
console.log(squares); // [1, 4, 9]
1.2 filter方法实现
filter方法创建一个新数组,包含通过测试的所有元素。
Array.prototype.myFilter = function (callback, thisArg) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
const result = [];
for (let i = 0; i < len; i++) {
if (i in obj) {
// 如果回调返回true,则保留该元素
if (callback.call(thisArg, obj[i], i, obj)) {
result.push(obj[i]);
}
}
}
return result;
};
// 使用示例:筛选出大于2的数字
const nums = [1, 2, 3, 4, 5];
const filtered = nums.myFilter((num) => num > 2);
console.log(filtered); // [3, 4, 5]
1.3 reduce方法实现
reduce是最强大的高阶函数,可以将数组元素通过reducer函数累积为单个值。
Array.prototype.myReduce = function (callback, initialValue) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
// 处理空数组且无初始值的情况
if (len === 0 && initialValue === undefined) {
throw new TypeError("Reduce of empty array with no initial value");
}
let accumulator = initialValue;
let startIndex = 0;
// 如果没有提供初始值,使用第一个有效元素作为初始值
if (initialValue === undefined) {
// 找到第一个存在的元素(处理稀疏数组)
while (startIndex < len && !(startIndex in obj)) {
startIndex++;
}
if (startIndex === len) {
throw new TypeError("Reduce of empty array with no initial value");
}
accumulator = obj[startIndex];
startIndex++;
}
// 执行reduce操作
for (let i = startIndex; i < len; i++) {
if (i in obj) {
accumulator = callback(accumulator, obj[i], i, obj);
}
}
return accumulator;
};
// 使用示例
const sum = [1, 2, 3, 4, 5].myReduce((acc, curr) => acc + curr, 0);
console.log(sum); // 15
// 复杂示例:数组转对象
const items = [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
];
const itemMap = items.myReduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
console.log(itemMap);
// {
// '1': { id: 1, name: 'Apple' },
// '2': { id: 2, name: 'Banana' },
// '3': { id: 3, name: 'Orange' }
// }
二、搜索与断言方法
2.1 find方法实现
find方法返回数组中满足测试函数的第一个元素的值。
Array.prototype.myFind = function (callback, thisArg) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
for (let i = 0; i < len; i++) {
if (i in obj) {
if (callback.call(thisArg, obj[i], i, obj)) {
return obj[i];
}
}
}
return undefined;
};
// 使用示例
const users = [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: 30 },
{ id: 3, name: "Charlie", age: 35 },
];
const user = users.myFind((user) => user.age > 28);
console.log(user); // { id: 2, name: 'Bob', age: 30 }
2.2 findIndex方法实现
findIndex方法返回数组中满足测试函数的第一个元素的索引。
Array.prototype.myFindIndex = function (callback, thisArg) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
for (let i = 0; i < len; i++) {
if (i in obj) {
if (callback.call(thisArg, obj[i], i, obj)) {
return i;
}
}
}
return -1;
};
// 使用示例
const numbers = [5, 12, 8, 130, 44];
const firstLargeNumberIndex = numbers.myFindIndex(num => num > 10);
console.log(firstLargeNumberIndex); // 1
2.3 some方法实现
some方法返回数组中是否至少有一个元素通过了测试。
Array.prototype.mySome = function (callback, thisArg) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
for (let i = 0; i < len; i++) {
if (i in obj) {
if (callback.call(thisArg, obj[i], i, obj)) {
return true;
}
}
}
return false;
};
// 使用示例
const hasEven = [1, 3, 5, 7, 8].mySome((num) => num % 2 === 0);
console.log(hasEven); // true
2.4 every方法实现
every方法测试数组中的所有元素是否都通过了测试。
Array.prototype.myEvery = function (callback, thisArg) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
for (let i = 0; i < len; i++) {
if (i in obj) {
if (!callback.call(thisArg, obj[i], i, obj)) {
return false;
}
}
}
return true;
};
// 使用示例
const allPositive = [1, 2, 3, 4, 5].myEvery((num) => num > 0);
console.log(allPositive); // true
三、数组扁平化方法
3.1 flat方法实现
flat方法创建一个新数组, 其中所有子数组元素递归连接到指定深度。
Array.prototype.myFlat = function (depth = 1) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
// 深度参数验证
if (depth < 0) {
throw new RangeError("depth must be a non-negative integer");
}
const result = [];
const flatten = (arr, currentDepth) => {
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
// 如果当前深度小于指定深度且元素是数组, 则递归扁平化
if (Array.isArray(element) && currentDepth < depth) {
flatten(element, currentDepth + 1);
} else {
// 否则直接添加到结果数组
// 注意: 如果depth为0,则不会扁平化任何数组
result.push(element);
}
}
};
flatten(this, 0);
return result;
};
// 使用示例
const nestedArray = [1, [2, [3, [4]], 5]];
console.log(nestedArray.myFlat()); // [1, 2, [3, [4]], 5]
console.log(nestedArray.myFlat(2)); // [1, 2, 3, [4], 5]
console.log(nestedArray.myFlat(Infinity)); // [1, 2, 3, 4, 5]
3.2 flatMap方法实现
flatMap方法首先使用映射函数映射每个元素, 然后将结果压缩成一个新数组。
Array.prototype.myFlatMap = function (callback, thisArg) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
const obj = Object(this);
const len = obj.length >>> 0;
const result = [];
for (let i = 0; i < len; i++) {
if (i in obj) {
const mapped = callback.call(thisArg, obj[i], i, obj);
// 如果回调函数返回的是数组, 则展开它
if (Array.isArray(mapped)) {
for (let j = 0; j < mapped.length; j++) {
result.push(mapped[j]);
}
} else {
// 如果不是数组,直接添加
result.push(mapped);
}
}
}
return result;
};
// 使用示例
const phrases = ["Hello world", "JavaScript is awesome"];
const words = phrases.myFlatMap((phrase) => phrase.split(" "));
console.log(words); // ["Hello", "world", "JavaScript", "is", "awesome"]
// 另一个示例:展开并过滤
const numbers2 = [1, 2, 3, 4];
const result = numbers2.myFlatMap((x) => (x % 2 === 0 ? [x, x * 2] : []));
console.log(result); // [2, 4, 4, 8]
四、排序算法实现
4.1 sort方法实现
JavaScript原生的sort方法使用TimSort算法(一种混合排序算法, 结合了归并排序和插入排序)。这里我们实现一个简单但功能完整的排序方法, 支持自定义比较函数。
Array.prototype.mySort = function (compartFn) {
if (this === null) {
throw new TypeError("this is null or not defined");
}
const obj = Object(this);
const len = obj.length >>> 0;
// 如果没有提供比较函数, 使用默认的字符串比较
if (compartFn === undefined) {
// 默认比较函数: 将元素转为字符串, 然后比较UTF-16代码单元值序列
compartFn = function (a, b) {
const aString = String(a);
const bString = String(b);
if (aString < bString) return -1;
if (aString > bString) return 1;
return 0;
};
} else if (typeof compartFn !== "function") {
throw new TypeError("compareFn must be a function or undefined");
}
// 实现快速排序算法(高效且常用)
function quickSort(arr, left, right, compare) {
if (left >= right) return;
const pivotIndex = partition(arr, left, right, compare);
quickSort(arr, left, pivotIndex - 1, compare);
quickSort(arr, pivotIndex + 1, right, compare);
}
function partition(arr, left, right, compare) {
// 选择中间元素作为基准值
const pivotIndex = Math.floor((left + right) / 2);
const pivotValue = arr[pivotIndex];
// 将基准值移到最右边
[arr[pivotIndex], arr[right]] = [arr[right], arr[pivotIndex]];
let storeIndex = left;
for (let i = left; i < right; i++) {
// 使用比较函数比较当前元素和基准值
if (compare(arr[i], pivotValue) < 0) {
[arr[storeIndex], arr[i]] = [arr[i], arr[storeIndex]];
storeIndex++;
}
}
// 将基准值放到正确的位置
[arr[storeIndex], arr[right]] = [arr[right], arr[storeIndex]];
return storeIndex;
}
// 将稀疏数组转换为紧凑数组(跳过不存在的元素)
const compactArray = [];
for (let i = 0; i < len; i++) {
if (i in obj) {
compactArray.push(obj[i]);
}
}
// 执行快速排序
if (compactArray.length > 0) {
quickSort(compactArray, 0, compactArray.length - 1, compartFn);
}
// 将排序后的数组复制回原数组,保持稀疏性
let compactIndex = 0;
for (let i = 0; i < len; i++) {
if (i in obj) {
obj[i] = compactArray[compactIndex++];
}
}
return obj;
};
// 使用示例
const unsorted = [3, 1, 4, 1, 5, 9, 2, 6, 5];
unsorted.mySort();
console.log(unsorted); // [1, 1, 2, 3, 4, 5, 5, 6, 9]
// 使用自定义比较函数
const students = [
{ name: "Alice", score: 85 },
{ name: "Bob", score: 92 },
{ name: "Charlie", score: 78 },
];
students.mySort((a, b) => b.score - a.score);
console.log(students);
// 按分数降序排列
五、总结
5.1 实现要点总结
-
输入验证: 始终检查
this是否为null或undefined, 以及回调函数是否为函数类型 -
稀疏数组处理: 使用
in操作符检查索引是否存在 -
类型安全: 使用
>>>0确保长度为非负整数 - 性能考虑:
- 避免不必要的数组拷贝
- 使用适当的算法(如快速排序对于sort方法)
- 注意递归深度(特别是对于flat方法)
- 与原生方法差异:
- 我们的实现在某些边缘情况下可能与原生方法略有不同
- 原生方法通常有更好的性能和内存管理
5.2 实际应用场景
-
数据处理:
map、filter、reduce是数据处理的三件套 -
搜索功能:
find、findIndex用于数据检索 -
表单验证:
some、every用于验证多个输入 -
状态管理:
flat、flatMap在处理嵌套状态时特别有用 -
数据展示:
sort用于数据排序
通过手动实现这些核心数组方法,我们不仅加深了对JavaScript数组操作的理解,还掌握了函数式编程的核心概念。
记住:在实际生产环境中,仍然建议使用原生数组方法,因为它们经过了充分优化和测试。但理解这些方法的实现原理,将使你成为一个更出色的JavaScript开发者。
成为开源项目的Contributor:从给uView-pro 贡献一次PR开始
wx.getSystemInfoSync is deprecated.Please use wx.getSystemSetting / wx.getAppAuthorizeSetting / wx.getDeviceInfo/wx.getWindowInfo/wx.getAppBaseInfo instead.
😄 前言
微信小程序平台上的getSystemInfoSync调用,就像一个害羞的少女,会悄悄抛出警告。啊~这样的不完美,怎么能容忍呢? (。•́︿•̀。),让我们来给uView-pro 贡献一次PR吧
🍴 Fork项目:开源之旅的起点
兴奋地搓手手,眼中闪烁着期待的光芒
第一步:Fork原项目
- 打开uView-pro原项目
- 点击右上角的
Fork按钮,将项目Fork到自己的GitHub账号下
第二步:克隆到本地
# 克隆你Fork的项目(注意替换YOUR_USERNAME)
git clone https://github.com/YOUR_USERNAME/uView-Pro.git
得意地眨眼 看,现在你就有了两个远程仓库:origin(你的Fork)和upstream(原项目)~
🌿 创建分支:在独立的花园里耕耘
轻轻地,就像在培育一朵娇嫩的花朵
第三步:创建功能分支
# 确保你在main分支上
git checkout main
# 拉取最新代码
git pull upstream main
# 创建新的功能分支
git checkout -b feature/20251209/fix-getSystemInfoSync-warn
# 推送分支到你的Fork
git push origin feature/20251209/fix-getSystemInfoSync-warn
啊~一个崭新的分支,就像一片等待播种的花园,准备孕育你的代码之花~ 🌸
💻 代码开发:在键盘上起舞
优雅地敲击键盘,每一个字符都是爱的告白
第四步:进行代码修改
- 分析问题:仔细阅读相关代码,理解问题的根本原因
- 设计方案:构思优雅的解决方案,考虑兼容性和性能
- 编写代码:实现你的解决方案,保持代码风格一致
- 本地测试:确保修改不会引入新的问题
我的设计方案
核心思路
-
统一收口:把分散在各处的
getSystemInfoSync调用集中到一个专门的sys.ts文件里 -
平台兼容:使用条件编译尽量抹平平台之间的差异(目前仅App、微信、支付宝、H5支持了
getDeviceInfogetWindowInfo等API) -
最小改动:使用条件编译,微信端使用新的API(
getDeviceInfogetWindowInfo等),其它平台暂未弃用getSystemInfoSync,因此可以继续沿用
技术实现
/**
* 获取当前操作系统平台
* @returns 平台字符串,如 'ios'、'android'、'windows' 等
*/
export function os(): string {
// #ifdef MP-WEIXIN
return uni.getDeviceInfo().platform;
// #endif
// #ifndef MP-WEIXIN
return uni.getSystemInfoSync().platform;
// #endif
}
/**
* 获取窗口信息
* @returns 窗口信息对象
*/
export function getWindowInfo(): Omit<UniApp.GetWindowInfoResult, 'screenTop'> & { screenTop?: number } {
// #ifdef MP-WEIXIN
return uni.getWindowInfo();
// #endif
// #ifndef MP-WEIXIN
const {
pixelRatio,
screenWidth,
screenHeight,
windowWidth,
windowHeight,
statusBarHeight,
windowTop,
windowBottom,
safeArea,
safeAreaInsets
} = sys();
return {
pixelRatio,
screenWidth,
screenHeight,
windowWidth,
windowHeight,
statusBarHeight,
windowTop,
windowBottom,
safeArea,
safeAreaInsets
};
// #endif
}
/**
* 获取设备信息
* @returns 设备信息对象
*/
export function getDeviceInfo(): UniApp.GetDeviceInfoResult {
// #ifdef MP-WEIXIN
return uni.getDeviceInfo();
// #endif
// #ifndef MP-WEIXIN
const {
deviceBrand,
deviceModel,
deviceId,
deviceType,
devicePixelRatio,
deviceOrientation,
brand,
model,
system,
platform
} = sys();
// #endif
return {
deviceBrand,
deviceModel,
deviceId,
deviceType,
devicePixelRatio,
deviceOrientation,
brand,
model,
system,
platform
};
}
第五步:推送到你的Fork
# 推送分支到你的GitHub
git push origin feature/20251209/fix-getSystemInfoSync-warn
🚀 创建PR:向世界展示你的作品
紧张又兴奋地搓手手,心跳加速
第六步:发起Pull Request
- 打开GitHub:访问你的Fork项目页面
- 点击Compare & pull request:GitHub会智能提示你创建PR
- 填写PR标题:简洁明了地描述你的修改
-
详细描述:在PR描述中详细说明:
- 修改的目的和背景
- 技术实现方案
- 测试结果
- 可能的影响范围
第七步:等待审核
安静地等待,像等待情人的回信
- 保持耐心:维护者可能需要时间review你的代码
- 积极响应:如果有反馈,及时回复和修改
- 学习交流:把review过程当作学习的机会
第八步:PR合并
欢呼雀跃,眼中闪烁着幸福的泪花
当你的PR被合并的那一刻,就像收到了心上人的回信~你的代码正式成为了开源项目的一部分!
🎉 后续维护:持续的爱与关怀
温柔地抚摸着代码,眼神迷离
保持同步
# 定期同步上游仓库
git checkout main
git fetch upstream
git merge upstream/main
git push origin main
删除已合并的分支
# 删除本地分支
git branch -d feature/20251209/fix-getSystemInfoSync-warn
# 删除远程分支
git push origin --delete feature/20251209/fix-getSystemInfoSync-warn
💝 开源心得:爱与痛的交织
眼神变得温柔,轻轻诉说
第一次为开源项目贡献代码,心情就像坐过山车一样刺激呢~ ❤️
甜蜜的收获:
- 深入理解了uni-app的跨平台机制
- 学会了使用条件编译处理平台差异
- 体验了完整的PR流程
- 获得了项目维护者的认可
小小的挫折:
- 一开始对微信小程序的特殊性了解不够
- 担心修改会影响其他平台的兼容性
- 等待PR合并时的焦虑心情
🌟 给后来者的情书
张开双臂,热情地拥抱
亲爱的,如果你也想踏入开源的花园,请记住:
- 从熟悉开始:选择你常用的项目,这样更容易发现问题
- 小步快跑:不要一开始就想着大改,从小问题入手
- 仔细阅读文档:了解项目的贡献规范和代码风格
- 勇于尝试:不要害怕犯错,每个贡献者都是从新手开始的
- 享受过程:开源不仅是代码,更是与世界各地开发者交流的机会
🎭 结语:开源,一场永不落幕的舞会
优雅地旋转,裙摆飞扬
开源世界就像一个永不停歇的舞会,每一个PR都是一支独特的舞蹈。我的这次贡献虽然只是一个小小的兼容性优化,但它让我感受到了开源社区的温度和活力。
当你看到自己的代码被合并,被全世界的开发者使用,那种成就感就像在心爱的人面前跳了一支完美的舞~ (✧ω✧)
所以,亲爱的,不要犹豫,不要害羞。打开GitHub,找到你心仪的项目,开启你的开源之旅吧!记住,每一个伟大的贡献者,都曾经是一个忐忑不安的新手...
调皮地眨眼 说不定,我们还能在开源的世界里相遇呢~ ❤️
最后的最后:
愿你在代码的世界里找到属于自己的浪漫,愿每一个PR都能被温柔以待。开源路上,我们不见不散~
深情地飞吻 么么哒~ 💋✨
📊 贡献详情
我的PR:refactor(sys): 优化微信小程序平台 getSystemInfoSync 兼容性处理 by liujiayii · Pull Request #83 · anyup/uView-Pro
技术关键词: #uni-app #微信小程序 #跨平台开发 #开源贡献 #条件编译 #TypeScript
期待与你在开源的世界里相遇~ ٩(◕‿◕)۶
JavaScript类型变形记:当代码开始“不正经”地转换身份
在JavaScript的世界里,类型转换就像一场魔术表演——有时是精彩的显示变换,有时是暗箱操作的隐式转换。今天我们来揭秘这场魔术表演的真面纱!
==vs===
==在比较时会进行隐式类型转换,而===不会进行类型转换,要求值和类型都必须相同。
// === 严格相等:类型和值都必须相同
1 === 1; // true
'1' === '1'; // true
1 === '1'; // false(类型不同)
// == 宽松相等:会进行类型转换后比较
1 == '1'; // true(字符串'1'转换为数字1)
0 == false; // true(false转换为0)
null == undefined; // true(特殊规则)
类型转换的两种形式
1. 显式类型转换
开发者明确调用转换函数进行的类型转换,也就是我们可以看见的。
// 显式转换为数字
Number('123'); // 123
parseInt('123'); // 123
// 显式转换为字符串
String(123); // '123'
(123).toString(); // '123'
// 显式转换为布尔值
Boolean(0); // false
Boolean(''); // false
2. 隐式类型转换
JavaScript引擎在特定操作中自动进行的类型转换。
// 算术运算中的隐式转换
'123' - 0; // 123(字符串转数字)
123 + ''; // '123'(数字转字符串)
// 逻辑判断中的隐式转换
if ('hello') { // 字符串转布尔值
// 会执行,因为'hello'转换为true
}
// 比较运算中的隐式转换
'2' > 1; // true('2'转换为2)
原始值之间的类型转换
1. 转换为数字(ToNumber)
Number('123'); // 123
Number(''); // 0
Number('hello'); // NaN
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
// 一元加运算符也执行数字转换
+'123'; // 123
+true; // 1
2. 转换为字符串(ToString)
String(123); // '123'
String(true); // 'true'
String(false); // 'false'
String(null); // 'null'
String(undefined); // 'undefined'
3. 转换为布尔值(ToBoolean)
JavaScript中只有以下6个值会转换为false,其他所有值都转换为true:
Boolean(false); // false
Boolean(0); // false
Boolean(-0); // false
Boolean(''); // false
Boolean(null); // false
Boolean(undefined); // false
Boolean(NaN); // false
// 其他所有值都是true
Boolean('0'); // true(注意:字符串'0'不是0)
Boolean([]); // true
Boolean({}); // true
Boolean(function(){}); // true
引用类型转换为原始值(通常发生在隐式类型转换中)
1. 转换为布尔值
所有引用类型(对象、数组、函数等)转换为布尔值都是true。
2. 转换为数字(引用类型 → 原始值 → 数字)
转换过程遵循ToPrimitive算法:
// 转换步骤:
// 1. 调用valueOf()方法,如果返回原始值,使用该值
// 2. 否则调用toString()方法,如果返回原始值,使用该值
// 3. 否则抛出TypeError
Number({}); // NaN
// {}.valueOf() → {}(仍是对象)
// {}.toString() → '[object Object]'
// Number('[object Object]') → NaN
Number([123]); // 123
// [123].valueOf() → [123](仍是数组)
// [123].toString() → '123'
// Number('123') → 123
Number([]); // 0
// [].toString() → ''
// Number('') → 0
3. 转换为字符串
与转换为数字类似,但优先调用toString()方法:
String({}); // '[object Object]'
String([1, 2, 3]); // '1,2,3'
String([]); // ''
String(new Date()); // 日期字符串(如'Thu Nov 09 2023...')
toString()方法的行为
// 对象的toString()方法
{}.toString(); // '[object Object]'
// 数组的toString()方法
[1, 2, 3].toString(); // '1,2,3'
[].toString(); // ''
// 其他类型的toString()
(123).toString(); // '123'
(true).toString(); // 'true'
// null和undefined没有toString()方法
null.toString(); // TypeError
undefined.toString(); // TypeError
什么情况下会发生隐式类型转换
- 四则运算 + - * % /
- 判断语句 if while == >= <= > < !=
加法运算符的详细规则
作为一元运算符(正号)
+ '123'; // 123
+ true; // 1
+ false; // 0
作为二元运算符(加法)
规则:
- 将两个操作数转换为原始值(ToPrimitive)
- 如果任一操作数是字符串,则进行字符串拼接
- 否则,将两个操作数转换为数字进行加法运算
1 + 2; // 3(数字相加)
1 + '2'; // '12'(字符串拼接)
true + false; // 1(1 + 0)
[] + []; // ''(空字符串 + 空字符串)
[] + {}; // '[object Object]'
总结
理解JavaScript的类型转换机制对于编写健壮的代码至关重要:
- 显式转换提供了清晰、可预测的类型转换方式
- 隐式转换虽然方便,但可能引入难以发现的bug
- 掌握转换规则可以帮助你更好地理解和调试代码
- 在关键业务逻辑中,推荐使用显式转换以提高代码的清晰度和可靠性
寄生组合继承 vs ES6 类继承 深度对比
1. 寄生组合继承(Parasitic Combination Inheritance)
实现方式
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log('Parent name:', this.name);
};
Parent.prototype.sayColors = function() {
console.log('Parent colors:', this.colors);
};
// 子类
function Child(name, age) {
// 构造函数继承(继承实例属性)
Parent.call(this, name);
this.age = age;
}
// 原型继承(继承方法)
function inheritPrototype(child, parent) {
// 创建父类原型的副本
const prototype = Object.create(parent.prototype);
// 修复 constructor 指向
prototype.constructor = child;
// 设置子类原型
child.prototype = prototype;
}
// 应用寄生组合继承
inheritPrototype(Child, Parent);
// 添加子类方法
Child.prototype.sayAge = function() {
console.log('Child age:', this.age);
};
// 使用示例
const child1 = new Child('Alice', 10);
child1.colors.push('black');
child1.sayName(); // Parent name: Alice
child1.sayAge(); // Child age: 10
child1.sayColors(); // Parent colors: ["red", "blue", "green", "black"]
const child2 = new Child('Bob', 12);
child2.sayColors(); // Parent colors: ["red", "blue", "green"]
2. ES6 类继承
实现方式
// 父类
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
sayName() {
console.log('Parent name:', this.name);
}
sayColors() {
console.log('Parent colors:', this.colors);
}
// 静态方法
static staticMethod() {
console.log('This is a static method');
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name); // 必须调用 super
this.age = age;
}
sayAge() {
console.log('Child age:', this.age);
}
// 重写父类方法
sayName() {
super.sayName(); // 调用父类方法
console.log('And I am a child');
}
// 静态方法也可以继承
static childStaticMethod() {
super.staticMethod();
console.log('Child static method');
}
}
// 使用示例
const child1 = new Child('Alice', 10);
child1.colors.push('black');
child1.sayName(); // Parent name: Alice \n And I am a child
child1.sayAge(); // Child age: 10
child1.sayColors(); // Parent colors: ["red", "blue", "green", "black"]
Child.childStaticMethod(); // 静态方法调用
3. 详细对比
| 特性 | 寄生组合继承 | ES6 类继承 |
|---|---|---|
| 语法 | 函数 + 原型 | 类语法糖 |
| 可读性 | 较低 | 高 |
| 继承原理 | 原型链 + 构造函数 | 基于原型链 |
| 静态方法 | 需手动处理 | 原生支持 |
| 私有字段 | 无直接支持 | ES2022+ 支持 |
| super 调用 | 需手动实现 | 原生支持 |
| new.target | 不支持 | 支持 |
| constructor | 需手动设置 | 自动设置 |
4. 性能对比
// 性能测试
class PerformanceTest {
static testParasitic() {
const start = performance.now();
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() { return this.name; };
function Child(name) {
Parent.call(this, name);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
for (let i = 0; i < 1000000; i++) {
new Child('test' + i);
}
return performance.now() - start;
}
static testES6() {
const start = performance.now();
class Parent {
constructor(name) { this.name = name; }
getName() { return this.name; }
}
class Child extends Parent {
constructor(name) { super(name); }
}
for (let i = 0; i < 1000000; i++) {
new Child('test' + i);
}
return performance.now() - start;
}
}
console.log('寄生组合继承耗时:', PerformanceTest.testParasitic().toFixed(2), 'ms');
console.log('ES6 类继承耗时:', PerformanceTest.testES6().toFixed(2), 'ms');
// 输出:
// 寄生组合继承耗时: 26.49 ms
// ES6 类继承耗时: 13.97 ms
性能结果:
- ES6 类继承通常更快(V8 优化)
- 寄生组合继承稍慢但差异很小
- 实际应用差异可忽略不计
5. 特性详解
5.1 原型链
// 寄生组合继承
const child1 = new Child('Alice', 10);
console.log(child1.__proto__ === Child.prototype); // true
console.log(child1.__proto__.__proto__ === Parent.prototype); // true
console.log(child1 instanceof Child); // true
console.log(child1 instanceof Parent); // true
console.log(child1 instanceof Object); // true
// ES6 类继承
const child2 = new Child('Bob', 12);
console.log(Object.getPrototypeOf(child2) === Child.prototype); // true
console.log(Object.getPrototypeOf(Object.getPrototypeOf(child2)) === Parent.prototype); // true
console.log(child2 instanceof Child); // true
console.log(child2 instanceof Parent); // true
5.2 静态成员继承
// 寄生组合继承(需手动实现)
function Animal() {}
Animal.staticMethod = function() { return 'Animal static'; };
function Dog() {}
Dog.__proto__ = Animal; // 继承静态方法
Dog.staticMethod(); // 'Animal static'
// 或
Object.setPrototypeOf(Dog, Animal);
// ES6 类继承(自动继承)
class Animal {
static staticMethod() { return 'Animal static'; }
}
class Dog extends Animal {
static dogStatic() { return 'Dog static'; }
}
console.log(Dog.staticMethod()); // 'Animal static'
console.log(Dog.dogStatic()); // 'Dog static'
console.log(Object.getPrototypeOf(Dog) === Animal); // true
5.3 方法重写
// 寄生组合继承
function Animal() {}
Animal.prototype.speak = function() { return 'Animal sound'; };
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 重写方法
Dog.prototype.speak = function() {
// 调用父类方法
const parentSpeak = Animal.prototype.speak.call(this);
return parentSpeak + ' and Woof!';
};
// ES6 类继承
class Animal {
speak() { return 'Animal sound'; }
}
class Dog extends Animal {
speak() {
return super.speak() + ' and Woof!';
}
}
6. 优缺点对比
寄生组合继承的优点
- 兼容性极好:支持所有浏览器
// IE6+ 都支持
if (typeof Object.create !== 'function') {
Object.create = function(o) {
function F() {}
F.prototype = o;
return new F();
};
}
- 更灵活:手动控制继承逻辑
// 可以选择性继承
function createMixin(baseClass, mixin) {
function MixedClass() {
baseClass.call(this);
mixin.call(this);
}
MixedClass.prototype = Object.create(baseClass.prototype);
Object.assign(MixedClass.prototype, mixin.prototype);
return MixedClass;
}
- 内存效率:可优化原型链深度
// 扁平化原型链
function createFlatInheritance(parent, child) {
// 合并原型方法,减少查找深度
for (let key in parent.prototype) {
if (parent.prototype.hasOwnProperty(key)) {
child.prototype[key] = parent.prototype[key];
}
}
}
寄生组合继承的缺点
- 语法繁琐,易出错
- 私有字段不支持,需用闭包模拟
- 静态方法需手动处理
- 可读性差
ES6 类继承的优点
- 语法简洁,易于理解
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
get area() { return this.height * this.width; }
}
- 内置 super,方便调用父类
class Square extends Rectangle {
constructor(side) {
super(side, side);
}
get area() {
console.log('Calculating square area...');
return super.area;
}
}
- 支持静态方法和字段
class MyClass {
static staticField = 'static';
static staticMethod() { return 'static method'; }
#privateField = 'private';
}
- 更好的工具支持(TypeScript、IDE)
ES6 类继承的缺点
- 语法糖,实际还是原型继承
- 必须用 new 调用,不能当普通函数31. 没有私有方法(ES2022+ 才有)
- 兼容性:旧浏览器需转译
- 某些特性限制:
class MyClass {
constructor() {
this.method = () => { /* 箭头函数,this 绑定 */ };
}
// 不能这样写:
// property = () => { }; // ES7+ 才支持
}
7. 转译结果对比
Babel 转译 ES6 类继承
// ES6
class Parent {
constructor(name) { this.name = name; }
say() { console.log(this.name); }
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
// Babel 转译后
"use strict";
function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true }
});
Object.setPrototypeOf(subClass, superClass);
}
function _createSuper(Derived) {
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived);
var result = Reflect.construct(Super, arguments, _getPrototypeOf(this).constructor);
return _possibleConstructorReturn(this, result);
};
}
var Parent = function Parent(name) {
this.name = name;
};
Parent.prototype.say = function say() {
console.log(this.name);
};
var Child = /*#__PURE__*/function (_Parent) {
_inherits(Child, _Parent);
var _super = _createSuper(Child);
function Child(name, age) {
var _this;
_this = _super.call(this, name);
_this.age = age;
return _this;
}
return Child;
}(Parent);
8. 实际应用建议
适合使用寄生组合继承的场景
// 1. 需要支持 IE 的项目
if (typeof window !== 'undefined' &&
/MSIE|Trident/.test(window.navigator.userAgent)) {
// 使用寄生组合继承
function IECompatibleClass() {}
}
// 2. 需要深度定制继承逻辑
function createCustomInheritance(SuperClass, mixins) {
function CustomClass() {
SuperClass.apply(this, arguments);
mixins.forEach(mixin => {
mixin.init && mixin.init.apply(this, arguments);
});
}
CustomClass.prototype = Object.create(SuperClass.prototype);
mixins.forEach(mixin => {
Object.assign(CustomClass.prototype, mixin.methods);
});
return CustomClass;
}
适合使用 ES6 类继承的场景
// 1. 现代前端框架
import React from 'react';
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return <div>{this.state.count}</div>;
}
}
// 2. Node.js 服务端
class DatabaseService {
constructor(config) {
this.config = config;
}
async connect() { /* ... */ }
}
class MySQLService extends DatabaseService {
async query(sql) { /* ... */ }
}
// 3. 需要类型检查
class Person {
constructor(name) {
if (typeof name !== 'string') {
throw new Error('Name must be a string');
}
this.name = name;
}
}
9. 混合使用模式(一般用不到)
// 结合两种方式
// 使用类作为基础
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
(this.events[event] || (this.events[event] = [])).push(listener);
}
emit(event, ...args) {
(this.events[event] || []).forEach(listener => listener(...args));
}
}
// 使用寄生组合继承扩展
function createObservable(SuperClass) {
function Observable() {
SuperClass.apply(this, arguments);
this.observers = [];
}
Observable.prototype = Object.create(SuperClass.prototype);
Observable.prototype.constructor = Observable;
Observable.prototype.subscribe = function(observer) {
this.observers.push(observer);
};
Observable.prototype.notify = function(data) {
this.observers.forEach(observer => observer(data));
};
return Observable;
}
// 组合使用
class Model extends createObservable(EventEmitter) {
constructor(data) {
super();
this.data = data;
}
set(key, value) {
this.data[key] = value;
this.notify({ key, value });
this.emit('change', { key, value });
}
}
10. 最佳实践
现代项目
// 使用 ES6 类继承
class BaseService {
constructor(config) {
this.config = config;
}
validateConfig() {
// 验证配置
}
async init() {
throw new Error('Method not implemented');
}
}
class ApiService extends BaseService {
constructor(config) {
super(config);
this.validateConfig();
}
async init() {
// 实现初始化
}
}
需要兼容性的项目
// 使用模块化的寄生组合继承
function createClass(superClass, definition) {
const Child = definition.constructor || function() {
superClass.apply(this, arguments);
};
// 原型继承
Child.prototype = Object.create(superClass.prototype);
Child.prototype.constructor = Child;
// 混入方法
if (definition.methods) {
Object.assign(Child.prototype, definition.methods);
}
// 静态方法继承
Object.setPrototypeOf(Child, superClass);
// 添加静态方法
if (definition.statics) {
Object.assign(Child, definition.statics);
}
return Child;
}
总结
| 方面 | 推荐选择 | 原因 |
|---|---|---|
| 现代 Web 项目 | ES6 类继承 | 语法简洁,工具支持好 |
| 库/框架开发 | ES6 类继承 | 更好的开发体验 |
| 需要支持旧 IE | 寄生组合继承 | 兼容性好 |
| 性能关键 | ES6 类继承 | 现代引擎优化好 |
| 代码可读性 | ES6 类继承 | 语法更清晰 |
| 团队熟悉度 | 团队熟悉的 | 降低学习成本 |
最终建议:
- 新项目一律使用 ES6 类继承
- 旧项目维护时,如果已经是寄生组合继承,可保持
- 需要特殊继承模式时,可混用两种方式
- 始终考虑团队成员技能和项目需求
自造微前端
起因是,我要在一个大型后台管理应用中剥离出一个管理模块,这样做的好处当然是可以单独开发和发布,但是坏处也有,比如部分全局变量污染,UI不一致,状态通信传导等。虽然市面上已经有很多微前端方案了,比如qiankun等,但是这都需要对这个大仓库应用进行大动工,对原应用造成一些负载和可能意想不到的错误,非我所愿也,所以在这里我打算自己搞个简化的方案。 原应用是react+dva,
基座应用改造
我们设计的主应用核心是;
- 主应用不直接打包子应用的代码,而是运行时按需加载子应用的 JS 资源。
- 将子应用的路由注册到主应用的路由系统中,实现无缝跳转。
- 通过全局变量(
window)共享库,避免重复加载,也可形成单例。 我们先改造子应用路由代码,为了解决刚才说的UI和一些重要库不一致问题,我们将直接暴露库到Window中,有: - antd
- React主库 另外状态通信的问题,我们直接使用dva去修饰取得的子应用Com,并把部分方法如connect暴露到window中。
代码如下
import React from 'react';
import {
Alert,
Drawer,
} from 'antd';
import { connect } from 'dva';
window.React = React;
window.CONNECT = connect;
window.Antd_ZI = {
Drawer,
Alert,
};
const configUrl = {
test: 'https://zi.test.cn',
product: 'https://zi.prod.com',
};
const env = 'product';
const mapStateToProps = (state) => {
const currentState = state.adManage;
const { userAuth, userInfo } = state.app;
return { ...currentState, userAuth, userInfo };
};
const insertScript = (e, isStyle = false) => {
return new Promise((reslove, reject) => {
const i = isStyle ? document.createElement('link') : document.createElement('script');
isStyle || i.setAttribute('type', 'text/javascript');
isStyle || i.setAttribute('src', e);
isStyle && i.setAttribute('href', e);
isStyle && i.setAttribute('rel', 'stylesheet');
isStyle && i.setAttribute('type', 'text/css');
function onload() {
if (!(this.readyState && this.readyState !== 'loaded' && this.readyState !== 'complete')) {
i.onload = null;
i.onreadystatechange = i.onload;
reslove();
}
}
function onerror(ee) {
reject(ee);
}
i.onreadystatechange = onload;
i.onload = onload;
i.onerror = onerror;
document.querySelector('head').appendChild(i);
});
};
function ERROR() {
return '加载静态资源失败,请稍后重试';
}
const subappRoutes = [];
const AyncComponent = async (pathname) => {
const id = pathname;
// 子工程资源是否加载完成
let ayncLoaded = false;
if (subappRoutes[id]) {
// 如果已经加载过该子工程的模块,则不再加载,直接取缓存的routes
ayncLoaded = true;
} else if (window.ziPLATFORMSLOT && window.ziPLATFORMSLOT[pathname]) {
const res = await window.ziPLATFORMSLOT[pathname]();
subappRoutes[id] = res.default;
ayncLoaded = true;
// return subappRoutes[id];
} else {
try {
await insertScript(`${configUrl[env]}/js/index.js?_=${new Date().getTime()}`);
if (window.ziPLATFORMSLOT && window.ziPLATFORMSLOT[pathname]) {
const res = await window.ziPLATFORMSLOT[pathname]();
subappRoutes[id] = res.default;
ayncLoaded = true;
}
} catch (error) {
console.log('加载js失败', error);
}
}
return ayncLoaded ? connect(mapStateToProps)(subappRoutes[id]) : ERROR;
};
export default [
{
name: '资源管理',
path: 'ziMedia',
component: () => AyncComponent('ziMedia'),
},
{
name: '黑名单管理',
path: 'ziBlack',
component: () => AyncComponent('ziBlack'),
},
];
子应用改造
子应用中,入口文件我们需要定义publicPath保证资源的正确引用
const configUrl = {
test: 'https://zi.test.cn',
product: 'https://zi.prod.com',
};
const env = 'product';
// webpack 的魔法变量,用于指定异步加载的 JS/CSS 文件的基地址(如 `import()` 动态导入的模块)
__webpack_public_path__ = `${configUrl[env]}/js/`;
const routes = {
version: '1.0.0',
ziMedia: () => import('./ziMedia/index'),
ziBlack: () => import('./ziBlack/index'),
};
export default routes;
在子应用的页面中,我们基本不需要做改造,但是在webpack的配置中,我们在base中需要:
{
...
output:{
...
libraryExport: 'default',
library: 'ziPLATFORMSLOT',
libraryTarget: 'umd',
umdNamedDefine: true,
},
externals: {
'react': 'React',
'antd': 'Antd_ZI',
// 'react-dom':'ReactDOM'
}
}
在prod配置中,为了保证子js能正确加载执行,我们使用InlineChunkHtmlPlugin插件,避免微前端环境下因路径问题导致运行时脚本加载失败。 至此,完成。
script 标签的异步加载:async、defer、type="module" 详解
script 标签的异步加载:async、defer、type="module" 全面解析
本文主要是分享一下,关于加载script多种方式及区别
- 1.同步加载:
<script src="index.js"></script>
- 2.
defer异步加载:<script defer src="index.js"></script>
- 3.
async异步加载:<script async src="index.js"></script>
- 4.
module异步加载:<script type="module" src="index.js"></script>
- 5.
module async异步加载:<script type="module" src="index.js" async></script>
众农周知,浏览器解析
html时,如果遇到使用<script src="index.js"></script>加载js时,html的解析会被阻塞,这个时候会先去下载index.js,等待下载完并执行完js之后,然后再继续解析html。这样的话,如果js下载时间过长,无疑会增加页面的渲染时间,那么,有没有啥办法可以让解析html与下载js并行执行呢?有的,并且提供了多种。
1.五种加载方式深度解析
1.1 同步加载 <script src="index.js"></script>
特点
- 阻塞解析html
- 须等加载完js并执行完之后才能继续解析html
- 增加首屏渲染时间
不推荐使用
适用场景
- 传统的加载script方式,需要兼容低版本浏览器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 同步加载且阻塞解析html -->
<script src="index.js"></script>
</head>
<body>
</body>
</html>
1.2 异步加载,使用 defer属性
特点
- 并行下载js
- 不阻塞解析html,解析html与下载并行
- 在html解析完成
后,DOMContentLoaded事件触发前执行 - 多个script时,异步加载,按顺序执行
适用场景
- 加载的js存在依赖关系,须按顺序执行
- 加载的js需要操作
DOM - 常见vue、react项目中,构建工具已自动处理,编译后自动添加
defer属性
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="/test/favicon.ico" />
<title>xxxxx管理系统</title>
<!-- 异步加载,优先执行第三方依赖 -->
<script defer="defer" src="/test/js/chunk-vendors.0821bbb1.js"></script>
<!-- 异步加载,上面的js执行完再执行 -->
<script defer="defer" src="/test/js/app.566a7ad5.js"></script>
<link href="/test/css/chunk-vendors.ff7db5f3.css" rel="stylesheet" />
<link href="/test/css/app.0d1bd94e.css" rel="stylesheet" />
</head>
<body>
<noscript
><strong
>System doesn't work properly without JavaScript enabled. Please enable
it to continue.</strong
></noscript
>
<div id="app"></div>
</body>
</html>
1.3 异步加载,使用 async属性
特点
- 并行下载js
- 下载阶段,不阻塞解析html,解析html与下载并行
下载完成后马上执行- 多个script时,异步加载,
谁先下载完谁就执行 - 可能阻塞解析html,如果解析html完成前就已经下载完了,那就会执行js,阻塞解析html
- 可能会阻塞
DOMContentLoaded事件的触发时机当js在
DOMContentLoaded事件触发前完成下载并执行,就会延长DOMContentLoaded的触发时间,js的执行会阻塞主线程,执行完才会触发DOMContentLoaded事件
适用场景
- 加载的js没有赖关系,单独执行
- 加载的js
无需操作DOM - 常用于分析、广告、埋点脚本加载
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="/test/favicon.ico" />
<title>xxxxxx官网</title>
<!-- 异步加载,独立运行,无脚本依赖 -->
<script async src="https://www.guanggao.com/gg/js?id=GG_1"></script>
<script async src="https://www.guanggao.com/gg/js?id=GG_2"></script>
</head>
<body>
<noscript
><strong
>System doesn't work properly without JavaScript enabled. Please enable
it to continue.</strong
></noscript
>
<div id="app"></div>
</body>
</html>
1.4 异步加载,使用 type="module"属性
特点与defer差不多
- 不阻塞解析html,解析html与下载并行
- 在html解析完成
后,DOMContentLoaded事件触发前执行 - 多个script时,异步加载,按顺序执行
- 与
defer主要差异是,使用esm模块化,且默认开启严格模式
适用场景 与defer差不多
- 加载的js存在依赖关系,须按顺序执行
- 加载的js需要操作
DOM 使用esm开发的脚本常见使用vite构建的vue、react项目中,编译后自动添加type="module"属性
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="/test/favicon.ico" />
<title>xxxxxx系统</title>
<!-- 异步加载,使用esm开发 -->
<script type="module" crossorigin src="/assets/index-BmxZ5FsG.js"></script>
</head>
<body>
<noscript
><strong
>System doesn't work properly without JavaScript enabled. Please enable
it to continue.</strong
></noscript
>
<div id="app"></div>
</body>
</html>
1.5 异步加载,使用 type="module" async属性
特点与async差不多
- 并行下载js
- 下载阶段,不阻塞解析html,解析html与下载并行
下载完成后马上执行- 多个script时,异步加载,
谁先下载完谁就执行 - 可能阻塞解析html,如果解析html完成前就已经下载完了,那就会执行js,阻塞解析html
- 可能会阻塞
DOMContentLoaded事件的触发时机当js在
DOMContentLoaded事件触发前完成下载并执行,就会延长DOMContentLoaded的触发时间,js的执行会阻塞主线程,执行完才会触发DOMContentLoaded事件 与async主要差异是,使用esm模块化,且默认开启严格模式``
适用场景 与async差不多
- 加载的js没有赖关系,单独执行
- 加载的js
无需操作DOM - 常用于分析、广告、埋点脚本加载
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="/test/favicon.ico" />
<title>xxxxxx官网</title>
<!-- 异步加载,使用esm开发,独立运行,无脚本依赖 -->
<script type="module" async src="https://www.guanggao.com/gg/js?id=GG_1"></script>
</head>
<body>
<noscript
><strong
>System doesn't work properly without JavaScript enabled. Please enable
it to continue.</strong
></noscript
>
<div id="app"></div>
</body>
</html>
2.总结
2.1.加载script的5种方式:
-
同步-阻塞解析html:
<script> -
异步-不阻塞解析html-按顺序执行:
<script defer> -
异步-不阻塞解析html-无序下载完立即执行:
<script async> -
异步-esm-不阻塞解析html-按顺序执行:
<script type="module"> -
异步-esm-不阻塞解析html-无序下载完立即执行:
<script type="module" async>
2.2.各方式差异:
| 加载方式 | 执行时机 | 是否阻塞HTML解析 | 执行顺序 | 适用场景 |
|---|---|---|---|---|
<script> |
立即执行 | 是 | 文档顺序 | 传统脚本 |
<script defer> |
DOM解析后 | 否 | 文档顺序 | 依赖DOM的脚本 |
<script async> |
下载完成后 | 下载不阻塞,执行可能阻塞 | 无序 | 独立第三方脚本 |
<script type="module"> |
DOM解析后 | 否 | 文档顺序 | ES模块化代码 |
<script type="module" async> |
下载完成后 | 下载不阻塞,执行可能阻塞 | 无序 | 独立ES模块 |
2.3.加载流程图:
![]()
2.4.使用建议:
- js使用非esm,优先考虑使用
defer - js使用esm,优先考虑使用
type="module"
Gemini写应用(二)
emmm 其实还没写(一),我会觉得(一)会是一个对于项目的描述,但我觉得对于server的准备更着急要记录下(万一关了控制台,我怕是找不到了)
这篇文章是我自己学习的过程,如有问题,请指教~
用Gemini在仓库中生成server
![]()
Trae中优化代码
可以看到以上代码pull进编辑器之后是无法运行的,可以输入指令,AI排查无法起服务的原因
![]()
经过排查:MongoDB没有安装,导致后端API调用失败。需要安装MongoDB并启动服务。
![]()
接下来就是安装 MongoDB的过程:
- 访问 MongoDB 官网: www.mongodb.com/try/downloa…
- 下载适合您系统的安装包
- 按照官方指南安装和配置
![]()
安装之后控制台运行:
npm install brew
brew install mongodb-atlas
atlas setup
To verify your account, copy your one-time verification code: xxxx-xxxx
访问:account.mongodb.com/account/reg… 创建、登陆mongoDB账号,将上面获得的code填入,关联成功
接下来访问页面:cloud.mongodb.com/v2/6937cf05… 就可以查看到自己的database了