[{"data":1,"prerenderedAt":3917},["ShallowReactive",2],{"\u002F2026\u002F20260528":3,"surround-\u002F2026\u002F20260528":3906},{"id":4,"title":5,"body":6,"categories":3884,"date":3886,"description":3887,"draft":3888,"extension":3889,"image":3890,"meta":3891,"navigation":3893,"path":3894,"permalink":3890,"published":3890,"readingTime":3895,"recommend":3890,"references":3890,"seo":3900,"sitemap":3901,"stem":3902,"tags":3903,"type":3904,"updated":3886,"__hash__":3905},"content\u002Fposts\u002F2026\u002F20260528.md","高频八股合集",{"type":7,"value":8,"toc":3719},"minimark",[9,14,19,52,56,66,70,81,91,95,116,120,130,134,148,156,160,178,182,202,206,213,225,229,247,253,257,263,269,273,285,299,305,321,327,344,350,363,369,382,386,402,405,408,462,468,475,481,488,511,517,534,540,546,552,560,566,590,600,604,619,654,660,668,674,695,699,714,718,722,733,761,769,772,780,783,787,803,822,828,844,850,861,867,877,883,894,900,914,918,933,939,955,961,979,985,996,1000,1012,1032,1038,1049,1055,1066,1070,1080,1086,1092,1110,1114,1120,1126,1139,1142,1153,1156,1160,1175,1181,1187,1208,1215,1220,1270,1277,1281,1287,1324,1330,1342,1351,1355,1367,1373,1385,1391,1395,1400,1419,1425,1434,1438,1441,1444,1458,1464,1485,1489,1495,1513,1519,1522,1526,1544,1547,1563,1574,1580,1589,1592,1597,1600,1629,1646,1652,1655,1680,1687,1714,1727,1733,1737,1743,1752,1758,1763,1769,1775,1779,1793,1799,1803,1813,1819,1823,1833,1837,1844,1848,1852,1857,1860,1863,1867,1876,1885,1890,1896,1905,1909,1921,1932,1938,1949,1953,1959,1965,1979,1985,1988,1992,2008,2011,2017,2020,2026,2034,2038,2044,2055,2061,2064,2068,2098,2104,2108,2114,2120,2123,2127,2136,2139,2142,2145,2149,2155,2162,2165,2169,2172,2182,2192,2198,2201,2205,2223,2227,2237,2241,2245,2256,2260,2271,2284,2293,2299,2303,2312,2326,2332,2341,2350,2354,2374,2379,2384,2392,2396,2403,2409,2413,2420,2424,2438,2442,2456,2460,2473,2477,2480,2484,2488,2495,2498,2501,2505,2511,2514,2517,2521,2527,2538,2541,2545,2551,2555,2558,2569,2575,2579,2584,2587,2593,2596,2600,2606,2610,2624,2628,2631,2637,2641,2644,2648,2651,2655,2661,2664,2668,2720,2724,2733,2745,2764,2767,2771,2774,2777,2780,2783,2786,2790,2793,2796,2799,2802,2806,2809,2812,2815,2818,2822,2829,2833,2840,2852,2859,2862,2866,2869,2872,2879,2882,2885,2889,2895,2911,2914,2918,2924,2927,2941,2944,2948,2959,2963,2966,2970,2977,2983,2986,2990,3006,3009,3012,3015,3019,3022,3028,3031,3035,3038,3041,3052,3058,3062,3069,3076,3079,3083,3086,3090,3097,3107,3113,3117,3120,3123,3126,3130,3133,3137,3140,3143,3147,3157,3180,3184,3187,3190,3194,3200,3203,3206,3210,3217,3221,3224,3228,3231,3235,3238,3242,3245,3249,3252,3256,3259,3262,3266,3269,3272,3275,3279,3282,3285,3288,3292,3295,3298,3301,3305,3308,3311,3314,3318,3321,3325,3332,3335,3339,3342,3345,3348,3352,3355,3358,3361,3365,3368,3371,3374,3378,3381,3385,3389,3396,3403,3408,3412,3415,3419,3422,3425,3428,3432,3435,3438,3441,3444,3447,3450,3454,3457,3467,3470,3474,3477,3484,3490,3494,3497,3501,3508,3511,3515,3518,3521,3527,3531,3541,3554,3560,3564,3567,3577,3583,3587,3594,3600,3604,3611,3617,3621,3631,3634,3640,3644,3654,3657,3661,3668,3672,3675,3679,3682,3686,3689,3693,3696,3699],[10,11,13],"h2",{"id":12},"html-css","HTML \u002F CSS",[15,16,18],"h3",{"id":17},"_1-html-语义化","1. HTML 语义化",[20,21,22,26,27,31,32,35,36,39,40,43,44,47,48,51],"p",{},[23,24,25],"strong",{},"HTML 语义化","就是用合适的标签表达内容含义，比如导航用 ",[28,29,30],"code",{"code":30},"nav","，主体用 ",[28,33,34],{"code":34},"main","，文章用 ",[28,37,38],{"code":38},"article","，而不是全部用 ",[28,41,42],{"code":42},"div","。它的价值主要体现在 ",[23,45,46],{},"SEO","、",[23,49,50],{},"可访问性"," 和代码可维护性上，因为搜索引擎、读屏器和开发者都能更容易理解页面结构。",[15,53,55],{"id":54},"_2-doctype-与标准模式","2. DOCTYPE 与标准模式",[20,57,58,61,62,65],{},[28,59,60],{"code":60},"\u003C!DOCTYPE html>"," 用来告诉浏览器按 HTML5 标准解析页面。如果不写或写错，浏览器可能进入",[23,63,64],{},"怪异模式","，导致盒模型、布局计算等行为和标准模式不一致，页面在不同浏览器中更容易出现兼容问题。",[15,67,69],{"id":68},"_3-meta-viewport","3. meta viewport",[20,71,72,73,76,77,80],{},"移动端常见的 ",[28,74,75],{"code":75},"meta viewport"," 是为了控制布局视口宽度，典型写法是 ",[28,78,79],{"code":79},"width=device-width, initial-scale=1.0","。如果不设置，移动端浏览器可能按桌面宽度渲染页面，再整体缩小，导致文字和布局异常。",[82,83,89],"pre",{"className":84,"code":86,"language":87,"meta":88},[85],"language-html","\u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \u002F>\n","html","",[28,90,86],{"__ignoreMap":88},[15,92,94],{"id":93},"_4-script-的-async-与-defer","4. script 的 async 与 defer",[20,96,97,98,101,102,105,106,109,110,112,113,115],{},"普通 ",[28,99,100],{"code":100},"script"," 会阻塞 HTML 解析；",[23,103,104],{},"async"," 是脚本下载完就立即执行，执行顺序不稳定；",[23,107,108],{},"defer"," 是脚本下载不阻塞解析，并在 DOM 解析完成后按顺序执行。业务脚本通常更适合用 ",[28,111,108],{"code":108},"，第三方统计这类独立脚本更适合用 ",[28,114,104],{"code":104},"。",[15,117,119],{"id":118},"_5-preload-prefetch","5. preload \u002F prefetch",[20,121,122,125,126,129],{},[23,123,124],{},"preload"," 是提前加载当前页面马上要用的关键资源，比如首屏字体、首屏大图、关键 JS；",[23,127,128],{},"prefetch"," 是浏览器空闲时预取未来可能用到的资源，比如下一页路由包。二者不要滥用，否则会抢占关键资源带宽，反而影响首屏。",[15,131,133],{"id":132},"_6-盒模型","6. 盒模型",[20,135,136,137,140,141,144,145,147],{},"CSS 盒模型分为 content、padding、border、margin。标准盒模型中 ",[28,138,139],{"code":139},"width"," 只表示 content 宽度，而 ",[28,142,143],{"code":143},"border-box"," 会把 padding 和 border 也算进 width，项目里常统一设置为 ",[28,146,143],{"code":143}," 来降低布局计算成本。",[82,149,154],{"className":150,"code":152,"language":153,"meta":88},[151],"language-css","* {\n  box-sizing: border-box;\n}\n","css",[28,155,152],{"__ignoreMap":88},[15,157,159],{"id":158},"_7-bfc","7. BFC",[20,161,162,165,166,47,169,47,172,47,175,115],{},[23,163,164],{},"BFC"," 是块级格式化上下文，可以理解为一块独立的布局区域，内部元素布局不会影响外部。常用它解决 margin 折叠、清除浮动、防止文字环绕浮动元素等问题，触发方式包括 ",[28,167,168],{"code":168},"overflow: hidden",[28,170,171],{"code":171},"display: flow-root",[28,173,174],{"code":174},"position: absolute",[28,176,177],{"code":177},"display: flex",[15,179,181],{"id":180},"_8-层叠上下文与-z-index","8. 层叠上下文与 z-index",[20,183,184,187,188,47,191,47,194,197,198,201],{},[23,185,186],{},"层叠上下文","决定元素在 z 轴上的绘制层级，",[28,189,190],{"code":190},"position + z-index",[28,192,193],{"code":193},"transform",[28,195,196],{"code":196},"opacity \u003C 1"," 等都会创建新的层叠上下文。很多时候 ",[28,199,200],{"code":200},"z-index"," 不生效，不是数值不够大，而是它被限制在父级层叠上下文内部。",[15,203,205],{"id":204},"_9-选择器优先级","9. 选择器优先级",[20,207,208,209,212],{},"CSS 优先级大致是：内联样式 > ID 选择器 > class \u002F 属性 \u002F 伪类 > 标签 \u002F 伪元素。实际项目中应避免层级过深和滥用 ",[28,210,211],{"code":211},"!important","，否则样式覆盖关系会变得很难维护。",[20,214,215,216,220,221,224],{},"内联样式        最高\nID 选择器       #app\n类\u002F属性\u002F伪类     .box ",[217,218,219],"span",{},"type=\"text\""," ",[222,223],"hover",{},"\n标签\u002F伪元素      div p ::before\n通配符           *",[15,226,228],{"id":227},"_10-flex-布局","10. Flex 布局",[20,230,231,234,235,238,239,242,243,246],{},[23,232,233],{},"Flex"," 是一维布局方案，适合处理一行或一列内的排列、对齐和剩余空间分配。主轴用 ",[28,236,237],{"code":237},"justify-content"," 控制，交叉轴用 ",[28,240,241],{"code":241},"align-items"," 控制，",[28,244,245],{"code":245},"flex: 1"," 常用于让元素占满剩余空间。",[82,248,251],{"className":249,"code":250,"language":153,"meta":88},[151],".row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n",[28,252,250],{"__ignoreMap":88},[15,254,256],{"id":255},"_11-grid-布局","11. Grid 布局",[20,258,259,262],{},[23,260,261],{},"Grid"," 是二维布局方案，适合同时控制行和列，比如后台看板、图片墙、复杂表单。简单横向排列用 Flex 更轻，复杂区域划分用 Grid 更直观。",[82,264,267],{"className":265,"code":266,"language":153,"meta":88},[151],".grid {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 16px;\n}\n",[28,268,266],{"__ignoreMap":88},[15,270,272],{"id":271},"_12-水平垂直居中","12. 水平垂直居中",[20,274,275,276,278,279,281,282,284],{},"水平垂直居中的核心是：让子元素在父容器的主轴和交叉轴上都处于中间位置。实际开发中最常用的是 ",[23,277,233],{}," 和 ",[23,280,261],{},"，因为它们不依赖子元素固定宽高，也不需要手动计算偏移量。绝对定位加 ",[28,283,193],{"code":193}," 也很常见，适合弹窗、浮层、提示框这类脱离文档流的元素。",[20,286,287,290,291,294,295,298],{},[23,288,289],{},"Flex 居中","适合一维布局，比如按钮内容、空状态、登录框、卡片内容居中。父元素设置为弹性容器后，",[28,292,293],{"code":293},"justify-content: center"," 控制主轴居中，",[28,296,297],{"code":297},"align-items: center"," 控制交叉轴居中。",[82,300,303],{"className":301,"code":302,"language":153,"meta":88},[151],".parent {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n",[28,304,302],{"__ignoreMap":88},[20,306,307,310,311,314,315,278,317,320],{},[23,308,309],{},"Grid 居中","写法更简洁，适合只有一个核心子元素需要居中的场景。",[28,312,313],{"code":313},"place-items: center"," 是 ",[28,316,297],{"code":297},[28,318,319],{"code":319},"justify-items: center"," 的简写。",[82,322,325],{"className":323,"code":324,"language":153,"meta":88},[151],".center {\n  display: grid;\n  place-items: center;\n}\n",[28,326,324],{"__ignoreMap":88},[20,328,329,332,333,278,336,339,340,343],{},[23,330,331],{},"绝对定位 + transform"," 适合需要脱离普通文档流的元素。",[28,334,335],{"code":335},"top: 50%",[28,337,338],{"code":338},"left: 50%"," 是把元素左上角移动到父容器中心，",[28,341,342],{"code":342},"transform: translate(-50%, -50%)"," 再按元素自身宽高往回移动一半，从而实现真正居中。",[82,345,348],{"className":346,"code":347,"language":153,"meta":88},[151],".parent {\n  position: relative;\n}\n\n.child {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n",[28,349,347],{"__ignoreMap":88},[20,351,352,353,356,357,360,361,115],{},"如果元素宽高固定，也可以用绝对定位四边为 ",[28,354,355],{"code":355},"0"," 配合 ",[28,358,359],{"code":359},"margin: auto","，但它依赖明确的尺寸，灵活性不如 ",[28,362,193],{"code":193},[82,364,367],{"className":365,"code":366,"language":153,"meta":88},[151],".child {\n  position: absolute;\n  inset: 0;\n  width: 200px;\n  height: 120px;\n  margin: auto;\n}\n",[28,368,366],{"__ignoreMap":88},[20,370,371,372,374,375,378,379,381],{},"面试回答时可以按这个顺序说：优先使用 Flex 或 Grid；如果元素是弹窗、浮层等脱离文档流的场景，使用绝对定位加 ",[28,373,193],{"code":193},"；如果宽高固定，也可以用 ",[28,376,377],{"code":377},"inset: 0"," 加 ",[28,380,359],{"code":359},"。其中 Flex\u002FGrid 更现代、更通用，绝对定位方案更适合特殊定位场景。",[15,383,385],{"id":384},"_13-响应式布局","13. 响应式布局",[20,387,388,391,392,47,395,47,398,401],{},[23,389,390],{},"响应式布局","的核心是让页面在不同屏幕宽度下保持可读、可用，常用手段包括媒体查询、弹性布局、百分比、",[28,393,394],{"code":394},"rem",[28,396,397],{"code":397},"vw",[28,399,400],{"code":400},"clamp()","。现代项目更推荐用布局容器、断点和弹性单位组合，而不是只按设计稿等比缩放。",[20,403,404],{},"面试里可以先从目标说起：响应式不是简单把页面“缩小”，而是让内容、布局、字号、间距和交互区域根据设备宽度重新组织。比如桌面端三栏展示，平板端两栏，手机端一栏；导航从横向菜单变成折叠菜单；图片和表格需要避免撑破容器。",[20,406,407],{},"常见实现方式可以分为四类：",[409,410,411,418,424,446],"ol",{},[412,413,414,417],"li",{},[23,415,416],{},"弹性布局","：使用 Flex 或 Grid，让容器自动分配空间，减少固定宽度。",[412,419,420,423],{},[23,421,422],{},"媒体查询","：在关键断点切换布局，比如手机、平板、桌面。",[412,425,426,429,430,47,433,47,436,439,440,47,442,47,444,115],{},[23,427,428],{},"弹性单位","：宽度优先用 ",[28,431,432],{"code":432},"%",[28,434,435],{"code":435},"fr",[28,437,438],{"code":438},"minmax()","，字号和间距可以结合 ",[28,441,394],{"code":394},[28,443,397],{"code":397},[28,445,400],{"code":400},[412,447,448,451,452,47,455,47,458,461],{},[23,449,450],{},"资源适配","：图片使用 ",[28,453,454],{"code":454},"max-width: 100%",[28,456,457],{"code":457},"picture",[28,459,460],{"code":460},"srcset","，避免小屏加载过大的图片。",[82,463,466],{"className":464,"code":465,"language":153,"meta":88},[151],".page {\n  width: min(100% - 32px, 1200px);\n  margin: 0 auto;\n}\n\n.cards {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n  gap: 16px;\n}\n",[28,467,465],{"__ignoreMap":88},[20,469,470,471,474],{},"媒体查询适合处理“结构变化”，比如列数、侧边栏、导航形态，而不是每个尺寸都写一套样式。实际项目中常用移动端优先：基础样式先照顾小屏，再用 ",[28,472,473],{"code":473},"min-width"," 逐步增强大屏布局。",[82,476,479],{"className":477,"code":478,"language":153,"meta":88},[151],".layout {\n  display: grid;\n  gap: 16px;\n}\n\n@media (min-width: 768px) {\n  .layout {\n    grid-template-columns: 240px 1fr;\n  }\n}\n\n@media (min-width: 1024px) {\n  .layout {\n    grid-template-columns: 280px 1fr 320px;\n  }\n}\n",[28,480,478],{"__ignoreMap":88},[20,482,483,47,485,487],{},[28,484,394],{"code":394},[28,486,397],{"code":397}," 和媒体查询可以理解成三个工具，各自解决的问题不一样。",[20,489,490,493,494,496,497,499,500,503,504,47,507,510],{},[23,491,492],{},"rem 适合做统一尺寸控制","，比如字号、间距、按钮高度、卡片内边距。",[28,495,394],{"code":394}," 依赖根元素 ",[28,498,87],{"code":87}," 的 ",[28,501,502],{"code":502},"font-size","，所以只要根字号确定，页面里的 ",[28,505,506],{"code":506},"1rem",[28,508,509],{"code":509},"2rem"," 都会按同一套基准计算。",[82,512,515],{"className":513,"code":514,"language":153,"meta":88},[151],"html {\n  font-size: 16px;\n}\n\n.card {\n  padding: 1rem; \u002F* 16px *\u002F\n  font-size: 1rem; \u002F* 16px *\u002F\n}\n",[28,516,514],{"__ignoreMap":88},[20,518,519,522,523,526,527,530,531,533],{},[23,520,521],{},"vw 适合做跟屏幕宽度强相关的尺寸","，",[28,524,525],{"code":525},"1vw"," 等于视口宽度的 ",[28,528,529],{"code":529},"1%","，屏幕越宽，计算出来的值越大。它适合标题、横幅、某些需要随屏幕流式变化的尺寸，但纯 ",[28,532,397],{"code":397}," 容易在小屏上太小、大屏上太大。",[82,535,538],{"className":536,"code":537,"language":153,"meta":88},[151],".title {\n  font-size: 4vw;\n}\n",[28,539,537],{"__ignoreMap":88},[20,541,542,545],{},[23,543,544],{},"媒体查询适合做布局结构切换","，比如手机端一列、平板两列、桌面三列。这类变化不是简单缩放，而是布局结构变了，所以更适合用媒体查询。",[82,547,550],{"className":548,"code":549,"language":153,"meta":88},[151],".list {\n  display: grid;\n  grid-template-columns: 1fr;\n}\n\n@media (min-width: 768px) {\n  .list {\n    grid-template-columns: repeat(3, 1fr);\n  }\n}\n",[28,551,549],{"__ignoreMap":88},[20,553,554,556,557,559],{},[28,555,400],{"code":400}," 可以给流式尺寸设置上下限，常用来弥补纯 ",[28,558,397],{"code":397}," 失控的问题。它的三个参数分别是最小值、理想值、最大值。",[82,561,564],{"className":562,"code":563,"language":153,"meta":88},[151],".title {\n  font-size: clamp(20px, 4vw, 40px);\n}\n",[28,565,563],{"__ignoreMap":88},[20,567,568,569,572,573,576,577,580,581,583,584,586,587,589],{},"上面这句的意思是：字号最小不低于 ",[28,570,571],{"code":571},"20px","，中间尽量按 ",[28,574,575],{"code":575},"4vw"," 跟随屏幕变化，最大不超过 ",[28,578,579],{"code":579},"40px","。所以面试时可以总结为：",[28,582,394],{"code":394}," 管统一尺寸，",[28,585,397],{"code":397}," 管随屏幕流式变化，媒体查询管布局结构切换，",[28,588,400],{"code":400}," 给变化范围兜底。",[20,591,592,593,47,596,599],{},"响应式布局还要注意几个常见坑：不要大量写死 ",[28,594,595],{"code":595},"width: 375px",[28,597,598],{"code":598},"height: 100vh","；长文本、表格、图片要允许换行或滚动；按钮点击区域在移动端不能太小；断点应该根据内容何时放不下决定，而不是机械照搬设备型号。面试总结时可以说：我会先用流式布局保证基础伸缩，再在关键断点调整结构，最后用弹性单位和图片适配补齐细节。",[15,601,603],{"id":602},"_14-回流与重绘","14. 回流与重绘",[20,605,606,609,610,613,614,278,616,115],{},[23,607,608],{},"回流","是布局发生变化，比如宽高、位置、字体大小改变；",[23,611,612],{},"重绘","是视觉样式变化，比如颜色、背景、阴影改变。回流成本通常更高，优化时要减少频繁读写布局，动画尽量使用 ",[28,615,193],{"code":193},[28,617,618],{"code":618},"opacity",[20,620,621,622,47,624,47,627,47,630,47,633,47,636,47,639,47,642,47,645,47,648,47,650,653],{},"做动画时要尽量少改会影响布局的属性，比如 ",[28,623,139],{"code":139},[28,625,626],{"code":626},"height",[28,628,629],{"code":629},"top",[28,631,632],{"code":632},"right",[28,634,635],{"code":635},"bottom",[28,637,638],{"code":638},"left",[28,640,641],{"code":641},"margin",[28,643,644],{"code":644},"padding",[28,646,647],{"code":647},"border-width",[28,649,502],{"code":502},[28,651,652],{"code":652},"line-height","。这些属性会改变元素尺寸或位置，容易触发回流。",[82,655,658],{"className":656,"code":657,"language":153,"meta":88},[151],"\u002F* 不推荐：会影响布局 *\u002F\n.box:hover {\n  width: 300px;\n  left: 100px;\n}\n",[28,659,657],{"__ignoreMap":88},[20,661,662,663,665,666,115],{},"更推荐把位置和缩放交给 ",[28,664,193],{"code":193},"，把显隐过渡交给 ",[28,667,618],{"code":618},[82,669,672],{"className":670,"code":671,"language":153,"meta":88},[151],"\u002F* 推荐：更适合动画 *\u002F\n.box:hover {\n  transform: translateX(100px) scale(1.2);\n  opacity: 0.8;\n}\n",[28,673,671],{"__ignoreMap":88},[20,675,676,677,47,680,47,683,47,686,689,690,278,692,694],{},"另外，",[28,678,679],{"code":679},"box-shadow",[28,681,682],{"code":682},"background-color",[28,684,685],{"code":685},"filter",[28,687,688],{"code":688},"border-radius"," 这类属性通常不会像宽高位置那样触发布局，但可能造成较重的重绘。尤其是大面积阴影、模糊滤镜、多个元素同时变化时，也要谨慎使用。面试时可以总结为：动画优先用 ",[28,691,193],{"code":193},[28,693,618],{"code":618},"；少改宽高、位置、间距、字体这类布局属性；大面积阴影、模糊和背景变化也要注意重绘成本。",[15,696,698],{"id":697},"_15-css-动画性能","15. CSS 动画性能",[20,700,701,704,705,708,709,47,711,713],{},[28,702,703],{"code":703},"transition"," 适合简单状态切换，",[28,706,707],{"code":707},"animation + keyframes"," 适合复杂连续动画。性能上应优先改变 ",[28,710,193],{"code":193},[28,712,618],{"code":618},"，因为它们通常可以走合成线程，避免频繁触发布局和绘制。",[10,715,717],{"id":716},"javascript","JavaScript",[15,719,721],{"id":720},"_1-js-数据类型","1. JS 数据类型",[20,723,724,725,728,729,732],{},"JavaScript 数据类型可以先分成两大类：",[23,726,727],{},"原始类型","和",[23,730,731],{},"引用类型","。原始类型保存的是值本身，引用类型保存的是对象的引用地址。",[20,734,735,736,47,739,47,742,47,745,47,748,47,751,47,754,757,758,115],{},"原始类型有 7 种：",[28,737,738],{"code":738},"string",[28,740,741],{"code":741},"number",[28,743,744],{"code":744},"boolean",[28,746,747],{"code":747},"undefined",[28,749,750],{"code":750},"null",[28,752,753],{"code":753},"symbol",[28,755,756],{"code":756},"bigint","。可以用口诀记：",[23,759,760],{},"字数布，空未符大",[82,762,767],{"className":763,"code":765,"language":766,"meta":88},[764],"language-text","字：string      字符串\n数：number      数字\n布：boolean     布尔值\n空：null        空值\n未：undefined   未定义\n符：symbol      唯一符号\n大：bigint      大整数\n","text",[28,768,765],{"__ignoreMap":88},[20,770,771],{},"引用类型主要是对象，包括普通对象、数组、函数、日期、正则等。它们赋值时复制的是引用地址，所以两个变量可能指向同一个对象。",[82,773,778],{"className":774,"code":776,"language":777,"meta":88},[775],"language-ts","const a = { count: 1 };\nconst b = a;\n\nb.count = 2;\nconsole.log(a.count); \u002F\u002F 2\nSNB U Null SB\n","ts",[28,779,776],{"__ignoreMap":88},[20,781,782],{},"面试时可以这样说：原始类型按值存储，引用类型按引用访问，所以引用类型修改内部属性时，其他指向同一对象的变量也能看到变化。",[15,784,786],{"id":785},"_2-类型判断","2. 类型判断",[20,788,789,790,47,793,47,796,278,799,802],{},"JS 里常见的类型判断方法有 ",[28,791,792],{"code":792},"typeof",[28,794,795],{"code":795},"instanceof",[28,797,798],{"code":798},"Array.isArray()",[28,800,801],{"code":801},"Object.prototype.toString.call()","。面试时可以按“先判断基础类型，再判断复杂对象”的思路回答。",[20,804,805,808,809,47,811,47,813,47,815,47,817,47,819,821],{},[23,806,807],{},"typeof 适合判断原始类型","，比如 ",[28,810,738],{"code":738},[28,812,741],{"code":741},[28,814,744],{"code":744},[28,816,747],{"code":747},[28,818,753],{"code":753},[28,820,756],{"code":756},"，也可以判断函数。它的优点是写法简单，缺点是判断对象时不够细。",[82,823,826],{"className":824,"code":825,"language":777,"meta":88},[775],"typeof \"hello\"; \u002F\u002F \"string\"\ntypeof 123; \u002F\u002F \"number\"\ntypeof true; \u002F\u002F \"boolean\"\ntypeof undefined; \u002F\u002F \"undefined\"\ntypeof Symbol(\"id\"); \u002F\u002F \"symbol\"\ntypeof 10n; \u002F\u002F \"bigint\"\ntypeof (() => {}); \u002F\u002F \"function\"\n",[28,827,825],{"__ignoreMap":88},[20,829,830,831,834,835,838,839,841,842,115],{},"需要注意两个坑：",[28,832,833],{"code":833},"typeof null"," 的结果是 ",[28,836,837],{"code":837},"\"object\"","，这是历史遗留问题；数组、普通对象、日期对象用 ",[28,840,792],{"code":792}," 判断出来也都是 ",[28,843,837],{"code":837},[82,845,848],{"className":846,"code":847,"language":777,"meta":88},[775],"typeof null; \u002F\u002F \"object\"\ntypeof []; \u002F\u002F \"object\"\ntypeof {}; \u002F\u002F \"object\"\ntypeof new Date(); \u002F\u002F \"object\"\n",[28,849,847],{"__ignoreMap":88},[20,851,852,855,856,278,858,860],{},[23,853,854],{},"Array.isArray() 专门判断数组","，比 ",[28,857,792],{"code":792},[28,859,795],{"code":795}," 更直接。",[82,862,865],{"className":863,"code":864,"language":777,"meta":88},[775],"Array.isArray([]); \u002F\u002F true\nArray.isArray({}); \u002F\u002F false\n",[28,866,864],{"__ignoreMap":88},[20,868,869,872,873,876],{},[23,870,871],{},"instanceof 用来判断对象是否在某个构造函数的原型链上","。它适合判断某个对象是不是由某个类或构造函数创建出来的，比如 ",[28,874,875],{"code":875},"date instanceof Date","。但它不适合判断原始类型，而且在 iframe、跨窗口场景下可能不稳定。",[82,878,881],{"className":879,"code":880,"language":777,"meta":88},[775],"const date = new Date();\n\ndate instanceof Date; \u002F\u002F true\n[] instanceof Array; \u002F\u002F true\n{} instanceof Object; \u002F\u002F true\n\"hello\" instanceof String; \u002F\u002F false\n",[28,882,880],{"__ignoreMap":88},[20,884,885,888,889,47,891,893],{},[23,886,887],{},"Object.prototype.toString.call() 更适合做精确类型判断","，它可以区分数组、对象、日期、正则、",[28,890,750],{"code":750},[28,892,747],{"code":747}," 等。",[82,895,898],{"className":896,"code":897,"language":777,"meta":88},[775],"const getType = (value: unknown) =>\n  Object.prototype.toString.call(value).slice(8, -1).toLowerCase();\n\ngetType(\"hello\"); \u002F\u002F \"string\"\ngetType([]); \u002F\u002F \"array\"\ngetType({}); \u002F\u002F \"object\"\ngetType(new Date()); \u002F\u002F \"date\"\ngetType(\u002Fabc\u002F); \u002F\u002F \"regexp\"\ngetType(null); \u002F\u002F \"null\"\ngetType(undefined); \u002F\u002F \"undefined\"\n",[28,899,897],{"__ignoreMap":88},[20,901,902,903,905,906,908,909,911,912,115],{},"所以实际使用时可以这样记：普通原始类型用 ",[28,904,792],{"code":792},"；数组用 ",[28,907,798],{"code":798},"；判断实例关系用 ",[28,910,795],{"code":795},"；如果要写一个通用的精确类型判断函数，就用 ",[28,913,801],{"code":801},[15,915,917],{"id":916},"_3-与","3. == 与 ===",[20,919,920,278,923,926,927,929,930,932],{},[28,921,922],{"code":922},"==",[28,924,925],{"code":925},"==="," 的核心区别是：",[28,928,922],{"code":922}," 会先做隐式类型转换，",[28,931,925],{"code":925}," 不会转换类型。",[82,934,937],{"className":935,"code":936,"language":777,"meta":88},[775],"1 == \"1\"; \u002F\u002F true\n1 === \"1\"; \u002F\u002F false\n",[28,938,936],{"__ignoreMap":88},[20,940,941,942,944,945,947,948,314,951,954],{},"工程里更推荐 ",[28,943,925],{"code":925},"，因为它的判断规则更直接，不容易出现奇怪结果。",[28,946,922],{"code":922}," 的转换规则很多，比如字符串和数字比较时会尝试把字符串转成数字，",[28,949,950],{"code":950},"null == undefined",[28,952,953],{"code":953},"true","，但它们和其他值比较又不相等。",[82,956,959],{"className":957,"code":958,"language":777,"meta":88},[775],"null == undefined; \u002F\u002F true\nnull === undefined; \u002F\u002F false\n\"\" == 0; \u002F\u002F true\nfalse == 0; \u002F\u002F true\n",[28,960,958],{"__ignoreMap":88},[20,962,963,278,966,968,969,972,973,278,976,115],{},[28,964,965],{"code":965},"Object.is",[28,967,925],{"code":925}," 大部分时候类似，但它能正确判断 ",[28,970,971],{"code":971},"NaN","，也能区分 ",[28,974,975],{"code":975},"+0",[28,977,978],{"code":978},"-0",[82,980,983],{"className":981,"code":982,"language":777,"meta":88},[775],"NaN === NaN; \u002F\u002F false\nObject.is(NaN, NaN); \u002F\u002F true\n\n+0 === -0; \u002F\u002F true\nObject.is(+0, -0); \u002F\u002F false\n",[28,984,982],{"__ignoreMap":88},[20,986,987,988,990,991,993,994,115],{},"面试总结：业务代码优先用 ",[28,989,925],{"code":925},"；需要处理 ",[28,992,971],{"code":971}," 或区分正负零时，可以考虑 ",[28,995,965],{"code":965},[15,997,999],{"id":998},"_4-var-let-const","4. var \u002F let \u002F const",[20,1001,1002,47,1005,47,1008,1011],{},[28,1003,1004],{"code":1004},"var",[28,1006,1007],{"code":1007},"let",[28,1009,1010],{"code":1010},"const"," 的区别主要看三个点：作用域、变量提升、能不能重新赋值。",[20,1013,1014,1016,1017,115,1019,278,1021,1023,1024,1027,1028,1031],{},[28,1015,1004],{"code":1004}," 是函数作用域，会变量提升，所以在声明前访问不会报错，而是得到 ",[28,1018,747],{"code":747},[28,1020,1007],{"code":1007},[28,1022,1010],{"code":1010}," 是块级作用域，只在 ",[28,1025,1026],{"code":1026},"{}"," 内有效，并且存在",[23,1029,1030],{},"暂时性死区","，声明前访问会报错。",[82,1033,1036],{"className":1034,"code":1035,"language":777,"meta":88},[775],"console.log(a); \u002F\u002F undefined\nvar a = 1;\n\nconsole.log(b); \u002F\u002F ReferenceError\nlet b = 2;\n",[28,1037,1035],{"__ignoreMap":88},[20,1039,1040,1042,1043,1045,1046,1048],{},[28,1041,1007],{"code":1007}," 可以重新赋值，",[28,1044,1010],{"code":1010}," 不能重新绑定。但 ",[28,1047,1010],{"code":1010}," 限制的是变量和地址的绑定，不代表对象内部属性不能改。",[82,1050,1053],{"className":1051,"code":1052,"language":777,"meta":88},[775],"const user = { name: \"A\" };\nuser.name = \"B\"; \u002F\u002F 可以\n\n\u002F\u002F user = { name: \"C\" }; \u002F\u002F 不可以\n",[28,1054,1052],{"__ignoreMap":88},[20,1056,1057,1058,1060,1061,1063,1064,115],{},"面试总结：优先用 ",[28,1059,1010],{"code":1010},"，需要重新赋值再用 ",[28,1062,1007],{"code":1007},"，尽量不用 ",[28,1065,1004],{"code":1004},[15,1067,1069],{"id":1068},"_5-作用域与作用域链","5. 作用域与作用域链",[20,1071,1072,1075,1076,1079],{},[23,1073,1074],{},"作用域","就是变量能被访问的范围。JS 使用的是",[23,1077,1078],{},"词法作用域","，也就是函数写在哪里，它能访问哪些外部变量就基本确定了，不是看函数在哪里被调用。",[20,1081,1082,1083,115],{},"查找变量时，会先找当前作用域；当前找不到，就去外层作用域找；一直找到全局作用域。这条从内到外的查找路径就是",[23,1084,1085],{},"作用域链",[82,1087,1090],{"className":1088,"code":1089,"language":777,"meta":88},[775],"const name = \"global\";\n\nfunction outer() {\n  const name = \"outer\";\n\n  function inner() {\n    console.log(name);\n  }\n\n  inner();\n}\n\nouter(); \u002F\u002F \"outer\"\n",[28,1091,1089],{"__ignoreMap":88},[20,1093,1094,1095,1098,1099,1102,1103,1106,1107,1109],{},"上面 ",[28,1096,1097],{"code":1097},"inner"," 里面没有 ",[28,1100,1101],{"code":1101},"name","，就向外找到 ",[28,1104,1105],{"code":1105},"outer"," 里的 ",[28,1108,1101],{"code":1101},"。面试时可以一句话总结：作用域决定变量可见范围，作用域链决定变量查找顺序。",[15,1111,1113],{"id":1112},"_6-闭包","6. 闭包",[20,1115,1116,1119],{},[23,1117,1118],{},"闭包","可以先理解成：函数“记住了”它定义时能访问的外部变量。即使外层函数已经执行结束，只要内部函数还被外部引用，它仍然能继续访问那些变量。",[82,1121,1124],{"className":1122,"code":1123,"language":777,"meta":88},[775],"const createCounter = () => {\n  let count = 0;\n  return () => ++count;\n};\n\nconst next = createCounter();\nnext(); \u002F\u002F 1\nnext(); \u002F\u002F 2\n",[28,1125,1123],{"__ignoreMap":88},[20,1127,1094,1128,1131,1132,1135,1136,1138],{},[28,1129,1130],{"code":1130},"createCounter"," 执行完以后，按理说 ",[28,1133,1134],{"code":1134},"count"," 应该结束了。但返回的函数还在使用 ",[28,1137,1134],{"code":1134},"，所以这个变量没有被释放，这就是闭包。",[20,1140,1141],{},"闭包常见用途有三个：",[409,1143,1144,1147,1150],{},[412,1145,1146],{},"保存私有变量，比如计数器。",[412,1148,1149],{},"缓存中间结果，比如记忆化函数。",[412,1151,1152],{},"做函数柯里化或延迟执行。",[20,1154,1155],{},"闭包的风险是：如果闭包长期引用大对象、DOM 节点、定时器数据，这些内容可能无法被垃圾回收，造成内存泄漏。面试总结：闭包的本质是函数保留了外层作用域的变量引用。",[15,1157,1159],{"id":1158},"_7-原型与原型链","7. 原型与原型链",[20,1161,1162,1163,1166,1167,1170,1171,1174],{},"JS 里对象可以通过",[23,1164,1165],{},"原型","共享属性和方法。每个对象都有一个内部原型 ",[28,1168,1169],{"code":1169},"[[Prototype]]","，平时常见的 ",[28,1172,1173],{"code":1173},"__proto__"," 可以理解成访问它的方式。",[20,1176,1177,1178,115],{},"属性查找规则是：先找对象自身，找不到再沿着原型继续往上找，这条链就是",[23,1179,1180],{},"原型链",[82,1182,1185],{"className":1183,"code":1184,"language":777,"meta":88},[775],"function Person(name) {\n    this.name = name; \u002F\u002F 私人财产\n}\n\n\u002F\u002F 在公共仓库（原型）中放入一个方法\nPerson.prototype.sayHello = function() {\n    console.log(\"你好，我是 \" + this.name);\n};\n\nconst user1 = new Person(\"张三\");\nconst user2 = new Person(\"李四\");\n\n\u002F\u002F user1 和 user2 都可以使用公共仓库里的方法\nuser1.sayHello(); \u002F\u002F \"你好，我是 张三\"\nuser2.sayHello(); \u002F\u002F \"你好，我是 李四\"\n",[28,1186,1184],{"__ignoreMap":88},[20,1188,1189,1190,1193,1194,278,1197,1200,1201,1203,1204,1207],{},"在这里，",[28,1191,1192],{"code":1192},"Person.prototype"," 就是 ",[28,1195,1196],{"code":1196},"user1",[28,1198,1199],{"code":1199},"user2"," 的",[23,1202,1165],{},"。这样做的好处是：",[28,1205,1206],{"code":1206},"sayHello"," 方法在内存中只有一份，所有的实例共享它，极大地节省了内存。",[20,1209,1210,1211,1214],{},"当你试图访问一个对象的某个属性或方法时，JavaScript 引擎会执行一套“甩锅机制”",[23,1212,1213],{},"，这条甩锅的路径就是","原型链。",[20,1216,1217],{},[23,1218,1219],{},"查找规则如下：",[409,1221,1222,1228,1241,1251],{},[412,1223,1224,1227],{},[23,1225,1226],{},"先找自己："," 引擎首先在对象本身的属性中查找。如果找到了，就直接用。",[412,1229,1230,1233,1234,1236,1237,1240],{},[23,1231,1232],{},"问上级（原型）："," 如果自己身上没有，引擎就会通过隐藏的 ",[28,1235,1173],{"code":1173}," 属性，顺藤摸瓜去该对象的",[23,1238,1239],{},"原型对象","（公共仓库）里找。",[412,1242,1243,1246,1247,1250],{},[23,1244,1245],{},"继续向上："," 如果原型对象里也没有，就会继续去",[23,1248,1249],{},"原型的原型","里找。",[412,1252,1253,1256,1257,1260,1261,1263,1264,1266,1267,1269],{},[23,1254,1255],{},"终点："," 这样一层一层找下去，直到找到最顶层的 ",[28,1258,1259],{"code":1259},"Object.prototype","。如果连 ",[28,1262,1259],{"code":1259}," 的原型（即 ",[28,1265,750],{"code":750},"）都没有，就会返回 ",[28,1268,747],{"code":747},"（或者报错说这不是一个函数）。",[20,1271,1272,1273,1276],{},"面试总结：原型用于对象之间共享能力，原型链用于属性查找；构造函数的 ",[28,1274,1275],{"code":1275},"prototype"," 会成为实例的原型。",[15,1278,1280],{"id":1279},"_8-this-指向","8. this 指向",[20,1282,1283,1286],{},[28,1284,1285],{"code":1285},"this"," 不看函数在哪里定义，主要看函数怎么被调用。可以按四条规则记：",[409,1288,1289,1295,1301,1315],{},[412,1290,1291,1292,1294],{},"普通函数直接调用：严格模式下是 ",[28,1293,747],{"code":747},"，非严格模式下通常指向全局对象。",[412,1296,1297,1298,1300],{},"对象方法调用：谁点出来调用，",[28,1299,1285],{"code":1285}," 就指向谁。",[412,1302,1303,47,1306,47,1309,1312,1313,115],{},[28,1304,1305],{"code":1305},"call",[28,1307,1308],{"code":1308},"apply",[28,1310,1311],{"code":1311},"bind","：显式指定 ",[28,1314,1285],{"code":1285},[412,1316,1317,1320,1321,1323],{},[28,1318,1319],{"code":1319},"new"," 调用：",[28,1322,1285],{"code":1285}," 指向新创建的对象。",[82,1325,1328],{"className":1326,"code":1327,"language":777,"meta":88},[775],"const user = {\n  name: \"Tom\",\n  say() {\n    return this.name;\n  },\n};\n\nuser.say(); \u002F\u002F \"Tom\"\n",[28,1329,1327],{"__ignoreMap":88},[20,1331,1332,1333,1335,1336,1338,1339,1341],{},"箭头函数没有自己的 ",[28,1334,1285],{"code":1285},"，它会捕获外层作用域的 ",[28,1337,1285],{"code":1285},"，所以不能用箭头函数当需要动态 ",[28,1340,1285],{"code":1285}," 的对象方法。",[20,1343,1344,1345,1347,1348,1350],{},"面试总结：普通函数的 ",[28,1346,1285],{"code":1285}," 看调用方式，箭头函数的 ",[28,1349,1285],{"code":1285}," 看定义时外层作用域。",[15,1352,1354],{"id":1353},"_9-call-apply-bind","9. call \u002F apply \u002F bind",[20,1356,1357,47,1359,47,1361,1363,1364,1366],{},[28,1358,1305],{"code":1305},[28,1360,1308],{"code":1308},[28,1362,1311],{"code":1311}," 都是用来改变函数执行时的 ",[28,1365,1285],{"code":1285},"，区别在于参数形式和是否立即执行。",[82,1368,1371],{"className":1369,"code":1370,"language":777,"meta":88},[775],"function say(this: { name: string }, prefix: string) {\n  return `${prefix}${this.name}`;\n}\n\nsay.call({ name: \"Tom\" }, \"Hi \"); \u002F\u002F 立即执行，参数一个个传\nsay.apply({ name: \"Tom\" }, [\"Hi \"]); \u002F\u002F 立即执行，参数用数组传\n\nconst boundSay = say.bind({ name: \"Tom\" }, \"Hi \");\nboundSay(); \u002F\u002F bind 不立即执行，返回新函数\n",[28,1372,1370],{"__ignoreMap":88},[20,1374,1375,1376,1378,1379,1381,1382,1384],{},"简单记：",[28,1377,1305],{"code":1305}," 是参数列表，",[28,1380,1308],{"code":1308}," 是参数数组，",[28,1383,1311],{"code":1311}," 是先绑定、后执行。",[82,1386,1389],{"className":1387,"code":1388,"language":777,"meta":88},[775],"function myBind\u003CT extends (...args: any[]) => any>(fn: T, ctx: unknown) {\n  return (...args: Parameters\u003CT>): ReturnType\u003CT> => fn.apply(ctx, args);\n}\n",[28,1390,1388],{"__ignoreMap":88},[15,1392,1394],{"id":1393},"_10-new-做了什么","10. new 做了什么",[20,1396,1397,1399],{},[28,1398,1319],{"code":1319}," 的作用是根据构造函数创建实例对象。内部可以拆成四步：",[409,1401,1402,1405,1410,1416],{},[412,1403,1404],{},"创建一个新对象。",[412,1406,1407,1408,115],{},"把新对象的原型指向构造函数的 ",[28,1409,1275],{"code":1275},[412,1411,1412,1413,1415],{},"用新对象作为 ",[28,1414,1285],{"code":1285}," 执行构造函数。",[412,1417,1418],{},"如果构造函数返回对象，就返回这个对象；否则返回新创建的对象。",[82,1420,1423],{"className":1421,"code":1422,"language":777,"meta":88},[775],"function myNew\u003CT extends object>(Ctor: new (...args: any[]) => T, ...args: any[]) {\n  const obj = Object.create(Ctor.prototype);\n  const result = Ctor.apply(obj, args);\n  return result && typeof result === \"object\" ? result : obj;\n}\n",[28,1424,1422],{"__ignoreMap":88},[20,1426,1427,1428,1430,1431,1433],{},"面试时不用死背实现细节，重点说清：",[28,1429,1319],{"code":1319}," 会建立实例和原型之间的关系，并让构造函数里的 ",[28,1432,1285],{"code":1285}," 指向这个实例。",[15,1435,1437],{"id":1436},"_11-event-loop","11. Event Loop",[20,1439,1440],{},"Event Loop 是 JS 处理同步任务和异步回调的机制。因为 JS 主线程一次只能执行一段代码，所以异步任务完成后不能立刻插队执行，而是要排进任务队列，等主线程空了再执行。",[20,1442,1443],{},"浏览器里可以先按这个顺序记：",[409,1445,1446,1449,1452,1455],{},[412,1447,1448],{},"执行当前同步代码。",[412,1450,1451],{},"清空本轮产生的所有微任务。",[412,1453,1454],{},"浏览器根据情况进行渲染。",[412,1456,1457],{},"取出一个宏任务执行，进入下一轮循环。",[82,1459,1462],{"className":1460,"code":1461,"language":777,"meta":88},[775],"console.log(\"sync\");\nsetTimeout(() => console.log(\"macro\"), 0);\nPromise.resolve().then(() => console.log(\"micro\"));\nconsole.log(\"end\");\n\u002F\u002F sync -> end -> micro -> macro\n",[28,1463,1461],{"__ignoreMap":88},[20,1465,1466,1467,278,1470,1473,1474,1477,1478,1481,1482,1484],{},"上面代码里，同步代码先输出 ",[28,1468,1469],{"code":1469},"sync",[28,1471,1472],{"code":1472},"end","；",[28,1475,1476],{"code":1476},"Promise.then"," 是微任务，先于 ",[28,1479,1480],{"code":1480},"setTimeout"," 执行；",[28,1483,1480],{"code":1480}," 是宏任务，最后执行。",[15,1486,1488],{"id":1487},"_12-宏任务与微任务","12. 宏任务与微任务",[20,1490,1491,1492,115],{},"宏任务和微任务都是异步回调队列，但执行优先级不同：",[23,1493,1494],{},"微任务会在当前同步代码结束后立刻清空，宏任务一轮只取一个执行",[20,1496,1497,1498,47,1500,47,1503,1506,1507,47,1509,1512],{},"常见微任务有 ",[28,1499,1476],{"code":1476},[28,1501,1502],{"code":1502},"queueMicrotask",[28,1504,1505],{"code":1505},"MutationObserver","。常见宏任务有 ",[28,1508,1480],{"code":1480},[28,1510,1511],{"code":1511},"setInterval","、I\u002FO、UI 事件。",[82,1514,1517],{"className":1515,"code":1516,"language":777,"meta":88},[775],"setTimeout(() => console.log(\"timeout\"));\n\nPromise.resolve().then(() => {\n  console.log(\"promise 1\");\n  Promise.resolve().then(() => console.log(\"promise 2\"));\n});\n\nconsole.log(\"sync\");\n\u002F\u002F sync -> promise 1 -> promise 2 -> timeout\n",[28,1518,1516],{"__ignoreMap":88},[20,1520,1521],{},"注意：微任务优先级高，但不是越多越好。如果一直往微任务队列里塞任务，浏览器可能迟迟没有机会渲染页面。",[15,1523,1525],{"id":1524},"_13-promise-原理","13. Promise 原理",[20,1527,1528,1531,1532,1535,1536,1539,1540,1543],{},[23,1529,1530],{},"Promise"," 可以理解成一个异步结果的容器。它有三种状态：",[28,1533,1534],{"code":1534},"pending"," 表示进行中，",[28,1537,1538],{"code":1538},"fulfilled"," 表示成功，",[28,1541,1542],{"code":1542},"rejected"," 表示失败。",[20,1545,1546],{},"状态变化有两个特点：",[409,1548,1549,1560],{},[412,1550,1551,1552,1554,1555,1557,1558,115],{},"只能从 ",[28,1553,1534],{"code":1534}," 变成 ",[28,1556,1538],{"code":1538}," 或 ",[28,1559,1542],{"code":1542},[412,1561,1562],{},"一旦状态改变，就不能再改。",[20,1564,1565,1568,1569,1571,1572,115],{},[28,1566,1567],{"code":1567},"then"," 的关键是：它会返回一个新的 Promise，所以才能链式调用。上一个 ",[28,1570,1567],{"code":1567}," 回调的返回值，会传给下一个 ",[28,1573,1567],{"code":1567},[82,1575,1578],{"className":1576,"code":1577,"language":777,"meta":88},[775],"Promise.resolve(1)\n  .then((value) => value + 1)\n  .then((value) => console.log(value)); \u002F\u002F 2\n",[28,1579,1577],{"__ignoreMap":88},[20,1581,1582,1583,1585,1586,1588],{},"如果回调里返回的是普通值，下一个 ",[28,1584,1567],{"code":1567}," 会拿到这个值；如果返回的是 Promise，下一个 ",[28,1587,1567],{"code":1567}," 会等待这个 Promise 完成。",[20,1590,1591],{},"以下是 Promise 运作的核心原理：",[409,1593,1594],{},[412,1595,1596],{},"核心机制：三种状态 (Three States)",[20,1598,1599],{},"一个 Promise 实例在任何时候都只能处于以下三种状态之一：",[1601,1602,1603,1609,1619],"ul",{},[412,1604,1605,1608],{},[23,1606,1607],{},"Pending（等待中）","：初始状态，也就是事情还在进行中，既没成功也没失败。",[412,1610,1611,1614,1615,1618],{},[23,1612,1613],{},"Fulfilled（已成功）","：操作成功完成。此时会触发 ",[28,1616,1617],{"code":1617},"resolve"," 函数，并传递一个成功的值。",[412,1620,1621,1624,1625,1628],{},[23,1622,1623],{},"Rejected（已失败）","：操作失败。此时会触发 ",[28,1626,1627],{"code":1627},"reject"," 函数，并传递一个失败的原因（通常是 Error 对象）。",[20,1630,1631,1634,1635,1638,1639,1642,1643,115],{},[23,1632,1633],{},"核心铁律：状态不可逆"," Promise 的状态流转是单向且不可逆的。只能从 ",[28,1636,1637],{"code":1637},"Pending -> Fulfilled"," 或者从 ",[28,1640,1641],{"code":1641},"Pending -> Rejected","。一旦状态改变（也就是状态落锤定音，称为 settled），就",[23,1644,1645],{},"永远不会再变了",[409,1647,1649],{"start":1648},2,[412,1650,1651],{},"核心架构：发布-订阅模式",[20,1653,1654],{},"Promise 内部其实维护了两个队列（数组）：",[1601,1656,1657,1667],{},[412,1658,1659,1662,1663,1666],{},[23,1660,1661],{},"成功回调队列","：存放通过 ",[28,1664,1665],{"code":1665},".then()"," 注册的成功处理函数。",[412,1668,1669,1662,1672,1675,1676,1679],{},[23,1670,1671],{},"失败回调队列",[28,1673,1674],{"code":1674},".catch()","（或 ",[28,1677,1678],{"code":1678},".then"," 的第二个参数）注册的失败处理函数。",[20,1681,1682,1683,1686],{},"当你 ",[28,1684,1685],{"code":1685},"new Promise"," 并执行异步操作时：",[409,1688,1689,1701,1711],{},[412,1690,1691,1692,1557,1694,1696,1697,1700],{},"如果异步操作还在 Pending，你调用的 ",[28,1693,1665],{"code":1665},[28,1695,1674],{"code":1674}," 会把对应的回调函数",[23,1698,1699],{},"存入对应的队列中","（订阅）。",[412,1702,1703,1704,1557,1707,1710],{},"当异步操作完成，调用了 ",[28,1705,1706],{"code":1706},"resolve()",[28,1708,1709],{"code":1709},"reject()"," 时，Promise 内部会遍历对应的队列，将里面的回调函数依次拿出来执行（发布）。",[412,1712,1713],{},"执行时机：微任务 (Microtask)",[20,1715,1716,1717,278,1719,1722,1723,1726],{},"这是很多人容易忽略的一点。Promise 的回调函数（",[28,1718,1678],{"code":1678},[28,1720,1721],{"code":1721},".catch"," 里的代码）",[23,1724,1725],{},"永远是异步执行的","，即使 Promise 已经处于成功状态。 它们会被推入 JavaScript 事件循环（Event Loop）的微任务队列（Microtask Queue）中，在当前同步代码执行完毕后、下一个宏任务开始前立刻执行。",[20,1728,1729,1730,1732],{},"面试总结：Promise 解决的是异步结果的状态管理，",[28,1731,1567],{"code":1567}," 返回新 Promise 是链式调用的基础。",[15,1734,1736],{"id":1735},"_14-async-await","14. async \u002F await",[20,1738,1739,1742],{},[28,1740,1741],{"code":1741},"async\u002Fawait"," 是 Promise 的语法糖，目的是把异步流程写得更像同步代码。",[20,1744,1745,1747,1748,1751],{},[28,1746,104],{"code":104}," 函数一定会返回 Promise；",[28,1749,1750],{"code":1750},"await"," 会暂停当前 async 函数后面的代码，等待右侧 Promise 完成后再继续执行。注意它只是暂停当前 async 函数，不会阻塞整个 JS 线程。",[82,1753,1756],{"className":1754,"code":1755,"language":777,"meta":88},[775],"const load = async () => {\n  try {\n    const [user, orders] = await Promise.all([fetchUser(), fetchOrders()]);\n    return { user, orders };\n  } catch (error) {\n    console.error(error);\n  }\n};\n",[28,1757,1755],{"__ignoreMap":88},[20,1759,1760,1762],{},[28,1761,1750],{"code":1750}," 后面的继续执行逻辑会进入微任务队列，所以它和 Promise 的执行顺序题经常一起考。",[82,1764,1767],{"className":1765,"code":1766,"language":777,"meta":88},[775],"async function run() {\n  console.log(\"a\");\n  await Promise.resolve();\n  console.log(\"b\");\n}\n\nrun();\nconsole.log(\"c\");\n\u002F\u002F a -> c -> b\n",[28,1768,1766],{"__ignoreMap":88},[20,1770,1771,1772,1774],{},"面试总结：",[28,1773,1741],{"code":1741}," 本质还是 Promise，只是让异步代码更容易读。",[15,1776,1778],{"id":1777},"_15-深拷贝","15. 深拷贝",[20,1780,1781,1784,1785,1788,1789,1792],{},[23,1782,1783],{},"浅拷贝","只复制第一层，嵌套对象仍然共享引用；",[23,1786,1787],{},"深拷贝","会递归复制嵌套结构。面试手写时至少要处理对象、数组和循环引用，实际项目中可以优先考虑 ",[28,1790,1791],{"code":1791},"structuredClone"," 或成熟工具库。",[82,1794,1797],{"className":1795,"code":1796,"language":777,"meta":88},[775],"const deepClone = \u003CT extends object>(source: T, cache = new WeakMap()): T => {\n  if (cache.has(source)) return cache.get(source);\n  const target: any = Array.isArray(source) ? [] : {};\n  cache.set(source, target);\n\n  Reflect.ownKeys(source).forEach((key) => {\n    const value = (source as any)[key];\n    target[key] = value && typeof value === \"object\" ? deepClone(value, cache) : value;\n  });\n\n  return target;\n};\n",[28,1798,1796],{"__ignoreMap":88},[15,1800,1802],{"id":1801},"_16-防抖与节流","16. 防抖与节流",[20,1804,1805,1808,1809,1812],{},[23,1806,1807],{},"防抖","是在连续触发停止一段时间后再执行，适合搜索输入、窗口 resize；",[23,1810,1811],{},"节流","是在固定时间内最多执行一次，适合滚动、拖拽、按钮连点限制。两者都是为了降低高频事件带来的性能压力。",[82,1814,1817],{"className":1815,"code":1816,"language":777,"meta":88},[775],"const debounce = \u003CT extends (...args: any[]) => void>(fn: T, delay: number) => {\n  let timer: ReturnType\u003Ctypeof setTimeout>;\n  return (...args: Parameters\u003CT>) => {\n    clearTimeout(timer);\n    timer = setTimeout(() => fn(...args), delay);\n  };\n};\nconst throttle = \u003CT extends (...args: any[]) => void>(fn: T, delay: number) => {\n  let last = 0;\n  return (...args: Parameters\u003CT>) => {\n    const now = Date.now();\n    if (now - last >= delay) {\n      last = now;\n      fn(...args);\n    }\n  };\n};\n",[28,1818,1816],{"__ignoreMap":88},[15,1820,1822],{"id":1821},"_17-模块化","17. 模块化",[20,1824,1825,1828,1829,1832],{},[23,1826,1827],{},"CommonJS"," 是运行时加载，主要用于 Node；",[23,1830,1831],{},"ESM"," 是静态模块系统，支持编译期分析和 Tree Shaking。现代前端工程通常优先使用 ESM，库包会同时产出 ESM 和 CJS 来兼容不同环境。",[15,1834,1836],{"id":1835},"_18-垃圾回收","18. 垃圾回收",[20,1838,1839,1840,1843],{},"JS 垃圾回收主要基于",[23,1841,1842],{},"可达性分析","，从根对象出发无法访问到的对象会被回收。常见内存泄漏包括未清理事件监听、定时器、闭包持有大对象、全局缓存无限增长、DOM 被移除但仍被 JS 引用。",[10,1845,1847],{"id":1846},"typescript","TypeScript",[15,1849,1851],{"id":1850},"_1-typescript-的价值","1. TypeScript 的价值",[20,1853,1854,1856],{},[23,1855,1847],{}," 可以理解成给 JavaScript 加了一层类型检查。它不会改变 JS 的运行时逻辑，最终还是会编译成 JS，但它能在代码运行前发现很多类型错误。",[20,1858,1859],{},"它的价值主要有三点：第一，用类型把业务数据结构表达清楚；第二，重构时更安全，改字段名或函数参数时 IDE 能帮你发现影响范围；第三，团队协作时，别人看类型就能知道这个函数需要什么、返回什么。",[20,1861,1862],{},"面试总结：TS 不是为了让代码变复杂，而是用类型提前暴露错误、描述约束、提升维护性。",[15,1864,1866],{"id":1865},"_2-interface-与-type","2. interface 与 type",[20,1868,1869,278,1872,1875],{},[28,1870,1871],{"code":1871},"interface",[28,1873,1874],{"code":1874},"type"," 都能描述类型，很多场景可以互换，但侧重点不一样。",[20,1877,1878,1880,1881,1884],{},[28,1879,1871],{"code":1871}," 更适合描述对象结构，比如用户、订单、组件 props，也适合给 class 做 ",[28,1882,1883],{"code":1883},"implements","。它还能声明合并，也就是同名 interface 会自动合在一起。",[20,1886,1887,1889],{},[28,1888,1874],{"code":1874}," 更灵活，适合联合类型、交叉类型、条件类型、工具类型这类类型运算。",[82,1891,1894],{"className":1892,"code":1893,"language":777,"meta":88},[775],"interface User {\n  id: number;\n  name: string;\n}\n\ntype Status = \"loading\" | \"success\" | \"error\";\ntype UserWithRole = User & { role: string };\n",[28,1895,1893],{"__ignoreMap":88},[20,1897,1898,1899,1901,1902,1904],{},"面试总结：对象模型优先用 ",[28,1900,1871],{"code":1871},"，复杂类型组合优先用 ",[28,1903,1874],{"code":1874},"，项目里保持团队风格统一更重要。",[15,1906,1908],{"id":1907},"_3-any-unknown-never","3. any \u002F unknown \u002F never",[20,1910,1911,47,1914,47,1917,1920],{},[28,1912,1913],{"code":1913},"any",[28,1915,1916],{"code":1916},"unknown",[28,1918,1919],{"code":1919},"never"," 可以按安全程度理解。",[20,1922,1923,1925,1926,1928,1929,1931],{},[28,1924,1913],{"code":1913}," 表示“随便是什么类型”，用了它以后 TS 基本不再检查，容易把错误传染到后续代码里。",[28,1927,1916],{"code":1916}," 也表示未知类型，但使用前必须先判断类型，所以更安全。",[28,1930,1919],{"code":1919}," 表示永远不会出现的值，常用于抛错函数、死循环函数、联合类型穷尽检查。",[82,1933,1936],{"className":1934,"code":1935,"language":777,"meta":88},[775],"function handle(value: unknown) {\n  if (typeof value === \"string\") {\n    return value.toUpperCase();\n  }\n}\n",[28,1937,1935],{"__ignoreMap":88},[20,1939,1940,1941,1943,1944,1473,1946,1948],{},"面试总结：外部输入优先用 ",[28,1942,1916],{"code":1916},"，不要上来就用 ",[28,1945,1913],{"code":1913},[28,1947,1919],{"code":1919}," 常用于保证分支已经处理完整。",[15,1950,1952],{"id":1951},"_4-泛型","4. 泛型",[20,1954,1955,1958],{},[23,1956,1957],{},"泛型","就是“类型参数”。普通函数的参数传的是值，泛型传的是类型。它解决的问题是：代码可以复用，但类型不能丢。",[82,1960,1963],{"className":1961,"code":1962,"language":777,"meta":88},[775],"const pick = \u003CT extends object, K extends keyof T>(obj: T, key: K): T[K] => obj[key];\n",[28,1964,1962],{"__ignoreMap":88},[20,1966,1094,1967,1970,1971,1974,1975,1978],{},[28,1968,1969],{"code":1969},"T"," 表示对象类型，",[28,1972,1973],{"code":1973},"K extends keyof T"," 表示 key 必须是这个对象真实存在的属性。返回值 ",[28,1976,1977],{"code":1977},"T[K]"," 表示返回对应属性的类型。",[82,1980,1983],{"className":1981,"code":1982,"language":777,"meta":88},[775],"const user = { id: 1, name: \"Tom\" };\nconst name = pick(user, \"name\"); \u002F\u002F string\n",[28,1984,1982],{"__ignoreMap":88},[20,1986,1987],{},"面试总结：泛型让函数、组件、工具类型在复用时仍然保留输入和输出之间的类型关系。",[15,1989,1991],{"id":1990},"_5-联合类型与交叉类型","5. 联合类型与交叉类型",[20,1993,1994,1997,1998,1473,2001,2004,2005,115],{},[23,1995,1996],{},"联合类型","表示“可能是其中一种”，用 ",[28,1999,2000],{"code":2000},"|",[23,2002,2003],{},"交叉类型","表示“同时拥有这些能力”，用 ",[28,2006,2007],{"code":2007},"&",[20,2009,2010],{},"联合类型常用来描述状态，因为一个状态在同一时间只能属于一种情况。",[82,2012,2015],{"className":2013,"code":2014,"language":777,"meta":88},[775],"type State =\n  | { type: \"loading\" }\n  | { type: \"success\"; data: string }\n  | { type: \"error\"; message: string };\n",[28,2016,2014],{"__ignoreMap":88},[20,2018,2019],{},"交叉类型常用来组合多个对象能力。",[82,2021,2024],{"className":2022,"code":2023,"language":777,"meta":88},[775],"type User = { id: number; name: string };\ntype Permission = { role: string };\ntype Admin = User & Permission;\n",[28,2025,2023],{"__ignoreMap":88},[20,2027,1771,2028,2030,2031,2033],{},[28,2029,2000],{"code":2000}," 是“或”，适合多种可能；",[28,2032,2007],{"code":2007}," 是“且”，适合能力叠加。",[15,2035,2037],{"id":2036},"_6-类型收窄","6. 类型收窄",[20,2039,2040,2043],{},[23,2041,2042],{},"类型收窄","就是把一个宽泛类型缩小成更具体的类型。因为 TS 只有确认类型后，才允许你安全访问对应属性或方法。",[20,2045,2046,2047,47,2049,47,2052,2054],{},"常见收窄方式有 ",[28,2048,792],{"code":792},[28,2050,2051],{"code":2051},"in",[28,2053,795],{"code":795},"、字面量判断。",[82,2056,2059],{"className":2057,"code":2058,"language":777,"meta":88},[775],"function print(value: string | number) {\n  if (typeof value === \"string\") {\n    return value.toUpperCase();\n  }\n\n  return value.toFixed(2);\n}\n",[28,2060,2058],{"__ignoreMap":88},[20,2062,2063],{},"面试总结：类型收窄是 TS 根据判断条件理解代码分支，从而给出更精确类型。",[15,2065,2067],{"id":2066},"_7-工具类型","7. 工具类型",[20,2069,2070,47,2073,47,2076,47,2079,47,2082,47,2085,2088,2089,47,2092,728,2095,115],{},[28,2071,2072],{"code":2072},"Partial",[28,2074,2075],{"code":2075},"Required",[28,2077,2078],{"code":2078},"Readonly",[28,2080,2081],{"code":2081},"Pick",[28,2083,2084],{"code":2084},"Omit",[28,2086,2087],{"code":2087},"Record"," 是最常见的工具类型。它们的底层主要依赖",[23,2090,2091],{},"映射类型",[23,2093,2094],{},"索引访问类型",[23,2096,2097],{},"条件类型",[82,2099,2102],{"className":2100,"code":2101,"language":777,"meta":88},[775],"type MyPick\u003CT, K extends keyof T> = {\n  [P in K]: T[P];\n};\n\ntype MyReadonly\u003CT> = {\n  readonly [K in keyof T]: T[K];\n};\n",[28,2103,2101],{"__ignoreMap":88},[15,2105,2107],{"id":2106},"_8-infer","8. infer",[20,2109,2110,2113],{},[28,2111,2112],{"code":2112},"infer"," 用在条件类型中，用来临时推断某个位置上的类型。常见场景是提取函数返回值、Promise 内部值、数组元素类型等。",[82,2115,2118],{"className":2116,"code":2117,"language":777,"meta":88},[775],"type MyReturnType\u003CT> = T extends (...args: any[]) => infer R ? R : never;\ntype AwaitedValue\u003CT> = T extends Promise\u003Cinfer R> ? R : T;\n",[28,2119,2117],{"__ignoreMap":88},[10,2121,2122],{"id":2122},"浏览器原理",[15,2124,2126],{"id":2125},"_1-从输入-url-到页面展示","1. 从输入 URL 到页面展示",[20,2128,2129,2130,728,2133,115],{},"从输入 URL 到页面展示，可以拆成两个大阶段：",[23,2131,2132],{},"网络请求阶段",[23,2134,2135],{},"浏览器渲染阶段",[20,2137,2138],{},"网络阶段大致是：先查缓存；缓存没有命中就做 DNS 解析，把域名变成 IP；然后建立 TCP 连接；如果是 HTTPS，还要进行 TLS 握手；接着发送 HTTP 请求，服务端返回 HTML。",[20,2140,2141],{},"渲染阶段大致是：浏览器解析 HTML 生成 DOM，解析 CSS 生成 CSSOM，两者合成渲染树；然后计算每个节点的位置和大小，也就是布局；再绘制颜色、文字、图片、阴影；最后把不同图层合成到屏幕上。",[20,2143,2144],{},"面试总结：这道题不要一口气背流水账，可以按“缓存、DNS、连接、请求响应、解析、布局、绘制、合成”这条线讲，再结合性能优化补充关键 CSS、JS 阻塞和首屏资源。",[15,2146,2148],{"id":2147},"_2-浏览器渲染流程","2. 浏览器渲染流程",[20,2150,2151,2152,115],{},"浏览器渲染流程可以记成：",[23,2153,2154],{},"DOM、CSSOM、渲染树、布局、绘制、合成",[20,2156,2157,2158,2161],{},"DOM 描述页面结构，CSSOM 描述样式规则。渲染树只包含需要显示的节点，比如 ",[28,2159,2160],{"code":2160},"display: none"," 的元素不会进入渲染树。布局阶段计算元素的位置和大小，绘制阶段把文字、颜色、边框、阴影画出来，合成阶段把不同图层合成最终画面。",[20,2163,2164],{},"CSS 会阻塞渲染，因为浏览器必须知道样式才能正确绘制页面。JS 可能阻塞 HTML 解析，因为 JS 可能会修改 DOM 或读取样式。面试总结：首屏优化很多时候就是减少关键 CSS\u002FJS 对这条渲染链路的阻塞。",[15,2166,2168],{"id":2167},"_3-dom-事件流","3. DOM 事件流",[20,2170,2171],{},"DOM 事件流描述的是一次事件从哪里开始、经过哪里、最后到哪里。它分为三个阶段：捕获阶段、目标阶段、冒泡阶段。",[20,2173,2174,2175,47,2178,2181],{},"捕获阶段是事件从 ",[28,2176,2177],{"code":2177},"window",[28,2179,2180],{"code":2180},"document"," 一层层往目标元素走；目标阶段是到达真正触发事件的元素；冒泡阶段是事件再从目标元素一层层往外冒。",[20,2183,2184,2187,2188,2191],{},[23,2185,2186],{},"事件委托","就是利用冒泡，把事件监听放在父元素上，通过 ",[28,2189,2190],{"code":2190},"event.target"," 判断真正点击的是哪个子元素。它适合列表、表格、动态新增节点，因为不需要给每个子元素单独绑定事件。",[82,2193,2196],{"className":2194,"code":2195,"language":777,"meta":88},[775],"list.addEventListener(\"click\", (event) => {\n  const target = event.target as HTMLElement;\n  const item = target.closest(\"[data-id]\");\n  if (item) console.log(item.getAttribute(\"data-id\"));\n});\n",[28,2197,2195],{"__ignoreMap":88},[20,2199,2200],{},"面试总结：事件流讲传播顺序，事件委托讲如何利用冒泡减少事件绑定。",[15,2202,2204],{"id":2203},"_4-浏览器本地存储","4. 浏览器本地存储",[20,2206,2207,2210,2211,2214,2215,2218,2219,2222],{},[28,2208,2209],{"code":2209},"Cookie"," 容量小，会随请求自动携带，适合服务端会话；",[28,2212,2213],{"code":2213},"localStorage"," 持久保存，适合非敏感配置；",[28,2216,2217],{"code":2217},"sessionStorage"," 页面会话结束后清除；",[28,2220,2221],{"code":2221},"IndexedDB"," 适合大量结构化数据。敏感 Token 不建议直接放 localStorage，因为 XSS 后容易被读取。",[15,2224,2226],{"id":2225},"_5-web-worker","5. Web Worker",[20,2228,2229,2232,2233,2236],{},[23,2230,2231],{},"Web Worker"," 可以把耗时计算放到独立线程中执行，避免阻塞主线程渲染。它不能直接操作 DOM，只能通过 ",[28,2234,2235],{"code":2235},"postMessage"," 和主线程通信，适合大数据计算、文件解析、图片处理等场景。",[10,2238,2240],{"id":2239},"vue","Vue",[15,2242,2244],{"id":2243},"_1-vue2-与-vue3-区别","1. Vue2 与 Vue3 区别",[20,2246,2247,2248,2251,2252,2255],{},"Vue2 响应式基于 ",[28,2249,2250],{"code":2250},"Object.defineProperty","，对新增属性和数组部分操作需要额外处理；Vue3 基于 ",[28,2253,2254],{"code":2254},"Proxy","，拦截能力更完整。Vue3 还引入了组合式 API，更适合复杂逻辑复用和 TypeScript 类型推导。",[15,2257,2259],{"id":2258},"_2-vue-响应式原理","2. Vue 响应式原理",[20,2261,2262,2263,2266,2267,2270],{},"Vue 响应式的核心是：读取数据时进行",[23,2264,2265],{},"依赖收集","，修改数据时进行",[23,2268,2269],{},"派发更新","。简单理解就是 effect 执行时记录“谁用到了这个数据”，数据变化时再通知这些 effect 重新执行。",[20,2272,2273,2274,2277,2278,2280,2281,2283],{},"可以用一个例子理解：模板里用了 ",[28,2275,2276],{"code":2276},"user.name","，渲染函数执行时会读取 ",[28,2279,2276],{"code":2276},"，Vue 就记录“这个渲染函数依赖了 name”。之后 ",[28,2282,2276],{"code":2276}," 被修改，Vue 就能找到对应渲染函数并重新更新页面。",[20,2285,2286,2287,2289,2290,2292],{},"Vue2 和 Vue3 的区别主要在拦截方式：Vue2 用 ",[28,2288,2250],{"code":2250}," 拦截对象已有属性的读取和修改；Vue3 用 ",[28,2291,2254],{"code":2254}," 代理整个对象，能拦截能力更完整，比如新增属性、删除属性、数组操作等。",[82,2294,2297],{"className":2295,"code":2296,"language":777,"meta":88},[775],"type Effect = () => void;\nconst bucket = new WeakMap\u003Cobject, Map\u003CPropertyKey, Set\u003CEffect>>>();\nlet activeEffect: Effect | null = null;\n\nconst effect = (fn: Effect) => {\n  activeEffect = fn;\n  fn();\n  activeEffect = null;\n};\n\nconst reactive = \u003CT extends object>(target: T): T =>\n  new Proxy(target, {\n    get(obj, key, receiver) {\n      if (activeEffect) {\n        let depsMap = bucket.get(obj);\n        if (!depsMap) bucket.set(obj, (depsMap = new Map()));\n        let deps = depsMap.get(key);\n        if (!deps) depsMap.set(key, (deps = new Set()));\n        deps.add(activeEffect);\n      }\n      return Reflect.get(obj, key, receiver);\n    },\n    set(obj, key, value, receiver) {\n      const ok = Reflect.set(obj, key, value, receiver);\n      bucket.get(obj)?.get(key)?.forEach((fn) => fn());\n      return ok;\n    },\n  });\n",[28,2298,2296],{"__ignoreMap":88},[15,2300,2302],{"id":2301},"_3-ref-与-reactive","3. ref 与 reactive",[20,2304,2305,278,2308,2311],{},[28,2306,2307],{"code":2307},"ref",[28,2309,2310],{"code":2310},"reactive"," 都能创建响应式数据，区别主要在使用对象和访问方式。",[20,2313,2314,2316,2317,2320,2321,2323,2324,115],{},[28,2315,2307],{"code":2307}," 适合基本类型，也适合需要整体替换的值；在 JS 里要通过 ",[28,2318,2319],{"code":2319},".value"," 访问，在模板中会自动解包。",[28,2322,2310],{"code":2310}," 适合对象或数组，访问属性时不需要 ",[28,2325,2319],{"code":2319},[82,2327,2330],{"className":2328,"code":2329,"language":777,"meta":88},[775],"const count = ref(0);\ncount.value++;\n\nconst user = reactive({ name: \"Tom\" });\nuser.name = \"Jerry\";\n",[28,2331,2329],{"__ignoreMap":88},[20,2333,2334,2335,2337,2338,115],{},"注意：",[28,2336,2310],{"code":2310}," 直接解构可能丢失响应式，因为解构出来的是普通值。需要保留响应式时，可以用 ",[28,2339,2340],{"code":2340},"toRefs",[20,2342,2343,2344,2346,2347,2349],{},"面试总结：基本类型和整体替换用 ",[28,2345,2307],{"code":2307},"，复杂对象用 ",[28,2348,2310],{"code":2310},"，解构 reactive 要小心。",[15,2351,2353],{"id":2352},"_4-computed-与-watch","4. computed 与 watch",[20,2355,2356,278,2359,2362,2363,2365,2366,522,2369,2365,2371,115],{},[28,2357,2358],{"code":2358},"computed",[28,2360,2361],{"code":2361},"watch"," 的区别可以记成：",[28,2364,2358],{"code":2358}," 负责",[23,2367,2368],{},"算出一个值",[28,2370,2361],{"code":2361},[23,2372,2373],{},"变化后做一件事",[20,2375,2376,2378],{},[28,2377,2358],{"code":2358}," 是带缓存的派生状态，依赖不变就不会重新计算，适合从已有状态计算新状态，比如总价、过滤列表、格式化展示。",[20,2380,2381,2383],{},[28,2382,2361],{"code":2361}," 是监听数据变化后执行副作用，比如请求接口、写日志、操作本地缓存、调用第三方 API。",[20,2385,2386,2387,2389,2390,115],{},"面试总结：能用计算属性表达的数据，就优先用 ",[28,2388,2358],{"code":2358},"；需要在变化后执行动作，才用 ",[28,2391,2361],{"code":2361},[15,2393,2395],{"id":2394},"_5-nexttick","5. nextTick",[20,2397,2398,2399,2402],{},"Vue 更新 DOM 是异步批量执行的，修改响应式数据后，DOM 不会立刻同步更新。",[28,2400,2401],{"code":2401},"nextTick"," 的作用是等本轮数据更新对应的 DOM patch 完成后再执行回调，常用于获取最新 DOM 尺寸或滚动位置。",[82,2404,2407],{"className":2405,"code":2406,"language":777,"meta":88},[775],"const callbacks: Array\u003C() => void> = [];\nlet pending = false;\n\nconst nextTick = (cb: () => void) => {\n  callbacks.push(cb);\n  if (!pending) {\n    pending = true;\n    Promise.resolve().then(() => {\n      pending = false;\n      callbacks.splice(0).forEach((fn) => fn());\n    });\n  }\n};\n",[28,2408,2406],{"__ignoreMap":88},[15,2410,2412],{"id":2411},"_6-vue-diff-与-key","6. Vue Diff 与 key",[20,2414,2415,2416,2419],{},"Vue Diff 会尽量复用同层级节点，减少真实 DOM 操作。",[28,2417,2418],{"code":2418},"key"," 的作用是标识节点身份，尤其在列表插入、删除、排序时，如果 key 不稳定，可能导致节点错误复用和组件状态错乱。",[15,2421,2423],{"id":2422},"_7-组件通信","7. 组件通信",[20,2425,2426,2427,278,2430,2433,2434,2437],{},"父子组件通信用 ",[28,2428,2429],{"code":2429},"props",[28,2431,2432],{"code":2432},"emit","，跨层级通信用 ",[28,2435,2436],{"code":2436},"provide\u002Finject","，复杂全局状态用 Pinia 或 Vuex。不要把所有通信都放到事件总线里，否则数据流会变得难追踪。",[15,2439,2441],{"id":2440},"_8-v-if-与-v-show","8. v-if 与 v-show",[20,2443,2444,2447,2448,2451,2452,2455],{},[28,2445,2446],{"code":2446},"v-if"," 是真正创建或销毁 DOM，适合低频切换；",[28,2449,2450],{"code":2450},"v-show"," 是通过 ",[28,2453,2454],{"code":2454},"display"," 控制显示隐藏，适合高频切换。判断标准就是切换频率和初始渲染成本。",[15,2457,2459],{"id":2458},"_9-keep-alive","9. keep-alive",[20,2461,2462,2465,2466,278,2469,2472],{},[28,2463,2464],{"code":2464},"keep-alive"," 用来缓存组件实例，避免组件来回切换时重复创建和销毁。常见场景是 tab 页、列表页返回详情页后保留滚动位置，它会触发 ",[28,2467,2468],{"code":2468},"activated",[28,2470,2471],{"code":2471},"deactivated"," 生命周期。",[15,2474,2476],{"id":2475},"_10-pinia","10. Pinia",[20,2478,2479],{},"Pinia 是 Vue3 推荐状态库，API 更简洁，TypeScript 推导也更好。相比 Vuex，它弱化了 mutation 概念，可以直接通过 action 或 store 实例修改状态，使用体验更接近组合式 API。",[10,2481,2483],{"id":2482},"react","React",[15,2485,2487],{"id":2486},"_1-react-核心思想","1. React 核心思想",[20,2489,2490,2491,2494],{},"React 的核心思想是：",[23,2492,2493],{},"UI 是状态的结果","。你不用手动告诉浏览器“把这个 DOM 改成什么”，而是描述在某个 state 下页面应该长什么样，状态变化后 React 重新计算 UI 并更新 DOM。",[20,2496,2497],{},"这就是声明式 UI。命令式写法关注“怎么一步步改 DOM”，声明式写法关注“当前状态应该显示什么”。",[20,2499,2500],{},"面试总结：React 用组件拆分 UI，用 state 驱动视图，用声明式方式降低复杂 UI 的维护成本。",[15,2502,2504],{"id":2503},"_2-虚拟-dom","2. 虚拟 DOM",[20,2506,2507,2510],{},[23,2508,2509],{},"虚拟 DOM","本质是用 JS 对象描述真实 DOM。状态变化后，React 会生成新的虚拟 DOM，再和旧的虚拟 DOM 做 diff，找出需要更新的部分，最后再更新真实 DOM。",[20,2512,2513],{},"它的价值不是“永远比手写 DOM 快”，而是让复杂 UI 的更新过程更可预测，并且同一套描述可以适配不同平台，比如浏览器 DOM、React Native。",[20,2515,2516],{},"面试总结：虚拟 DOM 是 UI 的中间表示，核心价值是声明式更新、diff 优化和跨平台能力。",[15,2518,2520],{"id":2519},"_3-fiber","3. Fiber",[20,2522,2523,2526],{},[23,2524,2525],{},"Fiber"," 可以理解成 React 为了“可中断渲染”设计的一种任务单元和数据结构。以前一次更新如果组件树很大，React 可能长时间占用主线程，导致页面卡顿。Fiber 把大任务拆成很多小任务，让 React 有机会暂停、恢复、丢弃低优先级任务。",[20,2528,2529,2530,2533,2534,2537],{},"React 更新大致分为两个阶段：",[28,2531,2532],{"code":2532},"render"," 阶段负责计算变化，可以被中断；",[28,2535,2536],{"code":2536},"commit"," 阶段负责把变化真正提交到 DOM，必须同步完成，不能中断。",[20,2539,2540],{},"面试总结：Fiber 解决的是大组件树更新时主线程被长时间占用的问题，让 React 支持优先级调度和更流畅的交互。",[15,2542,2544],{"id":2543},"_4-react-diff","4. React Diff",[20,2546,2547,2548,2550],{},"React Diff 基于两个假设：不同类型节点直接替换，同层列表通过 ",[28,2549,2418],{"code":2418}," 判断能否复用。稳定 key 很重要，如果用数组 index 当 key，在插入、删除、排序时可能导致组件状态错位。",[15,2552,2554],{"id":2553},"_5-hooks-原理","5. Hooks 原理",[20,2556,2557],{},"Hooks 的状态和副作用是按调用顺序保存的，所以 Hook 不能写在条件、循环、嵌套函数里。只要每次渲染 Hook 调用顺序一致，React 就能正确找到每个 Hook 对应的状态。",[20,2559,2560,2561,2564,2565,2568],{},"可以把 Hooks 想成一个按顺序排列的列表：第一次 ",[28,2562,2563],{"code":2563},"useState"," 对应第一个状态，第二次 ",[28,2566,2567],{"code":2567},"useEffect"," 对应第二个副作用。如果某次渲染因为条件判断少调用了一个 Hook，后面的顺序就全乱了。",[82,2570,2573],{"className":2571,"code":2572,"language":777,"meta":88},[775],"let hooks: unknown[] = [];\nlet index = 0;\n\nfunction useState\u003CT>(initial: T) {\n  const current = index;\n  hooks[current] ??= initial;\n  const setState = (value: T) => {\n    hooks[current] = value;\n    index = 0;\n    render();\n  };\n  return [hooks[index++] as T, setState] as const;\n}\n\nfunction render() {\n  \u002F\u002F rerender component\n}\n",[28,2574,2572],{"__ignoreMap":88},[15,2576,2578],{"id":2577},"_6-useeffect","6. useEffect",[20,2580,2581,2583],{},[28,2582,2567],{"code":2567}," 用来处理副作用。副作用就是那些不只是计算 UI 的事情，比如请求数据、订阅事件、设置定时器、操作浏览器 API。",[20,2585,2586],{},"它会在浏览器绘制后异步执行，所以不会阻塞页面绘制。返回的函数用于清理副作用，避免组件卸载后仍然保留监听器、定时器或旧请求。",[82,2588,2591],{"className":2589,"code":2590,"language":777,"meta":88},[775],"useEffect(() => {\n  const timer = setInterval(() => setCount((n) => n + 1), 1000);\n  return () => clearInterval(timer);\n}, []);\n",[28,2592,2590],{"__ignoreMap":88},[20,2594,2595],{},"依赖数组决定 effect 什么时候重新执行：不传依赖数组，每次渲染后都执行；传空数组，只在挂载后执行一次；传具体依赖，依赖变化后执行。",[15,2597,2599],{"id":2598},"_7-uselayouteffect","7. useLayoutEffect",[20,2601,2602,2605],{},[28,2603,2604],{"code":2604},"useLayoutEffect"," 在 DOM 更新后、浏览器绘制前同步执行，适合读取布局并立刻修正页面，比如测量元素尺寸后调整位置。它会阻塞浏览器绘制，所以普通请求、订阅、日志不要用它。",[15,2607,2609],{"id":2608},"_8-usememo-usecallback-reactmemo","8. useMemo \u002F useCallback \u002F React.memo",[20,2611,2612,2615,2616,2619,2620,2623],{},[28,2613,2614],{"code":2614},"useMemo"," 缓存计算结果，",[28,2617,2618],{"code":2618},"useCallback"," 缓存函数引用，",[28,2621,2622],{"code":2622},"React.memo"," 缓存组件渲染结果。它们不是越多越好，因为缓存和比较也有成本，适合用于昂贵计算或减少子组件不必要重渲染。",[15,2625,2627],{"id":2626},"_9-setstate","9. setState",[20,2629,2630],{},"React 状态更新可能被批处理，所以依赖旧状态时应使用函数式更新。直接修改对象或数组不会产生新引用，可能导致 React 无法判断变化，应该返回新的对象或数组。",[82,2632,2635],{"className":2633,"code":2634,"language":777,"meta":88},[775],"setCount((count) => count + 1);\nsetList((list) => [...list, item]);\n",[28,2636,2634],{"__ignoreMap":88},[15,2638,2640],{"id":2639},"_10-受控与非受控组件","10. 受控与非受控组件",[20,2642,2643],{},"受控组件的表单值由 React state 控制，适合校验、联动和统一管理；非受控组件由 DOM 自己保存状态，适合文件上传或简单表单。复杂表单通常以受控为主，但也要注意性能和输入延迟。",[15,2645,2647],{"id":2646},"_11-context","11. Context",[20,2649,2650],{},"Context 适合跨层级传递低频变化的数据，比如主题、语言、登录用户信息。高频变化的大状态不适合全部塞进 Context，否则会导致大范围重渲染，通常需要拆分 Context 或引入状态库。",[15,2652,2654],{"id":2653},"_12-react-合成事件","12. React 合成事件",[20,2656,2657,2658,2660],{},"React 的合成事件封装了浏览器原生事件，统一了不同浏览器的差异，并通过事件委托减少事件绑定数量。React 17 之后事件委托从 ",[28,2659,2180],{"code":2180}," 调整到根容器，方便多个 React 版本共存。",[10,2662,2663],{"id":2663},"网络与安全",[15,2665,2667],{"id":2666},"_1-http-状态码","1. HTTP 状态码",[20,2669,2670,2671,2674,2675,2678,2679,2682,2683,2686,2687,47,2690,47,2693,47,2696,47,2699,47,2702,47,2705,47,2708,47,2711,47,2714,47,2717,115],{},"HTTP 状态码用于表示请求结果，",[28,2672,2673],{"code":2673},"2xx"," 成功，",[28,2676,2677],{"code":2677},"3xx"," 重定向，",[28,2680,2681],{"code":2681},"4xx"," 客户端错误，",[28,2684,2685],{"code":2685},"5xx"," 服务端错误。高频状态码包括 ",[28,2688,2689],{"code":2689},"200",[28,2691,2692],{"code":2692},"204",[28,2694,2695],{"code":2695},"301",[28,2697,2698],{"code":2698},"302",[28,2700,2701],{"code":2701},"304",[28,2703,2704],{"code":2704},"400",[28,2706,2707],{"code":2707},"401",[28,2709,2710],{"code":2710},"403",[28,2712,2713],{"code":2713},"404",[28,2715,2716],{"code":2716},"500",[28,2718,2719],{"code":2719},"502",[15,2721,2723],{"id":2722},"_2-http-缓存","2. HTTP 缓存",[20,2725,2726,2727,728,2730,115],{},"HTTP 缓存可以分为",[23,2728,2729],{},"强缓存",[23,2731,2732],{},"协商缓存",[20,2734,2735,2736,278,2739,2742,2743,115],{},"强缓存的意思是：浏览器先看本地缓存是否还在有效期内，如果没过期，就直接用本地资源，不发请求。常见响应头是 ",[28,2737,2738],{"code":2738},"Cache-Control",[28,2740,2741],{"code":2741},"Expires","，现代项目更常用 ",[28,2744,2738],{"code":2738},[20,2746,2747,2748,2750,2751,2754,2755,278,2758,2754,2761,115],{},"协商缓存的意思是：浏览器会带着缓存标识去问服务器“我这个资源还能不能用”。如果资源没变，服务器返回 ",[28,2749,2701],{"code":2701},"，浏览器继续用本地缓存；如果变了，就返回新资源。常见响应头是 ",[28,2752,2753],{"code":2753},"ETag"," \u002F ",[28,2756,2757],{"code":2757},"If-None-Match",[28,2759,2760],{"code":2760},"Last-Modified",[28,2762,2763],{"code":2763},"If-Modified-Since",[20,2765,2766],{},"面试总结：强缓存是不发请求直接用，协商缓存是发请求确认后再决定用不用。",[15,2768,2770],{"id":2769},"_3-http11-http2-http3","3. HTTP\u002F1.1 \u002F HTTP\u002F2 \u002F HTTP\u002F3",[20,2772,2773],{},"HTTP\u002F1.1、HTTP\u002F2、HTTP\u002F3 可以按“怎么让传输更快、更稳”来理解。",[20,2775,2776],{},"HTTP\u002F1.1 支持长连接，不用每个请求都重新建连接，但浏览器对同一个域名的连接数有限，而且同一连接上的请求容易互相等待。",[20,2778,2779],{},"HTTP\u002F2 主要改进是多路复用和头部压缩。多路复用表示多个请求可以在同一个 TCP 连接里并发传输，不用严格一个等一个；头部压缩减少了重复请求头的体积。但 HTTP\u002F2 底层还是 TCP，如果 TCP 丢包，同一连接里的数据仍然会受影响。",[20,2781,2782],{},"HTTP\u002F3 基于 QUIC，QUIC 跑在 UDP 之上，改善了 TCP 层面的队头阻塞，也更适合弱网下的连接迁移。",[20,2784,2785],{},"面试总结：HTTP\u002F1.1 解决长连接，HTTP\u002F2 解决应用层多路复用，HTTP\u002F3 用 QUIC 改善 TCP 队头阻塞和弱网体验。",[15,2787,2789],{"id":2788},"_4-tcp-与-udp","4. TCP 与 UDP",[20,2791,2792],{},"TCP 和 UDP 的核心区别是：TCP 更重视可靠性，UDP 更重视低延迟。",[20,2794,2795],{},"TCP 是面向连接的，传输前要先建立连接；它保证数据可靠、有序到达，所以适合 HTTP\u002FHTTPS、文件传输这类不能丢数据的场景。TCP 的代价是连接和可靠性机制会带来额外开销。",[20,2797,2798],{},"UDP 是无连接的，不保证可靠到达，也不保证顺序，但开销小、延迟低，适合实时音视频、游戏、直播、QUIC 这类更在意实时性的场景。丢一点数据可以由应用层自己处理。",[20,2800,2801],{},"面试总结：要可靠、有序选 TCP；要低延迟、能接受部分丢包或应用层兜底，可以选 UDP。",[15,2803,2805],{"id":2804},"_5-https-tls","5. HTTPS \u002F TLS",[20,2807,2808],{},"HTTPS 本质是 HTTP 加 TLS，解决 HTTP 明文传输不安全的问题。",[20,2810,2811],{},"它主要提供三件事：加密，防止内容被窃听；身份认证，确认你访问的确实是目标网站；完整性校验，防止数据传输过程中被篡改。",[20,2813,2814],{},"TLS 握手阶段会校验证书，并协商后续通信使用的会话密钥。非对称加密主要用于身份认证和密钥协商，真正传输大量数据时使用对称加密，因为它性能更好。",[20,2816,2817],{},"面试总结：HTTPS 不是新的应用层协议，而是 HTTP 套了一层 TLS，用证书和加密保证安全通信。",[15,2819,2821],{"id":2820},"_6-dns","6. DNS",[20,2823,2824,2825,2828],{},"DNS 的作用是把域名解析成 IP 地址，查询会经过浏览器缓存、系统缓存、递归 DNS、权威 DNS 等环节。优化手段包括 DNS 缓存、",[28,2826,2827],{"code":2827},"dns-prefetch","、减少不必要的跨域名资源。",[15,2830,2832],{"id":2831},"_7-跨域与-cors","7. 跨域与 CORS",[20,2834,2835,2836,2839],{},"跨域来自浏览器的",[23,2837,2838],{},"同源策略","。同源要求协议、域名、端口都相同，只要有一个不同，就算跨域。它限制的是浏览器里的脚本访问响应内容，目的是保护用户数据。",[20,2841,2842,2843,47,2846,47,2849,115],{},"CORS 是服务端通过响应头告诉浏览器“这个来源可以访问我”。常见响应头有 ",[28,2844,2845],{"code":2845},"Access-Control-Allow-Origin",[28,2847,2848],{"code":2848},"Access-Control-Allow-Methods",[28,2850,2851],{"code":2851},"Access-Control-Allow-Headers",[20,2853,2854,2855,2858],{},"如果是复杂请求，比如使用了特殊请求头、非简单方法，浏览器会先发一个 ",[28,2856,2857],{"code":2857},"OPTIONS"," 预检请求，确认服务端允许后再发真实请求。",[20,2860,2861],{},"面试总结：跨域不是请求一定发不出去，而是浏览器基于同源策略拦截了响应；CORS 的关键在服务端响应头。",[15,2863,2865],{"id":2864},"_8-cookie-session-token-jwt","8. Cookie \u002F Session \u002F Token \u002F JWT",[20,2867,2868],{},"Cookie、Session、Token、JWT 都和登录态有关，但职责不同。",[20,2870,2871],{},"Cookie 是浏览器保存的一小段数据，会自动随同域请求发送。Session 通常保存在服务端，浏览器只保存一个 session id，服务端根据这个 id 找到用户状态。",[20,2873,2874,2875,2878],{},"Token 通常是服务端签发的一段凭证，前端保存后放到请求头里，比如 ",[28,2876,2877],{"code":2877},"Authorization","。它适合前后端分离、多端登录、跨域接口调用。",[20,2880,2881],{},"JWT 是一种自包含 Token，里面可以带用户信息和过期时间，并通过签名防篡改。优点是服务端可以无状态校验；缺点是签发后不容易主动吊销，续期和泄露处理更复杂。",[20,2883,2884],{},"面试总结：Cookie 是存储和自动携带机制，Session 是服务端登录态，Token 是访问凭证，JWT 是一种带签名的 Token 格式。",[15,2886,2888],{"id":2887},"_9-xss","9. XSS",[20,2890,2891,2894],{},[23,2892,2893],{},"XSS"," 是攻击者把恶意脚本注入页面，让脚本在用户浏览器里执行。它可能窃取 Cookie、Token，伪造用户操作，或者篡改页面内容。",[20,2896,2897,2898,2901,2902,47,2905,47,2908,115],{},"常见来源是把用户输入当成 HTML 直接渲染，比如评论、昵称、富文本内容没有过滤。防护重点是：输入要校验，输出到页面前要转义；富文本要用白名单过滤；设置 CSP 限制脚本来源；敏感 Cookie 设置 ",[28,2899,2900],{"code":2900},"HttpOnly","，避免被 JS 读取；尽量少用 ",[28,2903,2904],{"code":2904},"innerHTML",[28,2906,2907],{"code":2907},"v-html",[28,2909,2910],{"code":2910},"dangerouslySetInnerHTML",[20,2912,2913],{},"面试总结：XSS 防的是恶意脚本在页面里执行，核心是不要信任用户输入，输出时做好转义和白名单过滤。",[15,2915,2917],{"id":2916},"_10-csrf","10. CSRF",[20,2919,2920,2923],{},[23,2921,2922],{},"CSRF"," 是攻击者利用用户已经登录的状态，诱导浏览器向目标网站发起请求。因为 Cookie 会自动携带，所以服务端可能误以为这是用户本人操作。",[20,2925,2926],{},"它和 XSS 的区别是：XSS 是把脚本注入你的页面里执行；CSRF 不一定能读取响应内容，而是借用用户登录态发请求。",[20,2928,2929,2930,2933,2934,2754,2937,2940],{},"防护方式包括：设置 ",[28,2931,2932],{"code":2932},"SameSite"," Cookie，减少跨站自动携带 Cookie；使用 CSRF Token，让攻击者拿不到合法 token；校验 ",[28,2935,2936],{"code":2936},"Origin",[28,2938,2939],{"code":2939},"Referer","；转账、改密码这类关键接口增加二次验证。",[20,2942,2943],{},"面试总结：CSRF 防的是伪造用户请求，重点是不要只靠 Cookie 判断用户意图。",[15,2945,2947],{"id":2946},"_11-点击劫持","11. 点击劫持",[20,2949,2950,2951,2954,2955,2958],{},"点击劫持是把目标页面放进透明 iframe 中，诱导用户点击攻击者想让他点击的位置。防护方式是设置 ",[28,2952,2953],{"code":2953},"X-Frame-Options"," 或 CSP 的 ",[28,2956,2957],{"code":2957},"frame-ancestors","，限制页面被其他站点嵌入。",[15,2960,2962],{"id":2961},"_12-websocket-与-sse","12. WebSocket 与 SSE",[20,2964,2965],{},"WebSocket 是全双工长连接，客户端和服务端可以互相主动发送消息，适合 IM、协同编辑、实时游戏。SSE 是服务端到客户端的单向推送，基于 HTTP，适合通知、日志、AI 流式输出，并且天然支持断线重连。",[15,2967,2969],{"id":2968},"_13-请求取消与竞态","13. 请求取消与竞态",[20,2971,2972,2973,2976],{},"搜索框、切页、组件卸载时，旧请求可能比新请求更晚返回，导致页面显示过期数据。常用 ",[28,2974,2975],{"code":2975},"AbortController"," 取消请求，或者用请求序号判断只处理最后一次响应。",[82,2978,2981],{"className":2979,"code":2980,"language":777,"meta":88},[775],"const controller = new AbortController();\nfetch(\"\u002Fapi\u002Fsearch\", { signal: controller.signal });\ncontroller.abort();\n",[28,2982,2980],{"__ignoreMap":88},[10,2984,2985],{"id":2985},"性能优化",[15,2987,2989],{"id":2988},"_1-核心性能指标","1. 核心性能指标",[20,2991,2992,2993,47,2996,47,2999,47,3002,3005],{},"常见性能指标有 ",[23,2994,2995],{},"FCP",[23,2997,2998],{},"LCP",[23,3000,3001],{},"CLS",[23,3003,3004],{},"INP","，可以分别对应用户感受。",[20,3007,3008],{},"FCP 看“页面什么时候开始有内容”；LCP 看“首屏主要内容什么时候出来”；CLS 看“页面加载过程中有没有乱跳”；INP 看“用户点击、输入后页面响应快不快”。",[20,3010,3011],{},"优化时不要凭感觉乱改，要先看是加载慢、渲染慢、布局不稳定，还是交互卡顿，再选择对应手段。",[20,3013,3014],{},"面试总结：FCP\u002FLCP 关注加载体验，CLS 关注视觉稳定性，INP 关注交互响应。",[15,3016,3018],{"id":3017},"_2-首屏优化","2. 首屏优化",[20,3020,3021],{},"首屏优化的核心是：让用户尽快看到可用的主要内容。它不是只优化某一个点，而是同时看资源、渲染、接口和图片。",[20,3023,3024,3025,3027],{},"常见手段包括：减少首屏 JS\u002FCSS 体积，使用路由懒加载；关键 CSS 内联或优先加载；首屏大图压缩并设置合适尺寸；关键资源用 ",[28,3026,124],{"code":124},"；接口慢时做缓存、并行请求或骨架屏；对 SEO 或首屏要求高的页面可以考虑 SSR\u002FSSG。",[20,3029,3030],{},"实际项目中，LCP 往往受首屏大图、字体、主包体积和首屏接口影响。面试总结：首屏优化就是缩短关键渲染路径，让关键内容先出来，非关键内容延后加载。",[15,3032,3034],{"id":3033},"_3-代码分割","3. 代码分割",[20,3036,3037],{},"代码分割就是把一个很大的 JS 包拆成多个小包，让用户先加载当前页面需要的代码，其他页面或低频功能等用到时再加载。",[20,3039,3040],{},"它主要解决首屏 JS 太大、解析执行时间太长的问题。常见拆分方式有按路由拆、按组件拆、按业务模块拆、把第三方依赖单独拆。",[20,3042,3043,3044,3047,3048,3051],{},"React 常用 ",[28,3045,3046],{"code":3046},"lazy + Suspense","，Vue 常用动态 ",[28,3049,3050],{"code":3050},"import()"," 配合路由懒加载。",[82,3053,3056],{"className":3054,"code":3055,"language":777,"meta":88},[775],"const Page = lazy(() => import(\".\u002FPage\"));\n",[28,3057,3055],{"__ignoreMap":88},[15,3059,3061],{"id":3060},"_4-tree-shaking","4. Tree Shaking",[20,3063,3064,3065,3068],{},"Tree Shaking 是构建时删除没有被使用的代码。它依赖 ESM 的静态结构，因为 ESM 的 ",[28,3066,3067],{"code":3067},"import\u002Fexport"," 在编译阶段就能分析出依赖关系。",[20,3070,3071,3072,3075],{},"要让 Tree Shaking 更容易生效，代码应尽量使用 ESM，避免模块顶层产生不可预测副作用，并正确配置 ",[28,3073,3074],{"code":3074},"sideEffects","。如果把有副作用的样式或初始化逻辑误标成无副作用，可能会被错误删除。",[20,3077,3078],{},"面试总结：代码分割是“按需加载代码”，Tree Shaking 是“删除没用代码”，两者都能减少最终加载成本，但解决的问题不同。",[15,3080,3082],{"id":3081},"_5-图片优化","5. 图片优化",[20,3084,3085],{},"图片优化包括压缩体积、使用 WebP\u002FAVIF、按屏幕尺寸加载合适图片、懒加载非首屏图片、给图片设置宽高避免 CLS。首屏关键图片不要盲目懒加载，反而可以配合 preload 提高加载优先级。",[15,3087,3089],{"id":3088},"_6-长列表优化","6. 长列表优化",[20,3091,3092,3093,3096],{},"长列表如果一次性渲染几千个 DOM，会导致渲染和滚动卡顿。",[23,3094,3095],{},"虚拟列表","只渲染可视区域附近的数据，用一个总高度容器撑开滚动条，从而显著减少 DOM 数量。",[20,3098,3099,3100,3103,3104,3106],{},"它的核心思路是：用户虽然有一万条数据，但屏幕上同一时间只能看到几十条，所以只渲染可视区附近的几十条。滚动时根据 ",[28,3101,3102],{"code":3102},"scrollTop"," 计算当前应该显示哪一段数据，再用 ",[28,3105,193],{"code":193}," 或占位高度把它放到正确位置。",[82,3108,3111],{"className":3109,"code":3110,"language":777,"meta":88},[775],"const getRange = (scrollTop: number, itemHeight: number, viewHeight: number, total: number) => {\n  const start = Math.floor(scrollTop \u002F itemHeight);\n  const size = Math.ceil(viewHeight \u002F itemHeight);\n  return {\n    start,\n    end: Math.min(total, start + size + 2),\n    offset: start * itemHeight,\n  };\n};\n",[28,3112,3110],{"__ignoreMap":88},[15,3114,3116],{"id":3115},"_7-交互性能优化","7. 交互性能优化",[20,3118,3119],{},"交互卡顿通常来自主线程太忙，比如长任务、频繁重渲染、大量 DOM、同步布局、复杂计算。用户点击或输入后，如果主线程还在忙，页面就会迟迟没有响应。",[20,3121,3122],{},"优化方式包括：把大任务拆成小块分批执行；把重计算放到 Web Worker；用缓存减少重复计算；用虚拟列表减少 DOM；在 React\u002FVue 中减少无效渲染；把非紧急更新延后。",[20,3124,3125],{},"面试总结：交互优化的核心是减少主线程被长时间占用，让用户操作能尽快得到响应。",[15,3127,3129],{"id":3128},"_8-内存优化","8. 内存优化",[20,3131,3132],{},"内存泄漏常见来源包括未清理定时器、事件监听、闭包持有大对象、全局缓存无限增长、组件卸载后异步回调还在执行。排查时可以用 Chrome DevTools 的 Memory 面板对比 Heap Snapshot，看对象是否能被正常回收。",[15,3134,3136],{"id":3135},"_9-前端监控","9. 前端监控",[20,3138,3139],{},"前端监控通常包括错误监控、性能监控、接口监控、资源加载监控和用户行为埋点。一个完整闭环应该包含采集、上报、聚合、告警、定位和修复验证，而不是只把日志打出来。",[10,3141,3142],{"id":3142},"工程化",[15,3144,3146],{"id":3145},"_1-webpack","1. Webpack",[20,3148,3149,3150,2754,3153,3156],{},"Webpack 可以理解成一个模块打包器。它从入口文件开始，沿着 ",[28,3151,3152],{"code":3152},"import",[28,3154,3155],{"code":3155},"require"," 递归分析依赖，形成模块依赖图，然后经过 loader 转换不同类型文件，再通过 plugin 扩展构建流程，最终输出 bundle。",[20,3158,3159,3160,3163,3164,3167,3168,3171,3172,3175,3176,3179],{},"核心概念可以这样记：",[28,3161,3162],{"code":3162},"entry"," 是入口，",[28,3165,3166],{"code":3166},"output"," 是出口，",[28,3169,3170],{"code":3170},"loader"," 负责转换文件，",[28,3173,3174],{"code":3174},"plugin"," 负责扩展流程，",[28,3177,3178],{"code":3178},"chunk"," 是拆出来的代码块。",[15,3181,3183],{"id":3182},"_2-vite","2. Vite",[20,3185,3186],{},"Vite 快主要快在开发阶段。它利用浏览器原生 ESM，启动时不需要先把整个项目打成一个包，而是按需加载当前页面用到的源码模块，所以冷启动快；文件修改后也只更新受影响的模块，所以 HMR 快。",[20,3188,3189],{},"生产环境下，Vite 通常还是基于 Rollup 打包，做代码压缩、分包和兼容处理。面试总结：Webpack 开发阶段更偏“先打包再运行”，Vite 开发阶段更偏“按需加载源码模块”。",[15,3191,3193],{"id":3192},"_3-loader-与-plugin","3. Loader 与 Plugin",[20,3195,3196,3197,115],{},"Loader 和 Plugin 的区别可以一句话记：",[23,3198,3199],{},"Loader 处理文件内容，Plugin 介入构建流程",[20,3201,3202],{},"Loader 像转换器，把 TS、CSS、图片等不同类型文件转换成 Webpack 能理解的模块。Plugin 像插件，可以在构建生命周期中做额外事情，比如生成 HTML、压缩资源、抽离 CSS、分析包体积。",[20,3204,3205],{},"面试总结：Loader 关注“某类文件怎么变成模块”，Plugin 关注“构建过程里额外做什么”。",[15,3207,3209],{"id":3208},"_4-babel-与-swc","4. Babel 与 SWC",[20,3211,3212,3213,3216],{},"Babel 主要用于 JS 语法转换和兼容性处理，生态成熟；SWC 用 Rust 实现，编译速度更快。",[28,3214,3215],{"code":3215},"preset-env"," 会根据目标浏览器决定需要转换哪些语法，以及是否注入 polyfill。",[15,3218,3220],{"id":3219},"_5-source-map","5. Source Map",[20,3222,3223],{},"Source Map 用来把压缩混淆后的线上代码映射回源码，方便定位报错行列。生产环境要控制访问权限，否则可能把源码直接暴露给外部用户。",[15,3225,3227],{"id":3226},"_6-npm-yarn-pnpm","6. npm \u002F yarn \u002F pnpm",[20,3229,3230],{},"npm 是默认包管理器，yarn 提供过更稳定的依赖安装体验，pnpm 通过内容寻址和硬链接节省磁盘，并能更好地避免幽灵依赖。现代 monorepo 项目里 pnpm workspace 使用非常多。",[15,3232,3234],{"id":3233},"_7-monorepo","7. Monorepo",[20,3236,3237],{},"Monorepo 是把多个包放在同一个仓库中管理，适合组件库、工具库、多应用协作。它的优势是统一规范、统一依赖、方便跨包联调，难点是任务编排、依赖拓扑、版本发布和权限边界。",[15,3239,3241],{"id":3240},"_8-eslint-prettier-stylelint","8. ESLint \u002F Prettier \u002F Stylelint",[20,3243,3244],{},"ESLint 管 JS\u002FTS 代码质量，Prettier 管代码格式，Stylelint 管样式规范。团队规范最好落到编辑器、pre-commit 和 CI 中，避免只靠人工 code review。",[15,3246,3248],{"id":3247},"_9-cicd","9. CI\u002FCD",[20,3250,3251],{},"CI 负责自动化校验，比如 lint、test、build；CD 负责自动化部署，比如发布、灰度、回滚。前端流水线要重点保证环境变量隔离、构建可复现、产物可追踪、失败可回滚。",[15,3253,3255],{"id":3254},"_10-微前端","10. 微前端",[20,3257,3258],{},"微前端是把大型前端拆成多个可独立开发、部署和运行的子应用，适合多团队维护的大型中后台。核心难点是路由隔离、样式隔离、状态通信、依赖共享、性能和部署治理。",[10,3260,3261],{"id":3261},"状态管理与架构",[15,3263,3265],{"id":3264},"_1-前端状态分类","1. 前端状态分类",[20,3267,3268],{},"前端状态不要一上来都放全局 store，可以先按来源和用途分类。",[20,3270,3271],{},"本地 UI 状态只影响当前组件，比如弹窗开关、输入框内容，适合放组件 state。跨组件共享状态会被多个页面或组件使用，比如用户信息、主题、权限，适合放全局 store。服务端状态来自接口，比如列表数据、详情数据，它还涉及缓存、过期、重试、分页，适合交给 React Query \u002F SWR 这类工具。URL 状态适合放筛选条件、分页参数，因为它需要可分享、可回退。",[20,3273,3274],{},"面试总结：状态管理的关键不是工具，而是先判断状态属于哪一类，再选合适位置管理。",[15,3276,3278],{"id":3277},"_2-redux-zustand-pinia","2. Redux \u002F Zustand \u002F Pinia",[20,3280,3281],{},"Redux、Zustand、Pinia 都是状态管理工具，但适合的场景不同。",[20,3283,3284],{},"Redux 强调单向数据流、不可变更新和可预测性，适合大型项目、多人协作、需要严格调试和中间件能力的场景。Zustand 更轻量，写法简单，适合 React 中小型项目或局部复杂状态。Pinia 是 Vue3 推荐状态库，API 简洁，类型推导友好。",[20,3286,3287],{},"面试总结：选型要看团队规模、状态复杂度、调试需求、类型体验和维护成本，不要只说“哪个更好”。",[15,3289,3291],{"id":3290},"_3-服务端状态管理","3. 服务端状态管理",[20,3293,3294],{},"服务端状态指的是从接口拿来的数据，比如用户列表、订单详情、搜索结果。它和普通 UI 状态不一样，因为它会过期、需要重新请求、可能失败、可能分页、还可能出现请求竞态。",[20,3296,3297],{},"React Query、SWR 这类库解决的是远程数据同步问题，比如缓存、重新验证、重试、预取、请求去重。Redux、Pinia 更偏客户端状态管理，两者不应该完全混为一谈。",[20,3299,3300],{},"面试总结：接口数据优先考虑服务端状态管理，本地交互状态再考虑组件 state 或全局 store。",[15,3302,3304],{"id":3303},"_4-组件设计","4. 组件设计",[20,3306,3307],{},"好的组件不是 props 越多越好，而是职责清楚、输入输出稳定、默认行为合理、扩展点明确。",[20,3309,3310],{},"展示组件主要负责 UI 展示，尽量少关心数据从哪里来；容器组件负责请求数据、处理副作用、连接状态管理；基础组件要尽量通用，不要塞太多业务特例，否则后面复用和维护都会变难。",[20,3312,3313],{},"面试总结：组件封装要控制职责边界，业务变化放在外层组合，基础能力放在组件内部。",[15,3315,3317],{"id":3316},"_5-权限设计","5. 权限设计",[20,3319,3320],{},"前端权限常分为路由权限、菜单权限、按钮权限和数据权限。前端权限主要是控制用户体验和入口展示，真正的安全必须由后端校验，否则用户仍然可以绕过前端直接请求接口。",[15,3322,3324],{"id":3323},"_6-错误边界","6. 错误边界",[20,3326,3327,3328,3331],{},"React 的 Error Boundary 可以捕获渲染阶段错误，Vue 可以用 ",[28,3329,3330],{"code":3330},"errorCaptured"," 或全局错误处理。它们不能覆盖所有错误，比如异步错误、事件回调错误、接口错误仍然需要单独捕获和上报。",[10,3333,3334],{"id":3334},"测试",[15,3336,3338],{"id":3337},"_1-单元测试","1. 单元测试",[20,3340,3341],{},"单元测试用于验证一个函数、组件或模块的最小行为。它适合工具函数、复杂计算逻辑、稳定的业务规则。",[20,3343,3344],{},"比如金额计算、权限判断、URL 解析、数据转换，这些逻辑一旦错了影响很大，而且输入输出明确，就很适合写单元测试。",[20,3346,3347],{},"面试总结：单元测试关注小范围逻辑是否正确，重点测行为结果，不要过度依赖内部实现细节。",[15,3349,3351],{"id":3350},"_2-组件测试","2. 组件测试",[20,3353,3354],{},"组件测试关注组件在页面上的展示和交互是否符合预期。它不是测试组件内部调用了哪个函数，而是从用户视角看：文本有没有出现、按钮能不能点击、输入后页面有没有变化。",[20,3356,3357],{},"Testing Library 推荐通过文本、角色、label 查询元素，因为这更接近真实用户和辅助技术访问页面的方式。",[20,3359,3360],{},"面试总结：组件测试少测实现，多测用户能看到什么、能做什么。",[15,3362,3364],{"id":3363},"_3-e2e-测试","3. E2E 测试",[20,3366,3367],{},"E2E 测试是从真实用户路径出发，验证一整条业务链路，比如登录、下单、支付、核心表单提交。",[20,3369,3370],{},"它覆盖面强，能发现前后端联动问题，但运行慢、维护成本高，所以不适合把所有细节都写成 E2E。通常只覆盖核心路径和高风险流程。",[20,3372,3373],{},"面试总结：单元测试保逻辑，组件测试保交互，E2E 测试保关键链路。",[15,3375,3377],{"id":3376},"_4-mock","4. Mock",[20,3379,3380],{},"Mock 的作用是隔离外部依赖，让测试更稳定可控。接口 Mock 可以用 MSW，在浏览器和 Node 测试环境中拦截请求，比直接 mock fetch 更贴近真实网络行为。",[10,3382,3384],{"id":3383},"nodejs-与-ssr","Node.js 与 SSR",[15,3386,3388],{"id":3387},"_1-node-事件循环","1. Node 事件循环",[20,3390,3391,3392,3395],{},"Node 也有事件循环，但它和浏览器不完全一样。Node 的事件循环分成多个阶段，比如 timers、poll、check 等，分别处理定时器、I\u002FO 回调、",[28,3393,3394],{"code":3394},"setImmediate"," 等任务。",[20,3397,3398,3399,3402],{},"Node 里还要特别注意 ",[28,3400,3401],{"code":3401},"process.nextTick","，它的优先级高于普通 Promise 微任务，容易在执行顺序题里出现。",[20,3404,3405,3406,115],{},"面试总结：浏览器事件循环重点看宏任务、微任务和渲染；Node 事件循环还要区分阶段，并注意 ",[28,3407,3401],{"code":3401},[15,3409,3411],{"id":3410},"_2-express-与-koa","2. Express 与 Koa",[20,3413,3414],{},"Express 中间件模型直接，生态成熟；Koa 基于洋葱模型，更强调 async\u002Fawait。中间件本质上就是按顺序处理请求、响应和错误，适合做鉴权、日志、异常处理、代理转发等。",[15,3416,3418],{"id":3417},"_3-ssr","3. SSR",[20,3420,3421],{},"SSR 是服务端渲染，意思是服务端先把页面 HTML 生成好，再返回给浏览器。浏览器拿到 HTML 后能更快看到内容，然后前端 JS 再接管交互，这个接管过程叫水合。",[20,3423,3424],{},"SSR 的优点是首屏更快、SEO 更好；缺点是服务端压力更大，工程复杂度更高。常见难点包括数据预取、状态注水、同构代码、水合不一致、缓存策略。",[20,3426,3427],{},"面试总结：SSR 解决的是首屏和 SEO 问题，但会增加服务端和工程复杂度。",[15,3429,3431],{"id":3430},"_4-csr-ssr-ssg","4. CSR \u002F SSR \u002F SSG",[20,3433,3434],{},"CSR、SSR、SSG 的区别在于 HTML 什么时候生成。",[20,3436,3437],{},"CSR 是浏览器端渲染，服务端先返回一个空壳 HTML，再由 JS 拉数据、生成页面。它交互灵活，但首屏和 SEO 较弱。",[20,3439,3440],{},"SSR 是请求时在服务端生成 HTML，首屏和 SEO 更好，但服务端压力更大。",[20,3442,3443],{},"SSG 是构建时提前生成静态 HTML，访问时直接返回静态文件，性能好、部署简单，但适合内容变化不太频繁的页面。",[20,3445,3446],{},"面试总结：强交互后台常用 CSR，重 SEO 和首屏可考虑 SSR，内容稳定的营销页、文档、博客适合 SSG。",[10,3448,3449],{"id":3449},"移动端与小程序",[15,3451,3453],{"id":3452},"_1-移动端-1px-问题","1. 移动端 1px 问题",[20,3455,3456],{},"移动端 1px 问题来自 CSS 像素和设备物理像素比例不同。比如 DPR 为 2 的屏幕上，1 个 CSS 像素可能对应 2 个物理像素，所以边框看起来会比设计稿更粗。",[20,3458,3459,3460,1557,3463,3466],{},"常见方案是用伪元素画边框，再通过 ",[28,3461,3462],{"code":3462},"transform: scaleY(0.5)",[28,3464,3465],{"code":3465},"scaleX(0.5)"," 压缩，或者使用 UI 组件库提供的 hairline 边框方案。",[20,3468,3469],{},"面试总结：1px 问题本质是 CSS 像素和物理像素不一致导致的视觉粗细问题。",[15,3471,3473],{"id":3472},"_2-安全区域适配","2. 安全区域适配",[20,3475,3476],{},"全面屏和刘海屏需要处理 safe area，避免底部按钮、固定导航或顶部内容被系统手势区域、刘海区域遮挡。",[20,3478,3479,3480,3483],{},"常用 ",[28,3481,3482],{"code":3482},"env(safe-area-inset-bottom)"," 给底部固定元素增加安全间距，也可以处理顶部、左侧、右侧安全区域。",[82,3485,3488],{"className":3486,"code":3487,"language":153,"meta":88},[151],".footer {\n  padding-bottom: env(safe-area-inset-bottom);\n}\n",[28,3489,3487],{"__ignoreMap":88},[15,3491,3493],{"id":3492},"_3-移动端点击与滚动","3. 移动端点击与滚动",[20,3495,3496],{},"早期移动端有 300ms 点击延迟，现代浏览器在正确设置 viewport 后基本已经解决。复杂手势场景要注意 touch 事件、滚动穿透、被动监听和手势冲突。",[15,3498,3500],{"id":3499},"_4-小程序架构","4. 小程序架构",[20,3502,3503,3504,3507],{},"小程序通常分为逻辑层和视图层，两者通过桥通信。频繁 ",[28,3505,3506],{"code":3506},"setData"," 或一次传输大量数据会影响性能，所以应减少更新频率和数据体积，只更新真正变化的字段。",[10,3509,3510],{"id":3510},"手写与算法高频",[15,3512,3514],{"id":3513},"_1-并发控制","1. 并发控制",[20,3516,3517],{},"并发控制是限制同时执行的异步任务数量。比如有 100 个请求，不希望一次性全发出去，而是最多同时发 5 个，谁完成了就补下一个。",[20,3519,3520],{},"它常用于批量上传、批量请求、图片处理。核心思路是维护一个任务指针，启动固定数量 worker，每个 worker 执行完一个任务后继续取下一个。",[82,3522,3525],{"className":3523,"code":3524,"language":777,"meta":88},[775],"async function limit\u003CT>(tasks: Array\u003C() => Promise\u003CT>>, max: number): Promise\u003CT[]> {\n  const result: T[] = [];\n  let next = 0;\n\n  async function worker() {\n    while (next \u003C tasks.length) {\n      const current = next++;\n      result[current] = await tasks[current]();\n    }\n  }\n\n  await Promise.all(Array.from({ length: max }, worker));\n  return result;\n}\n",[28,3526,3524],{"__ignoreMap":88},[15,3528,3530],{"id":3529},"_2-发布订阅","2. 发布订阅",[20,3532,3533,3534,3536,3537,3540],{},"发布订阅通过一个事件中心解耦事件发送者和接收者。发送者只负责 ",[28,3535,2432],{"code":2432}," 一个事件，不需要知道谁在监听；接收者只负责 ",[28,3538,3539],{"code":3539},"on"," 订阅事件，不需要知道是谁触发的。",[20,3542,3543,3544,3546,3547,3549,3550,3553],{},"它常用于组件通信、插件机制、业务事件流。核心 API 一般是 ",[28,3545,3539],{"code":3539}," 订阅、",[28,3548,2432],{"code":2432}," 发布、",[28,3551,3552],{"code":3552},"off"," 取消订阅。",[82,3555,3558],{"className":3556,"code":3557,"language":777,"meta":88},[775],"class EventBus {\n  private events = new Map\u003Cstring, Set\u003C(...args: unknown[]) => void>>();\n\n  on(type: string, fn: (...args: unknown[]) => void) {\n    const set = this.events.get(type) ?? new Set();\n    set.add(fn);\n    this.events.set(type, set);\n  }\n\n  emit(type: string, ...args: unknown[]) {\n    this.events.get(type)?.forEach((fn) => fn(...args));\n  }\n\n  off(type: string, fn: (...args: unknown[]) => void) {\n    this.events.get(type)?.delete(fn);\n  }\n}\n",[28,3559,3557],{"__ignoreMap":88},[15,3561,3563],{"id":3562},"_3-lru-缓存","3. LRU 缓存",[20,3565,3566],{},"LRU 的意思是最近最少使用。它解决的是缓存空间有限时该淘汰谁的问题：缓存满了，就优先删除最久没有被访问的数据。",[20,3568,3569,3570,3573,3574,3576],{},"JS 的 ",[28,3571,3572],{"code":3572},"Map"," 会保持插入顺序，所以可以用它实现 LRU：每次访问某个 key，就先删除再重新插入，让它变成“最新使用”；超过容量时删除 ",[28,3575,3572],{"code":3572}," 里的第一个 key。",[82,3578,3581],{"className":3579,"code":3580,"language":777,"meta":88},[775],"class LRU\u003CK, V> {\n  private cache = new Map\u003CK, V>();\n\n  constructor(private capacity: number) {}\n\n  get(key: K) {\n    const value = this.cache.get(key);\n    if (value === undefined) return undefined;\n    this.cache.delete(key);\n    this.cache.set(key, value);\n    return value;\n  }\n\n  set(key: K, value: V) {\n    if (this.cache.has(key)) this.cache.delete(key);\n    this.cache.set(key, value);\n    if (this.cache.size > this.capacity) {\n      this.cache.delete(this.cache.keys().next().value);\n    }\n  }\n}\n",[28,3582,3580],{"__ignoreMap":88},[15,3584,3586],{"id":3585},"_4-数组去重","4. 数组去重",[20,3588,3589,3590,3593],{},"基础类型数组去重可以直接用 ",[28,3591,3592],{"code":3592},"Set","，因为 Set 天然不允许重复值。对象数组去重一般要根据业务唯一键，比如 id，用 Map 保存最后一次或第一次出现的对象。",[82,3595,3598],{"className":3596,"code":3597,"language":777,"meta":88},[775],"const unique = \u003CT>(list: T[]) => [...new Set(list)];\n",[28,3599,3597],{"__ignoreMap":88},[15,3601,3603],{"id":3602},"_5-数组扁平化","5. 数组扁平化",[20,3605,3606,3607,3610],{},"数组扁平化就是把多层嵌套数组展开成一层。工程里可以用 ",[28,3608,3609],{"code":3609},"flat(Infinity)","，手写时通常用递归或栈来实现。",[82,3612,3615],{"className":3613,"code":3614,"language":777,"meta":88},[775],"const flatten = (arr: unknown[]): unknown[] =>\n  arr.reduce\u003Cunknown[]>((res, item) => {\n    return res.concat(Array.isArray(item) ? flatten(item) : item);\n  }, []);\n",[28,3616,3614],{"__ignoreMap":88},[15,3618,3620],{"id":3619},"_6-柯里化","6. 柯里化",[20,3622,3623,3624,3627,3628,115],{},"柯里化是把一个接收多个参数的函数，变成多个连续接收参数的函数。比如 ",[28,3625,3626],{"code":3626},"sum(1, 2, 3)"," 可以变成 ",[28,3629,3630],{"code":3630},"sum(1)(2)(3)",[20,3632,3633],{},"它常用于参数复用、延迟执行和函数组合，本质是利用闭包保存已经传入的参数。",[82,3635,3638],{"className":3636,"code":3637,"language":777,"meta":88},[775],"const curry = (fn: (...args: any[]) => any, ...args: any[]): any =>\n  args.length >= fn.length ? fn(...args) : (...rest: any[]) => curry(fn, ...args, ...rest);\n",[28,3639,3637],{"__ignoreMap":88},[15,3641,3643],{"id":3642},"_7-常见算法方向","7. 常见算法方向",[20,3645,3646,3647,3650,3651,3653],{},"前端算法高频集中在数组、字符串、哈希表、栈、队列、链表、树、双指针、滑动窗口。面试时不仅要写出代码，还要说清时间复杂度和空间复杂度，比如哈希去重通常是 ",[23,3648,3649],{},"O(n)"," 时间和 ",[23,3652,3649],{}," 空间。",[10,3655,3656],{"id":3656},"项目深挖",[15,3658,3660],{"id":3659},"_1-项目介绍怎么讲","1. 项目介绍怎么讲",[20,3662,3663,3664,3667],{},"项目介绍不要只罗列技术栈，建议按",[23,3665,3666],{},"业务背景、项目目标、个人职责、技术难点、解决方案、最终结果","来讲。面试官真正关注的是你是否真的参与过核心问题，以及你做出的技术决策是否有依据。",[15,3669,3671],{"id":3670},"_2-技术选型怎么讲","2. 技术选型怎么讲",[20,3673,3674],{},"技术选型要围绕业务诉求、团队熟悉度、生态成熟度、性能、维护成本和迁移成本展开。不要只说“因为它快”或“因为它新”，要说明它具体解决了什么问题，以及有没有权衡过替代方案。",[15,3676,3678],{"id":3677},"_3-复杂组件封装","3. 复杂组件封装",[20,3680,3681],{},"复杂组件比如表格、表单、上传器、编辑器，要重点讲状态模型、扩展点、异常处理、性能优化和类型设计。好的封装不是参数越多越好，而是默认行为稳定、核心能力可组合、业务特例不污染基础组件。",[15,3683,3685],{"id":3684},"_4-性能治理项目","4. 性能治理项目",[20,3687,3688],{},"性能治理要讲闭环：如何发现问题、用什么指标定位、做了哪些优化、上线后收益如何。最好准备具体数据，比如主包体积减少多少、LCP 降低多少、接口耗时减少多少、错误率下降多少。",[15,3690,3692],{"id":3691},"_5-线上问题排查","5. 线上问题排查",[20,3694,3695],{},"线上问题排查可以按影响范围、复现路径、监控日志、接口链路、最近发布、回滚策略来讲。修复后还要补回归测试、监控告警或防御性代码，体现你不是只修一次 bug，而是在降低同类问题再次发生的概率。",[10,3697,3698],{"id":3698},"复习优先级",[409,3700,3701,3707,3713],{},[412,3702,3703,3706],{},[23,3704,3705],{},"第一优先级","：JavaScript 执行机制、闭包、原型链、Promise、Event Loop、Vue\u002FReact 响应式与渲染、HTTP 缓存、跨域、安全。",[412,3708,3709,3712],{},[23,3710,3711],{},"第二优先级","：TypeScript 泛型与高级类型、浏览器渲染、性能优化、Webpack\u002FVite、状态管理、组件设计。",[412,3714,3715,3718],{},[23,3716,3717],{},"第三优先级","：SSR、微前端、监控体系、CI\u002FCD、测试、小程序、Node、复杂项目复盘和算法手写。",{"title":88,"searchDepth":3720,"depth":3720,"links":3721},4,[3722,3740,3760,3770,3777,3789,3803,3818,3829,3841,3849,3855,3861,3867,3876,3883],{"id":12,"depth":1648,"text":13,"children":3723},[3724,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739],{"id":17,"depth":3725,"text":18},3,{"id":54,"depth":3725,"text":55},{"id":68,"depth":3725,"text":69},{"id":93,"depth":3725,"text":94},{"id":118,"depth":3725,"text":119},{"id":132,"depth":3725,"text":133},{"id":158,"depth":3725,"text":159},{"id":180,"depth":3725,"text":181},{"id":204,"depth":3725,"text":205},{"id":227,"depth":3725,"text":228},{"id":255,"depth":3725,"text":256},{"id":271,"depth":3725,"text":272},{"id":384,"depth":3725,"text":385},{"id":602,"depth":3725,"text":603},{"id":697,"depth":3725,"text":698},{"id":716,"depth":1648,"text":717,"children":3741},[3742,3743,3744,3745,3746,3747,3748,3749,3750,3751,3752,3753,3754,3755,3756,3757,3758,3759],{"id":720,"depth":3725,"text":721},{"id":785,"depth":3725,"text":786},{"id":916,"depth":3725,"text":917},{"id":998,"depth":3725,"text":999},{"id":1068,"depth":3725,"text":1069},{"id":1112,"depth":3725,"text":1113},{"id":1158,"depth":3725,"text":1159},{"id":1279,"depth":3725,"text":1280},{"id":1353,"depth":3725,"text":1354},{"id":1393,"depth":3725,"text":1394},{"id":1436,"depth":3725,"text":1437},{"id":1487,"depth":3725,"text":1488},{"id":1524,"depth":3725,"text":1525},{"id":1735,"depth":3725,"text":1736},{"id":1777,"depth":3725,"text":1778},{"id":1801,"depth":3725,"text":1802},{"id":1821,"depth":3725,"text":1822},{"id":1835,"depth":3725,"text":1836},{"id":1846,"depth":1648,"text":1847,"children":3761},[3762,3763,3764,3765,3766,3767,3768,3769],{"id":1850,"depth":3725,"text":1851},{"id":1865,"depth":3725,"text":1866},{"id":1907,"depth":3725,"text":1908},{"id":1951,"depth":3725,"text":1952},{"id":1990,"depth":3725,"text":1991},{"id":2036,"depth":3725,"text":2037},{"id":2066,"depth":3725,"text":2067},{"id":2106,"depth":3725,"text":2107},{"id":2122,"depth":1648,"text":2122,"children":3771},[3772,3773,3774,3775,3776],{"id":2125,"depth":3725,"text":2126},{"id":2147,"depth":3725,"text":2148},{"id":2167,"depth":3725,"text":2168},{"id":2203,"depth":3725,"text":2204},{"id":2225,"depth":3725,"text":2226},{"id":2239,"depth":1648,"text":2240,"children":3778},[3779,3780,3781,3782,3783,3784,3785,3786,3787,3788],{"id":2243,"depth":3725,"text":2244},{"id":2258,"depth":3725,"text":2259},{"id":2301,"depth":3725,"text":2302},{"id":2352,"depth":3725,"text":2353},{"id":2394,"depth":3725,"text":2395},{"id":2411,"depth":3725,"text":2412},{"id":2422,"depth":3725,"text":2423},{"id":2440,"depth":3725,"text":2441},{"id":2458,"depth":3725,"text":2459},{"id":2475,"depth":3725,"text":2476},{"id":2482,"depth":1648,"text":2483,"children":3790},[3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802],{"id":2486,"depth":3725,"text":2487},{"id":2503,"depth":3725,"text":2504},{"id":2519,"depth":3725,"text":2520},{"id":2543,"depth":3725,"text":2544},{"id":2553,"depth":3725,"text":2554},{"id":2577,"depth":3725,"text":2578},{"id":2598,"depth":3725,"text":2599},{"id":2608,"depth":3725,"text":2609},{"id":2626,"depth":3725,"text":2627},{"id":2639,"depth":3725,"text":2640},{"id":2646,"depth":3725,"text":2647},{"id":2653,"depth":3725,"text":2654},{"id":2663,"depth":1648,"text":2663,"children":3804},[3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817],{"id":2666,"depth":3725,"text":2667},{"id":2722,"depth":3725,"text":2723},{"id":2769,"depth":3725,"text":2770},{"id":2788,"depth":3725,"text":2789},{"id":2804,"depth":3725,"text":2805},{"id":2820,"depth":3725,"text":2821},{"id":2831,"depth":3725,"text":2832},{"id":2864,"depth":3725,"text":2865},{"id":2887,"depth":3725,"text":2888},{"id":2916,"depth":3725,"text":2917},{"id":2946,"depth":3725,"text":2947},{"id":2961,"depth":3725,"text":2962},{"id":2968,"depth":3725,"text":2969},{"id":2985,"depth":1648,"text":2985,"children":3819},[3820,3821,3822,3823,3824,3825,3826,3827,3828],{"id":2988,"depth":3725,"text":2989},{"id":3017,"depth":3725,"text":3018},{"id":3033,"depth":3725,"text":3034},{"id":3060,"depth":3725,"text":3061},{"id":3081,"depth":3725,"text":3082},{"id":3088,"depth":3725,"text":3089},{"id":3115,"depth":3725,"text":3116},{"id":3128,"depth":3725,"text":3129},{"id":3135,"depth":3725,"text":3136},{"id":3142,"depth":1648,"text":3142,"children":3830},[3831,3832,3833,3834,3835,3836,3837,3838,3839,3840],{"id":3145,"depth":3725,"text":3146},{"id":3182,"depth":3725,"text":3183},{"id":3192,"depth":3725,"text":3193},{"id":3208,"depth":3725,"text":3209},{"id":3219,"depth":3725,"text":3220},{"id":3226,"depth":3725,"text":3227},{"id":3233,"depth":3725,"text":3234},{"id":3240,"depth":3725,"text":3241},{"id":3247,"depth":3725,"text":3248},{"id":3254,"depth":3725,"text":3255},{"id":3261,"depth":1648,"text":3261,"children":3842},[3843,3844,3845,3846,3847,3848],{"id":3264,"depth":3725,"text":3265},{"id":3277,"depth":3725,"text":3278},{"id":3290,"depth":3725,"text":3291},{"id":3303,"depth":3725,"text":3304},{"id":3316,"depth":3725,"text":3317},{"id":3323,"depth":3725,"text":3324},{"id":3334,"depth":1648,"text":3334,"children":3850},[3851,3852,3853,3854],{"id":3337,"depth":3725,"text":3338},{"id":3350,"depth":3725,"text":3351},{"id":3363,"depth":3725,"text":3364},{"id":3376,"depth":3725,"text":3377},{"id":3383,"depth":1648,"text":3384,"children":3856},[3857,3858,3859,3860],{"id":3387,"depth":3725,"text":3388},{"id":3410,"depth":3725,"text":3411},{"id":3417,"depth":3725,"text":3418},{"id":3430,"depth":3725,"text":3431},{"id":3449,"depth":1648,"text":3449,"children":3862},[3863,3864,3865,3866],{"id":3452,"depth":3725,"text":3453},{"id":3472,"depth":3725,"text":3473},{"id":3492,"depth":3725,"text":3493},{"id":3499,"depth":3725,"text":3500},{"id":3510,"depth":1648,"text":3510,"children":3868},[3869,3870,3871,3872,3873,3874,3875],{"id":3513,"depth":3725,"text":3514},{"id":3529,"depth":3725,"text":3530},{"id":3562,"depth":3725,"text":3563},{"id":3585,"depth":3725,"text":3586},{"id":3602,"depth":3725,"text":3603},{"id":3619,"depth":3725,"text":3620},{"id":3642,"depth":3725,"text":3643},{"id":3656,"depth":1648,"text":3656,"children":3877},[3878,3879,3880,3881,3882],{"id":3659,"depth":3725,"text":3660},{"id":3670,"depth":3725,"text":3671},{"id":3677,"depth":3725,"text":3678},{"id":3684,"depth":3725,"text":3685},{"id":3691,"depth":3725,"text":3692},{"id":3698,"depth":1648,"text":3698},[3885],"技术","2026-05-28 12:00","复习建议：先理解，再记忆。遇到抽象概念时，优先问三个问题：它解决什么问题、和相似概念有什么区别、项目里什么时候会用。",false,"md",null,{"slots":3892},{},true,"\u002F2026\u002F20260528",{"text":3896,"minutes":3897,"time":3898,"words":3899},"89 min read",88.56,5313600,17712,{"title":5,"description":3887},{"loc":3894},"posts\u002F2026\u002F20260528",[],"tech","knIIRwCX9KpajpZgKGzi0vCIaSaU41JYVa_T2Cx632Y",[3907,3912],{"title":3908,"path":3909,"stem":3910,"date":3911,"type":3904,"children":-1},"携程暑期面经1","\u002F2026\u002F20265161","posts\u002F2026\u002F20265161","2026-05-15 18:08",{"title":3913,"path":3914,"stem":3915,"date":3916,"type":3904,"children":-1},"八股提纲版","\u002F2026\u002Ffrontend-core-interview-question-outline","posts\u002F2026\u002Ffrontend-core-interview-question-outline","2026-05-28 19:00",1779968289640]