[
  {
    "path": ".github/ISSUE_TEMPLATE/提交招聘网址.md",
    "content": "---\nname: 提交招聘网址\nabout: 提交招聘网址\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n## 公司\n\n## 网址\n\n- 校招\n- 社招\n\n## 城市\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/提交问题和建议.md",
    "content": "---\nname: 提交问题和建议\nabout: 提交问题和建议\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n## 遇到了什么问题？\n\n## 有什么建议？\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/提交面试真题.md",
    "content": "---\nname: 提交面试真题\nabout: 提交你遇到的面试真题，我们会收录并解答\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n请按照如下模板提交面试题，尽量提供全面信息，否则不予收录。\n\n## 题目描述\n\n题目标题\n\n题目描述\n\n相关代码（可选）\n\n相关截图（可选）\n\n## 面试的公司\n\n公司名称\n\n官网网址\n\n## 城市\n\nxx\n"
  },
  {
    "path": ".github/workflows/deploy-gh-pages.yml",
    "content": "# Sample workflow for building and deploying a Jekyll site to GitHub Pages\nname: Deploy to GitHub Pages\n\non:\n  # Runs on pushes targeting the default branch\n  push:\n    branches: ['prod-deploy']\n    paths:\n      - '.vitepress/**'\n      - 'docs/**'\n      - index.md\n      - package.json\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - name: build\n        run: |\n          npm install\n          npm run docs:build\n          echo \"---------- vitepress build done ----------\"\n      - name: set ssh key # 临时设置 ssh key\n        run: |\n          mkdir -p ~/.ssh/\n          echo \"${{secrets.SSH_KEY_FOR_DEPLOY_GH_PAGES}}\" > ~/.ssh/id_rsa\n          chmod 600 ~/.ssh/id_rsa\n          ssh-keyscan \"github.com\" >> ~/.ssh/known_hosts\n          echo \"---------- set ssh-key ok ----------\"\n      - name: download and replace # 下载现有文件，替换\n        run: |\n          git clone git@github.com:mianshipai/mianshipai.github.io.git\n          echo \"---------- git clone ok ----------\"\n          cp -r ./.vitepress/dist/* ./mianshipai.github.io ## 用最新构建出来的文件，替换现有的\n          echo \"---------- replace ok ----------\"\n      - name: upload latest files # 上传文件\n        run: |\n          cd ./mianshipai.github.io\n          git config user.name \"github-actions\"\n          git config user.email \"github-actions@github.com\"\n          echo \"---------- begin git status ----------\"\n          echo `git status`\n          echo \"---------- end git status ----------\"\n          git add .\n          git commit -m \"update by github actions\"\n          echo \"---------- begin git push ----------\"\n          git push origin main\n          echo \"---------- end git push ----------\"\n      - name: delete ssh key # 删除 ssh key\n        run: rm -rf ~/.ssh/id_rsa\n"
  },
  {
    "path": ".gitignore",
    "content": "/coverage\n/src/client/shared.ts\n/src/node/shared.ts\n*.log\n*.tgz\n.DS_Store\n.idea\n.temp\n.vite_opt_cache\ndist\ncache\ntemp\nexamples-temp\nnode_modules\npnpm-global\nTODOs.md\n*.timestamp-*.mjs\n\n.vscode\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n.env\n.env.test\n\nISSUE.md\n\n.npmrc\n\n.cursor"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npm run format:fix\ngit add ."
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  printWidth: 120, // max 120 chars in line, code is easy to read\n  useTabs: false, // use spaces instead of tabs\n  tabWidth: 2, // \"visual width\" of of the \"tab\"\n  trailingComma: 'es5', // add trailing commas in objects, arrays, etc.\n  semi: false, // add ; when needed\n  singleQuote: true, // '' for stings instead of \"\"\n  bracketSpacing: true, // import { some } ... instead of import {some} ...\n  arrowParens: 'always', // braces even for single param in arrow functions (a) => { }\n  jsxSingleQuote: false, // \"\" for react props, like in html\n  bracketSameLine: false, // pretty JSX\n  endOfLine: 'lf', // 'lf' for linux, 'crlf' for windows, we need to use 'lf' for git\n}\n"
  },
  {
    "path": ".vitepress/config.mts",
    "content": "import { defineConfig } from 'vitepress'\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n  lang: 'zh-CN',\n  title: '前端面试派',\n  description: '系统专业的前端面试导航，大厂面试规范，开源免费',\n  head: [\n    ['link', { rel: 'icon', href: '/favicon.ico' }],\n    ['meta', { name: 'author', content: '双越老师' }],\n    [\n      'meta',\n      {\n        name: 'keywords',\n        content:\n          '前端, 面试, 前端面试, 面试题, 刷题, 面试流程, 前端面试流程, 面试准备, 简历, 前端简历, 开源, 免费, Javascript, Typescript, React, Vue, webpack, vite, HTTP, 算法',\n      },\n    ],\n    // baidu 统计\n    [\n      'script',\n      {},\n      `\n      var _hmt = _hmt || [];\n      (function() {\n        if (location.hostname.indexOf('mianshipai.com')<0) return;\n        var hm = document.createElement(\"script\");\n        hm.src = \"https://hm.baidu.com/hm.js?b53b3c926f6f6f5be6a9ac7e0911622b\";\n        var s = document.getElementsByTagName(\"script\")[0];\n        s.parentNode.insertBefore(hm, s);\n      })();\n      `,\n    ],\n    // 二维码\n    [\n      'script',\n      {},\n      `\n      setTimeout(function() {\n        // const container = document.getElementById('qrcode-container');\n        // container.innerHTML = '<img src=\"/docs/imgs/qr-code-img.jpg\" style=\"width: 200px; margin: 0 auto;\"/><span style=\"font-size:12px;\">如加群失败，加作者vx <code>fe-wfp</code>，备注 <code>面试派</code></span>';\n      }, 2000);\n      `,\n    ],\n    // top banner ad\n    [\n      'script',\n      {},\n      `\n      setTimeout(function() {\n        const header = document.querySelector('header');\n        if (header == null) return;\n        header.style.top = '25px';\n        const ad = document.createElement('div');\n        ad.style.backgroundColor = 'oklch(97.3% .071 103.193)';\n        ad.style.color = '#333';\n        ad.style.height = '25px';\n        ad.style.position = 'fixed';\n        ad.style.top = '0';\n        ad.style.left = '0';\n        ad.style.width = '100%';\n        ad.style.zIndex = '9999';\n        ad.style.lineHeight = '25px';\n        ad.style.fontSize = '13px';\n\n        const adContent = document.createElement('div');\n        // adContent.innerHTML = '前端学 Node 全栈和 AI 开发，可加入【划水AI】项目研发小组。双越老师开发，复杂项目，真实上线，持续维护升级。在此进入有优惠 &gt;&gt;';\n        adContent.innerHTML = '跟着 双越老师 开发一个 AI Agent 智能体项目，复杂业务 真实上线，适合前端人员学习和开发。点击查看优惠价格 &gt;&gt;';\n        adContent.style.width = '80%';\n        adContent.style.textAlign = 'center';\n        adContent.style.margin = '0 auto';\n        adContent.style.cursor = 'pointer';\n        adContent.addEventListener('click', function() {\n          // window.open('https://www.huashuiai.com/join?from=前端面试派', '_blank');\n          window.open('https://www.huashuiai.com/pub/ai-agent-camp', '_blank');\n        });\n\n        const adClose = document.createElement('div');\n        adClose.innerHTML = 'x';\n        adClose.style.width = '16px';\n        adClose.style.textAlign = 'center';\n        adClose.style.position = 'absolute';\n        adClose.style.right = '8px';\n        adClose.style.top = '0';\n        adClose.style.cursor = 'pointer';\n        adClose.addEventListener('click', function(event) {\n          event.stopPropagation();\n          ad.parentNode.removeChild(ad);\n          header.style.top = '0';\n        });\n\n        ad.appendChild(adContent);\n        ad.appendChild(adClose);\n        header.parentNode.insertBefore(ad, header);\n      }, 1000);\n      `,\n    ],\n  ],\n  themeConfig: {\n    // https://vitepress.dev/reference/default-theme-config\n    nav: [\n      // { text: '首页', link: '/' },\n      // { text: '正确写简历', link: '/docs/before-interview/write-resume.md' },\n      { text: '1v1 面试咨询 🔥', link: '/docs/services/1v1.md' },\n      { text: '加群讨论答疑', link: '/docs/services/group.md' },\n      {\n        text: '成为贡献者',\n        link: 'https://github.com/mianshipai/mianshipai-web#%E8%B4%A1%E7%8C%AE%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88',\n      },\n    ],\n\n    search: {\n      provider: 'local',\n    },\n\n    outline: {\n      level: [2, 3],\n      label: '目录',\n    },\n\n    sidebar: [\n      {\n        text: '面试准备',\n        items: [\n          {\n            text: '了解面试流程',\n            link: '/docs/before-interview/process.md',\n          },\n          { text: '分析 JD 招聘要求', link: '/docs/before-interview/jd.md' },\n          {\n            text: '正确写简历',\n            link: '/docs/before-interview/write-resume.md',\n          },\n          {\n            text: '如何投递简历',\n            link: '/docs/before-interview/post-resume.md',\n          },\n        ],\n      },\n      {\n        text: '笔试',\n        items: [\n          { text: '数据结构和算法', link: '/docs/written-exam/algorithm' },\n          { text: 'JS 手写代码', link: '/docs/written-exam/JS-writing' },\n          { text: 'JS 读代码', link: '/docs/written-exam/JS-reading' },\n        ],\n      },\n      {\n        text: '一面',\n        items: [\n          { text: '计算机基础', link: '/docs/first-exam/ComputerBase.md' },\n          { text: 'HTML 和 CSS', link: '/docs/first-exam/HTML-CSS.md' },\n          { text: 'JS 基础知识', link: '/docs/first-exam/JS' },\n          { text: 'TS 类型', link: '/docs/first-exam/TS' },\n          { text: 'HTTP 网络请求', link: '/docs/first-exam/HTTP.md' },\n        ],\n      },\n      {\n        text: '二面',\n        items: [\n          { text: 'Vue 使用', link: '/docs/second-exam/vue-usage.md' },\n          { text: 'Vue 原理', link: '/docs/second-exam/vue-inner.md' },\n          { text: 'React 使用', link: '/docs/second-exam/react-usage.md' },\n          { text: 'React 原理', link: '/docs/second-exam/react-inner.md' },\n          { text: '小程序', link: '/docs/second-exam/mini-program.md' },\n          { text: '前端工程化', link: '/docs/second-exam/engineering.md' },\n          { text: '鸿蒙应用开发', link: '/docs/second-exam/HarmonyOS-application-development.md' },\n          { text: 'Nodejs', link: '/docs/second-exam/nodejs.md' },\n        ],\n      },\n      {\n        text: '三面',\n        items: [\n          { text: '场景题', link: '/docs/third-exam/scene.md' },\n          { text: '交叉面试', link: '/docs/third-exam/cross-test.md' },\n          { text: '项目难点/成绩', link: '/docs/third-exam/project.md' },\n          // { text: '系统设计', link: '/docs/third-exam/system-design.md' },\n          { text: '前端 Leader 面试', link: '/docs/third-exam/leader-test.md' },\n          { text: '反问面试官', link: '/docs/third-exam/ask-in-reply.md' },\n        ],\n      },\n      {\n        text: 'HR 面',\n        items: [\n          { text: '行为面试', link: '/docs/hr-exam/behavioural-test.md' },\n          { text: '谈薪技巧', link: '/docs/hr-exam/salary.md' },\n        ],\n      },\n      {\n        text: '服务',\n        items: [\n          { text: '加群讨论答疑', link: '/docs/services/group.md' },\n          {\n            text: '成为贡献者',\n            link: 'https://github.com/mianshipai/mianshipai-web#%E8%B4%A1%E7%8C%AE%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88',\n          },\n          { text: '内推工作', link: '/docs/services/job.md' },\n          { text: '1v1 面试咨询 🔥', link: '/docs/services/1v1.md' },\n        ],\n      },\n      // {\n      //   text: 'Examples',\n      //   items: [\n      //     { text: 'Markdown Examples', link: '/docs/markdown-examples' },\n      //     { text: 'Runtime API Examples', link: '/docs/api-examples' },\n      //   ],\n      // },\n    ],\n\n    socialLinks: [{ icon: 'github', link: 'https://github.com/mianshipai/mianshipai-web' }],\n\n    footer: {\n      message:\n        '<a href=\"https://www.wangeditor.com/\" target=\"_blank\">wangEditor</a> | <a href=\"https://www.huashuiai.com/\" target=\"_blank\">划水AI</a> | <a href=\"https://github.com/mianshipai/mianshipai-web/issues\" target=\"_blank\">提交问题和建议</a>',\n      copyright: 'Copyright © 2025-present Mianshipai 面试派',\n    },\n  },\n  ignoreDeadLinks: ['./vue-inner/index'],\n})\n"
  },
  {
    "path": "README.md",
    "content": "# 面试派\n\n系统专业的前端面试导航，网址 https://www.mianshipai.com/\n\n- [双越老师](https://juejin.cn/user/1714893868765373) 主导开发\n- 大厂面试规范\n- 多人共建，开源免费\n\n## 贡献题目和答案\n\n欢迎提交 PR 来贡献题目和答案，同时你可以贴上自己的博客链接，我们相互导流，共同受益。\n\n具体步骤如下。\n\n### fork 这个项目\n\n在 GitHub fork 这个项目到你的空间，然后 git clone 下载这个项目到本地，本地运行项目\n\n```\nnpm i\nnpm run docs:dev\n```\n\n### 修改代码\n\n可以新增题目，也可以修改现有题目的答案（如你有更好的答案）\n\n每个题目的格式如下\n\n```md\n## 前端常见的设计模式有哪些？以及应用场景\n\n参考答案\n\n::: details\n\n（简明扼要的写出答案的核心内容，给出必要的解释、代码示例、截图）\n\n（注意，这里不要写的太过于详细，篇幅适中，适合在面试中 2-3 分钟解答完毕）\n\n（更详细的内容可写到你的博客里，贴到下面）\n\n（最后，可以参考 AI 给出的答案，但不要无脑照抄 AI ，要对自己产出的内容负责）\n\n:::\n\n参考资料 （如没有，就不写）\n\n::: details\n\n- 你的博客链接\n\n:::\n```\n\nPS. 如有图片，统一放在 `docs/imgs` 目录下。\n\n### 关于你的博客\n\n你的博客内容里一定要有到[面试派](https://www.mianshipai.com/)的导流文案，否则不予通过，我们是相互引流。\n\n博客内容一定要和该题目相关，要保证质量和内容，不能为了引流和引流。\n\n### 提交 PR\n\n把修改的代码 git commit 和 push 到你自己的 GitHub 项目\n\n然后提交 PR 到 [mianshipai-web](https://github.com/mianshipai/mianshipai-web) 的 `main` 分支，并添加 reviewer 审核人 `wangfupeng1988`\n\n管理员会每日查看 PR 并审核\n\n## 发布上线\n\n管理员会统一提交 PR ，把 `main` 合并到 `prod-deploy` 分支，合并后即可触发 [actions](https://github.com/mianshipai/mianshipai-web/actions) 并发布上线。\n"
  },
  {
    "path": "docs/api-examples.md",
    "content": "---\noutline: deep\n---\n\n# Runtime API Examples\n\nThis page demonstrates usage of some of the runtime APIs provided by VitePress.\n\nThe main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:\n\n```md\n<script setup>\nimport { useData } from 'vitepress'\n\nconst { theme, page, frontmatter } = useData()\n</script>\n\n## Results\n\n### Theme Data\n\n<pre>{{ theme }}</pre>\n\n### Page Data\n\n<pre>{{ page }}</pre>\n\n### Page Frontmatter\n\n<pre>{{ frontmatter }}</pre>\n```\n\n<script setup>\nimport { useData } from 'vitepress'\n\nconst { site, theme, page, frontmatter } = useData()\n</script>\n\n## Results\n\n### Theme Data\n\n<pre>{{ theme }}</pre>\n\n### Page Data\n\n<pre>{{ page }}</pre>\n\n### Page Frontmatter\n\n<pre>{{ frontmatter }}</pre>\n\n## More\n\nCheck out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).\n"
  },
  {
    "path": "docs/before-interview/jd.md",
    "content": "# 分析 JD 招聘要求\n\nJD, Job Description 职位描述，分析 JD 可以看到公司对候选人的要求和期望。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 通过 JD 能看到什么\n\n- 工作内容，负责什么产品和业务\n- 薪资范围（参考**最小值**即可，如 15k-20k 那就是 15k）和福利待遇\n- 技术栈，但不一定写的很清楚\n- 经验要求，例如要求多少年工作经验\n\n根据这些内容，review 一下自己的经验和能力，看是否要投递这个职位。\n\n## 案例分析\n\n![](../imgs/jd.png)\n\n从这个 JD 可以看出如下关键信息\n\n- 工资 10k 多一点，不算高\n- 上市公司，公司规模很大，有利于自我成长\n- 和微信有交互，面试之前需要多看看微信开发文档 —— 重要！\n- 有产品经理和 UI 设计师，说明团队、项目都比较正规\n- 有商业保险\n\n## 不要过于在意 JD\n\n- JD 是 HR 发布的，很多内容都是从网上抄来的\n- HR 不懂技术，写的可能会比较含糊\n- 要求会过于理想化，所以“面试造火箭，工作拧螺丝”\n\n所以，不要过于在意 JD ，如果你觉得这个机会比较适合自己，就大胆投递简历，博取一个机会。\n"
  },
  {
    "path": "docs/before-interview/post-resume.md",
    "content": "# 投递简历\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 投递方式\n\n- 各大招聘网站或 App ，如拉勾、BOSS 直聘、脉脉等\n- 各大公司的招聘网站，如搜索 `字节 招聘` 即可找到 https://jobs.bytedance.com/\n- 猎头推荐\n- 结识公司内部员工（同学，前同事，朋友等关系），内推\n- HR 主动联系你\n\n## 各大公司招聘网站\n\n- 字节 https://jobs.bytedance.com/\n- 滴滴 https://talent.didiglobal.com/\n- 百度 https://talent.baidu.com/\n- 美团 https://hr.meituan.com/\n- 快手 https://zhaopin.kuaishou.cn/\n- 腾讯\n  - 校园招聘 https://join.qq.com/\n  - 社会招聘 https://careers.tencent.com/\n- 华为 https://career.huawei.com/\n- 阿里 https://talent.alibaba.com/\n- 蚂蚁 https://talent.antgroup.com/home\n- 小红书 https://job.xiaohongshu.com/\n- 科大讯飞 https://campus.iflytek.com/\n- 小米 https://hr.xiaomi.com/\n- Oppo https://careers.oppo.com/\n- 京东 https://zhaopin.jd.com/\n- 网易\n  - 校园招聘 https://campus.163.com/\n  - 社会招聘 https://hr.163.com/\n- 拼多多 https://careers.pddglobalhr.net/\n- 携程 https://job.ctrip.com/\n- 贝壳 https://campus.ke.com/\n- 联想 https://talent.lenovo.com.cn/\n\n如有其他公司招聘网址，欢迎给我们[提交](https://github.com/mianshipai/mianshipai-web/issues/new/choose)，我们会及时收录。\n\n## 更多\n\n面试派 正在规划一个前端交流社群，会把猎头、内推等机会都汇总到这里，WIP...\n"
  },
  {
    "path": "docs/before-interview/process.md",
    "content": "# 了解面试流程\n\n没有工作经验、面试经验的同学，需要先整体了解一下程序员面试的常规流程。\n\n::: tip\n实际面试时，流程可能会有变动，灵活处理，可免费 [加群](/docs/services/group.md) 讨论咨询。\n\n也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 寻找工作机会\n\n- 各大招聘网站或 App ，如拉勾、BOSS 直聘、脉脉等\n- 各大公司的招聘网站，如搜索 `字节 招聘` 即可找到 https://jobs.bytedance.com/\n- 猎头推荐\n- 结识公司内部员工（同学，前同事，朋友等关系），内推\n- HR 主动联系你\n\n::: tip\n如何让猎头、HR 找到你呢？—— 主动暴露自己\n\n- 发表高质量博客\n- 做社区有用的开源项目\n- 注册脉脉、LinkedIn 等，增加社交关系（如有名校、大厂等 title）\n\n:::\n\n## 投递简历\n\n找到工作机会，主动投递简历给 HR 或者内推者。\n\n- 简历要写好，做好简历优化，珍惜投递机会\n- 简历使用 PDF 格式，不要用 word 文档！！！\n- 要以邮件形式投递，且是工作邮箱。如百度的 HR 工作邮箱一般是 `xxx@baidu.com`\n\n## 收到面试邀请\n\n如果你的简历被筛选通过，会收到 HR 的面试邀请。恭喜你，面试流程正式开始。\n\n准备好：纸、笔、打印简历、电脑。如果是现场面试，都带过去。\n\n## HR 初步沟通\n\n一般 HR 会先和你做初步沟通，远程电话或视频形式，大约 20-30 分钟，以确定你这个人是可以正常沟通的，没有极端的情绪和沟通障碍。\n\n你可以提前看看 [HR 行为面试](../hr-exam/behavioural-test.md) 的问题，在这一步有可能会问到其中的几个问题。\n\n## 一面\n\n技术面试，以考察基础知识为主。面试官一般是基层开发人员，你未来的同事。\n\n某些公司在一面开始之前，可能会让你做笔试题，考察算法、写代码、读代码等基础问题。\n\n::: tip\n如果让你写代码，你可以申请用自己电脑写，一般都会同意。\n:::\n\n## 二面\n\n技术面试，以考察框架和项目为主。面试官一般是团队的高级/资深工程师/项目负责人，你未来的导师。\n\n二面结束后，很多公司会安排交叉面试，也是技术面试。交叉，即请其他团队的高级/资深工程师，再考察一遍。\n\n## 三面\n\n团队 Leader 面试，不再关注技术细节，多考察项目组织和设计能力，挖掘技术潜力。\n\n如果你是应届生或工作经验较少，没太多项目设计能力，三面可能会很快结束，但不代表你会被拒绝。\n\n## HR 面\n\n所有技术面试都通过了以后，HR 会进行行为面试，然后谈薪资。但这一步不会立刻发 offer ，会有内部评审。\n\n## 发 offer\n\n如果 HR 面试没问题，且薪资谈妥，最后会正式发 offer 邮件，通知你哪天入职。\n\nPS. 入职前要和 HR 确认好，会签国家法定的劳动合同，会按照国家规定缴纳社保和公积金，最好能有商业医疗保险。这些在中大厂都是标配，但不是所有公司都有，自己确认好。\n"
  },
  {
    "path": "docs/before-interview/write-resume.md",
    "content": "# 如何正确写简历\n\n简历只有一个目的：引起 HR 或面试官的注意，让他们给你打电话。所以，如何让自己“亮”起来很重要。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 简历格式\n\n简历的模块和顺序\n\n- 个人信息\n- 核心能力总结（可选）\n- 教育经历\n- 专业技能\n- 工作经历\n- 项目经验\n\n简历模版：TODO...\n\n## 个人信息\n\n- 必备信息：姓名，电话，邮箱，个人网址或博客（如有），城市\n- 可选：头像照片，性别，年龄，工作年限\n- 不用写：婚姻状况，政治面貌，籍贯等和工作无关的信息\n- 不要写期望薪资，等和 HR 谈薪再说\n\n## 核心能力总结（可选）\n\n工作经验久的“老司机”，可以总结一下自己的核心能力，例如\n\n- 项目架构能力，负责过 xxx 大型项目\n- 团队领导能力，带领过 xxx 人研发团队\n- xxx 业务的解决方案能力\n\n注意，既然是总结，就不要贪多，写 3-4 条核心的即可，其他的在下文内容写。\n\n## 教育经历\n\n- 大学 + 研究生（如有）\n- 写明：学校 学历 专业 入学/毕业时间\n- 在学校的参赛获奖记录，写在这里\n- 计算机相关的培训和证书（如 PMP 软考等），写在这里\n- 如果学校或学历没有竞争力，如大专、非计算机相关专业，可以把教育经历写到最后\n- 如果是专升本，建议只写本科学历\n\n总之，是优势就尽早暴露，不是优势则尽量避免暴露。\n\n## 专业技能\n\n用 5 - 10 点来表达自己的专业技能，例如\n\n::: info\n\n- 熟悉常见的数据结构和算法，熟悉常见的设计模式\n- 熟悉前端基础 HTML CSS ES6+ JS TS 语法，熟悉 HTTP 协议\n- 熟悉 Vue2 Vue3 全家桶，熟悉 ElementPlus VantUI ，熟悉 Vue 原理和源码\n- 熟悉 React 和 Hooks 语法，熟悉 AntD ，熟悉 React 原理\n- 熟悉 Next.js 服务端框架，熟悉 RSC ，用过 Mongodb MySQL Redis\n- 熟悉微信小程序开发，熟悉 uni-app 和 taro 框架\n- 熟悉 Webpack 和 Vite 配置，熟悉常见插件，熟悉性能优化相关配置\n- 熟悉前端单元测试框架 Vitest 和 Jest ，熟悉 E2E 测试\n- 熟悉前端 CI/CD 配置，熟悉 Gitlab CI 和 GitHub actions 常见配置\n- 熟悉 Nodejs 服务端开发，熟悉 Express Koa Nestjs 框架\n\n:::\n\n我评审了 N 多简历，在专业技能这部分发现过很多问题：\n\n::: info\n\n- 很多同学完全不懂数据结构和算法\n  1. 删掉这一条不要暴露这个弱点\n  2. 业余需要恶补，除非一直呆在小公司\n- 多年的 Vue 工程师，没有其他技术栈\n- 很多工作多年的同学，不了解服务端，纯前端，无复杂项目\n- 不要写“了解”，了解的意思就是不懂。要么写“熟悉”，要么别写\n- 对 Vue React 的原理，仅知道几个概念，没有自己认真总结过\n\n其实每次写简历，也是对个人能力的自我评价，要看到自己的不足，未来继续进步。\n\n:::\n\n## 工作经历\n\n- 写明公司，角色，入职离职时间，工作职责/成绩\n- 如工作多年，角色要能看出成长。例如刚毕业是`前端工程师`，后来是`高级前端工程师`\n- 工作职责/成绩，不要写“正确的废话”，要写出自己**真实具体的事情**，例如\n  - 负责 xxx 项目，从 0 开发 a b c 功能\n  - 带领 xxx 团队，完成 x y z 能力建设\n\n## 项目经验\n\n- 根据个人的工作年限，写 2-5 个项目\n- 项目要涵盖专业技能里的技术栈\n- 把最重要的项目放在前面，不要非得按时间排序\n\n每个项目的格式如下：\n\n::: info\n\n项目描述：这是一个 xxx 项目，服务于 xxx 。它主要包含 a b c 功能。我主要负责 x y z 模块。\n\n技术栈：TS + Nextjs + React + ShadcnUI + Auth.js + Prisma + PostgreSQL + Tiptap\n\n项目职责：\n\n- 作为项目前端负责人，负责需求评审、技术选型、系统设计、代码走查、项目管理等工作\n- 基于 Tiptap 二次开发富文本编辑器，如悬浮菜单、slash command、表格、上传图片到 OSS\n- 集成 ChatGPT AI 功能到富文本编辑器，实现 AI 写作、AI 优化文本等功能，并保障 AI 接口稳定\n- 基于 Tiptap 开源的 Hocuspocus Server 二次开发协同编辑功能，实现 Tiptap 多人协同编辑\n- 使用 GitHub Actions 配置 CI/CD 流程，使用 Docker 部署测试环境，使用阿里云 Serverless 上线\n- 继续补充...\n\n:::\n\n## 其他常见问题\n\n### 简历是否只写一页？\n\n完全没必要，根据自己的真实情况写即可，我更推荐写 2-3 页。\n\n### 是否要写个人评价？\n\n大部分情况下没必要，我评审这么多简历，个人评价全都是虚的，什么热爱技术、热爱学习...\n\n你如何证明自己热爱技术、热爱学习？—— 大部分人都会被问住。\n\n如果你真的有证明的（博客 开源 看书等），你可以写到前面，这是个人优势。\n"
  },
  {
    "path": "docs/first-exam/ComputerBase.md",
    "content": "# 计算机基础\n\n计算机基础在校招中考核较为常见，尤其是大厂，不可忽视。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 进程和线程的区别\n\n参考答案\n\n::: details\n\n**进程**是操作系统中一个正在运行的程序，每个进程都有自己的地址空间、内存、文件描述符等资源。\n\n**线程**是进程中的一个执行单元，是 cpu 调度的最小单元，共享进程的资源，但有自己的独立执行流。\n\n二者的区别\n\n- 进程拥有独立的堆栈空间和数据段，需要分配独立的地址空间，开销大\n- 线程开销小，切换快，没进程安全，从通信机制上看，线程可以共享数据段\n\n举例\n\n- 浏览器中一个 tab 就是一个进程，进程中某个线程崩了，整个进程就会崩，但是这不会影响其他的进程，因此进程之间相对独立\n- Nginx 或 Nodejs PM2 开启的多个 instance 是进程，每个 instance 之间不能共享内存数据\n- JS 中 WebWorker 是一个线程，它可以和和主线程共享内存数据，独立运行不阻塞 UI\n\n:::\n\n## 进程间的通信\n\n参考答案\n\n::: details\n\n进程间通信（Inter-Process Communication，IPC）是指在操作系统中，不同进程之间交换信息和数据的过程，常见的进程通信方式包括：\n\n- 管道：用于单向或双向通信\n- 消息队列：允许进程通过信息传递进行通信\n- 共享内存：多个进程可以访问同一块内存区域\n- 信号量：用于进程间的同步\n- 套接字：用于网络通信\n\n线程间通信与进程间通信类似，但由于线程之间共享进程的资源，线程间通信通常比进程间通信更高效\n\n:::\n\n## 单核 CPU 如何实现并发\n\n参考答案\n\n::: details\n\n单核CPU 主要是通过时间片轮转和上下文切换来实现并发\n\n时间片轮转\n\n- CPU将时间划分为很小的时间片，通常是几十毫秒\n- 每个进程、线程分配到一个时间片\n- CPU轮流执行每个进程、线程的时间片\n- 当一个时间片用完，CPU就会切换到下一个进程、线程\n\n上下文切换\n\n- 在切换进程、线程时，CPU需要保存当前进程的状态（上下文），包括\n  - 程序计数器的值\n  - 寄存器的值\n  - 内存映射信息\n- 加载下一个要执行的进程、线程的上下文\n\n> 在任意时刻，CPU只能执行一个任务，由于切换速度非常快，给用户的感觉就像是在同时运行多个程序，所以这种机制被称为“伪并发”，若线程过多也不好，频繁的上下文切换会带来一定的性能开销，所以过多的线程反而会带来性能下降的问题\n\n:::\n\n## CPU 调度算法有哪些？\n\n参考答案\n\n::: details\n\n- 先来先服务（First Come First Serve）\n  - 最简单的调度算法\n  - 进程按到达的顺序排队，先到达的先执行\n  - 缺点：可能导致长时间的等待，特别是当一个长进程在队列前面时\n- 短作业优先（Shortest Job First）\n  - 优先执行预计运行时间最短的进程\n  - 可以是非抢占式或是抢占式（shortest Remaining Time First，SRTF）\n  - 可能导致“饥饿”现象，即长作业可能永远得不到执行\n- 优先级调度（Priority Scheduling）\n  - 每个进程分配一个优先级，优先级高的进程先执行\n  - 也可能导致“饥饿”现象，通常使用老化（aging）技术来解决\n- 轮转调度（Round Robin）\n  - 每个进程分配一个固定的时间片，时间片用完后，进程被放到队列的末尾\n  - 适用于时间共享系统\n  - 时间片的大小对系统性能有很大影响\n- 多级队列调度（Multilevel Queue Scheduling）\n  - 将进程分成多个队列，每个队列有不同的优先级\n  - 不同队列可以使用不同的调度算法\n- 多级反馈队列调度（Multilevel Feedback Queue）\n  - 允许进程在不同的队列之间移动\n  - 根据进程的行为动态调整其优先级\n\n:::\n\n## linux 如何查找你的进程占用的那个端口\n\n参考答案\n\n::: details\n\n使用 netstat 命令\n\n```bash\n# 查看所有端口占用情况\nnetstat -tunlp\n# 查看特定端口，比如 8080\nnetstat -tunlp | grep 8080\n```\n\n- t - tcp 协议；u - udp 协议；n - 显示端口号；l - 仅显示监听端口；p - 显示进程信息\n\n使用 lsof 命令\n\n```bash\n# 查看特定端口，比如 8080\nlsof -i :8080\n```\n\n- i - 显示网络连接\n\n:::\n\n## 单核服务器连接数超载了怎么办\n\n参考答案\n\n::: details\n\n- 优化代码和查询\n\n  确保应用程序代码和数据库查询是高效的，以减少每个连接的资源消耗\n\n- 使用负载均衡\n\n  将流量分配到多个服务器上，以分散负载\n\n- 增加连接池\n\n  使用连接池来管理数据库连接，减少连接的创建和销毁开销\n\n- 限制连接数\n\n  配置服务器以限制每个客户端的最大连接数，以防止单个客户端占用过多资源\n\n- 使用缓存\n\n  利用缓存机制（如 Redis，Memcached）来减少对数据库的访问次数\n\n:::\n\n## 请简述一个编译器的执行过程。前端有哪些常见的编译工具？\n\n参考答案\n\n::: details\n\n编译器的执行过程\n\n1. 词法分析\n   - 将源代码转换为一系列的标记（tokens），这些标记是编程语言的基本语法单位\n2. 语法分析\n   - 根据语言的语法规则，将标记序列转换为语法书（parse tree），也称为抽象语法树（AST）\n3. 语义分析\n   - 检查语法树是否符合语言的语义规则，例如类型检查，作用域检查等\n4. 中间代码生成\n   - 将语法树转换为中间代码，这种代码通常独立于机器\n5. 代码优化\n   - 对中间代码进行优化，以提高程序的执行效率\n6. 目标代码生成\n   - 将中间代码转换为目标机器代码\n7. 代码生成后优化 - 对生成的目标代码进行进一步优化\n   > 编译器的目的是将我们编写的源码转换为机器码（目标代码），以便计算机能够读懂执行\n\n前端常见的编译工具\n\n- Babel\n  - 用于将现代 JavaScript 代码转换为向后兼容的版本\n- TypeScript Compiler\n  - 将 TypeScript 代码转换为 JavaScript\n- Sass/SCSS\n  - 将 Sass/SCSS 代码转换为 CSS\n- Webpack\n  - 用于打包 JavaScript 模块，并支持多种编译和转换插件\n\n:::\n\n## 什么是编译型语言和解释型语言，他们有什么区别？\n\n参考答案\n\n::: details\n\n高级编程语言分为解释型语言和编译型语言\n\n编译型语言\n\n- 常见编译型语言：C，C++，Java，Go，Rust\n- 执行方式：编译型语言的代码在运行期间由编译器一次性翻译成机器码，生成的机器码可以在目标机器上运行\n- 优点：代码在运行前就已经被翻译成机器码，运行速度通常更快\n- 缺点：需要编译步骤，开发和调试过程可能较慢\n\n解释型语言\n\n- 常见解释性语言：JavaScript，Python，Ruby，PHP\n- 执行方式：解释型语言的代码在运行期间由解释器逐行翻译成机器码并执行。这意味着每次运行程序时，代码都需要被重新解释\n- 优点：由于不需要编译成机器码，开发和调试过程通常更快，更灵活\n- 缺点：运行速度通常比编译型语言慢，因为每次执行都需要进行翻译\n\n编译型语言和解释型语言的区别\n\n- 执行速度：编译型语言通常比解释型语言快，因为他们直接运行机器码\n- 开发灵活性：解释型语言通常更灵活，适合快速开发和迭代\n- 错误检测：编译型语言在编译阶就可以捕获更多的语法和类型错误，而解释型语言通常在运行时才发现错误\n\n> 举个🌰：读一门外文著作，编译型语言就是给你将这本著作翻译成中文，然后你就可以直接看中文了。解释型语言就是给你一个翻译，一边看一边翻译，下次看仍需要翻译\n\nJIT（Just-In-Time）编译\n\n为了结合编译型和解释型语言的优点，JIT 随之诞生，可以理解为“即时编译”\n\n- 执行方式：JIT 编译在程序运行时将部分代码编译成机器码，而不是逐行解释，这种编译方式在代码即将被执行时进行，因此得名“即时编译”\n- 应用场景：现代 JavaScript 引擎（如 V8 引擎）通常使用 JIT 编译来提高性能，包括 Java 虚拟机（JVM）也会使用 JIT 编译来提高性能\n\n:::\n\n## 简述 JS 垃圾回收的过程。用什么算法？\n\n参考答案\n\n::: details\n\n垃圾回收（Garbage Collection, GC）是自动管理内存的过程。JavaScript 引擎会自动检测不再使用的对象，并释放它们所占用的内存。常用的垃圾回收算法是标记-清除（Mark-and-Sweep）算法。\n\n垃圾回收过程\n\n1. 标记阶段\n\n- 垃圾回收器会从根对象（如全局对象、局部变量）开始，遍历所有对象，标记所有可达的对象\n- 可达对象：从根对象可以通过引用链访问到的对象\n\n2. 清除阶段\n\n- 标记阶段结束后，所有未被标记的对象被视为不可达对象\n- 垃圾回收器会清除这些不可达对象，释放它们所占用的内存\n\n常用算法\n\n- 标记-清除\n  - 最常用的垃圾回收算法\n- 引用计数\n  - 每个对象有一个引用计数器，当对象被引用时，计数器加1，当对象不再被引用时，计数器减1，当引用计数为 0 时，表示该对象不再被使用，可以被回收。无法解决循环引用的问题，如今已很少使用\n- 分代回收\n  - 现代 JavaScript 引擎通常使用分代回收策略，将内存分为新生代和老生代。新生代存储生命周期短的对象，老生代存储生命周期长的对象。不同代的对象使用不同的回收策略，以提高效率。\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7432127587918282792\n\n:::\n\n## 什么是内存泄漏？如何排查？JS 内存泄漏的常见原因？\n\n参考答案\n\n::: details\n\n内存泄露是指在程序运行过程中，程序未能释放不再使用的内存空间，导致内存资源被浪费。\n\n排查内存泄露\n\n1. 使用内存分析工具\n   - 浏览器开发者工具：Chrome 的 DevTools 提供了内存分析工具 Memory，可以监控内存使用情况\n   - 也可以结合 setInterval 使用 console.memory 查看内存使用的快照\n2. 代码审查\n   - 检查代码中是否有未释放的事件监听器，定时器，全局变量，确保不再需要某对象时，及时解除引用\n3. 性能监控\n   - 监控应用程序的内存使用情况，观察是否有持续增长的趋势\n   - 使用日志记录内存使用情况，帮助识别内存泄露的模式\n\nJS 内存泄露的常见原因\n\n1. 意外的全局变量\n   - 忘记使用 var，let，const 声明变量时，变量会被挂载到全局对象上\n2. 闭包\n   - 闭包中引用了不再需要的外部变量，导致这些变量无法被垃圾回收\n3. 未清理的 DOM 引用\n   - 删除 DOM 元素时，未能清理相关的事件监听器或引用\n4. 定时器和回调\n   - 未能清理不再需要的 setInterval 或 setTimeout 回调\n\n:::\n\n## 简述 JS 运行时的堆栈内存模型？\n\n参考答案\n\n::: details\n\n在 JS 运行时，内存管理主要依赖于堆（Heap）和栈（Stack）两种数据结构\n\n栈（Stack）\n\n- 特点：栈是一种后进先出（LIFO）的数据结构，用于存储函数调用和原始数据类型\n- 用途：当函数被调用时，相关的执行上下文（包括局部变量，函数参数）会被压入栈中，当函数执行完毕后，栈顶的执行上下文会被弹出栈\n- 限制：栈内存的空间大小通常十分有限，适合存储生命周期短，大小固定的数据（比如无限递归不断创建栈帧，会导致爆栈）\n\n堆（Heap）\n\n- 特点：堆是一种动态内存分配的，无序的数据结构，用于存储对象和复杂数据类型\n- 用途：堆用于存储动态分配的内存，比如对象，数组，函数等\n- 限制：堆内存的分配和释放速度较慢，容易导致内存碎片化\n\n这里用一段 js 代码画图举例\n\n```js\nlet a = 1\nlet obj = {\n  name: 'Heap',\n}\n\nfunction foo() {\n  let b = 2\n  console.log(b)\n  function bar() {\n    let c = 3\n    console.trace()\n    console.log(c)\n  }\n  bar()\n}\n\nfoo()\n```\n\n![](../imgs/call-stack.png)\n\n栈\n\n- 调用栈用于跟踪程序执行的活动函数。每当一个函数被调用时，它的执行上下文会被推入调用栈。当函数执行完毕后，它的执行上下文会从调用栈中弹出。\n- 在这个例子中，最初的调用栈会包含一个匿名的全局执行上下文（anonymous），然后是 foo 函数的执行上下文，最后是 bar 函数的执行上下文。\n- 当 bar 函数执行完毕后，它的执行上下文会被弹出调用栈，然后 foo 函数的执行上下文也会被弹出调用栈，最后全局执行上下文也会被弹出调用栈。\n\n堆\n\n- 全局存储的 obj 对象，在栈中仅仅是一个引用地址，真正的对象存储在堆中。\n\n:::\n\n## 冯·诺依曼架构是什么？\n\n参考答案\n\n::: details\n\n冯·诺依曼架构确定了现代计算机结构中的五大部件：\n\n1. 输入设备: 键盘，鼠标，摄像头等\n2. 输出设备: 显示器，打印机，扬声器等\n3. 存储器: 计算机的记忆装置，主要存放数据和程序。分为内部存储器（内存/主存储器）和外部存储器（硬盘，光盘，U盘等）\n   - 内存：也称之为主存储器，又分为随机存储器（RAM）和只读存储器（ROM）\n     - RAM 存放的是计算机在通电运行的过程中即时的数据，计算机的内存容量就是指的 RAM 的容量。RAM 可读，可写，断电会数据丢失\n     - ROM 存放的是每次计算机开机都需要处理的，固定不变的程序和数据，比如 BIOS 程序。ROM 可读，不可写，断电不会丢失\n   - 外存：外存是硬盘，是计算机的辅助存储器，可以长期保存数据，断电不会丢失。当计算机需要从外存读取数据时，需要将数据从外存读取到内存中，然后才能下一步处理。根据介质不同，外存可分为软盘，硬盘，光盘。硬盘最为常见，硬盘又分为机械硬盘（HD）和固态硬盘（SSD）。\n4. 运算器: 算术逻辑单元（ALU），负责执行算术和逻辑运算\n5. 控制器: 控制整个计算机系统的工作流程，包括指令的执行顺序，数据传输等，运算器和控制器通常集成在一起，就是我们熟知的 CPU\n\n![](../imgs/von-neumann.png)\n\n> 图中的箭头流向是数据流向。数据从输入设备进入到计算机，存放在存储器中，控制器负责控制运算器对存储器中的数据进行运算，运算的结果再次放入存储器中，通过控制器将存储器中的计算结果输出到输出设备。\n\n:::\n\n## 计算机内部为何使用二进制？\n\n参考答案\n\n::: details\n\n- 硬件实现简单：二进制只需要两个状态，通常用电压的高低来表示（如高电压表示 1，低电压表示 0）。这种简单的状态切换使得硬件电路设计更为简单和可靠\n- 抗干扰能力强：在电路中，二进制的两个状态（0 和 1）可以通过明显的电压差来区分，这使得系统对噪声和干扰的容忍度更高，数据传输更稳定\n- 逻辑运算方便：计算机的基本运算是逻辑运算，二进制系统与布尔代数非常契合，能够进行与，或，非等逻辑运算，简化了计算机的设计和操作\n- 存储和处理效率高：二进制数据在计算机中可以直接存储和处理，避免了其他进制系统转换带来的复杂性和效率损失\n- 历史和标准化：从计算机发展的早期，二进制就被广泛采用，形成了标准化的设计和技术积累，进一步推动了其普及和应用\n\n:::\n\n## 二进制如何表示负数和小数？\n\n参考答案\n\n::: details\n\n表示负数\n下面用 8 位二进制数 -2 举例\n\n1. 求补码\n\n```js\n  - 2 的原码：0000 0010 // 原码就是最简单的表示方法\n  - 2 的反码：1111 1101 // 反码是原码的反向\n  - 2 的补码：1111 1110 // 补码是反码加 1\n```\n\n1111 1110 就是 -2 的二进制\n\n2. 模减\n\n```js\n  - 模：2 ^ 8 = 256\n  - 256 - 2 = 254\n```\n\n254 的二进制就是 -2 的二进制 1111 1110\n\n表示小数\n下面用 10.625 转换为二进制举例\n\n1. 整数部分\n\n- 将整数部分除以2，记录余数，直到商为0。余数的逆序即为二进制表示。\n\n```js\n  - 10 / 2 = 5 余 0\n  - 5 / 2 = 2 余 1\n  - 2 / 2 = 1 余 0\n  - 1 / 2 = 0 余 1\n```\n\n逆序排列：1010\n\n2. 小数部分\n\n- 将小数部分乘以2，记录整数部分，直到小数部分为0或达到所需的精度。整数部分的顺序即为二进制表示。\n\n```js\n  - 0.625 * 2 = 1.25 取整 1\n  - 0.25 * 2 = 0.5 取整 0\n  - 0.5 * 2 = 1 取整 1\n```\n\n顺序排列：0.101\n\n10.625 的二进制就是 1010.101\n\n:::\n\n## 什么是虚拟内存，为何要使用虚拟内存？\n\n参考答案\n\n::: details\n\n什么是虚拟内存？\n\n虚拟内存相对物理内存，它是一种计算机内存管理技术，它为每个进程提供了一个连续的地址空间，使得进程可以认为自己拥有一个完整的，连续的内存空间，而实际上，这个空间可能是分散的，并且部分可以存储在外存中\n\n为什么使用虚拟内存？\n\n1. 扩展内存容量：虚拟内存允许计算机使用硬盘空间来扩展物理内存的容量，即使物理内存不足，程序也可以运行，因为不常用的数据可以被交换到磁盘上\n2. 内存保护：每个进程都有自己的虚拟地址空间，防止一个进程访问另一个进程的内存，提供了内存保护\n3. 简化内存管理：程序员不需要担心物理内存的分配和管理，操作系统负责将虚拟地址映射到物理地址\n4. 多任务处理：通过虚拟内存，多个进程可以同时运行，操作系统可以在它们之间切换，提供多任务处理能力\n5. 程序加载和执行的灵活性：程序可以被分成多个部分，只有需要的部分才会被加载到内存中，减少了内存的使用\n\n:::\n\n## 什么是 Unicode 编码？它和常见的 UTF-8 有什么关系？\n\n参考答案\n\n::: details\n\n- Unicode 俗称万国码，它为每个字符提供了一个唯一的数字标识，这个数字标识被称为码点。Unicode 的出现就是为了解决 ASCII 编码的局限性，ASCII 编码只能表示 128 个字符，全球各个国家的字符远不止 128 个，所以 Unicode 应运而生。\n\n- Unicode 的定义了一个字符集和一系列编码方案（UTF-8，UTF-16，UTF-32），UTF-8 是 Unicode 最常用的编码方案，它是一种变长编码，根据不同的字符，使用不同的字节数来表示。对于 ASCII 字符，使用 1 个字节表示，与 ASCII 编码兼容\n\n- GBK 编码是一种用于中文字符的编码标准，扩展了 GB2312 编码，支持简体和繁体中文字符。GBK 编码使用 2 个字节表示一个中文字符，适合中文环境，但不支持 Unicode\n\n:::\n\n## 简述计算机网络的 OSI 模型\n\n参考答案\n\n::: details\n\nOSI（开放系统互联）模型是一个用于理解和实现网络协议的七层概念框架。每一层都有特定的功能，并与其直接上下的层进行通信。\n\n1. 物理层(Physical Layer)：这是OSI模型的最低层，负责设备之间的物理连接，包括通过物理介质传输原始比特流。它涉及硬件组件，如电缆、交换机和网络接口卡。\n\n2. 数据链路层(Data Link Layer)：负责节点到节点的数据传输以及错误检测和纠正，确保数据在物理链路上的可靠传输。它分为两个子层：媒体访问控制（MAC）层和逻辑链路控制（LLC）层。\n\n3. 网络层(Network Layer)：负责数据的路由、转发和寻址，确定数据到达目的地的最佳物理路径。像IP（互联网协议）这样的协议在这一层运行。\n\n4. 传输层(Transport Layer)：为应用程序提供端到端的通信服务，负责错误恢复、流量控制和确保完整的数据传输。像TCP（传输控制协议）和UDP（用户数据报协议）这样的协议在这一层运行。\n\n5. 会话层(Session Layer)：管理应用程序之间的会话，建立、维护和终止应用程序之间的连接，负责会话的检查点和恢复。\n\n6. 表示层(Presentation Layer)：负责数据的翻译、加密和压缩，确保数据以可用的格式呈现给应用层，充当网络和应用之间的翻译器。\n\n7. 应用层(Application Layer)：这是OSI模型的最高层，直接为终端用户应用程序提供网络服务，负责电子邮件、文件传输和网页浏览等网络服务。像HTTP、FTP和SMTP这样的协议在这一层运行。\n\n![OSI 模型](../imgs/OSI.png)\n\n:::\n\n## 一个域名对应一个 ip 吗\n\n参考答案\n\n::: details\n\n一个域名不一定只对应一个 IP 地址，具体情况如下：\n\n- 单个域名对应单个 IP 地址：这是最简单的情况，一个域名解析到一个固定的 IP 地址。\n- 单个域名对应多个 IP 地址：这种情况通常用于负载均衡和高可用性。通过 DNS 轮询（Round Robin DNS），一个域名可以解析到多个 IP 地址，用户的请求会被分配到不同的服务器上。\n- 多个域名对应单个 IP 地址：多个域名可以指向同一个 IP 地址，这在虚拟主机中很常见。通过服务器配置，服务器可以根据请求的域名来提供不同的内容。\n- CDN（内容分发网络）：CDN 服务会根据用户的地理位置将域名解析到不同的 IP 地址，以提高访问速度和可靠性。\n\n因此，域名和 IP 地址之间的关系可以是多对多的，具体取决于网络架构和配置。\n\n:::\n\n## UDP 和 TCP 协议的区别？有什么应用场景\n\n参考答案\n\n::: details\n\nUDP（User Datagram Protocol，用户数据报协议）\n\n- 无连接协议：UDP 是一种无连接协议，发送数据前不需要建立连接。\n- 不提供错误恢复：UDP 不提供错误恢复机制，如果数据包丢失，不会进行重传。\n- 传输速度快：由于没有错误检查和连接建立的过程，UDP 传输速度更快，延迟更低。\n- 应用场景：适用于对速度要求高且允许偶尔数据丢失的应用，如直播流媒体、在线游戏和语音通话（VoIP）。\n\nTCP（Transmission Control Protocol，传输控制协议）\n\n- 面向连接协议：TCP 在发送数据前需要建立连接，确保通信的可靠性。\n- 提供错误恢复：TCP 提供错误检查，确保数据按顺序且无误地传输。\n- 传输速度较慢：由于需要建立连接和进行错误检查，TCP 的传输速度相对较慢。\n- 应用场景：适用于对数据完整性和顺序有严格要求的应用，如网页浏览、电子邮件和文件传输。\n\n:::\n\n## 数组和链表有什么区别？从内存结构上来说\n\n参考答案\n\n::: details\n\n- 数组：\n  - 连续内存分配：数组在内存中是连续分配的，这意味着数组的所有元素在内存中是紧挨着的。\n  - 随机访问：由于数组的连续性，可以通过索引直接访问任意元素，访问速度快，时间复杂度为 O(1)。\n  - 固定大小：数组的大小在创建时就确定了，不能动态调整。\n- 链表：\n  - 非连续内存分配：链表的每个元素（称为节点）在内存中可以是分散的，每个节点通过指针指向下一个节点。\n  - 顺序访问：访问链表中的元素需要从头节点开始，逐个遍历，访问速度较慢，时间复杂度为 O(n)。\n  - 动态大小：链表可以动态调整大小，方便插入和删除操作。\n\n:::\n"
  },
  {
    "path": "docs/first-exam/HTML-CSS.md",
    "content": "# HTML 和 CSS\n\n随着 Vue React 等框架，和各种 CSS UI 组件库的普及，HTML 和 CSS 很容易被忽略。\n\n如果你是实习生、应届生或刚毕业不久，HTML 和 CSS 知识一定要认真准备，大厂必考。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## DOCTYPE 是什么，都有哪些属性？\n\n参考答案\n\n::: details\n\nHTML 的 `<!DOCTYPE>` 声明是文档类型声明，用于告知浏览器当前 HTML 文档使用的 HTML 版本，从而确保文档以正确的模式渲染。它通常出现在 HTML 文档的第一行。\n\n在现代开发中，推荐使用 HTML5 的简单声明\n\n```html\n<!DOCTYPE html>\n```\n\n在之前的 HTML 版本中，如 HTML4 ，会有其他写法，不过现在已经不常用。\n\n```html\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n```\n\n:::\n\n## meta 标签是干什么的，都有什么属性和作用\n\n参考答案\n\n::: details\n\nHTML 中的 `<meta>` 标签用于提供页面的**元信息**，这些信息不会直接显示在网页内容中，但对浏览器、搜索引擎和其他服务非常重要。\n\n常见的 meta 信息如下：\n\n1. 字符编码。指定网页的字符编码，确保正确显示内容。\n\n```html\n<meta charset=\"UTF-8\" />\n```\n\n2. 页面视口设置（响应式设计）。控制页面在移动设备上的显示和缩放行为。\n\n```html\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n```\n\n- `width=device-width`：页面宽度匹配设备屏幕宽度\n- `initial-scale=1.0`：初始缩放比例为 1\n\n3. 搜索引擎优化（SEO）。提供描述性信息，便于搜索引擎索引。\n\n```html\n<meta\n  name=\"keywords\"\n  content=\"前端, 面试, 前端面试, 面试题, 刷题, 面试流程, 前端面试流程, 面试准备, 简历, 前端简历, Javascript, Typescript, React, Vue, webpack, vite, HTTP, 算法\"\n/>\n<meta name=\"description\" content=\"面试派，双越老师整理的前端面试真实流程，大厂面试规范，开源免费\" />\n<meta name=\"robots\" content=\"index, follow\" />\n```\n\n4. 作者信息。提供网页作者信息。\n\n```html\n<meta name=\"author\" content=\"双越老师\" />\n```\n\n:::\n\n## 什么是 DOM ，它和 HTML 有什么区别？\n\n参考答案\n\n::: details\n\nDOM 即 Document Object Model 文档对象模型，它是一个 JS 对象。而 HTML 是一种标记语言（和 XML 类似）用于定义网页的内容和结构。\n\nDOM 的特点\n\n- 树形结构，DOM 树\n- 可编程，可以使用 Javascript 读取和修改 DOM 数据\n- 动态性，通过 DOM API 动态修改结构和数据\n\nHTML 到 DOM 的过程\n\n- HTML 解析：浏览器解析 HTML 代码，生成 DOM 树。\n- CSSOM 生成：解析 CSS，生成 CSSOM（CSS 对象模型）。\n- 渲染树：结合 DOM 和 CSSOM，生成渲染树。\n- 页面渲染：根据渲染树将内容显示在页面上。\n\n:::\n\n## 如何理解 HTML5 语义化 ？有哪些常见的语义化标签？\n\n理解 HTML5 语义化\n\n::: details\n\nHTML5 语义化是指通过使用具有明确含义的标签，使网页的结构和内容更加清晰，方便浏览器、开发者以及搜索引擎理解网页内容。\n\n语义化的核心在于让标签不仅描述外观，还能表达内容的含义，从而提升网页的可读性、可维护性和可访问性。\n\n- 提高代码可读性：开发者无需额外注释即可理解代码结构。\n- 增强 SEO（搜索引擎优化）：搜索引擎能更好地抓取和理解网页内容。\n- 提升可访问性：辅助技术（如屏幕阅读器）可以更准确地解释页面内容。\n- 支持更好的浏览器兼容性：现代浏览器能够更高效地渲染语义化结构。\n\n:::\n\n常见的 HTML5 语义化标签\n\n::: details\n\n- `<header>` 注意：要区别于 `<head>`\n- `<nav>`\n- `<main>`\n- `<article>`\n- `<section>`\n- `<aside>`\n- `<footer>`\n- `<figure>`\n- `<figcaption>`\n- `<mark>`\n- `<time>`\n- `<summary>`\n- `<details>`\n\n:::\n\n写一个 HTML5 语义化的例子\n\n::: details\n\n```html\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>HTML5 语义化示例</title>\n  </head>\n  <body>\n    <header>\n      <h1>面试派</h1>\n      <nav>\n        <ul>\n          <li><a href=\"#home\">首页</a></li>\n          <li><a href=\"#about\">关于</a></li>\n          <li><a href=\"#contact\">联系</a></li>\n        </ul>\n      </nav>\n    </header>\n\n    <main>\n      <section id=\"home\">\n        <h2>欢迎访问</h2>\n        <p>这是一个 HTML5 语义化的示例。</p>\n      </section>\n      <section id=\"about\">\n        <h2>关于我们</h2>\n        <article>\n          <h3>我们的历史</h3>\n          <p>这是关于我们历史的介绍。</p>\n        </article>\n      </section>\n    </main>\n\n    <aside>\n      <h3>相关文章</h3>\n      <ul>\n        <li><a href=\"#\">文章 1</a></li>\n        <li><a href=\"#\">文章 2</a></li>\n      </ul>\n    </aside>\n\n    <footer>\n      <p>&copy; 2025 &copy; 面试派</p>\n    </footer>\n  </body>\n</html>\n```\n\n:::\n\n## DOM 节点的 attr 和 property 有何区别\n\n- attr 指的是 HTML 属性（attribute）\n- property 指的是 DOM 对象的属性（property）\n\n主要区别\n\n::: details\n\n定义不同\n\n- attr 定义在 HTML 元素上的初始属性，存储在 DOM 元素的属性列表中，与 HTML 源代码一一对应。\n- property 是 DOM 对象的属性，是通过浏览器解析 HTML 并生成 DOM 对象时动态创建的，供 JavaScript 操作。\n\n存储位置不同\n\n- attr 是 HTML 的一部分，存储在元素的 HTML 标记 中。\n- property 是 DOM 的一部分，存储在 JavaScript 对象中。\n\n行为不同\n\n- attr 一般是静态的，表示元素初始的值，即从 HTML 源代码中解析的值，通常不会因用户操作或脚本修改而自动更新。除非你手动使用 JS 修改值。\n- property 一般是动态的，表示当前状态，可以通过 JavaScript 修改，并反映在 DOM 中。\n\n对于一些常用的属性（如 id、value、checked 等），attr 和 property 会部分同步：\n\n- 修改 attr 会影响 property 值。\n- 而修改 property 可能不会同步回 attr。\n\n总结，一般来说，attr 用于设置元素的初始状态，而 property 用于操作和获取当前状态。\n\n:::\n\n## 如何一次性插入多个 DOM 节点？考虑性能\n\n参考答案\n\n::: details\n\n直接多次操作 DOM（如多次 `appendChild` 或 `innerHTML` 更新）会导致性能问题，因为每次操作都会触发 DOM 的重新渲染。\n\n`DocumentFragment` 是一个轻量级的文档片段，可以在内存中操作节点，最后一次性插入到 DOM 中，从而减少重绘和回流。\n\n```js\n// 获取目标容器\nconst container = document.getElementById('list')\n\n// 创建 DocumentFragment\nconst fragment = document.createDocumentFragment()\n\n// 创建多个节点并添加到 fragment 中\nfor (let i = 1; i <= 1000; i++) {\n  const li = document.createElement('li')\n  li.textContent = `item ${i}`\n  fragment.appendChild(li)\n}\n\n// 一次性插入到 DOM\ncontainer.appendChild(fragment)\n```\n\n:::\n\n## offsetHeight scrollHeight clientHeight 有什么区别\n\n参考答案\n\n::: details\n\n`offsetHeight` 元素的总高度，包括内容高度、内边距（padding）、水平滚动条高度（如果存在）、以及边框（border）。不包括外边距（margin）。\n\n`scrollHeight` 元素的实际内容高度，包括不可见的溢出部分（scrollable content），大于等于 `clientHeight`。\n\n`clientHeight` 元素的可见内容高度，包括内容高度和内边距（padding），但不包括水平滚动条高度、边框（border）和外边距（margin）。\n\n:::\n\n## HTMLCollection 和 NodeList 的区别\n\n在操作 DOM 时，`HTMLCollection` 和 `NodeList` 都是用来表示节点集合的对象，它们的区别是：\n\n::: details\n\n`HTMLCollection` 只包含 **HTML 元素**节点。通过 `document.getElementsByTagName` 或 `document.getElementsByClassName` 返回的结果是 `HTMLCollection。`\n\n`NodeList` 包括 **元素节点、文本节点、注释节点** 等，不仅仅是 **HTML 元素**节点\n\n- 通过 `document.querySelectorAll` 返回的是 静态 `NodeList`\n- 通过 `childNodes` 返回的是 动态 `NodeList`\n\n当文档结构发生变化时\n\n- `HTMLCollection` 和 动态 `NodeList` 会随着 DOM 的变化自动更新\n- 静态 `NodeList` **不会**随着 DOM 的变化自动更新\n\n:::\n\n## Node 和 Element 有什么区别？\n\n在 DOM（文档对象模型）中，HTML Element 和 Node 都是表示文档结构中的对象，但它们有不同的定义和用途。\n\n::: details\n\nNode 是 DOM 树中所有类型对象的基类，是一个接口，表示文档树中的一个节点。它有多个子类型，Element 是其中的一个。其他的还有 Text、Comment 等。\n\nNode 常见属性如 `nodeName` `nodeValue`\n\nHTML Element 是 Node 的子类，专门表示 HTML 元素节点。它提供了与 HTML 元素相关的更多功能，如属性、样式等。HTML Element 仅表示 HTML 元素节点，通常对应 HTML 标签，如 `<div>`, `<p>`, `<a>` 等。\n\nElement 常见属性和方法如 `innerHTML` `getAttribute` `setAttribute`\n\n:::\n\n## 开发一个无限下拉加载图片的页面，如何给每个图片绑定 click 事件？\n\n参考答案\n\n::: details\n\n使用 **事件委托** 实现，避免重复绑定事件，性能高，适合动态加载的场景。\n\n代码示例\n\n```html\n<div id=\"image-container\" style=\"height: 400px; overflow-y: scroll; border: 1px solid #ccc;\">\n  <!-- 加载图片 -->\n</div>\n\n<script>\n  const container = document.getElementById('image-container')\n\n  // 模拟 API 请求加载图片\n  let page = 1 // 当前加载的页码\n  const loadImages = () => {\n    for (let i = 1; i <= 10; i++) {\n      const img = document.createElement('img')\n      img.src = `https://via.placeholder.com/150?text=Image+${(page - 1) * 10 + i}`\n      img.style.margin = '10px'\n      img.alt = `Image ${(page - 1) * 10 + i}`\n      img.className = 'image-item' // 添加统一的类名\n      container.appendChild(img)\n    }\n    page++\n  }\n\n  // 绑定父容器的 click 事件\n  container.addEventListener('click', (event) => {\n    if (event.target.tagName === 'IMG') {\n      alert(`You clicked on ${event.target.alt}`)\n    }\n  })\n\n  // 监听滚动事件，实现无限加载\n  container.addEventListener('scroll', () => {\n    if (container.scrollTop + container.clientHeight >= container.scrollHeight) {\n      loadImages() // 加载更多图片\n    }\n  })\n\n  // 初次加载图片\n  loadImages()\n</script>\n```\n\n以上代码中，我们把 `click` 事件统一绑定在 `container` 容器中，然后判断 `event.target.tagName === 'IMG'` 即触发事件。\n\n:::\n\n## window.onload 和 DOMContentLoaded 的区别是什么？\n\n这两个事件都用于检测页面的加载状态，但触发的时机和作用范围有所不同。\n\n::: details\n\n`DOMContentLoaded` 是当 **DOM 树构建完成**（HTML 被解析完成，不等待样式表、图片、iframe 等资源加载）时触发，不依赖于外部资源。\n\n`window.onload` 是当 **整个页面及所有资源**（包括样式表、图片、iframe、脚本等）加载完成时触发，依赖于外部资源。\n\n`DOMContentLoaded` 会更早触发。\n\n使用推荐\n\n- 如果你的逻辑只依赖 DOM 的加载（如操作页面结构、绑定事件），使用 `DOMContentLoaded`。\n- 如果你的逻辑需要依赖页面所有资源加载完成（如获取图片尺寸、执行动画），使用 `window.onload`。\n\n:::\n\n## script 标签放在 head 里，怎么解决加载阻塞的问题\n\n在 HTML 中，`<script>` 标签通常会阻塞页面的渲染，尤其是当它放在 `<head>` 部分时，因为浏览器会在执行 JavaScript 代码之前停止解析 HTML。\n\n可参考的解决方案\n\n::: details\n\n1. 使用 `async` 属性。当 `async` 属性添加到 `<script>` 标签时，脚本会异步加载，并在加载完成后立即执行，不会阻塞页面的渲染。适用于不依赖其他脚本或页面内容的独立脚本，但多个 JS 文件时无法保证加载和执行顺序。\n\n```html\n<head>\n  <script src=\"script.js\" async></script>\n</head>\n```\n\n2. 使用 `defer` 属性。`defer` 属性使得脚本延迟执行，直到 HTML 文档解析完毕。这意味着脚本不会阻塞 HTML 渲染，且会按照文档中 `<script>` 标签的顺序执行。适用于依赖 DOM 元素的脚本（如操作页面内容）。\n\n```html\n<head>\n  <script src=\"script.js\" defer></script>\n</head>\n```\n\n3. 将 `<script>` 放在 `<body>` 最后。\n\n:::\n\n## 常见的 HTML 标签哪些是 inline 元素，哪些是 block 元素，哪些是 inline-block 元素\n\n参考答案\n\n::: details\n\n1. `inline` 元素有：`a`, `span`, `img`, `strong`, `em`, `b`, `i`, `abbr`, `code`, `br`, `q`（引用）, `sub`（下标）, `sup`（上标）\n\n2. `block` 元素有：`div`, `p`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `ul`, `ol`, `li`, `form`, `section`, `article`, `footer`, `header`, `nav`\n\n3. `inline-block` 元素有：`input` `button`\n\n注意，`table` 虽然也是独占一行，但它 `display: table` 不是 `block`\n\n:::\n\n## 常见的 CSS 选择器有哪些？\n\n参考答案\n\n::: details\n\n| 选择器类型          | 示例                               | 说明                                       |\n| ------------------- | ---------------------------------- | ------------------------------------------ |\n| **元素选择器**      | `p`                                | 选择所有 `<p>` 元素                        |\n| **类选择器**        | `.button`                          | 选择所有 `class=\"button\"` 的元素           |\n| **ID 选择器**       | `#header`                          | 选择 `id=\"header\"` 的元素                  |\n| **通用选择器**      | `*`                                | 选择页面中的所有元素                       |\n| **后代选择器**      | `div p`                            | 选择 `div` 内的所有 `<p>` 元素             |\n| **子元素选择器**    | `div > p`                          | 选择 `div` 的直接子元素 `<p>`              |\n| **相邻兄弟选择器**  | `h1 + p`                           | 选择紧接在 `<h1>` 后面的 `<p>` 元素        |\n| **通用兄弟选择器**  | `h1 ~ p`                           | 选择所有紧跟在 `<h1>` 后面的 `<p>` 元素    |\n| **属性选择器**      | `a[href]`                          | 选择具有 `href` 属性的所有 `<a>` 元素      |\n| **`:hover`**        | `a:hover`                          | 选择鼠标悬停时的 `<a>` 元素                |\n| **`:first-child`**  | `p:first-child`                    | 选择父元素中的第一个 `<p>` 元素            |\n| **`:nth-child(n)`** | `li:nth-child(odd)`                | 选择父元素中所有奇数位置的 `<li>` 元素     |\n| **`::before`**      | `p::before { content: \"Note: \"; }` | 在每个 `<p>` 元素的前面插入 \"Note: \"       |\n| **`::after`**       | `p::after { content: \".\"; }`       | 在每个 `<p>` 元素的后面插入一个句点        |\n| **`:not()`**        | `p:not(.highlight)`                | 选择所有不具有 `highlight` 类的 `<p>` 元素 |\n\n注意，这里的内容比较全，面试时你也许记不住所有，但只要能说上一半儿。\n\n:::\n\n## CSS 盒子模型，尺寸计算\n\n如下代码，请问 `div1` 的 `offsetWidth` 是多大？\n\n```html\n<!-- 如下代码，请问 div1 的 offsetWidth 是多大？ -->\n<style>\n  #div1 {\n    width: 100px;\n    padding: 10px;\n    border: 1px solid #ccc;\n    margin: 10px;\n  }\n</style>\n\n<div id=\"div1\"></div>\n```\n\n答案\n\n::: details\n`offsetWidth` 是指 `元素内容 + 内间距 + 边框`的距离，不包括外间距<br>\n所以 `offsetWidth` 是 `122px`\n:::\n\n追问：如果想要让 `offsetWidth` 等于 `100px` ，还需要再增加一个什么属性？\n\n答案\n\n::: details\n增加 `box-sizing: border-box;`\n:::\n\n## margin 纵向重叠\n\n如下代码，`AAA` 和 `BBB` 之间的距离是多少？\n\n```html\n<!-- 如下代码，AAA 和 BBB 之间的距离是多少？ -->\n<style>\n  p {\n    font-size: 16px;\n    line-height: 1;\n    margin-top: 10px;\n    margin-bottom: 15px;\n  }\n</style>\n\n<p>AAA</p>\n<p></p>\n<p></p>\n<p></p>\n<p>BBB</p>\n```\n\n答案\n\n::: details\n`AAA` 和 `BBB` 之间的距离是 `15px`\n:::\n\n## lineHeight 如何继承？\n\n如下代码，`<p>` 标签的行高将会是多少？\n\n```html\n<!--如下代码，p 标签的行高将会是多少？-->\n<style>\n  body {\n    font-size: 20px;\n    line-height: 200%;\n  }\n  p {\n    font-size: 16px;\n  }\n</style>\n\n<body>\n  <p>AAA</p>\n</body>\n```\n\n答案\n\n::: details\n`line-height` 不同类型的值，继承规则是不一样的\n\n- 写具体的数值，如 `30px`，则继承该数值 —— 比较好理解\n- 写百分比，如 `200%` ，则继承当前计算出来的值，如上述题目 —— 重要！！！\n- 写比例，如 `2` 或 `1.5` ，则继承比例\n\n所以，该问题的的答案是，继承 `40px` 。\n:::\n\n## margin 负值问题\n\n参考答案\n\n::: details\n\n- `margin-left` 负值，元素左移\n- `margin-top` 负值，元素上移\n- `margin-right` 负值，自身宽度缩小，右侧元素会跟进，但内容不受影响\n- `margin-bottom` 负值，自身高度缩小，下方元素会跟进，但内容不受影响\n\n:::\n\n## 什么是 BFC 如何触发 BFC？\n\n参考答案\n\n::: details\n\nBFC (Block formatting context) 直译为\"块级格式化上下文\"。它是一个独立的渲染区域，与这个区域外部毫不相干。即，BFC 里面的的内容再怎么发生变化，也不会影响到 BFC 外面的布局，这一点是在网页布局中非常有用的。先说，能形成 BFC 的条件有：\n\n- 根元素\n- `float` 属性不为 `none`\n- `position` 为 `absolute` 或 `fixed`\n- `display` 为 `inline-block` `table-cell` `table-caption` `flex` `inline-flex`\n- `overflow` 不为 `visible`\n\nBFC 在网页布局中经常用来清除浮动（特别是在使用 `float` 布局的情况下），最常被用来触发 BFC 的属性是`overflow: hidden`，例如要实现一个左图右文的布局：\n\n```html\n<style>\n  .bfc {\n    overflow: hidden; /* 触发 BFC */\n  }\n  .left {\n    float: left;\n  }\n</style>\n\n<div class=\"bfc\">\n  <img src=\"x.png\" class=\"left\" style=\"margin-right: 10px;\" />\n  <p class=\"bfc\">text...</p>\n</div>\n```\n\n:::\n\n## 使用 CSS 实现居中对齐，有哪几种方式？\n\n实现水平居中对齐\n\n::: details\n\n1. inline 元素用`text-align: center;`即可，如下：\n\n```css\n.container {\n  text-align: center;\n}\n```\n\n2. block 元素可使用`margin: auto;`\n\n```css\n.container {\n  text-align: center;\n}\n.item {\n  width: 1000px;\n  margin: auto;\n}\n```\n\n3. 绝对定位元素可结合`left`和`margin`实现，但是必须知道宽度\n\n```css\n.container {\n  position: relative;\n  width: 500px;\n}\n.item {\n  width: 300px;\n  height: 100px;\n  position: absolute;\n  left: 50%;\n  margin-left: -150px;\n}\n```\n\n:::\n\n实现垂直居中对齐\n\n::: details\n\n1. inline 元素可设置`line-height`的值等于`height`值，如单行文字垂直居中：\n\n```css\n.container {\n  height: 50px;\n  line-height: 50px;\n}\n```\n\n2. 绝对定位元素，可结合`top`和`margin`实现，但是必须知道尺寸\n\n- 优点：兼容性好；\n- 缺点：需要提前知道尺寸，\n\n```css\n.container {\n  position: relative;\n  height: 200px;\n}\n.item {\n  width: 80px;\n  height: 40px;\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  margin-top: -20px;\n  margin-left: -40px;\n}\n```\n\n3. 绝对定位可结合`transform`实现居中\n\n- 优点：不需要提前知道尺寸；\n- 缺点：兼容性不好（现代浏览器都没问题）\n\n```css\n.container {\n  position: relative;\n  height: 200px;\n}\n.item {\n  width: 80px;\n  height: 40px;\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -50%);\n}\n```\n\n4. 绝对定位结合`margin: auto`，不需要提前知道尺寸，兼容性好\n\n```css\n.container {\n  position: relative;\n  height: 300px;\n}\n.item {\n  width: 100px;\n  height: 50px;\n  position: absolute;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  margin: auto;\n}\n```\n\n:::\n\n## 什么是 CSS 定位上下文？absolute 和 relative 分别依据谁来定位？\n\n参考答案\n\n::: details\n\n- `relative` 是相对于自身定位的（且不会影响其他元素的定位）\n- `absolute` 是相对于上层最近的一个定位元素来定位的，如果没有就依赖于 `body` 定位。\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.ruanyifeng.com/blog/2019/11/css-position.html\n\n:::\n\n## CSS `overflow: hidden` `display：none` 和 `visibility: hidden` 有什么区别\n\n参考答案\n\n::: details\n\n- `overflow: hidden` 溢出内容不可见，未溢出的部分正常可见\n- `display：none` 隐藏内容，不占用任何空间，内容变化不会重新渲染\n- `visibility: hidden` 隐藏元素，但保留其占据的空间，内容变化会重新渲染\n\n:::\n\n## CSS `px` `%` `em` `rem` `vw/vh` 的区别\n\n参考答案\n\n::: details\n\n| 单位    | 基准                     | 绝对/相对 | 优点                       | 缺点                 | 适用场景                 |\n| ------- | ------------------------ | --------- | -------------------------- | -------------------- | ------------------------ |\n| `px`    | 固定像素                 | 绝对      | 精确，简单易用             | 缺乏响应式能力       | 固定尺寸元素             |\n| `%`     | 父元素尺寸               | 相对      | 灵活，适合响应式设计       | 依赖父元素           | 响应式布局，流式设计     |\n| `em`    | 当前元素字体大小         | 相对      | 动态调整，适合局部相对设计 | 嵌套复杂，计算难预测 | 动态字体、内外边距等     |\n| `rem`   | 根元素字体大小（`html`） | 相对      | 全局一致，计算简单         | 需要设置根元素字体   | 全局比例调整，响应式设计 |\n| `vw/vh` | 视口宽度或高度           | 相对      | 基于视口，适合全屏设计     | 小屏显示可能不理想   | 全屏布局，视口动态调整   |\n\n使用建议:\n\n- 响应式设计：结合使用 rem 和 %。\n- 固定大小：使用 px 定义精确尺寸。\n- 全屏布局：使用 vw 和 vh。\n- 动态比例设计：em 和 rem 都是优秀的选择，但推荐 rem 更加简洁统一。\n\n:::\n\n## 如何实现 Retina 屏 1px 像素边框\n\n参考答案\n\n::: details\n\n1. 使用 `transform: scale` 实现。\n\n```css\n.retina-border {\n  position: relative;\n}\n\n.retina-border::after {\n  content: '';\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  height: 1px; /* 边框的物理宽度 */\n  background-color: black; /* 边框颜色 */\n  transform: scaleY(0.5); /* 缩放到 0.5 */\n  transform-origin: 0 0; /* 缩放起点 */\n}\n```\n\n2. 使用 `box-shadow` 模拟边框\n\n```css\n.retina-border {\n  position: relative;\n  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); /* 通过阴影模拟边框 */\n}\n```\n\n:::\n\n## 使用 CSS 画一个三角形\n\n参考答案\n\n::: details\n\n使用 CSS “画”一个向上的三角形，重点在于使用透明边框。\n\n```html\n<style>\n  .triangle-up {\n    width: 0;\n    height: 0;\n    border-left: 50px solid transparent;\n    border-right: 50px solid transparent;\n    border-bottom: 50px solid #000; /* 底部颜色即为三角形颜色 */\n  }\n</style>\n<div class=\"triangle-up\"></div>\n```\n\n:::\n\n## 如何实现黑白主题变化？\n\n参考答案\n\n::: details\n\n可使用 CSS 变量\n\n```css\n/* 定义变量 */\n:root,\n:host {\n  --color: #333;\n  --bg-color: #fff;\n}\n\n/* 使用变量 */\np {\n  color: var(--color);\n  background-color: var(--bg-color);\n}\n```\n\n:::\n\n## 如何实现响应式布局？\n\nCSS 实现响应式布局可以使页面在不同的设备和屏幕尺寸上有良好的显示效果，以下是几种常见的实现方式：\n\n::: details\n\n1. 使用媒体查询（Media Queries）。媒体查询是响应式布局的核心技术，通过检测设备的宽度、高度、分辨率等条件应用不同的样式。可根据屏幕宽度调整字体大小、布局样式等。\n\n```css\n/* 默认样式 */\nbody {\n  font-size: 16px;\n  padding: 20px;\n}\n\n/* 屏幕宽度小于等于768px时的样式 */\n@media (max-width: 768px) {\n  body {\n    font-size: 14px;\n    padding: 10px;\n  }\n}\n\n/* 屏幕宽度大于1200px时的样式 */\n@media (min-width: 1200px) {\n  body {\n    font-size: 18px;\n    padding: 30px;\n  }\n}\n```\n\n2. 使用弹性盒子（Flexbox）。创建水平或垂直方向上的自适应布局，比如导航栏、网格布局。\n\n```css\n.container {\n  display: flex;\n  flex-wrap: wrap; /* 允许换行 */\n}\n\n.item {\n  flex: 1 1 200px; /* 每个子项占据至少200px，随空间调整 */\n  margin: 10px;\n  background-color: #f0f0f0;\n}\n```\n\n3. 使用网格布局（CSS Grid Layout）。 创建复杂的自适应网格布局，比如图片库、商品列表。\n\n```css\n.container {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n  gap: 20px;\n}\n\n.item {\n  background-color: #d4edda;\n  padding: 10px;\n}\n```\n\n4. 使用百分比和单位 vh/vw 实现宽度和高度的自适应。实现宽度和高度随窗口变化而调整。\n\n```css\n.container {\n  width: 80%; /* 占父容器的80% */\n  height: 50vh; /* 占视口高度的50% */\n  background-color: #e9ecef;\n}\n```\n\n:::\n\n## 如何理解 `z-index` ？\n\n::: details\n\n- `z-index` 是一个 CSS 属性，用于控制元素的堆叠顺序（沿 Z 轴的显示顺序）。值越大，元素越靠前显示，反之值越小，元素越靠后。\n- `z-index` 只适用于**定位**的元素，需要设置 `position` 属性为 `relative`、`absolute`、`fixed` 或 `sticky`，否则 `z-index` 不生效。\n- `z-index` 只在**同级**比较，父子元素的 `z-index` 不会互相影响。\n\n:::\n\n## 使用 flex 设计一个“四合院”布局\n\n如下图\n\n![](../imgs/css-layout.png)\n\n参考答案\n\n::: details\n\n```html\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>CSS 四合院</title>\n    <style>\n      html * {\n        margin: 0;\n        padding: 0;\n      }\n      html {\n        height: 100%;\n      }\n      body {\n        display: flex;\n        flex-direction: column;\n        height: 100%;\n      }\n      #header {\n        height: 50px;\n        background-color: red;\n      }\n      #container {\n        flex: 1;\n        display: flex;\n      }\n      #left-container {\n        width: 100px;\n        background-color: green;\n      }\n      #main-container {\n        flex: 1;\n        background-color: #ccc;\n      }\n      #right-container {\n        width: 200px;\n        background-color: yellow;\n      }\n      #footer {\n        height: 50px;\n        background-color: blue;\n      }\n    </style>\n  </head>\n  <body>\n    <!-- HTML5 标签 语义化 -->\n    <header id=\"header\">header</header>\n    <section id=\"container\">\n      <aside id=\"left-container\">left</aside>\n      <section id=\"main-container\">main</section>\n      <!-- 或者用 article ，看用途 -->\n      <aside id=\"right-container\">right</aside>\n      <!-- 或者用 section ，看用途 -->\n    </section>\n    <footer id=\"footer\">footer</footer>\n  </body>\n</html>\n```\n\n:::\n\n## 你用过哪些 CSS 相关的技术，如库、框架、预处理语言、后处理语言等\n\n参考答案\n\n::: details\n\n- CSS 框架：TailwindCSS BootStrap\n- CSS 预处理语言：Less Sass Stylus\n- CSS 后处理语言：PostCSS Autoprefixer\n- CSS 组件库：ElementUI AntDesign\n- CSS-in-JS：Styled-Components Emotion\n- CSS 工具：Normalize.css Animate.css\n\n:::\n"
  },
  {
    "path": "docs/first-exam/HTTP.md",
    "content": "# HTTP 网络请求\n\nHTTP 和 Ajax 是前后端沟通的桥梁，面试重点考察，无论工作经验长短。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## TCP 是如何建立连接的，三次握手，四次挥手\n\n参考答案\n\n::: details\n\n三次握手\n\n- 客户端向服务端发送建立连接请求，客户端进入 SYN-SEND 状态\n- 服务端收到建立连接请求后，向客户端发送一个应答，服务端进入 SYN-RECEIVED 状态\n- 客户端接收到应答后，向服务端发送确认接收到应答，客户端进入 ESTABLISHED 状态\n\n四次挥手\n\n- 客户端向服务端发送断开连接请求\n- 服务端收到断开连接请求后，告诉应用层去释放 tcp 连接\n- 服务端向客户端发送最后一个数据包 FINBIT ，服务端进入 LAST-ACK 状态\n- 客户端收到服务端的断开连接请求后，向服务端确认应答\n\n> 三次握手四次挥手，客户端都是主动方，服务端都是被动方。在状态方面：三次握手的客户端和服务端都是由原来的 closed 变为 established，四次挥手的客户端和服务端都是由原来的 established 变为 closed。\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7350107540327022601\n  :::\n\n## HTTP 几个版本的区别\n\n参考答案\n\n::: details\n\n#### HTTP/0.9 - 单行协议\n\n- 只有 GET 请求行，无请求头和请求体\n- 只能传输 HTML 文件，以 ASCII 字符流返回\n- 无响应头\n\n#### HTTP/1.0 - 多类型支持\n\n- 支持多种文件类型传输，不限于 ASCII 编码\n- 引入请求头和响应头( key-value 形式)\n- 每个请求都需要建立新的 TCP 连接\n\n#### HTTP/1.1 - 持久连接\n\n- 引入持久连接( keep-alive )：一个 TCP 连接可传输多个 HTTP 请求\n- 默认开启 keep-alive，通常限制 6-8 个并发连接\n- 存在队头阻塞问题：前面的请求阻塞会影响后续请求\n- 引入 Host 字段，支持虚拟主机\n- 引入 Chunk transfer 机制处理动态内容长度\n\n#### HTTP/2.0 - 多路复用\n\n- 一个域名只使用一个 TCP 长连接\n- 引入二进制分帧层，实现多路复用\n- 可对请求设置优先级\n- 引入 HTTPS(HTTP + TLS) 加密\n\n#### HTTP/3.0 - QUIC 协议\n\n- 基于 UDP 协议而非 TCP\n- 实现了类似 TCP 的流量控制和可靠传输\n- 集成 TLS 加密\n- 实现多路复用\n- 解决 TCP 队头阻塞问题\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7350520171611652147\n  :::\n\n## HTTP 常见的状态码\n\n参考答案\n\n::: details\n\n- 200 请求成功，请求在服务端被正确处理\n- 204 响应成功，没有数据\n- 205 服务器处理成功，浏览器应重置文档视图\n- 206 服务器成功处理了部分get请求\n- 301 资源永久重定向\n- 302 资源临时重定向\n- 303 让你查看其他地址\n- 304 请求的资源没有修改，服务端不会返回任何资源，协商缓存\n- 400 请求语法错误，服务器看不懂\n- 401 请求没有携带信息，比如 token 认证失败\n- 403 请求被拒绝、敏感词\n- 404 找不到资源\n- 500 服务器内部错误，无法完成请求\n- 501 服务器不支持当前请求所需的功能\n- 503 服务器系统维护或者超载，暂时无法处理客户端的请求\n\n:::\n\n## HTTP 常见 Header\n\n参考答案\n\n::: details\n\n请求头\n\n- accept: text/html 告诉服务端我期望接收到一个html的文件\n- accept-encoding: gzip, deflate, br 告诉服务端以这种方式压缩\n- accept-language: zh-CN 告诉服务端以中文的格式返回\n- authorization: 告诉服务端授权信息\n- cookie: 告诉服务端客户端存储的 cookie\n- origin: 告诉服务端请求的来源\n\n响应头\n\n- content-encoding: br 告诉浏览器压缩方式是br\n- content-type: text/html; charset=utf-8 告诉浏览器以这种方式，编码加载\n- cache-control: 告诉浏览器缓存策略\n- expires: 告诉浏览器缓存过期时间\n- set-cookie: 告诉浏览器设置 cookie\n- access-control-allow-origin: \\* 告诉浏览器允许跨域\n\n:::\n\n## URL 包含哪些部分？\n\n参考答案\n\n::: details\n\nURL (Uniform Resource Locator) 包含以下部分：\n\n1. 协议 (protocol)：如 `http://`、`https://`、`ftp://` 等\n\n2. 域名 (domain)：如 `www.example.com`\n\n   - 子域名：`www`\n   - 主域名：`example`\n   - 顶级域名：`com`\n\n3. 端口号 (port)：如 `:80`、`:443`（可选，HTTP 默认 80，HTTPS 默认 443）\n\n4. 路径 (path)：如 `/blog/article`\n\n5. 查询参数 (query string)：如 `?id=123&name=test`\n\n6. 锚点/片段标识符 (fragment)：如 `#header`\n\n示例：https://www.example.com:80/blog/article?id=123&name=test#header\n\n:::\n\n## GET 和 POST 请求的区别\n\n参考答案\n\n::: details\n\n- 协议层面：请求行里一定要有请求方法，官方为了统一语义，定义了 GET 表示拿数据，POST 表示上传数据，PUT 表示修改数据，所以 GET，POST 请求这里仅仅是语义上的差别，没有说哪个请求必须做啥\n- 应用层面：开发者约定俗成的规范，GET 请求的请求体会设空，不是没有请求体\n- 浏览器层面：GET 请求会缓存，有历史记录\n\n:::\n\n## Ajax Fetch Axios 三者有什么区别？\n\n参考答案\n\n::: details\n\nAjax、Fetch 和 Axios 都是用于发送 HTTP 请求的技术，但有以下区别：\n\nAjax (Asynchronous JavaScript and XML)\n\n- 是一种技术统称，不是具体的 API\n- 最常用的实现是 XMLHttpRequest (XHR)\n- 写法比较繁琐，需要手动处理各种状态\n- 回调地狱问题\n- 不支持 Promise\n\nFetch\n\n- 浏览器原生 API\n- 基于 Promise\n- 更简洁的写法\n- 不需要额外引入\n- 只对网络请求报错，对 400、500 都当做成功的请求\n- 默认不带 cookie\n- 不支持请求超时控制\n- 不支持请求取消\n- 不支持请求进度监控\n\nAxios\n\n- 第三方库，需要额外引入\n- 基于 Promise\n- 支持浏览器和 Node.js\n- 请求/响应拦截器\n- 自动转换 JSON 数据\n- 客户端支持防止 XSRF\n- 支持请求取消\n- 支持请求超时控制\n- 支持请求进度监控\n- 支持并发请求\n- 自动转换请求和响应数据\n\n使用建议：\n\n- 如果是简单的请求，使用 Fetch 即可\n- 如果需要更多功能，建议使用 Axios\n- 现代项目中已经很少直接使用 XMLHttpRequest\n\n:::\n\n## Fetch 和 XMLHTTPRequest 有什么区别？\n\n参考答案\n\n::: details\n\n语法和使用\n\n- Fetch 基于 Promise，代码更简洁优雅\n- XHR 使用回调函数，容易产生回调地狱\n- Fetch 的 API 设计更简单现代\n- XHR 的 API 设计较老，使用相对复杂\n\n功能特性\n\n- Fetch 默认不发送 cookies，需要配置 credentials\n- XHR 默认发送 cookies\n- Fetch 不能监听上传进度\n- XHR 可以监听上传和下载进度\n- Fetch 不能直接取消请求（需要 AbortController）\n- XHR 可以通过 abort() 直接取消请求\n\n错误处理\n\n- Fetch 只有网络错误才会 reject，HTTP 错误码不会导致 reject\n- XHR 可以处理所有类型的错误，包括 HTTP 错误码\n\n浏览器支持\n\n- Fetch 是现代浏览器标准 API\n- XHR 有更好的浏览器兼容性，包括旧版本\n\n:::\n\n## 什么是 Restful API ？\n\n参考答案\n\n::: details\n\nRESTful API 是一种软件架构风格，用于设计网络应用程序的接口。主要特点：\n\n资源导向\n\n- 使用 URL 定位资源\n- 每个资源都有唯一的 URL\n- 资源可以有多种表现形式（如 JSON、XML）\n\nHTTP 方法对应操作\n\n- GET：获取资源\n- POST：创建资源\n- PUT：更新资源（完整更新）\n- PATCH：更新资源（部分更新）\n- DELETE：删除资源\n\n无状态\n\n- 服务器不保存客户端状态\n- 每个请求包含所需的所有信息\n- 有利于横向扩展\n\n统一接口\n\n- 使用标准的 HTTP 方法\n- 使用标准的 HTTP 状态码\n- 返回格式一致（通常是 JSON）\n\n:::\n\n## 什么是 GraphQL ？\n\n参考答案\n\n::: details\n\nGraphQL 是一种用于 API 的查询语言和运行时，由 Facebook 开发。主要特点：\n\n查询灵活性\n\n- 客户端可以精确指定需要哪些数据\n- 可以在一个请求中获取多个资源\n- 避免了传统 REST API 的过度获取和获取不足问题\n\n类型系统\n\n- 强类型的 Schema 定义\n- 自动生成文档\n- 开发时有更好的类型提示\n\n单个端点\n\n- 只需要一个 API 端点\n- 所有查询都发送到同一个地址\n- 通过查询语句区分不同的操作\n\n主要操作类型\n\n- Query：获取数据\n- Mutation：修改数据\n- Subscription：实时数据订阅\n\n优点\n\n- 减少网络请求\n- 避免版本化问题\n- 强类型保障\n- 更好的开发体验\n\n缺点\n\n- 学习成本较高\n- 缓存较为复杂\n- 服务端实现复杂度增加\n\n:::\n\n## 如何理解 cookie\n\n参考答案\n\n::: details\n\nCookie 是服务器发送到用户浏览器并保存在本地的一小块数据。\n\n主要特点：\n\n- 由服务器生成，浏览器进行存储\n- 每次请求时会自动携带对应域名下的 cookie\n- 可设置过期时间\n- 默认情况下随着浏览器关闭而删除（会话 cookie）\n\n常用属性：\n\n- name：cookie 名称\n- value：cookie 值\n- domain：指定 cookie 所属域名\n- path：指定 cookie 所属路径\n- expires/max-age：过期时间\n- secure：只在 HTTPS 下传输\n- httpOnly：禁止 JS 访问\n- sameSite：跨站点请求限制\n\n使用场景：\n\n- 会话状态管理（用户登录状态、购物车等）\n- 个性化设置（用户偏好、主题等）\n- 浏览器行为跟踪（分析用户行为等）\n\n限制：\n\n- 大小限制：通常为 4KB\n- 数量限制：每个域名下的 cookie 数量有限\n- 安全性：明文传输（除非使用 HTTPS）\n- 作用域：只能在所属域名下使用\n\n:::\n\n## 为何现代浏览器都禁用第三方 cookie\n\n参考答案\n\n::: details\n\n主要原因是保护用户隐私和安全：\n\n隐私问题\n\n- 第三方 Cookie 可以跨站点追踪用户行为\n- 广告商可以构建用户画像和浏览历史\n- 用户数据可能被未经授权收集和使用\n\n安全风险\n\n- 增加 CSRF（跨站请求伪造）攻击风险\n- 可能被用于会话劫持\n- 恶意网站可能滥用第三方 Cookie\n\n技术影响\n\n- Safari 和 Firefox 已默认禁用第三方 Cookie\n- Chrome 计划在 2024 年完全禁用第三方 Cookie\n- 替代方案：\n  - First-Party Cookie\n  - localStorage\n  - Privacy Sandbox\n  - FLoC (Federated Learning of Cohorts)\n\n:::\n\n## 如何理解 Session ？\n\n参考答案\n\n::: details\n\nSession 是服务器端的会话管理机制：\n\n基本概念\n\n- 服务器为每个用户创建的临时会话存储空间\n- 用于保存用户的会话状态\n- 通过 SessionID 来识别不同用户\n- SessionID 通常保存在 Cookie 中\n\n工作流程\n\n1. 用户首次访问服务器时，服务器创建 Session 并生成 SessionID\n2. 服务器将 SessionID 通过 Cookie 发送给客户端\n3. 客户端后续请求会自动携带包含 SessionID 的 Cookie\n4. 服务器通过 SessionID 找到对应 Session 并识别用户\n\n特点\n\n- 安全性较高：敏感数据存储在服务器\n- 服务器负载较大：需要存储所有用户的 Session\n- 依赖 Cookie：通常需要 Cookie 来存储 SessionID\n- 集群问题：需要考虑 Session 共享\n\n使用场景\n\n- 用户登录状态管理\n- 购物车\n- 权限验证\n- 表单验证\n\n与 Cookie 的区别\n\n- 存储位置：Session 在服务器，Cookie 在客户端\n- 安全性：Session 较安全，Cookie 相对不安全\n- 存储容量：Session 容量较大，Cookie 通常限制 4KB\n- 性能：Session 消耗服务器资源，Cookie 消耗带宽资源\n\n:::\n\n## 什么是 JWT 描述它的工作过程\n\n参考答案\n\n::: details\n\nJWT (JSON Web Token) 是一种开放标准，用于在各方之间安全地传输信息。\n\n组成部分（用 . 分隔的三部分）：\n\n- Header（头部）：指定加密算法和令牌类型\n- Payload（负载）：包含声明（claims）的实际数据\n- Signature（签名）：对前两部分的签名，用于验证消息未被篡改\n\n工作流程：\n\n1. 用户登录成功后，服务器创建 JWT\n\n   - 设置 Header 和 Payload\n   - 使用密钥生成签名\n   - 将三部分组合成 token\n\n2. 服务器将 token 返回给客户端\n\n   - 客户端存储在 localStorage 或 cookie 中\n\n3. 后续请求携带 token\n\n   - 通常放在 Authorization header\n   - 格式：`Bearer <token>`\n\n4. 服务器验证 token\n   - 检查签名是否有效\n   - 验证是否过期\n   - 验证其他声明（claims）\n\n特点：\n\n- 无状态：服务器不需要存储会话信息\n- 可扩展：负载部分可以包含自定义数据\n- 跨域友好：可以在不同域名下使用\n- 性能好：验证在服务端完成，不需要查询数据库\n\n安全考虑：\n\n- 不要在 payload 中存储敏感信息\n- 设置合理的过期时间\n- 使用 HTTPS 传输\n- 妥善保管签名密钥\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7346430299490189348\n\n:::\n\n## JWT 如何自动更新 token ？\n\n参考答案\n\n::: details\n\nJWT token 自动更新主要有以下几种方案：\n\n双 token 机制\n\n- access token：短期令牌，用于接口认证\n- refresh token：长期令牌，用于刷新 access token\n- 优点：安全性高，即使 access token 泄露影响有限\n- 缺点：实现相对复杂，需要额外存储 refresh token\n\n工作流程：\n\n1. 用户登录后获取 access token 和 refresh token\n2. 使用 access token 访问接口\n3. access token 过期时，使用 refresh token 获取新的 access token\n4. refresh token 过期时，需要重新登录\n\n```js\n// 前端示例代码\nasync function request(url, options) {\n  try {\n    const res = await fetch(url, {\n      ...options,\n      headers: {\n        Authorization: `Bearer ${getAccessToken()}`,\n      },\n    })\n\n    if (res.status === 401) {\n      // access token 过期，尝试刷新\n      const newToken = await refreshToken()\n      if (newToken) {\n        // 使用新 token 重试请求\n        return request(url, options)\n      } else {\n        // refresh token 也过期，跳转登录\n        redirectToLogin()\n      }\n    }\n\n    return res\n  } catch (error) {\n    console.error(error)\n  }\n}\n```\n\n滑动过期机制\n\n- 每次请求都刷新 token 过期时间\n- 类似于会话超时机制\n- 优点：实现简单，用户体验好\n- 缺点：安全性相对较低\n\n无感刷新机制\n\n- 在 token 即将过期时自动刷新\n- 可以通过定时器或请求拦截器实现\n- 优点：用户无感知，体验好\n- 缺点：需要处理并发请求的问题\n\n最佳实践：\n\n- 根据业务安全需求选择合适的方案\n- access token 过期时间不宜过长（如 2 小时）\n- refresh token 过期时间可以较长（如 7 天）\n- 重要操作仍需要二次验证\n- 考虑 token 注销机制\n\n:::\n\n## 什么是 SSO 单点登录，描述它的工作过程\n\n参考答案\n\n::: details\n\nSSO (Single Sign On) 单点登录是一种身份验证机制，允许用户使用一组凭证访问多个相关但独立的系统。\n\n基本概念\n\n- 一次登录，全局通用\n- 多个子系统共享用户会话\n- 统一的认证中心\n- 提高用户体验和安全性\n\n工作流程：\n\n用户首次访问系统\n\n- 用户访问系统 A\n- 系统 A 检查无登录状态\n- 重定向到 SSO 认证中心\n- 带上系统 A 的地址作为参数\n\nSSO 认证中心处理\n\n- 检查用户是否已登录 SSO\n- 未登录则显示登录页面\n- 用户输入账号密码\n- 认证中心验证身份\n\n回到系统 A\n\n- SSO 生成票据（ticket）\n- 重定向回系统 A\n- 带上票据参数\n- 系统 A 验证票据\n- 创建本地会话\n\n访问系统 B\n\n- 用户访问系统 B\n- 系统 B 检查无登录状态\n- 重定向到 SSO 认证中心\n- SSO 发现用户已登录\n- 直接生成票据返回\n- 系统 B 验证票据\n- 创建本地会话\n\n实现方式：\n\n- 基于 Cookie\n- 基于 Token\n- 基于 SAML\n- 基于 OAuth\n- 基于 CAS\n\n优点：\n\n- 提升用户体验\n- 减少密码管理\n- 统一认证流程\n- 提高安全性\n\n缺点：\n\n- 认证中心单点故障\n- 配置相对复杂\n- 需要额外的安全考虑\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7454474417318690831\n\n:::\n\n## 什么是跨域？如何实现跨域通讯？\n\n参考答案\n\n::: details\n\n跨域是指浏览器的同源策略限制，当前域名的 JavaScript 代码试图访问其他域名下的资源时会受到限制。\n\n同源的定义：\n\n- 协议相同（http/https）\n- 域名相同\n- 端口相同\n\n跨域解决方案：\n\nCORS（跨域资源共享）\n\n- 服务器设置 Access-Control-Allow-Origin 等响应头\n- 可以配置允许的请求方法、请求头、是否允许携带认证信息等\n- 最常用的跨域解决方案\n\nJSONP\n\n- 利用 `<script>` 标签不受同源策略限制的特点\n- 只支持 GET 请求\n- 需要服务器配合返回 JavaScript 代码\n\n代理服务器\n\n- 开发环境：webpack-dev-server、vite 等的 proxy 配置\n- 生产环境：Nginx 反向代理\n\npostMessage\n\n- HTML5 标准中的 API\n- 用于不同窗口间的跨域通信\n- 可以在父子页面（iframe）或者多窗口间通信\n\nWebSocket\n\n- 建立在 TCP 之上的协议\n- 天然支持跨域\n- 适合需要实时通信的场景\n\ndocument.domain（已废弃）\n\n- 仅适用于主域名相同的情况\n- 将子域和主域的 document.domain 设为相同的主域\n\n最佳实践：\n\n- 优先使用 CORS，配置得当的情况下最安全\n- 需要兼容旧浏览器时可以考虑 JSONP\n- 开发环境优先使用代理服务器\n- 特殊场景（如页面通信）可以考虑 postMessage\n- 需要实时通信时使用 WebSocket\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7346079038555602955\n\n:::\n\n## HTTP 请求跨域时为何要发送 options 请求\n\n参考答案\n\n::: details\n\nOPTIONS 请求是 CORS 预检请求(Preflight Request)，用于检查实际请求是否可以安全地发送。\n\n触发条件：\n\n- 使用非简单请求方法：除 GET、POST、HEAD 之外的方法\n- 使用非简单请求头：除 Accept、Accept-Language、Content-Language、Content-Type 之外的请求头\n- Content-Type 不是以下之一：\n  - application/x-www-form-urlencoded\n  - multipart/form-data\n  - text/plain\n\n工作流程：\n\n1. 浏览器发送 OPTIONS 预检请求，包含：\n\n   - Origin：请求来源\n   - Access-Control-Request-Method：实际请求使用的方法\n   - Access-Control-Request-Headers：实际请求使用的请求头\n\n2. 服务器响应预检请求，返回：\n\n   - Access-Control-Allow-Origin：允许的源\n   - Access-Control-Allow-Methods：允许的方法\n   - Access-Control-Allow-Headers：允许的请求头\n   - Access-Control-Max-Age：预检请求的缓存时间\n\n3. 如果预检通过，浏览器才会发送实际请求\n\n优化建议：\n\n- 尽可能使用简单请求，避免触发预检\n- 合理设置 Access-Control-Max-Age 缓存预检结果\n- 服务端正确配置 CORS 响应头\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7403185402347159588\n\n:::\n\n## options 请求会携带 cookie 吗\n\n参考答案\n\n::: details\n\nOPTIONS 请求通常不会携带 Cookie。它是一个预检请求，用于检查实际请求是否可以安全地发送。浏览器在发送 OPTIONS 请求时，不会自动附带 Cookie 和 Authorization 等认证信息，除非明确设置了 `credentials` 选项。\n\n如果需要在 OPTIONS 请求中携带 Cookie，可以在请求中设置 `credentials: 'include'`，但通常不推荐这样做，因为 OPTIONS 请求的目的就是检查跨域请求的安全性，而不是进行身份验证。\n\n:::\n\n## 简述浏览器的缓存策略\n\n参考答案\n\n::: details\n\n浏览器缓存策略主要分为两种：强缓存和协商缓存。\n\n强缓存\n\n- 不需要向服务器发送请求，直接使用本地缓存\n- 通过 HTTP 响应头控制：\n  - Cache-Control：\n    - max-age：缓存有效时间（秒）\n    - no-cache：需要和服务器协商验证\n    - no-store：不使用任何缓存\n    - private：仅浏览器可缓存\n    - public：中间代理/CDN 等也可缓存\n  - Expires：过期时间点（已被 Cache-Control 取代）\n\n协商缓存\n\n- 需要向服务器发送请求验证资源是否有效\n- 如果有效返回 304，使用本地缓存\n- 通过以下响应头实现：\n  - Last-Modified/If-Modified-Since：基于文件修改时间\n  - ETag/If-None-Match：基于文件内容哈希值\n\n缓存位置（优先级从高到低）：\n\n1. Service Worker\n2. Memory Cache（内存缓存）\n3. Disk Cache（硬盘缓存）\n4. Push Cache（HTTP/2）\n\n最佳实践：\n\n- HTML：使用协商缓存\n- CSS、JS、图片：使用强缓存，文件名带 hash\n- API 请求：根据业务需求设置合适的缓存策略\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7352075703859183667\n\n:::\n\n## 什么是图片防盗链，如何实现？\n\n参考答案\n\n::: details\n\n图片防盗链是指服务器通过 HTTP 协议中的 Referer 字段来判断请求是否来自合法站点，从而防止其他网站直接引用本站图片资源。\n\n实现方式：\n\n服务器端实现\n\n- 检查 HTTP Referer 字段\n- 判断请求来源是否在白名单中\n- 对非法请求返回 403 或替代图片\n\nNginx 配置示例：\n\n```nginx\nlocation ~ .*\\.(gif|jpg|jpeg|png|bmp)$ {\n    valid_referers none blocked server_names *.example.com;\n    if ($invalid_referer) {\n        return 403;\n        # 或者返回替代图片\n        # rewrite ^/ /path/to/default.jpg break;\n    }\n}\n```\n\n其他防盗链方案：\n\n- 给图片添加水印\n- 使用 Token 验证\n- 使用 CDN 提供的防盗链功能\n- 对图片进行加密处理\n\n注意事项：\n\n- Referer 可以被伪造，不能作为唯一判断依据\n- 移动端 APP 可能不发送 Referer\n- 部分浏览器可能禁用 Referer\n- 需要考虑用户体验和 SEO 影响\n\n:::\n\n## 简述 HTTPS 加密过程\n\n参考答案\n\n::: details\n\nHTTPS 使用 TLS/SSL 协议进行加密，主要包含以下步骤：\n\n客户端发起请求\n\n- 发送支持的加密算法列表\n- 发送随机数 Client Random\n\n服务器回应\n\n- 选择加密算法\n- 发送数字证书（包含公钥）\n- 发送随机数 Server Random\n\n客户端验证证书\n\n- 验证证书是否由可信 CA 签发\n- 验证证书域名是否匹配\n- 验证证书是否在有效期内\n\n生成会话密钥\n\n- 客户端生成随机数 Pre-master secret\n- 使用服务器公钥加密 Pre-master secret\n- 客户端和服务器都通过三个随机数生成会话密钥\n  (Client Random + Server Random + Pre-master secret)\n\n开始加密通信\n\n- 双方使用会话密钥进行对称加密通信\n- 保证通信内容的机密性和完整性\n\n特点：\n\n- 采用混合加密：非对称加密传输密钥，对称加密传输数据\n- 数字证书保证服务器身份可信\n- 具有防篡改和不可否认性\n\n:::\n\n## 移动端 H5 如何抓包？\n\n参考答案\n\n::: details\n\n移动端 H5 抓包主要有以下几种方法：\n\nCharles/Fiddler\n\n- 电脑端安装抓包工具\n- 手机和电脑连接同一网络\n- 手机设置代理为电脑 IP 和端口\n- 安装并信任证书（HTTPS 抓包需要）\n\n优点：\n\n- 功能强大，可以查看详细请求信息\n- 支持请求修改和重放\n- 支持断点调试\n\nvConsole\n\n- 移动端调试面板\n- 直接在页面中引入 js 文件\n- 可以查看网络请求、console 日志等\n- 适合开发环境使用\n\n```js\n<script src=\"https://unpkg.com/vconsole/dist/vconsole.min.js\"></script>\n<script>\n  var vConsole = new VConsole();\n</script>\n```\n\nChrome Remote Debug\n\n- 安卓设备通过 USB 连接电脑\n- 开启开发者模式和 USB 调试\n- Chrome 访问 chrome://inspect\n- 可以使用完整的 Chrome DevTools\n\nSafari Web Inspector\n\n- iOS 设备通过 USB 连接 Mac\n- 开启 Web 检查器\n- Safari 开发菜单中选择设备\n- 可以使用完整的 Safari 调试工具\n\n注意事项：\n\n- HTTPS 抓包需要安装证书\n- 部分 App 可能有反抓包机制\n- 生产环境建议移除调试工具\n- 注意数据安全和隐私保护\n\n:::\n\n## script 标签的 defer 和 async 有什么区别\n\n参考答案\n\n::: details\n\nscript 标签的 defer 和 async 属性都是用于控制脚本的加载和执行时机：\n\n普通 script\n\n- 阻塞 HTML 解析\n- 立即下载并执行脚本\n- 按照在文档中的顺序执行\n\ndefer\n\n- 异步下载脚本，不阻塞 HTML 解析\n- 等到 HTML 解析完成后，DOMContentLoaded 事件触发前执行\n- 多个 defer 脚本按照在文档中的顺序执行\n- 适用于需要操作 DOM 的脚本\n- 只对外部脚本文件有效\n\nasync\n\n- 异步下载脚本，不阻塞 HTML 解析\n- 下载完成后立即执行，可能在 HTML 解析完成前执行\n- 多个 async 脚本的执行顺序不确定，取决于下载完成时间\n- 适用于独立的脚本，如统计和广告代码\n- 只对外部脚本文件有效\n\n使用建议：\n\n- 需要操作 DOM 或依赖其他脚本的代码使用 defer\n- 独立的、不依赖 DOM 和其他脚本的代码使用 async\n- 如果脚本之间有依赖关系，不要使用 async\n\n示例：\n\n```html\n<!-- 普通脚本 -->\n<script src=\"script.js\"></script>\n\n<!-- defer 脚本 -->\n<script defer src=\"script.js\"></script>\n\n<!-- async 脚本 -->\n<script async src=\"script.js\"></script>\n```\n\n:::\n\n## prefetch 和 dns-prefetch 分别是什么\n\n参考答案\n\n::: details\n\nprefetch 和 dns-prefetch 是两种不同的资源预加载技术：\n\nprefetch\n\n- 用于预加载将来可能需要的资源\n- 浏览器空闲时才会下载\n- 优先级较低，不影响当前页面加载\n- 适用于下一页可能用到的资源\n\n```html\n<!-- 预加载资源 -->\n<link rel=\"prefetch\" href=\"/next-page.js\" />\n<link rel=\"prefetch\" href=\"/images/large.jpg\" />\n```\n\ndns-prefetch\n\n- 预先解析域名的 DNS 记录\n- 减少 DNS 解析时间\n- 适用于即将请求其他域名的资源\n- 对跨域资源加载特别有效\n\n```html\n<!-- DNS 预解析 -->\n<link rel=\"dns-prefetch\" href=\"//example.com\" />\n<link rel=\"dns-prefetch\" href=\"//api.example.com\" />\n```\n\n使用建议：\n\n- 对确定即将访问的资源使用 prefetch\n- 对跨域资源较多的站点使用 dns-prefetch\n- 不要过度预加载，可能浪费带宽\n- 移动端要谨慎使用，考虑流量消耗\n\n相关技术：\n\n- preload：当前页面必需资源的预加载\n- preconnect：预先建立连接（DNS + TCP + TLS）\n- prerender：预先渲染整个页面\n\n:::\n\n## WebSocket 和 HTTP 协议有什么区别\n\n参考答案\n\n::: details\n\nWebSocket 和 HTTP 的主要区别：\n\n连接特性\n\n- HTTP 是短连接：每次请求都需要建立新的 TCP 连接（除非使用 keep-alive）\n- WebSocket 是持久化的长连接：只需要一次握手，后续可以持续通信\n\n通信方式\n\n- HTTP 是单向通信：客户端请求，服务器响应\n- WebSocket 是双向通信：客户端和服务器都可以主动发送数据\n\n数据格式\n\n- HTTP 每次请求都要带完整的 HTTP 头\n- WebSocket 第一次握手完成后，后续数据传输只需要很小的头部\n\n应用场景\n\n- HTTP 适合一次性的数据交互\n- WebSocket 适合实时性要求高的场景，如：\n  - 实时聊天\n  - 游戏实时数据\n  - 实时协作文档\n\n性能\n\n- WebSocket 的性能和效率通常优于 HTTP 轮询\n- WebSocket 可以更好地节省服务器资源和带宽\n\n支持性\n\n- HTTP 被所有浏览器支持\n- WebSocket 需要浏览器支持（现代浏览器普遍已支持）\n\n:::\n\n## 如何上传文件？使用 fetch 或者 axios\n\n参考答案\n\n::: details\n\n文件上传主要有以下几种方式：\n\n使用 FormData\n\n```js\n// HTML\n<input type=\"file\" id=\"file\">\n\n// fetch\nconst file = document.querySelector('#file').files[0]\nconst formData = new FormData()\nformData.append('file', file)\n\nfetch('/upload', {\n    method: 'POST',\n    body: formData\n})\n\n// axios\nconst formData = new FormData()\nformData.append('file', file)\n\naxios.post('/upload', formData, {\n    headers: {\n        'Content-Type': 'multipart/form-data'\n    }\n})\n```\n\n使用 Base64\n\n```js\n// 将文件转为 Base64\nfunction fileToBase64(file) {\n  return new Promise((resolve) => {\n    const reader = new FileReader()\n    reader.onload = () => resolve(reader.result)\n    reader.readAsDataURL(file)\n  })\n}\n\n// fetch\nconst base64 = await fileToBase64(file)\nfetch('/upload', {\n  method: 'POST',\n  body: JSON.stringify({ file: base64 }),\n  headers: {\n    'Content-Type': 'application/json',\n  },\n})\n\n// axios\nconst base64 = await fileToBase64(file)\naxios.post('/upload', {\n  file: base64,\n})\n```\n\n多文件上传\n\n```js\n// HTML\n<input type=\"file\" multiple id=\"files\">\n\n// fetch\nconst files = document.querySelector('#files').files\nconst formData = new FormData()\nArray.from(files).forEach(file => {\n    formData.append('files', file)\n})\n\nfetch('/upload', {\n    method: 'POST',\n    body: formData\n})\n\n// axios\nconst formData = new FormData()\nArray.from(files).forEach(file => {\n    formData.append('files', file)\n})\n\naxios.post('/upload', formData)\n```\n\n注意事项：\n\n- 设置正确的 Content-Type\n- 考虑文件大小限制\n- 添加上传进度显示\n- 处理上传错误\n- 考虑文件类型限制\n- 添加取消上传功能\n\n:::\n\n## 如何上传大文件？\n\n参考答案\n\n::: details\n\n大文件上传主要有以下几种方案：\n\n切片上传\n\n- 将大文件分割成小块\n- 并发上传多个切片\n- 服务端合并所有切片\n- 支持断点续传和进度显示\n\n实现步骤：\n前端切片\n\n```js\nfunction createFileChunk(file, size = 1 * 1024 * 1024) {\n  const chunks = []\n  let cur = 0\n  while (cur < file.size) {\n    chunks.push(file.slice(cur, cur + size))\n    cur += size\n  }\n  return chunks\n}\n```\n\n上传切片\n\n```js\nasync function uploadChunks(chunks) {\n  const requests = chunks.map((chunk, index) => {\n    const formData = new FormData()\n    formData.append('chunk', chunk)\n    formData.append('index', index)\n    return axios.post('/upload', formData)\n  })\n  await Promise.all(requests)\n}\n```\n\n发送合并请求\n\n```js\nawait axios.post('/merge', {\n  filename: file.name,\n  size: chunks.length,\n})\n```\n\n断点续传\n\n- 记录已上传的切片\n- 重新上传时跳过已上传的部分\n- 可以通过 localStorage 存储进度\n- 使用 hash 标识文件和切片\n\n秒传\n\n- 上传前先发送文件 hash\n- 服务端存在相同文件则直接返回\n- 可以使用 spark-md5 计算文件 hash\n\n性能优化\n\n- 并发控制：限制同时上传的切片数\n- 切片大小：根据网络状况动态调整\n- 进度显示：计算整体上传进度\n- 错误重试：单个切片上传失败后重试\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7356817667574136884\n\n:::\n\n## 如何实现图片懒加载？\n\n参考答案\n\n::: details\n\nIntersectionObserver API\n\n```js\nconst io = new IntersectionObserver((entries) => {\n  entries.forEach((entry) => {\n    if (entry.isIntersecting) {\n      entry.target.src = entry.target.dataset.original\n      entry.target.removeAttribute('data-original')\n      io.unobserve(entry.target)\n    }\n  })\n})\nconst imgs = document.querySelectorAll('img[data-original]')\nimgs.forEach((item) => {\n  io.observe(item)\n})\n```\n\n原生 loading 属性\n\n```js\nlet viewHeight = window.innerHeight\nfunction lazyLoad() {\n  let imgs = document.querySelectorAll('img[data-original]')\n  imgs.forEach((el) => {\n    let rect = el.getBoundingClientRect()\n    if (rect.top < viewHeight) {\n      let image = new Image()\n      image.src = el.dataset.original\n      image.onload = function () {\n        el.src = image.src\n      }\n      el.removeAttribute('data-original')\n    }\n  })\n}\nlazyLoad() // 页面初始加载时调用一次\ndocument.addEventListener('scroll', lazyLoad)\n```\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7320513026188116003\n\n:::\n\n## 在网络层面可做哪些性能优化？\n\n参考答案\n\n::: details\n\n网络性能优化可以从以下几个方面考虑：\n\n减少请求数量\n\n- 合并文件（CSS/JS 打包）\n- 雪碧图（CSS Sprites）\n- 图片懒加载\n- 按需加载/异步加载\n- 合理使用缓存\n\n减小资源体积\n\n- 代码压缩（minify）\n- Gzip/Brotli 压缩\n- 图片优化（压缩、webp格式）\n- Tree Shaking\n- 代码分割（Code Splitting）\n\nCDN 优化\n\n- 使用 CDN 分发静态资源\n- 合理设置 CDN 缓存\n- 选择合适的 CDN 节点\n- 配置 CDN 预热和刷新策略\n\nHTTP 优化\n\n- 使用 HTTP/2 多路复用\n- 开启 Keep-Alive\n- 合理设置缓存策略\n- DNS 预解析（dns-prefetch）\n- 预连接（preconnect）\n- 预加载（prefetch/preload）\n\n资源加载优化\n\n- 关键资源优先加载\n- 非关键资源延迟加载\n- 内联关键 CSS/JS\n- 异步加载非关键 JS（async/defer）\n- 优化资源加载顺序\n\n接口优化\n\n- 接口合并\n- GraphQL 按需查询\n- 数据缓存\n- 避免重复请求\n- 设置合理的超时时间\n\n监控和分析\n\n- 性能监控\n- 错误监控\n- 用户体验监控\n- 性能数据分析\n- 持续优化\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7362080157237116978\n\n:::\n"
  },
  {
    "path": "docs/first-exam/JS.md",
    "content": "# JS 基础知识\n\nJS 是前端开发的核心能力，面试重点考察，无论工作经验长短。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 了解哪些最新的 ES 新特性？\n\n参考答案\n\n::: details\n\n**特性 1: ES2024 的 JSON 模块**\n\n支持直接通过 `import` 语法加载 JSON 文件，避免额外的文件读取逻辑。\n\n```js\nimport config from './config.json' with { type: 'json' }\n\nconsole.log(config.setting) // 输出 JSON 文件中的指定属性\n```\n\n::: tip\n在 Node.js v18.20+ 和 v20+ 版本中，JSON 模块导入语法已从 `assert { type: 'json' }` 更新为 `with { type: 'json' }`，`assert` 语法已被弃用。\n:::\n\n**特性 2: ES2023 的 Array.prototype.findLast & Array.prototype.findLastIndex**\n\n两个数组新方法，用于从最后一个元素搜索数组元素。它们的功能与 `find() 和 findIndex()` 类似，但搜索从数组末尾开始。\n\n这些方法可在 `Array 和 TypedArray` 原型上使用。此功能通过消除手动数组反转的过程，为逆序搜索提供了一种有效的方法。\n\n```js\nconst isOdd = (number) => number % 2 === 1\nconst numbers = [1, 2, 3, 4, 5]\n\nconsole.log(numbers.findLast(isOdd)) // 5\nconsole.log(numbers.findLastIndex(isOdd)) // 4\n```\n\n**特性 3: ES2022 的类字段与私有方法**\n\n支持类中的私有字段 `（#field）` 和私有方法，增强了封装性。\n\n```js\nclass Counter {\n  #count = 0\n\n  increment() {\n    this.#count++\n  }\n\n  #logCount() {\n    console.log(this.#count)\n  }\n}\n\nconst counter = new Counter()\ncounter.increment()\n// counter.#logCount(); // 报错，私有方法不可访问\n```\n\n**特性 4: ES2021 的逻辑赋值运算符**\n\n新增 `&&=, ||=, ??=`，简化条件赋值逻辑。\n\n```js\nlet user = { name: 'Alice', age: null }\n\nuser.name ||= 'Default Name' // 如果 name 为 falsy，则赋值\nuser.age ??= 18 // 如果 age 为 null 或 undefined，则赋值\n\nconsole.log(user) // { name: 'Alice', age: 18 }\n```\n\n**特性 5: ES2020 的可选链和空值合并操作符**\n\n简化深层嵌套对象属性的访问，并安全处理空值。\n\n```js\nconst user = {\n  profile: {\n    details: { name: 'Alice' },\n  },\n}\n\nconst name = user.profile?.details?.name ?? 'Anonymous'\nconsole.log(name) // 输出 'Alice'\n\nconst age = user.profile?.age ?? 18\nconsole.log(age) // 输出 18\n```\n\n**特性 6: ES2019 的数组 flat 和 flatMap 方法**\n\nflat 展开多层嵌套数组，flatMap 结合映射与扁平化操作。\n\n```js\nconst nestedArray = [1, [2, [3, 4]], 5]\nconsole.log(nestedArray.flat(2)) // [1, 2, 3, 4, 5]\n\nconst strings = ['hello', 'world']\nconsole.log(strings.flatMap((str) => str.split('')))\n// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']\n```\n\n:::\n\n参考文档\n\n::: details\n\n- https://juejin.cn/post/7459351912133132351\n\n:::\n\n## `typeof` 能判断哪些类型\n\n参考答案\n\n::: details\n\n| **类型**                | **返回值**    | **备注**                                               |\n| ----------------------- | ------------- | ------------------------------------------------------ |\n| **Undefined**           | `\"undefined\"` | 当变量未被定义或未赋值时，返回此值。                   |\n| **Null**                | `\"object\"`    | 历史遗留问题，`null` 被错误地识别为对象。              |\n| **Boolean**             | `\"boolean\"`   | 适用于 `true` 或 `false` 值。                          |\n| **Number**              | `\"number\"`    | 适用于整数和浮点数（包括特殊值 `NaN` 和 `Infinity`）。 |\n| **String**              | `\"string\"`    | 适用于字符串（例如 `\"hello\"`）。                       |\n| **BigInt**              | `\"bigint\"`    | 适用于任意大的整数（例如 `10n`）。                     |\n| **Symbol**              | `\"symbol\"`    | 适用于 `Symbol` 类型。                                 |\n| **Function（classes）** | `\"function\"`  | 适用于可调用的对象（如函数和类定义）。                 |\n| **其他对象**            | `\"object\"`    | 包括数组、普通对象、日期对象、正则表达式等非函数对象。 |\n\n**注意：**\n\n1. **`typeof null === \"object\"`**\n   在 JavaScript 最初的实现中，JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针（大多数平台下值为 0x00），因此，null 的类型标签是 0，typeof null 也因此返回 \"object\"\n\n2. **实际使用**\n   对于更复杂的类型检测，可以使用工具函数，如 `Object.prototype.toString.call()` 或第三方库（如 `lodash`）。\n\n```js\n// 数值\ntypeof 37 === 'number'\ntypeof 3.14 === 'number'\ntypeof 42 === 'number'\ntypeof Math.LN2 === 'number'\ntypeof Infinity === 'number'\ntypeof NaN === 'number' // 尽管它是 \"Not-A-Number\" (非数值) 的缩写\ntypeof Number(1) === 'number' // Number 会尝试把参数解析成数值\ntypeof Number('shoe') === 'number' // 包括不能将类型强制转换为数字的值\n\ntypeof 42n === 'bigint'\n\n// 字符串\ntypeof '' === 'string'\ntypeof 'bla' === 'string'\ntypeof `template literal` === 'string'\ntypeof '1' === 'string' // 注意内容为数字的字符串仍是字符串\ntypeof typeof 1 === 'string' // typeof 总是返回一个字符串\ntypeof String(1) === 'string' // String 将任意值转换为字符串，比 toString 更安全\n\n// 布尔值\ntypeof true === 'boolean'\ntypeof false === 'boolean'\ntypeof Boolean(1) === 'boolean' // Boolean() 会基于参数是真值还是虚值进行转换\ntypeof !!1 === 'boolean' // 两次调用 !（逻辑非）运算符相当于 Boolean()\n\n// Symbols\ntypeof Symbol() === 'symbol'\ntypeof Symbol('foo') === 'symbol'\ntypeof Symbol.iterator === 'symbol'\n\n// Undefined\ntypeof undefined === 'undefined'\ntypeof declaredButUndefinedVariable === 'undefined'\ntypeof undeclaredVariable === 'undefined'\n\n// 对象\ntypeof { a: 1 } === 'object'\n\n// 使用 Array.isArray 或者 Object.prototype.toString.call\n// 区分数组和普通对象\ntypeof [1, 2, 4] === 'object'\n\ntypeof new Date() === 'object'\ntypeof /regex/ === 'object'\n\n// 下面的例子令人迷惑，非常危险，没有用处。避免使用它们。\ntypeof new Boolean(true) === 'object'\ntypeof new Number(1) === 'object'\ntypeof new String('abc') === 'object'\n\n// 函数\ntypeof function () {} === 'function'\ntypeof class C {} === 'function'\ntypeof Math.sin === 'function'\n```\n\n:::\n\n## `==` 和 `===` 有什么区别？\n\n参考答案\n\n::: details\n\n- **`==`（宽松相等）**：会在比较两个操作数时执行 **类型转换**，尝试将两者转换为相同类型后再比较。\n- **`===`（严格相等）**：不会执行类型转换，仅在类型和值完全相同的情况下返回 `true`。\n- **推荐使用 `===`**：因为它更严格、更符合预期，能避免潜在的错误。尤其是在需要精确判断值和类型时。\n- 实际工作中，使用 if (a == null) 可判断 a 是否是 null 或者 undefined。\n\n**常见比较结果**\n\n| **x**               | **y**               | **==** | **===** |\n| ------------------- | ------------------- | ------ | ------- |\n| `undefined`         | `undefined`         | ✅     | ✅      |\n| `null`              | `null`              | ✅     | ✅      |\n| `true`              | `true`              | ✅     | ✅      |\n| `false`             | `false`             | ✅     | ✅      |\n| `'foo'`             | `'foo'`             | ✅     | ✅      |\n| `0`                 | `0`                 | ✅     | ✅      |\n| `+0`                | `-0`                | ✅     | ✅      |\n| `+0`                | `0`                 | ✅     | ✅      |\n| `-0`                | `0`                 | ✅     | ✅      |\n| `0n`                | `-0n`               | ✅     | ✅      |\n| `0`                 | `false`             | ✅     | ❌      |\n| `\"\"`                | `false`             | ✅     | ❌      |\n| `\"\"`                | `0`                 | ✅     | ❌      |\n| `'0'`               | `0`                 | ✅     | ❌      |\n| `'17'`              | `17`                | ✅     | ❌      |\n| `[1, 2]`            | `'1,2'`             | ✅     | ❌      |\n| `new String('foo')` | `'foo'`             | ✅     | ❌      |\n| `null`              | `undefined`         | ✅     | ❌      |\n| `null`              | `false`             | ❌     | ❌      |\n| `undefined`         | `false`             | ❌     | ❌      |\n| `{ foo: 'bar' }`    | `{ foo: 'bar' }`    | ❌     | ❌      |\n| `new String('foo')` | `new String('foo')` | ❌     | ❌      |\n| `0`                 | `null`              | ❌     | ❌      |\n| `0`                 | `NaN`               | ❌     | ❌      |\n| `'foo'`             | `NaN`               | ❌     | ❌      |\n| `NaN`               | `NaN`               | ❌     | ❌      |\n\n说明：\n\n- ✅ 表示比较结果为 `true`\n- ❌ 表示比较结果为 `false`\n\n:::\n\n## 你熟悉哪些数组 API ？\n\n参考答案\n\n::: details\n\n1. **创建数组**\n   - `Array()`, `Array.of()`, `Array.from()`\n\n```js\nArray.of(1, 2, 3) // [1, 2, 3]\nArray.from('123') // ['1', '2', '3']\n```\n\n2. **添加/删除元素**\n   - `push()`: 在末尾添加\n   - `pop()`: 删除末尾\n   - `unshift()`: 在开头添加\n   - `shift()`: 删除开头\n\n```js\nlet arr = [1, 2]\narr.push(3) // [1, 2, 3]\narr.pop() // [1, 2]\narr.unshift(4) // [4, 1, 2]\narr.shift() // [1, 2]\n```\n\n3. **组合/拆分数组**\n   - `concat()`: 合并数组，不影响原数组，浅拷贝\n   - `join()`: 将数组连接为字符串\n   - `slice()`: 截取部分数组（不修改原数组）\n\n```js\n;[1, 2].concat([3, 4]) // [1, 2, 3, 4]\n;['a', 'b', 'c'].join('-') // 'a-b-c'\n```\n\n4. **替换/重组**\n   - `splice()`: 添加、删除或替换元素\n\n```js\nlet arr = [1, 2, 3]\narr.splice(1, 1, 'a') // [1, 'a', 3]\n```\n\n5. **查找单个元素**\n   - `indexOf()`: 查找首次出现的索引\n   - `lastIndexOf()`: 查找最后出现的索引\n   - `find()`: 找到第一个满足条件的元素\n   - `findIndex()`: 找到第一个满足条件的索引\n\n```js\n;[1, 2, 3].indexOf(2) // 1\n;[1, 2, 3, 2].lastIndexOf(2) // 3\n;[1, 2, 3].find((x) => x > 2) // 3\n```\n\n6. **判断**\n   - `includes()`: 判断是否包含某元素\n   - `some()`: 判断是否至少有一个元素满足条件\n   - `every()`: 判断是否所有元素满足条件\n\n```js\n;[1, 2, 3].includes(2) // true\n;[1, 2, 3].some((x) => x > 2) // true\n;[1, 2, 3].every((x) => x > 0) // true\n```\n\n7. **迭代**\n   - `forEach()`: 遍历元素，无法 break，可以用 try/catch 中 throw new Error 来停止\n\n```js\n;[1, 2, 3].forEach((item, index) => console.log(item, index))\n```\n\n8. **映射/变换**\n   - `map()`: 对每个元素进行操作并生成新数组\n\n```javascript\n;[1, 2, 3].map((x) => x * 2) // [2, 4, 6]\n```\n\n9. **过滤**\n   - `filter()`: 筛选出满足条件的元素\n\n```js\n;[1, 2, 3].filter((x) => x > 1) // [2, 3]\n```\n\n10. **规约**\n\n- `reduce()`: 将数组缩减为单一值\n- `reduceRight()`: 从右向左缩减\n\n```js\n;[1, 2, 3].reduce((acc, val) => acc + val, 0) // 6\n;['a', 'b', 'c'].reduceRight((acc, val) => acc + val, '') // 'cba'\n```\n\n11. **排序**\n\n- `sort()`: 对数组进行排序\n- `reverse()`: 反转数组顺序\n\n```js\n;[3, 1, 2].sort((a, b) => a - b) // [1, 2, 3]\n;[1, 2, 3].reverse() // [3, 2, 1]\n```\n\n12. **填充**\n\n- `fill()`: 用指定值填充数组\n\n```js\nnew Array(3).fill(0) // [0, 0, 0]\n```\n\n13. **扁平化**\n\n- `flat()`: 将多维数组展平成一维\n- `flatMap()`: 映射并展平\n\n```js\n;[1, [2, [3]]].flat(2) // [1, 2, 3]\n;[1, 2].flatMap((x) => [x, x * 2]) // [1, 2, 2, 4]\n```\n\n14. **复制/填充**\n\n- `copyWithin()`: 将数组的部分内容复制到其他位置\n\n```js\n;[1, 2, 3, 4].copyWithin(1, 2) // [1, 3, 4, 4]\n```\n\n15. **生成键值对**\n\n- `keys()`, `values()`, `entries()`\n\n```js\nconst arr = ['a', 'b', 'c']\n;[...arr.keys()] // [0, 1, 2]\n;[...arr.entries()] // [[0, 'a'], [1, 'b'], [2, 'c']]\n```\n\n16. **判断是否是数组**\n\n- `Array.isArray()`\n\n```js\nArray.isArray([1, 2, 3]) // true\n```\n\n:::\n\n## 值类型和引用类型的区别\n\n```js\n// 值类型\nlet a = 100\nlet b = a\na = 200\nconsole.log(b) // 100\n```\n\n```js\n// 引用类型\nlet a = { age: 20 }\nlet b = a\nb.age = 21\nconsole.log(a.age) // 21\n```\n\n参考答案\n\n::: details\n\n| 特性               | 值类型                                                                | 引用类型                                     |\n| ------------------ | --------------------------------------------------------------------- | -------------------------------------------- |\n| **存储内容**       | 数据值本身                                                            | 数据的引用（地址）                           |\n| **存储位置**       | 栈内存                                                                | 栈存引用，堆存实际数据                       |\n| **赋值方式**       | 拷贝值                                                                | 拷贝引用（地址）                             |\n| **变量之间独立性** | 互相独立，互不影响                                                    | 指向同一数据，互相影响                       |\n| **常见数据类型**   | 基本数据类型（如 `number，string，boolean，undefined，null，symbol`） | 复杂数据类型（如 `Object，Array，Function`） |\n\n1. 为什么有值类型和引用类型？\n\n- **值类型**适合存储简单、占用内存较小的数据，操作快速。\n- **引用类型**适合存储复杂、占用内存较大的数据，支持动态扩展。\n\n2. 如何避免引用类型的共享问题？\n\n- 如果需要创建引用类型的副本，使用深拷贝，而非浅拷贝。\n\n深拷贝例子：\n\n```javascript\nconst obj1 = { name: 'Alice' }\nconst obj2 = JSON.parse(JSON.stringify(obj1)) // 创建深拷贝\nobj2.name = 'Bob'\nconsole.log(obj1.name) // \"Alice\"\n```\n\n浅拷贝例子：\n\n```javascript\nconst obj1 = { name: 'Alice' }\nconst obj2 = { ...obj1 } // 浅拷贝\nobj2.name = 'Bob'\nconsole.log(obj1.name) // \"Alice\"\n```\n\n:::\n\n## 箭头函数和普通函数的区别\n\n参考答案\n\n::: details\n\n| 特性                       | 箭头函数                                       | 普通函数                              |\n| -------------------------- | ---------------------------------------------- | ------------------------------------- |\n| 语法                       | 简洁，使用 `=>` 定义                           | 使用 `function` 定义                  |\n| `this` 绑定                | 词法绑定，继承外层 `this`                      | 动态绑定，调用时决定                  |\n| `arguments` 对象           | 没有，需要使用 `...args`                       | 有自己的 `arguments` 对象             |\n| 是否能作为构造函数         | 不能                                           | 可以                                  |\n| 是否有 `prototype` 属性    | 没有                                           | 有                                    |\n| 是否支持 `bind/call/apply` | 不支持                                         | 支持                                  |\n| 适用场景                   | 用于回调函数、闭包、需要继承外层 `this` 的场景 | 需要动态绑定 `this`，或用作构造函数时 |\n\n```js\n// 箭头函数 this\nconst obj = {\n  name: 'Alice',\n  say: () => {\n    console.log(this.name) // undefined (继承全局作用域的 this)\n  },\n}\nobj.say()\n\n// 普通函数 this\nconst obj = {\n  name: 'Alice',\n  say: function () {\n    console.log(this.name) // \"Alice\" (this 指向 obj)\n  },\n}\nobj.say()\n\n// 箭头函数 不能作为构造函数\nconst Person = (name) => {\n  this.name = name\n}\nconst p = new Person('Alice') // TypeError: Person is not a constructor\n\n// 普通函数 构造函数\nfunction Person(name) {\n  this.name = name\n}\nconst p = new Person('Alice')\nconsole.log(p.name) // \"Alice\"\n\n// 箭头函数 ...args\nconst add = (...args) => {\n  console.log(args) // [1, 2, 3]\n}\nadd(1, 2, 3)\n\n// 普通函数 arguments\nfunction add() {\n  console.log(arguments) // Arguments(3) [1, 2, 3]\n}\nadd(1, 2, 3)\n\n// 箭头函数 不支持 `bind/call/apply`\nconst obj = {\n  value: 42,\n}\nconst arrowFn = () => {\n  console.log(this.value)\n}\narrowFn.call(obj) // undefined\n\n// 普通函数 支持 `bind/call/apply`\nconst obj = {\n  value: 42,\n}\nfunction normalFn() {\n  console.log(this.value)\n}\nnormalFn.call(obj) // 42\n```\n\n:::\n\n## 什么时候不能使用箭头函数\n\n参考答案\n\n::: details\n\n1. 需要动态绑定 `this` 的场景。\n2. 作为`构造函数`。\n3. 需要 `arguments` 对象的场景。\n4. 需要显式修改 `this` 的场景（使用 `bind/call/apply` 等）。\n5. 类的实例方法（特别是 `getter 和 setter`）。—— 无法动态绑定 `this`\n\n:::\n\n## for...in 和 for...of 的区别\n\n参考答案\n\n::: details\n\n| 特性               | `for...in`                   | `for...of`                              |\n| ------------------ | ---------------------------- | --------------------------------------- |\n| **用途**           | 遍历对象的 **可枚举属性**    | 遍历 **可迭代对象**（如数组、字符串等） |\n| **返回值**         | 返回 **键**（属性名）        | 返回 **值**（元素值）                   |\n| **适用范围**       | 对象、数组（不推荐用于数组） | 数组、字符串、Set、Map等可迭代对象      |\n| **是否遍历原型链** | 会遍历原型链上的可枚举属性   | 不会遍历原型链                          |\n\n```javascript\n// for...in 遍历对象\nconst obj = { name: 'Alice', age: 25 }\n\nfor (let key in obj) {\n  console.log(key) // 输出属性名：name, age\n  console.log(obj[key]) // 输出属性值：Alice, 25\n}\n\n// for...in 遍历数组，不推荐\nconst arr = [10, 20, 30]\n\nfor (let index in arr) {\n  console.log(index) // 输出索引：0, 1, 2\n  console.log(arr[index]) // 输出值：10, 20, 30\n}\n\n// for...of 遍历数组\nconst arr = [10, 20, 30]\n\nfor (let value of arr) {\n  console.log(value) // 输出值：10, 20, 30\n}\n```\n\n:::\n\n## JS 原型和原型链\n\n参考答案\n\n::: details\n\n![proto](../imgs/proto.jpg)\n\n**1. 原型（Prototype）**\n\n- 每个 **函数**（构造函数）都有一个 `prototype` 属性，指向其 **原型对象**。\n- 每个 **对象** 都有一个 `__proto__` 指向其构造函数的 `prototype`，形成继承关系。\n\n**2. 原型链（Prototype Chain）**\n\n- 访问对象属性时，先查找自身属性，找不到则沿 `__proto__` 逐级向上查找，直到 `null` 终止。\n- `Object.prototype.__proto__ === null`，原型链的顶端是 `Object.prototype`。\n\n```js\nfunction Person(name) {\n  this.name = name\n}\nPerson.prototype.sayHello = function () {\n  console.log('Hello!')\n}\n\nconst p = new Person('Rain')\nconsole.log(p.__proto__ === Person.prototype) // true\nconsole.log(Person.prototype.__proto__ === Object.prototype) // true\nconsole.log(Object.prototype.__proto__ === null) // true\n```\n\n:::\n\n## JS 继承有几种方式？\n\n参考答案\n\n::: details\n\n**1. 原型链继承**\n\n**核心思路：** 让子类的 `prototype` 指向父类实例。\n\n```js\nfunction Parent() {\n  this.name = 'Parent'\n}\nParent.prototype.sayHello = function () {\n  console.log('Hello from Parent')\n}\n\nfunction Child() {}\nChild.prototype = new Parent() // 继承 Parent\nChild.prototype.constructor = Child\n\nconst child = new Child()\nconsole.log(child.name) // \"Parent\"\nchild.sayHello() // \"Hello from Parent\"\n```\n\n✅ **优点：** 父类方法可复用\n❌ **缺点：** 1. 共享引用类型属性（如 `arr = []` 会被多个实例共享），2. 无法向父类构造函数传参\n\n**2. 借用构造函数继承**\n\n**核心思路：** 在子类构造函数中使用 `call` 继承父类属性。\n\n```js\nfunction Parent(name) {\n  this.name = name\n}\nfunction Child(name, age) {\n  Parent.call(this, name) // 继承 Parent\n  this.age = age\n}\nconst child = new Child('Rain', 18)\nconsole.log(child.name, child.age) // \"Rain\", 18\n```\n\n✅ **优点：** 1. 解决原型链继承共享问题，2. 可传参\n❌ **缺点：** 无法继承父类原型上的方法\n\n**3. 组合继承（原型链 + 构造函数继承，最常用）**\n\n**核心思路：** 结合前两种方式，**继承属性用构造函数，继承方法用原型链**。\n\n```js\nfunction Parent(name) {\n  this.name = name\n}\nParent.prototype.sayHello = function () {\n  console.log('Hello from Parent')\n}\n\nfunction Child(name, age) {\n  Parent.call(this, name) // 第 1 次调用 Parent\n  this.age = age\n}\n\nChild.prototype = new Parent() // 第 2 次调用 Parent\nChild.prototype.constructor = Child\n\nconst child = new Child('Rain', 18)\nconsole.log(child.name, child.age) // \"Rain\", 18\nchild.sayHello() // \"Hello from Parent\"\n```\n\n✅ **优点：** 解决了前两种方法的缺陷\n❌ **缺点：** 调用两次 `Parent` 构造函数（一次 `call`，一次 `Object.create()`）\n\n**4. Object.create() 继承（原型式继承）**\n\n**核心思路：** 直接用 `Object.create()` 创建一个新对象，继承已有对象。\n\n```js\nconst parent = {\n  name: 'Parent',\n  sayHello() {\n    console.log('Hello!')\n  },\n}\nconst child = Object.create(parent)\nchild.age = 18\nconsole.log(child.name, child.age) // \"Parent\", 18\nchild.sayHello() // \"Hello!\"\n```\n\n✅ **优点：** 适合创建对象而非类的继承\n❌ **缺点：** 不能传参，只适用于简单继承\n\n**5. 寄生组合继承（优化版，推荐）**\n\n**核心思路：** **组合继承的优化版本**，避免了 `Parent` 被调用两次的问题。\n\n```js\nfunction Parent(name) {\n  this.name = name\n}\nParent.prototype.sayHello = function () {\n  console.log('Hello from Parent')\n}\n\nfunction Child(name, age) {\n  Parent.call(this, name)\n  this.age = age\n}\nChild.prototype = Object.create(Parent.prototype) // 关键优化\nChild.prototype.constructor = Child\n\nconst child = new Child('Rain', 18)\nconsole.log(child.name, child.age) // \"Rain\", 18\nchild.sayHello() // \"Hello from Parent\"\n```\n\n✅ **优点：** 1. 继承属性和方法，2. 只调用一次 `Parent`\n❌ **缺点：** 代码略微复杂\n\n**6. ES6 class 继承（最现代化的方式）**\n\n**核心思路：** `class` 语法糖，实际仍然基于原型继承。\n\n```js\nclass Parent {\n  constructor(name) {\n    this.name = name\n  }\n  sayHello() {\n    console.log('Hello from Parent')\n  }\n}\n\nclass Child extends Parent {\n  constructor(name, age) {\n    super(name) // 继承属性\n    this.age = age\n  }\n}\n\nconst child = new Child('Rain', 18)\nconsole.log(child.name, child.age) // \"Rain\", 18\nchild.sayHello() // \"Hello from Parent\"\n```\n\n✅ **优点：** 语法更清晰，易读易用\n❌ **缺点：** 本质仍是 `prototype` 继承\n\n:::\n\n## JS 作用域和作用域链\n\n参考答案\n\n::: details\n\n- **作用域**：变量的可访问范围，分为 **全局作用域、函数作用域、块级作用域**。\n- **作用域链**：变量查找机制，从当前作用域 **逐级向上查找**，直到全局作用域或 `ReferenceError`。\n- **ES6 关键点**：\n  - `let` / `const` **具有块级作用域**，避免 `var` 变量提升带来的问题。\n  - **闭包** 利用作用域链，保留外部作用域的变量。\n\n```js\nvar a = 'global'\n\nfunction outer() {\n  var b = 'outer'\n\n  function inner() {\n    var c = 'inner'\n    console.log(a, b, c) // ✅ global outer inner\n  }\n\n  inner()\n}\n\nouter()\nconsole.log(b) // ❌ ReferenceError: b is not defined\n```\n\n:::\n\n## JS 自由变量，如何理解\n\n参考答案\n\n::: details\n**自由变量** 指的是 **在当前作用域中未声明，但在上层作用域中找到的变量**。\n\n在 JavaScript 中，当代码执行时，如果遇到一个变量：\n\n- **当前作用域** 找不到该变量，就会沿着 **作用域链** 向上查找，直到找到该变量或报 `ReferenceError`。\n- **这个在外层作用域中找到的变量，就是自由变量。**\n\n```js\nvar a = 10 // 全局变量（自由变量）\n\nfunction foo() {\n  console.log(a) // 访问自由变量 a\n}\n\nfoo() // 10\n```\n\n:::\n\n## JS 闭包，如何理解\n\n参考答案\n\n::: details\n**闭包的核心特性：**\n\n1. 访问外部函数作用域的变量\n2. 即使外部函数执行结束，变量依然被保留\n3. 不会被垃圾回收，直到闭包不再被引用\n\n**闭包的应用场景：**\n\n1. 私有变量（模拟封装）\n\n```js\nfunction createCounter() {\n  let count = 0 // 私有变量，外部无法直接访问\n  return {\n    increment: () => ++count,\n    decrement: () => --count,\n    getCount: () => count,\n  }\n}\n\nconst counter = createCounter()\nconsole.log(counter.increment()) // 1\nconsole.log(counter.increment()) // 2\nconsole.log(counter.getCount()) // 2\nconsole.log(counter.count) // undefined（外部无法直接访问）\n```\n\n2. 回调 & 事件监听\n\n```js\nfunction addEventLogger(eventName) {\n  return function () {\n    console.log(`Event ${eventName} triggered!`)\n  }\n}\n\ndocument.addEventListener('click', addEventLogger('click'))\n```\n\n3. 定时器 & 异步操作\n\n```js\nfunction delayedGreeting(name) {\n  setTimeout(() => {\n    console.log(`Hello, ${name}!`)\n  }, 2000)\n}\n\ndelayedGreeting('Rain') // 2 秒后打印 \"Hello, Rain!\"\n```\n\n**闭包的缺点：**\n\n1. 可能导致内存泄漏\n\n- 闭包会持有外部变量的引用，导致变量无法被垃圾回收\n- 解决方案：手动将变量置为 null 或谨慎管理作用域\n\n2. 滥用闭包可能影响性能\n\n- 每次调用都会创建新的作用域，影响垃圾回收机制\n- 适度使用，避免不必要的闭包\n\n:::\n\n## 同步和异步有什么区别？异步的意义是什么？\n\n参考答案\n\n::: details\n**同步**：任务按顺序执行，当前任务未完成时，后续代码必须等待，代码是**阻塞**的。\n**异步**：任务可以**不按顺序执行**，不会阻塞代码，后续代码可以继续执行，代码是**非阻塞**的。\n\n| 特性         | **同步**                         | **异步**                     |\n| ------------ | -------------------------------- | ---------------------------- |\n| **执行方式** | 顺序执行，阻塞后续任务           | 非阻塞，任务可以并行执行     |\n| **代码特点** | **阻塞**，必须等待上一个任务完成 | **非阻塞**，任务可以同时进行 |\n| **适用场景** | 计算密集型、简单逻辑处理         | 网络请求、I/O 操作、高并发   |\n\n```js\n// 同步\nconsole.log('任务 1')\nalert('等待用户操作...')\nconsole.log('任务 2') // 只有用户关闭 alert，任务 2 才能执行\n```\n\n```js\n// 异步\nconsole.log('任务 1')\n\nsetTimeout(() => {\n  console.log('任务 2（延迟 2 秒）')\n}, 2000)\n\nconsole.log('任务 3') // 任务 3 不会等待 任务 2\n// 任务 1\n// 任务 3\n// （2 秒后）\n// 任务 2（延迟 2 秒）\n```\n\n**为什么要用异步？（异步的意义）**\n\n1. 避免阻塞，提升用户体验\n\n- 异步任务（如网络请求、文件读写）可以在后台执行，避免阻塞 UI，保证页面流畅。\n\n2. 提升系统性能，支持高并发\n\n- 服务器可以同时处理多个请求，提高吞吐量（如 Node.js 处理高并发）。\n\n3. 更适合现代 Web 开发\n\n- `Promise` / `async-await` 让异步代码更可读，配合 `fetch` 进行网络请求，提升开发效率。\n\n:::\n\n## JS Promise 有几种状态？如何变化\n\n参考答案\n\n::: details\n\n**1. Promise 有几种状态？**\n\n| 状态                    | 说明                          | 是否可变更  |\n| ----------------------- | ----------------------------- | ----------- |\n| **Pending（进行中）**   | 初始状态，异步操作未完成      | ✅ 可以变更 |\n| **Fulfilled（已完成）** | 操作成功，返回 `resolve` 结果 | ❌ 变更结束 |\n| **Rejected（已拒绝）**  | 操作失败，返回 `reject` 错误  | ❌ 变更结束 |\n\n**2. Promise 状态如何变化？**\n\nPromise 的状态**只会从 `Pending` → `Fulfilled` 或 `Pending` → `Rejected`**，且**一旦变化就不会再改变**（不可逆）。\n\n```js\nconst promise = new Promise((resolve, reject) => {\n  setTimeout(() => {\n    resolve('成功')\n    // reject(\"失败\"); // 只会触发一次，状态不可逆\n  }, 1000)\n})\n\npromise.then((result) => console.log('Fulfilled:', result)).catch((error) => console.log('Rejected:', error))\n```\n\n:::\n\n## JS Promise 使用\n\n参考答案\n\n::: details\n**1. 什么是 Promise？**\n\n> **Promise 是 JavaScript 处理异步操作的一种方式**，用于解决回调地狱（Callback Hell）问题。\n> 它表示一个未来才会完成（或失败）的异步操作，并提供 `.then()`、`.catch()`、`.finally()` 方法进行处理。\n\n**2. Promise 的基本用法**\n\n**创建一个 Promise**\n\n```js\nconst myPromise = new Promise((resolve, reject) => {\n  setTimeout(() => {\n    let success = true\n    success ? resolve('操作成功') : reject('操作失败')\n  }, 1000)\n})\n```\n\n**使用 `then`、`catch` 处理结果**\n\n```js\nmyPromise\n  .then((result) => console.log('成功:', result)) // 处理成功\n  .catch((error) => console.log('失败:', error)) // 处理失败\n  .finally(() => console.log('操作结束')) // 无论成功或失败都会执行\n```\n\n**3. Promise 串行执行**\n\n**多个异步操作依次执行（避免回调地狱）**\n\n```js\nfunction step1() {\n  return new Promise((resolve) => setTimeout(() => resolve('Step 1 完成'), 1000))\n}\nfunction step2() {\n  return new Promise((resolve) => setTimeout(() => resolve('Step 2 完成'), 1000))\n}\n\nstep1()\n  .then((result) => {\n    console.log(result)\n    return step2() // 返回 Promise\n  })\n  .then((result) => console.log(result))\n  .catch((error) => console.error('错误:', error))\n```\n\n**4. Promise 并行执行**\n\n**多个异步任务同时执行，全部完成后再处理**\n\n```js\nconst p1 = new Promise((resolve) => setTimeout(() => resolve('任务 1'), 1000))\nconst p2 = new Promise((resolve) => setTimeout(() => resolve('任务 2'), 1500))\n\nPromise.all([p1, p2])\n  .then((results) => console.log('所有任务完成:', results))\n  .catch((error) => console.error('任务失败:', error))\n```\n\n**如果只要最快完成的结果**\n\n```js\nPromise.race([p1, p2])\n  .then((result) => console.log('最先完成的:', result))\n  .catch((error) => console.error('失败:', error))\n```\n\n**5. 面试回答总结**\n\n> **Promise 解决异步回调问题，提供 `.then()`、`.catch()`、`.finally()` 处理状态变化。支持 `Promise.all()` 并行执行，`Promise.race()` 竞争执行。用 `async/await` 可以让异步代码更清晰。**\n\n:::\n\n## async/await 使用\n\n参考答案\n\n::: details\n\nasync/await 是 ES2017（ES8）引入的 基于 Promise 的语法糖，用于更清晰地编写异步代码，使其看起来像同步代码，提高可读性。\n\n- async 关键字：用于声明一个异步函数，返回值始终是 Promise。\n- await 关键字：只能在 async 函数中使用，等待 Promise 解析（resolve）并返回结果，而不会阻塞线程。\n\n```js\nasync function fetchData() {\n  try {\n    let response = await fetch('https://api.example.com/data')\n    let data = await response.json()\n    console.log(data)\n  } catch (error) {\n    console.error('Error:', error)\n  }\n}\nfetchData()\n```\n\n:::\n\n## JS 异步执行顺序\n\n执行以下代码，会输出什么？\n\n```js\nasync function async1() {\n  console.log('async1')\n  await async2()\n  console.log('async1 end')\n}\nasync function async2() {\n  console.log('async2')\n}\nconsole.log('script start')\nsetTimeout(() => {\n  console.log('setTimeOut')\n}, 0)\nasync1()\nnew Promise((resolve) => {\n  console.log('promise')\n  resolve()\n}).then(() => {\n  console.log('promise2')\n})\nconsole.log('script end')\n```\n\n答案\n\n::: details\n\n```\nscript start\nasync1\nasync2\npromise\nscript end\nasync1 end\npromise2\nsetTimeOut\n```\n\n:::\n\n## 宏任务和微任务的区别\n\n参考答案\n\n::: details\n在 JavaScript 的 事件循环（Event Loop） 机制中，任务分为 **宏任务（Macro Task）** 和 **微任务（Micro Task）**：\n\n- **微任务优先**：微任务队列会在每次 宏任务执行完毕 后立即执行，保证微任务先执行完再进入下一个宏任务。\n- **宏任务**：常见的宏任务包括 `setTimeout、setInterval、setImmediate（Node.js）、I/O、UI 渲染`。\n- **微任务**：常见的微任务包括 `Promise.then、MutationObserver、queueMicrotask、process.nextTick（Node.js）`。\n\n```js\nconsole.log('start')\n\nsetTimeout(() => {\n  console.log('setTimeout')\n}, 0)\n\nPromise.resolve()\n  .then(() => {\n    console.log('promise1')\n  })\n  .then(() => {\n    console.log('promise2')\n  })\n\nconsole.log('end')\n\n// 输出：\n// start\n// end\n// promise1\n// promise2\n// setTimeout\n```\n\n:::\n\n## 描述 Event Loop 运行机制\n\n参考答案\n\n::: details\n\n![eventloop](../imgs/js-eventloop.png)\n\nEvent Loop（事件循环）是 JavaScript 处理 **异步操作** 的核心机制。它允许 JavaScript 以 **非阻塞** 的方式执行代码，即使遇到 I/O 操作（如网络请求、定时器），也不会影响主线程继续执行其他任务。\n\n**执行流程（核心步骤）**\n\n1. **执行同步任务**\n\n- 所有同步任务在 调用栈（Call Stack） 中依次执行，直到调用栈清空。\n\n2. **处理微任务**\n\n- 检查 微任务队列（MicroTask Queue） 是否有任务（如 Promise.then()、queueMicrotask()）。\n- 依次执行所有微任务，直到微任务队列清空。\n\n3. **执行宏任务**\n\n- 从 宏任务队列（MacroTask Queue） 取出 一个 任务（如 setTimeout 回调、I/O 任务），放入调用栈执行。\n\n4. **重复步骤 2（处理新的微任务）**\n\n- 宏任务执行完毕后，再次检查微任务队列，如果有新产生的微任务，立即执行所有微任务。\n\n5. **重复步骤 3（执行下一个宏任务）**\n\n- 继续取出下一个 宏任务，重复整个过程，形成循环（Event Loop）\n\n:::\n\n## Set 和 Array 有什么区别\n\n参考答案\n\n::: details\n\n| 特性               | **Array**                       | **Set**                        |\n| ------------------ | ------------------------------- | ------------------------------ |\n| **是否允许重复值** | ✅ 允许重复元素                 | ❌ 只能存储唯一值，自动去重    |\n| **索引访问**       | ✅ 可通过索引 (`arr[0]`) 访问   | ❌ 不支持索引访问              |\n| **查找性能**       | 🔴 `O(n)`，需要遍历整个数组     | 🟢 `O(1)`，基于哈希表查找更快  |\n| **删除性能**       | 🔴 `O(n)`，需要遍历查找删除     | 🟢 `O(1)`，删除性能更优        |\n| **遍历方式**       | ✅ `forEach` / `map` / `filter` | ✅ `forEach` / `for...of`      |\n| **适合的场景**     | 存储有序数据，支持索引访问      | 需要唯一值集合，去重、快速查找 |\n| **转换方式**       | `Array.from(set)` (Set → Array) | `new Set(array)` (Array → Set) |\n\n```js\n// Array 允许重复值\nconst arr = [1, 2, 2, 3, 4, 4]\nconsole.log(arr) // [1, 2, 2, 3, 4, 4]\n\n// Set 自动去重\nconst set = new Set(arr)\nconsole.log([...set]) // [1, 2, 3, 4]\n\n// Set 无索引访问\nconsole.log(set[0]) // undefined\n\n// Set 转 Array\nconst arrFromSet = Array.from(set)\nconsole.log(arrFromSet) // [1, 2, 3, 4]\n```\n\n:::\n\n## Map 和 Object 有什么区别\n\n参考答案\n\n::: details\n\n| 特性                      | **Object**                              | **Map**                               |\n| ------------------------- | --------------------------------------- | ------------------------------------- |\n| **键的类型**              | 只能是 `string` 或 `symbol`             | 可以是任何类型（对象、函数等）        |\n| **键值对的存储顺序**      | **无序**（属性顺序可能变化）            | **有序**（插入顺序保持不变）          |\n| **查找性能**              | 相对较慢（基于哈希表）                  | 更快（专门优化的键值存储结构）        |\n| **迭代方式**              | `for...in`，`Object.keys()` 等          | `forEach()`，`for...of`（支持迭代器） |\n| **获取键的方式**          | `Object.keys(obj)` 只能获取 `string` 键 | `map.keys()` 可获取所有类型的键       |\n| **获取大小**              | 需手动计算 `Object.keys(obj).length`    | `map.size` 直接获取大小               |\n| **是否能轻松转换为 JSON** | ✅ 可以 `JSON.stringify()`              | ❌ 不能直接 `JSON.stringify()`        |\n| **适用场景**              | 适用于存储结构化数据，如对象属性        | 适用于 **高效键值存储和查找**         |\n\n```js\n// Object 只能用字符串作为键\nconst obj = {}\nobj['key1'] = 'value1'\nobj[1] = 'value2' // 这里的 1 会被转换为 \"1\"\nconsole.log(obj) // { '1': 'value2', key1: 'value1' }\n\n// Map 可用任何类型作为键\nconst map = new Map()\nmap.set('key1', 'value1')\nmap.set(1, 'value2') // 数字 1 不会被转换为字符串\nconsole.log(map) // Map(2) { 'key1' => 'value1', 1 => 'value2' }\n\n// Object 迭代（无序）\nconsole.log(Object.keys(obj)) // ['1', 'key1']\n\n// Map 迭代（有序）\nconsole.log([...map.keys()]) // ['key1', 1]\n\n// Map 直接获取大小\nconsole.log(map.size) // 2\n\n// Object 需要手动计算大小\nconsole.log(Object.keys(obj).length) // 2\n```\n\n:::\n\n## setTimeout、requestAnimationFrame 和 requestIdleCallback 有什么区别\n\n参考答案\n\n::: details\n\n| 特性                | `setTimeout`                 | `requestAnimationFrame`          | `requestIdleCallback`                              |\n| ------------------- | ---------------------------- | -------------------------------- | -------------------------------------------------- |\n| **执行时机**        | 设定时间后执行（不保证准时） | **下一帧渲染前**（16.6ms 以内）  | **浏览器空闲时**（可能延迟执行）                   |\n| **主要用途**        | 延迟执行代码                 | **动画和流畅渲染**               | **低优先级任务**（如日志、分析）                   |\n| **帧率控制**        | **无**，可能丢帧             | **跟随屏幕刷新率**（一般 60FPS） | **不受限制**，完全取决于浏览器                     |\n| **影响页面性能**    | **可能影响页面流畅度**       | **保证流畅动画**                 | **不会阻塞主线程**                                 |\n| **是否适用于动画**  | ❌ 可能卡顿                  | ✅ 适合                          | ❌ 不适合                                          |\n| **是否受 CPU 影响** | ✅ 受影响                    | ✅ 受影响                        | ✅ 受影响                                          |\n| **适用场景**        | **定时任务、轮询**           | **动画、过渡、流畅 UI 渲染**     | **后台任务、低优先级执行（如数据同步、日志收集）** |\n\n**`setTimeout` - 定时执行**\n\n```js\nsetTimeout(() => {\n  console.log('100ms 后执行')\n}, 100)\n```\n\n**`requestAnimationFrame` - 适用于动画**\n\n```js\nfunction animate() {\n  console.log('下一帧渲染前执行')\n  requestAnimationFrame(animate)\n}\nrequestAnimationFrame(animate)\n```\n\n**`requestIdleCallback` - 空闲时执行**\n\n```js\nrequestIdleCallback((deadline) => {\n  while (deadline.timeRemaining() > 0) {\n    console.log('空闲时执行低优先级任务')\n  }\n})\n```\n\n:::\n\n## 写一个验证 email 的正则表达式\n\n参考答案\n\n::: details\n\n```js\nconst reg = /\\w+((-\\w+)|(\\.\\w+))*@[a-zA-Z0-9]+((\\.|-)[a-zA-Z0-9]+)*\\.[a-zA-Z0-9]+$/\nreg.test(email)\n```\n\n:::\n\n## JS 模块化规范有哪些？\n\n参考答案\n\n::: details\n\n1. **CommonJS**\n\n   - **概述**：这是 Node.js 中使用的模块化规范。它通过 `module.exports` 和 `require()` 来导出和引入模块。\n   - **特点**：同步加载，主要用于服务器端（Node.js）。\n   - **使用场景**：服务器端开发，尤其是在 Node.js 中。\n\n   ```javascript\n   // 导出模块\n   module.exports = function () {\n     console.log('Hello, CommonJS!')\n   }\n\n   // 导入模块\n   const hello = require('./hello')\n   hello()\n   ```\n\n2. **AMD（Asynchronous Module Definition）**\n\n   - **概述**：AMD 是一种异步加载模块的规范，常用于浏览器端。\n   - **特点**：支持异步加载，模块和依赖是按需加载的，通常使用 `define()` 和 `require()`。\n   - **使用场景**：浏览器端的模块化，尤其是当需要异步加载模块时。\n\n   ```javascript\n   define(['dependency'], function (dep) {\n     return function () {\n       console.log('Hello, AMD!')\n     }\n   })\n   ```\n\n3. **UMD（Universal Module Definition）**\n\n   - **概述**：UMD 是一个兼容多种模块化规范（CommonJS、AMD 和全局变量）的模块化方案。\n   - **特点**：确保模块在不同的环境中都能使用。\n   - **使用场景**：需要在多种环境下（如 Node.js、浏览器）使用的库或框架。\n\n   ```javascript\n   ;(function (root, factory) {\n     if (typeof exports === 'object' && typeof module !== 'undefined') {\n       module.exports = factory()\n     } else if (typeof define === 'function' && define.amd) {\n       define(factory)\n     } else {\n       root.myModule = factory()\n     }\n   })(this, function () {\n     return function () {\n       console.log('Hello, UMD!')\n     }\n   })\n   ```\n\n4. **ES6 Modules（ESM）**\n\n   - **概述**：ES6 模块化是 JavaScript 原生的模块化标准，使用 `import` 和 `export` 语法。\n   - **特点**：支持静态分析，加载时可以进行优化，现代 JavaScript 标准。\n   - **使用场景**：现代前端开发（浏览器和 Node.js）。\n\n   ```javascript\n   // 导出模块\n   export function greet() {\n     console.log('Hello, ESM!')\n   }\n\n   // 导入模块\n   import { greet } from './greet.js'\n   greet()\n   ```\n\n5. **SystemJS**\n\n   - **概述**：SystemJS 是一个支持多种模块规范（CommonJS、AMD 和 ESM）的模块加载器。\n   - **特点**：支持多种模块格式，动态加载模块。\n   - **使用场景**：需要跨模块加载器兼容的复杂应用。\n\n   ```javascript\n   System.config({\n     map: {\n       greet: './greet.js',\n     },\n   })\n   System.import('greet').then((greet) => {\n     greet()\n   })\n   ```\n\n:::\n\n## JS 如何捕获异常？有几种方式？\n\n参考答案\n\n::: details\n\n1. **try...catch 语句**\n\n```js\ntry {\n  // 可能会抛出异常的代码\n  throw new Error('Something went wrong!')\n} catch (error) {\n  // 捕获并处理异常\n  console.error('Caught an error:', error.message)\n}\n```\n\n2. **Promise 中的错误捕获（catch）**\n\n```js\nsomeAsyncFunction()\n  .then((result) => {\n    console.log(result)\n  })\n  .catch((error) => {\n    console.error('Async error caught:', error)\n  })\n```\n\n3. **window.onerror（全局错误处理）**\n\n```js\nwindow.onerror = function (message, source, lineno, colno, error) {\n  console.error(`Error occurred: ${message}`)\n  return true // 阻止默认错误处理\n}\n```\n\n:::\n\n## `0.1 + 0.2 === 0.3` 表达式返回什么？\n\n参考答案\n\n::: details\n`0.1 + 0.2 === 0.3` 在 JavaScript 中会返回 **`false`**。\n\n**原因：**\nJavaScript 中的浮点数运算存在精度问题。由于计算机在内部表示浮点数时不能精确表示某些小数，导致 `0.1 + 0.2` 的结果并不是精确的 `0.3`，而是一个接近于 `0.3` 的小数。\n\n具体来说，`0.1 + 0.2` 的计算结果是 `0.30000000000000004`，而不是 `0.3`。因此，当你用 `===`（严格相等）进行比较时，`0.30000000000000004` 和 `0.3` 不相等，结果为 `false`。\n\n**解决方法：**\n\n1. **四舍五入**：\n\n   ```javascript\n   console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON) // true\n   ```\n\n2. **自定义精度比较**：\n   将浮动值限制到一定的小数位，进行比较：\n   ```javascript\n   console.log(Math.round((0.1 + 0.2) * 100) / 100 === 0.3) // true\n   ```\n\n:::\n\n## 如何理解 JS 单线程？\n\n参考答案\n\n::: details\n**什么是 JavaScript 单线程？**\n\nJavaScript 是 **单线程** 的意思是它只有一个线程来执行代码，这意味着它一次只能执行一个任务。所有的 JavaScript 代码，默认情况下，都会按照顺序在同一个线程中依次执行。单线程的特性使得 JavaScript 相比多线程语言在处理并发时有一些限制，但它也有一套机制来处理异步操作，避免阻塞主线程。\n\n**为什么是单线程？**\n\nJavaScript 的设计目的是为了简化开发，尤其是在浏览器环境中。单线程可以避免多线程带来的复杂性，比如线程同步、资源竞争等问题。为了不让长时间的任务阻塞 UI 渲染，JavaScript 提供了异步编程的机制。\n\n**如何处理并发任务？**\n\n虽然 JavaScript 是单线程的，但它通过以下机制来实现并发任务的处理：\n\n1. **事件循环（Event Loop）**：JavaScript 使用事件循环来管理异步任务。通过事件循环，JavaScript 可以在任务执行时不中断主线程的执行。异步任务（比如 `setTimeout`、`Promise`、`XHR` 等）会先进入 **消息队列（Event Queue）**，当主线程空闲时，再从队列中取出任务执行。\n\n2. **Web APIs**：浏览器提供了 **Web APIs**（如 `setTimeout`、`fetch`、`DOM` 等）来处理一些异步操作。这些操作会被交给浏览器的 API 处理，处理完后通过事件循环机制将回调函数推送到消息队列，等待主线程执行。\n\n3. **异步编程**：通过 **`setTimeout`**、**`Promise`**、**`async/await`** 等方式，JavaScript 可以非阻塞地处理 I/O 操作，避免卡住整个程序的执行。\n\n:::\n\n## 什么是 WebWorker 如何理解它？\n\n参考答案\n\n::: details\n\n**Web Worker** 是一种浏览器提供的 API，允许你在一个独立的线程中执行 JavaScript 代码，**与主线程（UI 线程）分离**。Web Worker 可以处理计算密集型任务，如数据处理、文件解析等，这些任务通常会阻塞主线程，导致 UI 卡顿。通过 Web Worker，你可以将这些耗时操作移到后台线程，确保主线程始终保持响应状态。\n\n**工作原理：**\n\n1. **独立线程**：Web Worker 在一个与主线程（UI 线程）分离的线程中运行，主线程和 Worker 线程之间通过消息传递（postMessage）进行通信。\n2. **主线程与 Worker 通信**：主线程可以通过 `postMessage()` 方法向 Worker 发送数据，Worker 完成计算后，通过 `postMessage()` 将结果返回给主线程。\n\n3. **异步操作**：由于 Worker 在后台线程中运行，因此它的执行不会阻塞主线程，所有的计算任务都是异步执行的。\n\n4. **线程间通信**：Worker 无法直接访问主线程的 DOM、`window` 或者 `document` 等对象，它只能通过 `postMessage()` 与主线程进行数据交换。返回的数据是通过事件机制传递的，使用 `onmessage` 监听数据的返回。\n\n**Web Worker 的优势：**\n\n- **性能提升**：Web Worker 可以让长时间的计算任务在后台线程中执行，避免 UI 阻塞，提升用户体验。\n- **非阻塞性**：主线程可以继续处理用户交互和渲染，而不被复杂计算所阻塞。\n- **多线程处理**：对于 CPU 密集型任务，Web Worker 可以将工作分配给多个 Worker，实现并行计算，提高性能。\n\n**Web Worker 的应用场景：**\n\n- **大数据处理**：例如，处理大量的数组计算、排序、数据筛选等任务。\n- **图像处理**：例如，进行图像的处理和转换，而不影响 UI 渲染。\n- **音视频处理**：例如，音视频的编码、解码等计算密集型操作。\n- **异步任务**：一些需要后台执行的异步任务，可以通过 Worker 来处理。\n\n**Web Worker 的局限性：**\n\n- **无法操作 DOM**：Web Worker 在独立线程中运行，不能直接访问 DOM 和 `window`，只能通过消息传递来与主线程交换数据。\n- **数据传递**：数据通过 `postMessage()` 传递时会发生深拷贝，因此传递大数据时可能会有性能开销。\n- **浏览器支持**：大多数现代浏览器支持 Web Worker，但在旧版浏览器中可能不被支持。\n\n1. **创建一个 Web Worker：**\n\n   ```javascript\n   // main.js (主线程)\n   const worker = new Worker('worker.js') // 创建 Worker 实例\n\n   worker.postMessage('Hello, Worker!') // 向 Worker 发送消息\n\n   worker.onmessage = function (event) {\n     console.log('Worker says: ', event.data) // 接收 Worker 的响应\n   }\n   ```\n\n2. **Worker 文件（worker.js）：**\n   ```javascript\n   // worker.js (Worker 线程)\n   onmessage = function (event) {\n     console.log('Main thread says: ', event.data)\n     postMessage('Hello, Main Thread!') // 发送响应到主线程\n   }\n   ```\n\n:::\n\n## JS 如何进行内存管理和垃圾回收？\n\n参考答案\n\n::: details\n\nJavaScript 的内存管理是自动的，主要通过 **垃圾回收（GC）** 来实现。\n\n**内存管理：**\n\n1. JavaScript 使用 自动内存管理，开发者不需要手动分配和释放内存。\n2. 内存通过 堆（用于存储对象和数组等动态分配的内存）和 栈（用于存储函数调用和局部变量）进行管理。\n\n**常用的垃圾回收机制有：**\n\n1. **标记-清除（Mark-and-Sweep）**：标记活动对象，清除未标记对象，释放内存。\n2. **引用计数**：计算对象的引用次数，引用为 0 时回收。但会有循环引用的问题。\n3. **生成式垃圾回收**：通过将内存分为年轻代和老年代，优化垃圾回收频率，减少内存碎片。\n\n:::\n\n## 如何检测 JS 内存泄漏？内存泄漏的场景有哪些？\n\n参考答案\n\n::: details\n\n1. **使用浏览器开发者工具**：\n\n   - **Chrome DevTools** 中的 **Memory** 面板可以用来检测内存泄漏。可以查看 **Heap Snapshot** 和 **Allocation instrumentation on timeline**，分析对象分配、释放情况。\n   - **Heap Snapshot**：查看对象的分配情况，并通过比较不同时间点的快照来发现泄漏。\n   - **Timeline**：在页面交互过程中，查看内存的使用情况，发现持续增长的内存占用。\n\n2. **通过 `performance.memory` API**：\n\n   - 在支持的浏览器中，可以通过 `performance.memory` API 获取当前的内存使用情况（如 JS 堆内存大小），来跟踪内存的变化。\n\n   ```javascript\n   console.log(window.performance.memory)\n   ```\n\n3. **手动检测**：\n\n   - 通过创建和销毁对象，使用 `setInterval` 或 `setTimeout` 来检测是否有对象未被回收。\n   - 观察垃圾回收器是否清理不再使用的对象，如果内存不断增长，可能就是内存泄漏。\n\n4. **第三方工具**：\n   - **Valgrind**、**Memory.js** 等工具可以帮助检测内存泄漏。\n\n**内存泄漏的常见场景：**\n\n1. **全局变量**：\n\n   - 意外的全局变量会导致对象无法被回收。\n     ```javascript\n     function test() {\n       leakedVar = 'This is a global variable' // 未声明的变量成为全局变量\n     }\n     ```\n\n2. **未移除的事件监听器**：\n\n   - 如果事件监听器被绑定在 DOM 元素上，但没有在元素移除后正确移除，可能导致内存泄漏。\n     ```javascript\n     const button = document.getElementById('myButton')\n     button.addEventListener('click', function () {\n       /* some logic */\n     })\n     // 如果没有 button.removeEventListener，按钮被移除后内存仍未释放\n     ```\n\n3. **闭包（Closures）**：\n\n   - 闭包会保持对外部函数变量的引用，如果闭包生命周期过长，会导致外部函数的变量无法释放。\n     ```javascript\n     function createClosure() {\n       let largeObject = new Array(1000).fill('Some data')\n       return function () {\n         console.log(largeObject) // largeObject 被闭包引用，无法被 GC 回收\n       }\n     }\n     let closure = createClosure()\n     ```\n\n4. **DOM 引用**：\n\n   - 保留对已删除 DOM 元素的引用，导致内存泄漏。\n\n     ```javascript\n     let div = document.createElement('div')\n     document.body.appendChild(div)\n\n     // 删除 DOM\n     document.body.removeChild(div)\n\n     // ❌ 如果还保留引用，GC 无法回收\n     let cache = div\n\n     // ✔️ 手动断开引用，GC 才能回收\n     div = null\n     cache = null\n     ```\n\n5. **定时器（setInterval/setTimeout）未清除**：\n\n   - 如果定时器没有清除，仍然会占用内存。\n     ```javascript\n     let interval = setInterval(function () {\n       console.log('Running')\n     }, 1000)\n     // 如果没有 clearInterval(interval)，定时器将一直运行，导致内存泄漏\n     ```\n\n6. **Web Workers 和后台线程**：\n   - 如果 Web Worker 或后台线程没有正确终止，可能会导致内存泄漏。\n     ```javascript\n     const worker = new Worker('worker.js')\n     // 如果没有 worker.terminate()，worker 可能导致内存泄漏\n     ```\n\n:::\n\n## 如何理解 WebAssembly？\n\n参考答案\n\n::: details\n\n**WebAssembly（Wasm）** 是一种新的 Web 技术，它允许开发者将其他编程语言（如 C、C++、Rust 等）编译成高效的二进制代码，并在浏览器中运行。WebAssembly 旨在提供接近原生性能的 Web 体验，特别适用于高性能计算任务。\n\n**关键点：**\n\n1. **高效性**：WebAssembly 是一种二进制格式，比 JavaScript 的文本格式更紧凑，加载速度更快，执行速度更快，适用于 CPU 密集型任务，如图像处理、游戏开发和科学计算。\n\n2. **与 JavaScript 协作**：WebAssembly 和 JavaScript 可以协同工作，JavaScript 用于 UI 操作和事件处理，WebAssembly 负责计算密集型任务。它们通过 **共享内存** 和 **消息传递** 进行通信。\n\n3. **跨平台**：WebAssembly 是跨平台的，可以在所有支持 WebAssembly 的现代浏览器中运行，并且不需要针对不同操作系统和硬件做额外的修改。\n\n4. **安全性**：WebAssembly 运行在沙盒环境中，不能直接访问操作系统资源，保证了 Web 应用的安全性。\n\n**应用场景**：\n\n- **游戏开发**：通过高效的计算，WebAssembly 可以让 Web 上的游戏运行得更流畅。\n- **图像/视频处理**：利用 WebAssembly 进行高效的图像处理和视频编解码。\n- **科学计算**：WebAssembly 能大大提升 JavaScript 在处理大数据和复杂计算时的性能。\n\n:::\n\n## JS V8 Nodejs Deno Bun 这几个，他们是什么关系？\n\n参考答案\n\n::: details\n\n**1. V8**\n\n- **V8** 是一个开源的 **JavaScript 引擎**，由 Google 开发，主要用于 Chrome 浏览器和 Node.js。\n- V8 将 JavaScript 代码编译成机器代码并执行，从而提高 JavaScript 的执行效率。\n- **作用**：V8 是 JavaScript 执行的“心脏”，负责解析和执行 JavaScript 代码。\n- **关系**：V8 是 **Node.js** 和 **Deno** 的底层引擎。它本身不提供完整的 JavaScript 环境或库，只负责执行 JavaScript。\n\n**2. Node.js**\n\n- **Node.js** 是一个基于 V8 引擎的 **JavaScript 运行时环境**，使得 JavaScript 不仅可以在浏览器中运行，还可以在服务器端运行。\n- 它为 JavaScript 提供了 I/O 操作、文件系统访问、网络请求等功能，这些功能通常由操作系统提供。\n- **作用**：Node.js 使得 JavaScript 可以用于构建服务器端应用，支持事件驱动、非阻塞式 I/O 机制。\n- **关系**：Node.js 使用 V8 作为其 JavaScript 引擎，除此之外，它还包含一些额外的 API（如 `fs`、`http`、`path` 等）来提供对文件系统、网络等资源的访问。\n\n**3. Deno**\n\n- **Deno** 是一个由 **Node.js** 的原始开发者 **Ryan Dahl** 创建的新的 JavaScript/TypeScript 运行时环境。\n- 它同样使用 **V8 引擎**，但是与 Node.js 不同的是，Deno 内置了对 TypeScript 的支持，且具有现代化的安全特性（如权限控制）。\n- **作用**：Deno 旨在修复 Node.js 中存在的一些设计问题，提供更简洁和安全的运行时环境。\n- **关系**：Deno 使用 V8 作为 JavaScript 引擎，但它不是 Node.js 的直接继承者，而是对现有 JavaScript 运行时环境的一次重构，加入了许多新的功能和改进。\n\n**4. Bun**\n\n- **Bun** 是一个新兴的 **JavaScript/TypeScript 运行时**，其目标是提供更高效的性能，特别是在构建工具和服务器端应用中。\n- Bun 是基于 **JavaScriptCore**（Safari 浏览器的 JavaScript 引擎）构建的，而不是 V8。\n- **作用**：Bun 具有非常快速的执行速度，提供类似于 Node.js 的 API，同时它也是一个现代的构建工具（例如，能够快速打包、转译和运行 JavaScript/TypeScript 代码）。\n- **关系**：Bun 不是基于 V8 引擎，它使用的是 **JavaScriptCore** 引擎，但它与 Node.js 和 Deno 类似，作为一个 JavaScript 运行时环境提供底层支持。\n\n:::\n\n## 有了解过WeakMap吗？WeakMap与Map的区别是什么？\n\n参考答案\n\n::: details\n\n**1. 什么是WeakMap**\n\nWeakMap 是 JavaScript 中的一种集合类型，它存储键值对，且键必须是对象，并且键是弱引用的。这意味着，如果键对象没有其他引用，它会被垃圾回收器回收，对应的键值对也会被自动删除。\n\n**2. 与Map的区别**\n\n**键的类型**\n\n- **`Map`**：键可以是任意类型，包括基本数据类型（像字符串、数字等）和引用类型（如对象、函数）。\n- **`WeakMap`**：键只能是对象，不能使用基本数据类型作为键。\n\n**垃圾回收机制**\n\n- **`Map`**：对键所引用的对象是强引用。只要 `Map` 存在，键引用的对象就不会被垃圾回收，即便其他地方无该对象的引用。\n- **`WeakMap`**：对键所引用的对象是弱引用。若对象没有其他强引用，垃圾回收时对象会被回收，`WeakMap` 里对应的键值对也会自动移除。\n\n**可遍历性**\n\n- **`Map`**：是可迭代的，能使用 `for...of` 循环、`forEach` 方法等遍历其键值对。\n- **`WeakMap`**：不可迭代，没有 `keys()`、`values()`、`entries()` 这些迭代方法，也不能用 `for...of` 或 `forEach` 遍历。\n\n**方法和属性**\n\n- **`Map`**：有 `size` 属性来获取键值对数量，还有 `set()`、`get()`、`has()`、`delete()`、`clear()` 等方法。\n- **`WeakMap`**：只有 `set()`、`get()`、`has()`、`delete()` 方法，没有 `size` 属性和 `clear()` 方法。\n\n**使用场景**\n\n- **`Map`**：适用于需存储任意类型键值对，且要对这些键值对进行遍历和操作的场景，如缓存数据。\n- **`WeakMap`**：常用于避免内存泄漏的场景，例如给对象添加私有数据，当对象被销毁时，`WeakMap` 里相关数据也会被清理。\n\n:::\n\n## 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功？\n\n参考答案\n\n::: details\n\n迭代协议​\n题目问怎么能让var [a,b] = {a:1,b:2} 成立，那么我们首先要运行一下，看看它是怎么个不成立法。​\n\n```JavaScript\nconst obj = {​\n    a:'1',​\n    b:'2',​\n}​\n​\nconst [a,b] = obj​\n```\n\n运行之后打开控制台可以发现报错信息，它告诉我们obj这个对象是不可迭代的，那么我们想办法把obj变成可迭代的是不是就能解决这个问题，这要怎么做呢？想要搞明白这点我们需要先了解一下可迭代协议。​\n​\n可迭代协议的概念（ MDN ）​\n`可迭代协议允许`JavaScript`对象定义或定制它们的迭代行为，例如，在一个 for..of 结构中，哪些值可以被遍历到。一些内置类型同时是内置的可迭代对象，并且有默认的迭代行为，比如 Array 或者 Map，而其他内置类型则不是（比如 Object）。​\n  要成为可迭代对象，该对象必须实现 @@iterator 方法，这意味着对象（或者它原型链上的某个对象）必须有一个键为 @@iterator 的属性，可通过常量 Symbol.iterator 访问该属性：​\n  [Symbol.iterator]​\n  一个无参数的函数，其返回值为一个符合迭代器协议的对象。​\n  当一个对象需要被迭代的时候（比如被置入一个 for...of 循环时），首先，会不带参数调用它的 @@iterator 方法，然后使用此方法返回的迭代器获得要迭代的值。​`\n说人话就是，要想让obj成为一个可迭代的对象，就需要它实现 @@iterator 方法，具体表现为对象身上要有一个名为[Symbol.iterator] 的方法。而数组和Map则是一开始就有这个方法，所以它们是可迭代的。而对象身上则没有这个默认行为，所以不可迭代。真的是这样吗？我们创建一个数组，看看数组身上到底有没有[Symbol.iterator] 方法。​\n\n```JavaScript\nconst array = [1,2,3]​\nconsole.log(array)​\n```\n\n点开原型查看\n\n![proto](../imgs/image-iterator.png)\n​发现真的有一个Symbol.iterator()方法，该方法会返回一个迭代器对象。我们来调用一下\n\n```JavaScript\nconst array = [1,2,3]​\nconst iterator = array[Symbol.iterator]()​\nconsole.log(iterator)​\nconsole.log(iterator.next())​\nconsole.log(iterator.next())​\nconsole.log(iterator.next())​\nconsole.log(iterator.next())\n```\n\n打印iterator对象后发现在它的原型上有一个next()方法，调用next()方法，会得到一个对象value就是当前迭代的值，done则代表当前迭代器是否已经迭代完成。\n\n数组 解构 的本质\n\n```JavaScript\nconst array = [1,2,3]​\nvar [a,b,c] = array​\n// 本质上是​\nconst iterator = array[Symbol.iterator]()​\nvar a = iterator.next().value​\nvar b = iterator.next().value​\nvar c = iterator.next().value\n```\n\n解决方法​\n到此为止我们可知，要想满足迭代协议需要对象身上有一个名为[Symbol.iterator]的方法。再使用for..of或者解构赋值的时候会隐式的调用这个方法，得到一个迭代对象，通过迭代对象的next方法判断当前是否完成迭代和具体迭代的值。​\n也就是说我们要在obj上添加[Symbol.iterator]方法并且完成next方法的逻辑​\n\n最终代码如下：\n\n```javascript\nconst obj = {​\n    a: '1',​\n    b: '2',​\n    [Symbol.iterator]() {\n      let index = 0\n      const keys = Object.keys(this)\n      return {\n        next() {\n          return {\n            value: obj[keys[index]],\n            done: index++ >= keys.length\n          }\n        }\n      }\n    }\n}​\n​\nconst [a, b] = obj\n```\n\n当然，我们也可以用for...of去循环遍历这个对象，我看谁再说for...of不能遍历对象(doge)\n\n```Javascript\nfor(let i of obj){​\n    console.log(i)​\n}​\n// 1​\n// 2\n\n```\n\n:::\n\n## postMessage 有哪些使用场景？\n\n参考答案\n\n::: details\n\n**window.postMessage 定义**\n\n`window.postMessage()`方法可以安全地实现跨源通信。`window.postMessage()` 方法提供了一种受控机制来规避此限制，只要正确的使用，这种方法就很安全\n\n**用途**\n\n可用于两个不同的Ifrom（不同源） 之间的通讯​\n\n语法\n\n```JavaScript\notherWindow.postMessage(message, targetOrigin, [transfer]);\n```\n\n**参数说明**\n\n- data​\n- 从其他 window 中传递过来的对象。​\n- origin​\n- 调用 `postMessage` 时消息发送方窗口的 **origin** . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。例如 “**<https://example.org>** (隐含端口 443)”、“**<http://example.net>** (隐含端口 80)”、“**<http://example.com:8080**”。请注意，这个origin不能保证是该窗口的当前或未来origin，因为postMessage被调用后可能被导航到不同的位置。​>\n- source​\n- 对发送消息的**窗口**对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信。\n\n**例子**\n\n子框架传递信息\n\n```JavaScript\n<script>​\n​\n// 子框架向父框架发送信息​\n​\nfunction goParentIfromPostMessage(msg,parentUrl){​\n​\n    var parentUrl = window.parent.location.origin;​\n​\n        window.onload=function(){​\n​\n        window.parent.postMessage(msg,parentUrl);​\n​\n        }​\n    }​\n }​\n ​\n    goParentIfromPostMessage('msgStr',parentIfromUrl)​\n​\n</script>\n```\n\n父框架接收端\n\n```JavaScript\n<script>​\n​\n        window.addEventListener('message',function(e){​\n​\n            console.log(e.origin,e.data);​\n​\n            console.log(e.data);​\n​\n        })​\n​\n</script>\n```\n\n这样即可以实现简单的框架跨域通信，但是会有一些安全问题​\n\n**安全问题**\n\n如果您不希望从其他网站接收message，请不要为message事件添加任何事件侦听器。 这是一个完全万无一失的方式来避免安全问题。​\n如果您确实希望从其他网站接收message，请始终使用origin和source属性验证发件人的身份。 任何窗口（包括例如 **<http://evil.example.com）都可以向任何其他窗口发送消息，并且您不能保证未知发件人不会发送恶意消息。>** 但是，验证身份后，您仍然应该始终验证接收到的消息的语法。 否则，您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。​\n\n- 当您使用postMessage将数据发送到其他窗口时，始终指定精确的目标origin，而不是。恶意网站可以在您不知情的情况下更改窗口的位置，因此它可以拦截使用postMessage发送的数据。\n\n示例\n\n```JavaScript\n/*​\n * A窗口的域名是<http://example.com:8080>，以下是A窗口的script标签下的代码：​\n */​\n​\nvar popup = window.open(...popup details...);​\n​\n// 如果弹出框没有被阻止且加载完成​\n​\n// 这行语句没有发送信息出去，即使假设当前页面没有改变location（因为targetOrigin设置不对）​\npopup.postMessage(\"The user is 'bob' and the password is 'secret'\",​\n                  \"https://secure.example.net\");​\n​\n// 假设当前页面没有改变location，这条语句会成功添加message到发送队列中去（targetOrigin设置对了）​\npopup.postMessage(\"hello there!\", \"http://example.org\");​\n​\nfunction receiveMessage(event)​\n{​\n  // 我们能相信信息的发送者吗?  (也许这个发送者和我们最初打开的不是同一个页面).​\n  if (event.origin !== \"http://example.org\")​\n    return;​\n​\n  // event.source 是我们通过window.open打开的弹出页面 popup​\n  // event.data 是 popup发送给当前页面的消息 \"hi there yourself!  the secret response is: rheeeeet!\"​\n}​\nwindow.addEventListener(\"message\", receiveMessage, false);\n```\n\n```JavaScript\n/*​\n * 弹出页 popup 域名是<http://example.org>，以下是script标签中的代码:​\n */​\n​\n//当A页面postMessage被调用后，这个function被addEventListener调用​\nfunction receiveMessage(event)​\n{​\n  // 我们能信任信息来源吗？​\n  if (event.origin !== \"http://example.com:8080\")​\n    return;​\n​\n  // event.source 就当前弹出页的来源页面​\n  // event.data 是 \"hello there!\"​\n​\n  // 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把event.source​\n  // 作为回信的对象，并且把event.origin作为targetOrigin​\n  event.source.postMessage(\"hi there yourself!  the secret response \" +​\n                           \"is: rheeeeet!\",​\n                           event.origin);​\n}​\n​\nwindow.addEventListener(\"message\", receiveMessage, false)\n```\n\n:::\n"
  },
  {
    "path": "docs/first-exam/TS.md",
    "content": "# Typescript 面试题\n\nTypescript 已经全面普及，尤其大厂大型项目，前端熟悉 Typescript 是标配。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## TS 优缺点，使用场景\n\n参考答案\n\n::: details\n\n优点\n\n- 静态类型，减少类型错误\n- 有错误会在编译时提醒，而非运行时报错 —— 解释“编译时”和“运行时”\n- 智能提示，提高开发效率\n\n缺点\n\n- 学习成本高\n- 某些场景下，类型定义会过于混乱，可读性不好，如下代码\n- 使用不当会变成 anyscript\n\n```ts\ntype ModelFieldResolver<T, TKey extends keyof T = any> = (\n  this: T,\n  ...params: T[TKey] extends (...args: any) => any ? Parameters<T[TKey]> : never\n) => T[TKey]\n```\n\n适用场景\n\n- 大型项目，业务复杂，维护人员多\n- 逻辑性比较强的代码，依赖类型更多\n- 组内要有一个熟悉 TS 的架构人员，负责代码规范和质量\n\n:::\n\nPS. 虽然 TS 有很多问题，网上也有很多“弃用 TS”的说法，但目前 TS 仍然是最优解，而且各大前端框架都默认使用 TS 。\n\n## TS 基础类型有哪些\n\n参考答案\n\n::: details\n\n- boolean\n- number\n- string\n- symbol\n- bigint\n- Enum 枚举\n- Array 数组\n- Tuple 元祖\n- Object 对象\n- undefined\n- null\n- any void never unknown\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/basic-types.html\n\n:::\n\n## 数组 Array 和元组 Tuple 的区别是什么\n\n参考答案\n\n::: details\n\n数组元素只能有一种类型，元祖元素可以有多种类型。\n\n```ts\n// 数组，两种定义方式\nconst list1: number[] = [1, 2, 3]\nconst list2: Array<string> = ['a', 'b', 'c']\n\n// 元组\nlet x: [string, number] = ['x', 10]\n```\n\n:::\n\n## 枚举 enum 是什么？有什么使用场景？\n\nJS 中没有 enum 枚举，只学过 JS 你可能不知道 enum 。其实在 Java 和 C# 等高级语言中早就有了，TS 中也有。\n\n参考答案\n\n::: details\n\nenum 枚举，一般用于表示有限的一些选项，例如使用 enum 定义 4 个方向\n\n```ts\nenum Direction {\n  Up = 'UP',\n  Down = 'DOWN',\n  Left = 'LEFT',\n  Right = 'RIGHT',\n}\n```\n\n其他代码中，我们可以获取某一个方向，用于展示或存储。这样代码更具有可读性和维护行。\n\n```ts\nconst d = Direction.Up\n```\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/enums.html\n\n:::\n\n## keyof 和 typeof 有什么区别？\n\n参考答案\n\n::: details\n\n`typeof` 是 JS 基础用法，用于获取类型，这个很简单。\n\n`keyof` 是 TS 语法，用于获取所有 key 的类型，例如\n\n```ts\ninterface Person {\n  name: string\n  age: number\n  location: string\n}\n\ntype PersonType = keyof Person\n// 等价于 type PersonType = 'name' | 'age' | 'location'\n```\n\n可以把代码拷贝到这里来练习 https://www.tslang.cn/play/index.html\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7023238396931735583\n- https://juejin.cn/post/7096869746481561608\n\n:::\n\n## any void never unknown 有什么区别\n\n参考答案\n\n::: details\n\n主要区别：\n\n- `any` 任意类型（不进行类型检查）\n- `void` 没有任何类型，和 `any` 相反\n- `never` 永不存在的值的类型\n- `unknown` 未知类型（一个更安全的 any）\n\n代码示例\n\n```ts\nfunction fn(): void {} // void 一般定义函数返回值\n\n// 返回 never 的函数，必须存在无法达到的终点\nfunction error(message: string): never {\n  throw new Error(message)\n}\nfunction infiniteLoop(): never {\n  while (true) {}\n}\n\n// unknown 比直接使用 any 更安全\nconst a: any = 'abc'\nconsole.log(a.toUpperCase()) // 不会报错，但不安全\n\nconst b: unknown = 'abc'\n// console.log( b.toUpperCase() ) // 会报错！！！\nconsole.log((b as string).toUpperCase()) // 使用 as 转换类型，意思是告诉 TS 编译器：“我知道 b 的类型，我对安全负责”\n```\n\nPS：但现在 unknown 用的比 any 少很多，因为麻烦\n\n:::\n\n## unknown 和 any 区别\n\n参考答案\n\n::: details\n\n`unknown` 是更安全的 `any` ，如下代码\n\n```js\nconst a: any = 'x'\na.toString() // 不报错\n\nconst b: unknown = 'y'\n// b.toString() // 报错\n;(b as string).toString() // 不报错\n```\n\n:::\n\n## TS 访问修饰符 public protected private 有什么作用\n\n参考答案\n\n::: details\n\n- public 公开的，谁都能用 （默认）\n- protected 受保护的，只有自己和子类可以访问\n- private 私有的，仅自己可以访问\n\n这些规则很难用语法去具体描述，看代码示例\n\n```ts\nclass Person {\n  name: string = ''\n  protected age: number = 0\n  private girlfriend = '小丽'\n\n  // public protected private 也可以修饰方法、getter 等\n\n  constructor(name: string, age: number) {\n    this.name = name\n    this.age = age\n  }\n}\n\nclass Employee extends Person {\n  constructor(name: string, age: number) {\n    super(name, age)\n  }\n\n  getInfo() {\n    console.log(this.name)\n    console.log(this.age)\n    // console.log(this.girlfriend) // 这里会报错，private 属性不能在子类中访问\n  }\n}\n\nconst zhangsan = new Employee('张三', 20)\nconsole.log(zhangsan.name)\n// console.log(zhangsan.age) // 这里会报错，protected 属性不能在子类对象中访问，只能在子类中访问\n```\n\n:::\n\n追问：`#` 和 `private` 有什么区别呢？\n\n::: details\n\n`#` 在 TS 中可定义私有属性\n\n```ts\nclass Person {\n  #salary: number\n  constructor(\n    private name: string,\n    salary: number\n  ) {\n    this.#salary = salary\n  }\n}\n\nconst p = new Person('xxx', 5000)\n// const n = p.name // 报错\nconst n = (p as any).name // 可以通过“投机取巧”获取到\nconsole.log('name', n)\n\n// const s = p.#salary // 报错\n// const s = (p as any).#salary // 报错\n```\n\n区别：\n\n- `#` 属性，不能在参数中定义\n- `private` 属性，可通过 `as any` 强制获取\n- `#` 属性，更私密\n\n:::\n\n## type 和 interface 共同和区别，如何选择\n\ntype 和 interface 有很多相同之处，很多人因此而产生“选择困难症”，这也是 TS 热议的话题。\n\n共同点\n\n::: details\n\n- 都能描述一个对象结构\n- 都能被 class 实现\n- 都能被扩展\n\n```ts\n// 接口\ninterface User {\n  name: string\n  age: number\n  getName: () => string\n}\n\n// 自定义类型\ntype UserType = {\n  name: string\n  age: number\n  getName: () => string\n}\n\n// class UserClass implements User {\nclass UserClass implements UserType {\n  name = 'x'\n  age = 20\n  getName() {\n    return this.name\n  }\n}\n```\n\n:::\n\n区别\n\n::: details\n\n- type 可以声明基础类型\n- type 有联合类型和交差类型\n- type 可以被 `typeof` 赋值\n\n```ts\n// type 基础类型\ntype name = string\ntype list = Array<string>\n\n// type 联合类型\ntype info = string | number\n\ntype T1 = { name: string }\ntype T2 = { age: number }\n// interface T2 { age: number  } // 联合，还可以是 interface ，乱吧...\ntype T3 = T1 | T2\nconst a: T3 = { name: 'x' }\ntype T4 = T1 & T2\nconst b: T4 = { age: 20, name: 'x' }\n\n// typeof 获取\ntype T5 = typeof b\n\n//【补充】还有个 keyof ，它和 typeof 完全不同，它是获取 key 类型的\ntype K1 = keyof T5\nconst k: K1 = 'name'\n```\n\n:::\n\n如何选择？\n\n::: details\n\n根据社区的使用习惯，推荐使用方式\n\n- 能用 interface 就尽量用 interface\n- 除非必须用 type 的时候才用 type\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/interfaces.html\n\n:::\n\nPS. 其实你混淆 type 和 interface 不是你的问题，这是 TS 设计的问题，或者说 TS 设计初衷和后来演变带来的副作用。\n\n## 什么是泛型，如何使用它？\n\n只学过 JS 的同学不知道泛型，其实它早就是 C# 和 Java 中的重要概念了。初学泛型可能会比较迷惑，需要多些代码多练习。\n\n泛型的定义\n\n::: details\n\n泛型 Generics 即通用类型，可以灵活的定义类型而无需写死。\n\n```ts\nconst list: Array<string> = ['a', 'b']\nconst numbers: Array<number> = [10, 20]\n\ninterface User {\n  name: string\n  age: number\n}\nconst userList: Array<User> = [{ name: 'x', age: 20 }]\n```\n\n:::\n\n泛型的使用\n\n::: details\n\n1. 用于函数\n\n```ts\n// Type 一般可简写为 T\nfunction fn<Type>(arg: Type): Type {\n  return arg\n}\nconst x1 = fn<string>('xxx')\n\n// 可以有多个泛型，名称自己定义\nfunction fn<T, K>(a: T, b: K) {\n  console.log(a, b)\n}\nfn<string, number>('x', 10)\n```\n\n2. 用于 class\n\n```ts\nclass SomeClass<T> {\n  name: T\n  constructor(name: T) {\n    this.name = name\n  }\n  getName(): T {\n    return this.name\n  }\n}\nconst s1 = new SomeClass<String>('xx')\n```\n\n3. 用于 type\n\n```ts\nfunction fn<T>(arg: T): T {\n  return arg\n}\n\nconst myFn: <U>(arg: U) => U = fn // U T 随便定义\n```\n\n4. 用于 interface\n\n```ts\n// interface F1 {\n//   <T>(arg: T): T;\n// }\ninterface F1<T> {\n  (arg: T): T\n}\nfunction fn<T>(arg: T): T {\n  return arg\n}\nconst myFn: F1<number> = fn\n```\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/generics.html\n\n:::\n\n## 什么是交叉类型和联合类型\n\n### 交叉类型 `T1 & T2`\n\n交叉类型是将多个类型合并为一个类型，包含了所需的所有类型的特性。例如 `T1 & T2 & T3`\n\n代码示例\n\n::: details\n\n```ts\ninterface U1 {\n  name: string\n  city: string\n}\ninterface U2 {\n  name: string\n  age: number\n}\ntype UserType1 = U1 & U2\nconst userA: UserType1 = { name: 'x', age: 20, city: 'beijing' }\n\n// 可在 userA 获取所有属性，相当于“并集”\nuserA.name\nuserA.age\nuserA.city\n```\n\n:::\n\n注意事项\n\n::: details\n\n1. 两个类型的相同属性，如果类型不同（冲突了），则该属性是 `never` 类型\n\n```ts\n// 如上代码\n// U1 name:string ，U2 name: number\n// 则 UserType1 name 是 never\n```\n\n2. 基础类型没办法交叉，会返回 `never`\n\n```ts\ntype T = string & number // never\n```\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/advanced-types.html\n\n:::\n\n### 联合类型 `T1 | T2`\n\n一种“或”的关系。格式如 `T1 | T2 | T3`。代码示例如下\n\n::: details\n\n```ts\ninterface U1 {\n  name: string\n  city: string\n}\ninterface U2 {\n  name: string\n  age: number\n}\n\nfunction fn(): U1 | U2 {\n  return {\n    name: 'x',\n    age: 20,\n  }\n}\n```\n\n:::\n\n注意事项\n\n::: details\n\n基础类型可以联合\n\n```ts\ntype T = string | number\nconst a: T = 'x'\nconst b: T = 100\n```\n\n但如果未赋值的情况下，联合类型无法使用 string 或 number 的方法\n\n```ts\nfunction fn(x: string | number) {\n  console.log(x.length) // 报错\n}\n```\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/advanced-types.html\n\n:::\n\n## 是否用过工具类型\n\nTS 工具类型有 `Partial` `Required` `Omit` `ReadOnly` 等，熟练使用 TS 的人都会熟悉这些工具类型。\n\n参考答案\n\n::: details\n\n`Partial<T>` 属性设置为可选\n\n```ts\ninterface User {\n  name: string\n  age: number\n}\ntype User1 = Partial<User> // 属性全部可选，类似 `?`\nconst u: User1 = {}\n```\n\n`Require<T>` 属性设置为必选 （和 Partial 相反）\n\n`Pick<T, K>` 挑选部分属性\n\n```ts\ninterface User {\n  name: string\n  age: number\n  city: string\n}\ntype User1 = Pick<User, 'name' | 'age'> // 只选择两个属性\nconst u: User1 = { name: 'x', age: 20 }\n```\n\n`Omit<T, K>` 剔除部分属性（和 Pick 相反）\n\n`ReadOnly<T>` 属性设置为只读\n\n相当于为每个属性都设置一遍 `readonly`\n\n```ts\ninterface User {\n  name: string\n  age: number\n}\ntype User1 = Readonly<User>\nconst u: User1 = { name: 'x', age: 20 }\n// u.name = 'y' // 报错\n```\n\n:::\n\n## TS 这些符号 `?` `?.` `??` `!` `_` `&` `|` `#` 分别什么意思\n\n参考答案\n\n::: details\n\n`?` 可选属性，可选参数\n\n```ts\ninterface User {\n  name: string\n  age?: number\n}\nconst u: User = { name: 'xx' } // age 可写 可不写\n\nfunction fn(a: number, b?: number) {\n  console.log(a, b)\n}\nfn(10) // 第二个参数可不传\n```\n\n`?.` 可选链：有则获取，没有则返回 undefined ，但不报错。\n\n```ts\nconst user: any = {\n  info: {\n    city: '北京',\n  },\n}\n// const c = user && user.info && user.info.city\nconst c = user?.info?.city\nconsole.log(c)\n```\n\n`??` 空值合并运算符：当左侧的操作数为 null 或者 undefined 时，返回其右侧操作数，否则返回左侧操作数。\n\n```ts\nconst user: any = {\n  // name: '张三'\n  index: 0,\n}\n// const n1 = user.name ?? '暂无姓名'\nconst n2 = user.name || '暂无姓名' // 某些情况可用 || 代替\nconsole.log('name', n2)\n\nconst i1 = user.index ?? '暂无 index'\nconst i2 = user.index || '暂无 index' // 当是 0 （或 false 空字符串等）时，就不能直接用 || 代替\nconsole.log('index', i1)\n```\n\n`!` 非空断言操作符：忽略 undefined null ，自己把控风险\n\n```ts\nfunction fn(a?: string) {\n  return a!.length // 加 ! 表示忽略 undefined 情况\n}\n```\n\n`_` 数字分隔符：分割数字，增加可读性\n\n```ts\nconst million = 1_000_000\nconst phone = 173_1777_7777\n\n// 编译出 js 就是普通数字\n```\n\n其他的本文都有讲解\n\n- `&` 交叉类型\n- `|` 联合类型\n- `#` 私有属性\n\n:::\n\n## 什么是抽象类 abstract class\n\n抽象类是 C# 和 Java 的常见语法，TS 也有，但日常前端开发使用并不多。\n\n参考答案\n\n::: details\n\n抽象类，不能直接被实例化，必须派生一个子类才能使用。\n\n```ts\nabstract class Animal {\n  abstract makeSound(): void\n  move(): void {\n    console.log('roaming the earch...')\n  }\n}\n\n// const a = new Animal() // 直接实例化，报错\n\nclass Dog extends Animal {\n  // 必须要实现 Animal 中的抽象方法，否则报错\n  makeSound() {\n    console.log('wang wang')\n  }\n}\n\nconst d = new Dog()\nd.makeSound()\nd.move()\n```\n\n:::\n\n参考资料\n\n::: details\n\n- https://www.tslang.cn/docs/handbook/classes.html 其中搜索 `abstract class`\n\n:::\n\n## 如何扩展 window 属性，如何定义第三方模块的类型\n\n参考答案\n\n::: details\n\n```ts\ndeclare interface Window {\n  test: string\n}\n\nwindow.test = 'aa'\nconsole.log(window.test)\n```\n\n:::\n\n## 是否有过真实的 Typescript 开发经验，讲一下你的使用体验\n\n开放性问题，需要结合你实际开发经验来总结。可以从以下几个方面考虑\n\n::: details\n\n- 在 Vue React 或其他框架使用时遇到的障碍？\n- 在打包构建时，有没有遇到 TS 语法问题而打包失败？\n- 有没有用很多 `any` ？如何避免 `any` 泛滥？\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/6929793926979125255\n\n:::\n"
  },
  {
    "path": "docs/hr-exam/behavioural-test.md",
    "content": "# 行为面试技巧\n\nHR 行为面试，考察工作态度和价值观。\n\n::: tip\n\n- HR 权利很大，可以拒绝你入职。所以和 HR 聊天要态度温婉。\n- 无论你真实想法如何，HR 面试时要表现出一切都是为了公司和工作。工作好了，我才能好。\n- 注意倾听，HR 可能会“挖坑”（个人缺点、如何评价前领导等），不要啥实话都往外说。\n\n:::\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 个人介绍\n\n::: tip\n这个问题在任何面试环节都可能会被问到，提前准备。\n:::\n\n使用如下模板，用自然舒缓的语气 1min 之内表达完，时间不要太长。\n\n1. 个人履历，近几年的教育/工作经历\n2. 擅长的业务领域（如医疗、3D、地图等），做过的主要项目\n3. 擅长的技术栈 —— 程序员，最终还是要说到技术上\n\n注意事项\n\n- 不要太短，不要两三句话说完，要尽量全面的表达出自己\n- 不要太啰嗦，不要深入细节，对方可能听不懂（这才面试刚开始）\n- 要有条理性，不要想一个说一个，逻辑很混乱\n\n## 离职原因？\n\n常规说法：想换一个更大的平台，继续提升自己的能力。\n\n如被裁员：实话实说即可，现在这行情被裁员也很正常。\n\n注意事项\n\n- 不要谈钱，不要说为了涨工资而离职\n- 不要谈任何前公司的坏话，例如 xxx 项目/领导 不好而离职\n\n## 空窗期为何这么久？\n\n有些同学被裁员，然后迟迟未找到工作，空窗期可能几个月甚至半年多。\n\n推荐表达：一开始对自己要求比较高，想找一个理想的工作，一直没有合适的机会。\n\n注意事项\n\n- 不要说“学习了一段时间”，暴露自己技术能力不好\n- 不要说“休息了一段时间”，我们都自愿加班，根本不需要休息\n- 不要说“出去玩了”，老板和 HR 不喜欢爱玩的员工\n\n## 你为何选择我们公司？\n\n推荐表达方式\n\n- 提前熟悉公司背景、主营业务、JD 招聘信息、负责的项目\n- 说出其中的 xxx 项目/技术 比较吸引你，或者 xxx 福利比较吸引你\n- 再说出自己的 xxx 技能比较符合公司的招聘要求，所以就投了简历\n\n注意，千万不能说：因为你们公司招聘啊！—— 这种无脑回答\n\n## 你对这份工作有什么期许？\n\n推荐表达方式\n\n- 希望通过这个工作，参与项目，团队分享等，能让自己在 xxx 技术领域，提升更高一个层级\n- 自己技术提升了，才能更好的服务于公司的项目/产品，承担更重要的职责\n\n要点是：自己的进步是为了公司服务的，这是 HR 喜欢的价值观。\n\n## 你喜欢什么样的团队和领导？\n\n推荐表达方式\n\n- 喜欢一个有技术氛围的团队，喜欢分享技术的领导，大家可以一起成长进步\n- 技术氛围好，才能更好的保证代码质量和产品稳定性\n\n同理：谈技术进步，也是为了公司产品服务的，这是 HR 喜欢的价值观。\n\n## 未来 3-5 年的规划是什么？\n\n参考上文“你对这份工作有什么期许？”的回答方式：\n\n`技术进步 + 承担更重要的职责 + 为公司项目服务`\n\n## 如何看待加班？\n\n前两年我们一般回答：加班就像借钱，救急不救穷 —— 即，可以偶尔加班，但不能长久加班。\n\n但是，近两年风向早就变了，加班成了常态，而且有的会比较加班工时。\n\n所以现在再回复这个问题，就得换一种说法：认可加班。\n\n其实换位思考一下，如果我们也严格执行 8 小工作制，那你的工资还会这么高吗？你是否愿意每日工作 8 小时，同时降薪 40% ？\n\n## 你的优点是什么？\n\n我们的文化比较内敛，大家都过于谦虚，不擅长宣传自己，尤其是程序员行业。\n\n都会觉得自己没有啥优点，如果不提前准备，被问到这个问题可能就懵了。\n\n优点，就是指你觉得自己比较好的部分，并不是说一定比别人优秀、甚至是专家。不是的，大部分都是普通人。\n\n推荐表达方式\n\n- 优先选择**硬技能**，自己擅长的技术领域，业务领域，能更好的用于工作中。\n- 如果实在找不到擅长的技术领域，可以说一些**软技能**：认真，准时，容易沟通，为人和善友好，工作有激情，英语好...\n\n遇到这个问题你就想办法夸自己，不要过于谦虚 —— 能表达自己的优点，这本身也是个优点。\n\n## 你的缺点是什么？\n\n注意，这是个坑！不要傻乎乎的真把自己的缺点说出来，暴露给别人看。\n\n正确的方式是：说缺点，也要说自己的补救措施，最终体现自己是一个爱学习、要求进步的人。\n\n推荐的表达方式\n\n- 我觉得自己目前在 xxx 方面还有所欠缺\n- 但我已经开始通过 xxx 学习这方面的知识了，计划 xxx 天以后即可完成\n\n## 是否和前领导有过冲突？如何解决的？\n\n准备一个工作中具体的例子，使用 `STAR` 模型讲解，例如：\n\n- Situation 背景：记得在 xxx 项目中，我和领导有了不一致的意见，我认为应该先 xxx ，他认为应该先 yyy\n- Task 目标/任务：但我们都是为了同一个目标，能把项目尽快上线，达到客户的要求\n- Action 行动/解决方案：我们找来项目组核心成员，以及客户的核心成员，一起开会讨论，列出当前所有任务，划分优先级，重新制定时间计划，并且得到客户的认可\n- Result 结果：最后项目成功上线，且客户很满意我们的沟通方式和做事效率\n\n注意事项\n\n- 工作中的冲突一般都是：任务太多，资源（人，时间）不够\n- 冲突要完全是客观的，完全和工作相关的，一定是对事不对人的\n- 不要说前领导/前公司的坏话，大家都是为了把工作做好\n\n## 是否和同事/项目成员有过冲突？如何解决的？\n\n参考上一题。\n\n上一题中，和领导有冲突需要找客户沟通确认。而这一题，你和同事有了冲突，就需要找领导沟通确认。其他都是一样的。\n\n## 是否和客户/甲方有过冲突？如何解决的？\n\n参考上一题。\n\n和客户发生了冲突，首先要第一时间和你领导沟通，说明情况。再和领导一起找客户沟通。\n\n所以，这些问题都离不开 `同事 领导 客户` 这几个常见角色。\n\n做 C 端产品的可能没有明确的甲方/客户，那和你发生冲突的可能是 PM 产品经理，沟通方式是一样的。\n\n## 如果 PM 给你的项目加需求，你该如何应对？\n\n在项目管理中有个专业术语叫“需求蔓延”，英文 Scope creep / requirement creep\n\n意思就是本来定好了需求、开发计划，但是 PM 今天加个按钮，明天加个图标，后天加段文字...\n\n遇到这种问题的常见解决方式\n\n- 先和自己领导沟通\n  - 如果领导同意，且自己评估工作量不大，那就加上 —— 不要过于较真，HR 也不喜欢过于死板的人\n  - 如果领导同意，但自己评估工作量有点大，那就和领导说清楚（领导不一定熟悉每一个开发细节）\n  - 领导有可能直接不同意\n- 如果确定不接这个需求，如何回绝 PM 呢？\n  - 让他发邮件，抄送他领导，以及项目组成员，走正式的需求变更流程\n  - 一般此时 PM 就退缩了，不会为了这个小需求来发正式的邮件的\n- 最后，这样做是为了项目能稳定、按预期完成，否则需求蔓延太多，项目工期、质量都不可控\n\nPS. 其实这样的 PM 好对付，怕就怕在中小公司有这样的老板，摊上了也没办法...\n\n## 如果你项目的 deadline 很紧张，你该如何做？\n\n准备一个工作中具体的例子，使用 `STAR` 模型讲解，例如：\n\n- Situation 背景：在 xxx 公司时，有一个 xxx 项目，遇到过 deadline 很紧张的情况\n- Task 任务：客户/老板下令必须在 xxx 时间之前上线，我们要按照这个时间往前推\n- Action 行动/解决方案：\n  - 和领导沟通，让他帮忙协同其他人力资源（如测试人员），确保所有人都能按计划进行\n  - 和项目组开会，明确需求范围，列出功能，划分优先级，明确分工，制定开发计划\n  - 大家在一起集中加班开发，即时沟通，即时调整，快速开发完成，并提交测试\n- Result 结果：虽然大家很累、进度优点紧张，但项目最后按计划上线，我们团队也因此更加团结高效\n\n注意事项\n\n- 所有的冲突，解决方案都会涉及 `领导` `沟通` `优先级` `计划` 这几个关键词\n- 要提到加班，但不能只有加班。需要的是：有管理能力 + 加班\n\n## 介绍一个你最近做过的项目\n\n推荐的表达方式\n\n- 项目背景和功能介绍：这是一个 xxx 项目，它服务于 xxx 。它主要包含 a b c 功能/模块。\n- 技术栈：说出 2-3 个主要的即可，和 HR 不要说太多技术细节，能体现出技术即可\n- 个人的角色和成绩：我是这个项目中的核心前端开发人员，我主要负责 x y z 模块的开发。\n- 个人成长：通过开发这个项目，让我掌握了能独立承担项目/模块的能力，希望在未来的工作中继续发挥这方面的价值。\n\nPS. 通过介绍项目，来体现自己当前的能力\n\n## 说一件最让你有成就感的事情\n\n准备一个工作中具体的例子，使用 `STAR` 模型讲解，例如：\n\n- Situation 背景：介绍项目背景，要让 HR 能听懂\n- Task 任务：要完成 xxx 目标，而且有很大难度 —— 没难度哪儿来的成就感？\n- Action 行动/解决方案：通过 xxx 方式，最终解决了这个问题\n- Result 结果：自己得到 xxx 提升，学会了 xxx 能力\n\n注意事项\n\n- 一定是和工作相关的，非工作的不要说\n- 说的是一件事，但最后要总结为自己的能力，能做好未来的工作\n\n## 说一件最让你感觉失败的事情\n\n参考上一题。\n\n虽然做失败了，但让我学到了 xxx ，最终还是要总结为自己能力的提升。\n\n## 请用三个词概括自己\n\n可选的词：`计划性` `执行力` `热情` `积极` `创造性` `极客精神` `时间观念强` `善于沟通` `乐于分享` `组织能力` `管理能力` `专注`\n\n## 你的同事/领导如何评价你？\n\n我看很多同学在简历中写“自我评价”，你就可以把自我评价搬到这里。例如\n\n- 工作积极热情\n- 善于团队合作\n- 热爱技术\n\n如果有可能，尽量在说每一条的时候举一个例子证明一下。\n\nHR 天天听这些 `积极` `热情` `热爱` 这些词，耳朵早就听出茧子了，你不说点具体的例子，他们可能都听不进去。\n\n## 你业余有什么兴趣爱好？\n\n说几个程序员常见的爱好即可，例如玩游戏、看电影、听歌等。\n\n不要说太影响工作和加班的爱好，如徒步、骑行、长途旅行等。\n\n## 如何主持一次会议？\n\n综合考察你的沟通和组织能力，这也是程序员最欠缺的能力。\n\n- 明确会议主题、 todos 和核心参与人\n- 联系核心参与人，协同他们的时间，确定会议时间\n- 发送邮件给全体参会人员，抄送相关领导\n- 制定会议议程，主持会议，保证讨论话题的方向（不要蔓延到其他话题），做会议记录\n- 会议结束，整体会议记录和决议，发送邮件\n\nPS. 最关键的是：又开始有结束，知道开会干啥，知道开完会有了什么结论。\n"
  },
  {
    "path": "docs/hr-exam/salary.md",
    "content": "# 谈薪技巧\n\nHR 会把薪资尽量压低，以节省公司成本。而我们需要通过谈判，多争取一些薪资，当然要在合理范围之内。\n\n::: tip\n\n- 谈钱必伤感情，不要觉得不好意思，都是为自己争取利益，公平谈判。\n- 你担心 HR 拒绝你，其实 HR 也担心你不来（面试几轮，找个合适的候选人也不容易）\n\n:::\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 大厂校招有统一工资标准\n\n大厂会统一秋招、春招，只要面试通过会有统一的工资标准，大约年薪 200k 这个级别\n\n- 不同公司不一样，例如字节平均薪资就高一点\n- 不同岗位不一样，算法、大数据库会高一点\n- 不同学历不一样，研究生会高一点\n\n这是刚毕业的薪资水平，你未来如何涨薪，就看自己的能力和运气了。\n\n一般来说，跳槽涨薪 vs 公司内部涨薪，前者要快很多。但目前就业行情不好，也要谨慎跳槽，千万不要裸辞。\n\n## 参考你当前的薪资\n\n跳槽，一般会在你当前薪资基础上涨幅 10-20% 。除非你当前薪资特别低，例如你在二线城市工作，整体薪资水平低。\n\nPS. 当前就业行情不好，涨幅可能不会很大。\n\n## 参考公司规模\n\n一线城市的大公司薪资水品最高。公司规模越大，薪资水平相对会越高。初创小公司最“穷”且不稳定。\n\n如果你刚毕业，根据自己的能力，尽量去大规模的公司，别相信什么“小公司锻炼能力”。\n\n## 参考市场行情\n\n前些年我见过从某度跳槽到某滴涨薪 50% 的人，这两年就出现了降薪跳槽的人，以及大量待业人员。\n\n个人是斗不过环境的，具体来说就是：不要裸辞、不要拒绝外包、抓住任何工作机会。\n\n## 参考公司福利，但要认清福利的价值\n\n如果是大公司，公司福利几乎不用看，基本都不错，而且都是统一标准的。\n\n如果是中小公司，且 HR 说有 xxx 福利，为了压低一点薪资。此时你问清楚并且算清楚这些福利的具体价值，能抵多少工资？\n\n不要不好意思，谈钱本来就是伤感情的事儿，大家都是挣去自己的利益，这很正常。\n\n## 不要透露底牌，自己说出具体的期望薪资\n\n这是一个非常重要的技巧！！！\n\n和 HR 谈薪时，HR 一般会直接问：你能接受的期望薪资是多少？ —— HR 在问你的底牌\n\n此时你如果说出来一个具体数，如 20K ，亮了底牌，你大概率就输了。\n\n正确的回答方式应该是这样\n\n- 首先自己提前做好调研，查一下当前合理的薪资范围是多少（例如 20-40k），根据城市、公司规模、岗位等\n- 然后和 HR 谈薪时答复：我已经查到这个岗位的薪资是 20-40k ，不知道咱们公司能给到多少？\n\n你要把太极打回去，让 HR 去亮底牌，然后再自己做判断。\n\n## 多要现金，还是多要期权？\n\n公司一般会倾向于：多给你期权，少给你现金。所以你应该：多要现金，少要期权。\n\n当然，如果总包比较多的话，也不可能全要现金，公司没有那么多钱。合理比例即可。\n\n知乎、脉脉那些总包 100w 200w 的，真假先不说，即便是真的，也大部分都是期权。\n"
  },
  {
    "path": "docs/markdown-examples.md",
    "content": "# Markdown Extension Examples\n\nThis page demonstrates some of the built-in markdown extensions provided by VitePress.\n\n## Syntax Highlighting\n\nVitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:\n\n**Input**\n\n````md\n```js{4}\nexport default {\n  data () {\n    return {\n      msg: 'Highlighted!'\n    }\n  }\n}\n```\n````\n\n**Output**\n\n```js{4}\nexport default {\n  data () {\n    return {\n      msg: 'Highlighted!'\n    }\n  }\n}\n```\n\n## Custom Containers\n\n**Input**\n\n```md\n::: info\nThis is an info box.\n:::\n\n::: tip\nThis is a tip.\n:::\n\n::: warning\nThis is a warning.\n:::\n\n::: danger\nThis is a dangerous warning.\n:::\n\n::: details\nThis is a details block.\n:::\n```\n\n**Output**\n\n::: info\nThis is an info box.\n:::\n\n::: tip\nThis is a tip.\n:::\n\n::: warning\nThis is a warning.\n:::\n\n::: danger\nThis is a dangerous warning.\n:::\n\n::: details\nThis is a details block.\n:::\n\n## More\n\nCheck out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).\n"
  },
  {
    "path": "docs/second-exam/HarmonyOS-application-development.md",
    "content": "# 鸿蒙应用开发\n\n## 鸿蒙中地图功能如何实现，申请流程是什么样的\n\n::: details\n\n1. 主要通过 集成 Map Kit 的功能来实现\n2. Map Kit 功能很强大，比如有\n   1. [创建地图](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-creation-V5)：呈现内容包括建筑、道路、水系等。\n   2. [地图交互](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-interaction-V5)：控制地图的交互手势和交互按钮。\n   3. [在地图上绘制](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-drawing-V5)：添加位置标记、覆盖物以及各种形状等。\n   4. [位置搜索](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-location-services-V5)：多种查询 Poi 信息的能力。\n   5. [路径规划](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-navi-V5)：提供驾车、步行、骑行路径规划能力。\n   6. [静态图](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-static-diagram-V5)：获取一张地图图片。\n   7. [地图 Picker](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-advanced-controls-V5)：提供地点详情展示控件、地点选取控件、区划选择控件。\n   8. [通过 Petal 地图应用实现导航等能力](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-petalmaps-V5)：查看位置详情、查看路径规划、发起导航、发起内容搜索。\n   9. [地图计算工具](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-calculation-tool-V5)：华为地图涉及的 2 种坐标系及其使用区域和转换\n3. 在编码之前需要\n   1. 完成证书的申请和公钥指纹的一些配置\n   2. 还要在 AGC 平台上开通地图服务应用\n   3. 代码中使用 项目的 client_id\n   4. 最后开始编码\n\n:::\n\n## 一多开发是如何实现的\n\n::: details\n\n1. 一多开发是一次开发多端部署\n2. 主要分成三个核心部分\n   1. 工程级一多\n   2. 界面级一多\n   3. 能力级一多\n3. 工程级一多主要指的是使用华为鸿蒙推荐的三层架构来搭建项目，比如\n   1. 第一层，最底层是 common-公共能力层，用于存放公共基础能力集合（如工具库、公共配置等），一般是使用 HSP 包(动态共享包)，这样它被项目中多个模块引入的话，也只会保留一个备份。\n   2. 第二层，是 features-基础特性层，用于存放基础特性集合（如应用中相对独立的各个功能的 UI 及业务逻辑实现等）\n   3. 顶层是，products-产品定制层，用于针对不同设备形态进行功能和特性集成\n4. 界面级一多指的是一套代码可以适配不同尺寸、形态的设备，主要通过以下这些技术来实现\n   1. 自适应布局 等比拉伸缩放等等相关技术\n   2. 响应式布局 通过断点、媒体查询、栅格布局来实现\n5. 能力级一多主要指的是不同硬件设备支持能力不一样，如蓝牙、摄像头、传感器等等。这些主要通过判断当前设置是否支持该能力来决定是否调用相关的 api 功能。如利用编辑器工具的智能提示、和代码中使用的 caniuse 或者 try-catch 进行判断使用。\n\n:::\n\n## 三层架构\n\n::: details\n\n1. 第一层，最底层是 common-公共能力层，用于存放公共基础能力集合（如工具库、公共配置等），一般是使用 HSP 包(动态共享包)，这样它被项目中多个模块引入的话，也只会保留一个备份。\n2. 第二层，是 features-基础特性层，用于存放基础特性集合（如应用中相对独立的各个功能的 UI 及业务逻辑实现等）\n3. 顶层是，products-产品定制层，用于针对不同设备形态进行功能和特性集成\n\n:::\n\n## 录音有做过吗？avrecoder 有几种状态？\n\n::: details\n\n录音可以通过 AVRecorder 和 AudioCapturer 来实现。两者区别主要在支持录制声音的格式不同和控制录音文件的细小粒度不同上。AVRecorder 会简单一些，AudioCapturer 会复杂一些-还可以搭配 ai 语音功能使用\n\nAVRecorder 主要有以下这些状态：\n\n类型说明'idle'闲置状态。'prepared'参数设置完成'started'正在录制。'paused'录制暂停。'stopped'录制停止。'released'录制资源释放。'error'错误状态。\n\n![image-20250326151004432](../imgs/image-20241124081207334.png)\n\n:::\n\n## AVRecord 的录音步骤\n\n::: details\n\n1. 创建 AVRecorder 实例，实例创建完成进入 idle 状态。\n2. 设置业务需要的监听事件，监听状态变化及错误上报。\n3. 配置音频录制参数，调用 prepare()接口，此时进入 prepared 状态。\n4. 开始录制，调用 start()接口，此时进入 started 状态。\n\n:::\n\n## 图片上传有做过吗？图片处理，旋转、缩放、图片保存有做过吗？\n\n::: details\n\n做过相册图片的上传（如果是沙箱内的图片只需要 1 个步骤即可，直接上传），流程主要有 3 个步骤，基于 photoAccessHelper 、CoreFileKit、NetworkKit 来实现的\n\n1. photoAccessHelper 用来实现选择要上传的相册的图片\n2. CoreFileKit 将相册图片拷贝到沙箱目录\n3. NetworkKit 负责将沙箱目录内的图片上传到服务器上\n\n图片处理，旋转、缩放、图片保存主要基于 Image Kit 来实现。它提供有\n\n- 图片解码\n- 指将所支持格式的存档图片解码成统一的 PixelMap，以便在应用或系统中进行图片显示或图片处理。\n- PixelMap\n- 指图片解码后无压缩的位图，用于图片显示或图片处理。\n- 图片处理\n- 指对 PixelMap 进行相关的操作，如旋转、缩放、设置透明度、获取图片信息、读写像素数据等。\n- 图片编码\n- 指将 PixelMap 编码成不同格式的存档图片，用于后续处理，如保存、传输等。\n\n其中压缩图片是通过 一个 ImageKit 的 packing 函数，传入压缩比例(0-100)来是实现的。值越小体积越小\n\n:::\n\n## 视频有做过吗？\n\n::: details\n\n1. 如果是普通的视频播放直接使用 Video 组件来播放即可。功能相对弱一些\n2. 如果是对视频播放进行神帝的一些处理，如流媒体、本地资源解析、媒体资源解封装、视频解码和自定义渲染的这些功能，可以使用 AVPlayer 来实现。\n3. 如果类似做一个编辑视频的软件，那么就需要使用到对应的 CAPI 接口来实现了(调用底层 c++的能力)\n\n:::\n\n## 同事发给你代码，你怎么知道它的 bundlename\n\n::: details\n\n一般直接看 AppScope 中的字段就行\n\n:::\n\n## 鸿蒙如何和网页端通信？\n\n::: details\n\n1. 如果是应用的话，使用 web 组件和对应的 controller 的一些接口，如 [runJavaScript()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#runjavascript)和[registerJavaScriptProxy](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#registerjavascriptproxy)\n2. 如果是元服务的话，使用 AtomicServiceWeb 来实现，因为 2025 年 1 月 22 日后不支持使用 web。还有 AtomicServiceWeb 没有了 web 中的如 [runJavaScript()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#runjavascript)和[registerJavaScriptProxy](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#registerjavascriptproxy)接口，但是它一样可以通过页面的 url 进行参数的传递和鸿蒙端提供了 js sdk，也可以很方便的让 h5 端调用鸿蒙端的功能\n\n:::\n\n## 跨域是怎么处理的？\n\n::: details\n\n> 跨域存在于不同源的浏览器和服务器的网络通信中，因为鸿蒙端嵌套了 web 组件，理解成就是一个浏览器，因此也会存在跨域\n\n为了提高安全性，ArkWeb 内核不允许 file 协议或者 resource 协议访问 URL 上下文中来自跨域的请求。因此，在使用 Web 组件加载本地离线\n\n资源的时候，Web 组件会拦截 file 协议和 resource 协议的跨域访问。\n\n主要有两种解决方案\n\n1. 将本地资源替换成网络资源，也就是 file 协议访问的是本地的资源，我们将本地资源放在网络上，通过 http 请求的方式来加载，然后在后端设置 cors 跨域即可。同时，开发者需利用 Web 组件的[onInterceptRequest](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-components-web-V5#oninterceptrequest9)方法，对本地资源进行拦截和相应的替换\n2. 通过[setPathAllowingUniversalAccess 白名单](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#setpathallowinguniversalaccess12)设置一个路径列表。当使用 file 协议访问该列表中的资源时，允许进行跨域访问本地文件\n\n:::\n\n## 录音过程中息屏怎么处理？\n\n::: details\n\n可以通过申请[长时任务](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/continuous-task-V5)，实现在后台长时间运行。长时任务支持的类型，包含数据传输、音视频播放、录制、定位导航、蓝牙相关、多设备互联、WLAN 相关、音视频通话和计算任务\n\n开发步骤如下：\n\n1. 需要申请 ohos.permission.KEEP_BACKGROUND_RUNNING 权限\n2. 声明后台模式类型（录音等）\n3. 通过 @ohos.resourceschedule.backgroundTaskManager 和@ohos.app.ability.wantAgent 进行编码处理\n\n:::\n\n## 有做过华为支付吗？\n\n::: details\n\n需要企业资质、需要在 AGC 平台上开通服务。\n\n![image-20250326120252517](../imgs/image-20250326120252517.png)\n\n1. 商户客户端请求商户服务器创建商品订单。\n2. 商户服务器按照商户模型调用 Payment Kit 服务端[直连商户预下单](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-prepay-V5)或[平台类商户/服务商预下单](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-agent-prepay-V5)接口。\n3. 华为支付服务端返回预支付 ID（prepayId）。\n4. 商户服务端组建订单信息参数[orderStr](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-model-V5#section159202591414)返回给商户客户端。\n5. 商户客户端调用[requestPayment](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-paymentservice-V5#section192192415554)接口调起 Payment Kit 支付收银台。\n6. Payment Kit 客户端展示收银台。\n7. 用户通过收银台完成支付，Payment Kit 客户端会收到支付结果信息并请求 Payment Kit 服务端处理支付。\n8. Payment Kit 服务端成功受理支付订单并异步处理支付。\n9. Payment Kit 服务端将支付结果返回给 Payment Kit 客户端。\n10. Payment Kit 客户端展示支付结果页。\n11. 用户关闭支付结果页后 Payment Kit 客户端会返回支付状态给商户客户端。\n12. 支付处理完成后，Payment Kit 服务端会调用回调接口返回支付结果信息给商户服务端。\n13. 商户服务端收到支付结果回调响应后，使用[SM2 验签方式](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-rest-overview-V5#section17670192215175)对支付结果进行验签。\n\n:::\n\n## 说一下多线程\n\n::: details\n\n[参考](https://juejin.cn/post/7435302345448259622)\n\n![img](../imgs/asynccode.png)\n\n:::\n\n## HarmonyOS 中的生命周期\n\n::: details\n\n页面生命周期\n\n1. onpageshow：页面每次显示时触发，包括路由过程、应用进入前台等场景。例如，用户从后台切换应用到前台，或者通过路由跳转到该页面时，此方法会被调用\n2. onpagehide：页面每次隐藏时触发，包括路由过程、应用进入后台等场景。比如用户按下主页键将应用切换到后台，或者通过路由跳转到其他页面时，该页面的 onpagehide 方法会被执行\n3. onbackpress：当用户点击返回按钮时触发。如果返回值为 true，表示页面自己处理返回逻辑，不进行页面路由；返回 false 则表示使用默认的路由返回逻辑，不设置返回值时按照 false 处理\n4. abouttoappear：组件即将出现时回调该接口，具体时机为在创建自定义组件的新实例后，在执行其 build () 函数之前执行。在该函数中可以修改变量，更改将在后续执行 build () 函数中生效\n5. abouttodisappear：在自定义组件析构销毁之前执行。在此函数中不允许改变状态变量，特别是 @link 变量的修改可能会导致应用程序行为不稳定\n\n组件生命周期独有的\n\n1. abouttoappear：组件即将出现时回调该接口，具体时机为在创建自定义组件的新实例后，在执行其 build () 函数之前执行。在该函数中可以修改变量，更改将在后续执行 build () 函数中生效\n2. abouttodisappear：在自定义组件析构销毁之前执行。在此函数中不允许改变状态变量，特别是 @link 变量的修改可能会导致应用程序行为不稳定\n\nUIAbility 生命周期\n\n1. create 状态：在应用加载过程中，UIAbility 实例创建完成时触发，系统会调用 oncreate () 回调。可以在该回调中进行页面初始化操作，例如变量定义、资源加载等，用于后续的 UI 展示\n2. windowstagecreate 状态：UIAbility 实例创建完成之后，在进入 foreground 之前，系统会创建一个 windowstage。windowstage 创建完成后会进入 onwindowstagecreate () 回调，可以在该回调中设置 UI 加载、设置 windowstage 的事件订阅，如获焦 / 失焦、可见 / 不可见等事件\n3. foreground 状态：当 UIAbility 实例切换至前台时触发，对应于 onforeground () 回调。在 onforeground () 中可以申请系统需要的资源，或者重新申请在 onbackground 中释放的资源.\n4. background 状态：当 UIAbility 实例切换至后台时触发，对应于 onbackground () 回调。在该回调中可以释放 UI 界面不可见时无用的资源，或者在此回调中执行较为耗时的操作，例如状态保存等.\n5. windowstagedestroy 状态：在 UIAbility 实例销毁之前，会先进入 onwindowstagedestroy 回调，可以在该回调中释放 UI 界面资源\n6. destroy 状态：在 UIAbility 实例销毁时触发，可以在 ondestroy () 回调中进行系统资源的释放、数据的保存等操作\n\n:::\n\n## 用 Entry 和 Navigation 装饰的页面有哪些区别\n\n::: details\n\n1. @Entry 装饰的页面是应用的入口页面，通常用于展示应用的初始界面，而 Navigation 组件是一个导航容器，挂载在单个页面下，支持跨模块的动态路由。\n2. @Entry 页面具有通用的生命周期方法，而 Navigation 组件里的页面不执行 onPageShow、onPageHide 等生命周期回调。\n\n:::\n\n## HarmonyOS 中里面有几种包，分别有什么作用\n\n::: details\n\nHarmonyOS 中有三种类型的包：HAP（HarmonyOS Ability Package）、HAR（Harmony Archive）、HSP（Harmony Shared Package）。\n\n1. HAP 是应用安装和运行的基本单元，分为 entry 和 feature 两种类型。\n2. HAR 是静态共享包，用于代码和资源的共享。\n3. HSP 是动态共享包，用于应用内共享代码和资源。\n\n:::\n\n## 简单介绍一下 Stage 模型\n\n::: details\n\n1. Stage 模型是 HarmonyOS 应用开发的基础架构，它提供了面向对象的开发方式，规范化了进程创建的方式，并提供组件化开发机制。\n2. Stage 模型的组件天生具备分布式迁移和协同的能力，支持多设备形态和多窗口形态，重新定义了应用能力边界。\n\n:::\n\n## HarmonyOS 中的动画\n\n::: details\n\nHarmonyOS 提供了多种动画能力，包括属性动画、显式动画、转场动画、路径动画和粒子动画。\n\n:::\n\n## 如何进行路由页面传参\n\n::: details\n\n在 HarmonyOS 中，可以通过 router.pushUrl 方法跳转到目标页面，并携带参数。在进入被分享页面时，通过 router.getParams()来获取\n\n传递的数据。此外，还可以使用 LocalStorage 等在页面间共享状态。\n\n:::\n\n## ArkTS 和 TS 的区别有哪些区别\n\n::: details\n\nArkTS 是 HarmonyOS 优选的主力应用开发语言，它保持了 TypeScript 的基本风格，同时通过规范定义强化开发期静态检查和分析，提升程序执行稳定性和性能。ArkTS 与 TS 的主要区别在于 ArkTS 是静态类型的，而 TS 支持动态类型。ArkTS 在编译时进行类型检查，有助于在代码运行前发现和修复错误。\n\n1. **[强制使用静态类型](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#强制使用静态类型)** **ArkTS 中禁止使用 any 类型。**\n2. [禁止在运行时变更对象布局](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#禁止在运行时变更对象布局)\n\n- 向对象中添加新的属性或方法。\n- 从对象中删除已有的属性或方法。\n- 将任意类型的值赋值给对象属性。\n\n3. [限制运算符的语义](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#限制运算符的语义)\n\n```HTML\n// 一元运算符`+`只能作用于数值类型：\nlet t = +42;   // 合法运算\nlet s = +'42'; // 编译时错误\n```\n\n:::\n\n## 常见装饰器\n\n::: details\n\n- [@State](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-state-V5)：@State 装饰的变量拥有其所属组件的状态，可以作为其子组件单向和双向同步的数据源。当其数值改变时，会引起相关组件的渲染刷新。\n- [@Prop](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-prop-V5)：@Prop 装饰的变量可以和父组件建立单向同步关系，@Prop 装饰的变量是可变的，但修改不会同步回父组件。深拷贝。\n- [@Link](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-link-V5)：@Link 装饰的变量可以和父组件建立双向同步关系，子组件中@Link 装饰变量的修改会同步给父组件中建立双向数据绑定的数据源，父组件的更新也会同步给@Link 装饰的变量。\n- [@Provide/@Consume](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-provide-and-consume-V5)：@Provide/@Consume 装饰的变量用于跨组件层级（多层组件）同步状态变量，可以不需要通过参数命名机制传递，通过 alias（别名）或者属性名绑定。\n- [@Observed](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-observed-and-objectlink-V5)：@Observed 装饰 class，需要观察多层嵌套场景的 class 需要被@Observed 装饰。单独使用@Observed 没有任何作用，需要和@ObjectLink、@Prop 联用。\n- [@ObjectLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-observed-and-objectlink-V5)：@ObjectLink 装饰的变量接收@Observed 装饰的 class 的实例，应用于观察多层嵌套场景，和父组件的数据源构建双向同步。\n- [AppStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5)是应用程序中的一个特殊的单例[LocalStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-localstorage-V5)对象，是应用级的数据库，和进程绑定，通过[@StorageProp](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5#storageprop)和[@StorageLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5#storagelink)装饰器可以和组件联动。\n- AppStorage 是应用状态的“中枢”，将需要与组件（UI）交互的数据存入 AppStorage，比如持久化数据[PersistentStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-persiststorage-V5)和环境变量[Environment](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-environment-V5)。UI 再通过 AppStorage 提供的装饰器或者 API 接口，访问这些数据。\n- 框架还提供了 LocalStorage，AppStorage 是 LocalStorage 特殊的单例。LocalStorage 是应用程序声明的应用状态的内存“数据库”，通常用于页面级的状态共享，通过[@LocalStorageProp](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-localstorage-V5#localstorageprop)和[@LocalStorageLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-localstorage-V5#localstoragelink)装饰器可以和 UI 联动。\n\n:::\n\n## 鸿蒙的 router 和 Navigation 的对比\n\n::: details\n\n1. router 最多页面栈为 32 个，Navigation 无限制\n2. Navigation 支持一多开发，Auto 模式自适应单栏跟双栏显示\n3. Navigation 支持获取指定页面参数\n4. Navigation 清理指定路由\n5. Navigation 支持路由拦截\n\n:::\n\n## 能力对比\n\n::: details\n\n| 业务场景                                      | Navigation                              | Router                                      |\n| :-------------------------------------------- | :-------------------------------------- | :------------------------------------------ |\n| **一多能力**                                  | **支持，Auto 模式自适应单栏跟双栏显示** | **不支持**                                  |\n| 跳转指定页面                                  | pushPath & pushDestination              | pushUrl & pushNameRoute                     |\n| 跳转 HSP 中页面                               | 支持                                    | 支持                                        |\n| 跳转 HAR 中页面                               | 支持                                    | 支持                                        |\n| 跳转传参                                      | 支持                                    | 支持                                        |\n| **获取指定页面参数**                          | **支持**                                | **不支持**                                  |\n| 传参类型                                      | 传参为对象形式                          | 传参为对象形式，对象中暂不支持方法变量      |\n| 跳转结果回调                                  | 支持                                    | 支持                                        |\n| 跳转单例页面                                  | 支持                                    | 支持                                        |\n| 页面返回                                      | 支持                                    | 支持                                        |\n| 页面返回传参                                  | 支持                                    | 支持                                        |\n| 返回指定路由                                  | 支持                                    | 支持                                        |\n| 页面返回弹窗                                  | 支持，通过路由拦截实现                  | showAlertBeforeBackPage                     |\n| 路由替换                                      | replacePath & replacePathByName         | replaceUrl & replaceNameRoute               |\n| 路由栈清理                                    | clear                                   | clear                                       |\n| **清理指定路由**                              | **removeByIndexes & removeByName**      | 不支持                                      |\n| 转场动画                                      | 支持                                    | 支持                                        |\n| 自定义转场动画                                | 支持                                    | 支持，动画类型受限                          |\n| 屏蔽转场动画                                  | 支持全局和单次                          | 支持 设置 pageTransition 方法 duration 为 0 |\n| geometryTransition 共享元素动画               | 支持（NavDestination 之间共享）         | 不支持                                      |\n| 页面生命周期监听                              | UIObserver.on('navDestinationUpdate')   | UIObserver.on('routerPageUpdate')           |\n| 获取页面栈对象                                | 支持                                    | 不支持                                      |\n| **路由拦截**                                  | 支持通过 setInercption 做路由拦截       | 不支持                                      |\n| 路由栈信息查询                                | 支持                                    | getState() & getLength()                    |\n| 路由栈 move 操作                              | moveToTop & moveIndexToTop              | 不支持                                      |\n| 沉浸式页面                                    | 支持                                    | 不支持，需通过 window 配置                  |\n| 设置页面标题栏（titlebar）和工具栏（toolbar） | 支持                                    | 不支持                                      |\n| 模态嵌套路由                                  | 支持                                    | 不支持                                      |\n\n:::\n\n## 页面下拉刷新和页面上拉加载\n\n::: details\n\n1. 下拉刷新可以使用[Refresh](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-refresh-V5)组件，它提供了 onStateChange 和 onRefreshing 事件 用来实现下拉刷新的业务\n2. [List](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-list-V5#onreachend)、Scroll、Grid、WaterFall 等组件都提供了**上拉加载更多**事件，比如 List 组件的 onReachEnd 事件就是\n\n:::\n\n## 响应式布局\n\n::: details\n\n> [链接](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/responsive-layout-V5#栅格布局)\n\n1. 断点\n   1. 在 在 UIAbility 的[onWindowStageCreate](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/uiability-lifecycle-V5)生命周期回调中监听 窗口尺寸变化事件，获取到当前窗口大小\n   2. 因为窗口大小单位是 px，需要调用 px2vp 函数转成 vp\n   3. 然后存到 AppStorage 中\n   4. 最后页面 使用 AppStorage 即可\n2. 媒体查询\n   1. 主要通过 mediaquery 结合 断点来使用\n3. 栅格布局\n   1. 通过 GridRow 和 GridCol 来实现\n   2. 一列分成了 12 份， 结合栅格组件默认提供 xs、sm、md、lg 四个断点\n\n:::\n\n## 断点续传\n\n::: details\n\n> 鸿蒙发送网络请求有两套方案\n>\n> 1. Request ， 我们使用的 axios 就是 基于它封装的\n> 2. RCP ，Remote Communication Kit（远场通信服务）是华为提供的 HTTP 发起数据请求的 NAPI 封装 目前新项目再推动它\n> 3. [远场通信场景](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-scenario-V5)\n>    1. [获取服务器资源](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-getserverresources-V5)\n>    2. [发送数据到服务器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-senddatatoserver-V5)\n>    3. [断点续传](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-httpresume-V5)\n>    4. [双向证书校验](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-certificateverification-V5)\n>    5. [拦截器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-interceptor-V5)\n>    6. [使用自定义证书校验](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-validation-V5)\n>    7. [上传下载文件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-updownload-V5)\n>    8. [设置 TLS 版本号和加密套件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-settls-V5)\n>    9. [获取服务器资源 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-getserverresources-c-V5)\n>    10. [发送数据到服务器 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-senddatatoserver-c-V5)\n>    11. [断点续传 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-httpresume-c-V5)\n>    12. [双向证书校验 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-certificateverification-c-V5)\n>    13. [拦截器 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-interceptor-c-V5)\n>    14. [证书锁定](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-validationlock-V5)\n>    15. [响应校验](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-responsecheck-V5)\n>    16. [读写超时](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-readtimeout-V5)\n>    17. [请求暂停和恢复](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-pause-resume-V5)\n>    18. [同步读写流](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-asyncreadwrite-V5)\n\n:::\n\n### [断点续传](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/re\n\n::: details\nmote-communication-httpresume-V5)\n\n1. 利用了远场通信 RemoteCommunicationKit\n2. 发送网络请求，利用[TransferRange](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/remote-communication-rcp-V5#section838945575618)的 from 和 to 属性 进行截取下载内容，拼接到文件上即可\n\n:::\n\n## 双向证书校验\n\n::: details\n\n> 用于验证服务端和客户端之间的身份和数据完整性，确保通信的安全性。\n\n1. 导入远场通信模块和文件读写模块\n   1. ```HTML\n      import { rcp } from '@kit.RemoteCommunicationKit';\n      import { fileIo } from '@kit.CoreFileKit';\n      ```\n2. 使用文件读写模块 读取存在客户端的证书\n   1. ```HTML\n      // 读取\n       fileIo.read(file.fd\n       // 存到字符串 content中\n       // 将读取的数据转换为字符串\n      let content = String.fromChar\n      ```\n3. 然后调用给远场通信的 configuration 方法设置到 security.certificate.content 属性中\n   1. ```HTML\n       request.configuration = {\n            security: {\n              certificate: {\n                content: content,\n      ```\n\n:::\n\n## 项目优化\n\n::: details\n\n- **图片懒加载**：列表里的图片滑到可见区域再加载，减少内存占用。\n- **数据缓存**：用`Preferences`或数据库缓存首页数据，下次启动先展示缓存再刷新。\n- **减少布局嵌套**：用@Builder 代替自定义组件，多用线性布局，少用 flex 等弹性布局\n- **线程管理**：把 JSON 解析、图片解码丢到`Worker`线程，防止主线程卡顿。\n- **内存泄漏排查**：用 DevEco Studio 的 Profiler 工具，发现有个页面退出后监听器没注销，赶紧加了`onPageHide`里的释放逻辑。\n\n:::\n\n## forEach 和 LazyForEach\n\n::: details\n\n1. forEach 会把数据全部渲染出来\n2. LazyForEach 只会渲染可视区域\n\n:::\n\n## LazyForEach 如何实现更新\n\n::: details\n\n- **数据源绑定**：`LazyForEach` 需要与实现了 `IDataSource` 接口的数据源（如 `LazyDataSource`）绑定。当数据源发生变化（增、删、改）时，框架会自动触发更新。\n- **观察者模式**：数据源通过 `DataChangeListener` 通知 `LazyForEach` 数据变更。只有实际变化的项会触发局部更新，而非重新渲染整个列表。\n\n:::\n\n## Class 和 interface 的区别\n\n::: details\n\n1. Interface 只能定义类型，class 可以定义类型和保护功能实现\n2. interface 可以同时继承多个接口，class 只能同时继承一个父类\n3. 工作中两个都用，比如用 class 来封装了一些工具库 avplayer、首选项、全屏-沉浸式、axios 等\n\n:::\n\n## AVPlayer 的播放步骤\n\n::: details\n\n1. 创建实例 createAVPlayer()，AVPlayer 初始化 idle 状态。\n\n2. 设置业务需要的监听事件\n\n   | 事件类型        | 说明                                                                                                                                                                 |\n   | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n   | stateChange     | 必要事件，监听播放器的 state 属性改变。                                                                                                                              |\n   | error           | 必要事件，监听播放器的错误信息。                                                                                                                                     |\n   | durationUpdate  | 用于进度条，监听进度条长度，刷新资源时长。                                                                                                                           |\n   | timeUpdate      | 用于进度条，监听进度条当前位置，刷新当前时间。                                                                                                                       |\n   | seekDone        | 响应 API 调用，监听 seek()请求完成情况。当使用 seek()跳转到指定播放位置后，如果 seek 操作成功，将上报该事件。                                                        |\n   | speedDone       | 响应 API 调用，监听 setSpeed()请求完成情况。当使用 setSpeed()设置播放倍速后，如果 setSpeed 操作成功，将上报该事件。                                                  |\n   | volumeChange    | 响应 API 调用，监听 setVolume()请求完成情况。当使用 setVolume()调节播放音量后，如果 setVolume 操作成功，将上报该事件。                                               |\n   | bufferingUpdate | 用于网络播放，监听网络播放缓冲信息，用于上报缓冲百分比以及缓存播放进度。                                                                                             |\n   | audioInterrupt  | 监听音频焦点切换信息，搭配属性 audioInterruptMode 使用。如果当前设备存在多个音频正在播放，音频焦点被切换（即播放其他媒体如通话等）时将上报该事件，应用可以及时处理。 |\n\n3. 设置资源：设置属性 url，AVPlayer 进入 initialized 状态。\n\n4. 准备播放：调用 prepare()，AVPlayer 进入 prepared 状态，此时可以获取 duration，设置音量。\n\n5. 音频播控：播放 play()，暂停 pause()，跳转 seek()，停止 stop() 等操作。\n\n6. （可选）更换资源：调用 reset()重置资源，AVPlayer 重新进入 idle 状态，允许更换资源 url。\n\n7. 退出播放：调用 release()销毁实例，AVPlayer 进入 released 状态，退出播放。\n\n:::\n\n## 手动签名和自动签名的区别\n\n::: details\n\n:::\n\n### **核心区别总结**\n\n::: details\n\n| **对比维度**     | **自动签名**                                         | **手动签名**                                                               |\n| ---------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |\n| **适用场景**     | 单设备调试（单真机可用）                             | 多设备调试、断网环境调试                                                   |\n| **签名证书管理** | 由 DevEco Studio 自动生成签名证书并绑定当前设备 UDID | 需在**AGC**控制台申请调试证书\\*\\*\\*\\*、注册调试设备 UDID、配置调试 Profile |\n| **安装限制**     | 仅允许当前绑定的设备安装                             | 支持注册的所有调试设备安装                                                 |\n| **权限支持**     | **不支持**受限开放权限（如健康服务）                 | 支持受限权限（需通过 AGC 审核并提交场景说明）                              |\n| **发布用途**     | **禁止用于发布**                                     | 可生成与发布版本一致的签名包（需替换为正式证书）                           |\n| **受限服务依赖** | 无法使用部分依赖签名的开放能力（如 Health Kit）      | 支持所有开放能力                                                           |\n| **公钥指纹管理** | 自动生成调试指纹，需在发布前手动更新为发布指纹       | 需手动维护调试和发布的指纹                                                 |\n\n:::\n\n## webview 的性能优化(怎么加快 webview 的响应速度)\n\n::: details\n\n- 可以通过[prepareForPageLoad()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#prepareforpageload10)来预解析或者预连接将要加载的页面\n- 能够预测到 Web 组件将要加载的页面或者即将要跳转的页面。可以通过[prefetchPage()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#prefetchpage10)来预加载即将要加载页面\n- 可以通过 prefetchResource()预获取将要加载页面中的 post 请求。在页面加载结束时，可以通过 clearPrefetchedResource()清除后续不再使用的预获取资源缓存\n- 预编译生成编译缓存 可以通过 precompileJavaScript()在页面加载前提前生成脚本文件的编译缓存。\n\n:::\n"
  },
  {
    "path": "docs/second-exam/engineering.md",
    "content": "# 前端工程化\n\n前端工程化通过自动化工具和标准化流程，提升开发效率、代码质量和可维护性。其核心目标是优化开发、构建、测试和部署流程，减少人工干预和重复劳动，便于项目扩展和团队协作。\n\n常见的工具，如Vite和Webpack，提供高效的构建和打包能力，显著提升开发效率并丰富前端生态。这些工具的广泛应用使前端开发更加高效，且成为近年来面试中的热门话题。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## Vite为什么更快？\n\n::: details 参考答案\n\nVite 相比传统构建工具（如 Webpack）更快🚀，主要得益于以下几个核心特性：\n\n- 基于原生 ES 模块（ESM）：Vite 利用浏览器原生的 ES 模块，在开发模式下`按需加载`模块，避免了整体打包，从而减少了启动时间。它通过只编译实际修改的文件，提升了开发过程中的反馈速度。\n- 高效的热模块替换（HMR）：Vite 在开发模式下利用原生 ES 模块实现模块级的热更新。当文件发生变化时，Vite 只会重新加载发生变化的模块，而不是重新打包整个应用，极大提高了热更新的速度。\n- 使用 esbuild 进行快速编译：Vite 默认使用 esbuild 作为编译工具，相比传统的 JavaScript 编译工具（如 Babel、Terser），esbuild 提供显著的性能提升，能够快速完成代码转换和压缩，从而加速开发和构建过程。\n- 现代 JavaScript 特性支持：Vite 在生产环境中使用 Rollup 构建，支持优秀的树摇和代码拆分，有效减小构建体积。同时，Vite 利用现代浏览器特性（如动态导入、ES2015+ 模块），减少了 polyfill 的使用，提升了加载速度。\n- 预构建和缓存：Vite 在开发时会预构建常用依赖（如 Vue、React），并将其转换为浏览器可执行的格式，避免每次启动时重新编译。同时，Vite 会缓存这些预构建的依赖，并在启动时复用缓存，从而加快启动速度。\n\n:::\n\n## vite中如何使用环境变量？\n\n::: details 参考答案\n\n根据当前的代码环境变化的变量就叫做**环境变量**。比如，在生产环境和开发环境将BASE_URL设置成不同的值，用来请求不同的环境的接口。\n\nVite内置了 `dotenv` 这个第三方库， dotenv会自动读取 `.env` 文件， dotenv 从你的 `环境目录` 中的下列文件加载额外的环境变量：\n\n> .env # 所有情况下都会加载\n> .env.[mode] # 只在指定模式下加载\n\n默认情况下\n\n- `npm run dev` 会加载 `.env` 和 `.env.development` 内的配置\n- `npm run build` 会加载 `.env` 和 `.env.production` 内的配置\n- `mode` 可以通过命令行 `--mode` 选项来重写。\n  环境变量需以 VITE\\_ 前缀定义，且通过 `import.meta.env` 访问。\n\n示例：\n.env.development：\n\n```js\nVITE_API_URL = 'http://localhost:3000'\n```\n\n在代码中使用：\n\n```js\nconsole.log(import.meta.env.VITE_API_URL) // http://localhost:3000\n```\n\n> 参考博文：[vite中环境变量的使用与配置](https://juejin.cn/post/7172012247852515335)\n\n:::\n\n## vite如何实现根据不同环境(qa、dev、prod)加载不同的配置文件？\n\n::: details 参考答案\n\n在 Vite 中，根据不同环境设置不同配置的方式，类似于 Webpack 时代的配置方法，但更加简化。Vite 使用 `defineConfig` 函数，通过判断 `command` 和 `mode` 来加载不同的配置。\n\n- **通过 `defineConfig` 动态配置：**\n\nVite 提供的 `defineConfig` 函数可以根据 `command` 来区分开发环境（ `serve` ）和生产环境（ `build` ），并返回不同的配置。\n\n```javascript\nimport { defineConfig } from 'vite'\nimport viteBaseConfig from './vite.base.config'\nimport viteDevConfig from './vite.dev.config'\nimport viteProdConfig from './vite.prod.config'\n\nexport default defineConfig(({ command, mode, ssrBuild }) => {\n  if (command === 'serve') {\n    // 开发环境独有配置\n    return {\n      ...viteBaseConfig,\n      ...viteDevConfig,\n    }\n  } else {\n    // 生产环境独有配置\n    return {\n      ...viteBaseConfig,\n      ...viteProdConfig,\n    }\n  }\n})\n```\n\n- **创建不同的配置文件**\n\n`vite.base.config.ts` ：基础配置，适用于所有环境。\n\n```javascript\nimport {\n    defineConfig\n} from \"vite\";\nexport default defineConfig({\n    // 基础配置->使用所有场景\n    return {\n        plugins: [\n            vue()\n        ],\n    }\n});\n```\n\n`vite.dev.config.ts` ：开发环境配置。\n\n```javascript\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n  // 开发环境专有配置\n})\n```\n\n`vite.prod.config.ts` ：生产环境配置。\n\n```javascript\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n  // 生产环境专有配置\n})\n```\n\n> 参考博文：[vite指定配置文件及其在多环境下的配置集成方案](https://juejin.cn/post/7172009616967942175)\n\n:::\n\n## 简述Vite的依赖预加载机制。\n\n::: details 参考答案\n\nVite 的依赖预构建机制通过在开发模式下提前处理常用依赖（如 Vue、React 等），将这些依赖转换为浏览器可以直接执行的格式。这避免了每次启动时重新编译这些依赖，显著提升了启动速度。预构建的依赖被缓存，并在后续启动时复用缓存，进一步加速了开发过程中的构建和启动时间。\n\n具体来说，它的工作原理如下：\n\n- **依赖识别和路径补全**： Vite 会首先识别项目中需要的依赖，并对非绝对路径或相对路径的引用进行路径补全。比如，`Vue` 的加载路径会变为 `node_modules/.vite/deps/Vue.js?v=1484ebe8`，这一路径显示了 Vite 在 `node_modules/.vite/deps` 文件夹下存放了经过预处理的依赖文件。\n- **转换成 ES 模块**： 一些第三方包（特别是遵循 CommonJS 规范的包）在浏览器中无法直接使用。为了应对这种情况，Vite 会使用 **esbuild** 工具将这些依赖转换为符合 ES 模块规范的代码。转换后的代码会被存放在 `node_modules/.vite/deps` 文件夹下，这样浏览器就能直接识别并加载这些依赖。\n- **统一集成 ES 模块**： Vite 会对每个包的不同模块进行统一集成，将各个分散的模块（如不同的 ES 函数或组件）合并成一个或几个文件。这不仅减少了浏览器发起多个请求的次数，还能够加快页面加载速度。\n\n> 参考博文：[vite的基础使用及其依赖预加载机制](https://juejin.cn/post/7172007612379054093#heading-3)、[手写vite让你深刻了解Vite的文件加载原理](https://juejin.cn/post/7178803290820804667)\n\n:::\n\n## vite中如何加载、处理静态资源？\n\n::: details 参考答案\n\n🎯 **静态资源目录（public 目录）**：\n\n- 静态资源可以放在 `public` 目录下，这些文件不会经过构建处理，直接按原样复制到输出目录。在开发时可以通过 `/` 路径直接访问，如 `/icon.png`。\n- `public` 目录可通过 `vite.config.js` 中的 `publicDir` 配置项修改。\n\n🎯 **资源引入**：\n\n- **图片、字体、视频**：通过 `import` 引入，Vite 会自动将其处理为 URL 并生成带哈希值的文件名。在开发时，引用会是根路径（如 `/img.png`），在生产构建后会是如 `/assets/img.2d8efhg.png` 的路径。\n- **CSS、JS**：CSS 会被自动注入到页面中，JS 按模块处理。\n\n🎯 **强制作为 URL 引入**：通过 `?url` 后缀可以显式强制将某些资源作为 URL 引入。\n\n```js\nimport imgUrl from './img.png?url'\n```\n\n🎯 **强制作为原始内容引入**：通过 `?raw` 后缀将文件内容作为字符串引入。\n\n🎯 `new URL()` ：通过 `import.meta.url` 可以动态构建资源的 URL，这对于一些动态路径很有用。\n\n```js\nconst imgUrl = new URL('./img.png', import.meta.url).href\ndocument.getElementById('hero-img').src = imgUrl\n```\n\n> 参考博文：[vite中静态资源（css、img、svg等）的加载机制及其相关配](https://juejin.cn/post/7173467405522305055)\n\n:::\n\n## 如何在Vite项目中引入CSS预处理器?\n\n::: details 参考答案\n\n在 Vite 中使用 CSS 预处理器（如 Sass、Less）是非常简单的，Vite 默认支持这些预处理器，我们只需要安装相应的依赖即可。\n\n安装依赖：\n\n```js\nnpm install sass--save - dev\n```\n\n在 Vue 组件中使用：\n\n```vue\n<style lang=\"scss\">\n$primary-color: #42b983;\nbody {\n  background-color: $primary-color;\n}\n</style>\n```\n\n此外，我们可以通过在vite的 `preprocessorOptions` 中进行配置，使用CSS 预处理器的一些强大功能。\n\n对于 Less，假如我们需要在项目中全局使用某些变量，我们可以在 `vite.config.js` 中配置 `globalVars` ，使得变量在所有文件中无需单独引入：\n\n```javascript\n// vite.config.js\nimport { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\n\nexport default defineConfig({\n  plugins: [vue()],\n  css: {\n    preprocessorOptions: {\n      less: {\n        globalVars: {\n          blue: '#1CC0FF', // 定义全局变量\n        },\n      },\n    },\n  },\n})\n```\n\n一旦配置了全局变量，我们就可以在任何 Vue 组件中直接使用它，无需再次引入：\n\n```vue\n<style scoped lang=\"less\">\n.wrap {\n  background: red;\n  color: @blue; // 使用全局变量\n}\n</style>\n```\n\n> 参考博文：[vite中如何更优雅的使用css](https://juejin.cn/post/7175366648659411000)、[Vite中预处理器(如less)的配置](https://juejin.cn/post/7177549666291515447)、[使用postcss完善vite项目中的css配置](https://juejin.cn/post/7178454300572516409)\n\n:::\n\n## vite中可做的项目优化有哪些？\n\n::: details 参考答案\n\n1️⃣ 启用 Gzip/Brotli 压缩\n\n使用 `vite-plugin-compression` 插件开启 Gzip 或 Brotli 压缩，可以有效减小传输的文件体积，提升加载速度。\n\n安装依赖：\n\n```javascript\nnpm install vite - plugin - compression--save - dev\n```\n\n配置示例：\n\n```javascript\nimport compression from 'vite-plugin-compression'\nexport default defineConfig({\n  plugins: [\n    compression({\n      algorithm: 'gzip', // 或 'brotli' 压缩\n      threshold: 10240, // 文件大于 10KB 时启用压缩\n    }),\n  ],\n})\n```\n\n> 参考博文：[vite打包优化vite-plugin-compression的使用](https://juejin.cn/post/7222901994840244279)\n\n2️⃣ 代码分割\n\n- 🎯 路由分割\n\n使用动态导入实现按需加载，减小初始包的体积，提高页面加载速度。\n\n```javascript\nconst module = import('./module.js') // 动态导入\n```\n\n或者在路由中使用懒加载：\n\n```javascript\nconst MyComponent = () => import('./MyComponent.vue')\n```\n\n- 🎯 手动控制分包\n\n在 Vite 中，你可以通过配置 Rollup 的 `manualChunks` 选项来手动控制如何分割代码。这个策略适用于想要将特定的依赖或模块提取成单独的 chunk 文件。\n\n```javascript\nimport { defineConfig } from 'vite'\nexport default defineConfig({\n  build: {\n    minify: false,\n    // 在这里配置打包时的rollup配置\n    rollupOptions: {\n      manualChunks: (id) => {\n        if (id.includes('node_modules')) {\n          return 'vendor'\n        }\n      },\n    },\n  },\n})\n```\n\n> 参考博文：[Vite性能优化之分包策略](https://juejin.cn/post/7177982374259949624)\n\n3️⃣ 图片优化\n\n使用 `vite-plugin-imagemin` 插件对项目中的图片进行压缩，减少图片体积，提升加载速度。\n\n```javascript\nnpm install vite - plugin - imagemin--save - dev\n```\n\n```javascript\nexport default defineConfig({\n  plugins: [\n    ViteImagemin({\n      gifsicle: {\n        optimizationLevel: 3,\n      },\n      optipng: {\n        optimizationLevel: 7,\n      },\n      mozjpeg: {\n        quality: 85,\n      },\n      pngquant: {\n        quality: [0.65, 0.9],\n      },\n    }),\n  ],\n})\n```\n\n4️⃣ 依赖优化\n\n配置 Vite 的 `optimizeDeps` 选项，提前预构建常用依赖，减少开发环境下的启动时间。\n\n```javascript\nexport default defineConfig({\n  optimizeDeps: {\n    include: ['lodash', 'vue', 'react'], // 预构建依赖\n  },\n})\n```\n\n> 参考博文：[vite的基础使用及其依赖预加载机制](https://juejin.cn/post/7172007612379054093#heading-3)\n\n:::\n\n## 简述vite插件开发流程？\n\n::: details 参考答案\n\nVite 插件开发基于 Rollup 插件系统，因此其生命周期和钩子与 Rollup 插件非常相似。以下是开发流程和关键步骤：\n\n1️⃣ **理解插件生命周期**\nVite 插件有一系列生命周期钩子，每个钩子对应不同的功能需求，主要钩子包括：\n\n- **config**：用于修改 Vite 配置，通常在构建或开发过程中使用。\n- **configureServer**：用于修改开发服务器的行为，如自定义请求处理。\n- **transform**：对文件内容进行转换，适用于文件类型转换或代码处理。\n- **buildStart** 和 **buildEnd**：在构建过程开始和结束时触发，适用于日志记录或优化操作。\n\n插件开发的核心是根据具体需求，在合适的生命周期钩子中实现业务逻辑。\n\n2️⃣ **插件基本结构**\n\nVite 插件的基本结构如下：\n\n```javascript\nexport default function myVitePlugin() {\n  return {\n    name: 'vite-plugin-example', // 插件名称\n    config(config) {\n      // 修改 Vite 配置\n    },\n    configureServer(server) {\n      // 修改开发服务器行为\n    },\n    transform(src, id) {\n      // 对文件内容进行转换\n    },\n  }\n}\n```\n\n插件对象必须包含一个 `name` 属性，用于标识插件，还可以根据需求实现其他钩子。\n\n3️⃣ **插件开发**\n\n在插件开发过程中，根据需求实现不同的钩子逻辑。例如，假设我们需要创建一个插件来处理自定义文件类型并将其转换为 JavaScript：\n\n```javascript\nconst fileRegex = /\\.(my-file-ext)$/\n\nexport default function transformFilePlugin() {\n  return {\n    name: 'vite-plugin-transform-file',\n    transform(src, id) {\n      if (fileRegex.test(id)) {\n        return {\n          code: compileFileToJS(src), // 将文件内容转换为 JavaScript\n          map: null, // 可以返回 source map\n        }\n      }\n    },\n  }\n}\n```\n\n- **transform**：此钩子对符合 `fileRegex` 正则表达式的文件（`.my-file-ext`）进行转换，并返回转换后的 JavaScript 代码。\n\n4️⃣ **插件使用**\n\n插件开发完成后，可以在 Vite 配置中使用：\n\n```javascript\nimport transformFilePlugin from 'vite-plugin-transform-file'\n\nexport default {\n  plugins: [transformFilePlugin()],\n}\n```\n\n5️⃣ **发布插件**\n\n开发完成后，插件可以通过 npm 发布，或者将其托管在 GitHub 上，方便团队或社区使用。\n\n> 参考博文：[https://juejin.cn/post/7270528132167417915](https://juejin.cn/post/7270528132167417915)\n\n:::\n\n## 如何在Vite中配置代理？\n\n::: details 参考答案\n在 Vite 中配置代理可以通过 `server.proxy` 选项来实现。以下是一个示例配置：\n\n```javascript\n// vite.config.js\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  server: {\n    proxy: {\n      // 代理 /api 请求到目标服务器\n      '/api': {\n        target: 'http://localhost:5000', // 目标服务器地址\n        changeOrigin: true, // 修改请求头中的 Origin 字段为目标服务器的 origin\n        secure: false, // 是否允许 HTTPS 请求\n        rewrite: (path) => path.replace(/^\\/api/, ''), // 重写请求路径，将 /api 替换为空\n      },\n\n      // 代理某些静态资源请求\n      '/assets': {\n        target: 'http://cdn-server.com', // 目标是静态资源服务器\n        changeOrigin: true,\n        rewrite: (path) => path.replace(/^\\/assets/, '/static'), // 将 /assets 路径重写为 /static\n      },\n    },\n  },\n})\n```\n\n:::\n\n## Vite如何集成TypeScript？如何配置？\n\n::: details 参考方案\n\nVite 对 TypeScript 提供了开箱即用的支持，无需额外安装插件。\n\n我们创建一个 `index.html` 文件并引入 `main.ts` 文件：\n\n```javascript\n<script src=\"./main.ts\" type=\"module\">\n  {' '}\n</script>\n```\n\n在 `main.ts` 中，可以写入一些 TypeScript 代码：\n\n```javascript\nlet tip: string = \"这是一个vite项目，使用了ts语法\";\nconsole.log('tip: ', tip);\n```\n\n运行 `vite` 后，可以看到控制台输出内容，表明 Vite 天生支持 TypeScript。\n\n在 Vite 项目中，虽然默认支持 TypeScript，但 Vite 本身不会阻止编译时出现 TypeScript 错误。为了更严格的类型检查和错误提示，我们需要配置 TypeScript。\n\n- 添加 TypeScript 配置（如果没有）\n\n通过以下命令生成 `tsconfig.json` 配置文件\n\n```plain\nnpx tsc --init\n```\n\n创建好 `tsconfig.json` 后，Vite 会根据该配置文件来编译 TypeScript。\n\n- 强化 TypeScript 错误提示\n\nVite 默认不会阻止编译时的 TypeScript 错误。如果我们想要在开发时严格检查 TypeScript 错误并阻止编译，可以使用 `vite-plugin-checker` 插件。\n\n```javascript\nnpm i vite - plugin - checker--save - dev\n```\n\n然后在 `vite.config.ts` 中引入并配置该插件：\n\n```typescript\n// vite.config.ts\nimport checker from 'vite-plugin-checker'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [checker({ typescript: true })],\n})\n```\n\n这样，任何 TypeScript 语法错误都会在控制台显示，并阻止编译。\n\n- 打包时进行 TypeScript 检查\n\n虽然 Vite 只会执行 `.ts` 文件的转译，而不会执行类型检查，但我们可以通过以下方式确保在打包时进行 TypeScript 类型检查。\n\n修改 `package.json` 配置\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc --noEmit && vite build\"\n  }\n}\n```\n\n`tsc --noEmit` 会执行类型检查，但不会生成编译后的文件。如果存在类型错误，打包过程会被阻止。\n\n- TypeScript 智能提示\n\nVite 默认为 `import.meta.env` 提供了类型定义，但是对于自定义的 `.env` 文件，TypeScript 的智能提示默认不生效。为了实现智能提示，可以在 `src` 目录下创建一个 `env.d.ts` 文件：\n\n```typescript\n/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n  readonly VITE_APP_TITLE: string\n  readonly VITE_APP_HAHA: string\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv\n}\n```\n\n> 参考博文:https://juejin.cn/post/7177210200330829885\n\n:::\n\n## 什么是 Webpack？它的作用是什么？\n\n参考答案\n\n::: details\n\nWebpack 是一个开源的 **前端静态模块打包工具**，主要用于将现代 JavaScript 应用中的各种资源（代码、样式、图片等）转换为优化的静态文件。它是现代前端开发的核心工具之一，尤其在复杂项目中扮演着关键角色。\n\n**Webpack 的核心作用**\n\n1. **模块化支持**\n\n   - **解决问题**：将代码拆分为多个模块（文件），管理依赖关系。\n   - **支持语法**：\n\n     - ES Modules ( `import/export` )\n     - CommonJS ( `require/module.exports` )\n     - AMD 等模块化方案。\n\n```javascript\n// 模块化开发\nimport Header from './components/Header.js'\nimport styles from './styles/main.css'\n```\n\n2. **资源整合**\n   - **处理非 JS 文件**：将 CSS、图片、字体、JSON 等资源视为模块，统一管理。\n\n```javascript\n// webpack.config.js\nmodule.exports = {\n  module: {\n    rules: [\n      {\n        test: /\\.css$/,\n        use: ['style-loader', 'css-loader'],\n      },\n      {\n        test: /\\.(png|svg)$/,\n        type: 'asset/resource',\n      },\n    ],\n  },\n}\n```\n\n3. **代码优化**\n\n   - **功能**：\n\n     - **Tree Shaking**：删除未使用的代码。\n     - **代码分割（Code Splitting）**：按需加载代码，减少首屏体积。\n     - **压缩**：减小文件体积，提升加载速度。\n\n```javascript\n// 动态导入实现按需加载\nbutton.addEventListener('click', () => {\n  import('./module.js').then((module) => module.run())\n})\n```\n\n4. **开发工具集成**\n\n   - **功能**：\n\n     - **热更新（HMR）**：实时预览代码修改效果。\n     - **Source Map**：调试时映射压缩代码到源代码。\n     - **本地服务器**：快速启动开发环境。\n\n```javascript\ndevServer: {\n        hot: true, // 启用热更新\n        open: true, // 自动打开浏览器\n    },\n    devtool: 'source-map', // 生成 Source Map\n```\n\n5. **生态扩展**\n   - **Loader**：处理特定类型文件（如 `.scss` → `.css` ）。\n   - **Plugin**：优化构建流程（如生成 HTML、压缩代码）。\n\n```javascript\nplugins: [\n    new HtmlWebpackPlugin({\n        template: './src/index.html'\n    }),\n    new MiniCssExtractPlugin(),\n],\n```\n\n**Webpack 的工作流程**\n\n1. **入口（Entry）**：从指定文件（如 `index.js`）开始分析依赖。\n2. **依赖图（Dependency Graph）**：递归构建模块间的依赖关系。\n3. **加载器（Loaders）**：转换非 JS 资源（如编译 Sass、处理图片）。\n4. **插件（Plugins）**：在构建生命周期中执行优化任务。\n5. **输出（Output）**：生成优化后的静态文件（如 `bundle.js`）。\n\n**与其他工具对比**\n| **工具** | **定位** | **与 Webpack 的区别** |\n|----------------|-----------------------------|-------------------------------------------|\n| Gulp/Grunt | 任务运行器（Task Runner） | 处理文件流，但无模块化支持 |\n| Rollup | 库打包工具 | 更适合库开发，Tree Shaking 更激进 |\n| Vite | 新一代构建工具 | 基于原生 ESM，开发环境更快，生产依赖 Rollup |\n\n**适用场景**\n\n- **单页应用（SPA）**：如 React、Vue、Angular 项目。\n- **复杂前端工程**：多页面、微前端架构。\n- **静态网站生成**：结合 Markdown、模板引擎使用。\n\nWebpack 通过 **模块化整合**、**代码优化** 和 **开发效率提升**，解决了前端工程中资源管理混乱、性能瓶颈和开发体验差的问题。它不仅是打包工具，更是现代前端工程化的基础设施。\n\n:::\n\n## 如何使用 Webpack 配置多环境的不同构建配置？\n\n参考答案\n\n::: details\n\n在 Webpack 中配置多环境（如开发环境、测试环境、生产环境）的构建配置，可以通过 **环境变量注入** 和 **配置合并** 的方式实现。\n\n**步骤 1：安装依赖工具**\n\n```bash\nnpm install webpack-merge cross-env --save-dev\n```\n\n- **webpack-merge**：用于合并基础配置和环境专属配置。\n- **cross-env**：跨平台设置环境变量（兼容 Windows 和 macOS/Linux）。\n\n**步骤 2：创建配置文件结构**\n\n```\nproject/\n├── config/\n│   ├── webpack.common.js    # 公共配置\n│   ├── webpack.dev.js       # 开发环境配置\n│   └── webpack.prod.js      # 生产环境配置\n├── src/\n│   └── ...                  # 项目源码\n└── package.json\n```\n\n**步骤 3：编写公共配置 ( `webpack.common.js` )**\n\n```javascript\n// config/webpack.common.js\nconst path = require('path')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\n\nmodule.exports = {\n  entry: './src/index.js',\n  output: {\n    filename: '[name].bundle.js',\n    path: path.resolve(__dirname, '../dist'),\n    clean: true,\n  },\n  plugins: [\n    new HtmlWebpackPlugin({\n      template: './src/index.html',\n    }),\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: 'babel-loader',\n      },\n      {\n        test: /\\.css$/,\n        use: ['style-loader', 'css-loader'],\n      },\n    ],\n  },\n}\n```\n\n**步骤 4：编写环境专属配置**\n\n开发环境 ( `webpack.dev.js` )\n\n```javascript\n// config/webpack.dev.js\nconst { merge } = require('webpack-merge')\nconst common = require('./webpack.common.js')\nconst webpack = require('webpack')\n\nmodule.exports = merge(common, {\n  mode: 'development',\n  devtool: 'eval-source-map',\n  devServer: {\n    hot: true,\n    open: true,\n    port: 3000,\n  },\n  plugins: [\n    // 注入环境变量（可在代码中通过 process.env.API_URL 访问）\n    new webpack.DefinePlugin({\n      'process.env.API_URL': JSON.stringify('https://dev.api.com'),\n      'process.env.NODE_ENV': JSON.stringify('development'),\n    }),\n  ],\n})\n```\n\n生产环境 ( `webpack.prod.js` )\n\n```javascript\n// config/webpack.prod.js\nconst { merge } = require('webpack-merge')\nconst common = require('./webpack.common.js')\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin')\nconst webpack = require('webpack')\n\nmodule.exports = merge(common, {\n  mode: 'production',\n  devtool: 'source-map',\n  optimization: {\n    minimizer: [\n      '...', // 保留默认的 JS 压缩配置\n      new CssMinimizerPlugin(),\n    ],\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env.API_URL': JSON.stringify('https://prod.api.com'),\n      'process.env.NODE_ENV': JSON.stringify('production'),\n    }),\n  ],\n})\n```\n\n**步骤 5：配置 `package.json` 脚本**\n\n```json\n{\n  \"scripts\": {\n    \"start\": \"cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js\",\n    \"build:dev\": \"cross-env NODE_ENV=development webpack --config config/webpack.dev.js\",\n    \"build:prod\": \"cross-env NODE_ENV=production webpack --config config/webpack.prod.js\"\n  }\n}\n```\n\n**步骤 6：在代码中使用环境变量**\n\n```javascript\n// src/index.js\nconsole.log('当前环境:', process.env.NODE_ENV)\nconsole.log('API 地址:', process.env.API_URL)\n\n// 根据不同环境执行不同逻辑\nif (process.env.NODE_ENV === 'development') {\n  console.log('这是开发环境')\n} else {\n  console.log('这是生产环境')\n}\n```\n\n**步骤 7：运行命令**\n\n```bash\n# 启动开发服务器（热更新）\nnpm run start\n\n# 构建开发环境产物\nnpm run build:dev\n\n# 构建生产环境产物\nnpm run build:prod\n```\n\n**扩展：支持更多环境（如测试环境）**\n\n1. 创建 `webpack.stage.js`\n\n```javascript\n// config/webpack.stage.js\nconst { merge } = require('webpack-merge')\nconst common = require('./webpack.common.js')\nconst webpack = require('webpack')\n\nmodule.exports = merge(common, {\n  mode: 'production',\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env.API_URL': JSON.stringify('https://stage.api.com'),\n      'process.env.NODE_ENV': JSON.stringify('staging'),\n    }),\n  ],\n})\n```\n\n2. 添加 `package.json` 脚本\n\n```json\n{\n  \"scripts\": {\n    \"build:stage\": \"cross-env NODE_ENV=staging webpack --config config/webpack.stage.js\"\n  }\n}\n```\n\n| **配置项**   | **开发环境**          | **生产环境**           | **测试环境**            |\n| ------------ | --------------------- | ---------------------- | ----------------------- |\n| `mode`       | `development`         | `production`           | `production`            |\n| `devtool`    | `eval-source-map`     | `source-map`           | `source-map`            |\n| `devServer`  | ✅ 启用               | ❌ 不启用              | ❌ 不启用               |\n| **代码压缩** | ❌ 不压缩             | ✅ CSS/JS 压缩         | ✅ CSS/JS 压缩          |\n| **环境变量** | `API_URL=dev.api.com` | `API_URL=prod.api.com` | `API_URL=stage.api.com` |\n\n:::\n\n## Webpack 的核心概念有哪些？请简单解释。\n\n参考答案\n\n::: details\n\nWebpack 的核心概念是理解其工作原理和配置的基础，以下是它们的简要解释：\n\n**1. 入口（Entry）**\n\n- **作用**：定义 Webpack **构建依赖图的起点**，通常为项目的主文件（如 `index.js`）。\n\n```javascript\nentry: './src/index.js', // 单入口\n    entry: {\n        app: './src/app.js',\n        admin: './src/admin.js'\n    }, // 多入口\n```\n\n**2. 出口（Output）**\n\n- **作用**：指定打包后的资源**输出位置和命名规则**。\n\n```javascript\noutput: {\n    filename: '[name].bundle.js', // 输出文件名（[name] 为入口名称）\n    path: path.resolve(__dirname, 'dist'), // 输出目录（绝对路径）\n    clean: true, // 自动清理旧文件（Webpack 5+）\n}\n```\n\n**3. 加载器（Loaders）**\n\n- **作用**：让 Webpack **处理非 JavaScript 文件**（如 CSS、图片、字体等），将其转换为有效模块。\n\n```javascript\nmodule: {\n    rules: [{\n            test: /\\.css$/,\n            use: ['style-loader', 'css-loader']\n        }, // 处理 CSS\n        {\n            test: /\\.(png|svg)$/,\n            type: 'asset/resource'\n        }, // 处理图片（Webpack 5+）\n    ],\n}\n```\n\n**4. 插件（Plugins）**\n\n- **作用**：扩展 Webpack 功能，干预**整个构建流程**（如生成 HTML、压缩代码、提取 CSS）。\n\n```javascript\nplugins: [\n  new HtmlWebpackPlugin({\n    template: './src/index.html',\n  }), // 生成 HTML\n  new MiniCssExtractPlugin(), // 提取 CSS 为独立文件\n]\n```\n\n**5. 模式（Mode）**\n\n- **作用**：预设优化策略，区分**开发环境**（`development`）和**生产环境**（`production`）。\n\n```javascript\nmode: 'production', // 启用代码压缩、Tree Shaking 等优化\n```\n\n**6. 模块（Modules）**\n\n- **作用**：Webpack 将每个文件视为**模块**（如 JS、CSS、图片），通过依赖关系构建依赖图。\n- **特点**：支持 ESM、CommonJS、AMD 等模块化语法。\n\n**7. 代码分割（Code Splitting）**\n\n- **作用**：将代码拆分为多个文件（chunks），实现**按需加载**或**并行加载**，优化性能。\n- **实现方式**：\n  - 动态导入（`import()`）\n  - 配置 `optimization.splitChunks`\n\n**8. Tree Shaking**\n\n- **作用**：通过静态分析**移除未使用的代码**，减小打包体积。\n- **前提**：使用 ES Module（`import/export`），并启用生产模式（`mode: 'production'`）。\n\n:::\n\n## 如何在 Webpack 中实现 CSS 和 Sass 的处理？\n\n参考答案\n\n::: details\n\n在 Webpack 中处理 CSS 和 Sass（SCSS）需要配置相应的加载器（loaders）和插件（plugins）。\n\n**1. 安装所需依赖**\n\n```bash\nnpm install --save-dev \\\n  style-loader \\\n  css-loader \\\n  sass-loader \\\n  sass \\\n  postcss-loader \\\n  autoprefixer \\\n  mini-css-extract-plugin \\\n  css-minimizer-webpack-plugin\n```\n\n- **核心依赖**：\n  - `style-loader`：将 CSS 注入 DOM。\n  - `css-loader`：解析 CSS 文件中的 `@import` 和 `url()`。\n  - `sass-loader`：将 Sass/SCSS 编译为 CSS。\n  - `sass`：Sass 编译器（Dart Sass 实现）。\n- **可选工具**：\n  - `postcss-loader` 和 `autoprefixer`：自动添加浏览器前缀。\n  - `mini-css-extract-plugin`：提取 CSS 为独立文件（生产环境推荐）。\n  - `css-minimizer-webpack-plugin`：压缩 CSS（生产环境推荐）。\n\n**2. 基础 Webpack 配置**\n在 `webpack.config.js` 中添加以下规则和插件：\n\n**配置 CSS 和 SCSS 处理**\n\n```javascript\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin')\n\nmodule.exports = {\n  module: {\n    rules: [\n      // 处理 CSS 文件\n      {\n        test: /\\.css$/,\n        use: [\n          // 开发环境用 style-loader，生产环境用 MiniCssExtractPlugin.loader\n          process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',\n          'css-loader',\n          'postcss-loader', // 可选：添加浏览器前缀\n        ],\n      },\n      // 处理 SCSS/Sass 文件\n      {\n        test: /\\.(scss|sass)$/,\n        use: [\n          process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',\n          'css-loader',\n          'postcss-loader', // 可选：添加浏览器前缀\n          'sass-loader',\n        ],\n      },\n    ],\n  },\n  plugins: [\n    // 提取 CSS 为独立文件（生产环境）\n    new MiniCssExtractPlugin({\n      filename: '[name].[contenthash].css',\n    }),\n  ],\n  optimization: {\n    minimizer: [\n      // 压缩 CSS（生产环境）\n      new CssMinimizerPlugin(),\n    ],\n  },\n}\n```\n\n**3. 配置 PostCSS（可选）**\n创建 `postcss.config.js` 文件以启用 `autoprefixer` ：\n\n```javascript\nmodule.exports = {\n  plugins: [\n    require('autoprefixer')({\n      // 指定浏览器兼容范围\n      overrideBrowserslist: ['last 2 versions', '>1%', 'not dead'],\n    }),\n  ],\n}\n```\n\n通过配置 `css-loader` 、 `sass-loader` 和 `MiniCssExtractPlugin` ，Webpack 可以高效处理 CSS 和 Sass。关键点包括：\n\n1. 加载器顺序：从右到左（如 `[sass-loader, css-loader, style-loader]`）。\n2. 生产环境提取 CSS：使用 `MiniCssExtractPlugin`。\n3. 浏览器兼容性：通过 `postcss-loader` 和 `autoprefixer` 自动处理。\n\n:::\n\n## Webpack 中的入口和出口是什么？\n\n参考答案\n\n::: details\n\n在 Webpack 中，**入口（Entry）** 和 **出口（Output）** 是配置文件中的核心概念，决定了打包的起点和终点。它们共同定义了 Webpack 如何处理代码以及最终生成的资源。\n\n1. **入口（Entry）**\n   入口是 Webpack 构建依赖图的起点，它告诉 Webpack：**“从哪个文件开始分析代码的依赖关系？”**\n\n**作用**\n\n- 指定应用程序的起始文件。\n- 根据入口文件递归构建依赖关系树。\n- 支持单入口（单页面应用）或多入口（多页面应用）。\n\n**配置方式**\n在 `webpack.config.js` 中通过 `entry` 属性配置：\n\n```javascript\nmodule.exports = {\n  entry: './src/index.js', // 单入口（默认配置）\n\n  // 多入口（多页面应用）\n  entry: {\n    home: './src/home.js',\n    about: './src/about.js',\n  },\n}\n```\n\n**默认行为**\n\n- 如果未手动配置 `entry`，Webpack 默认使用 `./src/index.js` 作为入口。\n\n2. **出口（Output）**\n   出口是 Webpack 打包后的资源输出位置，它告诉 Webpack：**“打包后的文件放在哪里？如何命名？”**\n\n**作用**\n\n- 定义打包文件的输出目录和命名规则。\n- 处理静态资源的路径（如 CSS、图片等）。\n\n**配置方式**\n在 `webpack.config.js` 中通过 `output` 属性配置：\n\n```javascript\nconst path = require('path')\n\nmodule.exports = {\n  output: {\n    path: path.resolve(__dirname, 'dist'), // 输出目录（必须为绝对路径）\n    filename: 'bundle.js', // 单入口输出文件名\n\n    // 多入口时，使用占位符确保唯一性\n    filename: '[name].[contenthash].js',\n    clean: true, // 自动清理旧文件（Webpack 5+）\n  },\n}\n```\n\n**常用占位符**\n| 占位符 | 说明 |\n|---------------------|-------------------------------|\n| `[name]` | 入口名称（如多入口的 `home` ） |\n| `[hash]` | 根据构建生成的唯一哈希值 |\n| `[contenthash]` | 根据文件内容生成的哈希值 |\n| `[chunkhash]` | 根据代码块生成的哈希值 |\n\n:::\n\n## Webpack 中的 Loaders 和 Plugins 有什么区别\n\n参考答案\n\n::: details\n\n在 Webpack 中，**Loaders（加载器）** 和 **Plugins（插件）** 是构建流程中的两大核心概念，它们的作用和职责有明显区别。\n\n**1. 核心区别总结**\n| **特性** | **Loaders** | **Plugins** |\n|----------------|---------------------------------|------------------------------------|\n| **主要作用** | **转换文件内容**（如转译、预处理） | **扩展构建流程**（优化、资源管理、注入环境变量等） |\n| **执行时机** | 在模块加载时（文件转换为模块时） | 在整个构建生命周期（从初始化到输出）的各个阶段 |\n| **配置方式** | 通过 `module.rules` 数组配置 | 通过 `plugins` 数组配置（需要 `new` 实例化） |\n| **典型场景** | 处理 JS/CSS/图片等文件转译 | 生成 HTML、压缩代码、提取 CSS 等全局操作 |\n| **依赖关系** | 针对特定文件类型（如 `.scss` ） | 不依赖文件类型，可干预整个构建流程 |\n\n**2. Loaders 的作用与使用**\n**核心功能**\n\n- 将非 JavaScript 文件（如 CSS、图片、字体等）**转换为 Webpack 能处理的模块**。\n- 对代码进行预处理（如 Babel 转译、Sass 编译）。\n\n**配置示例**\n\n```javascript\n// webpack.config.js\nmodule.exports = {\n  module: {\n    rules: [\n      // 处理 CSS 文件\n      {\n        test: /\\.css$/,\n        use: ['style-loader', 'css-loader'],\n      },\n      // 处理 TypeScript 文件\n      {\n        test: /\\.tsx?$/,\n        use: 'ts-loader',\n      },\n      // 处理图片文件\n      {\n        test: /\\.(png|jpg|gif)$/,\n        type: 'asset/resource', // Webpack 5 内置方式（替代 file-loader）\n      },\n    ],\n  },\n}\n```\n\n**常见 Loaders**\n\n- `babel-loader`: 将 ES6+ 代码转译为 ES5。\n- `css-loader`: 解析 CSS 中的 `@import` 和 `url()`。\n- `sass-loader`: 将 Sass/SCSS 编译为 CSS。\n- `file-loader`: 处理文件（如图片）的导入路径。\n\n**3. Plugins 的作用与使用**\n**核心功能**\n\n- 扩展 Webpack 的能力，干预构建流程的**任意阶段**。\n- 执行更复杂的任务，如代码压缩、资源优化、环境变量注入等。\n\n**配置示例**\n\n```javascript\n// webpack.config.js\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\n\nmodule.exports = {\n  plugins: [\n    // 自动生成 HTML 文件，并注入打包后的资源\n    new HtmlWebpackPlugin({\n      template: './src/index.html',\n    }),\n    // 提取 CSS 为独立文件\n    new MiniCssExtractPlugin({\n      filename: '[name].[contenthash].css',\n    }),\n  ],\n}\n```\n\n**常见 Plugins**\n\n- `HtmlWebpackPlugin`: 生成 HTML 文件并自动引入打包后的资源。\n- `MiniCssExtractPlugin`: 将 CSS 提取为独立文件（替代 `style-loader`）。\n- `CleanWebpackPlugin`: 清理构建目录（Webpack 5 中可用 `output.clean: true` 替代）。\n- `DefinePlugin`: 注入全局常量（如 `process.env.NODE_ENV`）。\n\n**4. 执行流程对比**\n**Loaders 的执行流程**\n\n```plaintext\n文件资源 (如 .scss) → 匹配 Loader 规则 → 按顺序应用 Loaders → 转换为 JS 模块\n```\n\n- **顺序关键**：Loaders 从右到左（或从下到上）执行。\n  例如： `use: ['style-loader', 'css-loader', 'sass-loader']` 的执行顺序为：\n  `sass-loader` → `css-loader` → `style-loader` 。\n\n**Plugins 的执行流程**\n\n```plaintext\n初始化 → 读取配置 → 创建 Compiler → 挂载 Plugins → 编译模块 → 优化 → 输出\n```\n\n- **生命周期钩子**：Plugins 通过监听 Webpack 的[生命周期钩子](https://webpack.js.org/api/compiler-hooks/)（如 `emit`、`done`）干预构建流程。\n\n**5. 协作示例**\n一个同时使用 Loaders 和 Plugins 的典型场景：\n\n```javascript\n// webpack.config.js\nconst path = require('path')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\n\nmodule.exports = {\n  entry: './src/index.js',\n  output: {\n    filename: 'bundle.js',\n    path: path.resolve(__dirname, 'dist'),\n    clean: true,\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.scss$/,\n        // Loaders 处理链：sass → css → MiniCssExtractPlugin\n        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],\n      },\n    ],\n  },\n  plugins: [\n    // Plugin：提取 CSS 为文件\n    new MiniCssExtractPlugin(),\n    // Plugin：生成 HTML\n    new HtmlWebpackPlugin(),\n  ],\n}\n```\n\n:::\n\n## Webpack中, 如何实现按需加载？\n\n参考答案\n\n::: details\n\n在 Webpack 中实现按需加载（代码分割/懒加载）的核心思路是 **将代码拆分为独立 chunk，在需要时动态加载**。\n\n**一、基础方法：动态导入（Dynamic Import）**\n通过 `import()` 语法实现按需加载，Webpack 会自动将其拆分为独立 chunk。\n\n**1. 代码中使用动态导入**\n\n```javascript\n// 示例：点击按钮后加载模块\ndocument.getElementById('btn').addEventListener('click', async () => {\n  const module = await import('./module.js')\n  module.doSomething()\n})\n```\n\n**2. 配置 Webpack**\n确保 `webpack.config.js` 的 `output` 配置中包含 `chunkFilename` ：\n\n```javascript\nmodule.exports = {\n  output: {\n    filename: '[name].bundle.js',\n    chunkFilename: '[name].[contenthash].chunk.js', // 动态导入的 chunk 命名规则\n    path: path.resolve(__dirname, 'dist'),\n    publicPath: '/', // 确保 chunk 的公共路径正确\n  },\n}\n```\n\n**二、框架集成：React/Vue 路由级按需加载**\n结合前端框架的路由系统实现组件级懒加载。\n\n**React 示例**\n\n```javascript\nimport React, { Suspense, lazy } from 'react'\nimport { BrowserRouter as Router, Route, Switch } from 'react-router-dom'\n\nconst Home = lazy(() => import('./routes/Home'))\nconst About = lazy(() => import('./routes/About'))\n\nfunction App() {\n  return (\n    <Router>\n      <Suspense fallback={<div> Loading... </div>}>\n        {' '}\n        <Switch>\n          <Route exact path=\"/\" component={Home} />{' '}\n          <Route\n            path=\"/about\n        \"\n            component={About}\n          />{' '}\n        </Switch>{' '}\n      </Suspense>{' '}\n    </Router>\n  )\n}\n```\n\n**Vue 示例**\n\n```javascript\nconst routes = [\n  {\n    path: '/',\n    component: () => import('./views/Home.vue'),\n  },\n  {\n    path: '/about',\n    component: () => import('./views/About.vue'),\n  },\n]\n```\n\n**三、优化配置：代码分割策略**\n通过 `SplitChunksPlugin` 优化公共代码提取。\n\n**Webpack 配置**\n\n```javascript\nmodule.exports = {\n  optimization: {\n    splitChunks: {\n      chunks: 'all', // 对所有模块进行分割（包括异步和非异步）\n      cacheGroups: {\n        vendors: {\n          test: /[\\\\/]node_modules[\\\\/]/,\n          name: 'vendors', // 提取 node_modules 代码为 vendors 块\n          priority: 10, // 优先级\n          reuseExistingChunk: true,\n        },\n        common: {\n          minChunks: 2, // 被至少两个 chunk 引用的代码\n          name: 'common',\n          priority: 5,\n          reuseExistingChunk: true,\n        },\n      },\n    },\n  },\n}\n```\n\n**四、Babel 配置（如需支持旧浏览器）**\n安装 Babel 插件解析动态导入语法：\n\n```bash\nnpm install @babel/plugin-syntax-dynamic-import --save-dev\n```\n\n在 `.babelrc` 或 `babel.config.json` 中添加插件：\n\n```json\n{\n  \"plugins\": [\"@babel/plugin-syntax-dynamic-import\"]\n}\n```\n\n**五、预加载与预取（可选优化）**\n通过注释提示浏览器提前加载资源（需结合框架使用）。\n\n**React 示例**\n\n```javascript\nconst About = lazy(\n  () =>\n    import(\n      /* webpackPrefetch: true */ // 预取（空闲时加载）\n      /* webpackPreload: true */ // 预加载（与父 chunk 并行加载）\n      './routes/About'\n    )\n)\n```\n\n**六、验证效果**\n\n1. **构建产物分析**：\n\n   - 运行 `npx webpack --profile --json=stats.json` 生成构建报告。\n   - 使用 [Webpack Bundle Analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) 可视化分析 chunk 分布。\n\n2. **网络请求验证**：\n   - 打开浏览器开发者工具，观察触发动态导入时是否加载新 chunk。\n\n:::\n\n## 什么是 Tree Shaking？如何在 Webpack 中启用它？\n\n参考答案\n\n::: details\n\n**Tree Shaking（摇树优化）** 是一种在打包过程中 **移除 JavaScript 项目中未使用代码（Dead Code）** 的优化技术。它的名字形象地比喻为“摇动树以掉落枯叶”，即通过静态代码分析，识别并删除未被引用的模块或函数，从而减小最终打包体积。\n\n**Tree Shaking 的工作原理**\n\n1. **基于 ES Module（ESM）的静态结构**\n   ESM 的 `import/export` 是静态声明（代码执行前可确定依赖关系），而 CommonJS 的 `require` 是动态的。只有 ESM 能被 Tree Shaking 分析。\n2. **标记未使用的导出**\n   打包工具（如 Webpack）通过分析代码，标记未被任何模块导入的导出。\n3. **压缩阶段删除**\n   结合代码压缩工具（如 Terser）删除这些标记的未使用代码。\n\n**在 Webpack 中启用 Tree Shaking 的步骤**\n**1. 使用 ES Module 语法**\n确保项目代码使用 `import/export` ，而非 CommonJS 的 `require` 。\n\n```javascript\n// ✅ 正确：ESM 导出\nexport function add(a, b) {\n  return a + b\n}\nexport function subtract(a, b) {\n  return a - b\n}\n\n// ✅ 正确：ESM 导入\nimport { add } from './math'\n\n// ❌ 错误：CommonJS 导出\nmodule.exports = {\n  add,\n  subtract,\n}\n```\n\n**2. 配置 Webpack 的 `mode` 为 `production` **\n在 `webpack.config.js` 中设置 `mode: 'production'` ，这会自动启用 Tree Shaking 和代码压缩。\n\n```javascript\nmodule.exports = {\n  mode: 'production', // 启用生产模式优化\n  // ...\n}\n```\n\n**3. 禁用模块转换（Babel 配置）**\n确保 Babel 不会将 ESM 转换为 CommonJS。在 `.babelrc` 或 `babel.config.json` 中设置：\n\n```json\n{\n  \"presets\": [\n    [\"@babel/preset-env\", { \"modules\": false }] // 保留 ESM 语法\n  ]\n}\n```\n\n**4. 标记副作用文件（可选）**\n在 `package.json` 中声明哪些文件有副作用（如全局 CSS、Polyfill），避免被错误删除：\n\n```json\n{\n  \"sideEffects\": [\n    \"**/*.css\", // CSS 文件有副作用（影响样式）\n    \"src/polyfill.js\" // Polyfill 有副作用\n  ]\n}\n```\n\n若项目无副作用文件，直接设为 `false` ：\n\n```json\n{\n  \"sideEffects\": false\n}\n```\n\n**5. 显式配置 `optimization.usedExports` **\n在 `webpack.config.js` 中启用 `usedExports` ，让 Webpack 标记未使用的导出：\n\n```javascript\nmodule.exports = {\n  optimization: {\n    usedExports: true, // 标记未使用的导出\n    minimize: true, // 启用压缩（删除未使用代码）\n  },\n}\n```\n\n**验证 Tree Shaking 是否生效**\n**方法 1：检查打包后的代码**\n若未使用的函数（如 `subtract` ）被删除，说明 Tree Shaking 生效：\n\n```javascript\n// 打包前 math.js\nexport function add(a, b) {\n  return a + b\n}\nexport function subtract(a, b) {\n  return a - b\n}\n\n// 打包后（仅保留 add）\nfunction add(a, b) {\n  return a + b\n}\n```\n\n**方法 2：使用分析工具**\n通过 [Webpack Bundle Analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) 可视化分析打包结果：\n\n```bash\nnpm install --save-dev webpack-bundle-analyzer\n```\n\n配置 `webpack.config.js` ：\n\n```javascript\nconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin\n\nmodule.exports = {\n  plugins: [new BundleAnalyzerPlugin()],\n}\n```\n\n运行构建后，浏览器将自动打开分析页面，检查未使用的模块是否被移除。\n\n| **步骤**             | **关键配置**                         | **作用**                     |\n| -------------------- | ------------------------------------ | ---------------------------- |\n| 使用 ESM 语法        | `import/export`                      | 提供静态分析基础             |\n| 设置生产模式         | `mode: 'production'`                 | 自动启用 Tree Shaking 和压缩 |\n| 配置 Babel           | `\"modules\": false`                   | 保留 ESM 结构                |\n| 标记副作用文件       | `package.json` 的 `sideEffects` 字段 | 防止误删有副作用的文件       |\n| 显式启用 usedExports | `optimization.usedExports: true`     | 标记未使用的导出             |\n\n:::\n"
  },
  {
    "path": "docs/second-exam/mini-program.md",
    "content": "# 小程序\n\n小程序是很容易入门和掌握的技术栈，如果你技术栈偏窄，可以考虑补充一下小程序的知识。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 小程序双线程架构\n\n参考答案\n\n::: details\n\n**1. 架构组成**\n\n**（1）逻辑层（Service）**\n\n- **运行环境**：独立的 JavaScript 线程（如 JavaScriptCore 或 V8 引擎）。\n- **职责**：\n  - 处理业务逻辑（数据请求、事件响应、状态管理）。\n  - 调用小程序 API（如网络请求、本地存储）。\n  - 通过 `setData` 向渲染层传递数据。\n- **特点**：\n  - **无法直接操作 DOM**：与渲染层隔离，避免恶意脚本攻击。\n  - **单例运行**：全局状态统一管理（如 App 和 Page 对象）。\n\n**（2）渲染层（View）**\n\n- **运行环境**：WebView 线程（每个页面独立实例）。\n- **职责**：\n  - 解析 WXML/WXSS，渲染页面结构。\n  - 处理用户交互事件（点击、滑动），触发逻辑层响应。\n- **特点**：\n  - **数据驱动更新**：根据逻辑层传递的数据动态渲染。\n  - **轻量化**：不执行复杂逻辑，保障渲染流畅性。\n\n**（3）系统层（Native）**\n\n- **作用**：作为逻辑层与渲染层的通信桥梁，提供原生能力支持。\n- **核心功能**：\n  - **JSBridge**：序列化传递数据（JSON 格式）。\n  - **安全管控**：拦截非法操作（如直接访问 DOM）。\n  - **原生 API**：调用摄像头、地理位置等硬件功能。\n\n![wxyl](../imgs/wxyl.png)\n\n:::\n\n## 直接修改 this.data 为何不会触发视图更新？\n\n参考答案\n\n::: details\n\n小程序中直接修改 `this.data` 不会触发视图更新的原因如下：\n\n**1. 数据更新机制的设计**\n\n小程序采用 **显式更新** 策略，只有通过 `this.setData()` 方法修改数据时，才会触发以下流程：\n\n- **数据变更通知**：将修改的数据标记为“脏数据”（需更新）。\n- **通信到渲染层**：通过 JSBridge 将数据序列化后传递到 WebView 线程。\n- **视图差异化更新**：渲染层对比新旧数据差异，仅更新变化的 DOM 节点。\n\n直接修改 `this.data` 仅改变逻辑层的数据，但 **未触发上述流程**，因此渲染层无法感知数据变化。\n\n**2. 双线程架构的限制**\n\n小程序的逻辑层（Service）与渲染层（View）运行在独立线程中：\n\n- **逻辑层**：通过 JavaScriptCore 或 V8 引擎运行。\n- **渲染层**：在 WebView 中解析 WXML/WXSS。\n\n两者通过 **异步通信**（JSBridge）传递数据。\n\n直接修改 `this.data` 不会触发系统层的数据传递，导致渲染层无法同步更新。\n\n**3. 性能优化考量**\n\n若每次数据修改都自动触发更新：\n\n- **频繁通信开销**：高频数据变更（如循环中修改数据）会导致线程间通信阻塞。\n- **不必要的渲染**：中间状态的数据变更可能引发多次无效渲染。\n\n通过 `this.setData()` 的 **批量合并更新机制**，可优化性能：\n\n```javascript\n// 合并多次更新，仅触发一次通信和渲染\nthis.setData({ a: 1 })\nthis.setData({ b: 2 })\n// 等效于\nthis.setData({ a: 1, b: 2 })\n```\n\n**4. 数据一致性与安全性**\n\n- **脏数据风险**：直接修改 `this.data` 可能导致逻辑层与渲染层数据不一致。\n- **状态管理规范**：强制使用 `this.setData()` 确保数据变更可追踪，符合单向数据流原则。\n\n:::\n\n## setData 底层做了哪些性能优化处理？\n\n参考答案\n\n::: details\n\n**1. 核心优化机制**\n\n**(1) 数据通信优化**\n\n- **差异化更新（Diff 算法）**\n  对比新旧数据树，仅序列化并传输变化的部分。例如：\n\n  ```javascript\n  // 旧数据：{ a: 1, list: [{ id: 1 }, { id: 2 }] }\n  this.setData({ 'list[1].id': 3 })\n  // 实际传输：{ 'list[1].id': 3 }（而非整个 list 数组）\n  ```\n\n  **优化效果**：减少 60%~80% 的数据传输量。\n\n- **序列化过滤**\n  自动过滤 `undefined`、`Function`、`Symbol` 等不可序列化数据，避免无效通信。\n\n**(2) 更新调度优化**\n\n- **批量合并（Batching）**\n  同一事件循环内的多次 `setData` 调用合并为一次更新：\n\n  ```javascript\n  this.setData({ a: 1 })\n  this.setData({ b: 2 })\n  // 合并为 { a: 1, b: 2 }，触发单次通信\n  ```\n\n  **优化场景**：高频操作（如动画帧更新、滚动事件）。\n\n- **异步队列与优先级调度**\n  用户交互触发的更新优先级高于数据请求，优先保障交互流畅性。\n\n**(3) 渲染层优化**\n\n- **虚拟 DOM 对比（Virtual DOM Diff）**\n  生成最小化的 DOM 操作指令，避免全量渲染：\n- **WXS 脚本加速**\n  在渲染层直接处理轻量逻辑（如数据格式化），减少逻辑层通信：\n  ```wxml\n  <wxs module=\"utils\">\n    function formatPrice(price) { return '¥' + price; }\n    module.exports = { formatPrice };\n  </wxs>\n  <view>{{utils.formatPrice(100)}}</view>\n  ```\n\n**(4) 通信协议优化**\n\n- **二进制传输（如 Protocol Buffers）**\n  替代 JSON 序列化，体积减少 30%~50%，解析速度提升 2~5 倍。\n- **通道复用与流量控制**\n  复用 JSBridge 通道，避免频繁建立连接，满负荷时自动排队。\n\n:::\n\n## this.setData({ list: largeDataArray }) 有问题吗？\n\n参考答案\n\n::: details\n\n在小程序开发中，使用 `this.setData({ list: largeDataArray })` 传递一个大型数据数组（尤其是包含成千上万条数据时）**确实存在明显的性能问题**。\n\n**1. 核心问题分析**\n\n**(1) 数据传输瓶颈**\n\n- **JSBridge 序列化开销**：数据需从逻辑层（Service）序列化为 JSON 字符串，通过 JSBridge 传递到渲染层（View），数据量越大，序列化和传输时间越长。\n- **典型耗时**：传输 10,000 条数据（每条 100B）约耗时 **100~300ms**（中低端手机更久）。\n\n**(2) 渲染性能问题**\n\n- **DOM 节点爆炸**：渲染层需解析数据并生成大量 DOM 节点，导致：\n  - **内存占用高**：每个 DOM 节点消耗 0.1~1KB 内存，10,000 条数据可能占用 **1~10MB**。\n  - **渲染卡顿**：首次渲染或滚动时出现明显卡顿（帧率低于 30fps）。\n\n**(3) 频繁 GC（垃圾回收）**\n\n- **内存抖动**：频繁创建和销毁大型临时对象，触发 JavaScript 引擎垃圾回收，导致间歇性卡顿。\n\n**2. 优化方案**\n\n**(1) 分页加载（懒加载）**\n\n- **实现方式**：\n  ```javascript\n  Page({\n    data: { list: [], page: 0 },\n    onReachBottom() {\n      // 滚动到底部加载下一页\n      this.loadNextPage()\n    },\n    loadNextPage() {\n      const nextPageData = fetchData(this.data.page + 1)\n      this.setData({\n        list: this.data.list.concat(nextPageData),\n        page: this.data.page + 1,\n      })\n    },\n  })\n  ```\n- **优点**：减少单次传输数据量，避免内存峰值。\n\n**(2) 虚拟列表（按需渲染）**\n\n- **原理**：仅渲染可视区域内的元素。\n- **实现库**：使用 `wx-component` 或第三方库（如 `recycle-view`）。\n  ```xml\n  <!-- 微信小程序示例 -->\n  <recycle-view wx:for=\"{{list}}\" wx:key=\"id\">\n    <view>{{item.text}}</view>\n  </recycle-view>\n  ```\n- **优点**：渲染 100 万条数据时，内存占用仅 **1~2MB**。\n\n**(3) 纯数据字段（Pure Data）**\n\n- **适用场景**：需要存储数据但无需渲染的字段。\n  ```javascript\n  Component({\n    options: { pureDataPattern: /^_/ },\n    data: {\n      _fullList: largeDataArray, // 不触发渲染\n      visibleList: largeDataArray.slice(0, 20),\n    },\n  })\n  ```\n\n**(4) 数据压缩**\n\n- **精简字段**：\n\n  ```javascript\n  // 原始数据\n  const rawData = [{ id: 1, title: '...', desc: '...' /* 10+ 字段 */ }]\n\n  // 优化后\n  const optimizedData = rawData.map(({ id, title }) => ({ id, title }))\n  this.setData({ list: optimizedData })\n  ```\n\n- **压缩率**：减少 50%~80% 数据体积。\n\n**(5) WebWorker 计算**\n\n```javascript\n// 在 Worker 中处理数据\nconst worker = wx.createWorker('workers/data-handler.js')\nworker.postMessage({ action: 'filter', data: largeDataArray })\nworker.onMessage((res) => {\n  this.setData({ list: res.filteredData })\n})\n```\n\n**(6) 原生组件替代**\n\n- 使用 `<canvas>` 或 `<web-view>` 渲染超大数据（如地图、可视化图表）。\n\n:::\n\n## 小程序登录流程\n\n参考答案\n\n::: details\n\n![wxlogin](../imgs/api-login.jpg)\n\n**说明**\n\n- 调用 `wx.login()` 获取 临时登录凭证 `code`，并回传到开发者服务器。\n- 调用 `auth.code2Session` 接口，换取 用户唯一标识 `OpenID` 、 用户在微信开放平台账号下的唯一标识 `UnionID`（若当前小程序已绑定到微信开放平台账号） 和 会话密钥 `session_key`。\n\n之后开发者服务器可以根据用户标识来生成自定义登录态，用于后续业务逻辑中前后端交互时识别用户身份。\n\n**注意事项**\n\n- 会话密钥 `session_key` 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全，开发者服务器不应该把会话密钥下发到小程序，也不应该对外提供这个密钥。\n- 临时登录凭证 `code` 只能使用一次。\n\n**特殊字段**\n\n- `openid`：openid 是用来唯一标识用户的一个字符串。在微信小程序中，每个用户的 openid 都是唯一的。通过 openid，小程序可以获取用户的基本信息，如头像、昵称等。\n\n> 【注意】同一个用户在不同的小程序中拥有不同的openid。因此，在开发小程序时，不能使用openid来进行用户的唯一性判断。\n\n- `unionid`：unionid 是在用户绑定同一微信开放平台账号下的多个应用时，用来唯一标识用户的一个字符串。如果用户在多个小程序中使用同一个微信号进行登录授权，那么这些小程序中的 unionid 都是相同的。\n\n> 【注意】用户的 unionid 只有在用户将多个应用绑定到同一个微信开放平台账号下时才会生成。因此，如果用户没有绑定多个应用，那么小程序将无法获取用户的 unionid。\n\n- `code`：code 是用户登录凭证，由微信服务器颁发给小程序。在用户授权登录后，小程序可以通过调用微信登录接口获取用户的 code。然后，通过 code 向微信服务器请求用户的 `openid` 和 `session_key` 等信息。\n\n> 【注意】每个 code 只能使用一次，且有效期为 5 分钟。因此，在使用 code 进行登录时，需要及时将其转换成用户的 openid 和 session_key 等信息，以免出现 code 过期的情况。\n\n:::\n\n## 如何在不发版的情况下实现小程序的 AB 测试？\n\n参考答案\n\n::: details\n\n在小程序中实现无需发版的 AB 测试，可通过 **动态配置 + 数据驱动** 的方案完成。\n\n1. **云端配置管理**\n\n- **创建 AB 测试规则**：\n\n  ```json\n  // 示例配置（存储在云数据库/Redis）\n  {\n    \"experiment_id\": \"2023_button_color\",\n    \"groups\": [\n      {\n        \"name\": \"group_a\",\n        \"ratio\": 50, // 50%流量\n        \"params\": { \"button_color\": \"#FF0000\" }\n      },\n      {\n        \"name\": \"group_b\",\n        \"ratio\": 50,\n        \"params\": { \"button_color\": \"#00FF00\" }\n      }\n    ],\n    \"salt\": \"user_id\" // 分流依据\n  }\n  ```\n\n- **动态更新**：通过管理后台随时调整分组比例和参数\n\n2. **客户端分组逻辑**\n\n```javascript\n// 工具函数：一致性哈希分流\nfunction getABGroup(experimentId, userId) {\n  const hash = crypto\n    .createHash('md5')\n    .update(experimentId + userId)\n    .digest('hex')\n  const value = parseInt(hash.slice(0, 8), 16) % 100\n  return value < 50 ? 'group_a' : 'group_b' // 按比例分配\n}\n\n// 小程序启动时获取配置\nwx.cloud.callFunction({\n  name: 'getABConfig',\n  success: (res) => {\n    const userId = getApp().globalData.userId\n    const group = getABGroup(res.data.experiment_id, userId)\n    this.setData({ abParams: res.data.groups.find((g) => g.name === group).params })\n  },\n})\n```\n\n3. **界面动态渲染**\n\n```html\n<!-- WXML 根据配置渲染 -->\n<button style=\"background-color: {{abParams.button_color}};\" bindtap=\"handleClick\">立即购买</button>\n```\n\n4. **数据埋点上报**\n\n```javascript\n// 点击事件处理\nhandleClick() {\n  wx.reportAnalytics('button_click', {\n    experiment_id: '2023_button_color',\n    group: this.data.abGroup,\n    button_color: this.data.abParams.button_color\n  });\n}\n```\n\n5. **数据分析阶段**\n\n- **指标定义**：\n\n  ```sql\n  -- 示例：计算转化率差异\n  SELECT\n    group,\n    COUNT(DISTINCT user_id) AS total_users,\n    SUM(is_converted) / COUNT(DISTINCT user_id) AS conversion_rate\n  FROM\n    ab_test_events\n  WHERE\n    experiment_id = '2023_button_color'\n  GROUP BY\n    group;\n  ```\n\n- **统计显著性检验**：使用 T 检验或卡方检验验证结果可靠性\n\n**关键技术细节**\n\n1. **流量分配算法**\n\n- **分层采样**：不同实验独立分流（避免流量干扰）\n- **Sticky Bucket**：确保用户始终处于同一分组\n  ```javascript\n  // 本地缓存分组结果\n  const storedGroup = wx.getStorageSync(experimentId)\n  if (storedGroup) return storedGroup\n  ```\n\n2. **动态更新策略**\n\n- **定时轮询**：每 5 分钟检查配置更新\n- **WebSocket 推送**：实时生效新配置\n\n3. **灰度发布控制**\n\n```yaml\n# 云配置示例：分阶段放量\nrollout:\n  - stage: 1\n    percentage: 10%\n    start_time: 2025-01-01\n  - stage: 2\n    percentage: 100%\n    start_time: 2025-01-03\n```\n\n:::\n\n## 小程序的增量更新\n\n参考答案\n\n::: details\n\n小程序的增量更新机制主要依赖于小程序平台的设计\n\n- 当小程序开发者发布新版本时，小程序平台会比较新旧两个版本的差异，并生成一个包含差异信息的补丁文件。\n- 然后，当用户打开小程序时，小程序平台会检查用户设备上的小程序版本。\n- 如果发现用户的版本落后于服务器上的版本，那么就会下载补丁文件，而不是整个新版本的代码包。\n- 接着，小程序平台会应用补丁文件，将用户设备上的小程序更新到新版本。\n\n:::\n\n## 小程序性能优化\n\n参考答案\n\n::: details\n\n微信 IDE 的小程序评分功能位于调试器 -> Audits 面板中\n\n小程序性能优化的具体维度：\n\n1. 避免过大的 WXML 节点数目\n2. 避免执行脚本的耗时过长的情况\n3. 避免首屏时间太长的情况\n4. 避免渲染界面的耗时过长的情况\n5. 对网络请求做必要的缓存以避免多余的请求\n6. 所有请求的耗时不应太久\n7. 避免 setData 的调用过于频繁\n8. 避免 setData 的数据过大\n9. 避免短时间内发起太多的图片请求\n10. 避免短时间内发起太多的请求\n\n:::\n\n## 小程序 WXSS 与 CSS 的区别？\n\n参考答案\n\n::: details\n\n- wxss 背景图片只能引入外链，不能使用本地图片\n- 小程序样式使用 @import 引入 外联样式文件，地址为相对路径。\n- 尺寸单位为 rpx , rpx 是响应式像素,可以根据屏幕宽度进行自适应。\n\n:::\n\n## 小程序里拿不到 dom 相关的 api ？\n\n参考答案\n\n::: details\n\n微信小程序使用类似 Web 的 WXML 和 WXSS 语言来描述页面结构和样式，但并不提供直接操作 DOM 的 API。这主要有两个原因：\n\n- 首先，小程序运行在 JavaScriptCore 引擎中，而非浏览器中常见的 V8 引擎。\n\n  - 由于两者在实现方式上存在较大差异，JavaScriptCore 的执行速度相对较慢。\n  - 直接操作 DOM 会增加耗时，从而降低性能和用户体验。\n\n- 其次，小程序的设计初衷是提供一种轻量、快速启动的应用模式，其定位强调“去中心化、低门槛、高灵活性”。\n  - 如果允许开发者直接操作 DOM，可能会破坏这一设计理念，增加系统复杂度和开发难度。\n\n:::\n\n## 分包加载\n\n参考答案\n\n::: details\n\n小程序分包加载是一种优化技术，用于解决主包体积过大导致的首次加载性能问题。通过将非核心功能模块拆分为独立分包，实现按需加载和动态加载。\n\n一、分包加载核心概念\n\n1. **包类型**\n\n| **类型**     | **说明**                             | **特点**                     |\n| ------------ | ------------------------------------ | ---------------------------- |\n| **主包**     | 包含启动页面、核心公共组件和基础库   | 用户首次打开小程序时必须下载 |\n| **普通分包** | 依赖主包的功能模块，按需加载         | 可访问主包资源               |\n| **独立分包** | 不依赖主包的完整功能模块，可独立运行 | 无法访问主包资源             |\n\n2. **体积限制**\n\n| **包类型**   | **最大体积** | **总包体积限制**    |\n| ------------ | ------------ | ------------------- |\n| 主包         | 2MB          | 20MB (所有分包总和) |\n| 单个普通分包 | 2MB          |                     |\n| 单个独立分包 | 2MB          |                     |\n\n二、分包配置实现\n\n1. **目录结构**\n\n```bash\n├── app.js               # 主包入口\n├── app.json             # 分包配置\n├── subpackages          # 分包目录\n│   ├── user-center      # 普通分包\n│   │   ├── pages\n│   │   └── components\n│   └── shop             # 独立分包\n│       ├── app.js       # 独立分包入口\n│       └── pages\n└── common               # 公共代码（主包）\n```\n\n2. **app.json 配置**\n\n```json\n{\n  \"pages\": [\"pages/index/index\"], // 主包页面\n  \"subpackages\": [\n    {\n      \"root\": \"subpackages/user-center\",\n      \"name\": \"user\",\n      \"pages\": [\"profile\", \"settings\"],\n      \"independent\": false // 普通分包\n    },\n    {\n      \"root\": \"subpackages/shop\",\n      \"name\": \"shop\",\n      \"pages\": [\"home\", \"detail\"],\n      \"independent\": true // 独立分包\n    }\n  ],\n  \"preloadRule\": {\n    \"pages/index/index\": {\n      \"network\": \"all\",\n      \"packages\": [\"user\"] // 预加载普通分包\n    }\n  }\n}\n```\n\n三、分包加载策略\n\n1. **按需加载**\n\n- **普通分包**：当用户首次访问分包内页面时触发下载\n- **独立分包**：通过 `wx.navigateTo` 指定 `isIndependent` 参数加载\n  ```javascript\n  wx.navigateTo({\n    url: '/subpackages/shop/pages/home',\n    isIndependent: true,\n  })\n  ```\n\n2. **预加载优化**\n\n```json\n// app.json 预加载配置\n\"preloadRule\": {\n  \"pages/index/index\": {\n    \"packages\": [\"user\"],    // 预加载分包名\n    \"network\": \"wifi\"        // 仅WiFi下预加载\n  }\n}\n```\n\n**策略建议**：\n\n- 预加载不超过 **2个** 分包\n- 仅预加载用户可能访问的高频分包\n\n3. **懒加载配合**\n\n```javascript\n// 点击事件触发加载\nonTapShop() {\n  require('../../subpackages/shop/shop.js'); // 动态导入\n  wx.navigateTo({ url: '/subpackages/shop/pages/home' });\n}\n```\n\n:::\n\n## 冷启动与热启动的区别\n\n参考答案\n\n::: details\n\n**核心区别对比**\n\n| **对比维度**     | **冷启动**                          | **热启动**                           |\n| ---------------- | ----------------------------------- | ------------------------------------ |\n| **触发条件**     | 首次打开或销毁后重新打开            | 后台存活状态下重新唤醒               |\n| **资源加载**     | 重新下载代码包、初始化页面          | 直接从内存恢复页面                   |\n| **启动速度**     | 较慢（需完整加载）                  | 较快（无需重新初始化）               |\n| **生命周期流程** | 执行 `App.onLaunch` → `Page.onLoad` | 仅触发 `App.onShow` 和 `Page.onShow` |\n| **内存占用**     | 重新分配内存                        | 复用原有内存                         |\n| **数据状态**     | 全局数据需重新初始化                | 保留之前的运行状态                   |\n| **存活时间**     | 无限制                              | 默认后台存活 **5分钟**               |\n| **典型场景**     | 用户首次打开或主动杀死进程后重启    | 切换回微信聊天后重新进入             |\n\n**冷启动优化方案**\n\n1. **代码包瘦身**：\n\n   - 主包控制在 **2MB** 以内\n   - 使用分包加载（单个分包 ≤2MB）\n\n   ```javascript\n   // app.json 分包配置\n   {\n     \"subpackages\": [{\n       \"root\": \"subpackage\",\n       \"pages\": [\"pageA\", \"pageB\"]\n     }]\n   }\n   ```\n\n2. **预加载策略**：\n\n   ```javascript\n   // 提前加载非首屏必要资源\n   wx.loadSubpackage({\n     name: 'subpackage',\n     success: () => console.log('分包预加载完成'),\n   })\n   ```\n\n3. **缓存关键数据**：\n   ```javascript\n   // 冷启动时读取缓存\n   App({\n     onLaunch() {\n       const cache = wx.getStorageSync('userInfo')\n       if (cache) this.globalData.userInfo = cache\n     },\n   })\n   ```\n\n**热启动优化方案**\n\n1. **状态保持**：\n\n   ```javascript\n   // 页面隐藏时保存状态\n   Page({\n     onHide() {\n       wx.setStorageSync('pageState', this.data)\n     },\n   })\n   ```\n\n2. **内存管理**：\n\n   - 避免在全局对象中存储过大数据\n   - 及时清理无用定时器/事件监听\n\n   ```javascript\n   // 页面卸载时清理资源\n   Page({\n     onUnload() {\n       clearInterval(this.timer)\n       this.eventListener.close()\n     },\n   })\n   ```\n\n3. **后台保活策略**：\n   ```javascript\n   // 播放背景音频延长存活时间\n   wx.playBackgroundAudio({\n     dataUrl: 'silent.mp3', // 无声音频\n   })\n   ```\n\n**异常场景处理**\n\n| **场景**         | **冷启动表现** | **热启动表现**           |\n| ---------------- | -------------- | ------------------------ |\n| **代码更新**     | 强制下载新包   | 下次冷启动生效           |\n| **网络中断**     | 可能导致白屏   | 已加载内容仍可操作       |\n| **内存不足**     | 正常启动       | 可能被系统回收转为冷启动 |\n| **全局数据变更** | 重新初始化     | 保持最后一次修改值       |\n\n**调试技巧**\n\n1. **强制冷启动**：\n\n   ```javascript\n   // 开发阶段模拟冷启动\n   wx.reLaunch({ url: '/pages/index' })\n   ```\n\n2. **内存状态检查**：\n\n   ```javascript\n   // 查看当前内存占用\n   console.log(wx.getPerformance())\n   // 输出: { memory: 1024, ... }\n   ```\n\n3. **生命周期追踪**：\n   ```javascript\n   // 监听所有生命周期事件\n   const originalOnShow = Page.prototype.onShow\n   Page.prototype.onShow = function () {\n     console.log('Page.onShow triggered')\n     originalOnShow.call(this)\n   }\n   ```\n\n通过理解冷/热启动的差异，开发者可针对性优化小程序性能，建议将 **冷启动耗时控制在 1.5 秒内**，**热启动耗时控制在 300 毫秒内**，以达到最佳用户体验。\n\n:::\n\n## 组件通信方案\n\n参考答案\n\n::: details\n\n一、**父子组件通信**\n\n1. 父 → 子：Properties 传递\n\n```javascript\n// 父组件\n;<child-comp prop-data=\"{{parentData}}\" />\n\n// 子组件 properties 定义\nComponent({\n  properties: {\n    propData: { type: Object, value: {} },\n  },\n})\n```\n\n2. 子 → 父：自定义事件\n\n```javascript\n// 子组件触发事件\nthis.triggerEvent('customEvent', { value: data })\n\n// 父组件监听\n<child-comp bind:customEvent=\"handleEvent\" />\nPage({\n  handleEvent(e) {\n    console.log(e.detail.value)\n  }\n})\n```\n\n二、**逆向父组件访问**\n\n3. 获取父组件实例\n\n```javascript\n// 父组件设置 id\n;<child-comp id=\"childRef\" />\n\n// 父组件通过 selectComponent 获取\nPage({\n  getChild() {\n    const child = this.selectComponent('#childRef')\n    child.childMethod() // 调用子组件方法\n  },\n})\n```\n\n三、**兄弟组件通信**\n\n4. 共同父组件中转\n\n```mermaid\ngraph LR\nA[父组件] --> B[子组件A]\nA --> C[子组件B]\nB -- 事件 --> A\nA -- 更新数据 --> C\n```\n\n5. 全局事件总线\n\n```javascript\n// app.js 中创建事件中心\nApp({\n  eventBus: {\n    listeners: {},\n    on(event, fn) {\n      /* 监听 */\n    },\n    emit(event, data) {\n      /* 触发 */\n    },\n  },\n})\n\n// 组件 A 发送\nconst app = getApp()\napp.eventBus.emit('update', data)\n\n// 组件 B 接收\nComponent({\n  attached() {\n    app.eventBus.on('update', this.handleUpdate)\n  },\n})\n```\n\n四、**跨层级通信**\n\n6. 全局状态管理\n\n```javascript\n// app.js 定义共享数据\nApp({\n  globalData: {\n    userInfo: null,\n  },\n})\n\n// 任意组件读取/写入\nconst app = getApp()\napp.globalData.userInfo = { name: 'John' }\n\n// 监听变化（需手动实现）\nlet observer = null\nComponent({\n  attached() {\n    observer = setInterval(() => {\n      console.log(app.globalData.userInfo)\n    }, 500)\n  },\n  detached() {\n    clearInterval(observer)\n  },\n})\n```\n\n7. 页面间通信\n\n```javascript\n// PageA 跳转传参\nwx.navigateTo({\n  url: '/pages/pageB?id=123',\n})\n\n// PageB 获取参数\nPage({\n  onLoad(options) {\n    console.log(options.id) // 123\n  },\n})\n\n// 返回传参（需配合 getCurrentPages）\nconst pages = getCurrentPages()\nconst prevPage = pages[pages.length - 2]\nprevPage.setData({ feedback: 'success' })\n```\n\n五、**高级通信模式**\n\n8. 组件关系 (relations)\n\n```javascript\n// parent.json\n{\n  \"component\": true,\n  \"usingComponents\": {\n    \"child-comp\": \"../child/child\"\n  },\n  \"relations\": {\n    \"../child/child\": {\n      \"type\": \"child\",\n      \"linked(target) { /* 子组件插入时 */ }\"\n    }\n  }\n}\n\n// parent.js\nmethods: {\n  broadcastToChildren(data) {\n    this.children.forEach(child => {\n      child.receiveData(data)\n    })\n  }\n}\n\n// child.js\nmethods: {\n  receiveData(data) {\n    this.setData({ received: data })\n  }\n}\n```\n\n**六、其它第三方库**\n\n9. 对于超大型项目，建议结合 `Vuex` 或 `MobX` 等状态管理库（需使用 `uni-app`/`Taro` 等框架）。\n\n**方案对比**\n\n| 方案            | 适用场景           | 优点               | 缺点                 |\n| --------------- | ------------------ | ------------------ | -------------------- |\n| Properties      | 父子简单数据传递   | 官方推荐，类型校验 | 单向数据流           |\n| 自定义事件      | 子向父传递操作     | 解耦合             | 多层传递较复杂       |\n| selectComponent | 父直接调用子方法   | 快速直接           | 破坏封装性           |\n| 全局事件总线    | 任意组件间通信     | 灵活度高           | 需手动管理监听/卸载  |\n| 全局状态        | 跨页面共享数据     | 集中管理           | 非响应式，需手动监听 |\n| 页面路由传参    | 页面间简单数据传递 | 官方支持           | 数据类型受限         |\n| relations       | 存在逻辑关联的组件 | 官方关系管理       | 配置较复杂           |\n\n**最佳实践建议**\n\n1. **优先选择官方方案**：对于父子通信，务必使用 `properties` + `triggerEvent`\n2. **复杂场景组合使用**：全局状态管理 + 事件总线应对跨层级通信\n3. **性能优化关键点**：\n   - 避免在 `globalData` 中存储过大数据\n   - 使用 `debounce` 控制高频事件触发\n   ```javascript\n   // 防抖处理示例\n   let timer\n   function emitDebounced(event, data) {\n     clearTimeout(timer)\n     timer = setTimeout(() => {\n       app.eventBus.emit(event, data)\n     }, 300)\n   }\n   ```\n4. **内存泄漏防范**：\n   ```javascript\n   Component({\n     detached() {\n       // 必须移除全局监听\n       app.eventBus.off('update', this.handleUpdate)\n     },\n   })\n   ```\n   :::\n\n## wx:if 与 hidden 的区别\n\n参考答案\n\n::: details\n\n小程序中 `wx:if` 与 `hidden` 的区别主要体现在 **渲染机制**、**性能影响** 和 **使用场景** 上。\n\n**1. 核心区别**\n| **特性** | **wx:if** | **hidden** |\n|-------------------------|-----------------------------------------------|---------------------------------------------|\n| **渲染机制** | 条件为 `true` 时渲染组件，否则 **不渲染** | 始终渲染组件，通过 `display: none` **隐藏** |\n| **DOM 结构** | 条件不满足时 **移除组件节点** | 组件节点 **始终存在**，仅样式隐藏 |\n| **生命周期** | 切换时触发 `attached`/`detached` 生命周期 | 无生命周期触发，仅样式变化 |\n| **状态保留** | 条件切换后 **状态重置**（如输入框内容清空） | 隐藏时 **保留状态**（如输入框内容不变） |\n| **性能开销** | 适合 **低频切换**（减少初始渲染节点） | 适合 **高频切换**（避免重复创建/销毁节点） |\n| **使用语法** | 支持 `wx:if`/`wx:elif`/`wx:else` 链式条件判断 | 仅接受布尔值（`hidden=\"{{condition}}\"`） |\n\n**2. 使用场景对比**\n\n**(1) 推荐使用 `wx:if` 的场景**\n\n- **初始不渲染**：页面加载时不需要显示的组件（减少首屏节点数）。\n- **复杂条件判断**：需要多分支逻辑（如 `wx:elif`）。\n  ```html\n  <view wx:if=\"{{score >= 90}}\">优秀</view>\n  <view wx:elif=\"{{score >= 60}}\">及格</view>\n  <view wx:else>不及格</view>\n  ```\n- **大数据量组件**：如长列表，避免隐藏时占用内存。\n\n**(2) 推荐使用 `hidden` 的场景**\n\n- **高频切换**：如 Tab 切换、模态框显示隐藏。\n  ```html\n  <view hidden=\"{{!showPanel}}\">控制面板</view>\n  ```\n- **状态保留**：如表单输入中途需要临时隐藏。\n- **动画控制**：通过 CSS 过渡实现显示/隐藏动画。\n\n**3. 性能优化指南**\n| **场景** | **选择方案** | **理由** |\n|------------------------|-------------------|--------------------------------------------|\n| 首屏隐藏的大组件 | `wx:if` | 减少初始渲染节点，提升加载速度 |\n| 频繁切换的组件 | `hidden` | 避免重复创建/销毁，减少性能开销 |\n| 需要保留状态的表单 | `hidden` | 隐藏时保留输入内容 |\n| 多条件分支渲染 | `wx:if` | 语法支持更灵活 |\n\n**4. 底层原理**\n\n- **wx:if**：\n  通过 Virtual DOM 动态添加/删除组件节点，触发完整生命周期。\n\n- **hidden**：\n  仅修改 CSS 的 `display` 属性，不涉及节点操作。\n  ```javascript\n  // 伪代码实现\n  component.setStyle({\n    display: condition ? 'block' : 'none',\n  })\n  ```\n  :::\n\n## Taro/Uni-app 跨端原理对比\n\n参考答案\n\n::: details\n\n| 框架    | 技术栈    | 微信小程序 | H5  | App | 支付宝/百度小程序 |\n| ------- | --------- | ---------- | --- | --- | ----------------- |\n| Taro    | React/Vue | ✅         | ✅  | ✅  | ✅                |\n| uni-app | Vue       | ✅         | ✅  | ✅  | ✅                |\n| WePY    | Vue       | ✅         | ❌  | ❌  | ❌                |\n| mpvue   | Vue       | ✅         | ✅  | ❌  | ❌                |\n\n**1. Taro**\n\n京东凹凸实验室\n\n**优缺点**\n\n- Taro 在 App 端使用的是 React Native 的渲染引擎，原生的 UI 体验较好，但据说在实时交互和高响应要求的操作方面不是很理想。\n  微信小程序方面，结合度感觉没有那么顺滑，有一些常见功能还是需要自己去封装。\n- 另外就是开发环境难度稍高，需要自己去搭建 iOS 和 Android 的环境，对于想要一处开发到处应用的傻瓜式操作来讲，稍显繁琐。\n- 但 Taro 3 的出现，支持了 React 和 Vue 两种 DSL，适合的人群会更多一点，并且对快应用的支持也更好。\n\n**2. uni-app**\n\nDCloud\n\n**优缺点**\n\n- uni-app 在 App 渲染方面，提供了原生渲染引擎和小程序引擎的双选方案，加上自身的一些技术优化（renderjs），对于高性能和响应要求的场景展现得更为流畅。\n- 另外它整体的开发配套流程也做得很容易上手。比如有丰富的插件市场，使用简单，支持大量常用场景。\n- 还比如它的定制 IDE——HBuilder，提供了强大的整合能力。在用 HBuilder 之前，我心想：“还要多装一个编辑器麻烦，再好用能有 VS Code 好用？”用过之后：“真香！”\n- 虽然用惯了 VS Code 对比起来还是有一些痛点没有解决，但是对于跨平台开发太友好了，其他缺点都可以忍受。HBuilder 里支持直接跳转到微信开发者工具调试，支持真机实时预览，支持直接打包小程序和App，零门槛上手。\n\n:::\n\n## Taro 的实现原理\n\n参考答案\n\n::: details\n\n一、JSX 转换：Taro 通过 自定义 Babel/TypeScript 编译器 将 JSX 转换为通用虚拟 DOM。针对不同前端框架（React/Vue），在编译时生成对应框架的运行时代码，例如：\n\n```jsx\n// 输入\n;<View>Hello</View>\n\n// React 输出\nimport { createElement } from 'react'\ncreateElement('view', {}, 'Hello')\n\n// Vue 输出\nimport { h } from 'vue'\nh('view', {}, 'Hello')\n```\n\n二、多端适配：Taro 的核心架构分为 `编译时 和 运行时`：\n\n1. 编译时：通过 AST 解析将代码按目标平台转换，生成平台专属模板（如 .wxml / .swan）\n2. 运行时：\n\n- 实现 统一 API 层（如 Taro.request 映射到 wx.request / my.request）\n- 提供 虚拟 DOM 渲染器，通过 React Reconciler 对接不同平台渲染引擎\n- 实现 事件系统桥接，统一各端事件差异\n\n三、跨端样式处理：Taro 样式处理包含以下关键机制：\n\n1. 条件编译：通过 CSS 注释实现多平台样式隔离\n\n```css\n/* #ifdef weapp */\n.title {\n  color: red;\n}\n/* #endif */\n```\n\n2. 单位转换：将 px 按比例转为目标平台单位（如小程序 rpx）\n3. 作用域隔离：通过 CSS Modules 自动生成唯一类名\n4. JavaScript 样式：支持 styled-components 等 CSS-in-JS 方案\n\n四、构建系统：Taro 的构建系统特点：\n\n1. 插件化架构：通过 @tarojs/plugin- 前缀插件扩展功能\n2. 多编译引擎：\n\n- Web 端：仍使用 Webpack/Vite\n- 小程序：自研模板生成器\n- 按需编译：通过 Tree-shaking 仅打包使用到的组件\n\n五、运行时性能优化：\n\n1. 数据通信优化：\n\n- 自动合并 setData 调用\n- 使用 差异更新算法 减少数据传输量\n\n2. 渲染优化：\n\n- 虚拟 DOM 比对后批量更新\n- 组件按平台实现懒加载\n\n3. 包体积优化：\n\n- 按目标平台裁剪无用代码\n- 使用 分包加载 控制主包大小\n\n:::\n"
  },
  {
    "path": "docs/second-exam/nodejs.md",
    "content": "# Nodejs 面试题\n\n## 对 Node.js 的理解\n\n参考答案\n\n::: details\nNode.js 是一个基于 Chrome V8 引擎的开源、跨平台的 JavaScript 运行时环境。它具有以下核心特点：\n\n1. **运行环境**：让 JavaScript 可以在浏览器之外运行，使其成为一个服务器端的运行环境\n\n2. **非阻塞 I/O**：\n\n   - 采用非阻塞型 I/O 机制\n   - 执行 I/O 操作时不会造成阻塞\n   - 操作完成后通过事件通知执行回调函数\n   - 例如：执行数据库操作时，不需要等待数据返回，而是继续执行后续代码，数据库返回结果后再通过回调函数处理\n\n3. **事件驱动**：\n   - 基于事件循环（Event Loop）\n   - 新请求会被压入事件队列\n   - 通过循环检测队列中的事件状态变化\n   - 当检测到状态变化，执行对应的回调函数\n\n:::\n\n## Node.js 的优缺点\n\n参考答案\n\n::: details\n**优点：**\n\n1. 高并发处理能力强\n2. 适合 I/O 密集型应用\n3. 事件驱动非阻塞模式，程序执行效率高\n4. 使用 JavaScript，前后端可以使用同一种语言\n5. npm 生态系统非常强大\n\n**缺点：**\n\n1. 不适合 CPU 密集型应用\n2. 单线程模式，无法充分利用多核 CPU\n3. 可靠性相对较低，一旦出现未捕获的异常，整个程序可能崩溃\n4. 回调函数嵌套多时可能产生回调地狱\n\n:::\n\n## Node.js 应用场景\n\n参考答案\n\n::: details\n**最适合的场景：**\n\n1. I/O 密集型应用\n2. 实时交互应用\n3. 高并发请求处理\n\n**具体应用领域：**\n\n1. **Web 应用系统**\n\n   - 后台管理系统\n   - 用户表单收集系统\n   - 考试系统\n   - 高并发 Web 应用\n\n2. **实时通讯应用**\n\n   - 在线聊天室\n   - 实时通讯系统\n   - 图文直播系统\n   - WebSocket 应用\n\n3. **接口服务**\n\n   - RESTful API 服务\n   - 数据库操作接口\n   - 前端/移动端 API 服务\n\n4. **工具类应用**\n\n   - 构建工具（如 webpack）\n   - 开发工具\n   - 自动化脚本\n\n5. **微服务**\n   - 轻量级微服务\n   - 中间层服务（BFF）\n\n注意：虽然 Node.js 理论上可以开发各种应用，但在选择使用时应该考虑其是否适合特定场景，特别是需要避免在 CPU 密集型场景中使用。\n\n:::\n\n## Node.js 的全局对象有哪些？\n\n参考答案\n\n::: details\n\n在 Node.js 中，全局对象与浏览器环境不同。浏览器中的全局对象是 `window`，而 Node.js 中的全局对象是 `global`。需要注意的是，在 Node.js 模块中使用 `var` 声明的变量并不会成为全局变量，它们只在当前模块生效。\n\nNode.js 的全局对象可以分为两类：\n\n1. 真正的全局对象\n2. 模块级别的全局变量\n\n**真正的全局对象**\n\n1. **Buffer 类**\n\n   - 用于处理二进制数据\n   - 在 V8 堆外分配物理内存\n   - 创建后大小固定，不可更改\n   - 常用于文件操作、网络操作等场景\n\n2. **process**\n\n   - 提供当前 Node.js 进程信息\n   - 常用属性和方法：\n     - `process.env`：环境变量\n     - `process.argv`：命令行参数\n     - `process.cwd()`：当前工作目录\n     - `process.pid`：进程 ID\n     - `process.platform`：运行平台\n\n3. **console**\n\n   - `console.log()`：标准输出\n   - `console.error()`：错误输出\n   - `console.trace()`：打印调用栈\n   - `console.time()/timeEnd()`：计时器\n   - `console.clear()`：清空控制台\n\n4. **定时器函数**\n\n   - `setTimeout()/clearTimeout()`\n   - `setInterval()/clearInterval()`\n   - `setImmediate()/clearImmediate()`\n   - `process.nextTick()`\n\n5. **global**\n   - 全局命名空间对象\n   - 上述所有全局对象都是 `global` 的属性\n\n**模块级别的全局变量**\n\n这些变量虽然看起来是全局的，但实际上是每个模块独有的：\n\n1. **\\_\\_dirname**\n\n   - 当前模块的目录名\n   - 绝对路径\n\n   ```js\n   console.log(__dirname) // 输出：/当前目录的绝对路径\n   ```\n\n2. **\\_\\_filename**\n\n   - 当前模块的文件名\n   - 包含绝对路径\n\n   ```js\n   console.log(__filename) // 输出：/当前文件的绝对路径/文件名\n   ```\n\n3. **exports**\n\n   - 模块导出的快捷方式\n   - `module.exports` 的引用\n\n   ```js\n   exports.myFunction = () => {}\n   ```\n\n4. **module**\n\n   - 当前模块的引用\n   - 包含模块的元数据\n\n   ```js\n   module.exports = {\n     // 导出的内容\n   }\n   ```\n\n5. **require**\n   - 用于导入模块\n   - 可导入的内容：\n     - Node.js 核心模块\n     - 第三方模块\n     - 本地文件\n   ```js\n   const fs = require('fs')\n   const myModule = require('./myModule')\n   ```\n\n**注意事项**\n\n1. 模块级全局变量在 REPL（命令行交互）环境中不可用\n2. `exports` 是 `module.exports` 的引用，不能直接赋值\n3. Node.js 12 之后，还可以使用 `globalThis` 访问全局对象\n4. 某些全局对象在特定版本可能有变化，使用时需注意 Node.js 版本兼容性\n\n:::\n\n## Node.js 事件循环机制\n\n参考答案\n\n::: details\n\n事件循环是 Node.js 实现异步操作的核心机制，它允许 Node.js 执行非阻塞 I/O 操作。Node.js 是单线程的，但通过事件循环机制可以实现高并发。\n\n**事件循环的六个阶段**\n\n事件循环按照固定的顺序，循环执行以下六个阶段：\n\n1. **timers（定时器阶段）**\n\n   - 执行 `setTimeout` 和 `setInterval` 的回调\n   - 检查是否有到期的定时器\n\n2. **pending callbacks（待定回调阶段）**\n\n   - 执行延迟到下一个循环迭代的 I/O 回调\n   - 处理一些系统操作的回调（如 TCP 错误）\n\n3. **idle, prepare（仅系统内部使用）**\n\n   - 系统内部使用，不需要关注\n\n4. **poll（轮询阶段）**\n\n   - 检索新的 I/O 事件\n   - 执行 I/O 相关的回调\n   - 如果有必要会阻塞在这个阶段\n\n5. **check（检查阶段）**\n\n   - 执行 `setImmediate()` 的回调\n   - 在 poll 阶段结束后立即执行\n\n6. **close callbacks（关闭回调阶段）**\n   - 执行关闭事件的回调\n   - 如 `socket.on('close', ...)`\n\n**微任务和宏任务**\n\n在事件循环的每个阶段之间，会检查并执行微任务：\n\n**微任务（Microtasks）：**\n\n- `process.nextTick()`（优先级最高）\n- `Promise.then/catch/finally`\n- `queueMicrotask()`\n\n**宏任务（Macrotasks）：**\n\n- `setTimeout`\n- `setInterval`\n- `setImmediate`\n- I/O 操作\n\n**执行顺序示例**\n\n```js\nconsole.log('1: 同步代码')\n\nsetTimeout(() => {\n  console.log('2: setTimeout')\n}, 0)\n\nPromise.resolve().then(() => {\n  console.log('3: Promise')\n})\n\nprocess.nextTick(() => {\n  console.log('4: nextTick')\n})\n\nsetImmediate(() => {\n  console.log('5: setImmediate')\n})\n\n// 输出顺序：\n// 1: 同步代码\n// 4: nextTick\n// 3: Promise\n// 2: setTimeout\n// 5: setImmediate\n```\n\n**注意事项**\n\n1. **process.nextTick 的特殊性**\n\n   - 不属于事件循环的任何阶段\n   - 在每个阶段结束时优先执行\n   - 过度使用可能导致 I/O 饥饿\n\n2. **定时器的精确性**\n\n   - `setTimeout` 和 `setInterval` 的延时不能保证精确\n   - 受进程繁忙程度影响\n\n3. **setImmediate vs setTimeout(fn, 0)**\n\n   - 主模块中执行顺序不确定\n   - I/O 回调中 `setImmediate` 优先级更高\n\n4. **异步错误处理**\n   - 推荐使用 async/await 和 try/catch\n   - 避免回调地狱\n\n**最佳实践**\n\n1. 避免在关键任务中依赖定时器的精确性\n2. 合理使用 `process.nextTick`，避免阻塞事件循环\n3. I/O 操作中优先使用 `setImmediate` 而不是 `setTimeout`\n4. 使用 Promise 或 async/await 处理异步操作\n5. 注意内存泄漏，及时清理不需要的事件监听器\n\n:::\n\n## Node.js 中的 process 对象\n\n参考答案\n\n::: details\n\nprocess 是 Node.js 中的一个全局对象，它提供了当前 Node.js 进程的信息和控制能力。作为进程，它是计算机系统进行资源分配和调度的基本单位，具有以下特点：\n\n- 每个进程都拥有独立的空间地址和数据栈\n- 进程间数据隔离，需通过进程间通信机制实现数据共享\n- Node.js 是单线程的，启动一个文件会创建一个主线程\n\n**常用属性和方法**\n\n1. 系统信息相关\n\n- **process.env**：环境变量对象\n\n  ```js\n  console.log(process.env.NODE_ENV) // 获取环境变量\n  ```\n\n- **process.platform**：运行平台\n\n  ```js\n  console.log(process.platform) // 'darwin' for macOS\n  ```\n\n- **process.version**：Node.js 版本\n  ```js\n  console.log(process.version) // 'v16.x.x'\n  ```\n\n2. 进程信息相关\n\n- **process.pid**：当前进程 ID\n- **process.ppid**：父进程 ID\n- **process.uptime()**：进程运行时间\n- **process.title**：进程名称\n  ```js\n  console.log(process.pid) // 进程ID\n  process.title = 'my-app' // 设置进程标题\n  ```\n\n3. 路径与命令行\n\n- **process.cwd()**：当前工作目录\n\n  ```js\n  console.log(process.cwd()) // 返回当前工作目录的绝对路径\n  ```\n\n- **process.argv**：命令行参数\n  ```js\n  // node app.js --port 3000\n  const args = process.argv.slice(2) // ['--port', '3000']\n  ```\n\n4. 事件循环相关\n\n- **process.nextTick(callback)**：下一个事件循环触发回调\n  ```js\n  process.nextTick(() => {\n    console.log('下一个事件循环执行')\n  })\n  ```\n\n5. 标准流操作\n\n- **process.stdout**：标准输出\n- **process.stdin**：标准输入\n- **process.stderr**：标准错误\n  ```js\n  process.stdout.write('Hello World\\n')\n  ```\n\n6. 事件监听\n\n- **进程异常处理**\n\n  ```js\n  process.on('uncaughtException', (err) => {\n    console.error('未捕获的异常：', err)\n  })\n  ```\n\n- **进程退出监听**\n  ```js\n  process.on('exit', (code) => {\n    console.log(`进程退出码：${code}`)\n  })\n  ```\n\n**使用注意事项**\n\n1. **process.nextTick 与 setTimeout 的区别**\n\n   - `process.nextTick` 在当前事件循环结束时执行\n   - `setTimeout(fn, 0)` 在下一个事件循环开始时执行\n   - `nextTick` 优先级更高\n\n2. **环境变量的使用**\n\n   ```js\n   // 推荐使用\n   const NODE_ENV = process.env.NODE_ENV || 'development'\n   ```\n\n3. **工作目录**\n\n   - `process.cwd()` 返回 Node.js 进程执行时的工作目录\n   - 与 `__dirname` 不同，`process.cwd()` 可能会随着工作目录的改变而改变\n\n4. **异常处理**\n   - 建议使用 `uncaughtException` 捕获未处理的异常\n   - 但不建议用它来代替正常的错误处理流程\n\n:::\n\n## Express middleware(中间件) 工作原理\n\n参考答案\n\n::: details\n\n中间件（Middleware）是 Express 的核心概念，它是一个函数，可以访问请求对象（req）、响应对象（res）和应用程序请求-响应周期中的下一个中间件函数（next）。\n\n**工作流程**\n\n1. **请求处理流程**\n\n   - 请求从上到下依次经过中间件\n   - 每个中间件可以对请求进行处理和修改\n   - 通过 next() 将请求传递给下一个中间件\n   - 如果不调用 next()，请求将终止\n\n2. **基本结构**\n\n```js\nfunction middleware(req, res, next) {\n  // 1. 处理请求\n  // 2. 修改请求或响应对象\n  // 3. 调用 next() 传递给下一个中间件\n  next()\n}\n```\n\n**中间件分类**\n\n1. **应用级中间件**\n\n```js\nconst app = express()\n\n// 全局中间件\napp.use((req, res, next) => {\n  console.log('Time:', Date.now())\n  next()\n})\n\n// 路由特定中间件\napp.use('/user', (req, res, next) => {\n  console.log('Request Type:', req.method)\n  next()\n})\n```\n\n2. **路由级中间件**\n\n```js\nconst router = express.Router()\n\nrouter.use((req, res, next) => {\n  console.log('Router Specific Middleware')\n  next()\n})\n```\n\n3. **错误处理中间件**\n\n```js\napp.use((err, req, res, next) => {\n  console.error(err.stack)\n  res.status(500).send('Something broke!')\n})\n```\n\n**执行顺序示例**\n\n```js\napp.use((req, res, next) => {\n  console.log('1. First Middleware')\n  next()\n})\n\napp.use((req, res, next) => {\n  console.log('2. Second Middleware')\n  next()\n})\n\napp.get('/api', (req, res) => {\n  console.log('3. Route Handler')\n  res.send('Hello')\n})\n\n// 访问 /api 时的输出：\n// 1. First Middleware\n// 2. Second Middleware\n// 3. Route Handler\n```\n\n**中间件特点**\n\n1. **顺序重要性**\n\n   - 中间件的注册顺序决定了执行顺序\n   - 错误处理中间件应该放在最后\n\n2. **功能独立性**\n\n   - 每个中间件负责特定功能\n   - 可以组合使用多个中间件\n\n3. **请求响应周期**\n   - 可以修改请求和响应对象\n   - 可以终止请求-响应周期\n   - 可以调用下一个中间件\n\n**常见使用场景**\n\n1. **请求日志记录**\n\n```js\napp.use((req, res, next) => {\n  console.log(`${req.method} ${req.url}`)\n  next()\n})\n```\n\n2. **身份验证**\n\n```js\nfunction authenticate(req, res, next) {\n  if (req.headers.authorization) {\n    next()\n  } else {\n    res.status(401).send('Unauthorized')\n  }\n}\n```\n\n3. **数据处理**\n\n```js\napp.use(express.json()) // 解析 JSON 请求体\napp.use(express.urlencoded({ extended: true })) // 解析 URL 编码的请求体\n```\n\n**最佳实践**\n\n1. **合理使用 next()**\n\n   - 除非终止请求，否则总是调用 next()\n   - 在异步操作中正确处理 next()\n\n2. **错误处理**\n\n   - 使用 try-catch 捕获同步错误\n   - 使用 Promise 处理异步错误\n   - 通过 next(error) 传递错误\n\n3. **中间件设计**\n   - 保持中间件功能单一\n   - 适当使用路由级中间件\n   - 避免中间件中的副作用\n\n:::\n\n## Koa 洋葱模型\n\n参考答案\n\n::: details\n\nKoa 的中间件模型被称为`\"洋葱模型\"`，这是因为请求和响应像洋葱一样，需要经过多层\"表皮\"（中间件）的处理。这个过程是：\n\n- 请求从外到内依次经过中间件的前置处理\n- 到达最里层后\n- 响应从内到外依次经过中间件的后置处理\n\n**工作原理**\n\n1. **执行流程**\n\n```js\nconst Koa = require('koa')\nconst app = new Koa()\n\n// 中间件1\napp.use(async (ctx, next) => {\n  console.log('1. 进入中间件1')\n  await next()\n  console.log('5. 离开中间件1')\n})\n\n// 中间件2\napp.use(async (ctx, next) => {\n  console.log('2. 进入中间件2')\n  await next()\n  console.log('4. 离开中间件2')\n})\n\n// 中间件3\napp.use(async (ctx) => {\n  console.log('3. 到达中间件3')\n  ctx.body = 'Hello World'\n})\n\n// 输出顺序：\n// 1. 进入中间件1\n// 2. 进入中间件2\n// 3. 到达中间件3\n// 4. 离开中间件2\n// 5. 离开中间件1\n```\n\n**特点说明**\n\n1. **异步处理**\n\n   - 通过 async/await 实现异步操作的同步写法\n   - 每个中间件都可以等待下一个中间件执行完成\n\n2. **双向流动**\n\n   - 请求阶段：从外到内\n   - 响应阶段：从内到外\n   - 可以在响应阶段对数据进行再处理\n\n3. **错误处理**\n\n```js\napp.use(async (ctx, next) => {\n  try {\n    await next()\n  } catch (err) {\n    ctx.status = err.status || 500\n    ctx.body = err.message\n    ctx.app.emit('error', err, ctx)\n  }\n})\n```\n\n**实际应用示例**\n\n1. **日志记录**\n\n```js\napp.use(async (ctx, next) => {\n  const start = Date.now()\n  await next()\n  const ms = Date.now() - start\n  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)\n})\n```\n\n2. **响应处理**\n\n```js\napp.use(async (ctx, next) => {\n  await next()\n  // 响应阶段可以修改返回数据\n  if (ctx.body) {\n    ctx.body = {\n      code: 0,\n      data: ctx.body,\n      message: 'success',\n    }\n  }\n})\n```\n\n:::\n\n## Koa 与 Express 的区别\n\n参考答案\n\n::: details\n\n1. **中间件机制**\n\n   - Express：单向流动，中间件通过 next() 线性执行，一旦响应结束就不能修改\n   - Koa：洋葱模型，中间件既可以处理请求也可以处理响应，支持统一的错误处理\n\n2. **异步处理**\n\n   - Express：基于回调函数，容易陷入回调地狱，异步错误处理相对复杂\n   - Koa：基于 Promise 和 async/await，代码更简洁，异步流程控制更直观\n\n3. **上下文对象**\n\n   - Express：req 和 res 是分离的对象，功能相对分散\n   - Koa：ctx 统一上下文，封装了 request 和 response，API 设计更简洁优雅\n\n4. **功能内置**\n\n   - Express：内置了很多中间件，功能齐全，开箱即用\n   - Koa：核心功能精简，需要通过第三方中间件扩展，更加灵活\n\n5. **路由系统**\n\n   - Express：内置了强大的路由系统，支持链式调用\n   - Koa：路由需要通过第三方中间件实现（如 koa-router）\n\n6. **社区生态**\n\n   - Express：历史更悠久，社区更成熟，资源更丰富\n   - Koa：较新但发展迅速，设计更现代，适合新项目\n\n7. **错误处理**\n\n   - Express：通过特殊的错误处理中间件，需要手动传递错误\n   - Koa：通过 try/catch 优雅地处理错误，统一的错误处理更方便\n\n8. **适用场景**\n   - Express：适合快速开发，现有项目迁移，团队熟悉度高\n   - Koa：适合追求优雅代码，需要更好的异步流程控制的场景\n\n:::\n\n> 未完待续...\n"
  },
  {
    "path": "docs/second-exam/react-inner.md",
    "content": "# React 原理\n\n国内面试，大厂必考原理。\n\n::: tip\n\n1. 目标**不在**中大厂的同学，可以略过这一节。\n2. 对 React 使用尚不熟练的同学，不要在此花费太多精力，先熟悉使用再说。\n\n:::\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## JSX 的本质是什么？\n\n参考答案\n\n::: details\n\n**JSX（JavaScript XML）** 是一个 JavaScript 的语法扩展，允许在 JavaScript 代码中通过类 HTML 语法创建 React 元素。它需要通过 Babel 等工具编译为标准的 JavaScript 代码，最终生成 **React 元素对象**（React Element），这些元素共同构成虚拟 DOM（Virtual DOM）树。\n\n**核心原理**\n\n1. **JSX 编译为 React 元素**\n   JSX 会被转换为 `React.createElement()` 调用（或 React 17+ 的 `_jsx` 函数），生成描述 UI 结构的对象（React 元素），而非直接操作真实 DOM。\n\n   ```jsx\n   // JSX\n   const element = <h1 className=\"title\">Hello, world!</h1>\n\n   // 编译后（React 17 之前）\n   const element = React.createElement('h1', { className: 'title' }, 'Hello, world!')\n\n   // 编译后（React 17+，自动引入 _jsx）\n   import { jsx as _jsx } from 'react/jsx-runtime'\n   const element = _jsx('h1', { className: 'title', children: 'Hello, world!' })\n   ```\n\n2. **虚拟 DOM 的运作**\n   - React 元素组成虚拟 DOM 树，通过 Diff 算法对比新旧树差异，最终高效更新真实 DOM。\n   - 虚拟 DOM 是内存中的轻量对象，避免频繁操作真实 DOM 的性能损耗。\n\n**JSX 的核心特性**\n\n1. **类 HTML 语法与 JavaScript 的融合**\n\n   - **表达式嵌入**：通过 `{}` 嵌入 JavaScript 表达式（如变量、函数调用、三元运算符）：\n     ```jsx\n     const userName = 'Alice'\n     const element = <p>Hello, {userName.toUpperCase()}</p>\n     ```\n   - **禁止语句**：`{}` 内不支持 `if`/`for` 等语句，需改用表达式（如三元运算符或逻辑与）：\n     ```jsx\n     <div>{isLoggedIn ? 'Welcome' : 'Please Login'}</div>\n     ```\n\n2. **语法规则**\n\n   - **属性命名**：使用驼峰命名（如 `className` 代替 `class`，`htmlFor` 代替 `for`）。\n   - **闭合标签**：所有标签必须显式闭合（如 `<img />`）。\n   - **单一根元素**：JSX 必须有唯一根元素（或用 `<></>` 空标签包裹）。\n\n3. **安全性**\n   - **默认 XSS 防护**：JSX 自动转义嵌入内容中的特殊字符（如 `<` 转为 `&lt;`）。\n   - **例外场景**：如需渲染原始 HTML，需显式使用 `dangerouslySetInnerHTML`（需谨慎）：\n     ```jsx\n     <div dangerouslySetInnerHTML={{ __html: userContent }} />\n     ```\n\n**编译与工具链**\n\n1. **编译流程**\n   JSX 需通过 **Babel** 编译为浏览器可执行的 JavaScript。典型配置如下：\n\n   ```json\n   // .babelrc\n   {\n     \"presets\": [\"@babel/preset-react\"]\n   }\n   ```\n\n2. **React 17+ 的优化**\n   - 无需手动导入 React：编译器自动引入 `_jsx` 函数。\n   - 更简洁的编译输出：减少代码体积，提升可读性。\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7348651815759282226\n\n:::\n\n## 如何理解 React Fiber 架构？\n\n参考答案\n\n::: details\n\n1. **Fiber 架构的本质与设计目标**\n\nFiber 是 React 16+ 的**核心算法重写**，本质是**基于链表的增量式协调模型**。其核心目标并非单纯提升性能，而是重构架构以实现：\n\n- **可中断的异步渲染**：将同步递归的调和过程拆解为可暂停/恢复的异步任务。\n- **优先级调度**：高优先级任务（如用户输入）可打断低优先级任务（如数据更新）。\n- **并发模式基础**：为 `Suspense`、`useTransition` 等特性提供底层支持。\n\n2. **Fiber 节点的核心设计**\n\n每个组件对应一个 **Fiber 节点**，构成**双向链表树结构**，包含以下关键信息：\n\n- **组件类型**：函数组件、类组件或原生标签。\n- **状态与副作用**：Hooks 状态（如 `useState`）、生命周期标记（如 `useEffect`）。\n- **调度信息**：任务优先级（`lane` 模型）、到期时间（`expirationTime`）。\n- **链表指针**：`child`（子节点）、`sibling`（兄弟节点）、`return`（父节点）。\n\n```javascript\n// Fiber 节点结构简化示例\nconst fiberNode = {\n  tag: FunctionComponent, // 组件类型\n  stateNode: ComponentFunc, // 组件实例或 DOM 节点\n  memoizedState: {\n    /* Hooks 链表 */\n  },\n  pendingProps: {\n    /* 待处理 props */\n  },\n  lanes: Lanes.HighPriority, // 任务优先级\n  child: nextFiber, // 子节点\n  sibling: null, // 兄弟节点\n  return: parentFiber, // 父节点\n}\n```\n\n3. **Fiber 协调流程（两阶段提交）**\n\n**阶段 1：Reconciliation（协调/渲染阶段）**\n\n- **可中断的增量计算**：\n  React 将组件树遍历拆解为多个 **Fiber 工作单元**，通过循环（而非递归）逐个处理。\n  - 每次循环执行一个 Fiber 节点，生成子 Fiber 并连接成树。\n  - 通过 `requestIdleCallback`（或 Scheduler 包）在浏览器空闲时段执行，避免阻塞主线程。\n- **对比策略**：\n  根据 `key` 和 `type` 复用节点，标记 `Placement`（新增）、`Update`（更新）、`Deletion`（删除）等副作用。\n\n**阶段 2：Commit（提交阶段）**\n\n- **不可中断的 DOM 更新**：\n  同步执行所有标记的副作用（如 DOM 操作、生命周期调用），确保 UI 一致性。\n- **副作用分类**：\n  - **BeforeMutation**：`getSnapshotBeforeUpdate`。\n  - **Mutation**：DOM 插入/更新/删除。\n  - **Layout**：`useLayoutEffect`、`componentDidMount`/`Update`。\n\n4. **优先级调度机制**\n\nReact 通过 **Lane 模型** 管理任务优先级（共 31 个优先级车道）：\n\n- **事件优先级**：\n  ```javascript\n  // 优先级从高到低\n  ImmediatePriority（用户输入）\n  UserBlockingPriority（悬停、点击）\n  NormalPriority（数据请求）\n  LowPriority（分析日志）\n  IdlePriority（非必要任务）\n  ```\n- **调度策略**：\n  - 高优先级任务可抢占低优先级任务的执行权。\n  - 过期任务（如 Suspense 回退）会被强制同步执行。\n\n5. **Fiber 架构的优势与局限性**\n\n**优势**\n\n- **流畅的用户体验**：异步渲染避免主线程阻塞，保障高优先级任务即时响应。\n- **复杂场景优化**：支持大规模组件树的高效更新（如虚拟滚动、动画串联）。\n- **未来特性基础**：为并发模式（Concurrent Mode）、离线渲染（SSR）提供底层支持。\n\n**局限性**\n\n- **学习成本高**：开发者需理解底层调度逻辑以优化性能。\n- **内存开销**：Fiber 树的双向链表结构比传统虚拟 DOM 占用更多内存。\n\n6. **与旧架构的关键差异**\n\n| 特性           | Stack Reconciler（React 15-） | Fiber Reconciler（React 16+） |\n| -------------- | ----------------------------- | ----------------------------- |\n| **遍历方式**   | 递归（不可中断）              | 循环（可中断 + 恢复）         |\n| **任务调度**   | 同步执行，阻塞主线程          | 异步分片，空闲时段执行        |\n| **优先级控制** | 无                            | 基于 Lane 模型的优先级抢占    |\n| **数据结构**   | 虚拟 DOM 树                   | Fiber 链表树（含调度信息）    |\n\n:::\n\n## Fiber 结构和普通 VNode 区别\n\n参考答案\n\n::: details\n\n1. **本质差异**\n\n| 维度         | 普通 VNode（虚拟 DOM）          | Fiber 结构                           |\n| ------------ | ------------------------------- | ------------------------------------ |\n| **设计目标** | 减少真实 DOM 操作，提升渲染性能 | 实现可中断的异步渲染 + 优先级调度    |\n| **数据结构** | 树形结构（递归遍历）            | 双向链表树（循环遍历）               |\n| **功能范畴** | 仅描述 UI 结构                  | 描述 UI 结构 + 调度任务 + 副作用管理 |\n\n2. **数据结构对比**\n\n**普通 VNode（React 15 及之前）**\n\n```javascript\nconst vNode = {\n  type: 'div', // 节点类型（组件/原生标签）\n  props: { className: 'container' }, // 属性\n  children: [vNode1, vNode2], // 子节点（树形结构）\n  key: 'unique-id', // 优化 Diff 性能\n  // 无状态、调度、副作用信息\n}\n```\n\n- **核心字段**：仅包含 UI 描述相关属性（type、props、children）。\n\n**Fiber 节点（React 16+）**\n\n```javascript\nconst fiberNode = {\n  tag: HostComponent, // 节点类型（函数组件/类组件/DOM元素）\n  type: 'div', // 原生标签或组件构造函数\n  key: 'unique-id', // Diff 优化标识\n  stateNode: domNode, // 关联的真实 DOM 节点\n  pendingProps: { className: 'container' }, // 待处理的 props\n  memoizedProps: {}, // 已生效的 props\n  memoizedState: {\n    // Hooks 状态（函数组件）\n    hooks: [state1, effectHook],\n  },\n  updateQueue: [], // 状态更新队列（类组件）\n  lanes: Lanes.HighPriority, // 调度优先级（Lane 模型）\n  child: childFiber, // 第一个子节点\n  sibling: siblingFiber, // 下一个兄弟节点\n  return: parentFiber, // 父节点（构成双向链表）\n  effectTag: Placement, // 副作用标记（插入/更新/删除）\n  nextEffect: nextEffectFiber, // 副作用链表指针\n}\n```\n\n- **核心扩展**：\n  - **调度控制**：`lanes` 优先级、任务到期时间。\n  - **状态管理**：Hooks 链表（函数组件）、类组件状态队列。\n  - **副作用追踪**：`effectTag` 标记和副作用链表。\n  - **遍历结构**：`child`/`sibling`/`return` 构成双向链表。\n\n3. **协调机制对比**\n\n| 流程           | VNode（Stack Reconciler） | Fiber Reconciler              |\n| -------------- | ------------------------- | ----------------------------- |\n| **遍历方式**   | 递归遍历（不可中断）      | 循环遍历链表（可中断 + 恢复） |\n| **任务调度**   | 同步执行，阻塞主线程      | 异步分片，空闲时间执行        |\n| **优先级控制** | 无                        | Lane 模型（31 个优先级车道）  |\n| **副作用处理** | 统一提交 DOM 更新         | 构建副作用链表，分阶段提交    |\n\n- **Fiber 两阶段提交**：\n  1. **协调阶段**（可中断）：\n     - 增量构建 Fiber 树，标记副作用（`effectTag`）。\n     - 通过 `requestIdleCallback` 或 Scheduler 包分片执行。\n  2. **提交阶段**（同步不可中断）：\n     - 遍历副作用链表，执行 DOM 操作和生命周期方法。\n\n4. **能力扩展示例**\n\n   **a. 支持 Hooks 状态管理**\n\n- Fiber 节点通过 `memoizedState` 字段存储 Hooks 链表：\n\n```javascript\n// 函数组件的 Hooks 链表\nfiberNode.memoizedState = {\n  memoizedState: 'state value', // useState 的状态\n  next: {\n    // 下一个 Hook（如 useEffect）\n    memoizedState: { cleanup: fn },\n    next: null,\n  },\n}\n```\n\n- VNode 无状态管理能力，仅描述 UI。\n\n**b. 优先级调度实战**\n\n- **高优先级任务抢占**：\n  ```javascript\n  // 用户输入触发高优先级更新\n  input.addEventListener('input', () => {\n    React.startTransition(() => {\n      setInputValue(e.target.value) // 低优先级\n    })\n    // 高优先级更新立即执行\n  })\n  ```\n- VNode 架构无法实现任务中断和优先级插队。\n\n**c. 副作用批处理**\n\n- Fiber 通过 `effectList` 链表收集所有变更，统一提交：\n  ```javascript\n  // 提交阶段遍历 effectList\n  let nextEffect = fiberRoot.firstEffect\n  while (nextEffect) {\n    commitWork(nextEffect)\n    nextEffect = nextEffect.nextEffect\n  }\n  ```\n- VNode 架构在 Diff 后直接操作 DOM，无批处理优化。\n\n5. **性能影响对比**\n\n| 场景                      | VNode 架构         | Fiber 架构                   |\n| ------------------------- | ------------------ | ---------------------------- |\n| **大型组件树渲染**        | 主线程阻塞导致掉帧 | 分片渲染，保持 UI 响应       |\n| **高频更新（如动画）**    | 多次渲染合并困难   | 基于优先级合并或跳过中间状态 |\n| **SSR 水合（Hydration）** | 全量同步处理       | 增量水合，优先交互部分       |\n\n:::\n\n## 简述 React diff 算法过程\n\n参考答案\n\n::: details\n\nReact Diff 算法通过 **分层对比策略** 和 **启发式规则** 减少树对比的时间复杂度（从 O(n³) 优化至 O(n)）。其核心流程如下：\n\n**1. 分层对比策略**\n\nReact 仅对 **同一层级的兄弟节点** 进行对比，若节点跨层级移动（如从父节点 A 移动到父节点 B），则直接 **销毁并重建**，而非移动。\n**原因**：跨层操作在真实 DOM 中成本极高（需递归遍历子树），而实际开发中跨层移动场景极少，此策略以概率换性能。\n\n**2. 节点类型比对规则**\n\n**a. 元素类型不同**\n\n若新旧节点类型不同（如 `<div>` → `<span>` 或 `ComponentA` → `ComponentB`），则：\n\n1. 销毁旧节点及其子树。\n2. 创建新节点及子树，并插入 DOM。\n\n```jsx\n// 旧树\n<div>\n  <ComponentA />\n</div>\n\n// 新树 → 直接替换\n<span>\n  <ComponentB />\n</span>\n```\n\n**b. 元素类型相同**\n\n若类型相同，则复用 DOM 节点并更新属性：\n\n- **原生标签**：更新 `className`、`style` 等属性。\n- **组件类型**：\n  - 类组件：保留实例，触发 `componentWillReceiveProps` → `shouldComponentUpdate` 等生命周期。\n  - 函数组件：重新执行函数，通过 Hooks 状态判断是否需更新。\n\n```jsx\n// 旧组件（保留实例并更新 props）\n<Button className=\"old\" onClick={handleClick} />\n\n// 新组件 → 复用 DOM，更新 className 和 onClick\n<Button className=\"new\" onClick={newClick} />\n```\n\n**3. 列表节点的 Key 优化**\n\n处理子节点列表时，React 依赖 **key** 进行最小化更新：\n\n**a. 无 key 时的默认行为**\n\n默认使用 **索引匹配**（index-based diff），可能导致性能问题：\n\n```jsx\n// 旧列表\n;[<div>A</div>, <div>B</div>][\n  // 新列表（首部插入）→ 索引对比导致 B 被误判更新\n  ((<div>C</div>), (<div>A</div>), (<div>B</div>))\n]\n```\n\n此时 React 会认为索引 0 从 A → C（更新），索引 1 从 B → A（更新），并新增索引 2 的 B，实际应仅插入 C。\n\n**b. 使用 key 的优化匹配**\n\n通过唯一 key 标识节点身份，React 可精准识别移动/新增/删除：\n\n```jsx\n// 正确使用 key（如数据 ID）\n<ul>\n  {items.map((item) => (\n    <li key={item.id}>{item.text}</li>\n  ))}\n</ul>\n```\n\n**匹配规则**：\n\n1. 遍历新列表，通过 key 查找旧节点：\n\n   - 找到且类型相同 → 复用节点。\n   - 未找到 → 新建节点。\n\n2. 记录旧节点中未被复用的节点 → 执行删除。\n\n**c. 节点移动优化**\n\n若新旧列表节点仅顺序变化，React 通过 key 匹配后，仅执行 **DOM 移动操作**（非重建），例如：\n\n```jsx\n// 旧列表：A (key=1), B (key=2)\n// 新列表：B (key=2), A (key=1)\n// React 仅交换 DOM 顺序，而非销毁重建\n```\n\n**4. 性能边界策略**\n\n- **子树跳过**：若父节点类型变化，其子节点即使未变化也会被整体销毁。\n- **相同组件提前终止**：若组件 `shouldComponentUpdate` 返回 `false`，则跳过其子树 Diff。\n\n:::\n\n## React 和 Vue diff 算法的区别\n\n参考答案\n\n::: details\n\nReact 和 Vue 的 Diff 算法均基于虚拟 DOM，但在实现策略、优化手段和设计哲学上存在显著差异：\n\n**1. 核心算法策略对比**\n\n| **维度**     | **React**                     | **Vue 2/3**                          |\n| ------------ | ----------------------------- | ------------------------------------ |\n| **遍历方式** | 单向递归（同层顺序对比）      | 双端对比（头尾指针优化）             |\n| **节点复用** | 类型相同则复用，否则销毁重建  | 类型相同则尝试复用，优先移动而非重建 |\n| **静态优化** | 需手动优化（如 `React.memo`） | 编译阶段自动标记静态节点             |\n| **更新粒度** | 组件级更新（默认）            | 组件级 + 块级（Vue3 Fragments）      |\n\n**2. 列表 Diff 实现细节**\n\n**a. React 的索引对比策略**\n\n- **无 key 时**：按索引顺序对比，可能导致无效更新\n  ```jsx\n  // 旧列表：[A, B, C]\n  // 新列表：[D, A, B, C]（插入头部）\n  // React 对比结果：更新索引 0-3，性能低下\n  ```\n- **有 key 时**：通过 key 匹配节点，减少移动操作\n  ```jsx\n  // key 匹配后，仅插入 D，其他节点不更新\n  ```\n\n**b. Vue 的双端对比策略**\n\n分四步优化对比效率（Vue2 核心逻辑，Vue3 优化为最长递增子序列）：\n\n1. **头头对比**：新旧头指针节点相同则复用，指针后移\n2. **尾尾对比**：新旧尾指针节点相同则复用，指针前移\n3. **头尾交叉对比**：旧头 vs 新尾，旧尾 vs 新头\n4. **中间乱序对比**：建立 key-index 映射表，复用可匹配节点\n\n```js\n// 旧列表：[A, B, C, D]\n// 新列表：[D, A, B, C]\n// Vue 通过步骤3头尾对比，仅移动 D 到头部\n```\n\n**3. 静态优化机制**\n\n**a. Vue 的编译时优化**\n\n- **静态节点标记**：\n  模板中的静态节点（无响应式绑定）会被编译为常量，跳过 Diff\n\n  ```html\n  <!-- 编译前 -->\n  <div>Hello Vue</div>\n\n  <!-- 编译后 -->\n  _hoisted_1 = createVNode(\"div\", null, \"Hello Vue\")\n  ```\n\n- **Block Tree（Vue3）**：\n  动态节点按区块（Block）组织，Diff 时仅对比动态部分\n\n**b. React 的运行时优化**\n\n- **手动控制更新**：\n  需通过 `React.memo`、`shouldComponentUpdate` 或 `useMemo` 避免无效渲染\n  ```jsx\n  const MemoComp = React.memo(() => <div>Static Content</div>)\n  ```\n\n**4. 响应式更新触发**\n\n| **框架** | **机制**                   | **Diff 触发条件**                |\n| -------- | -------------------------- | -------------------------------- |\n| React    | 状态变化触发组件重新渲染   | 父组件渲染 → 子组件默认递归 Diff |\n| Vue      | 响应式数据变更触发组件更新 | 依赖收集 → 仅受影响组件触发 Diff |\n\n```javascript\n// Vue：只有 data.value 变化才会触发更新\nconst vm = new Vue({ data: { value: 1 } })\n\n// React：需显式调用 setState\nconst [value, setValue] = useState(1)\n```\n\n**5. 设计哲学差异**\n\n| **维度**     | **React**                  | **Vue**                    |\n| ------------ | -------------------------- | -------------------------- |\n| **控制粒度** | 组件级控制（开发者主导）   | 细粒度依赖追踪（框架主导） |\n| **优化方向** | 运行时优化（Fiber 调度）   | 编译时优化（模板静态分析） |\n| **适用场景** | 大型动态应用（需精细控制） | 中小型应用（快速开发）     |\n\n:::\n\n## React JSX 循环为何使用 `key` ？\n\n参考答案\n\n::: details\n\n1. **元素的高效识别与复用**\n\nReact 通过 `key` 唯一标识列表中的每个元素。当列表发生变化（增删改排序）时，React 会通过 `key` 快速判断：\n\n- **哪些元素是新增的**（需要创建新 DOM 节点）\n- **哪些元素是移除的**（需要销毁旧 DOM 节点）\n- **哪些元素是移动的**（直接复用现有 DOM 节点，仅调整顺序）\n\n如果没有 `key`，React 会默认使用数组索引（`index`）作为标识，这在动态列表中会导致 **性能下降** 或 **状态错误**。\n\n2. **避免状态混乱**\n\n如果列表项是 **有状态的组件**（比如输入框、勾选框等），错误的 `key` 会导致状态与错误的内容绑定。例如：\n\n```jsx\n// 如果初始列表是 [A, B]，用索引 index 作为 key：\n<ul>\n  {items.map((item, index) => (\n    <li key={index}>{item}</li>\n  ))}\n</ul>\n\n// 在头部插入新元素变为 [C, A, B] 时：\n// React 会认为 key=0 → C（重新创建）\n// key=1 → A（复用原 key=0 的 DOM，但状态可能残留）\n// 此时，原本属于 A 的输入框状态可能会错误地出现在 C 中。\n```\n\n3. **提升渲染性能**\n\n通过唯一且稳定的 `key`（如数据 ID），React 可以精准判断如何复用 DOM 节点。如果使用随机数或索引，每次渲染都会强制重新创建所有元素，导致性能浪费。\n\n:::\n\n## React 事件和 DOM 事件区别\n\n参考答案\n\n::: details\n\n1. **事件绑定方式**\n\n- **React 事件**\n  使用**驼峰命名法**（如 `onClick`、`onChange`），通过 JSX 属性直接绑定函数：\n\n  ```jsx\n  <button onClick={handleClick}>点击</button>\n  ```\n\n- **DOM 事件**\n  使用**全小写命名**（如 `onclick`、`onchange`），通过字符串或 `addEventListener` 绑定：\n  ```html\n  <button onclick=\"handleClick()\">点击</button>\n  ```\n  ```javascript\n  button.addEventListener('click', handleClick)\n  ```\n\n2. **事件对象（Event Object）**\n\n- **React 事件**\n  使用**合成事件（SyntheticEvent）**，是原生事件对象的跨浏览器包装。\n\n  - 通过 `e.nativeEvent` 访问原生事件。\n  - 事件对象会被复用（事件池机制），异步访问需调用 `e.persist()`。\n\n  ```jsx\n  const handleClick = (e) => {\n    e.persist() // 保持事件对象引用\n    setTimeout(() => console.log(e.target), 100)\n  }\n  ```\n\n- **DOM 事件**\n  直接使用浏览器原生事件对象，无复用机制。\n  ```javascript\n  button.addEventListener('click', (e) => {\n    console.log(e.target) // 直接访问\n  })\n  ```\n\n3. **事件传播与默认行为**\n\n- **React 事件**\n\n  - **阻止默认行为**：必须显式调用 `e.preventDefault()`。\n  - **阻止冒泡**：调用 `e.stopPropagation()`。\n\n  ```jsx\n  const handleSubmit = (e) => {\n    e.preventDefault() // 阻止表单默认提交\n    e.stopPropagation() // 阻止事件冒泡\n  }\n  ```\n\n- **DOM 事件**\n  - **阻止默认行为**：可调用 `e.preventDefault()` 或 `return false`（在 HTML 属性中）。\n  - **阻止冒泡**：调用 `e.stopPropagation()` 或 `return false`（仅部分情况）。\n  ```html\n  <form onsubmit=\"return false\">\n    <!-- 阻止默认提交 -->\n    <button onclick=\"event.stopPropagation()\">按钮</button>\n  </form>\n  ```\n\n4. **性能优化**\n\n- **React 事件**\n  采用**事件委托**机制：\n\n  - React 17 之前将事件委托到 `document` 层级。\n  - React 17+ 改为委托到渲染的根容器（如 `ReactDOM.render` 挂载的节点）。\n  - 减少内存占用，动态添加元素无需重新绑定事件。\n\n- **DOM 事件**\n  直接绑定到元素，大量事件监听时可能导致性能问题。\n\n5. **跨浏览器兼容性**\n\n- **React 事件**\n  合成事件抹平了浏览器差异（如 `event.target` 的一致性），无需处理兼容性问题。\n\n- **DOM 事件**\n  需手动处理浏览器兼容性（如 IE 的 `attachEvent` vs 标准 `addEventListener`）。\n\n6. **`this` 绑定**\n\n- **React 事件**\n  类组件中需手动绑定 `this` 或使用箭头函数：\n\n  ```jsx\n  class MyComponent extends React.Component {\n    handleClick() {\n      console.log(this) // 需绑定，否则为 undefined\n    }\n\n    render() {\n      return <button onClick={this.handleClick.bind(this)}>点击</button>\n    }\n  }\n  ```\n\n- **DOM 事件**\n  事件处理函数中的 `this` 默认指向触发事件的元素：\n  ```javascript\n  button.addEventListener('click', function () {\n    console.log(this) // 指向 button 元素\n  })\n  ```\n\n| 特性             | React 事件                   | DOM 事件                               |\n| ---------------- | ---------------------------- | -------------------------------------- |\n| **命名规则**     | 驼峰命名（`onClick`）        | 全小写（`onclick`）                    |\n| **事件对象**     | 合成事件（`SyntheticEvent`） | 原生事件对象                           |\n| **默认行为阻止** | `e.preventDefault()`         | `e.preventDefault()` 或 `return false` |\n| **事件委托**     | 自动委托到根容器             | 需手动实现                             |\n| **跨浏览器兼容** | 内置处理                     | 需手动适配                             |\n| **`this` 指向**  | 类组件中需手动绑定           | 默认指向触发元素                       |\n\nReact 事件系统通过抽象和优化，提供了更高效、一致的事件处理方式，避免了直接操作 DOM 的繁琐和兼容性问题。\n\n:::\n\n## 简述 React batchUpdate 机制\n\n参考答案\n\n::: details\n\nReact 的 **batchUpdate（批处理更新）机制** 是一种优化策略，旨在将多个状态更新合并为一次渲染，减少不必要的组件重新渲染次数，从而提高性能。\n\n**核心机制**\n\n1. **异步合并更新**\n   当在 **同一执行上下文**（如同一个事件处理函数、生命周期方法或 React 合成事件）中多次调用状态更新（如 `setState`、`useState` 的 `setter` 函数），React 不会立即触发渲染，而是将多个更新收集到一个队列中，最终合并为一次更新，统一计算新状态并渲染。\n\n2. **更新队列**\n   React 内部维护一个更新队列。在触发更新的代码块中，所有状态变更会被暂存到队列，直到代码执行完毕，React 才会一次性处理队列中的所有更新，生成新的虚拟 DOM，并通过 Diff 算法高效更新真实 DOM。\n\n**触发批处理的场景**\n\n1. **React 合成事件**\n   如 `onClick`、`onChange` 等事件处理函数中的多次状态更新会自动批处理。\n\n   ```jsx\n   const handleClick = () => {\n     setCount(1) // 更新入队\n     setName('Alice') // 更新入队\n     // 最终合并为一次渲染\n   }\n   ```\n\n2. **React 生命周期函数**\n   在 `componentDidMount`、`componentDidUpdate` 等生命周期方法中的更新会被批处理。\n\n3. **React 18+ 的自动批处理增强**\n   React 18 引入 `createRoot` 后，即使在异步操作（如 `setTimeout`、`Promise`、原生事件回调）中的更新也会自动批处理：\n   ```jsx\n   setTimeout(() => {\n     setCount(1) // React 18 中自动批处理\n     setName('Alice') // 合并为一次渲染\n   }, 1000)\n   ```\n\n**绕过批处理的场景**\n\n1. **React 17 及之前的异步代码**\n   在 `setTimeout`、`Promise` 或原生事件回调中的更新默认**不会**批处理，每次 `setState` 触发一次渲染：\n\n   ```jsx\n   // React 17 中会触发两次渲染\n   setTimeout(() => {\n     setCount(1) // 渲染一次\n     setName('Alice') // 渲染第二次\n   }, 1000)\n   ```\n\n2. **手动强制同步更新**\n   使用 `flushSync`（React 18+）可强制立即更新，绕过批处理：\n\n   ```jsx\n   import { flushSync } from 'react-dom'\n\n   flushSync(() => {\n     setCount(1) // 立即渲染\n   })\n   setName('Alice') // 再次渲染\n   ```\n\n**设计目的**\n\n1. **性能优化**\n   避免频繁的 DOM 操作，减少浏览器重绘和回流，提升应用性能。\n\n2. **状态一致性**\n   确保在同一个上下文中多次状态变更后，组件最终基于最新的状态值渲染，避免中间状态导致的 UI 不一致。\n\n**示例对比**\n\n- **自动批处理（React 18+）**\n\n  ```jsx\n  const handleClick = () => {\n    setCount((prev) => prev + 1) // 更新入队\n    setCount((prev) => prev + 1) // 更新入队\n    // 最终 count 增加 2，仅一次渲染\n  }\n  ```\n\n- **非批处理（React 17 异步代码）**\n  ```jsx\n  setTimeout(() => {\n    setCount((prev) => prev + 1) // 渲染一次\n    setCount((prev) => prev + 1) // 再渲染一次\n    // React 17 中触发两次渲染，count 仍为 2\n  }, 1000)\n  ```\n\n| 场景                  | React 17 及之前 | React 18+（使用 `createRoot`） |\n| --------------------- | --------------- | ------------------------------ |\n| **合成事件/生命周期** | 自动批处理      | 自动批处理                     |\n| **异步操作**          | 不批处理        | 自动批处理                     |\n| **原生事件回调**      | 不批处理        | 自动批处理                     |\n\nReact 的批处理机制通过合并更新减少了渲染次数，但在需要即时反馈的场景（如动画）中，可通过 `flushSync` 强制同步更新。\n\n:::\n\n## 简述 React 事务机制\n\n参考答案\n\n::: details\n\nReact 的 **事务机制（Transaction）** 是早期版本（React 16 之前）中用于 **批量处理更新** 和 **管理副作用** 的核心设计模式，其核心思想是通过“包装”操作流程，确保在更新过程中执行特定的前置和后置逻辑（如生命周期钩子、事件监听等）。随着 React Fiber 架构的引入，事务机制逐渐被更灵活的调度系统取代。\n\n**核心概念**\n\n1. **事务的定义**\n   事务是一个包含 **初始化阶段**、**执行阶段** 和 **收尾阶段** 的流程控制单元。每个事务通过 `Transaction` 类实现，提供 `initialize` 和 `close` 方法，用于在操作前后插入逻辑。例如：\n\n   ```javascript\n   const MyTransaction = {\n     initialize() {\n       /* 前置操作（如记录状态） */\n     },\n     close() {\n       /* 后置操作（如触发更新） */\n     },\n   }\n   ```\n\n2. **包装函数**\n   事务通过 `perform` 方法执行目标函数，将其包裹在事务的生命周期中：\n   ```javascript\n   function myAction() {\n     /* 核心逻辑（如调用 setState） */\n   }\n   MyTransaction.perform(myAction)\n   ```\n\n**在 React 中的应用场景**\n\n1. **批量更新（Batching Updates）**\n   在事件处理或生命周期方法中，多次调用 `setState` 会被事务合并为一次更新。例如：\n\n   ```javascript\n   class Component {\n     onClick() {\n       // 事务包裹下的多次 setState 合并为一次渲染\n       this.setState({ a: 1 })\n       this.setState({ b: 2 })\n     }\n   }\n   ```\n\n2. **生命周期钩子的触发**\n   在组件挂载或更新时，事务确保 `componentWillMount`、`componentDidMount` 等钩子在正确时机执行。\n\n3. **事件系统的委托**\n   合成事件（如 `onClick`）的处理逻辑通过事务绑定和解绑，确保事件监听的一致性和性能优化。\n\n**事务的工作流程**\n\n1. **初始化阶段**\n   执行所有事务的 `initialize` 方法（如记录当前 DOM 状态、锁定事件监听）。\n2. **执行目标函数**\n   运行核心逻辑（如用户定义的 `setState` 或事件处理函数）。\n3. **收尾阶段**\n   执行所有事务的 `close` 方法（如对比 DOM 变化、触发更新、解锁事件）。\n\n**事务机制的局限性**\n\n1. **同步阻塞**\n   事务的执行是同步且不可中断的，无法支持异步优先级调度（如 Concurrent Mode 的时间切片）。\n2. **复杂性高**\n   事务的嵌套和组合逻辑复杂，难以维护和扩展。\n\n**Fiber 架构的演进**\nReact 16 引入的 **Fiber 架构** 替代了事务机制，核心改进包括：\n\n1. **异步可中断更新**\n   通过 Fiber 节点的链表结构，支持暂停、恢复和优先级调度。\n2. **更细粒度的控制**\n   将渲染拆分为多个阶段（如 `render` 和 `commit`），副作用管理更灵活。\n3. **替代批量更新策略**\n   使用调度器（Scheduler）和优先级队列实现更高效的批处理（如 React 18 的自动批处理）。\n\n| 特性           | 事务机制（React <16）  | Fiber 架构（React 16+）        |\n| -------------- | ---------------------- | ------------------------------ |\n| **更新方式**   | 同步批量更新           | 异步可中断、优先级调度         |\n| **副作用管理** | 通过事务生命周期控制   | 通过 Effect Hook、提交阶段处理 |\n| **复杂度**     | 高（嵌套事务逻辑复杂） | 高（但更模块化和可扩展）       |\n| **适用场景**   | 简单同步更新           | 复杂异步渲染（如动画、懒加载） |\n\n事务机制是 React 早期实现批量更新的基石，但其同步设计无法满足现代前端应用的复杂需求。Fiber 架构通过解耦渲染过程，为 Concurrent Mode 和 Suspense 等特性奠定了基础，成为 React 高效渲染的核心。\n:::\n\n## 理解 React concurrency 并发机制\n\n参考答案\n\n::: details\n\nReact 的并发机制（Concurrency）是 React 18 引入的一项重要特性，旨在提升应用的响应性和性能。\n\n**1. 什么是 React 的并发机制？**\n\nReact 的并发机制允许 React 在渲染过程中根据任务的优先级进行调度和中断，从而确保高优先级的更新能够及时渲染，而不会被低优先级的任务阻塞。\n\n**2. 并发机制的工作原理：**\n\n- **时间分片（Time Slicing）：** React 将渲染任务拆分为多个小片段，每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务，避免长时间的渲染阻塞用户交互。\n\n- **优先级调度（Priority Scheduling）：** React 为不同的更新分配不同的优先级。高优先级的更新（如用户输入）会被优先处理，而低优先级的更新（如数据预加载）可以在空闲时处理。\n\n- **可中断渲染（Interruptible Rendering）：** 在并发模式下，React 可以中断当前的渲染任务，处理更高优先级的任务，然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。\n\n**3. 并发机制的优势：**\n\n- **提升响应性：** 通过优先处理高优先级任务，React 能够更快地响应用户输入，提升用户体验。\n\n- **优化性能：** 将渲染任务拆分为小片段，避免长时间的渲染阻塞，提升应用的整体性能。\n\n- **更好的资源利用：** 在主线程空闲时处理低优先级任务，充分利用系统资源。\n\n**4. 如何启用并发模式：**\n\n要在 React 应用中启用并发模式，需要使用 `createRoot` API：\n\n```javascript\nimport React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App'\n\nconst root = ReactDOM.createRoot(document.getElementById('root'))\nroot.render(<App />)\n```\n\n在并发模式下，React 会自动根据任务的优先级进行调度和渲染。\n\n:::\n\n## React reconciliation 协调的过程\n\n参考答案\n\n::: details\n\nReact 的 **协调（Reconciliation）** 是用于高效更新 UI 的核心算法。当组件状态或属性变化时，React 会通过对比新旧虚拟 DOM（Virtual DOM）树，找出最小化的差异并应用更新。以下是协调过程的详细步骤：\n\n1. **生成虚拟 DOM 树**\n\n- 当组件状态或属性变化时，React 会重新调用组件的 `render` 方法，生成新的**虚拟 DOM 树**（一个轻量级的 JavaScript 对象，描述 UI 结构）。\n- 虚拟 DOM 是实际 DOM 的抽象表示，操作成本远低于直接操作真实 DOM。\n\n2. **Diffing 算法（差异对比）**\n   React 使用 **Diffing 算法** 比较新旧两棵虚拟 DOM 树，找出需要更新的部分。对比规则如下：\n\n**规则一：不同类型的元素**\n\n- 如果新旧元素的 `type` 不同（例如从 `<div>` 变为 `<span>`），React 会**销毁旧子树**，**重建新子树**。\n  - 旧组件的生命周期方法（如 `componentWillUnmount`）会被触发。\n  - 新组件的生命周期方法（如 `constructor`、`componentDidMount`）会被触发。\n\n**规则二：相同类型的元素**\n\n- 如果元素的 `type` 相同（例如 `<div className=\"old\">` → `<div className=\"new\">`），React 会**保留 DOM 节点**，仅更新变化的属性。\n  - 对比新旧属性，仅更新差异部分（例如 `className`）。\n  - 组件实例保持不变，生命周期方法（如 `componentDidUpdate`）会被触发。\n\n**规则三：递归处理子节点**\n\n- 对于子节点的对比，React 默认使用**逐层递归**的方式。\n- **列表对比优化**：\n  - 当子元素是列表（例如通过 `map` 生成的元素）时，React 需要唯一 `key` 来标识元素，以高效复用 DOM 节点。\n  - 若未提供 `key`，React 会按顺序对比子节点，可能导致性能下降或状态错误（例如列表顺序变化时）。\n\n3. **更新真实 DOM**\n\n- 通过 Diffing 算法找出差异后，React 将生成一系列**最小化的 DOM 操作指令**（例如 `updateTextContent`、`replaceChild`）。\n- 这些指令会被批量应用到真实 DOM 上，以减少重绘和重排的次数，提高性能。\n\n4. **协调的优化策略**\n\n- **Key 的作用**：为列表元素提供唯一的 `key`，帮助 React 识别元素的移动、添加或删除，避免不必要的重建。\n- **批量更新（Batching）**：React 会将多个状态更新合并为一次渲染，减少重复计算。\n- **Fiber 架构**（React 16+）：\n  - 将协调过程拆分为可中断的“工作单元”（Fiber 节点），允许高优先级任务（如动画）优先处理。\n  - 支持异步渲染（Concurrent Mode），避免长时间阻塞主线程。\n\n:::\n\n## React 组件渲染和更新的全过程\n\n参考答案\n\n::: details\n\nReact 组件的渲染和更新过程涉及多个阶段，包括 **初始化、渲染、协调、提交、清理** 等。以下是 React 组件渲染和更新的全过程，结合源码逻辑和关键步骤进行详细分析。\n\n---\n\n**1. 整体流程概述**\nReact 的渲染和更新过程可以分为以下几个阶段：\n\n1. **初始化阶段**：创建 Fiber 树和 Hooks 链表。\n2. **渲染阶段**：生成新的虚拟 DOM（Fiber 树）。\n3. **协调阶段**：对比新旧 Fiber 树，找出需要更新的部分。\n4. **提交阶段**：将更新应用到真实 DOM。\n5. **清理阶段**：重置全局变量，准备下一次更新。\n\n**2. 详细流程分析**\n\n**（1）初始化阶段**\n\n- **触发条件**：组件首次渲染或状态/属性更新。\n- **关键函数**：`render`、`createRoot`、`scheduleUpdateOnFiber`。\n- **逻辑**：\n  1. 通过 `ReactDOM.render` 或 `createRoot` 初始化应用。\n  2. 创建根 Fiber 节点（`HostRoot`）。\n  3. 调用 `scheduleUpdateOnFiber`，将更新任务加入调度队列。\n\n**（2）渲染阶段**\n\n- **触发条件**：调度器开始执行任务。\n- **关键函数**：`performSyncWorkOnRoot`、`beginWork`、`renderWithHooks`。\n- **逻辑**：\n  1. 调用 `performSyncWorkOnRoot`，开始渲染任务。\n  2. 调用 `beginWork`，递归处理 Fiber 节点。\n  3. 对于函数组件，调用 `renderWithHooks`，执行组件函数并生成新的 Hooks 链表。\n  4. 对于类组件，调用 `instance.render`，生成新的虚拟 DOM。\n  5. 对于 Host 组件（如 `div`），生成对应的 DOM 节点。\n\n**（3）协调阶段**\n\n- **触发条件**：新的虚拟 DOM 生成后。\n- **关键函数**：`reconcileChildren`、`diff`。\n- **逻辑**：\n  1. 调用 `reconcileChildren`，对比新旧 Fiber 节点。\n  2. 根据 `diff` 算法，找出需要更新的节点。\n  3. 为需要更新的节点打上 `Placement`、`Update`、`Deletion` 等标记。\n\n**（4）提交阶段**\n\n- **触发条件**：协调阶段完成后。\n- **关键函数**：`commitRoot`、`commitWork`。\n- **逻辑**：\n  1. 调用 `commitRoot`，开始提交更新。\n  2. 调用 `commitWork`，递归处理 Fiber 节点。\n  3. 根据节点的标记，执行 DOM 操作（如插入、更新、删除）。\n  4. 调用生命周期钩子（如 `componentDidMount`、`componentDidUpdate`）。\n\n**（5）清理阶段**\n\n- **触发条件**：提交阶段完成后。\n- **关键函数**：`resetHooks`、`resetContext`。\n- **逻辑**：\n  1. 重置全局变量（如 `currentlyRenderingFiber`、`currentHook`）。\n  2. 清理上下文和副作用。\n  3. 准备下一次更新。\n\n:::\n\n## 为何 Hooks 不能放在条件或循环之内？\n\n参考答案\n\n::: details\n\n一个组件中的 hook 会以链表的形式串起来， FiberNode 的 memoizedState 中保存了 Hooks 链表中的第一个 Hook。\n\n在更新时，会复用之前的 Hook，如果通过了条件或循环语句，增加或者删除 hooks，在复用 hooks 过程中，会产生复用 hooks状态和当前 hooks 不一致的问题。\n\n:::\n\n## useEffect 的底层是如何实现的（美团）\n\n参考答案\n\n::: details\n\nuseEffect 是 React 用于管理副作用的 Hook，它在 commit 阶段 统一执行，确保副作用不会影响渲染。\n\n在 React 源码中，useEffect 通过 Fiber 机制 在 commit 阶段 进行处理：\n\n**(1) useEffect 存储在 Fiber 节点上**\n\nReact 组件是通过 Fiber 数据结构 组织的，每个 useEffect 都会存储在 fiber.updateQueue 中。\n\n**(2) useEffect 何时执行**\n\nReact 组件更新后，React 在 commit 阶段 统一遍历 effect 队列，并执行 useEffect 副作用。\n\nReact 使用 `useEffectEvent()` 注册 effect，在 commitLayoutEffect 之后，异步执行 useEffect，避免阻塞 UI 渲染。\n\n**(3) useEffect 依赖变化的处理**\n\n依赖数组的比较使用 `Object.is()`，只有依赖变化时才重新执行 useEffect。\n\n在更新阶段，React 遍历旧 effect，并先执行清理函数，然后再执行新的 effect。\n\n**简化的 useEffect 实现如下：**\n\n```js\nfunction useEffect(callback, dependencies) {\n  const currentEffect = getCurrentEffect() // 获取当前 Fiber 节点的 Effect\n\n  if (dependenciesChanged(currentEffect.dependencies, dependencies)) {\n    cleanupPreviousEffect(currentEffect) // 先执行上次 effect 的清理函数\n    const cleanup = callback() // 执行 useEffect 传入的回调\n    currentEffect.dependencies = dependencies\n    currentEffect.cleanup = cleanup // 存储清理函数\n  }\n}\n```\n\n相比 useLayoutEffect，useEffect 是 异步执行，不会阻塞 UI 渲染。\n\n:::\n"
  },
  {
    "path": "docs/second-exam/react-usage.md",
    "content": "# React 使用\n\nReact 是全球应用最广泛的框架，国内大厂多用 React\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## React 组件生命周期\n\n::: details 参考答案\n\nReact 组件生命周期分为以下三个阶段。\n\n**挂载阶段**：这是组件首次被创建并插入到 DOM 中的阶段。\n\n**更新阶段**：当组件的 props 或 state 发生变化时，就会触发更新阶段。\n\n**卸载阶段**：组件从 DOM 中移除时进入卸载阶段。\n\n函数组件是没有明确的生命周期方法，但可以通过 `useEffect` 来模拟生命周期行为。\n\n模拟**挂载阶段**的生命周期方法：\n\n- 只需要在 `useEffect` 的依赖数组中传入一个空数组 `[]`。这样，该副作用只会在组件挂载后运行一次。\n\n  ```js\n  useEffect(() => {\n    console.log('代码只会在组件挂载后执行一次')\n  }, [])\n  ```\n\n模拟**更新阶段**的生命周期方法：\n\n- 通过将依赖项放入依赖数组中，`useEffect` 可以在依赖项更改时执行。如果你省略了依赖数组，副作用将在每次渲染后执行。\n  ```js\n  // 注意这里没有提供依赖数组\n  useEffect(() => {\n    console.log('代码会在组件挂载后以及每次更新后执行')\n  })\n  // 特定依赖更新时执行\n  useEffect(() => {\n    console.log('代码会在 count 更新后执行')\n  }, [count])\n  ```\n\n模拟**卸载阶段**的生命周期方法：\n\n- 在 `useEffect` 的函数中返回一个函数，该函数会在组件卸载前执行。\n\n  ```js\n  useEffect(() => {\n    return () => {\n      console.log('代码会在组件卸载前执行')\n    }\n  }, [])\n  ```\n\n  :::\n\n## React 父子组件生命周期调用顺序\n\n::: details 参考答案\n\n函数组件的生命周期通过 `useEffect` 模拟，其调用顺序如下：\n\n**挂载阶段**\n\n- **父组件**：执行函数体（首次渲染）\n- **子组件**：执行函数体（首次渲染）\n- **子组件**：`useEffect`（挂载阶段）\n- **父组件**：`useEffect`（挂载阶段）\n\n**更新阶段**\n\n- **父组件**：执行函数体（重新渲染）\n- **子组件**：执行函数体（重新渲染）\n- **子组件**：`useEffect` 清理函数（如果依赖项变化）\n- **父组件**：`useEffect` 清理函数（如果依赖项变化）\n- **子组件**：`useEffect`（如果依赖项变化）\n- **父组件**：`useEffect`（如果依赖项变化）\n\n**卸载阶段**\n\n- **父组件**：`useEffect` 清理函数\n- **子组件**：`useEffect` 清理函数\n\n:::\n\n## React 组件通讯方式\n\n::: details 参考答案\n\n- **通过props向子组件传递数据**\n\n```js\n//父组件\nconst Parent = () => {\n  const message = 'Hello from Parent'\n  return <Child message={message} />\n}\n\n// 子组件\nconst Child = ({ message }) => {\n  return <div>{message}</div>\n}\n```\n\n- **通过回调函数向父组件传递数据**\n\n```js\n//父组件\nconst Parent = () => {\n  const handleData = (data) => {\n    console.log('Data from Child:', data)\n  }\n  return <Child onSendData={handleData} />\n}\n\n// 子组件\nconst Child = ({ message }) => {\n  return <button onClick={() => onSendData('Hello from Child')}>Send Data</button>\n}\n```\n\n- **使用refs调用子组件暴露的方法**\n\n```js\nimport React, { useRef, forwardRef, useImperativeHandle } from 'react'\n\n// 子组件\nconst Child = forwardRef((props, ref) => {\n  // 暴露方法给父组件\n  useImperativeHandle(ref, () => ({\n    sayHello() {\n      alert('Hello from Child Component!')\n    },\n  }))\n\n  return <div>Child Component</div>\n})\n\n// 父组件\nfunction Parent() {\n  const childRef = useRef(null)\n\n  const handleClick = () => {\n    if (childRef.current) {\n      childRef.current.sayHello()\n    }\n  }\n\n  return (\n    <div>\n      <Child ref={childRef} />\n      <button onClick={handleClick}>Call Child Method</button>\n    </div>\n  )\n}\n\nexport default Parent\n```\n\n- **通过Context进行跨组件通信**\n\n```js\nimport React, { useState } from 'react'\n\n// 创建一个 Context\nconst MyContext = React.createContext()\n\n// 父组件\nfunction Parent() {\n  const [sharedData, setSharedData] = useState('Hello from Context')\n\n  const updateData = () => {\n    setSharedData('Updated Data from Context')\n  }\n\n  return (\n    // 提供数据和更新函数\n    <MyContext.Provider value={{ sharedData, updateData }}>\n      <ChildA />\n    </MyContext.Provider>\n  )\n}\n\n// 子组件 A（引用子组件 B）\nfunction ChildA() {\n  return (\n    <div>\n      <ChildB />\n    </div>\n  )\n}\n\n// 子组件 B（使用 useContext）\nfunction ChildB() {\n  const { sharedData, updateData } = React.useContext(MyContext)\n  return (\n    <div>\n      <div>ChildB: {sharedData}</div>\n      <button onClick={updateData}>Update Data</button>\n    </div>\n  )\n}\n\nexport default Parent\n```\n\n- **使用状态管理库进行通信**\n\n  - **React Context + useReducer**\n\n    ```js\n    import React, { useReducer } from 'react'\n\n    const initialState = { count: 0 }\n\n    function reducer(state, action) {\n      switch (action.type) {\n        case 'increment':\n          return { count: state.count + 1 }\n        case 'decrement':\n          return { count: state.count - 1 }\n        default:\n          throw new Error()\n      }\n    }\n\n    const CounterContext = React.createContext()\n\n    function CounterProvider({ children }) {\n      const [state, dispatch] = useReducer(reducer, initialState)\n      return <CounterContext.Provider value={{ state, dispatch }}>{children}</CounterContext.Provider>\n    }\n\n    function Counter() {\n      const { state, dispatch } = React.useContext(CounterContext)\n      return (\n        <div>\n          Count: {state.count}\n          <button onClick={() => dispatch({ type: 'increment' })}>+</button>\n          <button onClick={() => dispatch({ type: 'decrement' })}>-</button>\n        </div>\n      )\n    }\n\n    function App() {\n      return (\n        <CounterProvider>\n          <Counter />\n        </CounterProvider>\n      )\n    }\n\n    export default App\n    ```\n\n  - **Redux**：使用 `Redux Toolkit` 简化 Redux 开发。\n\n    ```js\n    import { createSlice, configureStore } from '@reduxjs/toolkit'\n\n    const counterSlice = createSlice({\n      name: 'counter',\n      initialState: { value: 0 },\n      reducers: {\n        increment: (state) => {\n          state.value += 1\n        },\n        decrement: (state) => {\n          state.value -= 1\n        },\n      },\n    })\n\n    const { increment, decrement } = counterSlice.actions\n\n    const store = configureStore({\n      reducer: counterSlice.reducer,\n    })\n\n    store.subscribe(() => console.log(store.getState()))\n\n    store.dispatch(increment())\n    store.dispatch(decrement())\n    ```\n\n  - **MobX**\n\n  ```js\n  import { makeAutoObservable } from 'mobx'\n  import { observer } from 'mobx-react-lite'\n\n  class CounterStore {\n    count = 0\n\n    constructor() {\n      makeAutoObservable(this)\n    }\n\n    increment() {\n      this.count += 1\n    }\n\n    decrement() {\n      this.count -= 1\n    }\n  }\n\n  const counterStore = new CounterStore()\n\n  const Counter = observer(() => {\n    return (\n      <div>\n        Count: {counterStore.count}\n        <button onClick={() => counterStore.increment()}>+</button>\n        <button onClick={() => counterStore.decrement()}>-</button>\n      </div>\n    )\n  })\n\n  export default Counter\n  ```\n\n  - **Zustand**\n\n  ```\n  import create from \"zustand\";\n\n  const useStore = create((set) => ({\n    count: 0,\n    increment: () => set((state) => ({ count: state.count + 1 })),\n    decrement: () => set((state) => ({ count: state.count - 1 })),\n  }));\n\n  function Counter() {\n    const { count, increment, decrement } = useStore();\n    return (\n      <div>\n        Count: {count}\n        <button onClick={increment}>+</button>\n        <button onClick={decrement}>-</button>\n      </div>\n    );\n  }\n\n  export default Counter;\n  ```\n\n- **使用事件总线（Event Bus）进行通信**\n\n可以使用第三方库如 pubsub-js 来实现父子组件间通信。在父组件中订阅一个事件，子组件在特定情况下发布这个事件并传递数据。\n\n```js\nimport React from 'react'\nimport PubSub from 'pubsub-js'\n\nconst ParentComponent = () => {\n  React.useEffect(() => {\n    const token = PubSub.subscribe('childData', (msg, data) => {\n      console.log('Received data from child:', data)\n    })\n    return () => {\n      PubSub.unsubscribe(token)\n    }\n  }, [])\n\n  return <ChildComponent />\n}\n\nconst ChildComponent = () => {\n  const sendData = () => {\n    PubSub.publish('childData', { message: 'Hello from child' })\n  }\n\n  return <button onClick={sendData}>Send data from child</button>\n}\n\nexport default ParentComponent\n```\n\n:::\n\n## state 和 props 有什么区别？\n\n::: details 参考答案\n\n在 React 中，props 和 state 都用于管理组件的数据和状态。\n\n**Props（属性）：**\n\nprops 是组件之间传递数据的一种方式，用于从父组件向子组件传递数据。\nprops 是只读的，即父组件传递给子组件的数据在子组件中不能被修改。\nprops 是在组件的声明中定义，通过组件的属性传递给子组件。\nprops 的值由父组件决定，子组件无法直接改变它的值。\n当父组件的 props 发生变化时，子组件会重新渲染。\n\n**State（状态）：**\n\nstate 是组件内部的数据，用于管理组件的状态和变化。\nstate 是可变的，组件可以通过 setState 方法来更新和修改 state。\nstate 是在组件的构造函数中初始化的，通常被定义为组件的类属性。\nstate 的值可以由组件自身内部改变，通过调用 setState 方法触发组件的重新渲染。\n当组件的 state 发生变化时，组件会重新渲染。\n\n**总结：**\n\nprops 是父组件传递给子组件的数据，是只读的，子组件无法直接修改它。\nstate 是组件内部的数据，是可变的，组件可以通过 setState 方法来修改它。\nprops 用于组件之间的数据传递，而 state 用于管理组件自身的状态和变化。\n\n:::\n\n## React 有哪些内置 Hooks ？\n\n::: details 参考答案\n\nReact 目前有多个 Hooks API，可以参考[官方文档 Hooks](https://zh-hans.react.dev/reference/react/hooks)，可以按照功能进行分类:\n\n**1. 状态管理 Hooks**\n\n- useState: 用于在函数组件中添加局部状态。\n- useReducer: 用于管理复杂的状态逻辑，类似于 Redux 的 reducer。\n\n**2. 副作用 Hooks**\n\n- useEffect: 用于在函数组件中执行副作用操作（如数据获取、订阅、手动 DOM 操作等）。\n- useLayoutEffect: 与 useEffect 类似，但在 DOM 更新后同步执行，适用于需要直接操作 DOM 的场景。\n\n**3. 上下文 Hooks**\n\n- useContext: 用于访问 React 的上下文（Context）。\n\n**4. 引用 Hooks**\n\n- useRef: 用于创建一个可变的引用对象，通常用于访问 DOM 元素或存储可变值。\n\n**5. 性能优化 Hooks**\n\n- useMemo: 用于缓存计算结果，避免在每次渲染时都重新计算。\n- useCallback: 用于缓存回调函数，避免在每次渲染时都创建新的回调。\n\n**6. 其他 Hooks**\n\n- useDeferredValue: 延迟更新 UI 的某些部分。\n- useActionState: 根据某个表单动作的结果更新 state。\n- useImperativeHandle: 用于自定义暴露给父组件的实例值，通常与 forwardRef 一起使用。\n- useDebugValue: 用于在 React 开发者工具中显示自定义 Hook 的标签。\n- useOptimistic 帮助你更乐观地更新用户界面\n- useTransition: 用于标记某些状态更新为“过渡”状态，允许你在更新期间显示加载指示器。\n- useId: 用于生成唯一的 ID，可以生成传递给无障碍属性的唯一 ID。\n- useSyncExternalStore: 用于订阅外部存储（如 Redux 或 Zustand）的状态。\n- useInsertionEffect: 为 CSS-in-JS 库的作者特意打造的，在布局副作用触发之前将元素插入到 DOM 中\n\n:::\n\n## useEffect 和 useLayoutEffect 的区别\n\n::: details 参考答案\n\n**1. 执行时机**\n\n- **useEffect**:\n\n  - **执行时机**: 在浏览器完成绘制（即 DOM 更新并渲染到屏幕）之后异步执行。\n  - **适用场景**: 适用于大多数副作用操作，如数据获取、订阅、手动 DOM 操作等，因为这些操作通常不需要阻塞浏览器的渲染。\n\n- **useLayoutEffect**:\n  - **执行时机**: 在 DOM 更新之后，但在浏览器绘制之前同步执行。\n  - **适用场景**: 适用于需要在浏览器绘制之前同步执行的副作用操作，如测量 DOM 元素、同步更新 DOM 等。由于它是同步执行的，可能会阻塞浏览器的渲染，因此应谨慎使用。\n\n**2. 对渲染的影响**\n\n- **useEffect**:\n\n  - 由于是异步执行，不会阻塞浏览器的渲染过程，因此对用户体验的影响较小。\n  - 如果副作用操作导致状态更新，React 会重新渲染组件，但用户不会看到中间的闪烁或不一致的状态。\n\n- **useLayoutEffect**:\n  - 由于是同步执行，会阻塞浏览器的渲染过程，直到副作用操作完成。\n  - 如果副作用操作导致状态更新，React 会立即重新渲染组件，用户可能会看到中间的闪烁或不一致的状态。\n\n**3. 总结**\n\n- **useEffect**: 异步执行，不阻塞渲染，适合大多数副作用操作。\n- **useLayoutEffect**: 同步执行，阻塞渲染，适合需要在绘制前同步完成的副作用操作。\n\n:::\n\n## 为何 dev 模式下 useEffect 执行两次？\n\n::: details 参考答案\n\nReact 官方文档其实对这个问题进行了[解答](https://zh-hans.react.dev/reference/react/useEffect#my-effect-runs-twice-when-the-component-mounts)：\n\n在开发环境下，如果开启严格模式，React 会在实际运行 setup 之前额外运行一次 setup 和 cleanup。\n\n这是一个压力测试，用于验证 Effect 的逻辑是否正确实现。如果出现可见问题，则 cleanup 函数缺少某些逻辑。cleanup 函数应该停止或撤消 setup 函数所做的任何操作。一般来说，用户不应该能够区分 setup 被调用一次（如在生产环境中）和调用 setup → cleanup → setup 序列（如在开发环境中）。\n\n借助严格模式的目标是帮助开发者提前发现以下问题：\n\n1. 不纯的渲染逻辑：例如，依赖外部状态或直接修改 DOM。\n2. 未正确清理的副作用：例如，未在 useEffect 的清理函数中取消订阅或清除定时器。\n3. 不稳定的组件行为：例如，组件在多次挂载和卸载时表现不一致。\n\n通过强制组件挂载和卸载两次，React 可以更好地暴露这些问题。\n\n:::\n\n## React 闭包陷阱\n\n::: details 参考答案\n\n让我们举个例子：\n\n```jsx\nfunction Counter() {\n  const [count, setCount] = useState(0)\n\n  useEffect(() => {\n    const timer = setInterval(() => {\n      console.log(count) // 每次打印的都是初始值 0\n    }, 1000)\n\n    return () => clearInterval(timer)\n  }, []) // 依赖数组为空，effect 只运行一次\n\n  return (\n    <div>\n      <p>Count: {count}</p>\n      <button onClick={() => setCount(count + 1)}>Increment</button>\n    </div>\n  )\n}\n```\n\n在这个例子中：\n\n- `useEffect` 只在组件挂载时运行一次。\n- `setInterval` 的回调函数形成了一个闭包，捕获了初始的 `count` 值（即 0）。\n- 即使 `count` 状态更新了，`setInterval` 中的回调函数仍然访问的是旧的 `count` 值。\n\n闭包陷阱的根本原因是 JavaScript 的闭包机制：\n\n- 当一个函数被定义时，它会捕获当前作用域中的变量。\n- 如果这些变量是状态或 props，它们的值在函数定义时被“固定”下来。\n- 当状态或 props 更新时，闭包中的值并不会自动更新。\n\n为了避免闭包陷阱，可以将依赖的状态或 props 添加到 useEffect 的依赖数组中，这样每次状态更新时，useEffect 都会重新运行，闭包中的值也会更新。\n\n```jsx\nuseEffect(() => {\n  const timer = setInterval(() => {\n    console.log(count) // 每次打印最新的 count 值\n  }, 1000)\n\n  return () => clearInterval(timer)\n}, [count]) // 将 count 添加到依赖数组\n```\n\n:::\n\n## React state 不可变数据\n\n::: details 参考答案\n\n在 React 中，**状态（state）的不可变性** 是指你不能直接修改状态的值，而是需要创建一个新的值来替换旧的状态。\n\n使用不可变数据可以带来如下好处：\n\n1. **性能优化**\n\nReact 使用浅比较（shallow comparison）来检测状态是否发生变化。如果状态是不可变的，React 只需要比较引用（即内存地址）是否变化，而不需要深度遍历整个对象或数组。\n\n2. **可预测性**\n\n- 不可变数据使得状态的变化更加可预测和可追踪。\n- 每次状态更新都会生成一个新的对象或数组，这样可以更容易地调试和追踪状态的变化历史。\n\n3. **避免副作用**\n\n- 直接修改状态可能会导致意外的副作用，尤其是在异步操作或复杂组件中。\n- 不可变数据确保了状态的更新是纯函数式的，避免了副作用。\n\n**关于如何实现不可变数据？**\n\n1. **更新对象时使用新的对象**\n\n```jsx\n// ❌ 错误：直接修改状态\nstate.name = 'new name'\nsetState(state)\n```\n\n```jsx\n// ✅ 正确：创建新对象\nsetState({\n  ...state, // 复制旧状态\n  name: 'new name', // 更新属性\n})\n```\n\n2. **更新数组时使用新的数组**\n\n```jsx\n// ❌ 错误：直接修改数组\nstate.items.push(newItem)\nsetState(state)\n```\n\n```jsx\n// ✅ 正确：创建新数组\nsetState({\n  ...state,\n  items: [...state.items, newItem], // 添加新元素\n})\n```\n\n3. **使用工具库简化不可变更新**\n\n常用的库有：\n\n1. **Immer.js**\n   [Immer](https://immerjs.github.io/immer/) 是一个流行的库，它允许你以可变的方式编写代码，但最终生成不可变的数据。\n\n```jsx\nimport produce from 'immer'\n\nsetState(\n  produce(state, (draft) => {\n    draft.user.profile.name = 'new name' // 直接修改\n    draft.items.push(newItem) // 直接修改\n  })\n)\n```\n\n2. **Immutable.js**\n\n[Immutable.js](https://immutable-js.com/) 提供了不可变的数据结构（如 `List`、`Map` 等），可以更方便地处理不可变数据。\n\n```jsx\nimport { Map } from 'immutable'\n\nconst state = Map({ name: 'John', age: 30 })\nconst newState = state.set('name', 'Jane')\n```\n\n:::\n\n## React state 异步更新\n\n::: details 参考答案\n\n在 React 18 之前，React 采用批处理策略来优化状态更新。在批处理策略下，React 将在事件处理函数结束后应用所有的状态更新，这样可以避免不必要的渲染和 DOM 操作。\n\n然而，这个策略在异步操作中就无法工作了。因为 React 没有办法在适当的时机将更新合并起来，所以结果就是在异步操作中的每一个状态更新都会导致一个新的渲染。\n\n例如，当你在一个 onClick 事件处理函数中连续调用两次 setState，React 会将这两个更新合并，然后在一次重新渲染中予以处理。\n\n然而，在某些场景下，如果你在事件处理函数之外调用 setState，React 就无法进行批处理了。比如在 setTimeout 或者 Promise 的回调函数中。在这些场景中，每次调用 setState，React 都会触发一次重新渲染，无法达到批处理的效果。\n\nReact 18 引入了自动批处理更新机制，让 React 可以捕获所有的状态更新，并且无论在何处进行更新，都会对其进行批处理。这对一些异步的操作，如 Promise，setTimeout 之类的也同样有效。\n\n这一新特性的实现，核心在于 React 18 对渲染优先级的管理。React 18 引入了一种新的协调器，被称为“React Scheduler”。它负责管理 React 的工作单元队列。每当有一个新的状态更新请求，React 会创建一个新的工作单元并放入这个队列。当 JavaScript 运行栈清空，Event Loop 即将开始新的一轮循环时，Scheduler 就会进入工作，处理队列中的所有工作单元，实现了批处理。\n\n:::\n\n## React state 的“合并”特性\n\n::: details 参考答案\n\nReact **状态的“合并”特性** 是指当使用 `setState` 更新状态时，React 会将新状态与旧状态进行浅合并（shallow merge），而不是直接替换整个状态对象。\n\n合并特性在类组件中尤为明显，而在函数组件中需要手动实现类似的行为。\n\n1. **类组件中的状态合并**\n\n在类组件中，`setState` 会自动合并状态对象。例如：\n\n```jsx\nclass MyComponent extends React.Component {\n  constructor(props) {\n    super(props)\n    this.state = {\n      name: 'John',\n      age: 30,\n    }\n  }\n\n  updateName = () => {\n    this.setState({ name: 'Jane' }) // 只更新 name，age 保持不变\n  }\n\n  render() {\n    return (\n      <div>\n        <p>Name: {this.state.name}</p>\n        <p>Age: {this.state.age}</p>\n        <button onClick={this.updateName}>Update Name</button>\n      </div>\n    )\n  }\n}\n```\n\n在这个例子中：\n\n- 调用 `this.setState({ name: 'Jane' })` 只会更新 `name` 属性，而 `age` 属性保持不变。\n- React 会自动将新状态 `{ name: 'Jane' }` 与旧状态 `{ name: 'John', age: 30 }` 进行浅合并，结果是 `{ name: 'Jane', age: 30 }`。\n\n2. **函数组件中的状态替换**\n\n在函数组件中，`useState` 的 setter 函数不会自动合并状态。如果你直接传递一个新对象，它会完全替换旧状态。\n\n```jsx\nfunction MyComponent() {\n  const [state, setState] = useState({\n    name: 'John',\n    age: 30,\n  })\n\n  const updateName = () => {\n    setState({ name: 'Jane' }) // ❌ 直接替换，age 会丢失\n  }\n\n  return (\n    <div>\n      <p>Name: {state.name}</p>\n      <p>Age: {state.age}</p>\n      <button onClick={updateName}>Update Name</button>\n    </div>\n  )\n}\n```\n\n在这个例子中：\n\n- 调用 `setState({ name: 'Jane' })` 会完全替换状态对象，导致 `age` 属性丢失。\n- 最终状态变为 `{ name: 'Jane' }`，而不是 `{ name: 'Jane', age: 30 }`。\n\n3. **如何在函数组件中实现状态合并？**\n\n在函数组件中，如果需要实现类似类组件的状态合并特性，可以手动合并状态：\n\n方法 1：使用扩展运算符\n\n```jsx\nfunction MyComponent() {\n  const [state, setState] = useState({\n    name: 'John',\n    age: 30,\n  })\n\n  const updateName = () => {\n    setState((prevState) => ({\n      ...prevState, // 复制旧状态\n      name: 'Jane', // 更新 name\n    }))\n  }\n\n  return (\n    <div>\n      <p>Name: {state.name}</p>\n      <p>Age: {state.age}</p>\n      <button onClick={updateName}>Update Name</button>\n    </div>\n  )\n}\n```\n\n方法 2：使用 `useReducer`\n`useReducer` 可以更灵活地管理复杂状态，并实现类似合并的行为。\n\n```jsx\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'UPDATE_NAME':\n      return {\n        ...state,\n        name: action.payload,\n      }\n    default:\n      throw new Error()\n  }\n}\n\nfunction MyComponent() {\n  const [state, dispatch] = useReducer(reducer, {\n    name: 'John',\n    age: 30,\n  })\n\n  const updateName = () => {\n    dispatch({ type: 'UPDATE_NAME', payload: 'Jane' })\n  }\n\n  return (\n    <div>\n      <p>Name: {state.name}</p>\n      <p>Age: {state.age}</p>\n      <button onClick={updateName}>Update Name</button>\n    </div>\n  )\n}\n```\n\n:::\n\n## 什么是 React 受控组件？\n\n::: details 参考答案\n在 React 中，受控组件（Controlled Component） 是指表单元素（如 `<input>`、`<textarea>`、`<select>` 等）的值由 React 的状态（state）控制，而不是由 DOM 自身管理。换句话说，表单元素的值通过 value 属性绑定到 React 的状态，并通过 onChange 事件处理函数来更新状态。\n\n这是一个简单的受控组件示例：\n\n```jsx\nfunction ControlledInput() {\n  const [value, setValue] = useState('')\n\n  const handleChange = (event) => {\n    setValue(event.target.value) // 更新状态\n  }\n\n  return (\n    <div>\n      <input\n        type=\"text\"\n        value={value} // 绑定状态\n        onChange={handleChange} // 监听输入变化\n      />\n      <p>Current value: {value}</p>\n    </div>\n  )\n}\n```\n\n受控组件的优点：\n\n1. 完全控制表单数据：React 状态是表单数据的唯一来源，可以轻松地对数据进行验证、格式化或处理。\n2. 实时响应输入：可以在用户输入时实时更新 UI 或执行其他操作（如搜索建议）。\n3. 易于集成：与其他 React 状态和逻辑无缝集成。\n\n:::\n\n## 使用 React Hook 实现 useCount\n\n```js\n// count 从 0 计数，每一秒 +1 （可使用 setInterval）\nconst { count } = useTimer()\n```\n\n::: details 参考答案\n\n```jsx\nimport { useState, useEffect } from 'react'\n\nfunction useTimer() {\n  const [count, setCount] = useState(0)\n\n  useEffect(() => {\n    // 设置定时器，每秒钟增加 count\n    const intervalId = setInterval(() => {\n      setCount((prevCount) => prevCount + 1)\n    }, 1000)\n\n    // 清理定时器\n    return () => clearInterval(intervalId)\n  }, []) // 空数组表示仅在组件挂载时执行一次\n\n  return { count }\n}\n\nexport default function TimerComponent() {\n  const { count } = useTimer()\n\n  return (\n    <div>\n      <p>Count: {count}</p>\n    </div>\n  )\n}\n```\n\n:::\n\n## 使用 React Hook 实现 useRequest\n\n```js\nconst { loading, data, error } = useRequest(url) // 可只考虑 get 请求\n```\n\n::: details 参考答案\n\n```jsx\nimport { useState, useEffect } from 'react'\nimport axios from 'axios'\n\nfunction useRequest(url) {\n  const [data, setData] = useState(null) // 存储请求的数据\n  const [loading, setLoading] = useState(true) // 加载状态\n  const [error, setError] = useState(null) // 错误信息\n\n  useEffect(() => {\n    const fetchData = async () => {\n      setLoading(true) // 设置加载状态为 true\n      setError(null) // 清空先前的错误\n\n      try {\n        const response = await axios.get(url)\n        if (!response.ok) {\n          throw new Error('请求失败!')\n        }\n        setData(response.data) // 设置数据\n      } catch (err) {\n        setError(err.message) // 捕获错误并设置错误信息\n      } finally {\n        setLoading(false) // 请求结束，设置加载状态为 false\n      }\n    }\n\n    fetchData()\n  }, [url]) // 依赖于 url，当 url 改变时重新发起请求\n\n  return { loading, data, error }\n}\n\n// 使用示例\nexport default function RequestComponent() {\n  const { loading, data, error } = useRequest('https://xxx.xxxx.com/data')\n\n  if (loading) return <p>Loading...</p>\n  if (error) return <p>错误信息: {error}</p>\n  return (\n    <div>\n      <h3>请求结果:</h3>\n      <pre>{JSON.stringify(data)}</pre>\n    </div>\n  )\n}\n```\n\n:::\n\n## React 项目可做哪些性能优化？\n\n::: details 参考答案\n\n1. `useMemo`: 用于缓存昂贵的计算结果，避免在每次渲染时重复计算。\n\n```jsx\nfunction ExpensiveComponent({ items, filter }) {\n  const filteredItems = useMemo(() => {\n    return items.filter((item) => item.includes(filter))\n  }, [items, filter]) // 仅在 items 或 filter 变化时重新计算\n\n  return (\n    <ul>\n      {filteredItems.map((item) => (\n        <li key={item}>{item}</li>\n      ))}\n    </ul>\n  )\n}\n```\n\n2.  `useCallback`: 用于缓存回调函数，避免在每次渲染时创建新的函数实例。\n    `useCallback`\n\n```jsx\nfunction ParentComponent() {\n  const [count, setCount] = useState(0)\n\n  const handleClick = useCallback(() => {\n    setCount((prevCount) => prevCount + 1)\n  }, []) // 空依赖数组，函数不会重新创建\n\n  return (\n    <div>\n      <ChildComponent onClick={handleClick} />\n      <p>Count: {count}</p>\n    </div>\n  )\n}\n\nconst ChildComponent = React.memo(({ onClick }) => {\n  console.log('ChildComponent rendered')\n  return <button onClick={onClick}>Click me</button>\n})\n```\n\n3.  `React.memo`: 是一个高阶组件，用于缓存组件的渲染结果，避免在 props 未变化时重新渲染\n\n```jsx\nconst MyComponent = React.memo(({ value }) => {\n  console.log('MyComponent rendered')\n  return <div>{value}</div>\n})\n\nfunction ParentComponent() {\n  const [count, setCount] = useState(0)\n\n  return (\n    <div>\n      <button onClick={() => setCount(count + 1)}>Increment</button>\n      <MyComponent value=\"Hello\" /> {/* 不会因 count 变化而重新渲染 */}\n    </div>\n  )\n}\n```\n\n4.  `Suspense`: 用于在异步加载数据或组件时显示加载状态，可以减少初始加载时间，提升用户体验\n\n```jsx\nconst LazyComponent = React.lazy(() => import('./LazyComponent'))\n\nfunction MyComponent() {\n  return (\n    <React.Suspense fallback={<div>Loading...</div>}>\n      <LazyComponent />\n    </React.Suspense>\n  )\n}\n```\n\n5.  `路由懒加载`：通过动态导入（dynamic import）将路由组件拆分为单独的代码块，按需加载。可以减少初始加载的代码量，提升页面加载速度\n\n```jsx\nimport { BrowserRouter as Router, Route, Routes } from 'react-router-dom'\nimport React, { Suspense } from 'react'\n\nconst Home = React.lazy(() => import('./Home'))\nconst About = React.lazy(() => import('./About'))\n\nfunction App() {\n  return (\n    <Router>\n      <Suspense fallback={<div>Loading...</div>}>\n        <Routes>\n          <Route path=\"/\" element={<Home />} />\n          <Route path=\"/about\" element={<About />} />\n        </Routes>\n      </Suspense>\n    </Router>\n  )\n}\n```\n\n:::\n\n## 如何统一监听 React 组件报错\n\n::: details 参考答案\n\n1. Error Boundaries（错误边界）\n\n默认情况下，如果你的应用程序在渲染过程中抛出错误，React 将从屏幕上删除其 UI。为了防止这种情况，你可以将 UI 的一部分包装到 错误边界 中。错误边界是一个特殊的组件，可让你显示一些后备 UI，而不是显示例如错误消息这样崩溃的部分。\n\n要实现错误边界组件，你需要提供 static getDerivedStateFromError，它允许你更新状态以响应错误并向用户显示错误消息。你还可以选择实现 componentDidCatch 来添加一些额外的逻辑，例如将错误添加到分析服务。\n\n```jsx\nimport * as React from 'react'\n\nclass ErrorBoundary extends React.Component {\n  constructor(props) {\n    super(props)\n    this.state = { hasError: false }\n  }\n\n  static getDerivedStateFromError(error) {\n    // 更新状态，以便下一次渲染将显示后备 UI。\n    return { hasError: true }\n  }\n\n  componentDidCatch(error, info) {\n    logErrorToMyService(\n      error,\n      // 示例“组件堆栈”：\n      // 在 ComponentThatThrows 中（由 App 创建）\n      // 在 ErrorBoundary 中（由 APP 创建）\n      // 在 div 中（由 APP 创建）\n      // 在 App 中\n      info.componentStack,\n      // 仅在 react@canary 版本可用\n      // 警告：Owner Stack 在生产中不可用\n      React.captureOwnerStack()\n    )\n  }\n\n  render() {\n    if (this.state.hasError) {\n      // 你可以渲染任何自定义后备 UI\n      return this.props.fallback\n    }\n\n    return this.props.children\n  }\n}\n```\n\n然后你可以用它包装组件树的一部分：\n\n```jsx\n<ErrorBoundary fallback={<p>Something went wrong</p>}>\n  <Profile />\n</ErrorBoundary>\n```\n\n如果 Profile 或其子组件抛出错误，ErrorBoundary 将“捕获”该错误，然后显示带有你提供的错误消息的后备 UI，并向你的错误报告服务发送生产错误报告。\n\n2. 全局错误监听\n\n为了捕获 Error Boundaries 无法处理的错误（如事件处理器或异步代码中的错误），可以使用 JavaScript 的全局错误监听机制。\n\n- 使用 window.onerror 监听全局错误。\n- 使用 window.addEventListener('error', handler) 监听未捕获的错误。\n- 使用 window.addEventListener('unhandledrejection', handler) 监听未处理的 Promise 拒绝。\n\n```jsx\nimport React, { useEffect } from 'react'\n\nfunction GlobalErrorHandler() {\n  useEffect(() => {\n    // 监听全局错误\n    const handleError = (error) => {\n      console.error('Global error:', error)\n    }\n\n    // 监听未捕获的错误\n    window.onerror = (message, source, lineno, colno, error) => {\n      handleError(error)\n      return true // 阻止默认错误处理\n    }\n\n    // 监听未处理的 Promise 拒绝\n    window.addEventListener('unhandledrejection', (event) => {\n      handleError(event.reason)\n    })\n\n    // 清理监听器\n    return () => {\n      window.onerror = null\n      window.removeEventListener('unhandledrejection', handleError)\n    }\n  }, [])\n\n  return null\n}\n\n// 在应用的根组件中使用\nfunction App() {\n  return (\n    <div>\n      <GlobalErrorHandler />\n      <MyComponent />\n    </div>\n  )\n}\n```\n\n注意事项：\n\n1. 全局错误监听可以捕获 Error Boundaries 无法处理的错误，但无法阻止组件崩溃。\n2. 需要确保在生产环境中正确处理错误信息，避免暴露敏感信息。\n\n   :::\n\n## React19 升级了哪些新特性？\n\n::: details 参考答案\nReact 19 的更新内容可以参考 React [官方更新博客](https://zh-hans.react.dev/blog/2024/12/05/react-19)\n\n1. Actions 相关\n\n按照惯例，使用异步过渡的函数被称为 “Actions”。 在 Actions 的基础上，React 19 引入了 useOptimistic 来管理乐观更新，以及一个新的 Hook React.useActionState 来处理 Actions 的常见情况。在 react-dom 中添加了 `<form>` Actions 来自动管理表单和 useFormStatus 来支持表单中 Actions 的常见情况。\n\n2. 新的 API: use\n\n在 React 19 中，我们引入了一个新的 API 来在渲染中读取资源：use。\n\n例如，你可以使用 use 读取一个 promise，React 将挂起，直到 promise 解析完成：\n\n```jsx\nimport { use } from 'react'\n\nfunction Comments({ commentsPromise }) {\n  // `use` 将被暂停直到 promise 被解决.\n  const comments = use(commentsPromise)\n  return comments.map((comment) => <p key={comment.id}>{comment}</p>)\n}\n\nfunction Page({ commentsPromise }) {\n  // 当“use”在注释中暂停时,\n  // 将显示此悬念边界。\n  return (\n    <Suspense fallback={<div>Loading...</div>}>\n      <Comments commentsPromise={commentsPromise} />\n    </Suspense>\n  )\n}\n```\n\n3. 服务端组件和动作\n\nReact 服务端组件现已稳定，允许提前渲染组件。与服务端动作（通过“use server”指令启用）配对后，客户端组件可以无缝调用异步服务端函数。\n\n此外，还有一些 React 19 中的改进：\n\n4. ref 作为一个属性\n\n从 React 19 开始，你现在可以在函数组件中将 ref 作为 prop 进行访问：\n\n```jsx\nfunction MyInput({ placeholder, ref }) {\n  return <input placeholder={placeholder} ref={ref} />\n}\n\n//...\n;<MyInput ref={ref} />\n```\n\n新的函数组件将不再需要 forwardRef。\n\n5. 改进了水合错误的错误报告\n\n6. `<Context>` 作为提供者\n\n在 React 19 中，你可以将 `<Context>` 渲染为提供者，就无需再使用 `<Context.Provider>` 了：\n\n```jsx\nconst ThemeContext = createContext('')\n\nfunction App({ children }) {\n  return <ThemeContext value=\"dark\">{children}</ThemeContext>\n}\n```\n\n新的 Context 提供者可以使用 `<Context>`，我们将发布一个 codemod 来转换现有的提供者。在未来的版本中，我们将弃用 `<Context.Provider>`。\n\n更多更新请参考[官方更新博客](https://zh-hans.react.dev/blog/2024/12/05/react-19)\n\n:::\n\n## 简述 Redux 单向数据流\n\n::: details 参考答案\n这是 Redux 单向数据流的典型流程：\n\n```\nView -> Action -> Reducer -> State -> View\n```\n\n1. **View**：\n   - 用户在界面（View）上触发一个事件（如点击按钮）。\n2. **Action**：\n   - 事件触发一个 `action`，并通过 `store.dispatch(action)` 分发。\n3. **Reducer**：\n   - `store` 调用 `reducer`，传入当前的 `state` 和 `action`，生成一个新的 `state`。\n4. **State**：\n   - `store` 更新 `state`，并通知所有订阅了 `store` 的组件。\n5. **View**：\n   - 组件根据新的 `state` 重新渲染界面。\n\n**Redux 单向数据流的特点**\n\n1. **可预测性**：\n   - 由于状态更新是通过纯函数（`reducer`）完成的，相同的 `state` 和 `action` 总是会生成相同的新的 `state`。\n2. **集中管理**：\n   - 所有状态都存储在单一的 `store` 中，便于调试和管理。\n3. **易于测试**：\n   - `reducer` 是纯函数，没有副作用，易于测试。\n4. **时间旅行调试**：\n   - 通过记录 `action` 和 `state`，可以实现时间旅行调试（如 Redux DevTools）。\n\n---\n\n**示例代码**\n\n以下是一个完整的 Redux 示例：\n\n```javascript\n// 1. 定义 Action Types\nconst ADD_TODO = 'ADD_TODO'\n\n// 2. 定义 Action Creator\nfunction addTodo(text) {\n  return {\n    type: ADD_TODO,\n    payload: text,\n  }\n}\n\n// 3. 定义 Reducer\nfunction todoReducer(state = [], action) {\n  switch (action.type) {\n    case ADD_TODO:\n      return [...state, action.payload]\n    default:\n      return state\n  }\n}\n\n// 4. 创建 Store\nconst store = Redux.createStore(todoReducer)\n\n// 5. 订阅 Store\nstore.subscribe(() => {\n  console.log('Current State:', store.getState())\n})\n\n// 6. 分发 Action\nstore.dispatch(addTodo('Learn Redux'))\nstore.dispatch(addTodo('Build a project'))\n```\n\n:::\n\n## 用过哪些 Redux 中间件？\n\n::: details 参考答案\n\nRedux 中间件（Middleware）允许你在 `action` 被分发（`dispatch`）到 `reducer` 之前或之后执行额外的逻辑。中间件通常用于处理异步操作、日志记录、错误处理等任务。\n\n常用的 Redux 中间件有\n\n**1. Redux Thunk**\n\n- **描述**: Redux Thunk 是最常用的中间件之一，用于处理异步操作（如 API 调用）。\n- **特点**:\n  - 允许 `action` 是一个函数（而不仅仅是一个对象）。\n  - 函数可以接收 `dispatch` 和 `getState` 作为参数，从而在异步操作完成后手动分发 `action`。\n- **使用场景**: 处理异步逻辑（如数据获取）。\n- **示例**:\n  ```javascript\n  const fetchData = () => {\n    return (dispatch, getState) => {\n      dispatch({ type: 'FETCH_DATA_REQUEST' })\n      fetch('/api/data')\n        .then((response) => response.json())\n        .then((data) => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }))\n        .catch((error) => dispatch({ type: 'FETCH_DATA_FAILURE', error }))\n    }\n  }\n  ```\n\n**2. Redux Saga**\n\n- **描述**: Redux Saga 是一个基于生成器函数（Generator）的中间件，用于管理复杂的异步流程和副作用。\n- **特点**:\n  - 使用 ES6 的生成器函数来处理异步逻辑。\n  - 提供强大的副作用管理（如取消任务、并发执行等）。\n- **使用场景**: 复杂的异步流程（如竞态条件、任务取消等）。\n- **示例**:\n\n  ```javascript\n  import { call, put, takeEvery } from 'redux-saga/effects'\n\n  function* fetchData() {\n    try {\n      const data = yield call(fetch, '/api/data')\n      yield put({ type: 'FETCH_DATA_SUCCESS', payload: data })\n    } catch (error) {\n      yield put({ type: 'FETCH_DATA_FAILURE', error })\n    }\n  }\n\n  function* watchFetchData() {\n    yield takeEvery('FETCH_DATA_REQUEST', fetchData)\n  }\n  ```\n\n**3. Redux Logger**\n\n- **描述**: Redux Logger 是一个用于记录 `action` 和 `state` 变化的中间件。\n- **特点**:\n  - 在控制台中打印每个 `action` 的分发和 `state` 的变化。\n  - 便于调试和开发。\n- **使用场景**: 开发环境中的调试。\n- **示例**:\n  ```javascript\n  const store = createStore(rootReducer, applyMiddleware(logger))\n  ```\n\n**4. Redux Promise**\n\n- **描述**: Redux Promise 是一个用于处理 Promise 的中间件。\n- **特点**:\n  - 自动处理 Promise 类型的 `action`。\n  - 当 Promise 完成时，自动分发成功的 `action`；当 Promise 失败时，自动分发失败的 `action`。\n- **使用场景**: 简单的异步操作。\n- **示例**:\n  ```javascript\n  const fetchData = () => ({\n    type: 'FETCH_DATA',\n    payload: fetch('/api/data').then((response) => response.json()),\n  })\n  ```\n  :::\n\n## 你用过哪些 React 状态管理库？\n\n::: details 参考答案\n\n根据自己实际的使用情况作答：\n\n1. Redux\n\nRedux 是最流行的 React 状态管理库之一。它提供了一个全局的状态容器，允许你在应用的任何地方访问和更新状态。特点包括: 单向数据流、中间件支持、时间旅行调试。\n\n2. MobX\n\nMobX 是一个响应式状态管理库，它通过自动追踪状态的变化来更新 UI。特点包括: 响应式编程、简单易用、自动依赖追踪。\n\n3. Recoil\n\nRecoil 是 Facebook 推出的一个实验性状态管理库，专为 React 设计。特点包括: 原子状态管理、派生状态、与 React 深度集成。适用于需要细粒度状态管理的应用。\n\n4. zustand\n\nZustand 是一个轻量级的状态管理库，API 简单且易于使用。特点包括: 轻量、简单、支持中间件。适用于需要轻量级状态管理的应用。\n\n5. Jotai\n\nJotai 是一个基于原子状态管理的库，类似于 Recoil，但更加轻量。特点包括: 原子状态、简单易用、与 React 深度集成。适用于需要细粒度状态管理的应用。\n\n6. XState\n\nXState 是一个基于状态机的状态管理库，适用于复杂的状态逻辑和流程管理。特点包括: 状态机、可视化工具、复杂状态管理。适用于需要复杂状态逻辑和流程管理的应用。\n\n:::\n\n## 是否用过 SSR 服务端渲染？\n\n::: details 参考答案\n\n**SSR**\n\n服务端渲染（Server-Side Rendering, SSR）是一种在服务器端生成 HTML 并将其发送到客户端的技术。与传统的客户端渲染（CSR）相比，SSR 可以提供更快的首屏加载速度、更好的 SEO 支持以及更友好的用户体验。\n\n**SSR 的核心优势**\n\n1. **更快的首屏加载**：\n   - SSR 在服务器端生成 HTML，用户无需等待 JavaScript 加载完成即可看到页面内容。\n2. **更好的 SEO**：\n   - 搜索引擎可以抓取服务器渲染的完整 HTML 内容，而不是空的 `<div id=\"root\"></div>`。\n3. **更好的用户体验**：\n   - 对于低性能设备或网络较差的用户，SSR 可以提供更快的初始渲染。\n\n**SSR 的基本原理**\n\n1. **服务器端**：\n   - 使用 `ReactDOMServer` 将 React 组件渲染为 HTML 字符串。\n   - 将生成的 HTML 字符串嵌入到 HTML 模板中，并发送给客户端。\n2. **客户端**：\n   - 客户端接收到 HTML 后，React 会“接管”页面（hydration），使其成为可交互的 SPA（单页应用）。\n\n**React SSR 的框架支持**\n\n最常用的框架就是 Next.js，它是一个基于 React 的全栈开发框架，集成了最新的 React 特性，内置 SSR 支持，可以帮助你快速创建全栈应用。\n:::\n"
  },
  {
    "path": "docs/second-exam/vue-inner.md",
    "content": "# Vue 原理\n\n国内面试，大厂必考原理。\n\n::: tip\n\n1. 目标**不在**中大厂的同学，可以略过这一节。\n2. 对 Vue 使用尚不熟练的同学，不要在此花费太多精力，先熟悉使用再说。\n\n:::\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 什么是 MVVM\n\n::: details 参考答案\n\n**MVVM（Model-View-ViewModel）** 是一种用于构建用户界面的架构模式，用于现代的前端开发框架（Vue、Angular）。它通过 **数据绑定** 和 **视图模型** 提供了高效的 UI 更新和数据同步机制。\n\nMVVM 模式主要由 `Model` （模型）、 `View` （视图）、 `ViewModel` （视图模型）三个部分组成。\n\n- `Model`表示程序的核心数据和业务逻辑，它不关心用户界面，只负责数据的获取、存储和处理，并提供与外界交互的接口。\n- `View`负责展示数据和用户交互，简单来说他就是我们看到的UI 组件或 HTML 页面。\n- `ViewModel`是连接 `View` 和 `Model` 的桥梁，它不直接操作视图或模型，而是通过数据绑定将两者连接起来。\n\n参考下面的示例：\n\n```vue\n<div id=\"app\">\n  <input v-model=\"message\"/>\n  <p>{{ computedValue }}</p>\n</div>\n\n<script setup>\nconst message = ref('Hello, MVVM!')\n\nconst computedValue = computed(() => {\n  return '用户输入值变为:' + message.value\n})\n</script>\n```\n\n上述代码展示了一个输入框，当用户输入内容的时候，输入框下面的计算值会随之变化。在这个示例中， `message` 变量属于 `Model` ，它包含了应用的核心数据。输入框与页面展示就属于View，负责展示数据和用户交互。 `computed` 和 `v-model语法糖` 作为 `ViewModel` ，用于更新视图和数据。\n\n![](../imgs/vue/mvvm.png)\n\n:::\n\n## 什么是 VDOM 它和 DOM 有什么关系\n\n::: details 参考答案\n\n页面的所有元素、属性和文本都通过 `DOM` 节点表示， `VDOM（Virtual DOM，虚拟 DOM）` 是DOM渲染的一种优化，它是一个内存中的虚拟树，是真实 DOM 的轻量级 JavaScript 对象表示。\n\nVDOM主要用于优化 UI 渲染性能，它的工作流程大致如下：\n\n- 1️⃣**创建虚拟 DOM**：当组件的状态或数据发生变化时，Vue 会重新生成虚拟 DOM。\n- 2️⃣**比较虚拟 DOM 和真实 DOM**：Vue 使用一种高效的算法来比较新旧虚拟 DOM 的差异（即 diff 算法）。\n- 3️⃣**更新 DOM**：根据差异更新真实的 DOM，仅修改有变化的部分，而不是重新渲染整个 DOM 树。\n\n:::\n\n## 手写 VNode 对象，表示如下 DOM 节点\n\n```html\n<div class=\"container\">\n  <img src=\"x1.png\" />\n  <p>hello</p>\n</div>\n```\n\n> 如果你还不熟悉 `虚拟 DOM` 和 `渲染函数` 的概念的话，请先学习[vue的渲染机制](https://cn.vuejs.org/guide/extras/rendering-mechanism.html)\n\n::: details 参考答案\n\nVue 模板会被预编译成**虚拟 DOM 渲染函数**，我们也可以直接手写**渲染函数**，在处理高度动态的逻辑时，渲染函数相比于模板更加灵活，因为我们可以完全地使用 `JavaScript` 来构造我们想要的 `vnode` 。\n\nVue 提供了一个 `h()` 函数用于创建 `vnodes`\n\n```javascript\nh(type, props, children)\n```\n\n- `type`: 表示要渲染的节点类型（例如 HTML 标签名或组件）。\n- `props`: 一个对象，包含该节点的属性（例如 `class`、`style`、`src` 等）。\n- `children`: 子节点，可以是文本内容、数组或者其他 VNode。\n\n```javascript\nimport { h } from 'vue'\n\nexport default {\n  render() {\n    return h(\n      'div',\n      {\n        class: 'container',\n      },\n      [\n        h('img', {\n          src: 'x1.png',\n        }),\n        h('p', null, 'hello'),\n      ]\n    )\n  },\n}\n```\n\n:::\n\n## Vue 组件初始化的各个阶段都做了什么？\n\n::: details 参考答案\n从组件的创建到挂载到页面，再到组件的更新和销毁，每个阶段都有特定的任务和职责。\n\n🎯 组件实例创建：当我们第一次访问页面时，Vue创建组件实例，解析`props`、`data`、`methods`等属性方法，在组合式API中，执行 `setup()`。\n\n🎯 响应式系统建立：基于 `Proxy` 实现 `reactive`、`ref`，建立依赖收集和触发更新机制，`props` 传递时自动响应式处理。\n\n🎯 模板编译与渲染：将 template 编译为渲染函数，Vue 3 通过 静态提升等方式优化性能，Vite 预编译 `SFC（单文件组件）`。\n\n🎯 DOM 挂载：执行渲染函数生成 VNode，通过 `Patch 算法` 转换为真实 DOM 并插入页面，同时初始化子组件。`mounted（Options API`）或 `onMounted（Composition API）`触发，可进行 DOM 操作。\n\n🎯 响应式更新：状态变更触发 `Diff` 算法 计算最小 DOM 更新，`beforeUpdate`、`updated（Options API）`或 `onBeforeUpdate`、`onUpdated（Composition API）`执行相应逻辑。\n\n🎯 组件销毁：移除 DOM，清理副作用（解绑事件、销毁 `watcher`、清理 `effect`），递归卸载子组件，触发 `beforeUnmount`、`unmounted（Options API）`或 `onBeforeUnmount`、`onUnmounted（Composition API）`。\n\n> Vue 3 通过 `Proxy` 响应式、编译优化、生命周期调整提升性能，使组件更高效。\n\n:::\n\n## Vue 如何实现双向数据绑定\n\n::: details 参考答案\n\nVue 实现双向数据绑定的核心是通过**响应式系统**的 **数据劫持**和 **观察者模式**来实现的。\n\n🎯 **数据劫持**\n\nVue 2.x 使用 `Object.defineProperty` 对数据对象的每个属性递归添加 `getter/setter` ，当数据的属性被访问时，触发 `getter` ，当属性被修改时，触发 `setter` 通知视图进行更新。通过这种方式，Vue 可以监控数据的变化，并在数据变化时通知视图更新。\n\n> Vue 3.x 使用 Proxy通过代理对象拦截整个对象的操作，无需递归初始化所有属性，性能更好。\n\n🎯 **观察者模式**\n\nVue 的响应式系统通过 **观察者模式** 来实现数据与视图的同步更新，简化的流程如下：\n\n- **依赖收集**：当 Vue 组件的视图模板渲染时，它会读取数据对象的属性（例如 `{{ message }}`）。在读取属性时，getter方法会将视图组件与该数据属性建立依赖关系。\n\n![](../imgs/vue/数据绑定1.png)\n\n- **观察者（Watcher）**：每个依赖的数据都会对应一个观察者。观察者的作用是监听数据的变化，一旦数据发生变化，观察者会收到通知，进而触发视图的更新。\n\n![](../imgs/vue/数据绑定2.png)\n\n- **通知视图更新（Notify View Update）**：当数据通过 `setter` 修改时，Vue 会触发相应的观察者，通知相关的视图组件更新。\n\n![](../imgs/vue/数据绑定3.png)\n\n通过这种方式，Vue 可以监控数据的变化，并在数据变化时通知视图更新。\n\n:::\n\n## Vue 模板编译的过程\n\n::: details 参考答案\n\nVue 的模板编译过程是将开发者编写的模板语法（例如 `{{ message }}` 和 `v-bind` 等）转换为 JavaScript 代码的过程。它主要分为三个阶段：**模板解析**、**AST优化** 和 **代码生成**：\n\n1️⃣ **模板解析**\n\nVue 使用其解析器将 HTML 模板转换为 **抽象语法树（AST）**。在这个阶段，Vue 会分析模板中的标签、属性和指令，生成一颗树形结构。每个节点表示模板中的一个元素或属性。\n\n如：\n\n```javascript\n< div >\n    <\n    p > {\n        {\n            message\n        }\n    } < /p> <\nbutton v - on: click = \"handleClick\" > 点击 < /button> < /\ndiv >\n```\n\n被解析成的 AST 类似于下面的结构：\n\n```javascript\n{\n    type: 1, // 节点类型：1 表示元素节点\n    tag: 'div', // 元素的标签名\n    children: [ // 子节点（嵌套的 HTML 元素）\n        {\n            type: 1, // 子节点是一个元素节点\n            tag: 'p',\n            children: [{\n                type: 2, // 2 表示插值表达式节点\n                expression: 'message' // 表达式 'message'\n            }]\n        },\n        {\n            type: 1, // 另一个元素节点\n            tag: 'button',\n            events: { // 事件监听\n                click: 'handleClick' // 绑定 click 事件，执行 handleClick 方法\n            },\n            children: [{\n                type: 3, // 文本节点\n                text: '点击' // 按钮文本\n            }]\n        }\n    ]\n}\n```\n\n2️⃣ **AST优化**\n\nVue 在生成渲染函数前，会对 AST 进行优化。优化的核心目标是标记 **静态节点**，在渲染时，Vue 可以跳过这些静态节点，提升性能。\n\n> **静态节点**指所有的渲染过程中都不变化的内容，比如 `某个div标签内的静态文本`\n\n在 `vue3` 中，如果一个节点及其子树都不依赖于动态数据，那么该节点会被提升到渲染函数外部（静态提升），仅在组件初次渲染时创建。\n\n3️⃣ **代码生成**\n\n生成渲染函数是编译的最终阶段，这个阶段会将优化后的 AST 转换成 JavaScript 渲染函数。\n\n例如，像这样的模板：\n\n```html\n<div id=\"app\">{{ message }}</div>\n```\n\n最终会生成类似这样的渲染函数：\n\n```javascript\nfunction render() {\n  return createVNode(\n    'div',\n    {\n      id: 'app',\n    },\n    [createTextVNode(this.message)]\n  )\n}\n```\n\n渲染函数的返回值是一个 `虚拟 DOM（VDOM）树` ，Vue 会根据 `虚拟 DOM` 来更新实际的 `DOM` 。由于 `渲染函数` 被 Vue 的响应式系统包裹，当数据发生变化时，渲染函数会被重新执行生成新的虚拟 DOM，因此页面也会实时更新。\n\n:::\n\n## Vue 响应式原理\n\n::: details 参考答案\n\nVue 的响应式原理在 2.x 和 3.x 中有所不同，分别基于 `Object.defineProperty` 和 `Proxy` 实现。\n\n🎯 **Vue 2.x 的实现 ( `Object.defineProperty` )**\n\n`Object.defineProperty` 支持 IE9 及以上版本，兼容性非常好。它会递归遍历对象，对每个属性单独设置 `getter` 和 `setter` ，但也存在以下局限性：\n\n- **无法监听动态属性增删**\n  Vue 2.x 在新增或删除对象属性时不会触发视图更新，需通过 `Vue.set` 或 `Vue.delete` 手动处理。\n- **数组监听受限**\n  无法直接监听数组索引的修改（如 `arr[0] = 1` ）和 `length` 变化，因此 Vue 2.x 重写了数组的一些方法来解决这一问题。\n- **性能开销较大**\n  需要递归地为每个属性设置 `getter` 和 `setter` ，对深层嵌套的对象和大型数组性能较差。\n- **不支持 Map/Set 等数据结构**\n  只能代理普通对象和数组，不能处理像 `Map` 、 `Set` 等复杂数据结构。\n\n🚀 **Vue 3.x 的实现 ( `Proxy` )**\n\n为了解决 Vue 2.x 中的这些问题，Vue 3.x 采用了 `Proxy` ，带来了更优的性能和更全面的响应式支持：\n\n- **动态属性增删支持**\n  `Proxy` 可以直接代理整个对象，因此可以监听属性的动态增删，不再需要手动操作。\n- **完美支持数组和索引修改**\n  `Proxy` 能够监听数组索引的修改（如 `arr[0] = 1` ）以及 `length` 变化，避免了 Vue 2.x 中的重写数组方法。\n- **性能更优**\n  `Proxy` 采用懒代理模式，只有在访问属性时才会递归代理子对象，避免了递归遍历的性能开销。\n- **支持更多数据结构**\n  除了普通对象和数组， `Proxy` 还可以代理 `Map` 、 `Set` 等数据结构，提供了更强大的响应式能力。\n\n| 特性         | `Object.defineProperty` <br/>（Vue 2）    | `Proxy` <br/>（Vue 3）          |\n| ------------ | ----------------------------------------- | ------------------------------- |\n| 动态属性增删 | ❌ 不支持（需 `Vue.set` / `Vue.delete` ） | ✅ 支持                         |\n| 数组索引修改 | ❌ 需重写方法（如 `push` ）               | ✅ 直接监听                     |\n| 性能         | ⚠️ 递归初始化所有属性，性能较差           | ✅ 惰性代理，按需触发，性能更优 |\n| 数据结构支持 | ❌ 仅普通对象/数组                        | ✅ 支持 `Map` 、 `Set` 等       |\n| 兼容性       | ✅ 支持 IE9+                              | ❌ 不支持 IE                    |\n| 实现复杂度   | ⚠️ 需递归遍历对象，代码冗余               | ✅ 统一拦截，代码简洁           |\n\n:::\n\n## 为何 v-for 需要使用 key\n\n::: details 参考答案\n\n在 Vue.js 中，使用 `v-for` 渲染列表时，添加 key 属性是一个重要的最佳实践。\n\n- **提高性能**：当 Vue 更新视图时，它会根据 `key` 来识别哪些元素被修改、添加或移除。如果没有 `key`，Vue 会依赖其默认的算法（基于元素的位置）来比较元素，这样可能导致不必要的 DOM 操作。使用 `key` 后，Vue 能精确地找到每个项，从而减少不必要的 DOM 重排和重绘，提升性能。\n- **保持组件状态**：如果渲染的是一个组件（而不是普通的 DOM 元素），使用 `key` 可以确保组件在渲染更新时保持正确的状态。例如，如果列表中有表单输入框，每个输入框都有自己的状态，使用 `key` 可以确保输入框状态不会因列表排序或元素移除而丢失。\n- **避免渲染错误**：key 的存在可以帮助 Vue 确保在列表更新时，元素的顺序和内容保持稳定，避免出现不稳定的渲染或顺序错乱。\n\n:::\n\n## Vue diff 算法的过程\n\n::: details 参考答案\n\nVue的diff算法执行，依赖数据的的响应式系统：当数据发生改变时， `setter` 方法会让调用 `Dep.notify` 通知所有订阅者 `Watcher` ，订阅者会重新执行渲染函数，渲染函数内部通过diff 算法用于比较新旧虚拟 DOM 树的差异，并计算出最小的更新操作，最终更新相应的视图。\n\n![](../imgs/vue/render.png)\n\ndiff 算法的核心算法流程如下：\n\n- 节点对比\n  如果新旧节点类型相同，则继续比较它们的属性。如果节点类型不同（如元素和文本节点不同），则直接**替换**整个节点。\n- 属性更新：\n  如果节点类型相同，接下来检查节点的属性。对于不同的属性值进行更新，移除旧属性，添加新属性。\n- 子节点比对：\n  对于有子节点的元素（如 div），Vue 会使用不同的策略来优化子节点更新：\n  🎯 文本节点的更新：如果新旧子节点都是文本节点，直接更新文本内容。\n  🎯 数组类型子节点的比对：如果新旧子节点都是数组，Vue 会通过 `LIS 算法` 来优化节点的重新排列，避免过多的 DOM 操作。\n\n![](../imgs/vue/diff.png)\n\n:::\n\n## Vue3 diff 算法做了哪些优化？\n\n::: details 参考答案\n\n- 静态标记与动态节点的区分\n  Vue3引入了 `静态标记（Static Marking）` 机制，通过在模板编译阶段为静态节点添加标记，避免了对这些节点的重复比较。这使得Vue3能够更高效地处理静态内容，减少不必要的DOM操作。\n- 双端对比策略\n  Vue3的Diff算法采用了双端对比策略，即从新旧节点的头部和尾部同时开始比较，快速定位无序部分。这种策略显著减少了全量对比的复杂度，提升了性能。\n- 最长递增子序列（LIS）优化\n  在处理节点更新时，Vue3利用最长递增子序列（LIS）算法来优化对比流程。通过找到新旧节点之间的最长递增子序列，Vue3可以减少不必要的DOM操作，从而提高更新效率。\n- 事件缓存与静态提升\n  事件缓存：Vue3将事件缓存为静态节点，避免每次渲染时重新计算事件处理逻辑，从而减少性能开销。\n  静态提升：对于不参与更新的元素，Vue3将其提升为静态节点，仅在首次创建时进行处理，后续不再重复计算。\n- 类型检查与属性对比\n  Vue3在Diff算法中增加了类型检查和属性对比功能。如果节点类型不同，则直接替换；如果类型相同，则进一步对比节点的属性，生成更新操作。\n- 动态插槽的优化\n  Vue3对动态插槽进行了优化，通过动态节点的类型化处理，进一步提升了Diff算法的效率\n\n:::\n\n## Vue diff 算法和 React diff 算法的区别\n\n::: details\n\nVue 和 React 的 Diff 算法均基于虚拟 DOM，但在 `实现策略` 、 `优化手段` 和 `设计哲学` 上存在显著差异：\n\n**1. 核心算法策略对比**\n\n| **维度**     | **React**                      | **Vue 2/3**                          |\n| ------------ | ------------------------------ | ------------------------------------ |\n| **遍历方式** | 单向递归（同层顺序对比）       | 双端对比（头尾指针优化）             |\n| **节点复用** | 类型相同则复用，否则销毁重建   | 类型相同则尝试复用，优先移动而非重建 |\n| **静态优化** | 需手动优化（如 `React.memo` ） | 编译阶段自动标记静态节点             |\n| **更新粒度** | 组件级更新（默认）             | 组件级 + 块级（Vue3 Fragments）      |\n\n**2. 列表 Diff 实现细节**\n\n**a. React 的索引对比策略**\n\n- **无 key 时**：按索引顺序对比，可能导致无效更新\n\n```jsx\n// 旧列表：[A, B, C]\n// 新列表：[D, A, B, C]（插入头部）\n// React 对比结果：更新索引 0-3，性能低下\n```\n\n- **有 key 时**：通过 key 匹配节点，减少移动操作\n\n```jsx\n// key 匹配后，仅插入 D，其他节点不更新\n```\n\n**b. Vue 的双端对比策略**\n\n分四步优化对比效率（Vue2 核心逻辑，Vue3 优化为最长递增子序列）：\n\n1. **头头对比**：新旧头指针节点相同则复用，指针后移\n2. **尾尾对比**：新旧尾指针节点相同则复用，指针前移\n3. **头尾交叉对比**：旧头 vs 新尾，旧尾 vs 新头\n4. **中间乱序对比**：建立 key-index 映射表，复用可匹配节点\n\n```js\n// 旧列表：[A, B, C, D]\n// 新列表：[D, A, B, C]\n// Vue 通过步骤3头尾对比，仅移动 D 到头部\n```\n\n**3. 静态优化机制**\n\n**a. Vue 的编译时优化**\n\n- **静态节点标记**：\n  模板中的静态节点（无响应式绑定）会被编译为常量，跳过 Diff\n\n```html\n<!-- 编译前 -->\n<div>Hello Vue</div>\n\n<!-- 编译后 -->\n_hoisted_1 = createVNode(\"div\", null, \"Hello Vue\")\n```\n\n- **Block Tree（Vue3）**：\n  动态节点按区块（Block）组织，Diff 时仅对比动态部分\n\n**b. React 的运行时优化**\n\n- **手动控制更新**：\n  需通过 `React.memo` 、 `shouldComponentUpdate` 或 `useMemo` 避免无效渲染\n\n```jsx\nconst MemoComp = React.memo(() => <div>Static Content</div>)\n```\n\n**4. 响应式更新触发**\n\n| **框架** | **机制**                   | **Diff 触发条件**                |\n| -------- | -------------------------- | -------------------------------- |\n| React    | 状态变化触发组件重新渲染   | 父组件渲染 → 子组件默认递归 Diff |\n| Vue      | 响应式数据变更触发组件更新 | 依赖收集 → 仅受影响组件触发 Diff |\n\n```javascript\n// Vue：只有 data.value 变化才会触发更新\nconst vm = new Vue({\n  data: {\n    value: 1,\n  },\n})\n\n// React：需显式调用 setState\nconst [value, setValue] = useState(1)\n```\n\n**5. 设计哲学差异**\n\n| **维度**     | **React**                  | **Vue**                    |\n| ------------ | -------------------------- | -------------------------- |\n| **控制粒度** | 组件级控制（开发者主导）   | 细粒度依赖追踪（框架主导） |\n| **优化方向** | 运行时优化（Fiber 调度）   | 编译时优化（模板静态分析） |\n| **适用场景** | 大型动态应用（需精细控制） | 中小型应用（快速开发）     |\n\n:::\n\n## 简述 Vue 组件异步更新的过程\n\n参考答案\n\n::: details\n\nVue 组件的异步更新过程是其响应式系统的核心机制，主要通过 **批量更新** 和 **事件循环** 实现高效渲染，具体流程如下：\n\n**一、触发阶段：依赖收集与变更通知**\n\n1. **数据变更**\n   当组件内响应式数据（如 `data` 、 `props` ）被修改时，触发 `setter` 通知依赖（Watcher）。\n\n2. **Watcher 入队**\n   所有关联的 Watcher 会被推入 **异步更新队列**（ `queueWatcher` ），Vue 通过 `id` 去重，确保每个 Watcher 仅入队一次，避免重复更新。\n\n**二、调度阶段：异步队列处理** 3. **异步执行**\nVue 将队列刷新任务放入微任务队列（优先 `Promise.then` ，降级 `setImmediate` 或 `setTimeout` ），等待当前同步代码执行完毕后处理。\n\n```javascript\n// 伪代码：nextTick 实现\nconst timerFunc = () => {\n  if (Promise) {\n    Promise.resolve().then(flushQueue)\n  } else if (MutationObserver) {\n    /* 使用 MO */\n  } else {\n    setTimeout(flushQueue, 0)\n  }\n}\n```\n\n4. **合并更新**\n   同一事件循环中的多次数据变更会被合并为一次组件更新（如循环中修改数据 100 次，仅触发 1 次渲染）。\n\n**三、执行阶段：虚拟 DOM 与 DOM 更新** 5. **组件重新渲染**\n执行队列中的 Watcher 更新函数，触发组件的 `render` 生成新虚拟 DOM（VNode）。\n\n6. **Diff 与 Patch**\n   通过 **Diff 算法** 对比新旧 VNode，计算出最小化 DOM 操作，批量更新真实 DOM。\n\n**四、核心优势**\n\n- **性能优化**：避免频繁 DOM 操作，减少重排/重绘。\n- **数据一致性**：确保在同一事件循环中的所有数据变更后，视图一次性更新到最终状态。\n- **开发者友好**：通过 `Vue.nextTick(callback)` 在 DOM 更新后执行逻辑。\n\n```javascript\nexport default {\n  data() {\n    return {\n      count: 0,\n    }\n  },\n  methods: {\n    handleClick() {\n      this.count++ // Watcher 入队\n      this.count++ // 去重，仍只一个 Watcher\n      this.$nextTick(() => {\n        console.log('DOM已更新:', this.$el.textContent)\n      })\n    },\n  },\n}\n```\n\n点击事件中两次修改 `count` ，但 DOM 仅更新一次， `nextTick` 回调能获取最新 DOM 状态。\n\n**总结流程图**\n\n```\n数据变更 → Watcher 入队 → 微任务队列 → 批量执行 Watcher → 生成 VNode → Diff/Patch → DOM 更新\n```\n\n通过异步更新机制，Vue 在保证性能的同时，实现了数据驱动视图的高效响应。\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7054488305659805727\n\n:::\n\n## Vue 组件是如何渲染和更新的\n\n::: details 参考答案\n\nVue 组件的渲染和更新过程涉及从 `模板编译` 到 `虚拟 DOM` 的**构建**、**更新**和最终的实际 DOM 更新。下面是 Vue 组件渲染和更新的主要步骤：\n\n1️⃣ 组件渲染过程\nVue 的组件的渲染过程核心是其[模板编译](./vue-inner/#vue-模板编译的过程)过程，大致流程如下：\n首先，Vue会通过其响应式系统完成组件的 `data、computed 和 props` 等数据和模板的绑定，这个过程Vue 会利用 `Object.defineProperty（Vue2）` 或 `Proxy（Vue3）` 来追踪数据的依赖，保证数据变化时，视图能够重新渲染。随后，Vue会将模板编译成渲染函数，这个渲染函数会在每次更新时被调用，从而生成虚拟 DOM。\n最终，虚拟DOM被渲染成真实的 DOM 并插入到页面中，组件渲染完成，组件渲染的过程中，Vue 会依次触发相关的生命周期钩子。\n\n2️⃣ 组件更新过程\n当组件的状态（如 data、props、computed）发生变化时，响应式数据的 `setter` 方法会让调用Dep.notify通知所有 `订阅者Watcher` ，重新执行渲染函数触发更新。\n\n![](../imgs/vue/模板编译.png)\n\n渲染函数在执行时，会使用 diff 算法（例如：双端对比、静态标记优化等）生成新的虚拟DOM。计算出需要更新的部分后（插入、删除或更新 DOM），然后对实际 DOM 进行最小化的更新。在组件更新的过程中，Vue 会依次触发beforeUpdate、updated等相关的生命周期钩子。\n\n:::\n\n## 如何实现 keep-alive 缓存机制\n\n::: details 参考答案\n\n`keep-alive` 是 Vue 提供的一个内置组件，用来缓存组件的状态，避免在切换组件时重新渲染和销毁，从而提高性能。\n\n```vue\n<template>\n  <keep-alive>\n    <component :is=\"currentComponent\" />\n  </keep-alive>\n</template>\n```\n\nVue 3 的 keep-alive 的缓存机制原理如下：\n\n- 缓存池：keep-alive 内部使用一个 Map 存储已渲染的组件实例，键通常是组件的 key（或 name）。\n- 激活与挂起：如果组件切换时已经缓存，直接复用缓存的组件实例；如果组件未缓存，则渲染并缓存新的组件实例。\n  此外，keep-alive 还会激活特殊的钩子函数：\n- 当组件被缓存时，会触发 deactivated 钩子。\n- 当组件从缓存中恢复时，会触发 activated 钩子。\n\n一个简单的实现如下：\n\n```javascript\nconst KeepAliveImpl = {\n  name: 'KeepAlive',\n  // 已缓存的组件实例。\n  _cache: new Map(),\n  _activeCache: new Map(),\n\n  render() {\n    const vnode = this.$slots.default()[0] // 获取动态组件的 vnode\n    const key = vnode.key || vnode.type.name\n\n    if (this._cache.has(key)) {\n      const cachedVnode = this._cache.get(key)\n      this._activeCache.set(key, cachedVnode)\n      return cachedVnode\n    } else {\n      return vnode // 未缓存，直接渲染\n    }\n  },\n\n  mounted() {\n    const key = this.$vnode.key\n    if (!this._cache.has(key)) {\n      this._cache.set(key, this.$vnode)\n    }\n  },\n\n  beforeDestroy() {\n    const key = this.$vnode.key\n    this._cache.delete(key)\n  },\n}\n```\n\n:::\n\n## 为何 ref 需要 value 属性\n\n::: details 参考答案\n\nVue 3 中， `ref` 之所以需要 `.value` 属性，主要是因为 Vue 3 使用 `Proxy` 实现响应式。 `Proxy` 对对象或数组的每个属性进行深度代理，因此可以追踪嵌套属性的变化。而 `Proxy` 无法直接处理基本数据类型（如 `number` 、 `string` 、 `boolean` ），这使得 `reactive` 无法用于基本数据类型。为了实现基本数据类型的响应式，Vue 设计了 `ref` ，它将基本数据类型封装为一个包含 `value` 属性的对象，并通过 `getter` 和 `setter` 进行依赖追踪和更新。当访问或修改 `ref.value` 时，Vue 会触发依赖更新。\n\n:::\n"
  },
  {
    "path": "docs/second-exam/vue-usage.md",
    "content": "# Vue 使用\n\n[Vue](https://cn.vuejs.org/) 是国内最普及的前端框架，面试考察概率最高。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 🔥Vue3 和 Vue2 的区别有哪些？说几个主要的\n\nVue 3 提供了更现代化、更高性能的架构，通过 `Composition API` 和 `Proxy` 响应式系统等改进提升了开发体验 :tada::tada::tada:。相比于 Vue 2，它的优势如下:\n\n::: details 参考答案\n\n**性能优化**\n\n- **虚拟 DOM 重构**：Vue3的虚拟DOM采用了更高效的 `Diff算法`，减少了渲染和更新的开销。\n\n- **Tree-shaking 支持**：Vue3的代码结构模块化，支持按需引入，减小了打包体积。\n\n**Composition API**\n\n- Vue3引入了Composition API，使代码更模块化、复用性更强。\n\n- 使用 `setup()` 方法代替了部分选项式 API，通过函数的方式组织逻辑，代码更加清晰简洁。\n\n**响应式系统改进**\n\n- Vue3使用 `Proxy` 实现响应式，解决了 Vue2使用Object.defineProperty实现响应式的一些局限性，如无法监听新增属性和数组索引变化。\n\n**新特性和改进**\n\n- **Teleport**：可以将组件的DOM渲染到指定的DOM节点之外，例如模态框、通知等。\n\n- **Fragment 支持**：Vue3支持组件返回多个根节点，不再需要单一根节点。\n\n- Vue3原生支持 `TypeScript`，提供更完善的类型推导和开发体验。\n\n- Vue3支持为一个组件绑定多个 `v-model`，并且可以自定义 `prop` 和 `event` 名称。\n\n:::\n\n## 🔥Vue 组件的通信方式有哪些？\n\n组件之间的通信通常分为父子组件通信和跨组件通信。要注意，vue3 组件的通信方式和 vue2 有一定的区别。\n\n::: details 参考答案\n\n**父子组件通信**\n\n- Props：父组件通过 `props` 向子组件传递数据。\n- $emit：子组件通过 `$emit` 向父组件发送事件，并可以传递数据。\n- 获取组件实例对象，调用属性或方法：\n  💡 Vue 2：通过 `this.$parent` 获取父组件实例，或通过 `this.$children` 获取子组件实例。\n  💡 Vue 3：通过 `ref` 引用子组件，直接访问其属性和方法。\n- Vue 3 组件支持多个 `v-model` 绑定和自定义属性名，父子之间的双向绑定更加灵活。\n\n**跨组件通信**\n\n- Provide / Inject：父组件通过 `provide` 向后代组件传递数据，后代组件使用 `inject` 接收数据，适用于深层嵌套组件间的通信。\n- vuex：通过全局状态管理库 Vuex 共享状态，实现跨组件通信（vue2）。\n- pinia：Pinia 是 Vue 3 推荐的全局状态管理库，替代了 Vuex。\n- 事件总线（Vue 2）：Vue 2 中可以通过`Event Bus`实现组件间的通信，但在 Vue 3 中不推荐使用。\n- 全局事件处理器：通过在根组件$root或全局对象上监听事件，进行跨组件通信（Vue 3 推荐使用外部库，如 `mitt`）。\n  :::\n\n## 🔥Vue 组件的生命周期\n\nVue 组件的生命周期是指组件从创建到销毁的整个过程，包括组件的初始化、渲染、更新和销毁等阶段。在Vue2和Vue3中，组件的生命周期有一些区别。\n\n::: details Vue2\n\n- **创建阶段**\n\n1️⃣ **beforeCreate**：组件实例刚被创建，数据观测和事件/监听器设置之前。此时无法访问 `data` 、 `computed` 和 `methods` 等。\n2️⃣ **created**：组件实例已创建，数据观测、事件/监听器设置完成，此时可以访问 `data` 、 `computed` 和 `methods` 等，通常用于数据初始化。\n\n- **挂载阶段**\n\n3️⃣ **beforeMount**：在挂载开始之前，模板已编译， `el` 和 `template` 已经确定，但尚未渲染。\n4️⃣ **mounted**：组件实例挂载到 DOM 上之后，此时可以访问和操作 DOM。\n\n- **更新阶段**\n\n5️⃣ **beforeUpdate**：数据发生变化，DOM 尚未更新。可以在这里做一些数据处理，避免不必要的渲染。\n6️⃣ **updated**：数据变化，DOM 更新后调用。此时组件的 DOM 已经更新，可以访问和操作新的 DOM。\n\n- **销毁阶段**\n\n7️⃣ **beforeDestroy**：组件实例销毁之前。可以在此阶段进行清理工作，例如移除事件监听器、定时器等。\n8️⃣ **destroyed**：组件实例销毁之后。此时，所有的事件监听器和子组件已被销毁。\n\n:::\n\n::: details Vue3\n\n- **挂载阶段**\n\n1️⃣ **onBeforeMount**：等效于 Vue 2 中的 `beforeMount` ，在组件挂载之前调用。\n2️⃣ **onMounted**：等效于 Vue 2 中的 `mounted` ，在组件挂载之后调用。\n\n- **更新阶段**\n\n3️⃣ **onBeforeUpdate**：等效于 Vue 2 中的 `beforeUpdate` ，在数据更新之前调用。\n4️⃣ **onUpdated**：等效于 Vue 2 中的 `updated` ，在数据更新并渲染之后调用。\n\n- **销毁阶段**\n\n5️⃣ **onBeforeUnmount**：等效于 Vue 2 中的 `beforeDestroy` ，在组件卸载前调用。\n6️⃣ **onUnmounted**：等效于 Vue 2 中的 `destroyed` ，在组件卸载后调用。\n\n:::\n\n::: tip setup与生命周期\n\nsetup 作为 Vue3 的 Composition API 的一部分, 其内部函数的执行时机早于Mounted钩子。\n\n```vue{7}\n<script setup>\n  import { ref, onMounted } from 'vue';\n  console.log(\"setup\");\n  onMounted(() => {\n  console.log('onMounted');\n  });\n  // 执行结果:setup onMounted\n</script>\n\n```\n\n:::\n\n## Vue 组件在哪个生命周期发送 ajax 请求？\n\n在 Vue中，接口请求一般放在 `created` 或 `mounted` 生命周期钩子中。\n\n::: details 参考答案\n\n**created 钩子**\n\n- 优点：\n  ✅ **更快获取数据**：能尽早获取服务端数据，减少页面加载时间。\n  ✅ **SSR 支持**：支持服务器端渲染（SSR），在 SSR 环境中不会受到限制。\n\n- 缺点\n  ❌ UI 未渲染时发起请求：如果需要操作 DOM 或渲染数据，可能导致闪屏问题\n\n**mounted 钩子**\n\n- 优点：\n  ✅ **DOM 可用**：适合需要操作 DOM 或渲染数据后再发起请求的情况，避免闪屏。\n\n- 缺点\n  ❌ **请求延迟**：数据请求会稍微延迟，增加页面加载时间。\n  ❌ **SSR 不支持**：`mounted` 只在客户端执行，不适用于 SSR 环境。\n\n:::\n\n## Vue 父子组件生命周期调用顺序\n\n::: details 参考答案\n\n1️⃣ 创建阶段\n\n- 父组件：`beforeCreate` ➡️ `created`\n- 子组件：`beforeCreate` ➡️ `created`\n- 顺序：\n  父组件的 `beforeCreate` 和 `created` 先执行 ，子组件的 `beforeCreate` 和 `created` 后执行。\n  > 原因：父组件需要先完成自身的初始化（如 data、computed 等），才能解析模板中的子组件并触发子组件的初始化。\n\n2️⃣ 挂载阶段\n\n- 父组件：`beforeMount`\n- 子组件：`beforeMount` ➡️ `mounted`\n- 父组件：`mounted`\n- 顺序：\n  父 `beforeMount` → 子 `beforeCreate`→ 子 `created`→ 子 `beforeMount`→ 子 `mounted` → 父 `mounted`\n  > 原因：父组件在挂载前（beforeMount）需要先完成子组件的渲染和挂载，因为子组件是父组件模板的一部分。只有当所有子组件挂载完成后，父组件才会触发自身的 mounted。\n\n3️⃣ 更新阶段\n\n- 父组件：`beforeUpdate`\n- 子组件：`beforeUpdate` ➡️ `updated`\n- 父组件：`updated`\n- 顺序：\n  父 `beforeUpdate` → 子 `beforeUpdate` → 子 `updated` → 父 `updated`\n  > 原因：父组件的数据变化会触发自身更新流程，但子组件的更新必须在父组件更新前完成（因为子组件可能依赖父组件的数据），最终父组件的视图更新完成。\n\n4️⃣ 销毁阶段\n\n- 父组件：`beforeDestroy`\n- 子组件：`beforeDestroy` ➡️ `destroyed`\n- 父组件：`destroyed`\n- 顺序：\n  父 `beforeDestroy` → 子 `beforeDestroy` → 子 `destroyed` → 父 `destroyed`\n  > 原因：父组件销毁前需要先销毁所有子组件，确保子组件的资源释放和事件解绑，避免内存泄漏。\n\n::: tip\n注：vue3中，`setup()` 替代了 `beforeCreate` 和 `created`，但父子组件的生命周期顺序不变。\n\n:::\n\n## 🔥v-show 和 v-if 的区别\n\n::: details 参考答案\n\n- **渲染方式：**\n  💡v-if：条件为 true 时才会渲染元素，条件为 false 时销毁元素。\n  💡v-show：始终渲染元素，只是通过 CSS 控制 `display 属性`来显示或隐藏。\n- **适用场景：**\n  💡v-if：适用于条件变化不频繁的场景。\n  💡v-show：适用于条件变化频繁的场景。\n\n:::\n\n## 为何v-if和v-for不能一起使用？\n\n`v-if` 和 `v-for` 不能直接一起使用的原因，主要是因为它们在 **解析优先级** 和 **逻辑处理** 上存在冲突。\n\n::: details 参考答案\n\n由于`v-for` 的解析优先级高于 `v-if`，同时使用 v-if 和 v-for，Vue 首先会循环创建所有dom元素，然后根据条件来判断是否渲染每个元素，这种方式可能导致 Vue 进行大量的 DOM 操作，性能较差。其次，`v-for` 会为每个循环项创建一个新的作用域，而 `v-if` 的条件如果依赖于这个作用域内的数据，可能导致判断逻辑异常。\n\n为避免上述问题，vue官方推荐我们将 `v-if` 放到 `v-for` 外层，或者将 `v-if` 放置到 `v-for` 内部的单个节点上。\n\n```js\n<div v-if=\"show\">\n  <div v-for=\"item in list\" :key=\"item.id\">{{ item.name }}</div>\n</div>\n```\n\n:::\n\n## computed 和 watch 有什么区别\n\n::: details 参考答案\n\n**computed**用于计算基于响应式数据的值，并缓存结果:\n\n```vue\n<template>\n  <div>\n    <p>原始值：{{ count }}</p>\n    <p>计算后的数值：{{ doubledCount }}</p>\n  </div>\n</template>\n\n<script setup>\nimport { ref, computed } from 'vue'\n\nconst count = ref(2)\n\n// 计算属性\nconst doubledCount = computed(() => count.value * 2)\n</script>\n```\n\n**watch**用于监听数据变化并执行副作用操作\n\n```vue\n<template>\n  <div>\n    <p>原始数值：{{ count }}</p>\n    <button @click=\"count++\">增加数值</button>\n  </div>\n</template>\n\n<script setup>\nimport { ref, watch } from 'vue'\n\nconst count = ref(0)\n\n// 监听器\nwatch(count, (newVal, oldVal) => {\n  console.log(`数值从 ${oldVal} 变为 ${newVal}`)\n})\n</script>\n```\n\n| 特性         | `computed`                                                   | `watch`                                                  |\n| ------------ | ------------------------------------------------------------ | -------------------------------------------------------- |\n| **用途**     | 用于计算基于响应式数据的值，并缓存结果                       | 用于监听数据变化并执行副作用操作                         |\n| **返回值**   | 返回计算结果                                                 | 不返回值，执行副作用                                     |\n| **缓存机制** | 只有在访问时才会计算，会缓存计算结果，仅当依赖变化时重新计算 | 数据变化时立即执行回调，不缓存，每次数据变化都会触发回调 |\n| **适用场景** | 计算派生数据，避免不必要的重复计算                           | 执行异步操作、处理副作用操作，如 API 请求                |\n| **性能**     | 性能较好，避免重复计算                                       | 每次数据变化时都会执行回调函数                           |\n\n:::\n\n## 🔥watch 和 watchEffect 的区别\n\n`watch` 和 `watchEffect` 都是 Vue 3 中用于响应式数据变化时执行副作用的 API，它们的使用场景和工作机制存在区别：\n\n::: details 参考答案\n\n- **依赖追踪方式**\n\n`watch` ：需要显式声明依赖，监听指定的数据源；可以监听多个数据源或进行深度监听。\n\n```js\nimport { watch, reactive } from 'vue'\nconst state = reactive({\n  count: 0,\n})\nwatch(\n  () => state.count, // 显式声明监听的依赖\n  (newCount, oldCount) => {\n    console.log(`新值 ${newCount} 老值 ${oldCount}`)\n  }\n)\n```\n\n`watchEffect` ：会自动追踪 **作用域内所有的响应式依赖**，不需要显式声明依赖。\n\n```js\nimport { watchEffect, reactive } from 'vue'\nconst state = reactive({\n  count: 0,\n})\nwatchEffect(() => {\n  console.log(`Count 变化了: ${state.count}`) // 自动追踪 `state.count`\n})\n```\n\n- **执行时机**\n\n`watch` ：在监听的响应式数据变化后立即执行。\n\n`watchEffect` ：在 **组件挂载时** 执行一次副作用，并在 **依赖发生变化时** 再次执行。\n\n- **适用场景**\n\n`watch` ：适用于 **监听特定数据** 变化并执行副作用的场景，如 API 请求、保存操作等。适合需要 **访问新值和旧值** 进行比较的场景。\n\n`watchEffect` ：不需要访问旧值，适用于 **自动追踪多个响应式依赖** 的副作用，如渲染、自动保存等。\n\n:::\n\n> Vue官方API： [watchEffect](https://cn.vuejs.org/api/reactivity-core.html#watcheffect)\n\n## 🔥Vue3 ref 和 reactive 如何选择？\n\n`ref` 和 `reactive` 都是 Vue 3 中用来创建响应式数据的 API，他们的区别及使用场景如下。\n\n::: details 参考答案\n\n- **reactive的实现：**\n  `reactive` 通过 `Proxy` 对对象或数组的每个属性进行深度代理，实现响应式。这种设计使得 `reactive` 能自动追踪所有嵌套属性的变化，但由于 `Proxy` 无法直接处理基本数据类型（如 `number` 、 `string` 、 `boolean` ），因此， `reactive` 不适用于基本数据类型。\n\n- **ref的实现：**\n  为了实现基本数据类型的响应式，Vue 设计了 `ref` 。 `ref` 会将基本数据类型封装为一个包含 `value` 属性的对象，通过 `getter` 和 `setter` 实现响应式依赖追踪和更新。当访问或修改 `ref.value` 时，Vue 内部会触发依赖更新。此外，对于复杂数据类型（如对象或数组）， `ref` 的内部实现会直接调用 `reactive` ，将复杂数据类型变为响应式。\n\n::: tip 如何选择\n\n**Vue官方建议**使用 `ref()` 作为声明响应式状态的主要，因为 `reactive` 存在以下局限性：\n\n💡有限的值类型：它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。\n💡不能替换整个对象：由于 Vue 的响应式跟踪是通过属性访问实现的，因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象，因为这样的话与第一个引用的响应性连接将丢失：\n\n```js\nlet state = reactive({\n  count: 0,\n})\n\n// 上面的 ({ count: 0 }) 引用将不再被追踪\n// (响应性连接已丢失！)\nstate = reactive({\n  count: 1,\n})\n```\n\n💡对解构操作不友好：当我们将响应式对象的原始类型属性解构为本地变量时，或者将该属性传递给函数时，我们将丢失响应性连接\n\n```js\nconst state = reactive({\n  count: 0,\n})\n// 当解构时，count 已经与 state.count 断开连接\nlet { count } = state\n// 不会影响原始的 state\ncount++\n\n// 该函数接收到的是一个普通的数字\n// 并且无法追踪 state.count 的变化\n// 我们必须传入整个对象以保持响应性\ncallSomeFunction(state.count)\n```\n\n:::\n\n## 什么是动态组件？如何使用它？\n\n::: details 参考答案\n\n动态组件是 Vue 提供的一种机制，允许我们根据条件动态切换渲染的组件，而不需要手动修改模板。\n在Vue中，我们可以通过 ` <component>` 标签的 `:is` 属性指定需要渲染的组件：\n\n```vue\n<template>\n  <div>\n    <!-- 动态渲染组件 -->\n    <component :is=\"currentComponent\"></component>\n\n    <!-- 控制组件切换 -->\n    <button @click=\"currentComponent = 'ComponentA'\">显示组件A</button>\n    <button @click=\"currentComponent = 'ComponentB'\">显示组件B</button>\n  </div>\n</template>\n\n<script setup>\nimport { ref } from 'vue'\nimport ComponentA from './ComponentA.vue'\nimport ComponentB from './ComponentB.vue'\n\n// 当前显示的组件\nconst currentComponent = ref('ComponentA')\n</script>\n```\n\n`<component>` 标签的 `:is` 属性值可以是：\n\n- 被注册的组件名\n- 导入的组件对象\n- 一般的 HTML 元素\n\n当使用 `<component :is=\"...\">` 来在多个组件间作切换时，被切换掉的组件会被卸载。如果需要保留动态组件状态，使用 `<KeepAlive>` 组件即可。\n\n:::\n\n## 什么是 slot ，有什么应用场景？\n\nslot 是 Vue 中的一种用于 组件内容分发 的机制。它允许父组件向子组件插入内容，从而使组件更加灵活和可复用。\n\n::: details 参考答案\n\n在Vue中，插槽的使用方式可以分为四种：**默认插槽**、**具名插槽**、**条件插槽**和**作用域插槽**。\n\n- **默认插槽**\n\n默认插槽是最简单的插槽形式，它允许我们将组件的内容传递到组件内部的一个占位符中。\n\n子组件 `MyComponent.vue`\n\n```vue\n<template>\n  <div>\n    <p>我是子组件的标题</p>\n    <slot></slot>\n  </div>\n</template>\n```\n\n父组件\n\n```vue\n<template>\n  <MyComponent>\n    <p>这是插槽内容，由父组件传入</p>\n  </MyComponent>\n</template>\n```\n\n输出结果：\n\n```html\n<div>\n  <p>我是子组件的标题</p>\n  <p>这是插槽内容，由父组件传入</p>\n</div>\n```\n\n- **具名插槽**\n\n当子组件需要多个插槽时，可以为插槽命名，并由父组件指定内容放置到哪个插槽。\n\n子组件 `MyComponent.vue`\n\n```vue\n<template>\n  <slot name=\"header\">默认标题</slot>\n  <slot>默认内容</slot>\n  <slot name=\"footer\">默认页脚</slot>\n</template>\n```\n\n父组件\n\n```vue\n<template>\n  <MyComponent>\n    <template v-slot:header>\n      <h1>我来组成头部</h1>\n    </template>\n    <!-- 隐式的默认插槽 -->\n    <p>我来组成身体</p>\n    <template v-slot:footer>\n      <p>我来组成尾部</p>\n    </template>\n  </MyComponent>\n</template>\n```\n\n输出结果：\n\n```html\n<div>\n  <h1>我来组成头部</h1>\n  <p>我来组成身体</p>\n  <p>我来组成尾部</p>\n</div>\n```\n\n> `v-slot` 有对应的简写 `#` ，因此 `<template v-slot:header>` 可以简写为 `<template #header>` 。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。\n\n- **条件插槽**\n\n我们可以根据插槽是否存在来渲染某些内容:\n\n子组件 `MyComponent.vue`\n\n```vue\n<template>\n  <div class=\"card\">\n    <div v-if=\"$slots.header\" class=\"card-header\">\n      <slot name=\"header\" />\n    </div>\n\n    <div v-if=\"$slots.default\" class=\"card-content\">\n      <slot />\n    </div>\n\n    <div v-if=\"$slots.footer\" class=\"card-footer\">\n      <slot name=\"footer\" />\n    </div>\n  </div>\n</template>\n```\n\n- **作用域插槽**\n\n作用域插槽可以让子组件在渲染时将一部分数据提供给插槽，从而实现父组件的插槽访问到子组件的状态。\n\n子组件 `MyComponent.vue`\n\n```vue\n<template>\n  <ul>\n    <!-- 定义作用域插槽，并将 items 数据传递给父组件 -->\n    <slot :items=\"items\"></slot>\n  </ul>\n</template>\n\n<script setup>\nimport { ref } from 'vue'\n\n// 定义数据 items\nconst items = ref(['华为', '小米', '苹果'])\n</script>\n```\n\n```vue\n<template>\n  <MyComponent>\n    <!-- 使用作用域插槽，接收子组件传递的 items 数据 -->\n    <template #default=\"{ items }\">\n      <li v-for=\"(item, index) in items\" :key=\"index\">\n        {{ item }}\n      </li>\n    </template>\n  </MyComponent>\n</template>\n\n<script setup>\nimport MyComponent from './MyComponent.vue'\n</script>\n```\n\n输出结果：\n\n```html\n<ul>\n  <li>华为</li>\n  <li>小米</li>\n  <li>苹果</li>\n</ul>\n```\n\n> Vue官方API： [插槽 Slots](https://cn.vuejs.org/guide/components/slots.html#scoped-slots)\n\n::: tip 应用场景\n\n💡灵活的组件内容插入：\n插槽允许我们将内容插入组件中，而无需修改子组件内部逻辑，极大提高了组件的灵活性。\n\n💡构建通用组件：\n比如开发卡片、模态框、列表等组件，使用插槽可以轻松实现内容的自定义。模态框组件可通过插槽自定义标题、正文和按钮。\n\n💡减少重复代码：\n通过插槽，将公共逻辑封装到子组件中，而在父组件中只需插入变化的内容。\n\n:::\n\n## 🚀Vue 项目可做哪些性能优化？\n\n🔍在 Vue 项目中，我们可以利用 Vue 特有的功能和机制实现性能优化。\n\n::: details 参考答案\n\n1️⃣ **模板和指令优化**\n\n- 合理的使用 `v-if` 和 `v-show` 指令，避免不必要的渲染。\n- 使用 `v-for` 时，尽量提供唯一的 `key` ，避免重复渲染。\n- 使用 `v-once` 指令，只渲染一次，避免不必要的计算。\n- 使用 `v-memo` 指令，对使用`v-for`生成的列表进行渲染优化。`(vue3.2新增)`\n\n2️⃣ **组件优化**\n\n- 合理使用 `keep-alive` 组件，缓存组件实例，避免重复渲染。\n- 使用异步组件加载，减少首屏加载时间。\n\n```js\nconst AsyncComponent = defineAsyncComponent(() => import('./MyComponent.vue'))\n```\n\n- 配合 Vue Router 使用路由懒加载，实现路由页面按需加载。\n- 合理划分组件，提升复用性和渲染性能。\n\n3️⃣ **响应式优化**\n\n- 使用 `Object.freeze` 冻结对象，避免不必要的响应式。\n- 使用 stop 停止 不必要的watchEffect副作用执行，以减少性能消耗。\n- watch的优化\n\n  💡 避免滥用深度监听，降低性能开销。\n\n  💡 对于频繁触发的响应式数据变化，可以通过防抖和节流优化监听逻辑。\n\n```js\nimport { debounce } from 'lodash'\n\nwatch(\n  () => searchQuery,\n  debounce((newQuery) => {\n    fetchSearchResults(newQuery)\n  }, 300)\n)\n```\n\n💡 可以通过返回函数只监听具体的依赖，减少不必要的触发。\n\n```js\nwatch([() => user.name, () => user.age], ([newName, newAge]) => {\n  //...\n})\n```\n\n💡 当监听器在某些条件下不再需要时，可以通过返回的 stop 方法手动停止监听，以节省资源\n\n```js\nconst stop = watch(\n  () => data.value,\n  (newValue) => {\n    if (newValue === 'done') {\n      stop() // 停止监听\n    }\n  }\n)\n```\n\n💡 当多个监听器的回调逻辑类似时，可以合并监听\n\n```js\nwatch([() => user.name, () => user.age], ([newName, newAge]) => {\n  //...\n})\n```\n\n:::\n\n## 什么是 nextTick 如何应用它\n\n::: details 参考答案\n\n在 Vue.js 中， `nextTick` 是一个核心工具方法，用于处理 DOM 更新时机问题。它的核心作用是：**在下次 DOM 更新循环结束后执行回调，确保我们能操作到最新的 DOM 状态。**\n它的使用场景如下：\n\n- 数据变化后操作 DOM\n\n```vue\n<script setup>\nasync function increment() {\n  count.value++\n  // DOM 还未更新\n  console.log(document.getElementById('counter').textContent) // 0\n  await nextTick()\n  // DOM 此时已经更新\n  console.log(document.getElementById('counter').textContent) // 1\n}\n</script>\n\n<template>\n  <button id=\"counter\" @click=\"increment\">{{ count }}</button>\n</template>\n```\n\n- 在生命周期钩子中操作 DOM\n\n```vue\n<script setup>\nimport { ref, onMounted, nextTick } from 'vue'\n// 创建 DOM 引用\nconst element = ref(null)\n\nonMounted(() => {\n  // 直接访问可能未渲染完成\n  console.log(element.value.offsetHeight) // 0 或未定义\n  // 使用 nextTick 确保 DOM 已渲染\n  nextTick(() => {\n    console.log(element.value.offsetHeight) // 实际高度\n  })\n})\n</script>\n```\n\n注意，在vue2中和vue3的选项式 API中，我们使用this.$nextTick(callback)的方式调用。\n\n```js\nthis.$nextTick(() => {\n  console.log(this.$refs.text.innerText) // \"更新后的文本\"\n})\n```\n\n:::\n\n## 使用 Vue3 Composable 组合式函数，实现 useCount\n\n::: tip\n\n在 Vue 应用的概念中，“**组合式函数**”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。它和自定义 `React hooks` 非常相似。\n\n:::\n\n使用组合式函数实现如下需求：useCount 是一个计数逻辑管理的组合式函数，它返回一个 `count` 变量和增加、减少、重置count的方法。\n\n::: details 参考答案\n\n```vue\n<script setup>\nimport { ref } from 'vue'\n\n// 实现 useCount 组合式函数\nfunction useCount() {\n  const count = ref(0)\n  const increment = () => {\n    count.value++\n  }\n  const decrement = () => {\n    count.value--\n  }\n  const reset = () => {\n    count.value = 0\n  }\n  return {\n    count,\n    increment,\n    decrement,\n    reset,\n  }\n}\n\n// 使用 useCount 组合式函数\nconst { count, increment, decrement, reset } = useCount()\n</script>\n\n<template>\n  <div>\n    <h2>计数器: {{ count }}</h2>\n    <button @click=\"increment\">增加</button>\n    <button @click=\"decrement\">减少</button>\n    <button @click=\"reset\">重置</button>\n  </div>\n</template>\n```\n\n:::\n\n## 使用 Vue3 Composable 组合式函数，实现 useRequest\n\n```js\nconst { loading, data, error } = useRequest(url) // 可只考虑 get 请求\n```\n\n::: details 参考答案\n\n```ts\nimport { ref, computed } from 'vue';\nimport axios from 'axios';\n\n// 实现 useRequest 组合式函数\nfunction useRequest(url) {\n  const loading = ref(false); // 请求状态\n  const data = ref(null); // 响应数据\n  const error = ref(null); // 错误信息\n\n  const fetchData = async () => {\n    loading.value = true;\n    error.value = null;\n    try {\n      const response = await axios.get(url); /\n      data.value = response.data;\n    } catch (err) {\n      error.value = err.message || '请求失败'; /\n    } finally {\n      loading.value = false;\n    }\n  };\n\n  // 自动触发请求\n  fetchData();\n\n  return {\n    loading,\n    data,\n    error,\n  };\n}\n\nexport default useRequest;\n```\n\n使用\n\n```vue\n<script setup>\nimport useRequest from './useRequest'\nconst url = 'https://www.mianshipai.com/'\nconst { loading, data, error } = useRequest(url)\n</script>\n<template>\n  <div>\n    <h2>请求数据</h2>\n    <div v-if=\"loading\">加载中...</div>\n    <div v-else-if=\"error\">{{ error }}</div>\n    <div v-else>\n      <p>{{ data }}</p>\n    </div>\n  </div>\n</template>\n```\n\n:::\n\n## 自定义组件如何实现 v-model\n\n`v-model` 可以在组件上使用以实现双向绑定。\n\n::: details vue2\n在vue2中，自定义组件使用 `v-model` ，需要在组件内部定义 `value` prop，然后通过 `this.$emit('input', newValue)` 触发更新即可。\n\n```vue\n<!-- CustomInput.vue -->\n<template>\n  <input :value=\"value\" @input=\"$emit('input', $event.target.value)\" />\n</template>\n\n<script>\nexport default {\n  props: ['value'],\n}\n</script>\n```\n\n使用方式：\n\n```vue\n<CustomInput v-model=\"searchText\" />\n```\n\n:::\n\n::: details vue3\n与vue2类似，vue3自定义组件使用 `v-model` ，需要在组件内部定义 `modelValue` prop，然后通过 `emit('update:modelValue', newValue)` 触发更新\n\n```vue\n<!-- CustomInput.vue -->\n<template>\n  <input :value=\"modelValue\" @input=\"$emit('update:modelValue', $event.target.value)\" />\n</template>\n\n<script setup>\ndefineProps(['modelValue'])\ndefineEmits(['update:modelValue'])\n</script>\n```\n\n使用方式：\n\n```vue\n<CustomInput v-model=\"searchText\" />\n```\n\n---\n\n**👉注意，从 Vue 3.4 开始，官方推荐的实现方式是使用 defineModel() 宏：**\n\n```vue\n<!-- Child.vue -->\n<script setup>\nconst model = defineModel()\n\nfunction update() {\n  model.value++\n}\n</script>\n\n<template>\n  <div>父组件的 v-model 值为: {{ model }}</div>\n  <button @click=\"update\">Increment</button>\n</template>\n```\n\n父组件使用 v-model 绑定一个值：\n\n```vue\n<!-- Parent.vue -->\n<Child v-model=\"countModel\" />\n```\n\n`defineModel` 是一个便利宏，其返回的值是一个 `ref` 。它可以像其他 `ref` 一样被访问以及修改，不过它能起到在父组件和当前变量之间的双向绑定的作用：\n\n- 它的 `.value` 和父组件的 `v-model` 的值同步；\n- 当它被子组件变更了，会触发父组件绑定的值一起更新。\n  根据 `defineModel` 的特性，我们可以用 `v-model` 把这个 `ref` 绑定到一个原生 `input` 元素上：\n\n```vue\n<script setup>\nconst model = defineModel()\n</script>\n\n<template>\n  <input v-model=\"model\" />\n</template>\n```\n\n> 此外，v-model 可以接受自定义参数、添加修饰符，组件也可以绑定多个 v-model ，具体用法请参考\n> 官网文档：[组件 v-model](https://cn.vuejs.org/guide/components/v-model)\n\n:::\n\n## 如何统一监听 Vue 组件报错\n\n::: details 参考答案\n\n在 Vue 3 中，可以通过 全局错误处理器 `（errorHandler）` 和 生命周期钩子（例如 `onErrorCaptured` ）来统一监听和处理组件中的错误。\n\n- **通过全局错误处理器 `app.config.errorHandler`**\n\n```TypeScript\nimport { createApp } from 'vue';\nconst app = createApp(App);\n// 设置全局错误处理器\napp.config.errorHandler = (err, instance, info) => {\n  console.error('捕获到组件错误: ', err);\n  console.log('发生错误的组件实例: ', instance);\n  console.log('错误信息: ', info);\n};\n\napp.mount('#app');\n```\n\n- **局部错误捕获（onErrorCaptured）**\n\n`onErrorCaptured` 钩子可以捕获后代组件传递过程中的错误信息\n\n```vue\n<script setup>\nimport { onErrorCaptured } from 'vue'\n\nonErrorCaptured((err, instance, info) => {\n  console.error('局部捕获到错误: ', err)\n  console.log('错误来源组件: ', instance)\n  console.log('错误信息: ', info)\n\n  // 这个钩子可以通过返回 false 来阻止错误继续向上传递。\n  return false // 如果需要让错误冒泡到全局，省略或返回 true\n})\n</script>\n\n<template>\n  <div>\n    <h2>局部错误捕获示例</h2>\n    <ErrorProneComponent />\n  </div>\n</template>\n```\n\n> Vue官方API： [onErrorCaptured](https://cn.vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured)、[errorHandler](https://cn.vuejs.org/api/application.html#app-config-errorhandler)\n\n:::\n\n## Vuex 中 mutation 和 action 有什么区别？\n\n在 Vuex 中， `mutation` 和 `action` 是用于管理状态的两种核心概念。\n\n::: details 参考答案\n`mutation` 可以直接修改 `store` 中的 **state**值，它只支持同步操作。 `Action` 不能直接修改 **state**，而是通过调用 `mutation` 来间接修改，它用于处理异步操作。\n\n```js\nconst store = createStore({\n  state: {\n    count: 0, // 定义状态\n  },\n  mutations: {\n    // Mutation 示例（同步）\n    increment(state, payload) {\n      state.count += payload\n    },\n  },\n})\n\n// 组件中调用\nthis.$store.commit('increment', 5)\n```\n\n```js\nconst store = createStore({\n  state: {\n    count: 0, // 定义状态\n  },\n  mutations: {\n    // Mutation：同步修改状态\n    increment(state, payload) {\n      state.count += payload\n    },\n  },\n  actions: {\n    // Action：异步操作，延迟1秒后调用 mutation\n    asyncIncrement({ commit }, payload) {\n      setTimeout(() => {\n        commit('increment', payload) // 提交 mutation 修改状态\n      }, 1000)\n    },\n  },\n})\n\n// 组件中调用\nthis.$store.dispatch('asyncIncrement', 5)\n```\n\n**总结：**\n| 特性 | Mutation | Action |\n| --- | --- | --- |\n| 是否同步 | ✅ 同步 | ⏳ 异步（也可以处理同步） |\n| 是否直接修改 state | ✅ 直接修改 | ❌ 通过调用 mutation 修改 |\n| 调用方式 | `commit('mutationName')` | `dispatch('actionName')` |\n| 适用场景 | 简单的状态修改 | 异步操作（如 API 调用） |\n| 调试支持 | 完全支持，易于追踪 | 依赖于 mutation 的日志 |\n\n::: tip ⚠️ 为什么要有这样的区分？\n\n- 数据可预测性：通过强制 `Mutation` 同步修改 **State**，使得状态变更可追踪\n- 调试友好性：DevTools 可以准确捕捉每次状态快照\n- 代码组织：将同步逻辑与异步逻辑分离，提高代码可维护性\n\n:::\n\n参考文章：[VueX用法快速回顾](https://juejin.cn/post/7249033891809329212)\n\n## Vuex 和 Pinia 有什么区别？\n\n::: details 参考答案\n\n`Pinia` 和 `Vuex` 都是 Vue 的专属状态管理库，允许用户跨组件或页面共享状态。\n\n- **区别**\n\n| 特性                | **Vuex**                                | **Pinia**                                       |\n| ------------------- | --------------------------------------- | ----------------------------------------------- |\n| **版本支持**        | Vue 2 和 Vue 3                          | 仅支持 Vue 3（基于 `Composition API` ）         |\n| **API 风格**        | 基于传统的对象式 API                    | 基于 Composition API，类似于 `setup` <br/> 语法 |\n| **模块管理**        | 支持模块化（modules），但语法较复杂     | 模块化简单，**每个 store 就是一个独立模块**     |\n| **TypeScript 支持** | `TypeScript` 支持不完善，需手动定义类型 | 开箱即用的 `TypeScript` 支持，类型推导更强大    |\n| **性能**            | 更适合大型项目，但冗余代码较多          | 更加轻量，性能更好，支持按需加载                |\n| **状态持久化**      | 需要额外插件                            | 插件系统更加灵活，支持状态持久化插件            |\n\n- **代码对比**\n\n📝 **vuex**\n\n```javascript\n// store.js\nimport { createStore } from 'vuex'\n\nconst store = createStore({\n  state: {\n    count: 0,\n  },\n  mutations: {\n    increment(state) {\n      state.count++\n    },\n  },\n  actions: {\n    asyncIncrement({ commit }) {\n      setTimeout(() => {\n        commit('increment')\n      }, 1000)\n    },\n  },\n  getters: {\n    doubleCount: (state) => state.count * 2,\n  },\n})\n\nexport default store\n```\n\nvue组件中使用\n\n```vue\n<script>\nexport default {\n  // 计算属性\n  computed: {\n    count() {\n      return this.$store.state.count\n    },\n    doubleCount() {\n      return this.$store.getters.doubleCount\n    },\n  },\n  methods: {\n    // 同步增加\n    increment() {\n      this.$store.commit('increment')\n    },\n    // 异步增加\n    asyncIncrement() {\n      this.$store.dispatch('asyncIncrement')\n    },\n  },\n}\n</script>\n```\n\n📝**Pinia**\n\n```typescript\n// store.js\nimport { defineStore } from 'pinia'\n\nexport const useCounterStore = defineStore('counter', {\n  state: () => ({\n    count: 0,\n  }),\n  actions: {\n    increment() {\n      this.count++\n    },\n    async asyncIncrement() {\n      setTimeout(() => {\n        this.increment()\n      }, 1000)\n    },\n  },\n  getters: {\n    doubleCount: (state) => state.count * 2,\n  },\n})\n```\n\n组件中使用：\n\n```vue\n<script setup>\nimport { useCounterStore } from './store'\nconst counter = useCounterStore()\n</script>\n\n<template>\n  <h1>Count的计算值 {{ counter.count }}</h1>\n  <h2>Double的计算值 {{ counter.doubleCount }}</h2>\n  <button @click=\"counter.increment\">同步增加</button>\n  <button @click=\"counter.asyncIncrement\">异步增加</button>\n</template>\n```\n\n- **如何选择？**\n\n对于vue3项目，官方推荐使用pinia。因为它**更轻量、TypeScript 支持更好、模块化更简单且拥有更强的 DevTools 支持**。\n\n:::\n\n## Vue-router 导航守卫能用来做什么？\n\n::: details 参考答案\n\n`vue Router` 的**导航守卫**用于在路由跳转过程中对导航行为进行**拦截**和**控制**。这些守卫在路由进入、离开或更新时执行，可以用于多种场景，确保应用的导航逻辑符合预期。以下是常见的用途：\n\n- 认证和授权\n\n用于检查用户的登录状态或权限，防止未授权用户访问受限页面。\n\n```javascript\nrouter.beforeEach((to, from, next) => {\n  const isAuthenticated = !!localStorage.getItem('token')\n  if (to.meta.requiresAuth && !isAuthenticated) {\n    next('/login') // 未登录，跳转到登录页\n  } else {\n    next() // 已登录，正常导航\n  }\n})\n```\n\n- 数据预加载\n\n在进入路由前预加载必要的数据，确保页面渲染时数据已准备好。\n\n```javascript\nrouter.beforeEach(async (to, from, next) => {\n  if (to.name === 'userInfo') {\n    await store.dispatch('fetchUserData') // 预加载用户数据\n  }\n  next()\n})\n```\n\n- 动态修改页面标题\n\n根据路由信息动态更改浏览器标签页的标题，提升用户体验。\n\n```javascript\nrouter.afterEach((to) => {\n  document.title = to.meta.title || '自定义标题'\n})\n```\n\n- 动画和加载效果\n\n在路由切换时展示加载动画或过渡效果，提升用户体验。\n\n```javascript\nrouter.beforeEach((to, from, next) => {\n  store.commit('setLoading', true) // 开始加载动画\n  next()\n})\n\nrouter.afterEach(() => {\n  store.commit('setLoading', false) // 结束加载动画\n})\n```\n\n- 日志记录和分析\n\n在路由切换时记录用户行为，用于分析或调试。\n\n```javascript\nrouter.afterEach((to, from) => {\n  console.log(`用户从 ${from.fullPath} 跳转到 ${to.fullPath}`)\n})\n```\n\n- 防止访问不存在的页面\n\n通过守卫检查路由是否存在，避免导航到无效页面。\n\n```javascript\nrouter.beforeEach((to, from, next) => {\n  const routeExists = router.getRoutes().some((route) => route.name === to.name)\n  if (!routeExists) {\n    next('/404') // 跳转到 404 页面\n  } else {\n    next()\n  }\n})\n```\n\n关联文章：[5min带你快速回顾、学习VueRouter的使用！](https://juejin.cn/post/7359084604663840820)\n:::\n"
  },
  {
    "path": "docs/services/1v1.md",
    "content": "# 双越老师 1v1 面试咨询\n\n![](../imgs/1v1/banner.png)\n\n专业解决下面的问题：\n\n- 初入职场不知道如何写简历，如何写出内容和亮点\n- 不知道如何准备面试题，搜出很多资料，但无从下手\n- 工作几年，项目都是重复性的，写不出亮点和成绩\n- 工作快 10 年了，但还是一线开发人员，如何体现个人经验？\n- 业余不学习，基础知识很差，面试没信心\n- 工作多年只会 Vue ，不懂算法，没有技术广度和深度\n- 在一个公司呆久了，不知道该如何面试了\n- 刚毕业，没实际项目经验\n- ......\n\n## 作者介绍\n\n大家好我是[双越](https://juejin.cn/user/1714893868765373)，wangEditor 作者，前百度、滴滴资深前端工程师，PMP，慕课网金牌讲师，博客总流量 500w ，代表作品：\n\n- [wangEditor](https://www.wangeditor.com/) 开源 Web 富文本编辑器，GitHub Star 18k\n- [划水AI](https://www.huashuiai.com/) Node 全栈 AIGC 知识库，AI 智能写作，多人协同编辑\n- [前端面试派](https://www.mianshipai.com/) 系统专业的面试导航，大厂面试流程，开源免费\n\n我从 2017 年开始在慕课网讲授前端面试课程，是全网第一个做前端面试课程的讲师，至今已服务学员 5w+ 看过简历 2000+ 收到很多好评。而且，我此前一直是大厂面试官，非常清楚前端面试的流程和要求。\n\n![](../imgs/1v1/course_evaluation.png)\n\n## 缘起\n\n2023 年 5 月开始，我推出了一个简单的 1v1 技术咨询服务，可以聊任何技术话题。陆续有很多人找我咨询，但绝大部分人都是为了优化简历，发掘亮点。\n\n于是，我就改变了咨询方向，专门进行前端简历优化，帮助发掘个人、项目亮点。没想到效果非常好，他们给出了很好的评价，我也赚到了一些零花钱。\n\n![](../imgs/1v1/service_evaluation1.webp)\n\n再后来咨询的人多了，我发现大家除了简历优化之外，还有很多其他面试相关的问题。于是我就根据个人的工作、授课、咨询的经验，制定了一个**系统全面的面试准备解决方案**。可以帮大家快速全面的准备面试。\n\n![](../imgs/1v1/service_evaluation2.png)\n\n## 服务内容\n\n![](../imgs/1v1/mind-map.png)\n\n更多详细内容、价格、报名方式等，可 [加群](/docs/services/group.md) 咨询群主 `双越`\n"
  },
  {
    "path": "docs/services/group.md",
    "content": "# 加群讨论答疑\n\n加群方式 https://github.com/mianshipai/mianshipai-web/issues/1\n\nPS. 如果加群失败，可加作者 vx `fe-wfp` ，备注 `面试派`\n\n---\n\n加群可以\n\n- 讨论面试问题\n- 分享面试经历\n- 围观面试技巧\n\n加群纪律\n\n- 禁止刷屏灌水，否则踢出\n- 禁止发广告/软文，否则踢出\n- 禁止非友善话语，否则踢出\n"
  },
  {
    "path": "docs/services/job.md",
    "content": "# 内推工作\n\n## 提交你的工作机会\n\n如果你的公司有内推机会，你提交给 双越老师 或[前端面试派](https://www.mianshipai.com/)网站，我们帮你推广。你收取简历，你挣取**推荐费**（内推成功后，公司一般会给你推荐费几千元）。\n\n### 内容格式\n\n```\n\n### 标题，格式“城市，公司，职位简称，核心技术栈”\n\n- base 城市\n- 公司：名称 + 官网\n- 职位名称：如高级前端工程师（AI 方向）\n- 薪资范围（可选）\n- 核心技术栈：如 React umijs （整理几个最核心的，别写太多）\n- 学历要求：（如“本科及以上”，没有则写“不限”）\n- 工作经验要求：（如“3年以上工作经验”，没有则写“不限”）\n- 微信号：（让别人在微信中搜到你，联系你）\n- 邮箱：（你的邮箱，最好是工作邮箱，更专业，更吸引人）\n- 发布时间（可写当天）和有效期（可选）\n- **是否急聘** （不急聘则删掉这一行）\n\n备注：xxx （总结一些重要的备注和要求，套话就别写了，如没有就删掉这一行）\n\n```\n\n### 提交方式\n\n- 专业方式：给[前端面试派](https://www.mianshipai.com/)提交 PR ，[首页](https://www.mianshipai.com/)有提交方法\n- 简洁方式：联系作者 双越老师（微信 `fe-wfp`，备注 `提供内推`）把内容发给他\n\n### 注意事项\n\n- 提交的内推机会要经过自己的筛选，感觉成功率比较高，不好无脑乱提交\n- 收到简历以后，要自己 review 看是否合适，感觉合适再内推\n- 无论是否内推，都要及时给求职者回复邮件，反馈你的信息\n\n## 工作机会\n\n- 此列表按提交时间逆序排序\n- 看到合适的直接发简历到岗位邮箱，简历可要好好写～\n- 关注双越老师（微信 `fe-wfp`，备注 `看内推`）朋友圈可第一时间看到新内推\n\n（在此提交新的内推机会）\n\n### 武汉 AI 全栈开发 TypeScript\n\n- base 城市：武汉\n- 公司：[Popi.ar](https://popi.yuanzoo.cn/workstation/imag)\n- 职位名称：全栈工程师（AI 方向）/TypeScript工程师 （AI 应用与图像处理)\n- 薪资范围：20-40k\n- 核心技术栈： Electron 、TypeScript、 LangChain、Rag\n- 学历要求：本科及以上\n- 工作经验要求：不限\n- 微信号：`BigFFFFF`\n- 邮箱：`f@popi.art`\n- 发布时间2026.4.13—2026.6.31\n- **是否急聘** 是\n\n### 北京/深圳，极致上下文，AI/Agent 工程师，LLM·Agent·Python/Go\n\n- base 北京/深圳\n- 公司：极致上下文\n- 职位名称：AI/Agent 工程师\n- 薪资范围：25-50K·15薪\n- 核心技术栈：LLM 应用与 Agent 架构；RAG、Function Calling；LangChain / AutoGen / Dify；Python 或 Go\n- 学历要求：本科及以上\n- 工作经验要求：2 年以上\n- 微信号：`zkkysqs`\n- 邮箱：`jeek.zhou@apexcontext.com`\n- 发布时间：2026-04-08\n\n### 杭州 全栈 英语\n\n- base 杭州\n- 公司：一家专注于睡眠解决方案与家居产品的外资品牌公司\n- 职位名称：全栈开发（电商项目 英语）\n- 薪资范围：月base20-30K，13薪\n- 核心技术栈：前端React /vue，后端用至少一门服务端语言（PHP/Node.js/Python/Java/Go 等）搭建服务\n- 邮箱：（15905167024@163.com，微信同号）\n- 发布时间：2025/04/01\n- **是否急聘**\n\nJD 岗位描述：\n岗位职责:\n主导海外电商平台的系统架构设计、核心模块与主干业务编码，要求有电商业务经验，负责技术栈落地与工程规范。TO C业务开发经验丰富\n\n英语可作为工作语言，要求：\n\n1. 男 ，35岁以内，英语作为工作语言，有留学背景的优先推荐。（二面是全英面试）\n2. 有对接海外 SaaS 生态（Shopify、GA、Ads、Zendesk 等），完成系统集成的工作经验\n\n### 阿里 前端实习生\n\n- 公司：阿里巴巴（多bu在招）\n- 职位名称：前端开发实习生（27届）\n- 核心技术栈：React/Vue，以具体部门要求为准\n- 学历要求：本科及以上\n- 工作经验要求：不限\n- 微信号：`Lcccccc822`\n- 邮箱：`luanhuilin.lhl@alibaba-inc.com`\n- 发布时间：3.18\n- 有效期：3.18-4.18\n\n### 深圳 店小秘 前端 Vue\n\n- base 深圳\n- 公司：店小秘 https://www.dianxiaomi.com/\n- 职位名称：高级前端工程师\n- 薪资范围：20-30K \\* 13\n- 核心技术栈：Vue2/3\n- 学历要求：本科及以上学历\n- 工作经验要求：3年以上工作经验\n- 邮箱：shixin@dianxiaomi.com\n- 发布时间: 2026-03-10\n- 是否急聘：是\n- 备注：跨境Saas独角兽，HC多，面试流程快，有flutter商业项目加分\n\n### 上海/深圳 外企 高级前端工程师 React\n\n- base 上海/深圳\n- 公司：Traveloka 外企 OTA独角兽 https://careers.traveloka.com/jobs/cab40adb-3727-439f-b0d1-32bbe19c23b5\n- 职位名称：高级前端工程师\n- 薪资范围：对标大厂\n- 核心技术栈：React nextjs nodejs\n- 学历要求：本科及以上 知名高校加分\n- 工作经验要求：3-5年\n- 微信号：`luoqian48933`\n- 邮箱：`qian.luo@traveloka.com`\n- 发布时间: 2026-03-02\n- 语言要求：英语可作为工作语言， 能基础的口语表达\n- 备注：15天起超长年假，WLB 朝九晚六\n\n### 北京 前端 AI Leader\n\n- base 北京\n- 公司：AGI 独角兽\n- 职位名称：前端leader（AI 方向）\n- 薪资范围：对标P8-P9\n- 侧重：前端/全栈，有AI Agent/LLM应用层前端开发经验，深度使用 Coding Agent 工具，有知名开源项目维护/核心贡献\n- 学历要求：本科及以上\n- 工作经验要求：不限，看技术能力和技术规划\n- 微信号：LuckyConsultant369\n- 发布时间: 2026.2.24\n\n### 北京 前端 Vue\n\n- base 北京\n- 公司：北京圜晖科技有限公司 [官网](https://www.featuremaker.com/)\n- 职位名称：高级前端工程师\n- 薪资范围：20k-30k\n- 核心技术栈：vue/Slate\n- 学历要求：本科及以上\n- 工作经验要求：3年以上\n- 微信号：`hafanyao_`\n- 邮箱：hafanyao@126.com\n- 发布时间：2026-02-04\n\n### 北京 京东 前端工程师 金融风控 3年及以上经验 框架不限\n\n- base 城市\n- 公司：京东 www.jd.com\n- 职位名称：高级前端工程师（金融风控方向）\n- 薪资范围 20-40k\n- 核心技术栈：React Vue Webpack nodeJS\n- 学历要求：不限\n- 工作经验要求：3年以上工作经验\n- 微信号：17276467721\n- 邮箱：guanhaobin.1@jd.com\n- 发布时间 2026-01-07\n\n### 北京 京东 高级前端工程师 AI大模型方向 5年及以上经验 框架不限\n\n- base 北京\n- 公司：京东 www.jd.com\n- 职位名称：高级前端工程师（AI大模型方向）\n- 薪资范围 20-50k\n- 核心技术栈：React Vue Webpack nodeJS\n- 学历要求：不限\n- 工作经验要求：3年以上工作经验\n- 微信号：17276467721\n- 邮箱：guanhaobin.1@jd.com\n- 发布时间 2026-01-07\n  备注：精通Nodejs开发，有VSCode插件、VSCode/Electron/Theia等开发经验者优先\n\n### 京东 前端工程师 3年及以上经验 框架不限\n\n- base 北京 上海 深圳均有岗位\n- 公司：京东 www.jd.com\n- 职位名称：前端工程师\n- 薪资范围 20-50k\n- 核心技术栈：React Vue Webpack nodeJS\n- 学历要求：不限\n- 工作经验要求：3年以上工作经验\n- 微信号：17276467721\n- 邮箱：guanhaobin.1@jd.com\n- 发布时间 2026-01-07\n\n### 北京、武汉，大前端，NodeJS/AI\n\n- base 北京、武汉\n- 公司：奇安信网神\n- 职位名称：高级前端工程师（NodeJS/AI 方向）\n- 核心技术栈：Vue3、TS、Vite、NestJS、LangChain\n- 学历要求：本科及以上\n- 工作经验要求：4年以上工作经验\n- 微信号：15549446040\n- 邮箱：shuiqing1991@gmail.com\n- 发布时间：2026/01/04\n\n### 上海，上海华测导航，高级安卓开发工程师，Java\\Kotlin\n\n- base 上海\n- 上海华测导航:https://www.huace.cn/\n- 职位名称：高级安卓开发工程师\n- 核心技术栈：Android、Java、Kotlin\n- 学历要求：本科及以上\n- 工作经验要求：7年以上Android程序开发经验\n- 微信号：Xutaotao1108\n- 邮箱：taotao_xu@huace.cn\n- 发布时间: 2025.11.13\n\n工作内容：\n\n1、主导产品开发和技术开发项目的需求分析、技术可行性分析及详细设计\n2、负责产品子业务系统、模块开发，保证交付成果的进度和质量\n3、负责产品维护和技术支撑，保障用户问题及时解决\n4、完成领导安排的工作任务以及带教新人\n\n任职资格\n\n1、本科及以上学历，计算机、测绘、GIS专业优先,7年以上Android程序开发经验；\n2、掌握常用的设计模式和Android开发框架；熟悉MVC、MVP、MVVM架构；\n3、熟悉串口通信,CAN通信,Socket通信和使用\n4、熟悉java语言开发,kotlin语言开发,加分：熟悉全站仪知识、GNSS知识，LBS、GIS、地图类APP开发\n\n### 武汉 前端专家 React\n\n- base 武汉\n- 公司：创业公司，创始人是前腾讯美国公司总经理，公司总部在美国，武汉是国内的base地\n- 职位名称：前端专家\n- 薪资范围：年薪40-60w+期权\n- 核心技术栈：React\n- 邮箱：（15905167024@163.com，微信同号）\n- 发布时间：2025/11/10\n- **是否急聘**\n\nJD 岗位描述：\n\n- 领导与架构师：负责定义并持续优化Pear前端应用的架构设计，确保平台在扩展过程中\n  具备良好的可扩展性、模块化特性和可维护性。\n- 提升质量与可靠性：制定测试、可访问性、代码审查和部署卫生的工程标准。推动减\n  少前端相关生产问题。\n- 自主前端卓越：构建并扩展共享UI系统、组件库和框架，确保商店、事件和内部工具\n  之间的一致用户体验。\n- 导师与人才发展：通过代码审查、结对编程和结构化反馈指导并培养前端团队。识别每\n  位工程师的优势、不足和成长路径。\n- 跨部门协作：与产品、设计和后端团队合作，将业务目标转化为可实现、高效和可持续\n  的技术策略。\n\n### 北京 前端 React RN\n\n- base 北京\n- 公司：AI 独角兽企业，智谱 AI\n- 职位：前端工程师，也要实习生\n- 核心技术栈：React，React native\n- 邮箱：h532408092@163.com\n- 发布时间：2025.11\n\nJD 岗位描述：\n\n1、参与公司核心 AI 多模态产品的前端开发工作，保障系统的稳定性与扩展性；\n2、与产品经理、UI/UX 设计师协作，高质量完成页面重构与交互实现；\n3、推动前端工程化建设，提升构建效率和开发体验；\n4、持续优化前端性能，提升用户加载速度与交互体验；\n5、关注前沿技术发展，推动团队技术氛围与能力成长；\n6、有小程序、跨端框架（如 React Native）经验者优先考虑；\n\n任职要求：\n\n1、计算机相关专业本科及以上学历，2 年以上前端开发经验；\n2、精通 H5相关技术，熟悉 ES6+ 规范；\n3、熟悉主流前端框架 Vue / React / Angular 至少一种；\n4、熟悉 性能优化手段，了解浏览器渲染机制；\n5、熟悉 RESTful API、HTTP 协议及前后端协作流程；\n6、具备良好的沟通能力和团队协作意识，责任心强，有自我驱动力；\n\n加分项：\n\n1、有大型 TOC 类项目（电商、社交、直播、短视频等）开发经验；\n2、有用户体验敏感度，关注细节和交互体验。\n3、有开源社区贡献经历\n\n### 杭州，饿了么&淘宝闪购，前端开发专家，React\\React Native\n\n- base 杭州\n- 公司：饿了么&淘宝闪购\n- 职位名称：前端开发专家\n- 核心技术栈：React\\React Native\n- 学历要求：本科及以上\n- 工作经验要求：2年以上工作经验\n- 邮箱：liaowenrun.lwr@alibaba-inc.com\n- 2025年10月30日\n\n内推链接：https://talent.ele.me/off-campus/position-detail?lang=zh&positionId=5000038124&shareCode=C5x5PU4moQdLChJ%2F97eIRA%3D%3D\n\n### 上海/杭州/成都 蚂蚁集团 前端\n\n- base 上海、杭州、成都\n- 公司：蚂蚁集团\n- 职位名称：前端开发 (AI 技术产品方向)-支付宝\n- 薪资范围：面试聊\n- 核心技术栈：React，AI Agent\n- 学历要求：本科及以上\n- 工作经验要求：3 年以上工作经验\n- 微信号：Careteen\n- 邮箱：wangketing.wkt@antgroup.com\n- 发布时间：2025 年 10 月\n- **是否急聘** 是\n\n备注：\n职位描述 1.负责蚂蚁 Al Coding 产品或体验产品的全栈研发工作 2.Al Coding 产品方向:业界最新一代的前端搭建 IDE 、基于大模型的 U!生成、跨栈跨端的演染技术、一句话生成网页的在线 AI 平台、极致性能的前端工程 3.体验产品方向:负责智能 UI、体验巡检、数据可视化、体验分析 Copilot 等技术基建 4.探索 AI 时代前端工程师的领域上限\n\n职位要求 1.精通各种前端技术,包括 HTML5/CSS/JavaScript/Node.js 等。我们招聘前端工程师以及专业 Node 工程师 2.聪明、有好奇心、有求知欲、不喜欢重复的工作、不给自己打「前端」的标签，我们推类产品工程师角色 3.有 Al Coding 产品、低代码平台、DevOps、大流量 Node 应用等经验可作为加分项;有优质的技术组件产出或开源产品者优先。4.具备良好的沟通和团队协作能力，具备提升团队研发效率和性能的能力，具备用户体验的交互优化能力，具备充分的产品负责人意识和项目把控能力。\n\n### 【米哈游】协同文档领域前端工程师\n\n- base 上海\n- 公司：米哈游\n- 职位名称：如高级前端工程师（协同文档方向）\n- 薪资范围：面试聊\n- 核心技术栈：React、多人协同、编辑器\n- 学历要求：本科及以上\n- 工作经验要求：3年以上工作经验或者优秀者不限\n- 微信号：shiang_zhang\n- 邮箱：81118728@qq.com\n- 发布时间 2025 年 10 月\n- **是否急聘** 是\n\n备注：多人协同编辑领域前端复杂度够，技术成长天花板高，欢迎来挑战，前后端都有 HC\n\n### 成都 央企 前端 React\n\n- base 成都\n- 公司：某央企，稳定不卷，早九晚五，假期超长\n- 职位名称：前端工程师\n- 薪资范围：年薪18w\n- 核心技术栈：React\n- 邮箱：（15905167024@163.com，微信同号）\n- 发布时间：2025/10/11\n\n岗位职责:\n负责前端展示系统、可视化大屏等项目模块、组件的设计和实现;1、2.负责前端网页的设计、开发和为主，指导、协助团队完成前端UI视觉效果，并与后端进行接口对接;3、负责前端项目的兼容性优化、性能优化、体验优化、攻克技术难题\n遵循并优化产品前端代码标准及规范;5、精通浏览器的渲染机制以及J8的性能瓶颈，对于前端系统的性能有评测手段和优化技能;6、研究前沿的前端技术和解决方案，跟踪前端技术和行业发展趋势\n要求：统招211硕士起步，毕业2年及以上\n\n### 深圳 前端\n\n- base 深圳\n- 公司 富途\n- 职位 前端开发工程师\n- 薪资范围 15-30k\n- 核心技术栈：React、Vue\n- 学历要求：本科及以上学历\n- 工作经验要求：1-3年工作经验\n- 邮箱：shawnwang@futunn.com\n- 发布时间：2025-10-10\n- 备注：腾讯系(腾讯战略投资)\n\n职位诱惑：\n\n1. 行业内有竞争力的薪酬福利和办公环境；标配 Macbook Pro，前端开发必备利器；\n2. 有挑战的工作，有人性的工作量；\n3. 开放的技术心态，有一定自由度的技术尝试空间；\n4. 公平、开放的团队氛围；\n\n岗位职责:\n\n1. 负责公司研发效能平台的前端开发工作，包括但不限于：性能测试平台，容量管理平台，监控平台等。\n2. 负责设计平台的前端系统架构，不断提升开发效率及系统性能和稳定性。\n3. 持续的优化前端体验和页面响应速度，提升web界面的友好性和易用性；\n\n任职要求：\n\n1. 两年以上web前端开发经验，能独立编写复杂web前端应用\n2. 对计算机基础、数据结构和算法有一定了解；\n3. 能够熟练运用HTML、CSS、JavaScript构建高性能的web应用程序；\n4. 熟悉常见前端类库/框架，如Vue/React。\n5. 思路清晰，具备良好的沟通能力和团队协作精神。\n\n### 广州 乐趣无限 前端 Vue\n\n- base 广州\n- 公司 乐趣无限\n- 职位 前端开发工程师\n- 薪资范围 15-25k\n- 核心技术栈：Vue\n- 学历要求：专科及以上学历\n- 工作经验要求：3年以上工作经验\n- 邮箱：lishizhe@spreadfun.com\n- 发布时间：2025-09-23 有效期：长期\n- 备注：海外社交产品，千万级用户，HC 多，福利好\n\n### 深圳 店小秘 前端 Vue\n\n- base 深圳\n- 公司：店小秘 https://www.dianxiaomi.com/\n- 职位名称：高级前端工程师\n- 薪资范围：18-30K \\* 13\n- 核心技术栈：Vue, ElementPlus\n- 学历要求：专科及以上学历\n- 工作经验要求：3年以上工作经验\n- 邮箱：zhangteng@dianxiaomi.com\n- 发布时间: 2025-08-19\n- 备注：跨境Saas独角兽，HC多，面试流程快\n\n### 南京 京东五星电器 前端 React / 后端 Java\n\n- base 南京\n- 公司：京东五星电器\n- 职位名称：前端开发工程师、后端开发工程师\n- 薪资范围：15-23K \\* 15\n- 前端核心技术栈：React、Vue\n- 后端核心技术栈：Spring、Spring MVC、Spring Data JPA、Spring Boot、Spring Cloud\n- 邮箱：chenxin559@jd.com\n- 发布时间：2025-08-15 有效期：2025-10-31\n\nJD 岗位描述：\n\n前端岗位职责\n\n1.负责与产品经理、后端工程师紧密协作，按照公司开发规范，进行桌面端及移动端各类应用的前端开发；\n\n2.参与公司各项目中的前端功能设计及改进；\n\n3.与设计人员配合，实现高保真设计中的视觉效果；\n\n4.负责前端应用的测试、部署、监控、运维，及持续调优。\n\n前端任职资格\n\n1.本科以上学历，计算机相关专业；\n\n2.三年以上前端开发经验；\n\n3.熟练掌握HTML/CSS/JS（ES6+）：三年以上React或Vue技术栈的使用经验；\n\n4.熟悉iOS或安卓原生开发的优先，有零售/电商相关行业经验优先。\n\n后端岗位职责\n\n1.参与项目的需求分析、概要设计、详细设计，技术文档的编写；\n\n2.按照公司开发规范，负责相应软件系统的功能开发及测试；\n\n3.负责软件系统的部署、线上运维，服务器监控，管理，调优等；\n\n4.负责技术架构的持续完善和改进。\n\n后端任职资格\n\n1.本科以上学历，计算机相关专业；\n\n2.三年以上Java开发经历；\n\n3.精通Java及Spring，对Spring MVC、Spring Data JPA了解其原理和机制，熟悉Spring Boot、Spring Cloud框架；\n\n4.精通SQL，熟练使用MySQL、Oracle等数据库，并有一定的SQL优化能力，熟悉Redis等常用NoSQL解决方案，了解各自的优缺点以及使用场景；\n\n5.熟悉Linux操作系统，熟悉Tomcat等应用服务器的部署和配置。\n\n### 北京/杭州 阿里/高德 前端/后端/算法\n\n- base 北京、杭州\n- 公司： 阿里巴巴-高德地图\n- 职位名称： 高级前端工程师、高级后端工程师、高级算法等\n- 核心技术栈： React、Vue\n- 学历要求： 本科及以上\n- 工作经验： 2 年以上\n- 微信号： i3983761\n- 邮箱： likangning@live.com\n- 发布时间 2025-09-10 长期\n\n备注：HC 很多， 面试流程快。\n\n### 广州 集换社 前端\n\n- base 广州\n- 公司：集换社\n- 职位名称：高级前端工程师\n- 薪资范围：18-30k 14薪\n- 核心技术栈：React/Next.js\n- 学历要求：本科及以上\n- 工作经验要求：3年以上工作经验\n- 微信号：heinzz\n- 邮箱：hcxw_2016@163.com\n- 发布时间：2025.09.15\n\n备注：上班时间12点到18点，6小时工作制，真wlb\n\n### 上海 众安财产保险 前端\n\n- base 上海\n- 公司：众安在线财产保险股份有限公司\n- 职位名称：前端开发\n- 薪资范围： 20-30k \\* 15薪\n- 核心技术栈：如 React 、node\n- 学历要求：本科及以上\n- 工作经验要求：3年 - 5年\n- 微信号：Big-Zh_97\n- 邮箱：zhouhang@zhongan.com\n- 发布时间: 2025.09.15\n- 内推码：NTA4N8s\n- 内推官网： https://app.mokahr.com/su/xegsig\n\n### 东莞 全栈 前端\n\n- 广东省东莞市\n- 广东一一五科技股份有限公司 https://job.115.com/5/jobdetail#广东一一五科技股份有限公司-招聘首页\n- 职位名称：高级前端工程师（Windows/Web/鸿蒙+AI等）\n- 核心技术栈：c++/c#/Java\n- 薪资范围：年薪30万元+\n- 学历要求：本科及以上\n- 工作经验要求：无特殊要求\n- 微信号：18507539115\n- 邮箱：jianli@115.com\n- 发布时间 2025.09.15\n\n### 上海 阿里云 前端技术专家\n\n- base 上海\n- 公司：阿里云\n- 职位名称：前端专家岗位\n- 核心技术栈：React umijs NextJS webpack/vite， NodeJS\n- 学历要求：本科及以上\n- 工作经验要求：5年及以上工作经验\n- 微信号：yichanya1\n- 邮箱：2589768261@qq.com\n- 发布时间：2025.9.11\n\n具有大型系统架构设计，有 AI agent 项目架构经验优先，有 vscode 或谷歌插件开发经验加分。\n\n### 北京/上海 瓴岳科技 技术岗\n\n- 北京/上海 瓴岳科技\n- 内推投递链接：https://app.mokahr.com/su/18G8y2\n- 官网投递内推码：NTAgIwh\n- 职位名称： 技术类岗位（Java / Web / AI / 大数据 / 测试 / 网络安全 / SRE 等）、产品、风控等等\n- 学历要求：本科及以上\n- 工作经验要求：校招/社招\n- 微信号：wxid_rttjvta43v3e22\n- 发布时间：2025.9.9\n\n### 上海 拼多多 前端、后端、算法、客户端\n\n- base 上海\n- 公司：拼多多\n- 职位名称：前端、后端、算法、客户端\n- 学历要求：本科及以上\n- 工作经验要求：不限\n- 邮箱：a2944938071@163.com\n- 微信： a2944938071\n- 发布时间：2025/09/04\n\n备注：【拼多多集团-PDD校园招聘】内推链接：https://careers.pddglobalhr.com/campus/grad?t=wmllKZBPFx，内推码：wmllKZBPFx。期待你的加入！我们一起，无拼不青春！（通过此链接投递计入内推，内推简历优先筛选~）可以联系邮箱帮忙看进度\n\n### 南京 前端 Vue\n\n- base 南京\n- 公司：南数集团\n- 职位名称：前端工程师\n- 核心技术栈：Vue3 TS\n- 学历要求：本科\n- 工作经验：3年以上\n- 邮箱：838247132@qq.com\n- 微信：18241518968\n- 发布时间：2025/8/29\n\n### 深圳 物业系统创业公司 前端 Vue uniapp\n\n- base 深圳\n- 公司：深圳物业系统创业公司\n- 职位名称：前端工程师\n- 核心技术栈：vue3 uniapp\n- 微信号：CccccxyHide\n- 邮箱：1369276645@qq.com\n- 有效期：2025年9月16号截至\n- 薪资面议，需到场面试，不接受线上面试\n- **急聘**\n\nJD 岗位描述：\n\n1. 要求能熟练使用AI进行开发\n2. 具备2年-3年前端开发经验，熟练掌握HTML、JS、CSS等基础知识，能熟练掌握vue3相关语法和uniapp，并有实际项目研发经验\n3. 具备较强的分析和解决问题的能力，良好的沟通能力\n\n备注：另招聘市场部经理（女）\n\n### 深圳 物业系统创业公司 后端\n\n- base 深圳\n- 公司：深圳物业系统创业公司\n- 职位名称：IoT 物联网工程师\n- 核心技术栈：Java C C++\n- 微信号：CccccxyHide\n- 邮箱：1369276645@qq.com\n- 有效期：2025年9月16号截至\n- 薪资范围：15K-20K\n- 需到场面试，不接受线上面试\n- **急聘**\n\nJD 岗位描述：\n\n1. 计算机相关行业\n2. 2年以上物联网/IOT行业经验, 有一定的C, C++语言能力\n3. 与硬件团队紧密合作，参与部分现场硬件设备的安装指导\n\n### 北京 AI教育创业团队 前端/Android/后端\n\n- base 北京\n- 公司：AI教育创业公司\n- 职位名称：前端工程师\n- 核心技术栈：React\n- 邮箱：hr@xiaoluxue.com\n- 微信：13146669585\n\n备注:\n\n- 团队核心成员来自字节跳动、阿里巴巴等头部公司\n- 熟悉 AI 开发的优先\n- 同时招聘 Android 和 Golang 后端开发人员\n\n### 长沙/北京/重庆 前端 蚂蚁金服外包\n\n- base 暂时: 长沙，北京，重庆 （上海后期可能会有）\n- 公司：博彦科技外包蚂蚁金服\n- 职位名称：前端工程师\n- 薪资范围：看地区\n- 核心技术栈：如 React umijs ，移动端\n- 邮箱：929877449@qq.com\n- 微信：JavaScript\\_\\_\\_\\_c\n- **急聘**\n\n### 上海/深圳 React 急聘\n\n- base 上海/深圳\n- 公司：某3D打印巨头 https://makerworld.com.cn/zh\n- 职位名称：高级前端工程师（to c业务方向）\n- 薪资范围：base40-70k，15-16薪，另有丰厚奖金\n- 核心技术栈：React\n- 微信：L\\_\\_220321\n- 邮箱：（15905167024@163.com）\n- 发布时间：2025/8/26\n- **急聘**\n\nJD 岗位描述：\n\nhttps://m.zhipin.com/mpa/html/weijd/weijd-job/2475116958c0989503N709-9E1BX?date8=20250826&sid=qr_self_jd&openWeapp=1\n\n要求：统招211/985本科起步，一线互联网大厂北京，加分项：热情，野心勃勃，富有创业精神\n\n### 杭州 恒生电子 前端\n\n- base 杭州\n- 公司：恒生电子 https://www.hundsun.com/\n- 职位名称：前端工程师 2人，技术专家 1 人\n- 核心技术栈：Vue 移动端 React Nodejs\n- 邮箱：caohq33221@hundsun.com\n- 发布时间：2025.08.22\n\n备注：\n\n- 前端工程师需要 Vue 移动端\n- 技术专家需要 Vue React Nodejs\n\n### 杭州 前端\n\n- base：杭州(前期需驻场 3-4 个月临汾)\n- 公司：杭州尚情数据技术有限公司\n- 职位名称：前端工程师\n- 薪资范围：面议（根据能力与经验，支持股权/期权激励）\n- 核心技术栈：React、Vue\n- 邮箱：sky_zhang_jobs@163.com\n- 发布时间：2025-08-21\n- 是否急聘: 是\n\n### 北京 瓴岳科技 前端\n\n- base 北京\n- 公司：[瓴岳科技](https://fintopia.tech/)\n- 职位名称：前端工程师\n- 核心技术栈：如 React Vue\n- 邮箱：wry70948@gmail.com\n- 发布时间：2025-08-21 有效期：2025-10-31\n\nJD 岗位描述：\n\n- 计算机相关专业，2年以上互联网Web开发经验；能用自身的经历阐述，技术是如何赋能业务的；\n- 熟练掌握JS/CSS、浏览器原理、HTTP协议、网络安全等前端必要的基础知识；\n- 精通React或Vue技术原理，对前端工程化与模块化开发有深入理解；\n- 对前端稳定性建设、前端性能优化、前端工程效率有一定的理解和相关实践；\n- 具备结果导向思维、强大自驱力、追求卓越精神，具有良好的沟通能力和团队协作精神；\n- 有团队管理经验者优先，能带领技术小组不断提升业务理解，进行技术规划，执行关键技术决策，输出高质量的业务代码。\n\n加分项：\n\n- 985、211本科及以上学历优先；\n- 有前端微服务、渐进式重构、B端组件库搭建、低代码平台落地经验；\n- 金融科技、数字信贷、出海业务从业经验；\n- 对AI在前端工作流中的落地有见解、有实践。\n\nPS：也有其他技术职位，Java后端、测试、运维、数据仓库工程师等，详情见链接：[https://app.mokahr.com/su/18AJPt](https://app.mokahr.com/su/18AJPt)\n\n### 杭州 IOS\n\n- base 杭州\n- 公司：杭州铭师堂\n- 职位名称：iOS开发工程师\n- 核心技术栈：Objective-C 、Swift 、Flutter\n- 邮箱：tangbei@mistong.com\n- **是否急聘**：是\n\nJD 岗位描述：https://app.mokahr.com/su/ux33E\n\n还有几个其他技术类的岗位招聘：https://app.mokahr.com/su/6lO2o\n\n### 上海 科大讯飞 前端 Vue React\n\n- base 上海\n- 公司：科大讯飞\n- 职位名称：前端开发工程师（司法）\n- 核心技术栈：Vue、 React\n- 薪资范围： 根据职级定级情况，30K左右\n- 邮箱：lccong@iflytek.com\n- 发布时间 2025-08-13\n\nJD 岗位描述：\n\n1、负责项目Web前端核心模块的代码开发，主导复杂功能的技术实现，性能优化等核心问题。\n2、主导项目项目Web前端关键模块详细设计，及应用系统架构中涉及web前端部分设计，\n3、主导项目前端技术选型、框架搭建，制定跨端（Web/H5/小程序/App）解决方案。\n3、负责协同技术平台，推动前端工程化建设，包括脚手架、构建工具链、组件库等，提升团队开发效率。\n4、主导制定并推广前端开发规范（代码风格、组件化标准）、文档体系，定期进行技术评审与代码审查。\n4、接受短期驻场支撑项目建设，负责快速定位并解决客户现场技术问题的能力，保障项目顺利实施。\n任职要求：\n1、计算机相关专业本科以上学历；\n2、5年以上大前端或前端+nodejs开发经验,主导过至少两个大型项目。\n3、精通HTML5/CSS3/ES6+，深入理解浏览器渲染机制、网络协议（HTTP/WebSocket）及性能优化手段。\n4、至少熟练掌握React/Vue/Angular中一种框架及其生态（如Vuex/Redux），具备大型项目前端架构设计经验。\n5、熟悉Webpack/Vite等构建工具配置优化，具备模块化拆包、Tree Shaking等工程化实践经验。\n6、具备跨端开发能力（小程序/Hybrid App/Electron），熟悉移动端适配与性能调优。\n7、优秀的沟通与推动能力，能协调多方资源达成技术目标，适应短期出差支持项目。\n8、加分项：熟悉微前端架构（qiankun/Module Federation）或具备低代码平台开发经验。\n9、加分项：有后端开发经验（Java/Python），理解分布式系统设计原理。\n10、加分项：对AI工程化、大数据可视化等前沿领域有实践经验。\n\nPS. 后端 Java 和测试也有岗位，base 合肥/武汉\n\n### 南京 锐捷网络 前端 Vue React\n\n- base 南京\n- 公司：锐捷网络\n- 职位：高级/资深前端工程师\n- 核心技术栈：Vue React\n- 邮箱：qiyubu@ruijie.com.cn\n- 发布时间：2025.08.11\n\n### 上海 鸣鸣很忙 前端 React Vue\n\n- base 上海\n- 公司：鸣鸣很忙 https://www.hnlshm.com/\n- 职位：前端开发工程师\n- 核心技术栈：React Vue\n- 邮箱：17621713119@163.com\n- 发布时间：2025.08.07\n\nJD 岗位描述\n\n1、开发 CDP 数据可视化看板（用户分群分析、营销效果监测）；\n2、实现低代码营销活动页面配置系统（拖拽生成 H5/邮件模板）；\n3、对接埋点数据校验工具及行为事件可视化回放；\n4、优化前端性能（大数据量渲染、实时数据更新）。\n任职资格:\n1、五年以上 React/Vue 经验，熟练使用 ECharts/D3.js；\n2、有 CDP/BI 工具前端开发经验，熟悉 WebSocket 实时通信；\n3、掌握前端监控体系（Sentry/埋点 SDK 集成）；\n4、了解 Google Analytics/Adobe Analytics API 对接。\n\n### 广州 鸣鸣很忙 前端 React\n\n- base 广州\n- 公司：鸣鸣很忙 https://www.hnlshm.com/\n- 职位：高级前端开发工程师（新零售 O2O 方向）\n- 薪资范围：20K - 30K\n- 核心技术栈：React\n- 邮箱：17621713119@163.com\n- 发布时间：2025.08.07\n\n### 上海 前端 React RN\n\n- base: 上海（杨浦区创智天地）\n- 公司：上海即果信息技术有限公司\n- 职位：小宇宙前端开发工程师，前端开发工程师（AI 项目），AI插件全栈工程师（偏前端）\n- 核心技术栈：React，RN,flutter,node.js\n- 邮箱 ： zhanghao@iftech.io\n- 发布时间：2025.08.07\n- JD 岗位描述：\n  职位描述\n  计算机或相关专业毕业，2 年以上前端开发工作经验\n  熟练掌握HTML、JS/TS、CSS 等基础知识\n  掌握 React 及相关技术栈，并有实际项目研发经验\n  负责 Chrome 插件和网页的业务前端开发及优化迭代职位要求\n  具备较强的分析和解决问题的能力，良好的沟通能力\n  有良好的编程习惯，对代码交付质量有追求，有持续优化、勇于重构和积极探究的热情\n  有从 0 到 1 的前端工程实施经验，有从技术侧主动优化策略、规避风险、发现和解决问题、总结经验的意识\n  对前端技术有持续的学习热情\n  加分项\n  具有开源精神，关注、参与过开源项目\n  了解大语言模型应用开发，尝试过 Prompt 编写和模型调用。有独立完成 AI 小项目更好\n  具有全栈技术知识储备\n  具有大局观，能够从全局的角度出发，积极提出建议或想法，只为做对的事\n  足够理性，且兼具共情力\n  具有良好的产品品味，热爱观察、思考各类产品，对产品有自己的见解\n\n### 深圳 转转 前端实习生\n\n- base 深圳\n- 公司：转转 https://www.zhuanzhuan.com/\n- 职位：前端实习生\n- 发布时间：2025.08.07\n\nJD 岗位描述，投递简历：https://neitui.italent.cn/zhuanzhuan/sharejobs/detail?shareId=8d46ba55-85b9-4377-8212-1c749772ba67&language=zh_CN\n\n### 长沙 鸣鸣很忙 前端 React Vue\n\n- base 长沙\n- 公司：鸣鸣很忙 https://www.hnlshm.com/\n- 职位：前端工程师\n- 核心技术栈：React Vue\n- 邮箱：17621713119@163.com\n- 发布时间：2025.08.07\n\n### 北京/上海 百度 前端 React Vue\n\n- base 北京/上海\n- 公司：百度 https://www.baidu.com/\n- 职位：前端工程师\n- 核心技术栈：React Vue\n- 邮箱：zhangbo65@baidu.com\n- 发布时间：2025.08.07\n\nJD 岗位描述：\n\n- 参与大数据产品需求调研，理解业务需求，提供前端技术解决方案\n- 负责Web前端系统的架构设计、开发、测试与维护，确保代码质量和性能\n- 推动团队前端技术标准化，提升前端开发效率与产品质量\n- 与产品经理、UI/UE设计师、后端工程师紧密合作，共同推进项目进展，解决技术难题\n\n岗位要求：\n\n- 计算机及相关专业本科及以上学历，具备足够扎实的前端基础\n- 熟悉 HTML5、CSS3、JavaScript、Typescript，熟悉前端工程化和模块化开发\n- 熟悉 React、Vue 或 Angular 中至少一种主流前端框架，具备实际项目开发经验\n- 良好的沟通能力和团队协作精神，具有独立分析问题解决问题的能力\n- 加分项：在开源社区或技术论坛有贡献者优先；熟悉Linux系统，有一定的后端开发经验（如 Java、Go、Python 等）者优先；有流程可视化、流程编排工具开发经验者优先\n\n备注：另招服务端工程师和算法工程师，可联系上面的邮箱\n\n### 上海 即刻 前端 React\n\n- base 上海\n- 公司：即刻 App https://iftech.io/\n- 职位：前端开发工程师（AI 项目）\n- 核心技术栈：React\n- 邮箱：zhanghao@iftech.io\n- 发布时间：2025.08.07\n\n### 北京 前端 React RN\n\n- base 北京\n- 公司：AI 独角兽企业\n- 职位：前端工程师，也要实习生\n- 核心技术栈：React，React native\n- 邮箱：h532408092@163.com\n- 发布时间：2025.08.07\n\nJD 岗位描述：\n\n1、参与公司核心 AI 多模态产品的前端开发工作，保障系统的稳定性与扩展性；\n2、与产品经理、UI/UX 设计师协作，高质量完成页面重构与交互实现；\n3、推动前端工程化建设，提升构建效率和开发体验；\n4、持续优化前端性能，提升用户加载速度与交互体验；\n5、关注前沿技术发展，推动团队技术氛围与能力成长；\n6、有小程序、跨端框架（如 React Native）经验者优先考虑；\n\n任职要求：\n\n1、计算机相关专业本科及以上学历，2 年以上前端开发经验；\n2、精通 H5相关技术，熟悉 ES6+ 规范；\n3、熟悉主流前端框架 Vue / React / Angular 至少一种；\n4、熟悉 性能优化手段，了解浏览器渲染机制；\n5、熟悉 RESTful API、HTTP 协议及前后端协作流程；\n6、具备良好的沟通能力和团队协作意识，责任心强，有自我驱动力；\n\n加分项：\n\n1、有大型 TOC 类项目（电商、社交、直播、短视频等）开发经验；\n2、有用户体验敏感度，关注细节和交互体验。\n3、有开源社区贡献经历\n\n### 杭州 游侠客国际旅游 前端 Vue\n\n- base 杭州\n- 公司：游侠客国际旅游 https://www.youxiake.com/\n- 职位：前端开发工程师（C端：旅游APP方向）\n- 核心技术栈：Vue\n- 邮箱：whw@youxiake.com\n- 发布时间：2025.08.07\n\nJD 岗位描述：\n\n1、实现旅游类C端产品(Web/APP/小程序)精美页面：包括 banner、内容页、详情页、交互地图等模块；\n2、打磨视觉动效细节：像素级还原、CSS 动画、Lottie/Canvas、SVG/Three.js 等；\n3、结合旅行场景提升性能体验：确保界面在网络不稳、离线环境下依然流畅；\n4、与设计师紧密协作，从技术和审美双角度优化视觉方案；\n5、跟踪前端前沿技术，推动旅游产品在视觉表现上保持行业领先。\n\n岗位要求:\n\n1、全日制统招本科及以上学历，计算机或相关专业；\n2、1-3年 Web/移动端前端开发经验，至少有一个C端项目上线经验；\n3、熟练 Vue3+TypeScript，精通HTML5/CSS3/Javascript(Es6+)；\n4、精通 CSS 布局(Flex/Grid)、动画(CSS3 动画库、SVG/Canvas)；\n5、对 UI 视觉和动效有敏锐触觉，能精准复刻设计稿。\n\n### 长沙 得物 前端 React\n\n- base：长沙\n- 公司：得物 https://www.dewu.com/\n- 职位：前端开发工程师（效率）\n- 核心技术栈：React\n- 邮箱：o_zhuotuo@dewu.com\n- 发布时间：2025.08.07\n\nJD 岗位描述：https://poizon.jobs.feishu.cn/s/ZOJoP4xWLPk\n\n### 长沙 得物 前端 React\n\n- base：长沙\n- 公司：得物 https://www.dewu.com/\n- 职位：前端开发工程师（数据/交易/汇金）\n- 核心技术栈：React\n- 邮箱：o_zhuotuo@dewu.com\n- 发布时间：2025.08.07\n- **急聘**\n\nJD 岗位描述：https://poizon.jobs.feishu.cn/s/NjtITcaAHFs\n\n### 长沙 得物 前端 React\n\n- base：长沙\n- 公司：得物 https://www.dewu.com/\n- 职位：前端开发工程师（数据/交易）\n- 核心技术栈：React\n- 邮箱：o_zhuotuo@dewu.com\n- 发布时间：2025.08.07\n\nJD 岗位描述：https://poizon.jobs.feishu.cn/s/hUgkDsw1ZSI\n"
  },
  {
    "path": "docs/third-exam/ask-in-reply.md",
    "content": "# 反问面试官\n\n在每个面试环节都可以反问面试官，我们统一放在这里写。\n\n要根据面试情况，看面试过程是否顺畅，如感觉不好，就别反问了。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 团队使用的技术栈是什么？用什么框架？\n\n和自己的技术栈是否匹配？是否需要自己提前学习熟悉？\n\n## 如果我加入，我将会负责什么产品和业务？\n\n判断是不是这个公司的主流业务线？新业务线有机遇也有风险，边角的业务线不要去\n\n## 项目团队是否有完善的角色人员？\n\n如前端、服务端、客户端、QA、UI、PM、运营等...\n\n判断是否是正规部门，别是一个附属部门、附属产品\n\n## 9 点以后（或周末）打车是否可以报销？\n\n就是问会不会经常加班？周末是否双休？\n\n这关系到你的业余生活和学习时间\n\n## 对我后续的技术提升有什么建议？\n\n可以观察到面试官对自己的评价\n"
  },
  {
    "path": "docs/third-exam/cross-test.md",
    "content": "# 交叉面试\n\n二面结束以后，有可能会再找隔壁部门的高级/资深工程师交叉面试。交叉面试会综合考察候选人的技术能力。\n\n注意，不一定所有面试都会有交叉面试，但这些面试题还是要刷一遍的，都是常考题。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 求两个数组的交集和并集\n\n给两个数组，求数组的交集和并集\n\n```js\nconst arr1 = [1, 3, 4, 6, 7]\nconst arr2 = [2, 5, 3, 6, 1]\n\nfunction getIntersection(arr1, arr2) {\n  // 交集...\n}\n\nfunction getUnion(arr1, arr2) {\n  // 并集...\n}\n```\n\n参考答案\n\n::: details\n\n要点\n\n- 交集，转换为 Set ，因为 Set has 比数组 includes 快很多（前者 O(1) 后者 O(n)）\n- 并集，直接 add 即可，利用 Set 去重特性\n\n代码\n\n```js\nconst arr1 = [1, 3, 4, 6, 7]\nconst arr2 = [2, 5, 3, 6, 1]\n\n// 交集\nfunction getIntersection(arr1, arr2) {\n  const res = new Set()\n  const set2 = new Set(arr2)\n  for (let item of arr1) {\n    if (set2.has(item)) {\n      // 注意，这里要用 Set has 方法，比数组的 includes 快很多\n      res.add(item)\n    }\n  }\n  return Array.from(res)\n}\n\n// 并集\nfunction getUnion(arr1, arr2) {\n  const res = new Set(arr1)\n  for (let item of arr2) {\n    res.add(item) // 利用 Set 自动去重的特性\n  }\n  return Array.from(res)\n}\n\n// 测试\nconsole.log('交集', getIntersection(arr1, arr2))\nconsole.log('并集', getUnion(arr1, arr2))\n```\n\n:::\n\n## 数组转树\n\n通常我们有一个包含父子关系的数组，目标是将其转化为树形结构。\n\n示例数据：\n\n```javascript\nconst arr = [\n  { id: 1, parentId: null, name: 'Root' },\n  { id: 2, parentId: 1, name: 'Child 1' },\n  { id: 3, parentId: 1, name: 'Child 2' },\n  { id: 4, parentId: 2, name: 'Grandchild 1' },\n]\n```\n\n目标生成：\n\n```javascript\nconst tree = [\n  {\n    id: 1,\n    name: 'Root',\n    children: [\n      {\n        id: 2,\n        name: 'Child 1',\n        children: [{ id: 4, name: 'Grandchild 1', children: [] }],\n      },\n      {\n        id: 3,\n        name: 'Child 2',\n        children: [],\n      },\n    ],\n  },\n]\n```\n\n参考答案:\n\n::: details\n\n实现思路：\n\n1. 遍历数组，将每个元素存储到一个以 `id` 为键的 Map 中。\n2. 再次遍历数组，根据 `parentId` 将子节点挂载到父节点的 `children` 属性上。\n3. 提取 `parentId` 为 `null` 的顶层节点作为树的根。\n\n代码实现：\n\n```javascript\nfunction arrayToTree(arr) {\n  const idMap = new Map()\n  const result = []\n\n  // 初始化 Map\n  arr.forEach((item) => {\n    idMap.set(item.id, { ...item, children: [] })\n  })\n\n  // 构建树\n  arr.forEach((item) => {\n    const parent = idMap.get(item.parentId)\n    if (parent) {\n      parent.children.push(idMap.get(item.id))\n    } else {\n      result.push(idMap.get(item.id))\n    }\n  })\n\n  return result\n}\n\nconsole.log(JSON.stringify(arrayToTree(arr), null, 2))\n```\n\n注意点：\n\n- 确保 `parentId` 为 `null` 的节点是根节点。\n- 避免循环依赖：输入数据需要合法，否则会导致死循环。\n  :::\n\n## 树转数组\n\n将树形结构扁平化为数组，保留原有的层级关系。\n\n示例数据：\n\n```javascript\nconst tree = [\n  {\n    id: 1,\n    name: 'Root',\n    children: [\n      {\n        id: 2,\n        name: 'Child 1',\n        children: [{ id: 4, name: 'Grandchild 1', children: [] }],\n      },\n      {\n        id: 3,\n        name: 'Child 2',\n        children: [],\n      },\n    ],\n  },\n]\n```\n\n目标生成：\n\n```javascript\nconst arr = [\n  { id: 1, name: 'Root', parentId: null },\n  { id: 2, name: 'Child 1', parentId: 1 },\n  { id: 3, name: 'Child 2', parentId: 1 },\n  { id: 4, name: 'Grandchild 1', parentId: 2 },\n]\n```\n\n参考答案:\n\n::: details\n\n实现思路：\n\n1. 使用递归遍历树。\n2. 在每次递归中记录当前节点的 `parentId`。\n3. 将节点及其子节点逐一添加到结果数组中。\n\n代码实现：\n\n```javascript\nfunction treeToArray(tree, parentId = null) {\n  const result = []\n\n  tree.forEach((node) => {\n    const { id, name, children } = node\n    result.push({ id, name, parentId })\n    if (children && children.length > 0) {\n      result.push(...treeToArray(children, id))\n    }\n  })\n\n  return result\n}\n\nconsole.log(JSON.stringify(treeToArray(tree), null, 2))\n```\n\n注意点：\n\n- 递归中需避免重复引用。\n- 树节点的 `children` 属性需要有效（可以为空数组但不能为 `undefined`）。\n\n:::\n\n## cookie localStorage sessionStorage 三者有什么区别，有什么应用场景？\n\n参考答案：\n:::details\n区别：\n| **特性** | **Cookie** | **LocalStorage** | **SessionStorage** |\n|-------------------|-----------------------------|--------------------------|--------------------------|\n| 写入方式 | 服务端和前端都可写入，不过http-only情况下只允许服务端写入 | 前端 | 前端 |\n| 存储大小 | 4KB 左右 | 5~10MB | 5~10MB |\n| 生命周期 | 手动设置，默认关闭浏览器失效 | 长期保留，直至用户手动清理缓存 | 当前会话，关闭页面清除 |\n| 服务器交互 | **会** 随请求发送到服务器 | 不会 | 不会 |\n| 数据共享 | 同域下所有页面共享 | 同域下所有页面共享 | 当前页面及子页面共享 |\n\n应用场景：\n\n- **Cookie** ：小数据量、需与服务器交互的场景，如保存会话标识（如 `token`）。\n- **LocalStorage** ：需持久化存储、跨页面共享的数据，如用户设置、主题偏好。\n- **SessionStorage** ：页面刷新或跳转时临时保存的数据，如表单填写进度。\n  :::\n\n## 前端会有哪些安全问题？该如何预防？\n\n先从应用架构的角度来看，前端可以分为多个核心模块，每个模块都有可能成为攻击目标。\n\n:::details\n\n1. 用户界面与数据展示层\n\n- **攻击风险** ：跨站脚本攻击（XSS）、HTML注入、点击劫持\n- **防御措施** ：\n  - 严格过滤和转义用户输入，防止恶意代码注入\n  - 使用安全模板渲染，避免直接操作 DOM（例如避免使用 innerHTML）\n  - 配置内容安全策略（CSP），限制脚本来源\n  - 设置 X-Frame-Options 或 CSP 的 frame-ancestors 指令防止点击劫持\n\n2. 业务逻辑层\n\n- **攻击风险** ：业务逻辑漏洞导致未授权访问或功能滥用\n- **防御措施** ：\n  - 实现完善的权限校验和身份验证机制\n  - 定期进行代码审查和安全测试，及时发现逻辑漏洞\n\n3. 数据交互层\n\n- **攻击风险** ：跨站请求伪造（CSRF）、中间人攻击、数据窃取\n- **防御措施** ：\n  - 使用 HTTPS 加密数据传输，防止数据在传输过程中被窃取或篡改\n  - 在请求中加入 CSRF Token，并在服务器端验证请求合法性\n  - 配置严格的 CORS 策略，确保 API 调用来源可信\n\n4. 数据存储层\n\n- **攻击风险** ：本地存储数据泄露（LocalStorage、SessionStorage）、Cookie 劫持\n- **防御措施** ：\n  - 避免在前端存储敏感信息，如必须存储应进行加密处理\n  - 对 Cookie 设置 HttpOnly、Secure 等属性，降低被脚本读取的风险\n\n5. 资源加载与依赖管理层\n\n- **攻击风险** ：第三方依赖库漏洞、供应链攻击、外部资源篡改\n- **防御措施** ：\n  - 定期更新和审查第三方依赖，及时修补已知漏洞\n  - 使用子资源完整性（SRI）校验机制，确保加载的外部资源未被篡改\n  - 仅从可信源加载资源，杜绝未知或不受信任的代码注入\n\n6. 构建与部署流程\n\n- **攻击风险** ：构建工具或 CI/CD 流程被攻击，导致恶意代码注入\n- **防御措施** ：\n  - 加强构建环境安全管理，定期更新和审查构建工具及依赖\n  - 采用代码签名、版本管理和自动化安全测试，确保发布版本的完整性和可追溯性\n  - 将安全检测纳入 CI/CD 流程，实现自动化的安全漏洞扫描\n\n综上所述，只有从整体应用架构的角度出发，针对各个模块的不同攻击面采取多层次防御措施，才能真正保障前端系统的安全性和稳定性。\n:::\n\n## 常见的 git 命令有哪些？\n\n::: details\n\n1. **克隆远程仓库** ：每个开发者首先需要将远程仓库克隆到本地，以获取项目的最新代码。\n\n   ```bash\n   git clone <远程仓库URL>\n   ```\n\n2. **创建并切换到功能分支** ：在主分支（如 `main` 或 `master`）上创建一个新的功能分支，以便在该分支上进行开发，避免直接在主分支上工作。\n\n   ```bash\n   git checkout -b feature-branch\n   ```\n\n3. **进行开发并提交更改** ：在功能分支上进行开发，完成后将更改添加到暂存区并提交。\n\n   ```bash\n   git add .\n   git commit -m \"描述本次提交的内容\"\n   ```\n\n4. **同步远程主分支的最新更改** ：在推送之前，先拉取远程主分支的最新更改，以避免推送时发生冲突。\n\n   ```bash\n   git fetch origin main\n   git merge origin/main\n   ```\n\n5. **推送功能分支到远程仓库** ：将本地的功能分支推送到远程仓库，以便其他团队成员可以访问。\n\n   ```bash\n   git push origin feature-branch\n   ```\n\n6. **创建 Pull Request（PR）** ：在 GitHub 等平台上，从功能分支向主分支发起 PR，请求将功能分支的更改合并到主分支。\n\n7. **代码审查与合并** ：团队成员对 PR 进行审查，提出修改建议或直接在网页上进行评论。审查通过后，项目管理员或有权限的成员将 PR 合并到主分支。\n\n8. **删除已合并的功能分支（可选）** ：为保持仓库整洁，合并后可考虑删除远程和本地的功能分支。\n\n   ```bash\n   git branch -d feature-branch\n   git push origin --delete feature-branch\n   ```\n\n**注意事项** ：\n\n- **解决冲突** ：在拉取远程主分支的更改时，可能会遇到冲突。此时，需要手动解决冲突，然后提交解决后的更改。\n\n- **保持分支同步** ：在开发过程中，定期从远程主分支拉取最新的更改，以保持本地分支的同步，减少合并时的冲突。\n\n- **提交规范** ：遵循统一的提交规范，如使用清晰的提交信息，方便团队成员理解每次提交的目的和内容。\n\n:::\n\n## 如何使用 git 多人协作开发？\n\n根据项目的规模、性质和团队需求有不同的安排。\n\n::: details\n\n1. **小型项目** （例如：1-3 人的小型开发团队）\n\n- **共享仓库模型** ：大家都对同一个远程仓库进行操作。\n- **策略** ：\n  - 可以直接使用 `master/main` 分支，所有成员都可以在此分支上工作，避免复杂的分支管理。\n  - 每个开发者都在本地创建自己的功能分支进行开发，完成后合并回 `main` 或 `master`。\n  - 提交时保持简洁，并且在每次 `push` 前与远程仓库同步（`git pull --rebase`）。\n- **具体流程** ：\n  1.  `git clone` 克隆远程仓库。\n  2.  `git checkout -b feature-branch` 创建并切换到自己的功能分支。\n  3.  完成功能开发后，`git add .`、`git commit -m \"Description\"` 提交本地修改。\n  4.  使用 `git pull --rebase` 更新远程仓库，解决冲突。\n  5.  使用 `git push` 推送到远程仓库。\n  6.  其他成员拉取最新的修改，确保项目同步。\n\n2. **中型项目** （例如：3-10 人的团队）\n\n- **基于分支的协作** ：主分支用于发布，功能开发分支（feature branch）和修复分支（bugfix branch）被广泛使用。\n- **策略** ：\n  - `main` 或 `master` 作为生产分支，稳定且可以随时部署。\n  - 开发人员通过功能分支进行开发，提交合并请求（Pull Requests）前进行代码审查。\n  - 通过 `develop` 分支进行日常开发，`feature` 分支从 `develop` 分支创建，开发完成后合并回 `develop`。\n- **具体流程** ：\n  1.  `git clone` 克隆远程仓库。\n  2.  切换到 `develop` 分支并保持更新（`git pull`）。\n  3.  创建自己的功能分支 `git checkout -b feature-branch`。\n  4.  开发完成后，将功能分支推送到远程 `git push origin feature-branch`。\n  5.  创建 Pull Request (PR)，请求代码审查并合并到 `develop` 分支。\n  6.  定期将 `develop` 分支合并回 `main` 或 `master` 分支进行发布。\n\n3. **大型项目** （例如：10 人以上的团队）\n\n- **Git Flow** ：这是一个非常适合大团队协作的模型。通过多个分支策略进行管理，确保版本发布和功能开发的平稳过渡。\n- **策略** ：\n  - `main` 或 `master` 用于发布稳定版本。\n  - `develop` 分支用于日常开发，所有新功能都在此基础上开发。\n  - 使用 `feature` 分支进行独立的功能开发。\n  - 使用 `release` 分支准备发布版本，包含 Bug 修复和最后的稳定性验证。\n  - `hotfix` 分支用于快速修复生产环境的 bug。\n- **具体流程** ：\n  1.  `git clone` 克隆仓库，切换到 `develop` 分支。\n  2.  创建并切换到新的功能分支 `git checkout -b feature/feature-name`。\n  3.  在功能分支上开发，完成后推送并创建 PR 合并回 `develop` 分支。\n  4.  在 `develop` 分支合并后，测试团队测试新的功能，确保没有问题。\n  5.  若需发布新版本，从 `develop` 创建 `release` 分支，进行最后的 bug 修复和稳定性测试。\n  6.  发布后将 `release` 分支合并到 `main` 和 `develop` 分支。\n  7.  快速修复 bug 时，从 `main` 分支创建 `hotfix` 分支，修复后合并回 `main` 和 `develop`。\n\n4. **开源项目**\n\n- **Fork & Pull Request 模式** ：开源项目通常采用这种模式，每个贡献者通过自己的 Fork 进行开发，并通过 Pull Request 提交贡献。\n- **策略** ：\n  - 贡献者 Fork 项目仓库到自己的 GitHub（或其他平台）账户。\n  - 在 Fork 的仓库中开发新的功能或修复 bug。\n  - 完成开发后，创建 Pull Request 提交到原仓库进行审查。\n  - 项目维护者负责合并经过审查的代码，确保项目稳定。\n- **具体流程** ：\n  1.  `git fork` 仓库到自己的 GitHub 账户。\n  2.  `git clone` 自己 Fork 后的仓库。\n  3.  创建一个功能分支 `git checkout -b feature-name`。\n  4.  在功能分支上进行开发，提交修改并推送到自己的 Fork 仓库。\n  5.  提交 PR 请求合并到原仓库的 `main` 或 `develop` 分支。\n  6.  原项目维护者审查代码，若通过则合并；如果有问题，贡献者根据反馈修改代码。\n\n5. **闭源项目**\n\n- **私有仓库** ：闭源项目通常使用私有仓库进行管理，团队协作模式与开源项目类似，但可能不需要开放给外部贡献者。\n- **策略** ：\n  - 仅限团队内部访问，所有成员都在相同的权限范围内操作。\n  - 使用与中型项目类似的 Git Flow 或其他基于分支的工作流。\n- **具体流程** ：\n  1.  创建私有仓库并初始化 `main` 或 `master` 分支。\n  2.  开发人员从 `develop` 分支创建功能分支进行开发。\n  3.  完成后提交 PR 进行代码审查。\n  4.  审查通过后，合并回 `develop` 分支并准备发布。\n  5.  发布前测试人员验证，发布后合并到 `main`。\n\n总结\n\n- **小型项目** ：共享仓库模型，简单的开发流程。\n- **中型项目** ：功能分支管理，使用 `develop` 和 `feature` 分支。\n- **大型项目** ：Git Flow 模式，多分支管理，发布和修复分支分开。\n- **开源项目** ：Fork & Pull Request 模式，社区贡献，开放和审查。\n- **闭源项目** ：私有仓库，常用 Git Flow 或类似工作流，团队内部管理。\n\n:::\n\n## 是否熟悉 Linux 系统，常见的 Linux 命令有哪些？\n\n::: details\n\n1. **文件与目录操作**\n\n- `ls`：列出当前目录下的文件和目录。\n  - `ls -l`：显示详细信息（如权限、大小、修改时间）。\n  - `ls -a`：显示所有文件，包括隐藏文件。\n- `cd`：切换当前目录。\n  - `cd /path/to/directory`：跳转到指定目录。\n  - `cd ..`：跳转到上级目录。\n  - `cd ~`：跳转到用户的主目录。\n- `pwd`：显示当前工作目录的完整路径。\n- `mkdir`：创建目录。\n  - `mkdir new-directory`：创建名为 `new-directory` 的目录。\n- `rm`：删除文件或目录。\n  - `rm file.txt`：删除文件。\n  - `rm -rf directory/`：递归删除目录及其内容。\n- `cp`：复制文件或目录。\n  - `cp source.txt destination.txt`：复制文件。\n  - `cp -r source-directory/ destination/`：递归复制目录。\n- `mv`：移动文件或目录，或重命名。\n  - `mv oldname.txt newname.txt`：重命名文件。\n  - `mv file.txt /path/to/destination/`：移动文件。\n\n2. **文件内容查看与编辑**\n\n- `cat`：查看文件内容。\n  - `cat file.txt`：查看 `file.txt` 文件的内容。\n- `less`：分页查看文件内容，支持上下翻页。\n  - `less file.txt`：分页查看文件内容。\n- `head`：查看文件的前几行。\n  - `head -n 10 file.txt`：查看文件前 10 行。\n- `tail`：查看文件的后几行。\n  - `tail -n 10 file.txt`：查看文件后 10 行。\n  - `tail -f file.txt`：实时查看文件新增的内容（常用于查看日志文件）。\n- `nano` 或 `vim`：命令行文本编辑器。\n  - `nano file.txt`：用 `nano` 编辑文件（易用）。\n  - `vim file.txt`：用 `vim` 编辑文件（功能强大，学习曲线较陡）。\n\n3. **权限管理**\n\n- `chmod`：更改文件或目录的权限。\n  - `chmod 755 file.txt`：给文件设置读、写、执行权限。\n  - `chmod +x script.sh`：给脚本文件增加执行权限。\n- `chown`：更改文件或目录的所有者。\n  - `chown user:group file.txt`：将文件的所有者改为 `user`，所属组改为 `group`。\n\n4. **Git 与版本控制**\n\n- `git clone`：克隆远程 Git 仓库到本地。\n- `git pull`：从远程仓库拉取最新的更新。\n- `git push`：将本地的提交推送到远程仓库。\n- `git commit`：提交代码。\n- `git status`：查看当前工作区的状态。\n- `git branch`：列出所有本地分支。\n- `git checkout`：切换到其他分支。\n- `git merge`：合并分支。\n- `git log`：查看提交历史。\n\n5. **系统管理与监控**\n\n- `top`：查看系统的实时进程和资源使用情况。\n- `htop`：`top` 的增强版，图形化界面（需要安装）。\n- `ps`：查看当前正在运行的进程。\n  - `ps aux`：查看所有进程。\n- `kill`：终止进程。\n  - `kill PID`：杀死指定 PID 的进程。\n- `df`：查看磁盘空间使用情况。\n  - `df -h`：以人类可读的格式显示磁盘空间。\n- `free`：查看内存使用情况。\n  - `free -h`：以人类可读格式显示内存使用情况。\n\n6. **网络操作**\n\n- `ping`：测试网络连接。\n  - `ping google.com`：测试与 Google 的网络连接。\n- `curl`：获取网页或 API 响应。\n  - `curl https://api.example.com`：获取指定 URL 的内容。\n- `wget`：从网络下载文件。\n  - `wget http://example.com/file.zip`：下载文件。\n- `netstat`：查看网络连接。\n  - `netstat -tuln`：查看监听的端口。\n- `ssh`：远程连接到其他服务器。\n  - `ssh user@hostname`：通过 SSH 连接到远程服务器。\n\n7. **日志查看**\n\n- `tail -f /var/log/nginx/access.log`：实时查看 Nginx 访问日志。\n- `journalctl -u service-name`：查看特定服务的日志。\n- `grep`：在文件中查找特定的文本模式。\n  - `grep \"ERROR\" /var/log/nginx/error.log`：查找 Nginx 错误日志中的所有 `ERROR`。\n\n8. **包管理**\n\n- `apt`（Debian/Ubuntu 系）：\n  - `apt update`：更新软件包列表。\n  - `apt upgrade`：升级已安装的软件包。\n  - `apt install package-name`：安装指定软件包。\n  - `apt remove package-name`：卸载指定软件包。\n- `yum`（CentOS/RHEL 系）：\n  - `yum install package-name`：安装指定软件包。\n  - `yum update`：更新软件包。\n\n9. **前端开发相关**\n\n- **Node.js 项目管理** ：\n  - `npm install`：安装项目依赖。\n  - `npm run build`：执行构建命令（如构建生产环境的代码）。\n  - `npm start`：启动开发服务器。\n- **查看端口占用情况** ：\n  - `lsof -i :3000`：查看是否有进程在使用 3000 端口。\n  - `kill $(lsof -t -i :3000)`：杀掉占用 3000 端口的进程。\n- **使用 `pm2` 管理 Node.js 应用** ：\n  - `pm2 start app.js`：使用 `pm2` 启动 Node.js 应用。\n  - `pm2 restart app`：重启已启动的应用。\n  - `pm2 logs`：查看应用日志。\n\n10. **自动化与调度**\n\n- `cron`：定时任务调度。\n  - 编辑定时任务：`crontab -e`。\n  - 查看当前定时任务：`crontab -l`。\n- `at`：设置一次性任务。\n  - `at now + 5 minutes`：在 5 分钟后执行任务。\n\n:::\n\n## 如何调试前端代码？\n\n::: details\n调试前端代码的关键考点是调试流程和工具的掌握。\n\n调试的目标是识别和修复代码中的问题。\n\n我们可以从以下几个方面来看：\n\n1. **调试流程** ：\n\n   - **重现问题** ：确保你能准确重现问题，了解问题发生的条件。\n   - **隔离问题范围** ：通过分段注释、简化代码等方式，将问题范围逐渐缩小，便于定位问题。\n   - **检查错误信息** ：查看浏览器的控制台，检查是否有报错或警告，通常浏览器会提供比较详细的错误信息和堆栈跟踪。\n\n2. **常用调试工具** ：\n\n   - **浏览器开发者工具** ：Chrome、Firefox 等浏览器都自带强大的开发者工具，可以用来检查 DOM、样式、网络请求、控制台输出等。\n   - **断点调试** ：通过在代码中设置断点，逐步执行程序，查看各个变量的值，帮助定位问题。\n   - **调试输出** ：使用 `console.log()` 或更专业的调试工具（如 `debugger`）来输出变量或执行状态。\n\n3. **调试策略** ：\n   - **逐步排查** ：将代码分解成小模块，逐个排查，确认是哪个环节出了问题。\n   - **日志与错误追踪** ：在应用中嵌入日志功能，尤其是在生产环境中，利用像 Sentry 这样的工具收集并分析错误信息。\n\n最终，调试的关键是高效的定位和快速的修复。你在进行调试时，首先要了解应用的整体架构，然后再根据问题定位具体的模块或环节进行深入分析。\n\n:::\n\n## 移动端 H5 如何抓包网络请求？\n\n::: details\n\n抓包网络请求的关键考点是如何能够在移动端环境下监控和捕获网络流量。\n\n尤其是在 H5 应用中，通常需要处理多种协议（如 HTTP、HTTPS）和不同的网络层（如请求、响应、WebSocket 等）。\n\n我们可以从以下几个方面来进行分析：\n\n1. **抓包工具的选择** ：\n\n   - **Charles Proxy** ：一款功能强大的跨平台抓包工具，支持 HTTP、HTTPS 等协议的抓包，能够分析移动端应用的网络请求。可以通过在手机上设置代理，将手机的流量转发到电脑上进行抓包。\n   - **Fiddler** ：类似于 Charles，也是一个常用的抓包工具，可以抓取本地和移动设备的 HTTP 和 HTTPS 请求。通过配置代理服务器，将移动设备的流量通过它进行捕获。\n   - **Wireshark** ：适用于更低层次的网络分析，能够捕捉各种网络协议的数据包，但需要更高的网络知识。\n   - **Chrome DevTools** ：如果是调试 Web 移动版的 H5 页面，可以使用 Chrome 的开发者工具，直接通过远程调试功能抓取网络请求。\n\n2. **如何抓包** ：\n\n   - **设置代理** ：一般需要在移动端设备上设置代理，指向本地电脑的抓包工具。这样，所有移动设备的流量就会通过你的电脑转发，可以在抓包工具中查看和分析。\n     - 在手机的 Wi-Fi 设置中修改代理设置，填写电脑的 IP 地址和抓包工具的端口（通常是 8888 或 8889）。\n   - **HTTPS 证书问题** ：为了抓取 HTTPS 流量，抓包工具（如 Charles 或 Fiddler）通常需要安装它们的根证书。安装后，它们就能解密 HTTPS 请求，从而显示加密流量内容。\n   - **查看和分析数据** ：通过抓包工具，你可以看到每个请求的详细信息，如请求头、响应头、请求体、响应体等，帮助你诊断请求问题或调试接口。\n\n3. **调试时注意事项** ：\n   - **隐私和安全性** ：确保你只在合法的环境下抓包，避免捕获到敏感信息或违反用户隐私。\n   - **网络状态** ：抓包时可以模拟不同的网络环境（如模拟 3G、4G、Wi-Fi 等），帮助你排查网络质量对请求的影响。\n\n总结来说，抓包的关键是理解代理机制和 HTTPS 解密的过程，掌握合适的工具和流程，才能有效地分析移动端 H5 应用的网络请求。\n\n:::\n\n## 网页重绘 repaint 和重排 reflow 有什么区别\n\n::: details\n\n网页 **重绘（Repaint）** 和 **重排（Reflow）** 的区别可以从性能开销、触发原因和渲染机制三个关键点来分析：\n\n**1. 定义与触发条件**\n\n- **重绘（Repaint）**\n  重绘是指当元素的样式发生改变，但不影响布局时触发的渲染过程。\n  **触发条件** ：颜色、背景、边框等视觉样式的变化。\n\n  **示例** ：\n\n  ```css\n  element.style.backgroundColor = \"red\";\n  ```\n\n- **重排（Reflow）**\n  重排（又称回流）是指当页面布局或结构发生变化时，浏览器重新计算元素的位置和几何尺寸的过程。\n  **触发条件** ：DOM 节点的增删、元素位置的变化、盒模型属性（如 `width`, `height`, `padding` 等）的修改。\n\n  **示例** ：\n\n  ```css\n  element.style.width = \"300px\";\n  ```\n\n**2. 性能开销**\n\n- **重绘（Repaint）** ：\n  相对较轻，只需要更新像素信息，不需要重新计算布局。\n\n- **重排（Reflow）** ：\n  开销较大，可能会影响整个页面的渲染，尤其是当涉及到根节点或复杂嵌套布局时。\n\n**3. 如何优化**\n\n- **减少重绘和重排的方法** ：\n  - **合并样式更改** ：通过一次性设置多个样式属性，避免多次触发重排。\n  - **使用 `class` 替代内联样式** ：批量管理样式更高效。\n  - **避免频繁读取和写入 DOM 属性** ：将读取操作和写入操作分开。\n  - **使用 CSS3 硬件加速** ：如 `transform: translateZ(0);` 以减少重排。\n\n总结来说，重绘和重排的关键区别在于是否涉及布局计算。前者只影响视觉样式，后者会改变页面结构，性能开销也显著不同。\n\n:::\n\n## 网页多标签页之间如何通讯？和 iframe 如何通讯？\n\n::: details\n网页多标签页和 iframe 通讯的关键考点是跨窗口和跨域通信模型的选择，以及不同场景下的适用方法。可以从以下几个方面分析：\n\n**1. 多标签页之间的通讯方法**\n\n- **BroadcastChannel API**\n  同源的多个标签页可以使用 `BroadcastChannel` 进行消息广播，简单方便。\n  **示例** ：\n\n  ```javascript\n  const channel = new BroadcastChannel('my_channel')\n  channel.postMessage('Hello from another tab!')\n  channel.onmessage = (event) => {\n    console.log('Received message:', event.data)\n  }\n  ```\n\n- **LocalStorage + Storage 事件监听**\n  不同标签页可以共享 `localStorage`，通过监听 `storage` 事件实现通讯。\n  **示例** ：\n\n  ```javascript\n  window.addEventListener('storage', (event) => {\n    if (event.key === 'my_key') {\n      console.log('Received message:', event.newValue)\n    }\n  })\n  localStorage.setItem('my_key', 'Hello from another tab!')\n  ```\n\n- **Service Worker**\n  通过 `Service Worker` 作为中介，实现跨标签页通讯。适合 PWA 场景。\n\n- **WebSocket**\n  通过服务器中转实现实时通讯，适合跨域或需要长连接的场景。\n\n---\n\n**2. iframe 通讯方法**\n\n- **postMessage API**\n  最常用的方式，可以跨域发送消息。父页面和 iframe 双向通信都支持。\n  **示例（父页面向 iframe 发送消息）** ：\n\n  ```javascript\n  const iframe = document.querySelector('iframe')\n  iframe.contentWindow.postMessage('Hello iframe!', '*')\n  window.addEventListener('message', (event) => {\n    console.log('Received from iframe:', event.data)\n  })\n  ```\n\n- **URL Hash 传参**\n  通过修改 iframe 的 URL 哈希来传递参数。适用于简单场景。\n  **示例** ：\n\n  ```javascript\n  iframe.src = 'https://example.com#message=Hello'\n  ```\n\n- **共享 Cookie 或 LocalStorage**\n  在同源环境下可以通过共享存储机制间接通讯。\n\n---\n\n**3. 注意事项**\n\n- **安全性考虑** ：\n  - 使用 `postMessage` 时要指定目标源，避免消息被恶意网站接收。\n  - 避免直接信任外部传入的数据，做好验证与校验。\n- **兼容性与性能** ：\n  - 优先选择现代 API（如 `BroadcastChannel`）。\n  - 避免频繁存取 `localStorage` 导致性能问题。\n\n总结来说，选择通讯方法的关键在于是否同源、跨域需求、实时性要求等因素。\n:::\n\n## 什么是 axios 拦截器，能用来做什么？\n\nAxios 拦截器的关键是提供了网络请求生命周期的可控节点，能够有效地简化和规范前端网络请求的管理。\n\n::: details\n\n**Axios 拦截器** 的关键考点在于它是请求与响应流程中的中间层，用来在网络请求前后进行处理，满足业务需求和性能优化。可以从以下几个方面分析其用途和实践方法：\n\n---\n\n**1. 什么是 Axios 拦截器？**\n\nAxios 拦截器是 Axios 库提供的功能，可以在请求发出之前和响应数据返回之后进行拦截和处理。\n拦截器主要分为两类：\n\n- **请求拦截器（Request Interceptors）**\n- **响应拦截器（Response Interceptors）**\n\n---\n\n**2. Axios 拦截器的应用场景**\n\n- **添加通用请求头**\n  可以统一为所有请求添加认证 Token、语言信息等。\n\n  ```javascript\n  axios.interceptors.request.use((config) => {\n    config.headers['Authorization'] = 'Bearer my-token'\n    return config\n  })\n  ```\n\n- **全局错误处理**\n  统一处理服务器返回的错误，如用户未登录、网络错误等。\n\n  ```javascript\n  axios.interceptors.response.use(\n    (response) => response,\n    (error) => {\n      if (error.response.status === 401) {\n        alert('Unauthorized, please login')\n      }\n      return Promise.reject(error)\n    }\n  )\n  ```\n\n- **数据格式预处理**\n  对服务器返回的数据进行格式化，比如从 API 响应中提取有效数据部分。\n\n  ```javascript\n  axios.interceptors.response.use((response) => {\n    return response.data.result\n  })\n  ```\n\n- **请求节流与取消**\n  防止短时间内重复请求。\n\n  ```javascript\n  const CancelToken = axios.CancelToken\n  const source = CancelToken.source()\n\n  axios.get('/some-url', {\n    cancelToken: source.token,\n  })\n\n  source.cancel('Request canceled')\n  ```\n\n---\n\n**3. 注意事项**\n\n- **拦截器顺序** ：\n  请求拦截器会按添加顺序执行，而响应拦截器按相反顺序执行。\n- **错误处理机制** ：\n  错误处理函数需要显式调用 `Promise.reject(error)`，否则错误可能被吞掉。\n\n:::\n\n## 是否熟悉 Performance API ，是否了解常见的性能指标？\n\n::: details\n\n**Performance API** 的关键考点是提供了一组 Web 标准接口来获取和分析页面性能数据，帮助开发者定位性能瓶颈并优化体验。其中涉及到的常见性能指标如 FP、FCP、LCP 等能直观反映用户的视觉加载体验。\n\n---\n\n**1. 什么是 Performance API？**\n\nPerformance API 是浏览器提供的内置接口，用于测量网页的加载时间、资源性能和用户体验。\n\n**常用接口**\n\n- `performance.now()`：返回相对于页面加载时间的高精度时间戳。\n- `performance.mark()` 和 `performance.measure()`：创建和测量自定义性能标记。\n- `performance.getEntriesByType()`：获取特定类型的性能数据，如资源加载或导航时间。\n\n---\n\n**2. 常见性能指标**\n\n- **FP (First Paint)**\n  **首次绘制** ，指用户第一次看到页面内容时的时间点（通常是背景颜色）。\n- **FCP (First Contentful Paint)**\n  **首次内容绘制** ，页面中首个内容（如文字、图片）被绘制的时间点。\n  **优化思路** ：减少 CSS 阻塞、优化首屏加载内容。\n\n- **LCP (Largest Contentful Paint)**\n  **最大内容绘制** ，页面中最大内容元素（如主标题、图片）绘制完成的时间点。\n  **优化思路** ：使用延迟加载策略、优化图像加载。\n\n- **CLS (Cumulative Layout Shift)**\n  **累积布局偏移** ，页面加载过程中视觉内容意外变化的总量。\n  **优化思路** ：设置明确的宽高，避免懒加载导致布局移动。\n\n- **FID (First Input Delay)**\n  **首次输入延迟** ，用户第一次交互（如点击按钮）与浏览器响应之间的时间间隔。\n  **优化思路** ：减少主线程阻塞。\n\n- **TTI (Time to Interactive)**\n  **可交互时间** ，页面完成加载并能够快速响应用户交互的时间。\n\n---\n\n**3. 如何使用 Performance API**\n\n**示例：获取 FCP**\n\n```javascript\nnew PerformanceObserver((entryList) => {\n  const entries = entryList.getEntries()\n  entries.forEach((entry) => {\n    if (entry.name === 'first-contentful-paint') {\n      console.log('FCP:', entry.startTime)\n    }\n  })\n}).observe({ type: 'paint', buffered: true })\n```\n\n---\n\n**4. 注意事项**\n\n- **兼容性** ：不同 API 在浏览器中的支持情况可能不同，需合理降级处理。\n- **数据分析** ：结合 Lighthouse 等工具进行系统化分析，而不是依赖单一指标。\n\n:::\n\n## sourcemap 有何作用，如何配置？\n\n::: details\n\n**1. Source Map 的作用及配置**\n\nSource Map 是一种将压缩、混淆后的代码映射回源代码的文件，用于调试和定位错误。它的主要作用如下：\n\n- **调试优化** ：在开发者工具中看到源代码而非压缩后的代码。\n- **错误定位** ：在生产环境中准确定位代码错误。\n- **性能分析** ：配合性能工具对源代码进行优化分析。\n\n**配置方法**\n\n1. **Webpack 中配置**\n\n   ```javascript\n   module.exports = {\n     mode: 'production',\n     devtool: 'source-map', // 生成 Source Map\n   }\n   ```\n\n   常见选项：\n\n   - `source-map`: 完整映射，适合生产环境。\n   - `cheap-module-source-map`: 生成更快，但映射不包括列信息。\n   - `eval-source-map`: 适合开发环境，生成速度快。\n\n2. **Vite 配置**\n   ```javascript\n   export default {\n     build: {\n       sourcemap: true,\n     },\n   }\n   ```\n\n:::\n\n## 什么是 HTTPS 中间人攻击，如何预防\n\n::: details\n\n**中间人攻击（MITM, Man-In-The-Middle）** 是指攻击者拦截客户端与服务器之间的通信，获取敏感信息或篡改数据。\n\n**攻击原理**\n\n攻击者通过伪造证书或劫持网络流量，冒充服务器或客户端，使通信双方无法察觉中间人的存在。\n\n**预防措施**\n\n1. **启用 HTTPS 和强证书验证**\n\n   - 配置 TLS 并购买可信的 SSL 证书。\n   - 使用 HSTS（HTTP Strict Transport Security）强制 HTTPS 访问。\n\n2. **证书固定（Certificate Pinning）**\n   确保客户端只接受特定 CA 签发的证书。\n3. **开启 CORS 配置**\n   配置严格的跨域策略，减少不必要的网络暴露。\n\n4. **安全头部配置**\n\n   - 设置 `Content-Security-Policy` 防止资源篡改。\n   - 设置 `Strict-Transport-Security` 强制使用 HTTPS。\n\n5. **客户端验证**\n   通过双向 TLS（Mutual TLS）验证客户端身份。\n\n:::\n\n## 什么是 OOP ，面向对象三要素是什么？\n\n::: details\n\n**1. 什么是 OOP (Object-Oriented Programming)?**\n\n面向对象编程（Object-Oriented Programming，简称 OOP）是一种编程范式，通过将程序中的功能和数据封装为对象来实现模块化和复用。对象是具有属性（状态）和方法（行为）的实体，能够与其他对象进行交互。OOP 的核心思想是**通过模拟现实世界的模型来提高软件开发的灵活性与维护性** 。\n\n**2. 面向对象的三要素**\n\n1. **封装（Encapsulation）**\n\n   - **概念** ：将数据和操作数据的方法绑定在一起，对外隐藏对象的内部实现细节。\n   - **作用** ：\n     - 提高代码安全性，避免外部直接修改数据。\n     - 便于代码维护，减少不同模块之间的耦合。\n   - **示例**\n\n     ```javascript\n     class Person {\n       constructor(name, age) {\n         this._name = name // 私有变量（约定形式）\n         this._age = age\n       }\n\n       get name() {\n         return this._name\n       }\n\n       set name(newName) {\n         if (newName) this._name = newName\n       }\n     }\n     const person = new Person('Alice', 25)\n     console.log(person.name) // Alice\n     ```\n\n2. **继承（Inheritance）**\n\n   - **概念** ：子类继承父类的属性和方法，从而避免重复代码。\n   - **作用** ：\n     - 代码复用，减少重复。\n     - 建立层次结构，实现多态。\n   - **示例**\n\n     ```javascript\n     class Animal {\n       speak() {\n         console.log('Animal sound')\n       }\n     }\n\n     class Dog extends Animal {\n       speak() {\n         console.log('Woof!')\n       }\n     }\n     const dog = new Dog()\n     dog.speak() // Woof!\n     ```\n\n3. **多态（Polymorphism）**\n\n   - **概念** ：不同对象可以以不同的形式执行相同的方法调用。\n   - **作用** ：\n     - 增强代码的灵活性。\n     - 提高系统的扩展性。\n   - **示例**\n\n     ```javascript\n     class Shape {\n       draw() {\n         console.log('Drawing shape')\n       }\n     }\n\n     class Circle extends Shape {\n       draw() {\n         console.log('Drawing circle')\n       }\n     }\n\n     const shapes = [new Shape(), new Circle()]\n     shapes.forEach((shape) => shape.draw())\n     ```\n\n:::\n\n## 前端常见的设计模式有哪些？以及应用场景\n\n::: details\n\n1. **单例模式（Singleton Pattern）**\n\n- **概念** ：保证一个类只有一个实例，并提供全局访问点。\n- **应用场景** ：\n  - 全局状态管理，例如 Vuex 或 Redux Store。\n  - 浏览器缓存管理或全局配置。\n- **示例** ：\n  ```javascript\n  class Singleton {\n    constructor() {\n      if (!Singleton.instance) {\n        Singleton.instance = this\n      }\n      return Singleton.instance\n    }\n  }\n  const instance1 = new Singleton()\n  const instance2 = new Singleton()\n  console.log(instance1 === instance2) // true\n  ```\n\n---\n\n2. **工厂模式（Factory Pattern）**\n\n- **概念** ：通过工厂方法创建对象，而不是直接实例化。\n- **应用场景** ：\n  - 动态创建 UI 组件。\n  - 根据配置动态生成实例。\n- **示例** ：\n\n  ```javascript\n  class Button {\n    render() {\n      console.log('Render Button')\n    }\n  }\n\n  class Input {\n    render() {\n      console.log('Render Input')\n    }\n  }\n\n  class Factory {\n    static createElement(type) {\n      switch (type) {\n        case 'button':\n          return new Button()\n        case 'input':\n          return new Input()\n        default:\n          throw new Error('Unknown type')\n      }\n    }\n  }\n\n  const button = Factory.createElement('button')\n  button.render() // Render Button\n  ```\n\n---\n\n3. **观察者模式（Observer Pattern）**\n\n- **概念** ：一个对象（观察者）订阅另一个对象（被观察者）的变化。\n- **应用场景** ：\n  - 数据绑定和事件系统，例如 Vue 的响应式系统、EventEmitter。\n  - 实现消息推送功能。\n- **示例** ：\n\n  ```javascript\n  class Subject {\n    constructor() {\n      this.observers = []\n    }\n\n    subscribe(observer) {\n      this.observers.push(observer)\n    }\n\n    notify(data) {\n      this.observers.forEach((observer) => observer.update(data))\n    }\n  }\n\n  class Observer {\n    update(data) {\n      console.log(`Received: ${data}`)\n    }\n  }\n\n  const subject = new Subject()\n  const observer1 = new Observer()\n  subject.subscribe(observer1)\n  subject.notify('Hello!') // Received: Hello!\n  ```\n\n---\n\n4. **策略模式（Strategy Pattern）**\n\n- **概念** ：将一组算法封装到独立的类中，使得它们可以互换。\n- **应用场景** ：\n  - 表单验证策略。\n  - 动态路由匹配。\n- **示例** ：\n\n  ```javascript\n  class AddStrategy {\n    execute(a, b) {\n      return a + b\n    }\n  }\n\n  class MultiplyStrategy {\n    execute(a, b) {\n      return a * b\n    }\n  }\n\n  class Calculator {\n    constructor(strategy) {\n      this.strategy = strategy\n    }\n\n    calculate(a, b) {\n      return this.strategy.execute(a, b)\n    }\n  }\n\n  const calculator = new Calculator(new MultiplyStrategy())\n  console.log(calculator.calculate(2, 3)) // 6\n  ```\n\n---\n\n5. **代理模式（Proxy Pattern）**\n\n- **概念** ：通过代理控制对对象的访问。\n- **应用场景** ：\n  - API 请求的缓存代理。\n  - 数据过滤或格式化。\n- **示例** ：\n\n  ```javascript\n  const apiProxy = new Proxy(\n    {},\n    {\n      get(target, property) {\n        if (property in target) {\n          return target[property]\n        } else {\n          console.log(`Fetching ${property} from API...`)\n          // 模拟 API 请求\n          return `Data for ${property}`\n        }\n      },\n    }\n  )\n\n  console.log(apiProxy.user) // Fetching user from API...\n  ```\n\n---\n\n6. **装饰器模式（Decorator Pattern）**\n\n- **概念** ：在不修改原始对象的情况下动态扩展功能。\n- **应用场景** ：\n  - 动态扩展类的功能，例如权限控制。\n  - React 的高阶组件（HOC）。\n- **示例** ：\n\n  ```javascript\n  function withLogging(fn) {\n    return function (...args) {\n      console.log(`Calling ${fn.name} with`, args)\n      return fn(...args)\n    }\n  }\n\n  function add(a, b) {\n    return a + b\n  }\n\n  const loggedAdd = withLogging(add)\n  console.log(loggedAdd(2, 3)) // Calling add with [2, 3]\n  ```\n\n---\n\n7. **中介者模式（Mediator Pattern）**\n\n- **概念** ：通过一个中介对象来管理不同对象之间的交互，避免对象之间的直接引用。\n- **应用场景** ：\n  - 模块之间的解耦，例如前端组件通信。\n- **示例** ：\n\n  ```javascript\n  class Mediator {\n    constructor() {\n      this.channels = {}\n    }\n\n    subscribe(channel, fn) {\n      if (!this.channels[channel]) {\n        this.channels[channel] = []\n      }\n      this.channels[channel].push(fn)\n    }\n\n    publish(channel, data) {\n      if (this.channels[channel]) {\n        this.channels[channel].forEach((fn) => fn(data))\n      }\n    }\n  }\n\n  const mediator = new Mediator()\n  mediator.subscribe('event', (data) => console.log(`Received: ${data}`))\n  mediator.publish('event', 'Hello from Mediator!') // Received: Hello from Mediator!\n  ```\n\n---\n\n8. **命令模式（Command Pattern）**\n\n- **概念** ：将请求封装为对象，以便参数化不同的请求。\n- **应用场景** ：\n  - 撤销与重做功能。\n  - 任务队列管理。\n- **示例** ：\n\n  ```javascript\n  class Command {\n    constructor(execute, undo) {\n      this.execute = execute\n      this.undo = undo\n    }\n  }\n\n  class Light {\n    turnOn() {\n      console.log('Light is ON')\n    }\n\n    turnOff() {\n      console.log('Light is OFF')\n    }\n  }\n\n  const light = new Light()\n  const turnOnCommand = new Command(\n    () => light.turnOn(),\n    () => light.turnOff()\n  )\n  turnOnCommand.execute() // Light is ON\n  turnOnCommand.undo() // Light is OFF\n  ```\n\n---\n\n9. **适配器模式（Adapter Pattern）**\n\n- **概念** ：将一个类的接口转换成另一个接口，以便兼容不同系统。\n- **应用场景** ：\n  - 前端组件库的接口适配。\n  - 数据格式转换。\n- **示例** ：\n\n  ```javascript\n  class OldAPI {\n    getData() {\n      return 'Old API Data'\n    }\n  }\n\n  class NewAPI {\n    fetchData() {\n      return 'New API Data'\n    }\n  }\n\n  class Adapter {\n    constructor(api) {\n      this.api = api\n    }\n\n    getData() {\n      if (this.api instanceof OldAPI) {\n        return this.api.getData()\n      } else if (this.api instanceof NewAPI) {\n        return this.api.fetchData()\n      }\n    }\n  }\n\n  const adapter = new Adapter(new NewAPI())\n  console.log(adapter.getData()) // New API Data\n  ```\n\n---\n\n10. **组合模式（Composite Pattern）**\n\n- **概念** ：将对象组合成树形结构，以表示“部分-整体”的层次结构。\n- **应用场景** ：\n  - UI 组件树。\n  - 文件系统管理。\n- **示例** ：\n\n  ```javascript\n  class Component {\n    constructor(name) {\n      this.name = name\n    }\n\n    display() {\n      console.log(this.name)\n    }\n  }\n\n  class Composite extends Component {\n    constructor(name) {\n      super(name)\n      this.children = []\n    }\n\n    add(child) {\n      this.children.push(child)\n    }\n\n    display() {\n      console.log(this.name)\n      this.children.forEach((child) => child.display())\n    }\n  }\n\n  const root = new Composite('Root')\n  const leaf1 = new Component('Leaf 1')\n  const leaf2 = new Component('Leaf 2')\n\n  root.add(leaf1)\n  root.add(leaf2)\n  root.display()\n  ```\n\n:::\n\n## 观察者模式和发布订阅模式的区别\n\n::: details\n\n**核心区别**\n\n- **观察者模式：** 两个对象，通知者和观察者，直接关联。\n- **发布订阅模式：** 三个对象：事件中心、发布者、订阅者，彼此解耦。\n\n**详细解释**\n\n**1. 定义与结构**\n\n- **观察者模式** ：\n\n  - **简介：** 被观察者（Subject）维护一个观察者列表，状态变化时直接通知观察者（Observers）。\n  - **结构：** 被观察者与观察者直接关联。\n  - **简单类比：** 像微信群，群主发消息直接通知所有成员。\n\n- **发布订阅模式** ：\n  - **简介：** 通过事件中心（Event Bus）解耦发布者和订阅者。发布者将消息交给事件中心，事件中心分发给订阅者。\n  - **结构：** 发布者、事件中心、订阅者三者解耦。\n  - **简单类比：** 像电台广播，听众订阅不同频道，电台播出节目后，只有订阅该频道的听众收到。\n\n**2. 示例代码**\n\n#**观察者模式**\n\n```javascript\nclass Subject {\n  constructor() {\n    this.observers = []\n  }\n\n  addObserver(observer) {\n    this.observers.push(observer)\n  }\n\n  notify(data) {\n    this.observers.forEach((observer) => observer.update(data))\n  }\n}\n\nclass Observer {\n  update(data) {\n    console.log(`Received: ${data}`)\n  }\n}\n\nconst subject = new Subject()\nsubject.addObserver(new Observer())\nsubject.notify('Hello')\n```\n\n#**发布订阅模式**\n\n```javascript\nclass EventBus {\n  constructor() {\n    this.events = {}\n  }\n\n  subscribe(event, callback) {\n    if (!this.events[event]) this.events[event] = []\n    this.events[event].push(callback)\n  }\n\n  publish(event, data) {\n    ;(this.events[event] || []).forEach((callback) => callback(data))\n  }\n}\n\nconst eventBus = new EventBus()\neventBus.subscribe('greet', (data) => console.log(`Received: ${data}`))\neventBus.publish('greet', 'Hello Subscribers!')\n```\n\n**区别总结**\n\n| 特性     | 观察者模式             | 发布订阅模式           |\n| -------- | ---------------------- | ---------------------- |\n| 依赖关系 | 被观察者直接通知观察者 | 发布者与订阅者解耦     |\n| 中介角色 | 无                     | 事件中心               |\n| 适用场景 | 状态变化通知           | 广播消息，模块解耦     |\n| 耦合度   | 高                     | 低                     |\n| 触发方式 | 被观察者主动触发       | 发布者通过事件中心触发 |\n\n**选择建议**\n\n- **观察者模式：** 适合对象依赖明确的场景，如模型与视图同步。\n- **发布订阅模式：** 适合模块解耦的场景，如前端事件总线。\n\n**实践应用**\n\n- **观察者模式：** Vue 2.x 的响应式系统。\n- **发布订阅模式：** Node.js 的 `EventEmitter`、Vue 3.x 的事件总线。\n\n**总结类比帮助记忆**\n\n- **观察者模式：** 直接通知像微信群消息通知所有人。\n- **发布订阅模式：** 广播消息像电台分发到不同订阅频道。\n\n:::\n\n## 后端返回 10w 条数据，前端该如何处理？\n\n::: details\n\n当前如果后端一次返回 10w（100,000）条数据，直接在前端全部加载和渲染会导致内存占用高、页面卡顿、响应缓慢等性能问题。因此，处理这类大数据集时应采用以下策略：\n\n1. 服务器端分页和过滤\n\n- **服务器端分页：** 最理想的方法是让后端只返回当前页面所需的数据，通过接口传递分页参数（例如 page 和 limit）。这样前端只处理少量数据，减轻渲染压力。\n- **服务器端过滤和排序：** 在后端做数据筛选，只返回满足条件的数据，进一步减少前端接收的数据量。\n\n2. 前端虚拟化技术\n\n- **虚拟滚动（Virtual Scrolling）：** 如果一定要在前端加载大数据集，采用虚拟列表技术仅渲染当前可见区域的数据项。例如使用 React 的 [react-window](https://github.com/bvaughn/react-window) 或 [react-virtualized](https://github.com/bvaughn/react-virtualized)；对于 Vue 也有类似的虚拟列表组件。\n- **懒加载（Lazy Loading）：** 仅在用户滚动或交互时动态加载数据，避免一次性加载所有数据。\n\n3. 异步数据处理\n\n- **Web Worker：** 若前端需要对数据进行复杂计算，可以使用 Web Worker 将计算放到后台线程，避免阻塞主线程。\n- **分批渲染：** 将数据分为小批次，逐步渲染到页面上，从而分摊渲染开销，保持 UI 流畅。\n\n4. 数据缓存与状态管理\n\n- **数据缓存：** 对于已加载的数据可以使用内存缓存或者 IndexedDB 缓存，避免重复请求，提升用户体验。\n- **状态管理工具：** 使用 Redux、Vuex 等状态管理工具，结合分页、懒加载策略管理大数据集的状态，避免全量数据占用内存。\n\n总结\n\n最佳实践是尽量避免一次性将所有 10w 条数据传输到前端显示，优先在服务器端进行分页和过滤；如果确实需要在前端处理大量数据，则应采用虚拟化、懒加载和异步处理等技术以保障页面性能和用户体验。\n\n这种综合策略能有效平衡用户体验与数据处理能力，避免前端资源耗尽的问题。\n\n:::\n\n## 一个网页，一开始很流畅，越用越卡顿，你怎么办？\n\n::: details\n\n**1. 内存泄漏检查**\n\n- **表现：** 内存占用持续上升，不释放。\n- **解决方案：**\n  - 使用 Chrome DevTools 的 **Memory 面板** ，录制快照 (Heap Snapshot)，查看 DOM 节点、事件监听器是否未被正确清理。\n  - 确保组件卸载时清除定时器、事件监听 (`removeEventListener`) 和订阅。\n  - 避免闭包导致无法释放变量。\n\n---\n\n**2. 不必要的状态和数据堆积**\n\n- **表现：** 前端状态或数据管理混乱，状态持续增长。\n- **解决方案：**\n  - 检查状态管理工具（Redux、Vuex）中的数据，避免存储大规模冗余数据。\n  - 使用数据分页、懒加载策略，减少前端数据体积。\n  - 清理过期缓存数据，避免 IndexedDB 或 LocalStorage 无限堆积。\n\n---\n\n**3. 节流和防抖优化**\n\n- **表现：** 频繁用户交互导致过多重渲染或计算。\n- **解决方案：**\n  - 使用 `throttle` 和 `debounce` 控制滚动、输入等高频事件。\n  - 框架层面可使用 React.memo、Vue 的 computed 属性等避免不必要渲染。\n\n---\n\n**4. DOM 操作与渲染性能优化**\n\n- **表现：** 频繁重排重绘，页面渲染卡顿。\n- **解决方案：**\n  - 检查是否有频繁的 DOM 操作，优化为批量更新。\n  - 使用虚拟 DOM 或虚拟滚动（virtual scrolling）技术。\n  - 尽量避免触发 Layout Throttle 属性（如 `offsetWidth`、`getBoundingClientRect()`）。\n\n---\n\n**5. 垃圾回收 (GC) 问题**\n\n- **表现：** 短时间内频繁的卡顿现象。\n- **解决方案：**\n  - 检查大对象频繁创建导致的 GC 开销。\n  - 优化对象复用策略，减少不必要的内存分配。\n\n---\n\n**6. 资源管理优化**\n\n- **表现：** 资源加载越来越慢。\n- **解决方案：**\n  - 使用 `IntersectionObserver` 实现懒加载，避免图片和第三方资源过早加载。\n  - 确保 WebSocket 连接、第三方 SDK 及时关闭。\n\n---\n\n**7. 工具与监控**\n\n- **工具：**\n  - **Chrome DevTools：** 内存分析（Heap）、性能分析（Performance）。\n  - **Lighthouse:** 检查性能瓶颈。\n  - **前端监控平台（如 Sentry）：** 收集卡顿和性能数据。\n\n通过系统化分析和优化，逐步解决页面卡顿问题，提升用户体验。\n\n:::\n\n## 一个 web 系统，加载很慢，交给你来优化，你会怎么办？\n\n::: details\n\n**1. 性能分析与瓶颈定位**\n\n- **使用 Chrome DevTools 的 Performance 和 Network 面板** ，分析页面加载的时间消耗，找出以下关键瓶颈：\n  - **白屏时间** （First Paint）过长\n  - **首屏渲染时间** （Largest Contentful Paint, LCP）过慢\n  - **阻塞资源** 导致延迟加载\n  - **API 请求过多或响应时间长**\n\n---\n\n**2. 资源加载优化**\n\n- **减少 HTTP 请求数量：**\n  - 合并 CSS、JS 文件，或者采用 Tree Shaking 去掉无用代码。\n  - 使用雪碧图（Sprites）处理小图片，或者直接改用 SVG。\n- **压缩与优化资源：**\n  - 压缩图片（使用 WebP），优化视频大小。\n  - 压缩 JS、CSS、HTML 文件，开启 Gzip 或 Brotli 压缩。\n- **Lazy Loading：**\n  - 延迟加载图片和视频，使用 `loading=\"lazy\"` 属性。\n  - 采用懒加载策略来加载非首屏模块。\n\n---\n\n**3. 静态资源缓存**\n\n- **启用浏览器缓存：**\n  设置 `Cache-Control`、`ETag` 等响应头，缓存静态资源。\n\n- **使用 CDN：**\n  静态资源分发到 CDN 节点，减少服务器负载。\n\n---\n\n**4. 网络传输优化**\n\n- **启用 HTTP/2 或 HTTP/3:** 并行加载资源，降低传输延迟。\n- **减少跨域请求:** 优化 API 接口分布，避免预检请求（OPTIONS）。\n- **开启 DNS 预解析:** 提前解析第三方域名。\n\n---\n\n**5. 渲染与框架性能优化**\n\n- **服务端渲染（SSR）/静态生成（SSG）：**\n  减少客户端渲染时间，提升首屏性能。\n- **组件懒加载:** 分离路由和组件，按需加载代码。\n- **虚拟化列表:** 渲染大量数据时使用 `react-window` 或类似方案。\n\n---\n\n**6. 后端与 API 优化**\n\n- **数据库优化：**\n\n  - 建立索引，优化查询。\n  - 数据库结果分页返回。\n\n- **接口合并与优化：**\n  减少多次 API 调用，使用 GraphQL 或批量 API。\n\n- **缓存策略：**\n  使用 Redis 等缓存热点数据，减轻数据库查询压力。\n\n---\n\n**7. 用户体验提升**\n\n- **骨架屏:** 在加载内容前显示占位图，减少白屏时间。\n- **Loading 动画:** 提升用户感知体验。\n\n---\n\n**8. 监控与持续优化**\n\n- **引入性能监控工具:**\n  - Lighthouse 进行性能分析。\n  - Sentry 捕获性能问题。\n  - Web Vitals（FCP、LCP、CLS）实时监控。\n\n通过系统化分析和持续优化，可以显著提升 Web 系统的加载性能，带来更流畅的用户体验。\n\n:::\n\n## 你知道哪些前端或 JS 工具链？它们分别什么作用？\n\n::: details\n\n前端和 JavaScript 工具链中包含构建、打包、编译、优化等工具，它们为前端开发提供高效的开发和生产环境支持。以下是一些常见工具及其作用：\n\n**1. 构建与打包工具**\n\n**Webpack**\n\n- **作用：** 模块打包工具，支持各种静态资源（JS、CSS、图片等）的处理。\n- **特点：** 插件和 Loader 丰富，适用于大型复杂项目。\n- **场景：** 传统企业项目、需要自定义复杂配置的大型项目。\n\n**Vite**\n\n- **作用：** 现代前端开发工具，基于原生 ES 模块，提供极速开发服务器。\n- **特点：** 开发阶段几乎无需打包，HMR 快速；生产环境基于 Rollup 打包。\n- **场景：** 适合 Vue、React 等现代框架项目。\n\n**Rollup**\n\n- **作用：** 模块打包工具，擅长打包库和工具类代码。\n- **特点：** 输出体积小、支持 ES 模块优化。\n- **场景：** 用于打包 JS 库，如工具函数库。\n\n---\n\n**2. 编译与转译工具**\n\n**Babel**\n\n- **作用：** 将现代 JavaScript 转译为兼容旧浏览器的代码。\n- **特点：** 支持最新 JS 特性的编译，如 ES6、TypeScript。\n- **场景：** 需要兼容低版本浏览器的项目。\n\n**SWC (Speedy Web Compiler)**\n\n- **作用：** 超高速 JavaScript 和 TypeScript 编译器。\n- **特点：** 性能比 Babel 高出数倍，基于 Rust 编写。\n- **场景：** 追求编译速度的项目，如大型 React 应用。\n\n**esbuild**\n\n- **作用：** 超快速构建工具，支持打包与编译。\n- **特点：** 性能极高，支持 TypeScript 和 JSX 转译。\n- **场景：** 极简配置、需要高性能构建的项目。\n\n---\n\n**3. 包管理工具**\n\n**npm (Node Package Manager)**\n\n- **作用：** 管理项目依赖和包。\n- **特点：** 官方 Node.js 包管理工具。\n\n**Yarn**\n\n- **作用：** 更高效、更安全的包管理工具。\n- **特点：** 并行安装依赖，比 npm 更快。\n\n**pnpm**\n\n- **作用：** 高性能包管理工具。\n- **特点：** 去重依赖管理，占用磁盘空间少。\n\n---\n\n**4. 静态代码检查与格式化**\n\n**ESLint**\n\n- **作用：** 检测和规范 JavaScript 代码风格。\n\n**Prettier**\n\n- **作用：** 自动格式化代码，保持一致的代码风格。\n\n---\n\n**5. 任务自动化工具**\n\n**Gulp**\n\n- **作用：** 自动化任务运行器，用于构建流程管理（如压缩、编译等）。\n- **特点：** 基于流的构建方式。\n\n**Parcel**\n\n- **作用：** 零配置的打包工具。\n- **特点：** 自动处理依赖关系，适合快速开发原型。\n\n---\n\n**6. 测试工具**\n\n**Jest**\n\n- **作用：** JavaScript 单元测试框架。\n\n**Cypress**\n\n- **作用：** 前端端到端测试工具。\n\n---\n\n**总结**\n\n选择工具需要结合项目规模、性能要求和团队技术栈，例如：\n\n- **快速开发：** 选择 Vite + esbuild。\n- **大型复杂项目：** 使用 Webpack + Babel。\n- **库开发：** Rollup 是不错的选择。\n- **追求编译性能：** 考虑 SWC 或 esbuild。\n\n:::\n"
  },
  {
    "path": "docs/third-exam/leader-test.md",
    "content": "# 前端 Leader 面试\n\n二面结束以后，前端团队 Leader 面试，不再关注技术细节，更关注项目组织和设计能力。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 浏览器从输入 url 到显示网页的全过程\n\n::: tip\n这个问题在任何面试环节都有可能被考察，要提前准备。\n:::\n\n这个问题的答案内容比较多，回答的时候不一定要答的多细致，但一定不要错过关键步骤。\n\n关键步骤如下\n\n::: details\n\n- DNS 解析出 IP 地址\n- 建立 TCP 连接\n- 客户端发出 HTTP 请求\n- 服务端响应 HTTP 请求\n- 浏览器解析 HTML CSS\n- 渲染 DOM\n- 执行 JS 代码，可能会 ajax 加载内容，再次渲染 DOM\n- 加载媒体资源\n- 浏览器缓存机制\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/6905931622374342670\n- https://zhuanlan.zhihu.com/p/133906695\n- https://juejin.cn/post/6844904194801926157\n\n:::\n\n## 从 0 搭建一个前端项目，需要考虑哪些方面\n\n现在创建一个 Vue React 一般都是拿脚手架 cli 一键生成项目，这是最基础的。\n\n在实际工作中还需要考虑更多的内容。参考答案如下\n\n::: details\n\n- 代码仓库，发布到哪个 npm 仓库（如有需要）\n- 技术选型 Vue React 等\n- 代码目录规范\n- 打包构建 webpack 等，做打包优化\n- eslint prettier commit-lint\n- husky pre-commit\n- 单元测试 + 集成测试\n- CI/CD 流程，自动测试，自动发布测试环境，自动部署\n- 开发环境，预发布环境\n- 开发文档，研发规范\n\n:::\n\n## 如何实现 ajax 并发请求控制？\n\n现有 30 个异步请求需要发送，但由于某些原因，我们必须将同一时刻并发请求数量控制在 5 个以内，同时还要尽可能快速的拿到响应结果。应该怎么做？\n\n解题思路\n\n::: details\n\n- Promise.all 是需要固定数量的，并不适用这个题目的场景\n- 可使用 Promise + 递归调用来实现，一个请求完成后继续下一个\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/6916317088521027598\n- https://juejin.cn/post/7004257642130472996\n\n:::\n\nPS. 在一般的 web 项目中用不到这个需求，因为浏览器会自带并发请求数量的控制\n\n::: details\n\n- https://juejin.cn/post/7135687874250768398\n- https://blog.csdn.net/qq_56392992/article/details/135631680\n\n:::\n\n## React 和 Vue 有什么区别？更擅长哪一个？\n\n主要的区别\n\n::: details\n\n- Vue 默认使用 Vue template 模板语法（也支持 JSX）；React 默认使用 JSX 语法；\n- Vue 对初学者更友好，因为它提供了更多语法糖；而 React 需要良好的 JS 基础；\n- React 函数组件对 TS 的支持更加友好，因为它本身就是个 TS 函数；Vue 的 setup script 是它的自定义语法；\n- React 使用 Hooks ，有调用顺序、闭包陷阱等心智负担；Vue3 使用 composables 组合式 API ，心智负担少；\n- React 使用 state ，Vue3 使用 ref ，后者一直有 `.value` 的心智负担\n- 服务端组件方面，React 技术栈的 Next.js 和 Remix 做的更好一些；Vue 技术栈的 Nuxt.js 也在发展之中；\n\n:::\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7344536653463207973\n- https://juejin.cn/post/7347300843001462793\n\n:::\n\n这两个框架/lib 虽然有些区别，但在国内的实际工作中使用都没问题，擅长哪个就用哪个。\n\n但，你如果两个都熟悉，能让你增加很多面试机会，React 和 Vue 的都可以去面试。\n\n## 如何做好技术选型？\n\n当在工作中选择一个 语言/框架/工具 时，需要考虑什么？\n\n参考答案\n\n::: details\n\n- 社区热门程度，搜索引擎和 StackOverflow 上能搜出多少相关资料？\n- 创办时间和发展时间，不要用太新的技术，首先要求稳\n- 看使用人数，参考 GitHub star 数量和 npm 下载量\n- 看社区生态的完善程度，第三方的 UI 、组件、插件等，都是否完善\n- 团队成员的学习成本 —— 这一点很重要，很多人会忽略\n\n:::\n\n另，回答这个问题的时候，尽量举一个你实际工作中的例子，会更有说服力。\n\n## 如何理解技术方案设计？是否做过技术方案设计？\n\n所谓技术方案设计，就是把你要开发的内容，先写个文档或 PPT 说明一下自己要怎么开发，并和团队成员讨论一下。\n\n如果就是简单的功能或 bug ，10 几分钟就能搞定的，那不用技术方案设计。但复杂的工作，技术方案设计是很有必要的。因为\n\n::: details\n\n- 如果你真的成竹在胸，觉得它很简单，那你写个技术方案设计的文档应该也花不了多少时间，可能 1h 就写完了，项目不会因此而延期的。\n- 如果你憋半天写不出一篇技术方案设计文档，那你开发的时候就能很顺利？不可能的。它正好能验证你是不是眼高手低。\n- 技术方案评审，多人参加，会更容发现一些：功能重复、性能瓶颈、安全隐患等。\n\n:::\n\n技术方案设计一般包含如下部分\n\n::: details\n\n- state 数据结构，如会存储在 vuex 或 redux 中\n- 组件 UI 结构，嵌套关系，属性如何传递\n- 会用到哪些服务端的 API ，哪些是现有的，哪些需要新开发\n- 如有复杂逻辑，说明计算过程，时间复杂度\n- 是否有性能隐患？\n- 是否有安全隐患？\n\n:::\n\n## 线上出了严重 bug 你该如何解决？\n\n正确的处理步骤\n\n::: details\n\n- **回滚**，及时止损 —— 这一步最重要，很多人不知道这一步！！！\n- 通知项目组成员，看谁最近有过上线？—— 线上 bug 一般是最近一次上线导致的\n- 在本地或测试环境浮现 bug，查找原因\n- 修复，测试，重新上线\n- 开**复盘**会议，以后如何规避此类问题 —— 复盘会议，也是很多人不知道的\n\n:::\n\n另，如果你项目没有监控报警的话，最好加一个，这样线上有 bug 会及时报警。\n\n## 你参与的项目，研发流程是怎样的？\n\n如果是新项目研发，会采用传统的瀑布流形式\n\n::: details\n\n- 项目立项\n- 确认需求，编写需求文档，UI 设计图\n- 制定技术方案\n- 制定研发计划\n- 开发\n- 多端联调\n- 测试，修复 bug\n- 上线\n- 总结\n\n![](../imgs/pm-flow.jpeg)\n\n:::\n\n如果是已有项目的维护和升级，一般采用敏捷开发方式\n\n::: details\n\n- 项目有一个统一的任务列表，分优先级排列\n- 开发人员领取一个任务，负责设计、开发、提测等流程，每一步都会在任务看板中显示步骤和进展\n- 待这个任务上线以后，再领取下一个任务\n\n![](../imgs/pm-kanban.png)\n\n:::\n\n## 你如何保障代码质量？\n\n代码质量和系统稳定性有直接关系，保障代码质量也是高级程序员的必备技能。\n\n参考答案\n\n::: details\n\n- 配置统一的 eslint 和 prettier 规则，规范代码格式\n- 每次代码合并，都进行 code review ，外加每周一次团队 code review\n- 编写单元测试，提交 commit 时自动触发单元测试\n- 使用 Sentry 等平台进行线上错误报警，并及时修复问题\n\n:::\n\n## 是否写过单元测试和 e2e 测试？\n\n单元测试，是针对某个小型单元（组件，函数），常见的工具 Jest Vitest\n\ne2e 端到端测试，是针对一个系统 UI 和行为进行测试，常见的工具 Cypress Puppeteer\n\n参考资料\n\n::: details\n\n- https://juejin.cn/post/7123869512327233550\n- https://juejin.cn/post/6896890664726822920\n\n:::\n\n## 自定义 DSL 流程图\n\n请自定义 XML 来描述这个流程图\n\n![](../imgs/flow-chart.png)\n\n参考答案\n\n::: details\n\n```xml\n<chart>\n    <start-end id=\"start\">开始</start-end>\n    <flow id=\"flow1\">流程1</flow>\n    <judge id=\"judge1\">评审</judge>\n    <flow id=\"flow2\">流程2</flow>\n    <start-end id=\"end\">结束</start-end>\n    <arrow from=\"start\" to=\"flow1\"></arrow>\n    <arrow from=\"flow1\" to=\"judge1\"></arrow>\n    <arrow from=\"judge1\" to=\"flow2\">Y</arrow>\n    <arrow from=\"judge1\" to=\"end\">N</arrow>\n    <arrow from=\"flow2\" to=\"end\"></arrow>\n</chart>\n<!-- 另，每个节点还可以加上 x y 的定位信息，尺寸，边框、颜色等 -->\n```\n\n:::\n\n## 最近在看什么书？或者学什么新技术吗？\n\n如果你的简历写了自我评价，且在自我评价中写了“热爱技术” “持续学习”等描述，很可能会被问到这个问题。当然，你不写也不一定就不会被问到，还是提前准备好吧。\n\n现在和前端相关的一些新技术，有如下方向\n\n- Node 全栈，服务端渲染，如 Next.js 框架，这两年发展很迅速\n- AI 相关的产品和工具，如各种 AI 聊天、AI 写作，还有 Copilot Cursor 等 AI 工具\n\n也可以从你感兴趣的其他领域去说，只要能自圆其说即可。\n\n## 你的缺点是什么？\n\n注意，这是个坑！不要傻乎乎的真把自己的缺点说出来，暴露给别人看。\n\n正确的方式是：说缺点，也要说自己的补救措施，最终体现自己是一个爱学习、要求进步的人。\n\n推荐的表达方式\n\n::: details\n\n- 我觉得自己目前在 xxx 方面还有所欠缺\n- 但我已经开始通过 xxx 学习这方面的知识了，计划 xxx 天以后即可完成\n\n:::\n"
  },
  {
    "path": "docs/third-exam/project.md",
    "content": "# 项目难点/成绩\n\n面试必考环节，现在应届生都需要有一定的项目经验，实习或者个人项目。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 项目介绍\n\n> 恨不得一口气把所有项目细节全部说完。\n\n### 如何选择项目\n\n- 选择一个最能代表你综合技术能力的项目，它就是你的代表作，你的职场脸面\n- 选择贴合当前你面试公司业务的项目，更能激发面试官的兴趣\n- 不一定是最新做的，1-2 年之内的都可以（2 年前的就太久了）\n- 如果是职场新人，尽量选择大型项目，哪怕自己参与了一小部分\n- 如果是职场“老人”，尽量选择自己独立负责的项目，体现自己独挡一面的能力\n\n### 如何介绍\n\n按照如下模板，用自然平和的预期，在 90s 之内说完 —— 是的，别超过 2min\n\n- 项目的背景，名称，服务对象，主要功能和流程\n- 你在这个项目中的角色和职责，做了哪写主要的工作，取得了哪写成就（简单说，别深入细节）\n- 项目的技术栈：框架 + 重要的第三方库或服务\n\n不要擅自深入细节。如果你非常想表达技术细节，要先把上述内容讲完，然后问面试官一句：`这就是我的项目，你看我表达清楚了吗？其实还有一些技术细节，如果你有兴趣的话，我想再说说。`\n\n面试，是相互的沟通，不是你单方面的倾诉。\n\n### 注意事项\n\n- 对面试官来说，你是个陌生人，你的项目他也不知道。首先，你要让他知道这个项目是个啥！\n- 不要一上来就讲技术，当我连背景、功能都不知道的情况下，技术我不可能听明白\n- 不要着急深入细节，不要想着一口气把能说的全部说完，说太多太快对方根本听不进去\n\n## 项目成绩\n\n> 没啥成绩，我感觉在公司这几年啥也没干\n\n### 项目成绩是什么？\n\n思考这个问题很简单，从你入职到现在，你为项目/公司贡献了哪些东西？主要的东西，太细节的就忽略了。例如\n\n- 开发了 a b c 几个项目/功能\n- 对 xxx 模块进行优化\n- 攻坚 xxx 难题\n- 获得过 xxx 奖励\n\n公司每月给你开那么多工资，你怎么可能没有成绩呢？公司不养闲人。你认真总结一下，内容肯定很多。\n\n### 如何表达\n\n- 概括说出你所有的成绩\n- 找一个最具有代表性的，详细说\n  - 背景，需求\n  - 遇到的问题\n  - 解决方案，技术方案\n  - 结果\n- 最后总结：自己是个有价值的人\n\n### 注意事项\n\n- 不要下意识的以为自己没有成绩，每个人都有自己的价值\n- 这个问题更多的是考验你的总结和表达能力，并不是非得说出一些高大上的成绩\n- 日常工作中要写日报、周报、月报，多记录，就便于最后整理输出\n\n## 项目难点\n\n> 没啥难点，都是增删改查\n\n### 项目难点是什么？\n\n任何让你工作不顺利的事情，都可以总结为项目难点。例如\n\n- 遇到一个 bug 加班到深夜才排查出来\n- 一个功能非常复杂，两周才开发完\n- 一个项目/模块，需要开会召集各个部门的人，多次协调方案才最终通过\n\n### 如何表达\n\n推荐使用 `STAR` 模型：\n\n- Situation 背景和需求\n- Task 任务和目标\n- Action 技术方案\n- Result 结果\n\n面试之前建议准备 2 个项目难点，并用 `STAR` 模型写出大纲。\n\n### 注意事项\n\n- 人，对于做过的事情，都会觉得很简单，不要被这种本能所误导\n- 如果是职场新人，多从具体的技术方向寻找难点\n- 如果是职场“老人”，多从宏观的项目、架构、流程、解决方案这个层面来总结\n"
  },
  {
    "path": "docs/third-exam/scene.md",
    "content": "# 场景题\n\n## 如何设计实现一个准确的前端倒计时\n\n这个问题的核心是：**单纯用 `setInterval` 倒计时是不准时不可靠的**。`setInterval(fn, 1000)` 并不保证每 1000ms 准时执行一次。\n\nJS 是单线程的，当遇到大量计算、页面渲染、长任务等，`setInterval` 会被延迟执行。页面切到后台，定时器会被浏览器降频。本该 1 秒减一次，结果 1.2 秒甚至 2 秒才执行一次 → 倒计时变慢。\n\n设计要点：\n\n- 计时要以时间戳为基准（使用 `Date.now` 计时），而不是递减秒数\n- `setInterval` 只作为刷新工具。\n\n代码示例\n\n```js\nconst endTime = Date.now() + 60 * 1000 // 1分钟倒计时\n\nconst timer = setInterval(() => {\n  const now = Date.now()\n  const remain = endTime - now\n\n  if (remain <= 0) {\n    clearInterval(timer)\n    console.log('倒计时结束')\n    return\n  }\n\n  console.log(Math.floor(remain / 1000) + '秒')\n}, 1000)\n```\n\n总之，一个准确的前端倒计时应该以**时间戳差值**为核心，而不是依赖 `setInterval` 的次数；定时器只负责刷新 UI，每次通过 `目标时间 - 当前时间` 重新计算剩余时间，才能保证在卡顿、切后台等场景下依然准确。\n\n## 如何设计实现一个精准的支付秒杀倒计时\n\n这个问题的核心是：**前端倒计时必须和服务器时间一致，不能靠本地时间瞎算。**\n\n设计要点\n\n- 以服务器时间为准。禁止用 `new Date()` 直接作为倒计时依据（用户电脑时间不准）。必须先获取一次服务端当前时间。\n- 只计算时间差，不依赖本地绝对时间。计算公式 `剩余时间 = 活动开始时间 - 服务器时间`\n- 防止前端篡改计时，即真正是否可支付由后端控制，前端倒计时只是展示。篡改了也支付不了。\n\n代码示例\n\n```js\nconst diff = serverTime - Date.now()\n\nsetInterval(() => {\n  const remain = startTime - (Date.now() + diff)\n  updateUI(remain)\n}, 1000)\n```\n\n## 一个 Web 管理系统，使用越来越慢，如何排查\n\n这个问题的核心是：**慢在哪里，要先定位瓶颈，再针对性优化**。一般分网络、前端、后端三个方向进行定位。\n\n先定位问题，用 Chrome DevTools：\n\n- **Network**：看接口是否变慢（TTFB、响应时间）\n- **Performance**：看是否卡在 JS 执行或渲染\n- **Memory**：是否有内存泄漏（页面越用越卡）\n\n前端常见的问题有\n\n- 页面组件越来越多，**重复渲染 / 状态管理混乱**\n- **大列表一次性渲染**（上千条数据）\n- 事件监听未释放，导致**内存泄漏**\n- 打包体积变大，首屏加载慢\n\n对应的解决方案有：\n\n- 虚拟列表（只渲染可视区域）\n- 减少不必要的 re-render（memo、拆组件）\n- 检查未销毁的定时器、监听器\n- 按需加载（懒加载模块）\n\n接口慢常见的问题有\n\n- 接口响应时间变长\n- 一次请求返回数据过多\n- 串行请求过多\n\n对应的解决方案有\n\n- 分页 / 按需加载数据\n- 合并接口 or 并行请求\n- 开启 gzip / CDN / 缓存\n\n## 后端接口返回几万条数据 前端表格如何去展示处理\n\n这个问题考察的是：**大数据量渲染性能 + 用户体验 + 架构设计能力**。\n\n设计的关键点是\n\n- 不一次性渲染几万条数据（会卡死浏览器）\n- 分批加载 + 按需渲染\n- 保证滚动和操作流畅\n\n解决方案有\n\n- 后端分页返回（如果可以的话，但面试时一般规定后端一次性返回）\n- 前端使用虚拟表格，每次只渲染几十个 DOM\n- 如有数据处理，使用 web worker 解决，防止阻塞主线程\n- 减少 DOM 嵌套和复杂度\n\n## H5 瀑布流展示商品信息，低端安卓机和网络不稳定，如何优化？\n\n针对低端安卓和弱网用户，可以从 **图片压缩 + 懒加载 + 虚拟列表 + 降级策略 + 容错体验** 入手，减少资源体积、降低渲染压力，保证页面能“快加载、不白屏、可用性优先”。\n\n图片资源优化（关键）\n\n- 使用 **WebP / AVIF**，多尺寸图片（srcset），低端机优先加载小图\n- 首屏用 **低清图占位（LQIP / blur）** ，滚动再加载高清图\n- 图片压缩 + CDN\n- 避免一次性加载大量图片\n\n网络加载优化\n\n- **懒加载（IntersectionObserver）** ，只加载可视区域图片\n- 分批请求（分页 / 分段加载），不要一次拉全量数据\n- 请求失败自动重试 + 超时兜底\n- 弱网模式：降低图片质量或数量\n\n网页渲染优化\n\n- 使用 **虚拟列表 / 虚拟瀑布流**，只渲染屏幕内的 DOM\n- 避免频繁重排重绘（少用复杂阴影、动画）\n- 使用 `transform`、`opacity` 做动画，避免 `top/left`\n\n交互体验优化\n\n- 骨架屏 / loading 占位，避免白屏\n- 图片加载失败显示默认图\n- 滚动时不阻塞主线程（避免大 JS 计算）\n\n容错和降级方案\n\n- 低端机或弱网：自动切换 **简化模式（少图 / 小图 / 低清图）**\n- 关闭复杂动画、特效\n- 监控卡顿和加载失败（埋点）\n\n## 设计一个“单选框组件”，选项里面可能是图片、文字等，该如何设计。\n\n如果只包含图片、文本这两个，是比较好设计的，做 if-else 判断显示即可。但如果有其他自定义类型，就需要用到 `<slot>`\n\n```html\n<template>\n  <RadioGroup v-model=\"value\">\n    <!-- 文本选项 -->\n    <RadioItem value=\"text\">\n      <span>文本选项</span>\n    </RadioItem>\n\n    <!-- 图片选项 -->\n    <RadioItem value=\"image\">\n      <img src=\"https://via.placeholder.com/80\" />\n      <p>图片选项</p>\n    </RadioItem>\n\n    <!-- 自定义 slot（复杂内容） -->\n    <RadioItem value=\"custom\">\n      <div>\n        <h3>自定义内容</h3>\n        <p>可以放任意组件</p>\n        <button>按钮</button>\n      </div>\n    </RadioItem>\n  </RadioGroup>\n</template>\n```\n\n定义两个组件 `RadioGroup` 和 `RadioItem` ，`RadioGroup` 管理选中的数据\n\n```html\n<template>\n  <div class=\"radio-group\">\n    <slot />\n  </div>\n</template>\n\n<script setup>\n  import { provide } from 'vue'\n\n  const props = defineProps({\n    modelValue: [String, Number],\n  })\n  const emit = defineEmits(['update:modelValue'])\n\n  provide('radioValue', props)\n  provide('radioChange', (val) => {\n    emit('update:modelValue', val)\n  })\n</script>\n```\n\n`RadioItem` 负责各类数据的 UI 渲染，监听 change 事件来修改 value\n\n```\n<template>\n  <div\n    class=\"radio-item\"\n    :class=\"{ active: isChecked }\"\n    role=\"radio\"\n    :aria-checked=\"isChecked\"\n    @click=\"select\"\n  >\n    <slot />\n  </div>\n</template>\n\n<script setup>\nimport { inject, computed } from \"vue\";\n\nconst props = defineProps({\n  value: [String, Number]\n});\n\nconst radioValue = inject(\"radioValue\");\nconst radioChange = inject(\"radioChange\");\n\nconst isChecked = computed(() => radioValue.modelValue === props.value);\n\nconst select = () => {\n  radioChange(props.value);\n};\n</script>\n```\n\n把单选框设计成 **RadioGroup + RadioItem 的组合组件**，用数据驱动选项，通过 slot 支持图片和文字等自定义内容，使用受控模式管理选中状态，并兼顾可访问性和性能。\n\n## 如何排查网页白屏问题\n\n白屏问题本质：**页面没渲染出来或 JS 报错中断了渲染**。排查要有顺序，从外到内、从简单到复杂。\n\n先快速定位问题方向\n\n- 看有没有 JS 报错（语法错误、接口报错、资源 404）。\n- 看 HTML、JS、CSS 文件是否加载成功？核心接口是否返回 500 / 超时？\n- 看 DOM 是否渲染出来？还是 body 是空的？\n\n如果是 JS 报错了，就需要\n\n- try/catch 关键逻辑\n- 接入全局错误监控（`window.onerror`、`unhandledrejection`）\n\n如果是 HTML、JS、CSS 文件加载失败，就检查 CDN 是否配置错误？这一般不会是程序问题。\n\n如果核心接口返回 500 / 超时，那就在前端做容错方案，例如展示“获取数据失败，请刷新重试”\n\n还可以加 ErrorBoundary 容错组件，来最大范围的概括各类组件渲染报错，给用户提示友好信息。\n\n总之，先看控制台和网络请求，确认是 JS 报错、资源加载失败还是接口问题；再定位到具体代码。工程上通过错误监控、兜底 UI 和自动化监控来预防和快速发现白屏问题。\n\n## 让你启动一个新项目，你将如何开始这个项目？\n\n第一，要明确需求，先和产品、设计、后端对齐，搞清楚几个核心问题：\n\n- **做什么**：后台管理系统、C端页面、还是小程序？\n- **面向谁**：用户量多大、对性能/SEO 有没有要求？\n- **工期多久**：赶进度就用成熟方案，不搞花活\n\n第二，技术选型，要按公司团队情况选择，不要盲目求新\n\n- **语言** JS TS\n- **框架** Vue React Nextjs 等\n- **UI 组件库** AntD Element 等\n- **构建工具** Vite\n\n第三，工程化搭建，环境搭好，后续才能高效协作\n\n- 代码规范 ESLint + Prettier，保证风格统一\n- 配置 CI/CD 流程（GitHub Actions / Jenkins）。\n- 配置环境变量、打包优化（Tree Shaking、Code Splitting）和性能监控（Lighthouse / Sentry）。\n\n第四，架构设计\n\n- 代码目录结构\n- Vuex Redux 等前端状态数据结构\n- API 接口规范\n- 请求封装：Axios 统一封装，处理 token、错误码、loading\n- 权限控制：路由守卫 + 按钮级权限指令提前想好\n\n```\nsrc/\n├── api/        # 所有接口，按模块拆分\n├── components/ # 通用组件（Button、Modal...）\n├── views/      # 页面级组件\n├── hooks/      # 复用逻辑（useUser、useTable...）\n├── stores/     # 状态管理（Pinia / Zustand）\n├── router/     # 路由配置 + 权限守卫\n└── utils/      # 工具函数\n```\n\n## 如何实现前端线上监控 前端线上报错如何排查\n\n三个主要步骤：采集、上报、分析\n\n采集什么？\n\n- JS错误：`window.onerror`、`try-catch`捕获\n- 资源加载失败：`window.addEventListener('error')`监听资源\n- 接口请求：重写`XMLHttpRequest`和`fetch`\n- 性能数据：`Performance API`获取FP、FCP、LCP等\n- 用户行为：点击路径、路由变化\n\n怎么上报？\n\n- 封装成固定数据结构（错误信息、环境、用户、时间戳）\n- 使用`Navigator.sendBeacon`（页面关闭时也能发）\n- 图片打点（`new Image().src`）做简单上报\n- 批量压缩上报，减少请求次数\n\n数据存储和分析\n\n- 后端可用 **ElasticSearch/Kafka/数据库** 保存日志\n- 提供 **错误聚合、告警、统计报表**，快速定位问题\n\n前端问题如何排查\n\n- 前端报错日志分类、聚合，找出发生概率比较大的\n- 使用 source map 将压缩代码映射回原始源代码\n- 根据堆栈和出错代码判断逻辑或环境问题\n- 在本地开发环境复现问题，并修复问题\n\n## 一百万个人同时抢一个商品，如何判断谁是第一个？\n\n这个问题的关键不在于前端，而在于后端，前端只是发起请求和展示结果。所以这个问题一般会考察全栈岗位或者高级前端岗位，需要有一定后段能力的。\n\n后端实现这个功能，需要满足两点：\n\n- 支持高并发，因为有一百万人同时抢购\n- 要能准确识别第一个人，响应要快\n\n常见的解决方案是 **后端原子操作** ，这个方案最简单可靠，容易支持高并发\n\n- 所有请求打到后端\n- 用 Redis / 数据库做原子判断\n\n```\nSETNX product_lock userId\n```\n\n第一个写入的 user 就是赢家，其他人直接返回失败。前端只负责发起请求和展示结果。\n"
  },
  {
    "path": "docs/third-exam/system-design.md",
    "content": "# 系统设计\n\n综合考察一个候选人的技术方案设计能力，看能否培养为项目负责人，尤其工作经验 5 年以上的。\n\n## 使用 Vue/React 设计 TodoList\n\n明确功能，设计数据结构和组件结构\n\n## 使用 Vue/React 设计一个购物车\n\n明确功能，设计数据结构和组件结构\n\n## 使用 Vue/React 设计一个集联选择器\n\n如集联选择省、市、区县\n\n## 为 input 设计一个 autoComplete\n\n## 使用 JS + HTML 设计 撤销/重做 undo/redo\n\n## 开发一个 H5 抽奖页，需要后端提供哪些接口\n\n假如你刚接手一个抽奖 H5 运营页（微信朋友圈分享的），你应该和服务端确认哪些 API 接口？\n\n![](../imgs/h5-lottery.png)\n\n参考答案\n\n::: details\n\n- 用户身份的验证方式\n- 是否已经抽过（直接显示结果）\n- 点击，触发抽奖\n- 统计埋点（PV UV 自定义事件 性能 报错 分享行为等）\n\n:::\n\n## 设计一个前端统计 SDK\n\n## 全栈：设计一个新闻 Feed 小程序/H5\n\n考虑服务端接口，考虑性能等...\n\n## 全栈：设计一个博客网站\n\n## 全栈：设计一个购物网站\n\n## 全栈：设计一个多人聊天室\n"
  },
  {
    "path": "docs/written-exam/JS-reading.md",
    "content": "# JS 读代码\n\n读懂面试的代码，才能读懂工作中的代码。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## JS 编译\n\n以下代码，执行结果是什么？\n\n```js\nvar func = 1\nfunction func() {}\nconsole.log(func + func)\n```\n\n答案\n\n::: details\n\n```\n2\n```\n\n这题考察的 GO，也就是全局的预编译：\n\n1. 创建 GO 对象\n2. 找变量声明，将变量声明作为 key，值赋为 undefined\n3. 找函数声明，将函数名作为 GO 对象的key，值赋为函数体\n\n编译阶段：创建 GO 对象后，func 作为 key，值为 undefined，然后 func 变成了 函数体，所以在编译结束时，func 还是一个 function\n\n运行阶段：func 被赋值为 1，所以 func + func 就是 2\n\n:::\n\n## JS 引用类型\n\n以下代码，执行结果是什么？\n\n```js\nlet obj1 = { x: 1 }\nlet obj2 = obj1\nobj2.y = 2\nobj2 = { y: 20 }\nconsole.log('obj1', obj1)\n```\n\n答案\n\n::: details\n\n```\nobj1 { x: 1, y: 2 }\n```\n\nECMAScript 变量可能包含两种不同类型的值：基本类型值和引用类型值。\n\n基本类型包括：Undefined、Null、Boolean、Number、String、Symbol\n引用类型包括：Object、Array\n\n引用类型的值是保存在内存中的对象。与其他语言不同，JavaScript 并不允许直接访问内存中的位置，也就是说不能直接操作对象的内存空间。在操作对象时，实际上是在操作对象的引用而不是实际的对象。\n\n换言之，变量实际保存的是一个指针，这个指针指向放在内存中的对象\n\n当运行 `let obj2 = obj1;` 的时候，实际上是复制了一份指针，而不是复制了一份对象。\n\n![](../imgs/js-reading-value-reference.jpg)\n\n所以 `obj2.y = 2` 能够修改对象的值，`obj1` 能够访问修改后的对象。\n\n运行 `obj2 = { y: 20 };` 时，只是将 obj2 指向了新的对象，`obj1` 还是指向原来的对象。\n:::\n\n## JS parseInt\n\n以下代码，执行结果是什么？\n\n```js\n;['1', '2', '3'].map(parseInt)\n```\n\n答案\n\n::: details\n\n```\n[1, NaN, NaN]\n```\n\n查看 MDN 数组的 [map](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map) 方法，它的参数是一个函数，函数的参数是 `(element, index, array)`，`element` 是数组的元素，`index` 是元素的索引，`array` 是数组本身。\n\n查看 MDN 的 [parseInt](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt) 函数，它的参数是 `(string, radix)`，`string` 是要解析的字符串，`radix` 是 2-36 之间的整数，表示被解析字符串的基数。\n\n所以当遍历的时候，实际效果是：\n\nparseInt('1', 0) // radix 假如指定 0 或未指定，基数将会根据字符串的值进行推算。所以结果为 1。\n\nparseInt('2', 1) // radix 应该是 2-36 之间的整数，此时为 1，无法解析，返回 NaN\n\nparseInt('3', 2) // radix 是 2，表示 2 进制，注意这里不是指将其解析为 2 进制，而是按照 2 进制进行解析，但 3 不是 2 进制里的数字，所以无法解析，返回 NaN\n:::\n\n## JS this 1\n\n以下代码，执行结果是什么？\n\n```js\nconst User = {\n  count: 1,\n  getCount: function () {\n    return this.count\n  },\n}\nconsole.log('a ', User.getCount()) // what?\nconst func = User.getCount\nconsole.log('b', func()) // what?\n```\n\n答案\n\n::: details\n\n```\na 1\nb undefined\n```\n\n本题考察 this 的指向。\n\nthis 是一个指向对象的指针，this 的指向与所在方法的调用位置有关，而与方法的声明位置无关。\n\n作为方法调用时，this 指向调用它所在方法的对象；所以 a 为 1\n\n作为函数调用时，this 指向 window。所以 b 为 undefined.\n\n:::\n\n## JS this 2\n\n以下代码，执行结果是什么？\n\n```js\nconst obj = {\n  f1() {\n    const fn = () => {\n      console.log('this1', this)\n    }\n    fn()\n    fn.call(window)\n  },\n  f2: () => {\n    function fn() {\n      console.log('this2', this)\n    }\n    fn()\n    fn.call(this)\n  },\n}\nobj.f1()\nobj.f2()\n```\n\n答案\n\n::: details\n\n```\nthis1 obj 和 this1 obj\nthis2 window（严格模式下是 undefined） 和 this2 window\n```\n\n箭头函数没有自己的 this 对象\n\n对于普通函数来说，内部的 this 指向函数运行时所在的对象，但是这一点对箭头函数不成立。它没有自己的 this 对象，内部的 this 就是定义时上层作用域中的 this。也就是说，箭头函数内部的 this 指向是固定的，相比之下，普通函数的 this 指向是可变的，比如通过 call 来改变。\n\nobj.f1() 时，fn 是箭头函数，内部的 this 是定义时上层作用域中的 this，也就是 obj。箭头函数修改不了 this，所以 fn.call(window) 不会修改 this 指向。\n\nobj.f2() 时，fn 是普通函数，但 f2 是箭头函数，如果 f2 是普通函数，该方法内部的 this 指向 obj，但是写成箭头函数，this 指向全局对象，这是因为对象不构成单独的作用域，导致箭头函数定义时的作用域就是全局作用域。所以都是 windows（在非严格模式下）。\n\n:::\n\n## JS 自由变量 1\n\n以下代码，执行结果是什么？\n\n```js\nlet i\nfor (i = 1; i <= 3; i++) {\n  setTimeout(function () {\n    console.log(i)\n  }, 0)\n}\n```\n\n答案\n\n::: details\n\n```\n4 4 4\n```\n\ni 是全局变量，用来控制循环。循环调用了 3 次 setTimeout 延迟执行。当 setTimeout 执行的时候，i 已经变成了 4。\n\n此外查看 MDN [setTimeout](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setTimeout)，第二个参数为 delay，表示定时器在执行指定的函数或代码之前应该等待的时间，单位是毫秒。如果省略该参数，则使用值 0，意味着“立即”执行，或者更准确地说，在下一个事件循环执行。所以虽然数值设置为 0，但依然是异步行为。\n:::\n\n## JS 自由变量 2\n\n以下代码，执行结果是什么？\n\n```js\nlet n = 10\nfunction f1() {\n  n++\n  function f2() {\n    function f3() {\n      n++\n    }\n    let n = 20\n    f3()\n    n++\n  }\n  f2()\n  n++\n}\nf1()\nconsole.log('n', n)\n```\n\n答案\n\n::: details\n\n```\nn 12\n```\n\nlet 声明为 JavaScript 新增了块级作用域。所以 f2 内部的 n 是 f2 内部的变量，不会影响外层 n 的变化。\n\n关于 let 声明可以参考 《ECMAScript 6 入门》[let 和 const 命令](https://es6.ruanyifeng.com/#docs/let)\n:::\n\n## JS 闭包 1\n\n以下代码，执行结果是什么？\n\n```js\nconst n = 10\nfunction print() {\n  console.log(n)\n}\n\nfunction f1(fn) {\n  const n = 20\n  fn()\n}\nf1(print)\n```\n\n答案\n\n::: details\n\n```\n10\n```\n\nJavaScript 采用词法作用域(lexical scoping)，也就是静态作用域。换句话说，说函数的作用域在函数定义的时候就决定了。\n\n所以当调用 print 的时候，它会根据定义的位置向外查找变量，也就是 n = 10。\n\n关于词法作用域，参考 [《JavaScript 深入之词法作用域和动态作用域》](https://juejin.cn/post/6844903473012539405)\n:::\n\n## JS 闭包 2\n\n以下代码，执行结果是什么？\n\n```js\nfunction fn() {\n  let num = 10\n  return {\n    set: (n) => (num = n),\n    get: () => num,\n  }\n}\n\nlet num = 20\nconst { get, set } = fn()\nconsole.log('result1: ', get())\nset(100)\nconsole.log('result2: ', num)\n```\n\n答案\n\n::: details\n\n```\n10 20\n```\n\n由于 JavaScript 的闭包特性，函数内部的变量可以持续存在。\n\n所以当调用 `get()` 的时候，可以访问到 fn 函数作用域中的 num，所以 result1 输出 10。\n\nset(100)修改的是 fn 函数作用域中的 num，而不是全局的 num。\n\n所以全局的 num 值，仍然是 20。\n:::\n\n## JS Promise 1\n\n以下代码，执行结果是什么？\n\n```js\nconst promise = new Promise((resolve, reject) => {\n  console.log(1)\n  console.log(2)\n})\npromise.then(() => {\n  console.log(3)\n})\nconsole.log(4)\n```\n\n答案\n\n::: details\n\n```\n1 2 4\n```\n\n当创建新的 Promise 时，executor 函数会立即执行，所以打印了 1 和 2。\n\npromise.then() 注册了一个回调函数，但因为 executor 函数没有调用 resolve 或 reject，所以 Promise 永远处于 pending 状态，回调函数不会被执行，所以不会打印 3。\n\n最后执行 console.log(4)，打印 4。\n\n:::\n\n## JS Promise 2\n\n以下代码，执行结果是什么？\n\n```js\nconst promise = new Promise((resolve, reject) => {\n  console.log(1)\n  setTimeout(() => {\n    console.log('timerStart')\n    resolve('success')\n    console.log('timerEnd')\n  }, 0)\n  console.log(2)\n})\npromise.then((res) => {\n  console.log(res)\n})\nconsole.log(4)\n```\n\n答案\n\n::: details\n\n```\n1\n2\n4\ntimerStart\ntimerEnd\nsuccess\n```\n\n当创建新的 Promise 时，executor 函数会立即执行，所以打印了 1 和 2。\n\n即使 setTimeout 的延时设置为 0，它的回调函数依然是异步执行的，会被放入宏任务队列，要等到当前所有同步代码执行完毕后才会执行。\n\npromise.then() 注册了一个回调函数，\n\n按照代码顺序执行 console.log(4)，打印 4。\n\nsetTimeout 回调开始执行，打印 timerStart。此时调用 resolve 函数，但 Promise.then 回调也是异步的，所以不会立刻调用 then 函数，而是放入微任务队列。代码继续执行，打印 timerEnd。\n\n最后开始执行微任务，调用 then 函数，打印 success。\n\n:::\n\n## JS 异步执行顺序 1\n\n以下代码，执行结果是什么？\n\n```js\nconsole.log('start')\nsetTimeout(() => {\n  console.log('a')\n\n  Promise.resolve().then(() => {\n    console.log('c')\n  })\n})\nPromise.resolve().then(() => {\n  console.log('b')\n\n  setTimeout(() => {\n    console.log('d')\n  })\n})\nconsole.log('end')\n```\n\n答案\n\n::: details\n\n```\nstart end b a c d\n```\n\n本题涉及同步代码、微任务、宏任务的执行顺序。\n\n首先执行同步代码，打印 start 和 end。\n\nsetTimeout 回调函数会被放入宏任务队列，等待同步代码执行完毕后执行。\n\nPromise.resolve().then() 注册了一个回调函数，会被放入微任务队列。\n\n因为微任务优先级高于宏任务，所以 then 回调函数会首先执行，打印 b。回调函数调用了 setTimeout，将函数放入宏任务队列。\n\n然后执行之前的宏任务，打印 a。然后又注册了一个 then 回调函数，会被放入微任务队列。\n\n因为微任务优先级高于宏任务，所以 then 回调函数会首先执行，打印 c。\n\n最后执行宏任务，打印 d。\n\n:::\n\n## JS 异步执行顺序 2\n\n以下代码，执行结果是什么？\n\n```js\nPromise.resolve()\n  .then(() => {\n    console.log(0)\n    return Promise.resolve(4)\n  })\n  .then((res) => {\n    console.log(res)\n  })\n\nPromise.resolve()\n  .then(() => {\n    console.log(1)\n  })\n  .then(() => {\n    console.log(2)\n  })\n  .then(() => {\n    console.log(3)\n  })\n  .then(() => {\n    console.log(5)\n  })\n  .then(() => {\n    console.log(6)\n  })\n```\n\n答案\n\n::: details\n\n```\n0 1 2 3 4 5 6\n```\n\n1. 初始状态：\n\n   - 两个 Promise.resolve() 创建两个立即 resolved 的 Promise\n   - 它们的 .then 回调都被加入到第一轮微任务队列\n\n2. 第一轮微任务：\n   - 执行第一个链的第一个 then：打印 0，返回 Promise.resolve(4)\n   - 执行第二个链的第一个 then：打印 1\n3. 第二轮微任务：\n   - 第二个链的第二个 then 执行：打印 2\n   - 第一个链的第二个 then 暂时不执行，因为它在等待 Promise.resolve(4) 的解析\n4. 第三轮微任务：\n\n   - 第二个链的第三个 then 执行：打印 3\n\n5. 第四轮微任务：\n\n   - Promise.resolve(4) 完成解析\n   - 第一个链的第二个 then 执行：打印 4\n   - 第二个链的第四个 then 执行：打印 5\n\n6. 第五轮微任务：\n   - 第二个链的最后一个 then 执行：打印 6\n\n这道题的难点在于为什么 4 在 3 之后打印，延迟了 2 个微任务才执行。\n\n这是因为等待 Promise.resolve(4) 的解析需要一个微任务（这期间打印了 2），resolve 过程中发现是 Promise（准确的说是 thenable），V8 会进行一个不同处理，将其入列一个新任务，这期间打印了 3，然后在第四轮微任务中，第一个 Promise 打印 4，第 2 个 Promise 打印 5。\n\n拓展阅读：[promise.then 中 return Promise.resolve 后，发生了什么？](https://www.zhihu.com/question/453677175/answer/1834710779)\n:::\n"
  },
  {
    "path": "docs/written-exam/JS-writing.md",
    "content": "# JS 手写代码\n\n程序员最重要的就是动手能力。\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 手写深拷贝\n\n考虑循环引用\n\n::: details 参考答案\n\n简单的深拷贝：\n\n```js\nfunction cloneDeep(source, hash = new WeakMap()) {\n  if (!isObject(source)) return source\n  if (hash.has(source)) return hash.get(source)\n\n  var target = Array.isArray(source) ? [] : {}\n  hash.set(source, target)\n\n  for (var key in source) {\n    if (Object.prototype.hasOwnProperty.call(source, key)) {\n      if (isObject(source[key])) {\n        target[key] = cloneDeep(source[key], hash)\n      } else {\n        target[key] = source[key]\n      }\n    }\n  }\n  return target\n}\n```\n\n考虑更多比如爆栈的情况：\n\n```js\nfunction cloneDeep(x) {\n  const root = {}\n\n  const loopList = [\n    {\n      parent: root,\n      key: undefined,\n      data: x,\n    },\n  ]\n\n  while (loopList.length) {\n    const node = loopList.pop()\n    const parent = node.parent\n    const key = node.key\n    const data = node.data\n\n    let res = parent\n    if (typeof key !== 'undefined') {\n      res = parent[key] = {}\n    }\n\n    for (let k in data) {\n      if (data.hasOwnProperty(k)) {\n        if (typeof data[k] === 'object') {\n          loopList.push({\n            parent: res,\n            key: k,\n            data: data[k],\n          })\n        } else {\n          res[k] = data[k]\n        }\n      }\n    }\n  }\n\n  return root\n}\n```\n\n参考阅读：\n\n- [深拷贝的终极探索（99%的人都不知道）](https://segmentfault.com/a/1190000016672263)\n  :::\n\n## 手写 getType 函数\n\n获取详细的变量类型\n\n::: details 参考答案\n\n```js\nfunction getType(data) {\n  // 获取到 \"[object Type]\"，其中 Type 是 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等。\n  const originType = Object.prototype.toString.call(data)\n  // 可以直接截取第8位和倒数第一位，这样就获得了 Null、Undefined、Array、Function、Error、Boolean、Number、String、Date、RegExp 等\n  const type = originType.slice(8, -1)\n  // 再转小写，得到 null、undefined、array、function 等\n  return type.toLowerCase()\n}\n```\n\n:::\n\n## 手写 class 继承\n\n在某网页中，有三种菜单：button menu，select menu，modal menu。\n\n他们的共同特点：\n\n- 都有 `title` `icon` 属性\n- 都有 `isDisabled` 方法（可直接返回 `false`）\n- 都有 `exec` 方法，执行菜单的逻辑\n\n他们的不同点：\n\n- button menu，执行 `exec` 时打印 `'hello'`\n- select menu，执行 `exec` 时返回一个数组 `['item1', 'item2', 'item3']`\n- modal menu，执行 `exec` 时返回一个 DOM Element `<div>modal</div>`\n\n请用 ES6 语法写出这三种菜单的 class\n\n::: details 参考答案\n\n```js\nclass BaseMenu {\n  constructor(title, icon) {\n    this.title = title\n    this.icon = icon\n  }\n  isDisabled() {\n    return false\n  }\n}\n\nclass ButtonMenu extends BaseMenu {\n  constructor(title, icon) {\n    super(title, icon)\n  }\n  exec() {\n    console.log('hello')\n  }\n}\n\nclass SelectMenu extends BaseMenu {\n  constructor(title, icon) {\n    super(title, icon)\n  }\n  exec() {\n    return ['item1', 'item2', 'item3']\n  }\n}\n\nclass ModalMenu extends BaseMenu {\n  constructor(title, icon) {\n    super(title, icon)\n  }\n  exec() {\n    const div = document.createElement('div')\n    div.innerText = 'modal'\n    return div\n  }\n}\n```\n\n:::\n\n## 手写防抖 Debounce\n\n::: details 参考答案\n\n```js\nfunction debounce(func, wait, immediate) {\n  var timeout, result\n\n  var debounced = function () {\n    var context = this\n    var args = arguments\n\n    if (timeout) clearTimeout(timeout)\n    if (immediate) {\n      // 如果已经执行过，不再执行\n      var callNow = !timeout\n      timeout = setTimeout(function () {\n        timeout = null\n      }, wait)\n      if (callNow) result = func.apply(context, args)\n    } else {\n      timeout = setTimeout(function () {\n        func.apply(context, args)\n      }, wait)\n    }\n    return result\n  }\n\n  debounced.cancel = function () {\n    clearTimeout(timeout)\n    timeout = null\n  }\n\n  return debounced\n}\n```\n\n参考阅读：\n\n- [JavaScript 专题之跟着 underscore 学防抖](https://github.com/mqyqingfeng/Blog/issues/22)\n  :::\n\n## 手写截流 Throttle\n\n::: details 参考答案\n\n```js\nfunction throttle(func, wait, options) {\n  var timeout, context, args, result\n  var previous = 0\n  if (!options) options = {}\n\n  var later = function () {\n    previous = options.leading === false ? 0 : new Date().getTime()\n    timeout = null\n    func.apply(context, args)\n    if (!timeout) context = args = null\n  }\n\n  var throttled = function () {\n    var now = new Date().getTime()\n    if (!previous && options.leading === false) previous = now\n    var remaining = wait - (now - previous)\n    context = this\n    args = arguments\n    if (remaining <= 0 || remaining > wait) {\n      if (timeout) {\n        clearTimeout(timeout)\n        timeout = null\n      }\n      previous = now\n      func.apply(context, args)\n      if (!timeout) context = args = null\n    } else if (!timeout && options.trailing !== false) {\n      timeout = setTimeout(later, remaining)\n    }\n  }\n  throttled.cancel = function () {\n    clearTimeout(timeout)\n    previous = 0\n    timeout = null\n  }\n  return throttled\n}\n```\n\n参考阅读：\n\n- [JavaScript专题之跟着 underscore 学节流](https://github.com/mqyqingfeng/Blog/issues/26)\n  :::\n\n## 手写 bind\n\n::: details 参考答案\n\n```js\nFunction.prototype.bind2 = function (context) {\n  if (typeof this !== 'function') {\n    throw new Error('Function.prototype.bind - what is trying to be bound is not callable')\n  }\n\n  var self = this\n  var args = Array.prototype.slice.call(arguments, 1)\n\n  var fNOP = function () {}\n\n  var fBound = function () {\n    var bindArgs = Array.prototype.slice.call(arguments)\n    return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs))\n  }\n\n  fNOP.prototype = this.prototype\n  fBound.prototype = new fNOP()\n  return fBound\n}\n```\n\n参考阅读：\n\n- [JavaScript深入之bind的模拟实现](https://github.com/mqyqingfeng/Blog/issues/12)\n\n:::\n\n## 手写 call 和 apply\n\n::: details 参考答案\n\n```js\nFunction.prototype.call2 = function (context) {\n  var context = context || window\n  context.fn = this\n\n  var args = []\n  for (var i = 1, len = arguments.length; i < len; i++) {\n    args.push('arguments[' + i + ']')\n  }\n\n  var result = eval('context.fn(' + args + ')')\n\n  delete context.fn\n  return result\n}\n\nFunction.prototype.apply = function (context, arr) {\n  var context = Object(context) || window\n  context.fn = this\n\n  var result\n  if (!arr) {\n    result = context.fn()\n  } else {\n    var args = []\n    for (var i = 0, len = arr.length; i < len; i++) {\n      args.push('arr[' + i + ']')\n    }\n    result = eval('context.fn(' + args + ')')\n  }\n\n  delete context.fn\n  return result\n}\n```\n\n参考阅读：\n\n- [JavaScript深入之call和apply的模拟实现](https://github.com/mqyqingfeng/Blog/issues/11)\n\n:::\n\n## 手写 EventBus 自定义事件\n\n::: details 参考答案\n\n```js\nclass EventBus {\n  constructor() {\n    this.eventObj = {}\n    this.callbcakId = 0\n  }\n\n  $on(name, callbcak) {\n    if (!this.eventObj[name]) {\n      this.eventObj[name] = {}\n    }\n    const id = this.callbcakId++\n    this.eventObj[name][id] = callbcak\n    return id\n  }\n  $emit(name, ...args) {\n    const eventList = this.eventObj[name]\n    for (const id in eventList) {\n      eventList[id](...args)\n      if (id.indexOf('D') !== -1) {\n        delete eventList[id]\n      }\n    }\n  }\n  $off(name, id) {\n    delete this.eventObj[name][id]\n    if (!Object.keys(this.eventObj[name]).length) {\n      delete this.eventObj[name]\n    }\n  }\n  $once(name, callbcak) {\n    if (!this.eventObj[name]) {\n      this.eventObj[name] = {}\n    }\n    const id = 'D' + this.callbcakId++\n    this.eventObj[name][id] = callbcak\n    return id\n  }\n}\n```\n\n参考阅读：\n\n- [面试官：请手写一个EventBus，让我看看你的代码能力！](https://juejin.cn/post/7101481154565865486)\n  :::\n\n## 手写数组拍平 Array Flatten\n\n::: details 参考答案\n\n```js\nfunction flatten(input, shallow, strict, output) {\n  // 递归使用的时候会用到output\n  output = output || []\n  var idx = output.length\n\n  for (var i = 0, len = input.length; i < len; i++) {\n    var value = input[i]\n    // 如果是数组，就进行处理\n    if (Array.isArray(value)) {\n      // 如果是只扁平一层，遍历该数组，依此填入 output\n      if (shallow) {\n        var j = 0,\n          length = value.length\n        while (j < length) output[idx++] = value[j++]\n      }\n      // 如果是全部扁平就递归，传入已经处理的 output，递归中接着处理 output\n      else {\n        flatten(value, shallow, strict, output)\n        idx = output.length\n      }\n    }\n    // 不是数组，根据 strict 的值判断是跳过不处理还是放入 output\n    else if (!strict) {\n      output[idx++] = value\n    }\n  }\n\n  return output\n}\n```\n\n参考阅读：\n\n- [JavaScript专题之数组扁平化](https://github.com/mqyqingfeng/Blog/issues/36)\n  :::\n\n## 手写解析 URL 参数为 JS 对象\n\n::: details 参考答案\n\n```js\nfunction parseParam(url) {\n  const paramsStr = /.+\\?(.+)$/.exec(url)[1] // 将 ? 后面的字符串取出来\n  //exec() 方法用于检索字符串中的正则表达式的匹配。\n  const paramsArr = paramsStr.split('&') // 将字符串以 & 分割后存到数组中\n  let paramsObj = {}\n  // 将 params 存到对象中\n  paramsArr.forEach((param) => {\n    if (/=/.test(param)) {\n      // 处理有 value 的参数\n      let [key, val] = param.split('=') // 分割 key 和 value\n      val = decodeURIComponent(val) // 解码\n      val = /^\\d+$/.test(val) ? parseFloat(val) : val // 判断是否转为数字\n      //test() 方法用于检测一个字符串是否匹配某个模式.\n      if (paramsObj.hasOwnProperty(key)) {\n        // 如果对象有 key，则添加一个值\n        paramsObj[key] = [].concat(paramsObj[key], val)\n        //concat() 方法用于连接两个或多个数组。\n        //该方法不会改变现有的数组，而仅仅会返回被连接数组的一个副本。\n      } else {\n        // 如果对象没有这个 key，创建 key 并设置值\n        paramsObj[key] = val\n      }\n    } else {\n      // 处理没有 value 的参数\n      paramsObj[param] = true\n    }\n  })\n\n  return paramsObj\n}\n```\n\n参考阅读：\n\n- [解析 URL 参数为对象和字符串模板](https://juejin.cn/post/6950554221242499103)\n  :::\n\n## 手写数组去重\n\n::: details 参考答案\n\n```js\nvar unique = (a) => [...new Set(a)]\n```\n\n参考阅读：\n\n- [JavaScript 专题之数组去重](https://github.com/mqyqingfeng/Blog/issues/27)\n  :::\n\n## 手写红绿灯\n\n模拟一个红绿灯变化，红灯 1 秒，绿灯 1 秒，黄灯 1 秒，然后循环\n\n::: details 参考答案\n\n```js\nfunction red() {\n  console.log('red')\n}\n\nfunction green() {\n  console.log('green')\n}\n\nfunction yellow() {\n  console.log('yellow')\n}\n\nfunction light(cb, wait) {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      cb()\n      resolve()\n    }, wait)\n  })\n}\n\nfunction start() {\n  return Promise.resolve()\n    .then(() => {\n      return light(red, 1000)\n    })\n    .then(() => {\n      return light(green, 1000)\n    })\n    .then(() => {\n      return light(yellow, 1000)\n    })\n    .finally(() => {\n      return start()\n    })\n}\n\nstart()\n```\n\n:::\n\n## 手写 Promise\n\n::: details 参考答案\n\n```js\nclass MyPromise {\n  // 构造方法\n  constructor(executor) {\n    // 初始化值\n    this.initValue()\n    // 初始化this指向\n    this.initBind()\n    // 执行传进来的函数\n    executor(this.resolve, this.reject)\n  }\n\n  initBind() {\n    // 初始化this\n    this.resolve = this.resolve.bind(this)\n    this.reject = this.reject.bind(this)\n  }\n\n  initValue() {\n    // 初始化值\n    this.PromiseResult = null // 终值\n    this.PromiseState = 'pending' // 状态\n  }\n\n  resolve(value) {\n    // 如果执行resolve，状态变为fulfilled\n    this.PromiseState = 'fulfilled'\n    // 终值为传进来的值\n    this.PromiseResult = value\n  }\n\n  reject(reason) {\n    // 如果执行reject，状态变为rejected\n    this.PromiseState = 'rejected'\n    // 终值为传进来的reason\n    this.PromiseResult = reason\n  }\n}\n```\n\n参考阅读：\n\n- [看了就会，手写Promise原理，最通俗易懂的版本！！！](https://juejin.cn/post/6994594642280857630)\n  :::\n\n## 手写 Promise.all\n\n::: details 参考答案\n\n```js\nstatic all(promises) {\n  const result = []\n  let count = 0\n  return new MyPromise((resolve, reject) => {\n    const addData = (index, value) => {\n        result[index] = value\n        count++\n        if (count === promises.length) resolve(result)\n    }\n    promises.forEach((promise, index) => {\n        if (promise instanceof MyPromise) {\n            promise.then(res => {\n                addData(index, res)\n            }, err => reject(err))\n        } else {\n            addData(index, promise)\n        }\n    })\n  })\n}\n```\n\n参考阅读：\n\n- [看了就会，手写Promise原理，最通俗易懂的版本！！！](https://juejin.cn/post/6994594642280857630)\n  :::\n\n## 手写 Promise.race\n\n::: details 参考答案\n\n```js\nstatic race(promises) {\n  return new MyPromise((resolve, reject) => {\n    promises.forEach(promise => {\n      if (promise instanceof MyPromise) {\n          promise.then(res => {\n              resolve(res)\n          }, err => {\n              reject(err)\n          })\n      } else {\n          resolve(promise)\n      }\n    })\n  })\n}\n```\n\n参考阅读：\n\n- [看了就会，手写Promise原理，最通俗易懂的版本！！！](https://juejin.cn/post/6994594642280857630)\n  :::\n\n## 手写 Promise.allSettled\n\n::: details 参考答案\n\n```js\nstatic allSettled(promises) {\n  return new Promise((resolve, reject) => {\n    const res = []\n    let count = 0\n    const addData = (status, value, i) => {\n      res[i] = {\n          status,\n          value\n      }\n      count++\n      if (count === promises.length) {\n          resolve(res)\n      }\n    }\n    promises.forEach((promise, i) => {\n      if (promise instanceof MyPromise) {\n        promise.then(res => {\n          addData('fulfilled', res, i)\n        }, err => {\n          addData('rejected', err, i)\n        })\n      } else {\n        addData('fulfilled', promise, i)\n      }\n    })\n  })\n}\n```\n\n参考阅读：\n\n- [看了就会，手写Promise原理，最通俗易懂的版本！！！](https://juejin.cn/post/6994594642280857630)\n  :::\n\n## 手写一个 LazyMan 实现 sleep 机制\n\n```js\nLazyMan('Tony').eat('breakfast').sleep(3).eat('lunch').sleep(1).eat('dinner')\n// 输出:\n// Hi I am Tony\n// I am eating breakfast\n// 等待3秒...\n// I am eating lunch\n// 等待1秒...\n// I am eating dinner\n```\n\n::: details 参考答案\n\n```js\nclass LazyMan {\n  constructor(name) {\n    this.name = name\n    this.tasks = [] // 任务队列\n\n    // 初始任务\n    this.tasks.push(() => {\n      console.log(`Hi I am ${name}`)\n      return Promise.resolve()\n    })\n\n    // 使用 setTimeout 确保所有任务入队后再执行\n    setTimeout(() => {\n      this.runTasks()\n    }, 0)\n  }\n\n  // 执行任务队列\n  async runTasks() {\n    for (const task of this.tasks) {\n      await task()\n    }\n  }\n\n  eat(food) {\n    this.tasks.push(() => {\n      console.log(`I am eating ${food}`)\n      return Promise.resolve()\n    })\n    return this\n  }\n\n  sleep(seconds) {\n    this.tasks.push(() => {\n      console.log(`等待${seconds}秒...`)\n      return new Promise((resolve) => {\n        setTimeout(resolve, seconds * 1000)\n      })\n    })\n    return this\n  }\n}\n\n// 工厂函数，方便调用\nfunction createLazyMan(name) {\n  return new LazyMan(name)\n}\n```\n\n:::\n\n## 手写 curry 函数，实现函数柯里化\n\n::: details 参考答案\n\n1. 基础版本实现\n\n```js\nfunction curry(fn) {\n  return function curried(...args) {\n    // 如果传入的参数个数大于等于原函数的参数个数，直接执行\n    if (args.length >= fn.length) {\n      return fn.apply(this, args)\n    }\n    // 否则返回一个新函数，等待接收剩余参数\n    return function (...args2) {\n      return curried.apply(this, args.concat(args2))\n    }\n  }\n}\n\n// 使用示例\nfunction add(a, b, c) {\n  return a + b + c\n}\n\nconst curriedAdd = curry(add)\nconsole.log(curriedAdd(1)(2)(3)) // 6\nconsole.log(curriedAdd(1, 2)(3)) // 6\nconsole.log(curriedAdd(1)(2, 3)) // 6\n```\n\n2. 支持占位符的进阶版本\n\n```js\nfunction curry(fn, placeholder = '_') {\n  const length = fn.length\n\n  return function curried(...args) {\n    // 检查是否所有参数都已经填充（不包含占位符）\n    const checkFilled = (args) => {\n      // 统计非占位符的参数个数\n      const filledArgsCount = args.filter((arg) => arg !== placeholder).length\n      return filledArgsCount >= length\n    }\n\n    // 合并新旧参数，处理占位符\n    const mergeArgs = (existingArgs, newArgs) => {\n      const result = [...existingArgs]\n      let newArgsIndex = 0\n\n      // 遍历现有参数，将占位符替换为新参数\n      for (let i = 0; i < result.length && newArgsIndex < newArgs.length; i++) {\n        if (result[i] === placeholder) {\n          result[i] = newArgs[newArgsIndex++]\n        }\n      }\n\n      // 将剩余的新参数添加到结果中\n      return result.concat(newArgs.slice(newArgsIndex))\n    }\n\n    const mergedArgs = mergeArgs(args, [])\n\n    // 如果参数已经足够，执行原函数\n    if (checkFilled(mergedArgs)) {\n      // 过滤掉占位符\n      const finalArgs = mergedArgs.slice(0, length).filter((arg) => arg !== placeholder)\n      return fn.apply(this, finalArgs)\n    }\n\n    // 否则继续返回柯里化函数\n    return function (...nextArgs) {\n      return curried.apply(this, mergeArgs(mergedArgs, nextArgs))\n    }\n  }\n}\n\n// 使用示例\nconst add = (a, b, c) => a + b + c\nconst curriedAdd = curry(add)\nconst _ = '_' // 占位符\n\nconsole.log(curriedAdd(1)(2)(3)) // 6\nconsole.log(curriedAdd(1, 2)(3)) // 6\nconsole.log(curriedAdd(1)(_, 3)(2)) // 6\nconsole.log(curriedAdd(_, 2)(1)(3)) // 6\nconsole.log(curriedAdd(_, _, 3)(1)(2)) // 6\n```\n\n3. ES6 简化版本\n\n```js\nconst curry = (fn, arity = fn.length) => {\n  const curried = (...args) => (args.length >= arity ? fn(...args) : (...more) => curried(...args, ...more))\n  return curried\n}\n\n// 使用示例\nconst sum = (a, b, c) => a + b + c\nconst curriedSum = curry(sum)\n\nconsole.log(curriedSum(1)(2)(3)) // 6\nconsole.log(curriedSum(1, 2)(3)) // 6\nconsole.log(curriedSum(1)(2, 3)) // 6\n```\n\n:::\n\n## 手写 compose 函数\n\n::: details 参考答案\n\ncompose 函数是函数式编程中的一个重要概念，它将多个函数组合成一个函数，从右到左执行。\n\n1. 基础实现（使用 reduce）\n\n```js\nfunction compose(...fns) {\n  if (fns.length === 0) return (arg) => arg\n  if (fns.length === 1) return fns[0]\n\n  return fns.reduce(\n    (a, b) =>\n      (...args) =>\n        a(b(...args))\n  )\n}\n\n// 使用示例\nconst add1 = (x) => x + 1\nconst multiply2 = (x) => x * 2\nconst addThenMultiply = compose(multiply2, add1)\nconsole.log(addThenMultiply(5)) // (5 + 1) * 2 = 12\n```\n\n2. 支持异步函数的实现\n\n```js\nasync function composeAsync(...fns) {\n  if (fns.length === 0) return (arg) => arg\n  if (fns.length === 1) return fns[0]\n\n  return fns.reduce((a, b) => async (...args) => {\n    const result = await b(...args)\n    return a(result)\n  })\n}\n\n// 使用示例\nconst asyncAdd = async (x) => {\n  await new Promise((resolve) => setTimeout(resolve, 1000))\n  return x + 1\n}\nconst asyncMultiply = async (x) => {\n  await new Promise((resolve) => setTimeout(resolve, 1000))\n  return x * 2\n}\n\nconst asyncOperation = composeAsync(asyncMultiply, asyncAdd)\nasyncOperation(5).then((result) => console.log(result)) // 12 (after 2 seconds)\n```\n\n3. 从左到右执行的 pipe 实现\n\n```js\nfunction pipe(...fns) {\n  if (fns.length === 0) return (arg) => arg\n  if (fns.length === 1) return fns[0]\n\n  return fns.reduce(\n    (a, b) =>\n      (...args) =>\n        b(a(...args))\n  )\n}\n\n// 使用示例\nconst addOne = (x) => x + 1\nconst multiplyTwo = (x) => x * 2\nconst addThenMultiplyPipe = pipe(addOne, multiplyTwo)\nconsole.log(addThenMultiplyPipe(5)) // (5 + 1) * 2 = 12\n```\n\n4. 带错误处理的实现\n\n```js\nfunction composeWithError(...fns) {\n  if (fns.length === 0) return (arg) => arg\n  if (fns.length === 1) return fns[0]\n\n  return fns.reduce((a, b) => (...args) => {\n    try {\n      const result = b(...args)\n      return a(result)\n    } catch (error) {\n      console.error('Error in compose:', error)\n      throw error\n    }\n  })\n}\n\n// 使用示例\nconst divide = (x) => {\n  if (x === 0) throw new Error('Cannot divide by zero')\n  return 10 / x\n}\nconst square = (x) => x * x\n\nconst divideAndSquare = composeWithError(square, divide)\nconsole.log(divideAndSquare(2)) // (10 / 2)² = 25\ntry {\n  divideAndSquare(0) // 抛出错误\n} catch (e) {\n  console.log('Caught error:', e.message)\n}\n```\n\n使用场景示例：\n\n1. **数据转换管道**：\n\n```js\nconst toLowerCase = (str) => str.toLowerCase()\nconst removeSpaces = (str) => str.replace(/\\s/g, '')\nconst addPrefix = (str) => `prefix_${str}`\n\nconst processString = compose(addPrefix, removeSpaces, toLowerCase)\nconsole.log(processString('Hello World')) // 'prefix_helloworld'\n```\n\n2. **数学计算**：\n\n```js\nconst double = (x) => x * 2\nconst addTen = (x) => x + 10\nconst square = (x) => x * x\n\nconst calculate = compose(square, addTen, double)\nconsole.log(calculate(5)) // (5 * 2 + 10)² = 400\n```\n\n3. **数据处理链**：\n\n```js\nconst filterEven = (arr) => arr.filter((x) => x % 2 === 0)\nconst multiplyAll = (arr) => arr.map((x) => x * 2)\nconst sum = (arr) => arr.reduce((a, b) => a + b, 0)\n\nconst processNumbers = compose(sum, multiplyAll, filterEven)\nconsole.log(processNumbers([1, 2, 3, 4, 5, 6])) // 2*2 + 4*2 + 6*2 = 24\n```\n\n注意事项：\n\n1. compose 函数从右到左执行，而 pipe 函数从左到右执行\n2. 确保函数的输入输出类型匹配\n3. 处理异步操作时需要使用 async/await 版本\n4. 考虑错误处理机制\n5. 函数组合应该保持纯函数的特性\n\ncompose 函数是函数式编程中的重要工具，它能够帮助我们构建更加模块化和可维护的代码。通过组合小的、单一功能的函数，我们可以构建出复杂的数据转换管道。\n\n:::\n\n## 手写一个 LRU 缓存\n\n::: details 参考答案\n\nLRU（Least Recently Used）是一种缓存淘汰策略，它会优先删除最近最少使用的数据。下面提供两种实现方式：使用 Map 的简单实现和不使用 Map 的基础实现。\n\n1. 使用 Map 的实现\n\n```js\nclass LRUCache {\n  constructor(capacity) {\n    this.cache = new Map()\n    this.capacity = capacity\n  }\n\n  get(key) {\n    if (!this.cache.has(key)) return -1\n\n    // 将访问的元素移到最新使用的位置\n    const value = this.cache.get(key)\n    this.cache.delete(key)\n    this.cache.set(key, value)\n    return value\n  }\n\n  put(key, value) {\n    // 如果 key 已存在，先删除\n    if (this.cache.has(key)) {\n      this.cache.delete(key)\n    }\n    // 如果达到容量限制，删除最久未使用的元素\n    else if (this.cache.size >= this.capacity) {\n      // Map 的 keys() 会按插入顺序返回键\n      const firstKey = this.cache.keys().next().value\n      this.cache.delete(firstKey)\n    }\n\n    this.cache.set(key, value)\n  }\n}\n\n// 使用示例\nconst cache = new LRUCache(2)\ncache.put(1, 1) // 缓存是 {1=1}\ncache.put(2, 2) // 缓存是 {1=1, 2=2}\nconsole.log(cache.get(1)) // 返回 1\ncache.put(3, 3) // 删除 key 2，缓存是 {1=1, 3=3}\nconsole.log(cache.get(2)) // 返回 -1 (未找到)\n```\n\n2. 使用双向链表的实现（不依赖 Map）\n\n```js\n// 双向链表节点\nclass Node {\n  constructor(key, value) {\n    this.key = key\n    this.value = value\n    this.prev = null\n    this.next = null\n  }\n}\n\nclass LRUCache {\n  constructor(capacity) {\n    this.capacity = capacity\n    this.cache = {} // 哈希表用于O(1)查找\n    this.count = 0\n    // 创建头尾哨兵节点\n    this.head = new Node(0, 0)\n    this.tail = new Node(0, 0)\n    this.head.next = this.tail\n    this.tail.prev = this.head\n  }\n\n  // 将节点移到双向链表头部\n  moveToHead(node) {\n    this.removeNode(node)\n    this.addToHead(node)\n  }\n\n  // 从链表中删除节点\n  removeNode(node) {\n    node.prev.next = node.next\n    node.next.prev = node.prev\n  }\n\n  // 在链表头部添加节点\n  addToHead(node) {\n    node.prev = this.head\n    node.next = this.head.next\n    this.head.next.prev = node\n    this.head.next = node\n  }\n\n  // 删除链表尾部节点\n  removeTail() {\n    const node = this.tail.prev\n    this.removeNode(node)\n    return node\n  }\n\n  get(key) {\n    if (key in this.cache) {\n      const node = this.cache[key]\n      this.moveToHead(node)\n      return node.value\n    }\n    return -1\n  }\n\n  put(key, value) {\n    if (key in this.cache) {\n      // 如果 key 存在，更新值并移到头部\n      const node = this.cache[key]\n      node.value = value\n      this.moveToHead(node)\n    } else {\n      // 创建新节点\n      const newNode = new Node(key, value)\n      this.cache[key] = newNode\n      this.addToHead(newNode)\n      this.count++\n\n      // 如果超过容量，删除最久未使用的\n      if (this.count > this.capacity) {\n        const tail = this.removeTail()\n        delete this.cache[tail.key]\n        this.count--\n      }\n    }\n  }\n}\n\n// 使用示例\nconst cache = new LRUCache(2)\ncache.put(1, 1)\ncache.put(2, 2)\nconsole.log(cache.get(1)) // 返回 1\ncache.put(3, 3) // 删除 key 2\nconsole.log(cache.get(2)) // 返回 -1 (未找到)\ncache.put(4, 4) // 删除 key 1\nconsole.log(cache.get(1)) // 返回 -1 (未找到)\nconsole.log(cache.get(3)) // 返回 3\nconsole.log(cache.get(4)) // 返回 4\n```\n\n实现原理说明：\n\n1. **Map 实现版本**：\n\n   - 利用 Map 的特性，它能够记住键的原始插入顺序\n   - get 操作时将访问的元素移到最后（最新使用）\n   - put 操作时如果超出容量，删除第一个元素（最久未使用）\n\n2. **双向链表实现版本**：\n   - 使用哈希表实现 O(1) 的查找\n   - 使用双向链表维护数据的使用顺序\n   - 最近使用的数据放在链表头部\n   - 最久未使用的数据在链表尾部\n\n性能分析：\n\n1. **时间复杂度**：\n\n   - get 操作：O(1)\n   - put 操作：O(1)\n\n2. **空间复杂度**：\n   - O(capacity)，其中 capacity 是缓存的容量\n\n使用场景：\n\n1. **浏览器缓存**：\n\n```js\nconst browserCache = new LRUCache(100)\nbrowserCache.put('url1', 'response1')\nbrowserCache.put('url2', 'response2')\n```\n\n2. **内存缓存**：\n\n```js\nconst memoryCache = new LRUCache(1000)\nmemoryCache.put('userId1', userDataObject1)\nmemoryCache.put('userId2', userDataObject2)\n```\n\n3. **数据库查询缓存**：\n\n```js\nconst queryCache = new LRUCache(50)\nfunction query(sql) {\n  const cached = queryCache.get(sql)\n  if (cached !== -1) return cached\n\n  const result = executeQuery(sql)\n  queryCache.put(sql, result)\n  return result\n}\n```\n\n:::\n\n## 使用 Vue3 Composable 组合式函数，实现 useCount\n\n```js\nconst { count } = useCount() // count 初始值是 0 ，每一秒 count 加 1\n```\n\n::: details 参考答案\n\n```js\nimport { ref, onMounted, onUnmounted } from 'vue'\n\nexport function useCount() {\n  const count = ref(0)\n  let timer = null\n\n  // 开始计数\n  const startCount = () => {\n    timer = setInterval(() => {\n      count.value++\n    }, 1000)\n  }\n\n  // 组件挂载时开始计数\n  onMounted(() => {\n    startCount()\n  })\n\n  // 组件卸载时清除定时器\n  onUnmounted(() => {\n    if (timer) {\n      clearInterval(timer)\n    }\n  })\n\n  return {\n    count,\n  }\n}\n```\n\n:::\n\n## 使用 Vue3 Composable 组合式函数，实现 useRequest\n\n```js\nconst { loading, data, error } = useRequest(url) // 可只考虑 get 请求\n```\n\n::: details 参考答案\n\n```js\nimport { ref } from 'vue'\n\nexport function useRequest(url) {\n  const data = ref(null)\n  const loading = ref(false)\n  const error = ref(null)\n\n  const fetchData = async () => {\n    loading.value = true\n    error.value = null\n\n    try {\n      const response = await fetch(url)\n      if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`)\n      }\n      data.value = await response.json()\n    } catch (e) {\n      error.value = e\n    } finally {\n      loading.value = false\n    }\n  }\n\n  // 立即执行请求\n  fetchData()\n\n  return {\n    data,\n    loading,\n    error,\n  }\n}\n```\n\n:::\n\n## 使用 React Hook 实现 useCount\n\n```js\n// count 从 0 计数，每一秒 +1 （可使用 setInterval）\nconst { count } = useTimer()\n```\n\n::: details 参考答案\n\n```js\nimport { useState, useEffect } from 'react'\n\nfunction useTimer() {\n  const [count, setCount] = useState(0)\n\n  useEffect(() => {\n    const timer = setInterval(() => {\n      setCount((prev) => prev + 1)\n    }, 1000)\n\n    // 清理函数，组件卸载时清除定时器\n    return () => clearInterval(timer)\n  }, [])\n\n  return { count }\n}\n\nexport default useTimer\n```\n\n:::\n\n## 使用 React Hook 实现 useRequest\n\n```js\nconst { loading, data, error } = useRequest(url) // 可只考虑 get 请求\n```\n\n::: details 参考答案\n\n```js\nimport { useState, useEffect } from 'react'\n\nfunction useRequest(url) {\n  const [data, setData] = useState(null)\n  const [loading, setLoading] = useState(true)\n  const [error, setError] = useState(null)\n\n  useEffect(() => {\n    const fetchData = async () => {\n      setLoading(true)\n      setError(null)\n\n      try {\n        const response = await fetch(url)\n        if (!response.ok) {\n          throw new Error(`HTTP error! status: ${response.status}`)\n        }\n        const result = await response.json()\n        setData(result)\n      } catch (e) {\n        setError(e)\n      } finally {\n        setLoading(false)\n      }\n    }\n\n    fetchData()\n  }, [url])\n\n  return { data, loading, error }\n}\n\nexport default useRequest\n```\n\n:::\n\n## 手写 VNode 对象，表示如下 DOM 节点\n\n```html\n<div class=\"container\">\n  <img src=\"x1.png\" />\n  <p>hello</p>\n</div>\n```\n\n::: details 参考答案\n\n```js\nconst vnode = {\n  tag: 'div',\n  props: {\n    class: 'container',\n  },\n  children: [\n    {\n      tag: 'img',\n      props: {\n        src: 'x1.png',\n      },\n    },\n    {\n      tag: 'p',\n      props: {},\n      children: ['hello'],\n    },\n  ],\n}\n```\n\n:::\n"
  },
  {
    "path": "docs/written-exam/algorithm.md",
    "content": "# 数据结构和算法\n\n大厂前端面试，先从算法开始。\n\n::: tip\n\n1. 目标**不在**中大厂的同学，可以略过算法这一节。\n2. 算法 0 基础的同学，可先略过这一节，临时准备根本来不及，需要日常积累。\n3. 如果时间不够，每个分类刷 1-2 道，全刷完太多了。主要是掌握解题的套路。\n   :::\n\n::: tip\n如有疑问，可免费 [加群](/docs/services/group.md) 讨论咨询，也可参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 专业、系统、高效、全流程 准备前端面试\n:::\n\n## 算法基础 Basic\n\n磨刀不误砍柴工，别着急刷 LeetCode 等算法题，先把基础巩固好，会事半功倍。\n\n如果这些基础知识都无法自学搞懂，请[加群](/docs/services/group.md)寻求帮助，不要自己独立钻研，要掌握学习方法。\n\n### 前端常见的数据结构有哪些？有什么基础算法？有什么应用场景？\n\n#### 数组/字符串\n\n基础算法：\n\n- 排序：冒泡排序，快速排序等\n- 查找：二分查找\n\n#### 链表\n\n应用场景：React fiber 结构\n\n基础算法：遍历，反转\n\n#### 栈\n\n应用场景：\n\n- 撤销/重做 undo/redo\n- 内存堆栈模型\n\n基础算法：\n\n- 压栈 push\n- 出栈 pop\n\n#### 队列\n\n应用场景：\n\n- Event Loop 事件循环\n- 消息队列服务\n\n基础算法：\n\n- 入队 enqueue\n- 出队 dequeue\n\n#### 树\n\n应用场景：DOM 树，VDOM\n\n基础算法：\n\n- 深度优先搜索 DFS\n- 广度优先搜索 BFS\n\n#### 二叉树\n\n应用场景：\n\n- 基础的二叉树应用场景不多，主要用于学习和面试 😄\n- 二叉树扩展出来的，平衡二叉树、AVL 树、红黑树、B+ 树、Trie 树，有大量的应用场景，如数据库管理、文件系统管理、虚拟内存管理等\n- 前端使用的场景较少，了解其基础知识即可\n\n基础算法：\n\n- 前序遍历\n- 中序遍历\n- 后续遍历\n\n#### 堆\n\n应用场景：内存堆栈模型\n\n基础算法：堆排序\n\n#### 图\n\n应用场景：前端流程图、关系图，社交网络模型，搜索引擎\n\n基础算法：\n\n- 深度优先搜索 DFS\n- 广度优先搜索 BFS\n- 最短路径，Dijkstra 算法\n\n### 什么是时间复杂度？\n\n算法的时间复杂度，**定性**（数量级）的描述算法运行的时间，用 `O` 符号表示。\n\n常见的时间复杂度\n\n- `O(1)` 常数级，无循环\n- `O(n)` 线性，单层循环\n- `O(logn)` 二分算法\n- `O(n*logn)` 单层循环，嵌套二分算法\n- `O(n^2)` 两层循环\n- `O(n^3)` 三层循环，实际不可用\n\n图示如下\n\n![](../imgs/complexity.webp)\n\n### 什么是空间复杂度？\n\n同时间复杂度，只是把时间换成空间。时间是 CPU 的消耗，空间是内存的消耗。\n\n## 数组 Array\n\n### 两数之和\n\n- 题目 https://leetcode.cn/problems/two-sum/description/\n- 解答 https://leetcode.cn/problems/two-sum/solutions/\n\n### 买卖股票的最佳时机\n\n- 题目 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/\n- 解答 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/solutions/\n\n### 盛水最多的容器\n\n- 题目 https://leetcode.cn/problems/container-with-most-water/description/\n- 解答 https://leetcode.cn/problems/container-with-most-water/solutions/\n\n### 除自身以外数组的乘积\n\n- 题目 https://leetcode.cn/problems/product-of-array-except-self/description/\n- 解答 https://leetcode.cn/problems/product-of-array-except-self/solutions/\n\n## 字符串 String\n\n### 无重复字符的最长子串\n\n- 题目 https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/\n- 解答 https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/\n\n### 验证回文串\n\n- 题目 https://leetcode.cn/problems/valid-palindrome/description/\n- 解答 https://leetcode.cn/problems/valid-palindrome/solutions/\n\n### 反转字符串中的单词\n\n- 题目 https://leetcode.cn/problems/reverse-words-in-a-string/description/\n- 解答 https://leetcode.cn/problems/reverse-words-in-a-string/solutions/\n\n### 最长回文子串\n\n- 题目 https://leetcode.cn/problems/longest-palindromic-substring/description/\n- 解答 https://leetcode.cn/problems/longest-palindromic-substring/solutions/\n\n## 链表 Linked List\n\n### 反转链表\n\n- 题目 https://leetcode.cn/problems/reverse-linked-list/description/\n- 解答 https://leetcode.cn/problems/reverse-linked-list/solutions/\n\n### 合并两个有序链表\n\n- 题目 https://leetcode.cn/problems/merge-two-sorted-lists/description/\n- 解答 https://leetcode.cn/problems/merge-two-sorted-lists/solutions/\n\n### 删除链表的倒数第 N 个结点\n\n- 题目 https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/\n- 解答 https://leetcode.cn/problems/remove-nth-node-from-end-of-list/solutions/\n\n### 判断环形链表\n\n- 题目 https://leetcode.cn/problems/linked-list-cycle/description/\n- 解答 https://leetcode.cn/problems/linked-list-cycle/solutions/\n\n### 相交链表\n\n- 题目 https://leetcode.cn/problems/intersection-of-two-linked-lists/description/\n- 解答 https://leetcode.cn/problems/intersection-of-two-linked-lists/solutions/\n\n## 栈 Stack\n\n### 有效的括号\n\n- 题目 https://leetcode.cn/problems/valid-parentheses/description/\n- 解答 https://leetcode.cn/problems/valid-parentheses/solutions/\n\n### 最小栈\n\n- 题目 https://leetcode.cn/problems/min-stack/description/\n- 解答 https://leetcode.cn/problems/min-stack/solutions/\n\n### 用栈实现队列\n\n- 题目 https://leetcode.cn/problems/implement-queue-using-stacks/description/\n- 解答 https://leetcode.cn/problems/implement-queue-using-stacks/solutions/\n\n### 字符串解码\n\n- 题目 https://leetcode.cn/problems/decode-string/description/\n- 解答 https://leetcode.cn/problems/decode-string/solutions/\n\n## 队列 Queue\n\n### 用队列实现栈\n\n- 题目 https://leetcode.cn/problems/implement-stack-using-queues/description/\n- 解答 https://leetcode.cn/problems/implement-stack-using-queues/solutions/\n\n### 最近的请求次数\n\n- 题目 https://leetcode.cn/problems/number-of-recent-calls/description/\n- 解答 https://leetcode.cn/problems/number-of-recent-calls/\n\n## 二叉树 Binary Tree\n\n### 二叉树的最大深度\n\n- 题目 https://leetcode.cn/problems/maximum-depth-of-binary-tree/description/\n- 解答 https://leetcode.cn/problems/maximum-depth-of-binary-tree/solutions/\n\n### 验证二叉搜索树\n\n- 题目 https://leetcode.cn/problems/validate-binary-search-tree/description/\n- 解答 https://leetcode.cn/problems/validate-binary-search-tree/solutions/\n\n### 二叉树的层序遍历\n\n- 题目 https://leetcode.cn/problems/binary-tree-level-order-traversal/description/\n- 解答 https://leetcode.cn/problems/binary-tree-level-order-traversal/solutions/\n\n### 对称二叉树\n\n- 题目 https://leetcode.cn/problems/symmetric-tree/description/\n- 解答 https://leetcode.cn/problems/symmetric-tree/solutions/\n\n## 动态规划 Dynamic Programming\n\n### 第 N 个斐波那契数\n\n- 题目 https://leetcode.cn/problems/n-th-tribonacci-number/description/\n- 解答 https://leetcode.cn/problems/n-th-tribonacci-number/solutions/\n\n### 爬楼梯\n\n- 题目 https://leetcode.cn/problems/climbing-stairs/description/\n- 解答 https://leetcode.cn/problems/climbing-stairs/solutions/\n\n### 不同路径\n\n- 题目 https://leetcode.cn/problems/unique-paths/description/\n- 解答 https://leetcode.cn/problems/unique-paths/solutions/\n\n### 最长递增子序列\n\n- 题目 https://leetcode.cn/problems/longest-increasing-subsequence/description/\n- 解答 https://leetcode.cn/problems/longest-increasing-subsequence/solutions/\n\n### 零钱兑换\n\n- 题目 https://leetcode.cn/problems/coin-change/description/\n- 解答 https://leetcode.cn/problems/coin-change/solutions/\n\n## 分治 Divide and Conquer\n\n### 二分查找\n\n- 题目 https://leetcode.cn/problems/binary-search/description/\n- 解答 https://leetcode.cn/problems/binary-search/solutions/\n\n### 快速排序\n\n- 题目 https://leetcode.cn/problems/sort-an-array/description/\n- 解答 https://leetcode.cn/problems/sort-an-array/solutions/\n\n### 数组中的第 K 个最大元素\n\n- 题目 https://leetcode.cn/problems/kth-largest-element-in-an-array/description/\n- 解答 https://leetcode.cn/problems/kth-largest-element-in-an-array/solutions/\n\n### 最大子数组和\n\n- 题目 https://leetcode.cn/problems/maximum-subarray/description/\n- 解答 https://leetcode.cn/problems/maximum-subarray/solutions/\n\n## 双指针 Two Pointers\n\n### 移动零\n\n- 题目 https://leetcode.cn/problems/move-zeroes/description/\n- 解答 https://leetcode.cn/problems/move-zeroes/solutions/\n\n### 判断子序列\n\n- 题目 https://leetcode.cn/problems/is-subsequence/description/\n- 解答 https://leetcode.cn/problems/is-subsequence/solutions/\n\n### 两数之和 II - 输入有序数组\n\n- 题目 https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/description/\n- 解答 https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/solutions/\n\n### 接雨水\n\n- 题目 https://leetcode.cn/problems/trapping-rain-water/description/\n- 解答 https://leetcode.cn/problems/trapping-rain-water/solutions/\n"
  },
  {
    "path": "index.md",
    "content": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\n\nhero:\n  name: '前端面试派'\n  text: '系统专业的面试导航'\n  tagline: 双越老师 带队制作，大厂面试流程，开源免费\n  actions:\n    - theme: brand\n      text: 刷题\n      link: /docs/written-exam/algorithm.html\n    - theme: alt\n      text: 面试技巧\n      link: /docs/hr-exam/behavioural-test.html\n    - theme: alt\n      text: 1v1 面试咨询 🔥\n      link: /docs/services/1v1.md\n    - theme: alt\n      text: 内推岗位\n      link: /docs/services/job.md\n\nfeatures:\n  - title: 双越老师\n    details: 前百度、滴滴 资深工程师，wangEditor 作者，PMP，慕课网金牌讲师，国内最早讲解前端面试题的大厂讲师，学员累计 10w 人。\n  - title: 大厂面试流程\n    details: 依据大厂真实规范的面试流程，分为笔试、一面、二面、三面、HR 面试，全面覆盖每个面试环节，直接开始刷题，无需再自己查找。\n  - title: 多人共建 开源免费\n    details: 本网站源码全部开源到 Github，网站内容免费阅读，还可免费加入双越老师学员群，一起交流面试问题和技巧。\n  - title: 2025 持续更新\n    details: 前端技术变化快，每年都会有新的技术、框架和版本，也会有新的面试题，我们会根据技术变化持续维护，及时更新。\n---\n\n### 专业解决面试问题\n\n- 初入职场不知道如何写简历，如何写出内容和亮点\n- 不知道如何准备面试题，搜出很多资料，但无从下手\n- 工作几年，项目都是重复性的，写不出亮点和成绩\n- 工作快 10 年了，但还是一线开发人员，如何体现个人经验？\n- 业余不学习，基础知识很差，面试没信心\n- 工作多年只会 Vue ，不懂算法，没有技术广度和深度\n- 刚毕业，没实际项目经验\n\n如有上述问题，可 [加群](/docs/services/group.md) 讨论咨询，或参与 [1v1 面试咨询服务](/docs/services/1v1.md)， 系统、高效、全流程 准备前端面试～\n\n<!-- <div style=\"display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(224px, 1fr)); margin-top: 24px;\">\n  <div style=\"flex: 1; text-align: center;\">\n    <div style=\"background-color: var(--vp-c-bg-soft); padding: 16px 0 8px 0; border-radius: 6px; cursor: pointer;\" onclick=\"javascript:window.open('https://juejin.cn/user/1714893868765373', '_blank')\">\n      <figure style=\"width: 64px; height: 64px; border-radius: 50%; overflow: hidden; margin: 0 auto; box-shadow: var(--vp-shadow-3);\">\n        <img src=\"https://github.com/wangfupeng1988.png\" loading=\"lazy\"/>\n      </figure>\n      <div style=\"text-align: center;\">\n        <h4 style=\"font-size: 16px; line-height: 12px; font-weight: 600; text-decoration: none;\">双越老师</h4>\n        <p style=\"color: var(--vp-c-text-2); font-size: 14px; line-height: 1.5; padding: 0 16px;\">前百度、滴滴 资深工程师，wangEditor 作者，PMP</p>\n      </div>\n    </div>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <div style=\"background-color: var(--vp-c-bg-soft); padding: 16px 0 8px 0; border-radius: 6px; cursor: pointer;\">\n      <figure style=\"width: 64px; height: 64px; border-radius: 50%; overflow: hidden; margin: 0 auto; box-shadow: var(--vp-shadow-3);\" onclick=\"javascript:window.open('https://github.com/shixiaoshiOrz', '_blank')\">\n        <img src=\"https://github.com/shixiaoshiOrz.png\" loading=\"lazy\"/>\n      </figure>\n      <div style=\"text-align: center;\" onclick=\"javascript:window.open('https://juejin.cn/user/660148845294712', '_blank')\">\n        <h4 style=\"font-size: 16px; line-height: 12px; font-weight: 600;\">石小石Orz</h4>\n        <p style=\"color: var(--vp-c-text-2); font-size: 14px; line-height: 1.5; padding: 0 16px;\">掘金优秀创作者，阿里云专家博主&评测专家，文章阅读量超100万</p>\n      </div>\n    </div>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <div style=\"background-color: var(--vp-c-bg-soft); padding: 16px 0 8px 0; border-radius: 6px; cursor: pointer;\" onclick=\"javascript:window.open('https://juejin.cn/user/1943592288391496/posts', '_blank')\">\n      <figure style=\"width: 64px; height: 64px; border-radius: 50%; overflow: hidden; margin: 0 auto; box-shadow: var(--vp-shadow-3);\">\n        <img src=\"https://github.com/RainyNight9.png\" loading=\"lazy\"/>\n      </figure>\n      <div style=\"text-align: center;\">\n        <h4 style=\"font-size: 16px; line-height: 12px; font-weight: 600;\">雨夜寻晴天</h4>\n        <p style=\"color: var(--vp-c-text-2); font-size: 14px; line-height: 1.5; padding: 0 16px;\">掘金优秀创作者，开源作者，多年大厂经验，擅长Web 小程序和跨端开发</p>\n      </div>\n    </div>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <div style=\"background-color: var(--vp-c-bg-soft); padding: 16px 0 8px 0; border-radius: 6px; cursor: pointer;\" onclick=\"javascript:window.open('https://juejin.cn/user/116975171023884', '_blank')\">\n      <figure style=\"width: 64px; height: 64px; border-radius: 50%; overflow: hidden; margin: 0 auto; box-shadow: var(--vp-shadow-3);\">\n        <img src=\"https://github.com/DolphinFeng.png\" loading=\"lazy\"/>\n      </figure>\n      <div style=\"text-align: center;\">\n        <h4 style=\"font-size: 16px; line-height: 12px; font-weight: 600;\">Dolphin_海豚</h4>\n        <p style=\"color: var(--vp-c-text-2); font-size: 14px; line-height: 1.5; padding: 0 16px;\">掘金优秀作者，2023人气 NO.14 ，博客访问量 30w+，擅长各类面试题</p>\n      </div>\n    </div>\n  </div>\n</div> -->\n\n### 成为贡献者\n\n- 参与优秀开源项目，结识优秀博主和作者，积累社区知名度\n- 贡献内容时，可以插入自己的博客链接，给自己的博客引流\n\n[开始贡献题目和答案](https://github.com/mianshipai/mianshipai-web#%E8%B4%A1%E7%8C%AE%E9%A2%98%E7%9B%AE%E5%92%8C%E7%AD%94%E6%A1%88) ，[查看全部贡献者](https://github.com/mianshipai/mianshipai-web/graphs/contributors)\n\n<div style=\"display: flex; margin-top: 48px;\">\n  <div style=\"flex: 1; text-align: center;\">\n    <a href=\"https://talent.didiglobal.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/didi.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n  <div style=\"flex: 1;\">\n    <a href=\"https://jobs.bytedance.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/bd.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <a href=\"https://talent.baidu.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/baidu.png\" style=\"width: 80%\"/>\n    </a>\n  </div>\n  <div style=\"flex: 1;\">\n    <a href=\"https://hr.meituan.com/web/home\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/meituan.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n</div>\n\n<div style=\"display: flex; margin-top: 48px;\">\n  <div style=\"flex: 1; text-align: center;\">\n    <a href=\"https://careers.tencent.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/tencent.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <a href=\"https://career.huawei.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/huawei.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <a href=\"https://zhaopin.jd.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/jd.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n  <div style=\"flex: 1; text-align: center;\">\n    <a href=\"https://talent.alibaba.com/\" target=\"_blank\">\n      <img loading=\"lazy\" src=\"./docs/imgs/logos/ali.png\"  style=\"width: 80%\"/>\n    </a>\n  </div>\n</div>\n\n<div id=\"qrcode-container\" style=\"position:fixed; bottom:16px; right:0px; width:260px;\">\n  <!--config.mts 动态插入内容-->\n</div>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev\",\n    \"docs:build\": \"vitepress build\",\n    \"docs:preview\": \"vitepress preview\",\n    \"format\": \"prettier --check .\",\n    \"format:fix\": \"prettier --write --list-different .\",\n    \"prepare\": \"husky\"\n  },\n  \"devDependencies\": {\n    \"husky\": \"^9.1.7\",\n    \"prettier\": \"^3.4.2\",\n    \"vitepress\": \"^1.5.0\"\n  }\n}\n"
  }
]