[{"data":1,"prerenderedAt":13629},["ShallowReactive",2],{"\u002Ftutorials\u002Fjs":3,"tutorials-search\u002Ftutorials\u002Fjs":13122},{"id":4,"title":5,"body":6,"category":385,"cover":13112,"date":13113,"description":13114,"extension":13115,"meta":13116,"navigation":98,"path":13117,"seo":13118,"sitemap":13119,"stem":13120,"__hash__":13121},"tutorials\u002Ftutorials\u002Fjs.md","JavaScript 教程：从变量到异步实战",{"type":7,"value":8,"toc":12954},"minimark",[9,14,18,26,35,222,333,355,361,374,378,381,536,552,556,596,600,621,624,725,820,824,849,853,861,936,956,961,965,970,1075,1087,1090,1093,1180,1184,1195,1199,1226,1393,1398,1402,1557,1576,1579,1686,1689,1692,1700,1735,1762,1766,1769,1779,1844,1870,1874,1887,1948,1978,2008,2016,2056,2060,2076,2079,2110,2131,2135,2139,2148,2237,2247,2254,2281,2286,2289,2399,2424,2501,2509,2525,2671,2688,2692,2702,2706,2837,2852,2857,2861,2948,2961,2997,3001,3023,3086,3103,3106,3109,3206,3227,3231,3242,3329,3357,3361,3368,3372,3482,3504,3508,3610,3627,3630,3668,3672,3676,3684,3798,3820,3823,3830,3834,3848,3852,3973,3984,3987,4187,4191,4194,4221,4245,4249,4267,4270,4410,4422,4426,4536,4619,4623,4646,4721,4724,4817,4846,4850,4853,4904,4908,4919,5167,5170,5234,5261,5264,5416,5420,5435,5441,5595,5619,5622,5631,5635,5650,5654,5830,5833,5933,5944,5947,6041,6055,6058,6061,6173,6176,6226,6230,6234,6248,6330,6347,6351,6365,6369,6501,6520,6524,6613,6627,6631,6703,6714,6722,6728,6821,6826,6830,6915,6933,6937,7110,7123,7127,7145,7151,7155,7159,7162,7166,7233,7247,7251,7314,7327,7331,7471,7478,7481,7507,7510,7670,7676,7680,7713,7718,7722,7725,7729,7784,7804,7807,7847,7851,7908,7915,7919,7959,7968,7972,8023,8045,8049,8053,8059,8063,8148,8169,8173,8203,8214,8218,8313,8324,8328,8452,8466,8470,8484,8488,8492,8498,8502,8603,8609,8613,8668,8675,8679,8685,8798,8813,8817,8820,8868,8885,8888,9006,9009,9014,9018,9021,9025,9114,9152,9166,9169,9173,9273,9281,9285,9457,9475,9478,9498,9503,9507,9510,9514,9517,9528,9626,9630,9645,9648,9700,9704,9710,9777,9784,9874,9878,9895,9953,9956,9960,10028,10035,10039,10042,10131,10137,10140,10221,10225,10231,10418,10428,10432,10439,10502,10509,10516,10519,10524,10528,10535,10539,10555,10650,10657,10735,10739,10742,10819,10822,10826,10833,11056,11059,11094,11098,11107,11193,11204,11208,11211,11341,11350,11354,11357,11432,11435,11440,11444,11447,11451,11464,11513,11518,11522,11539,11554,11557,11679,11704,11708,11721,11792,11795,11845,11848,11901,11904,11919,11922,11926,11930,11937,11941,11944,11974,12004,12010,12014,12078,12190,12193,12212,12215,12219,12225,12236,12239,12243,12281,12285,12320,12324,12339,12344,12348,12355,12359,12366,12371,12374,12457,12464,12468,12472,12476,12479,12695,12698,12871,12874,12877,12880,12887,12890,12897,12941,12944,12947,12950],[10,11,13],"h2",{"id":12},"一javascript-是什么脚本的执行时机与绑定方式","一、JavaScript 是什么：脚本的执行时机与绑定方式",[15,16,17],"p",{},"HTML 决定「这是什么内容」，CSS 决定「它怎么呈现」，JavaScript 决定「它怎么响应」。前两者是声明式的描述，JS 则是真正的编程语言：变量、分支、循环、函数，还自带操作页面本身的能力。它在浏览器里由 JS 引擎（V8、SpiderMonkey 等）逐行执行；同一门语言搬到服务器就是 Node.js——本教程聚焦浏览器这一侧。",[15,19,20,21,25],{},"学 JS 之前先建立一条直觉：",[22,23,24],"strong",{},"页面在浏览器里是「边解析边执行」的","。HTML 教程第一章讲过解析过程，其中顺序地读标签这一点，直接决定了脚本的行为。",[27,28,30,34],"h3",{"id":29},"script-放哪里四种写法与各自的执行时机",[31,32,33],"code",{},"\u003Cscript>"," 放哪里：四种写法与各自的执行时机",[36,37,42],"pre",{"className":38,"code":39,"language":40,"meta":41,"style":41},"language-html shiki shiki-themes github-dark","\u003C!-- ① 放在 head 里：先下载执行，再解析后面的 HTML -->\n\u003Chead>\u003Cscript src=\"\u002Fjs\u002Fapp.js\">\u003C\u002Fscript>\u003C\u002Fhead>\n\n\u003C!-- ② 放在 \u003C\u002Fbody> 之前：前面的 DOM 已经解析完 -->\n\u003Cbody>…\u003Cscript src=\"\u002Fjs\u002Fapp.js\">\u003C\u002Fscript>\u003C\u002Fbody>\n\n\u003C!-- ③ head 里加 defer：并行下载，解析完再按顺序执行 -->\n\u003Chead>\u003Cscript src=\"\u002Fjs\u002Fapp.js\" defer>\u003C\u002Fscript>\u003C\u002Fhead>\n\n\u003C!-- ④ type=\"module\"：文件成为模块，自带 defer 行为与独立作用域 -->\n\u003Chead>\u003Cscript src=\"\u002Fjs\u002Fapp.js\" type=\"module\">\u003C\u002Fscript>\u003C\u002Fhead>\n","html","",[31,43,44,53,93,100,106,135,140,146,176,181,187],{"__ignoreMap":41},[45,46,49],"span",{"class":47,"line":48},"line",1,[45,50,52],{"class":51},"sAwPA","\u003C!-- ① 放在 head 里：先下载执行，再解析后面的 HTML -->\n",[45,54,56,60,64,67,70,74,77,81,84,86,88,90],{"class":47,"line":55},2,[45,57,59],{"class":58},"s95oV","\u003C",[45,61,63],{"class":62},"s4JwU","head",[45,65,66],{"class":58},">\u003C",[45,68,69],{"class":62},"script",[45,71,73],{"class":72},"svObZ"," src",[45,75,76],{"class":58},"=",[45,78,80],{"class":79},"sU2Wk","\"\u002Fjs\u002Fapp.js\"",[45,82,83],{"class":58},">\u003C\u002F",[45,85,69],{"class":62},[45,87,83],{"class":58},[45,89,63],{"class":62},[45,91,92],{"class":58},">\n",[45,94,96],{"class":47,"line":95},3,[45,97,99],{"emptyLinePlaceholder":98},true,"\n",[45,101,103],{"class":47,"line":102},4,[45,104,105],{"class":51},"\u003C!-- ② 放在 \u003C\u002Fbody> 之前：前面的 DOM 已经解析完 -->\n",[45,107,109,111,114,117,119,121,123,125,127,129,131,133],{"class":47,"line":108},5,[45,110,59],{"class":58},[45,112,113],{"class":62},"body",[45,115,116],{"class":58},">…\u003C",[45,118,69],{"class":62},[45,120,73],{"class":72},[45,122,76],{"class":58},[45,124,80],{"class":79},[45,126,83],{"class":58},[45,128,69],{"class":62},[45,130,83],{"class":58},[45,132,113],{"class":62},[45,134,92],{"class":58},[45,136,138],{"class":47,"line":137},6,[45,139,99],{"emptyLinePlaceholder":98},[45,141,143],{"class":47,"line":142},7,[45,144,145],{"class":51},"\u003C!-- ③ head 里加 defer：并行下载，解析完再按顺序执行 -->\n",[45,147,149,151,153,155,157,159,161,163,166,168,170,172,174],{"class":47,"line":148},8,[45,150,59],{"class":58},[45,152,63],{"class":62},[45,154,66],{"class":58},[45,156,69],{"class":62},[45,158,73],{"class":72},[45,160,76],{"class":58},[45,162,80],{"class":79},[45,164,165],{"class":72}," defer",[45,167,83],{"class":58},[45,169,69],{"class":62},[45,171,83],{"class":58},[45,173,63],{"class":62},[45,175,92],{"class":58},[45,177,179],{"class":47,"line":178},9,[45,180,99],{"emptyLinePlaceholder":98},[45,182,184],{"class":47,"line":183},10,[45,185,186],{"class":51},"\u003C!-- ④ type=\"module\"：文件成为模块，自带 defer 行为与独立作用域 -->\n",[45,188,190,192,194,196,198,200,202,204,207,209,212,214,216,218,220],{"class":47,"line":189},11,[45,191,59],{"class":58},[45,193,63],{"class":62},[45,195,66],{"class":58},[45,197,69],{"class":62},[45,199,73],{"class":72},[45,201,76],{"class":58},[45,203,80],{"class":79},[45,205,206],{"class":72}," type",[45,208,76],{"class":58},[45,210,211],{"class":79},"\"module\"",[45,213,83],{"class":58},[45,215,69],{"class":62},[45,217,83],{"class":58},[45,219,63],{"class":62},[45,221,92],{"class":58},[223,224,225,244],"table",{},[226,227,228],"thead",{},[229,230,231,235,238,241],"tr",{},[232,233,234],"th",{},"写法",[232,236,237],{},"阻塞解析吗",[232,239,240],{},"何时执行",[232,242,243],{},"顺序保证",[245,246,247,265,282,300,316],"tbody",{},[229,248,249,253,259,262],{},[250,251,252],"td",{},"普通脚本（默认）",[250,254,255,258],{},[22,256,257],{},"是","，下载并执行完才继续解析",[250,260,261],{},"下载完立即执行",[250,263,264],{},"按出现顺序，但都在解析中",[229,266,267,273,276,279],{},[250,268,269,272],{},[31,270,271],{},"\u003C\u002Fbody>"," 之前",[250,274,275],{},"前面的内容已解析完，无所谓",[250,277,278],{},"立即执行",[250,280,281],{},"按出现顺序",[229,283,284,289,292,295],{},[250,285,286],{},[31,287,288],{},"defer",[250,290,291],{},"否，并行下载",[250,293,294],{},"整个文档解析完成后",[250,296,297],{},[22,298,299],{},"严格按出现顺序",[229,301,302,307,310,313],{},[250,303,304],{},[31,305,306],{},"async",[250,308,309],{},"否",[250,311,312],{},"下载完立即执行（可能打断解析）",[250,314,315],{},"不保证，谁先下完谁先跑",[229,317,318,323,325,330],{},[250,319,320],{},[31,321,322],{},"type=\"module\"",[250,324,309],{},[250,326,327,328],{},"同 ",[31,329,288],{},[250,331,332],{},"按出现顺序（第二十章展开）",[15,334,335,336,339,340,342,343,346,347,350,351,354],{},"关键结论只有一条：",[22,337,338],{},"脚本执行时，只有它前面已经解析过的 HTML 才能被拿到","。写在 ",[31,341,63],{}," 里的普通脚本去操作 ",[31,344,345],{},"#late"," 必然拿到 ",[31,348,349],{},"null","，然后报出新手遇到的第一个错误 ",[31,352,353],{},"Cannot read properties of null","。",[356,357],"demo-html",{"height":358,"src":359,"title":360},580,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F01-first-script.html","示例 01：脚本的执行时机与绑定方式",[15,362,363,365,366,369,370,373],{},[31,364,288],{}," 还藏着一个细节：脚本在 ",[22,367,368],{},"DOMContentLoaded 之前","执行，而 DOMContentLoaded 与 window 的 ",[31,371,372],{},"load"," 事件是两回事——前者在 DOM 树就绪时触发，后者要等图片、样式等全部资源加载完。想「DOM 一好就干活」，监听 DOMContentLoaded 或直接用 defer 即可，别用 load。",[27,375,377],{"id":376},"事件绑定addeventlistener-与-onclick-的差别","事件绑定：addEventListener 与 onclick 的差别",[15,379,380],{},"给元素挂行为有两种常见写法，差别值得先知道：",[36,382,386],{"className":383,"code":384,"language":385,"meta":41,"style":41},"language-js shiki shiki-themes github-dark","const btn = document.querySelector('#btn')\n\nbtn.onclick = () => console.log('第一个')\nbtn.onclick = () => console.log('第二个')   \u002F\u002F 覆盖了上一个！最终只有这条生效\n\nbtn.addEventListener('click', () => console.log('第一个'))\nbtn.addEventListener('click', () => console.log('第二个'))   \u002F\u002F 两个都会执行\n","js",[31,387,388,416,420,449,476,480,508],{"__ignoreMap":41},[45,389,390,394,398,401,404,407,410,413],{"class":47,"line":48},[45,391,393],{"class":392},"snl16","const",[45,395,397],{"class":396},"sDLfK"," btn",[45,399,400],{"class":392}," =",[45,402,403],{"class":58}," document.",[45,405,406],{"class":72},"querySelector",[45,408,409],{"class":58},"(",[45,411,412],{"class":79},"'#btn'",[45,414,415],{"class":58},")\n",[45,417,418],{"class":47,"line":55},[45,419,99],{"emptyLinePlaceholder":98},[45,421,422,425,428,430,433,436,439,442,444,447],{"class":47,"line":95},[45,423,424],{"class":58},"btn.",[45,426,427],{"class":72},"onclick",[45,429,400],{"class":392},[45,431,432],{"class":58}," () ",[45,434,435],{"class":392},"=>",[45,437,438],{"class":58}," console.",[45,440,441],{"class":72},"log",[45,443,409],{"class":58},[45,445,446],{"class":79},"'第一个'",[45,448,415],{"class":58},[45,450,451,453,455,457,459,461,463,465,467,470,473],{"class":47,"line":102},[45,452,424],{"class":58},[45,454,427],{"class":72},[45,456,400],{"class":392},[45,458,432],{"class":58},[45,460,435],{"class":392},[45,462,438],{"class":58},[45,464,441],{"class":72},[45,466,409],{"class":58},[45,468,469],{"class":79},"'第二个'",[45,471,472],{"class":58},")   ",[45,474,475],{"class":51},"\u002F\u002F 覆盖了上一个！最终只有这条生效\n",[45,477,478],{"class":47,"line":108},[45,479,99],{"emptyLinePlaceholder":98},[45,481,482,484,487,489,492,495,497,499,501,503,505],{"class":47,"line":137},[45,483,424],{"class":58},[45,485,486],{"class":72},"addEventListener",[45,488,409],{"class":58},[45,490,491],{"class":79},"'click'",[45,493,494],{"class":58},", () ",[45,496,435],{"class":392},[45,498,438],{"class":58},[45,500,441],{"class":72},[45,502,409],{"class":58},[45,504,446],{"class":79},[45,506,507],{"class":58},"))\n",[45,509,510,512,514,516,518,520,522,524,526,528,530,533],{"class":47,"line":142},[45,511,424],{"class":58},[45,513,486],{"class":72},[45,515,409],{"class":58},[45,517,491],{"class":79},[45,519,494],{"class":58},[45,521,435],{"class":392},[45,523,438],{"class":58},[45,525,441],{"class":72},[45,527,409],{"class":58},[45,529,469],{"class":79},[45,531,532],{"class":58},"))   ",[45,534,535],{"class":51},"\u002F\u002F 两个都会执行\n",[15,537,538,540,541,543,544,547,548,551],{},[31,539,427],{}," 是「属性赋值」，同名的只能存一个；",[31,542,486],{}," 是「列表登记」，可以叠多个、还能按需移除（",[31,545,546],{},"removeEventListener","，要求传同一函数引用）。还有第三种——HTML 属性内联 ",[31,549,550],{},"onclick=\"…\"","——把结构、行为写在一起，属于早期教程的写法，新代码不要再用了。事件的完整机制（冒泡、捕获、委托）在第十五章。",[27,553,555],{"id":554},"表达式语句与分号","表达式、语句与分号",[15,557,558,559,562,563,566,567,570,571,574,575,566,578,566,581,584,585,588,589,592,593,595],{},"JS 里有两类语法单位：",[22,560,561],{},"表达式","产生一个值（",[31,564,565],{},"1 + 2","、",[31,568,569],{},"user.name","、函数调用），",[22,572,573],{},"语句","是一个动作（",[31,576,577],{},"const x = 1",[31,579,580],{},"if (…) {…}",[31,582,583],{},"return","）。分号在多数情况可以省略（自动分号插入，ASI），但 ASI 的规则不直观，有两条实践建议：",[22,586,587],{},"团队内统一风格","（本站统一不写行尾分号），以及",[22,590,591],{},"不要依赖 ASI 救场","——像 ",[31,594,583],{}," 后换行这类位置，ASI 的插入点常常和你的意图相反。",[10,597,599],{"id":598},"二变量与声明const-优先let-备用","二、变量与声明：const 优先，let 备用",[15,601,602,603,617,618,620],{},"现代 JS 的选型建议只有一句：",[22,604,605,606,608,609,612,613,616],{},"默认用 ",[31,607,393],{},"，确实需要重新赋值时才把它改成 ",[31,610,611],{},"let","，",[31,614,615],{},"var"," 基本不再使用","。这不是审美偏好，",[31,619,615],{}," 有几条历史遗留规则会持续制造 bug，下面逐个看。",[27,622,623],{"id":623},"三兄弟对比",[223,625,626,646],{},[226,627,628],{},[229,629,630,633,637,641],{},[232,631,632],{},"维度",[232,634,635],{},[31,636,393],{},[232,638,639],{},[31,640,611],{},[232,642,643,645],{},[31,644,615],{},"（老）",[245,647,648,666,679,694,710],{},[229,649,650,653,659,663],{},[250,651,652],{},"作用域",[250,654,655,656],{},"块级 ",[31,657,658],{},"{}",[250,660,655,661],{},[31,662,658],{},[250,664,665],{},"函数级",[229,667,668,671,674,677],{},[250,669,670],{},"重新赋值",[250,672,673],{},"禁止（对绑定而言）",[250,675,676],{},"允许",[250,678,676],{},[229,680,681,684,687,689],{},[250,682,683],{},"重复声明",[250,685,686],{},"报错",[250,688,686],{},[250,690,691],{},[22,692,693],{},"静默覆盖",[229,695,696,699,702,704],{},[250,697,698],{},"声明前访问",[250,700,701],{},"报错（TDZ）",[250,703,701],{},[250,705,706,707],{},"得到 ",[31,708,709],{},"undefined",[229,711,712,715,718,720],{},[250,713,714],{},"提升",[250,716,717],{},"有，但在 TDZ 中",[250,719,717],{},[250,721,722,723],{},"提升且初始化为 ",[31,724,709],{},[36,726,728],{"className":383,"code":727,"language":385,"meta":41,"style":41},"const PI = 3.14\nlet count = 0\ncount = count + 1        \u002F\u002F ✓ let 可以重新赋值\n\nconst user = { name: '阿蓝' }\nuser.name = '小青'        \u002F\u002F ✓ 合法！改的是对象的属性\nuser = {}                \u002F\u002F ✗ TypeError：向常量赋值（改的是绑定本身）\n",[31,729,730,742,754,772,776,794,807],{"__ignoreMap":41},[45,731,732,734,737,739],{"class":47,"line":48},[45,733,393],{"class":392},[45,735,736],{"class":396}," PI",[45,738,400],{"class":392},[45,740,741],{"class":396}," 3.14\n",[45,743,744,746,749,751],{"class":47,"line":55},[45,745,611],{"class":392},[45,747,748],{"class":58}," count ",[45,750,76],{"class":392},[45,752,753],{"class":396}," 0\n",[45,755,756,759,761,763,766,769],{"class":47,"line":95},[45,757,758],{"class":58},"count ",[45,760,76],{"class":392},[45,762,748],{"class":58},[45,764,765],{"class":392},"+",[45,767,768],{"class":396}," 1",[45,770,771],{"class":51},"        \u002F\u002F ✓ let 可以重新赋值\n",[45,773,774],{"class":47,"line":102},[45,775,99],{"emptyLinePlaceholder":98},[45,777,778,780,783,785,788,791],{"class":47,"line":108},[45,779,393],{"class":392},[45,781,782],{"class":396}," user",[45,784,400],{"class":392},[45,786,787],{"class":58}," { name: ",[45,789,790],{"class":79},"'阿蓝'",[45,792,793],{"class":58}," }\n",[45,795,796,799,801,804],{"class":47,"line":137},[45,797,798],{"class":58},"user.name ",[45,800,76],{"class":392},[45,802,803],{"class":79}," '小青'",[45,805,806],{"class":51},"        \u002F\u002F ✓ 合法！改的是对象的属性\n",[45,808,809,812,814,817],{"class":47,"line":142},[45,810,811],{"class":58},"user ",[45,813,76],{"class":392},[45,815,816],{"class":58}," {}                ",[45,818,819],{"class":51},"\u002F\u002F ✗ TypeError：向常量赋值（改的是绑定本身）\n",[27,821,823],{"id":822},"const-锁定的是绑定不是值","const 锁定的是「绑定」，不是值",[15,825,826,827,829,830,833,834,836,837,840,841,844,845,848],{},"这是 ",[31,828,393],{}," 最容易被误解的一点：它保证",[22,831,832],{},"变量永远指向同一个值","，但完全不关心这个值内部能不能变。所以 ",[31,835,393],{}," 声明对象后照样能改属性、",[31,838,839],{},"push"," 数组。真想要「整个对象冻住」，用 ",[31,842,843],{},"Object.freeze()","——注意它是",[22,846,847],{},"浅冻结","，嵌套的对象不受影响。",[27,850,852],{"id":851},"块级作用域与-tdz","块级作用域与 TDZ",[15,854,855,857,858,860],{},[31,856,611],{}," 和 ",[31,859,393],{}," 的作用域是「最近的这一对花括号」：",[36,862,864],{"className":383,"code":863,"language":385,"meta":41,"style":41},"if (true) {\n  let inner = '块内'\n  var leak = '泄漏了'\n}\nconsole.log(inner)   \u002F\u002F ✗ ReferenceError：块外根本不存在\nconsole.log(leak)    \u002F\u002F '泄漏了'，var 是函数级作用域，从 if 里跑了出来\n",[31,865,866,880,893,906,911,924],{"__ignoreMap":41},[45,867,868,871,874,877],{"class":47,"line":48},[45,869,870],{"class":392},"if",[45,872,873],{"class":58}," (",[45,875,876],{"class":396},"true",[45,878,879],{"class":58},") {\n",[45,881,882,885,888,890],{"class":47,"line":55},[45,883,884],{"class":392},"  let",[45,886,887],{"class":58}," inner ",[45,889,76],{"class":392},[45,891,892],{"class":79}," '块内'\n",[45,894,895,898,901,903],{"class":47,"line":95},[45,896,897],{"class":392},"  var",[45,899,900],{"class":58}," leak ",[45,902,76],{"class":392},[45,904,905],{"class":79}," '泄漏了'\n",[45,907,908],{"class":47,"line":102},[45,909,910],{"class":58},"}\n",[45,912,913,916,918,921],{"class":47,"line":108},[45,914,915],{"class":58},"console.",[45,917,441],{"class":72},[45,919,920],{"class":58},"(inner)   ",[45,922,923],{"class":51},"\u002F\u002F ✗ ReferenceError：块外根本不存在\n",[45,925,926,928,930,933],{"class":47,"line":137},[45,927,915],{"class":58},[45,929,441],{"class":72},[45,931,932],{"class":58},"(leak)    ",[45,934,935],{"class":51},"\u002F\u002F '泄漏了'，var 是函数级作用域，从 if 里跑了出来\n",[15,937,938,939,941,942,944,945,947,948,951,952,955],{},"「声明前访问报错」这条叫暂时性死区（TDZ）：从进入作用域到执行到声明语句之间，变量处于不可访问状态。对比 ",[31,940,615],{}," 的「提前访问得到 ",[31,943,709],{},"」——",[31,946,709],{}," 会装作没事继续跑，",[31,949,950],{},"ReferenceError"," 则会立刻炸给你看，",[22,953,954],{},"早报错远好过带病运行","，这也是推荐 const\u002Flet 的原因之一。",[356,957],{"height":958,"src":959,"title":960},620,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F02-variables.html","示例 02：const 与 let 的行为边界",[27,962,964],{"id":963},"var-的历史包袱重复声明与循环泄漏","var 的历史包袱：重复声明与循环泄漏",[15,966,967,969],{},[31,968,615],{}," 时代最常见的两个 bug：同名变量被第二次声明悄悄覆盖（不报任何错），以及 for 循环变量泄漏到外面：",[36,971,973],{"className":383,"code":972,"language":385,"meta":41,"style":41},"for (var i = 0; i \u003C 3; i++) { \u002F* … *\u002F }\nconsole.log(i)   \u002F\u002F 3 —— i 泄漏到了循环外面！\n\nfor (let j = 0; j \u003C 3; j++) { \u002F* … *\u002F }\nconsole.log(j)   \u002F\u002F ✗ ReferenceError：let 的 j 只在循环体内\n",[31,974,975,1014,1026,1030,1063],{"__ignoreMap":41},[45,976,977,980,982,984,987,989,992,995,997,1000,1003,1006,1009,1012],{"class":47,"line":48},[45,978,979],{"class":392},"for",[45,981,873],{"class":58},[45,983,615],{"class":392},[45,985,986],{"class":58}," i ",[45,988,76],{"class":392},[45,990,991],{"class":396}," 0",[45,993,994],{"class":58},"; i ",[45,996,59],{"class":392},[45,998,999],{"class":396}," 3",[45,1001,1002],{"class":58},"; i",[45,1004,1005],{"class":392},"++",[45,1007,1008],{"class":58},") { ",[45,1010,1011],{"class":51},"\u002F* … *\u002F",[45,1013,793],{"class":58},[45,1015,1016,1018,1020,1023],{"class":47,"line":55},[45,1017,915],{"class":58},[45,1019,441],{"class":72},[45,1021,1022],{"class":58},"(i)   ",[45,1024,1025],{"class":51},"\u002F\u002F 3 —— i 泄漏到了循环外面！\n",[45,1027,1028],{"class":47,"line":95},[45,1029,99],{"emptyLinePlaceholder":98},[45,1031,1032,1034,1036,1038,1041,1043,1045,1048,1050,1052,1055,1057,1059,1061],{"class":47,"line":102},[45,1033,979],{"class":392},[45,1035,873],{"class":58},[45,1037,611],{"class":392},[45,1039,1040],{"class":58}," j ",[45,1042,76],{"class":392},[45,1044,991],{"class":396},[45,1046,1047],{"class":58},"; j ",[45,1049,59],{"class":392},[45,1051,999],{"class":396},[45,1053,1054],{"class":58},"; j",[45,1056,1005],{"class":392},[45,1058,1008],{"class":58},[45,1060,1011],{"class":51},[45,1062,793],{"class":58},[45,1064,1065,1067,1069,1072],{"class":47,"line":108},[45,1066,915],{"class":58},[45,1068,441],{"class":72},[45,1070,1071],{"class":58},"(j)   ",[45,1073,1074],{"class":51},"\u002F\u002F ✗ ReferenceError：let 的 j 只在循环体内\n",[15,1076,1077,1079,1080,1082,1083,1086],{},[31,1078,611],{}," 在 ",[31,1081,979],{}," 里还有一条更微妙的规则——",[22,1084,1085],{},"每次迭代都会创建一个新的绑定","，这让「循环里创建函数、函数记住当次的值」成为自然行为。这一点在第七章讲闭包时会看到它的完整价值。",[27,1088,1089],{"id":1089},"命名规范",[15,1091,1092],{},"一条好名字省下十行注释。约定速查：",[1094,1095,1096,1111,1123,1135,1154,1173],"ul",{},[1097,1098,1099,1100,1103,1104,566,1107,1110],"li",{},"变量与函数用 ",[22,1101,1102],{},"camelCase","：",[31,1105,1106],{},"userName",[31,1108,1109],{},"fetchList","；",[1097,1112,1113,1114,1103,1117,566,1120,1110],{},"真正常量用 ",[22,1115,1116],{},"UPPER_SNAKE_CASE",[31,1118,1119],{},"MAX_RETRY",[31,1121,1122],{},"API_BASE",[1097,1124,1125,1126,566,1129,566,1132,1110],{},"布尔值带前缀：",[31,1127,1128],{},"isReady",[31,1130,1131],{},"hasPermission",[31,1133,1134],{},"canSubmit",[1097,1136,1137,1138,566,1141,612,1144,1147,1148,566,1151,1153],{},"合法字符：字母、数字、",[31,1139,1140],{},"$",[31,1142,1143],{},"_",[22,1145,1146],{},"不能数字开头","，不能是保留字（",[31,1149,1150],{},"class",[31,1152,583],{},"…）；中文标识符合法但别用；",[1097,1155,1156,1157,1160,1161,612,1163,566,1166,566,1169,1172],{},"名字要有语义：",[31,1158,1159],{},"price"," 好于 ",[31,1162,15],{},[31,1164,1165],{},"list2",[31,1167,1168],{},"temp",[31,1170,1171],{},"data1"," 这种名字说明还没想清楚它是什么；",[1097,1174,1175,1176,1179],{},"一行只声明一个变量，别写 ",[31,1177,1178],{},"let a = 1, b = 2"," 的逗号链——日后增删都容易漏。",[10,1181,1183],{"id":1182},"三数据类型原始值对象与类型判断","三、数据类型：原始值、对象与类型判断",[15,1185,1186,1187,1190,1191,1194],{},"JS 的值分两大家族：",[22,1188,1189],{},"原始值","（primitive，不可再分）与",[22,1192,1193],{},"对象","（object，属性的集合）。理解这条分界，等于拿到后面所有「为什么它没变」「为什么它跟着变了」类问题的钥匙。",[27,1196,1198],{"id":1197},"八种类型与-typeof","八种类型与 typeof",[15,1200,1201,1202,566,1205,566,1208,566,1211,566,1213,566,1215,566,1218,1221,1222,1225],{},"原始值有七种：",[31,1203,1204],{},"string",[31,1206,1207],{},"number",[31,1209,1210],{},"boolean",[31,1212,709],{},[31,1214,349],{},[31,1216,1217],{},"symbol",[31,1219,1220],{},"bigint","；加上 ",[31,1223,1224],{},"object"," 一族（含数组、函数、日期等）就是全部。",[223,1227,1228,1244],{},[226,1229,1230],{},[229,1231,1232,1235,1241],{},[232,1233,1234],{},"值",[232,1236,1237,1240],{},[31,1238,1239],{},"typeof"," 结果",[232,1242,1243],{},"备注",[245,1245,1246,1260,1282,1295,1309,1329,1344,1359,1378],{},[229,1247,1248,1253,1258],{},[250,1249,1250],{},[31,1251,1252],{},"'abc'",[250,1254,1255],{},[31,1256,1257],{},"'string'",[250,1259],{},[229,1261,1262,1271,1276],{},[250,1263,1264,1267,1268],{},[31,1265,1266],{},"42"," \u002F ",[31,1269,1270],{},"3.14",[250,1272,1273],{},[31,1274,1275],{},"'number'",[250,1277,1278,1281],{},[22,1279,1280],{},"只有一种数字类型","，不区分整数浮点",[229,1283,1284,1288,1293],{},[250,1285,1286],{},[31,1287,876],{},[250,1289,1290],{},[31,1291,1292],{},"'boolean'",[250,1294],{},[229,1296,1297,1301,1306],{},[250,1298,1299],{},[31,1300,709],{},[250,1302,1303],{},[31,1304,1305],{},"'undefined'",[250,1307,1308],{},"「未定义」，变量声明未赋值的默认值",[229,1310,1311,1315,1320],{},[250,1312,1313],{},[31,1314,349],{},[250,1316,1317],{},[31,1318,1319],{},"'object'",[250,1321,1322,1325,1326],{},[22,1323,1324],{},"历史 bug","，官方承认但为了兼容不改；判空用 ",[31,1327,1328],{},"=== null",[229,1330,1331,1336,1341],{},[250,1332,1333],{},[31,1334,1335],{},"Symbol('id')",[250,1337,1338],{},[31,1339,1340],{},"'symbol'",[250,1342,1343],{},"独一无二的标记",[229,1345,1346,1351,1356],{},[250,1347,1348],{},[31,1349,1350],{},"10n",[250,1352,1353],{},[31,1354,1355],{},"'bigint'",[250,1357,1358],{},"超出安全范围的大整数",[229,1360,1361,1368,1372],{},[250,1362,1363,1267,1365],{},[31,1364,658],{},[31,1366,1367],{},"[]",[250,1369,1370],{},[31,1371,1319],{},[250,1373,1374,1375],{},"数组也是 object，判定要用 ",[31,1376,1377],{},"Array.isArray()",[229,1379,1380,1385,1390],{},[250,1381,1382],{},[31,1383,1384],{},"function () {}",[250,1386,1387],{},[31,1388,1389],{},"'function'",[250,1391,1392],{},"可调用对象，但归类上仍是对象",[356,1394],{"height":1395,"src":1396,"title":1397},640,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F03-types.html","示例 03：类型判断、假值与转换",[27,1399,1401],{"id":1400},"原始值传拷贝对象传引用","原始值传「拷贝」，对象传「引用」",[36,1403,1405],{"className":383,"code":1404,"language":385,"meta":41,"style":41},"let a = 1\nlet b = a\nb = 2\nconsole.log(a)   \u002F\u002F 1 —— 原始值各存各的\n\nconst x = { n: 1 }\nconst y = x\ny.n = 2\nconsole.log(x.n) \u002F\u002F 2 —— 两个名字指向同一个对象！\n\nfunction bump(obj) { obj.n++ }   \u002F\u002F 参数也是引用传递\nbump(x)\nconsole.log(x.n) \u002F\u002F 3 —— 函数里改的就是外面那个对象\n",[31,1406,1407,1419,1431,1441,1453,1457,1474,1486,1495,1507,1511,1536,1545],{"__ignoreMap":41},[45,1408,1409,1411,1414,1416],{"class":47,"line":48},[45,1410,611],{"class":392},[45,1412,1413],{"class":58}," a ",[45,1415,76],{"class":392},[45,1417,1418],{"class":396}," 1\n",[45,1420,1421,1423,1426,1428],{"class":47,"line":55},[45,1422,611],{"class":392},[45,1424,1425],{"class":58}," b ",[45,1427,76],{"class":392},[45,1429,1430],{"class":58}," a\n",[45,1432,1433,1436,1438],{"class":47,"line":95},[45,1434,1435],{"class":58},"b ",[45,1437,76],{"class":392},[45,1439,1440],{"class":396}," 2\n",[45,1442,1443,1445,1447,1450],{"class":47,"line":102},[45,1444,915],{"class":58},[45,1446,441],{"class":72},[45,1448,1449],{"class":58},"(a)   ",[45,1451,1452],{"class":51},"\u002F\u002F 1 —— 原始值各存各的\n",[45,1454,1455],{"class":47,"line":108},[45,1456,99],{"emptyLinePlaceholder":98},[45,1458,1459,1461,1464,1466,1469,1472],{"class":47,"line":137},[45,1460,393],{"class":392},[45,1462,1463],{"class":396}," x",[45,1465,400],{"class":392},[45,1467,1468],{"class":58}," { n: ",[45,1470,1471],{"class":396},"1",[45,1473,793],{"class":58},[45,1475,1476,1478,1481,1483],{"class":47,"line":142},[45,1477,393],{"class":392},[45,1479,1480],{"class":396}," y",[45,1482,400],{"class":392},[45,1484,1485],{"class":58}," x\n",[45,1487,1488,1491,1493],{"class":47,"line":148},[45,1489,1490],{"class":58},"y.n ",[45,1492,76],{"class":392},[45,1494,1440],{"class":396},[45,1496,1497,1499,1501,1504],{"class":47,"line":178},[45,1498,915],{"class":58},[45,1500,441],{"class":72},[45,1502,1503],{"class":58},"(x.n) ",[45,1505,1506],{"class":51},"\u002F\u002F 2 —— 两个名字指向同一个对象！\n",[45,1508,1509],{"class":47,"line":183},[45,1510,99],{"emptyLinePlaceholder":98},[45,1512,1513,1516,1519,1521,1525,1528,1530,1533],{"class":47,"line":189},[45,1514,1515],{"class":392},"function",[45,1517,1518],{"class":72}," bump",[45,1520,409],{"class":58},[45,1522,1524],{"class":1523},"s9osk","obj",[45,1526,1527],{"class":58},") { obj.n",[45,1529,1005],{"class":392},[45,1531,1532],{"class":58}," }   ",[45,1534,1535],{"class":51},"\u002F\u002F 参数也是引用传递\n",[45,1537,1539,1542],{"class":47,"line":1538},12,[45,1540,1541],{"class":72},"bump",[45,1543,1544],{"class":58},"(x)\n",[45,1546,1548,1550,1552,1554],{"class":47,"line":1547},13,[45,1549,915],{"class":58},[45,1551,441],{"class":72},[45,1553,1503],{"class":58},[45,1555,1556],{"class":51},"\u002F\u002F 3 —— 函数里改的就是外面那个对象\n",[15,1558,1559,1560,1563,1564,1567,1568,1571,1572,1575],{},"所以「对象比较」不能靠 ",[31,1561,1562],{},"==="," 判断内容：",[31,1565,1566],{},"{n:1} === {n:1}"," 永远是 ",[31,1569,1570],{},"false","，因为比的是「是不是同一个对象」。判断内容相等需要逐字段比较，或用 ",[31,1573,1574],{},"JSON.stringify"," 做近似的深比较（有顺序敏感等局限）。",[27,1577,1578],{"id":1578},"数字的两个经典雷区",[36,1580,1582],{"className":383,"code":1581,"language":385,"meta":41,"style":41},"0.1 + 0.2 === 0.3          \u002F\u002F false！IEEE 754 双精度浮点的固有限制\nMath.abs(0.1 + 0.2 - 0.3) \u003C Number.EPSILON   \u002F\u002F 浮点比较的正确姿势\n\nNaN === NaN                \u002F\u002F false！NaN 不等于任何值，包括自己\nNumber.isNaN(NaN)          \u002F\u002F true，判断 NaN 要用它\nisNaN('12px')              \u002F\u002F true —— 老 isNaN 会先做类型转换，基本别用\n",[31,1583,1584,1604,1636,1640,1653,1671],{"__ignoreMap":41},[45,1585,1586,1589,1592,1595,1598,1601],{"class":47,"line":48},[45,1587,1588],{"class":396},"0.1",[45,1590,1591],{"class":392}," +",[45,1593,1594],{"class":396}," 0.2",[45,1596,1597],{"class":392}," ===",[45,1599,1600],{"class":396}," 0.3",[45,1602,1603],{"class":51},"          \u002F\u002F false！IEEE 754 双精度浮点的固有限制\n",[45,1605,1606,1609,1612,1614,1616,1618,1620,1623,1625,1628,1630,1633],{"class":47,"line":55},[45,1607,1608],{"class":58},"Math.",[45,1610,1611],{"class":72},"abs",[45,1613,409],{"class":58},[45,1615,1588],{"class":396},[45,1617,1591],{"class":392},[45,1619,1594],{"class":396},[45,1621,1622],{"class":392}," -",[45,1624,1600],{"class":396},[45,1626,1627],{"class":58},") ",[45,1629,59],{"class":392},[45,1631,1632],{"class":58}," Number.EPSILON   ",[45,1634,1635],{"class":51},"\u002F\u002F 浮点比较的正确姿势\n",[45,1637,1638],{"class":47,"line":95},[45,1639,99],{"emptyLinePlaceholder":98},[45,1641,1642,1645,1647,1650],{"class":47,"line":102},[45,1643,1644],{"class":396},"NaN",[45,1646,1597],{"class":392},[45,1648,1649],{"class":396}," NaN",[45,1651,1652],{"class":51},"                \u002F\u002F false！NaN 不等于任何值，包括自己\n",[45,1654,1655,1658,1661,1663,1665,1668],{"class":47,"line":108},[45,1656,1657],{"class":58},"Number.",[45,1659,1660],{"class":72},"isNaN",[45,1662,409],{"class":58},[45,1664,1644],{"class":396},[45,1666,1667],{"class":58},")          ",[45,1669,1670],{"class":51},"\u002F\u002F true，判断 NaN 要用它\n",[45,1672,1673,1675,1677,1680,1683],{"class":47,"line":137},[45,1674,1660],{"class":72},[45,1676,409],{"class":58},[45,1678,1679],{"class":79},"'12px'",[45,1681,1682],{"class":58},")              ",[45,1684,1685],{"class":51},"\u002F\u002F true —— 老 isNaN 会先做类型转换，基本别用\n",[15,1687,1688],{},"金额场景不要用浮点数直接算账（乘 100 转整数分，或交给后端），这是行业铁律。",[27,1690,1691],{"id":1691},"假值只有八个位置",[15,1693,1694,1696,1697,1103],{},[31,1695,870],{}," 里当条件用时，只有下面这些值算「假」，",[22,1698,1699],{},"其余全是真",[36,1701,1703],{"className":383,"code":1702,"language":385,"meta":41,"style":41},"false   0   -0   0n   ''   null   undefined   NaN\n",[31,1704,1705],{"__ignoreMap":41},[45,1706,1707,1709,1712,1715,1718,1720,1723,1726,1729,1732],{"class":47,"line":48},[45,1708,1570],{"class":396},[45,1710,1711],{"class":396},"   0",[45,1713,1714],{"class":392},"   -",[45,1716,1717],{"class":396},"0",[45,1719,1711],{"class":396},[45,1721,1722],{"class":392},"n",[45,1724,1725],{"class":79},"   ''",[45,1727,1728],{"class":396},"   null",[45,1730,1731],{"class":396},"   undefined",[45,1733,1734],{"class":396},"   NaN\n",[15,1736,1737,1738,566,1741,1744,1745,1748,1749,1751,1752,1754,1755,1758,1759,354],{},"最容易踩的是「长得像假、其实是真」的成员：字符串 ",[31,1739,1740],{},"'0'",[31,1742,1743],{},"'false'","、空格串 ",[31,1746,1747],{},"' '","、空数组 ",[31,1750,1367],{},"、空对象 ",[31,1753,658],{},"——统统为真。所以校验「用户填没填」应该判 ",[31,1756,1757],{},"value === ''","，而不是 ",[31,1760,1761],{},"if (value)",[10,1763,1765],{"id":1764},"四运算符相等短路与可选链","四、运算符：相等、短路与可选链",[15,1767,1768],{},"运算符大多和别的语言相通，这一章只聚焦 JS 特有的、写错了最难发现的三组符号。",[27,1770,1772,1775,1776,1778],{"id":1771},"会做类型转换-不会",[31,1773,1774],{},"=="," 会做类型转换，",[31,1777,1562],{}," 不会",[36,1780,1782],{"className":383,"code":1781,"language":385,"meta":41,"style":41},"1 == '1'        \u002F\u002F true —— 字符串被转成数字\n1 === '1'       \u002F\u002F false —— 类型不同\nnull == undefined      \u002F\u002F true —— 特例，唯一值得保留 == 的用法\n[] == false            \u002F\u002F true —— 空数组转成 '' 再转 0……\n'0' == false           \u002F\u002F true —— 新手最常踩的「字符串 0」\n",[31,1783,1784,1797,1808,1820,1833],{"__ignoreMap":41},[45,1785,1786,1788,1791,1794],{"class":47,"line":48},[45,1787,1471],{"class":396},[45,1789,1790],{"class":392}," ==",[45,1792,1793],{"class":79}," '1'",[45,1795,1796],{"class":51},"        \u002F\u002F true —— 字符串被转成数字\n",[45,1798,1799,1801,1803,1805],{"class":47,"line":55},[45,1800,1471],{"class":396},[45,1802,1597],{"class":392},[45,1804,1793],{"class":79},[45,1806,1807],{"class":51},"       \u002F\u002F false —— 类型不同\n",[45,1809,1810,1812,1814,1817],{"class":47,"line":95},[45,1811,349],{"class":396},[45,1813,1790],{"class":392},[45,1815,1816],{"class":396}," undefined",[45,1818,1819],{"class":51},"      \u002F\u002F true —— 特例，唯一值得保留 == 的用法\n",[45,1821,1822,1825,1827,1830],{"class":47,"line":102},[45,1823,1824],{"class":58},"[] ",[45,1826,1774],{"class":392},[45,1828,1829],{"class":396}," false",[45,1831,1832],{"class":51},"            \u002F\u002F true —— 空数组转成 '' 再转 0……\n",[45,1834,1835,1837,1839,1841],{"class":47,"line":108},[45,1836,1740],{"class":79},[45,1838,1790],{"class":392},[45,1840,1829],{"class":396},[45,1842,1843],{"class":51},"           \u002F\u002F true —— 新手最常踩的「字符串 0」\n",[15,1845,1846,1847,1856,1857,1860,1861,857,1863,1865,1866,1869],{},"规则只有一条：",[22,1848,1849,1850,1852,1853],{},"日常一律用 ",[31,1851,1562],{}," 与 ",[31,1854,1855],{},"!==","；唯一例外是 ",[31,1858,1859],{},"x == null","，它同时命中 ",[31,1862,349],{},[31,1864,709],{},"，读作「x 是空的吗」，写起来比 ",[31,1867,1868],{},"x === null || x === undefined"," 干净。",[27,1871,1873],{"id":1872},"逻辑运算的返回值是操作数短路是特性","逻辑运算的返回值是「操作数」，短路是特性",[15,1875,1876,857,1879,1882,1883,1886],{},[31,1877,1878],{},"&&",[31,1880,1881],{},"||"," 不返回布尔值，而是返回",[22,1884,1885],{},"决定结果的那一侧操作数","；并且左侧一旦能定结果，右侧就不执行（短路）：",[36,1888,1890],{"className":383,"code":1889,"language":385,"meta":41,"style":41},"const name = input || '游客'      \u002F\u002F input 为假值时用默认值\nisReady && start()                \u002F\u002F 常用简写：条件成立才执行\nconst count = data ?? 0           \u002F\u002F ?? 只在 null \u002F undefined 时兜底\n",[31,1891,1892,1912,1928],{"__ignoreMap":41},[45,1893,1894,1896,1899,1901,1904,1906,1909],{"class":47,"line":48},[45,1895,393],{"class":392},[45,1897,1898],{"class":396}," name",[45,1900,400],{"class":392},[45,1902,1903],{"class":58}," input ",[45,1905,1881],{"class":392},[45,1907,1908],{"class":79}," '游客'",[45,1910,1911],{"class":51},"      \u002F\u002F input 为假值时用默认值\n",[45,1913,1914,1917,1919,1922,1925],{"class":47,"line":55},[45,1915,1916],{"class":58},"isReady ",[45,1918,1878],{"class":392},[45,1920,1921],{"class":72}," start",[45,1923,1924],{"class":58},"()                ",[45,1926,1927],{"class":51},"\u002F\u002F 常用简写：条件成立才执行\n",[45,1929,1930,1932,1935,1937,1940,1943,1945],{"class":47,"line":95},[45,1931,393],{"class":392},[45,1933,1934],{"class":396}," count",[45,1936,400],{"class":392},[45,1938,1939],{"class":58}," data ",[45,1941,1942],{"class":392},"??",[45,1944,991],{"class":396},[45,1946,1947],{"class":51},"           \u002F\u002F ?? 只在 null \u002F undefined 时兜底\n",[15,1949,1950,1852,1952,1954,1955,1957,1958,612,1961,1963,1964,1267,1966,1968,1969,1971,1972,1975,1976,1103],{},[31,1951,1881],{},[31,1953,1942],{}," 的区别是高频考点：",[31,1956,1881],{}," 把 ",[22,1959,1960],{},"0 和空串也当成「没有值」",[31,1962,1942],{}," 只认 ",[31,1965,349],{},[31,1967,709],{},"。想保留用户填的 ",[31,1970,1717],{}," 或 ",[31,1973,1974],{},"''"," 时，必须用 ",[31,1977,1942],{},[36,1979,1981],{"className":383,"code":1980,"language":385,"meta":41,"style":41},"0 || 100   \u002F\u002F 100  ✗ 0 是假值，被兜底了\n0 ?? 100   \u002F\u002F 0    ✓ 0 是合法数字，保留\n",[31,1982,1983,1996],{"__ignoreMap":41},[45,1984,1985,1987,1990,1993],{"class":47,"line":48},[45,1986,1717],{"class":396},[45,1988,1989],{"class":392}," ||",[45,1991,1992],{"class":396}," 100",[45,1994,1995],{"class":51},"   \u002F\u002F 100  ✗ 0 是假值，被兜底了\n",[45,1997,1998,2000,2003,2005],{"class":47,"line":55},[45,1999,1717],{"class":396},[45,2001,2002],{"class":392}," ??",[45,2004,1992],{"class":396},[45,2006,2007],{"class":51},"   \u002F\u002F 0    ✓ 0 是合法数字，保留\n",[27,2009,2011,2012,2015],{"id":2010},"可选链-深层访问不再逐层判断","可选链 ",[31,2013,2014],{},"?.","：深层访问不再逐层判断",[36,2017,2019],{"className":383,"code":2018,"language":385,"meta":41,"style":41},"user?.profile?.name        \u002F\u002F 中途任何一环是 null\u002Fundefined 就返回 undefined，不报错\nuser.greet?.()             \u002F\u002F 方法存在才调用\nuser.tags?.[0]             \u002F\u002F 数组下标同样适用\n",[31,2020,2021,2029,2043],{"__ignoreMap":41},[45,2022,2023,2026],{"class":47,"line":48},[45,2024,2025],{"class":58},"user?.profile?.name        ",[45,2027,2028],{"class":51},"\u002F\u002F 中途任何一环是 null\u002Fundefined 就返回 undefined，不报错\n",[45,2030,2031,2034,2037,2040],{"class":47,"line":55},[45,2032,2033],{"class":58},"user.",[45,2035,2036],{"class":72},"greet",[45,2038,2039],{"class":58},"?.()             ",[45,2041,2042],{"class":51},"\u002F\u002F 方法存在才调用\n",[45,2044,2045,2048,2050,2053],{"class":47,"line":95},[45,2046,2047],{"class":58},"user.tags?.[",[45,2049,1717],{"class":396},[45,2051,2052],{"class":58},"]             ",[45,2054,2055],{"class":51},"\u002F\u002F 数组下标同样适用\n",[356,2057],{"height":1395,"src":2058,"title":2059},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F04-operators.html","示例 04：相等、短路与可选链",[15,2061,2062,2063,1267,2066,1267,2069,2072,2073,2075],{},"配套的逻辑赋值运算符 ",[31,2064,2065],{},"??=",[31,2067,2068],{},"||=",[31,2070,2071],{},"&&="," 用来「只在满足条件时写入」：补配置默认值用 ",[31,2074,2065],{}," 最精准——已设的值（哪怕是 0 或空串）不会被覆盖。",[27,2077,2078],{"id":2078},"两个小符号",[15,2080,2081,2083,2084,2087,2088,566,2091,566,2094,2097,2098,2101,2102,2105,2106,2109],{},[31,2082,765],{}," 既是加法也是字符串拼接：只要一侧是字符串，另一侧就被转成字符串（",[31,2085,2086],{},"'1' + 1 === '11'","）；而 ",[31,2089,2090],{},"-",[31,2092,2093],{},"*",[31,2095,2096],{},"\u002F"," 只做数学，会把字符串",[22,2099,2100],{},"强制转成数字","再算（",[31,2103,2104],{},"'6' \u002F '2' === 3","）。字符串和数字混算前，先显式用 ",[31,2107,2108],{},"Number()"," 转换，比依赖隐式规则可靠。",[15,2111,2112,2114,2115,2118,2119,2122,2123,2126,2127,2130],{},[31,2113,1005],{}," 有前缀后缀之分（",[31,2116,2117],{},"++i"," 先加再用、",[31,2120,2121],{},"i++"," 先用再加），但",[22,2124,2125],{},"不要在表达式里使用自增","——",[31,2128,2129],{},"arr[i++] = arr[i]"," 这种代码只有作者读得懂，拆成两行。",[10,2132,2134],{"id":2133},"五流程控制分支与循环","五、流程控制：分支与循环",[27,2136,2138],{"id":2137},"if-else-与提前-return","if \u002F else 与提前 return",[15,2140,2141,2143,2144,2147],{},[31,2142,870],{}," 的语法不必多讲，值得刻意练习的是",[22,2145,2146],{},"守卫子句","写法：把「不满足就退出」的条件提前处理，主干逻辑就永远不会缩进三层以上。",[36,2149,2151],{"className":383,"code":2150,"language":385,"meta":41,"style":41},"function getDiscount(user) {\n  if (!user) return 0           \u002F\u002F 守卫：没有用户，直接返回\n  if (!user.isVip) return 0     \u002F\u002F 守卫：不是会员，直接返回\n  return user.years > 3 ? 0.8 : 0.9   \u002F\u002F 主干清晰，只有一行\n}\n",[31,2152,2153,2167,2187,2205,2233],{"__ignoreMap":41},[45,2154,2155,2157,2160,2162,2165],{"class":47,"line":48},[45,2156,1515],{"class":392},[45,2158,2159],{"class":72}," getDiscount",[45,2161,409],{"class":58},[45,2163,2164],{"class":1523},"user",[45,2166,879],{"class":58},[45,2168,2169,2172,2174,2177,2180,2182,2184],{"class":47,"line":55},[45,2170,2171],{"class":392},"  if",[45,2173,873],{"class":58},[45,2175,2176],{"class":392},"!",[45,2178,2179],{"class":58},"user) ",[45,2181,583],{"class":392},[45,2183,991],{"class":396},[45,2185,2186],{"class":51},"           \u002F\u002F 守卫：没有用户，直接返回\n",[45,2188,2189,2191,2193,2195,2198,2200,2202],{"class":47,"line":95},[45,2190,2171],{"class":392},[45,2192,873],{"class":58},[45,2194,2176],{"class":392},[45,2196,2197],{"class":58},"user.isVip) ",[45,2199,583],{"class":392},[45,2201,991],{"class":396},[45,2203,2204],{"class":51},"     \u002F\u002F 守卫：不是会员，直接返回\n",[45,2206,2207,2210,2213,2216,2218,2221,2224,2227,2230],{"class":47,"line":102},[45,2208,2209],{"class":392},"  return",[45,2211,2212],{"class":58}," user.years ",[45,2214,2215],{"class":392},">",[45,2217,999],{"class":396},[45,2219,2220],{"class":392}," ?",[45,2222,2223],{"class":396}," 0.8",[45,2225,2226],{"class":392}," :",[45,2228,2229],{"class":396}," 0.9",[45,2231,2232],{"class":51},"   \u002F\u002F 主干清晰，只有一行\n",[45,2234,2235],{"class":47,"line":108},[45,2236,910],{"class":58},[15,2238,2239,2240,2243,2244,2246],{},"三元表达式 ",[31,2241,2242],{},"条件 ? 值1 : 值2"," 适合「二选一取值」的场合，不要嵌套超过一层——两层以上就该改写成 ",[31,2245,870],{}," 或映射表。",[27,2248,2250,2251,2253],{"id":2249},"switch-比较与-break","switch：",[31,2252,1562],{}," 比较与 break",[15,2255,2256,2259,2260,2265,2266,2269,2270,2273,2274,2280],{},[31,2257,2258],{},"switch"," 适合「一个值对多种情况」的分支，两条规则：",[22,2261,2262,2263],{},"比较用的是严格相等 ",[31,2264,1562],{},"（字符串 ",[31,2267,2268],{},"'5'"," 不会命中数字 ",[31,2271,2272],{},"5","）；",[22,2275,2276,2277],{},"每个 case 结尾要 ",[31,2278,2279],{},"break","，否则会从命中的位置一路「穿透」执行到底。穿透有时是特性——多个值共用一段逻辑时让 case 连续排列即可。",[356,2282],{"height":2283,"src":2284,"title":2285},680,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F05-control.html","示例 05：分支与循环，用 FizzBuzz 串起来",[27,2287,2288],{"id":2288},"三种循环与两种遍历",[36,2290,2292],{"className":383,"code":2291,"language":385,"meta":41,"style":41},"for (let i = 0; i \u003C arr.length; i++) { }   \u002F\u002F 经典 for：需要下标或跳步时用\nwhile (条件) { }                            \u002F\u002F 先判断，可能一次都不执行\ndo { } while (条件)                        \u002F\u002F 先执行一遍，再判断\n\nfor (const item of arr) { }                \u002F\u002F ✓ 遍历数组\u002F字符串\u002FMap\u002FSet 用 for...of，拿值\nfor (const key in obj) { }                 \u002F\u002F 遍历「键」；对象用 in，数组千万别用（见下）\n",[31,2293,2294,2328,2339,2355,2359,2379],{"__ignoreMap":41},[45,2295,2296,2298,2300,2302,2304,2306,2308,2310,2312,2315,2318,2320,2322,2325],{"class":47,"line":48},[45,2297,979],{"class":392},[45,2299,873],{"class":58},[45,2301,611],{"class":392},[45,2303,986],{"class":58},[45,2305,76],{"class":392},[45,2307,991],{"class":396},[45,2309,994],{"class":58},[45,2311,59],{"class":392},[45,2313,2314],{"class":58}," arr.",[45,2316,2317],{"class":396},"length",[45,2319,1002],{"class":58},[45,2321,1005],{"class":392},[45,2323,2324],{"class":58},") { }   ",[45,2326,2327],{"class":51},"\u002F\u002F 经典 for：需要下标或跳步时用\n",[45,2329,2330,2333,2336],{"class":47,"line":55},[45,2331,2332],{"class":392},"while",[45,2334,2335],{"class":58}," (条件) { }                            ",[45,2337,2338],{"class":51},"\u002F\u002F 先判断，可能一次都不执行\n",[45,2340,2341,2344,2347,2349,2352],{"class":47,"line":95},[45,2342,2343],{"class":392},"do",[45,2345,2346],{"class":58}," { } ",[45,2348,2332],{"class":392},[45,2350,2351],{"class":58}," (条件)                        ",[45,2353,2354],{"class":51},"\u002F\u002F 先执行一遍，再判断\n",[45,2356,2357],{"class":47,"line":102},[45,2358,99],{"emptyLinePlaceholder":98},[45,2360,2361,2363,2365,2367,2370,2373,2376],{"class":47,"line":108},[45,2362,979],{"class":392},[45,2364,873],{"class":58},[45,2366,393],{"class":392},[45,2368,2369],{"class":396}," item",[45,2371,2372],{"class":392}," of",[45,2374,2375],{"class":58}," arr) { }                ",[45,2377,2378],{"class":51},"\u002F\u002F ✓ 遍历数组\u002F字符串\u002FMap\u002FSet 用 for...of，拿值\n",[45,2380,2381,2383,2385,2387,2390,2393,2396],{"class":47,"line":137},[45,2382,979],{"class":392},[45,2384,873],{"class":58},[45,2386,393],{"class":392},[45,2388,2389],{"class":396}," key",[45,2391,2392],{"class":392}," in",[45,2394,2395],{"class":58}," obj) { }                 ",[45,2397,2398],{"class":51},"\u002F\u002F 遍历「键」；对象用 in，数组千万别用（见下）\n",[15,2400,2401,2404,2405,2408,2409,566,2411,2414,2415,2418,2419,1971,2421,1103],{},[31,2402,2403],{},"for...in"," 用在数组上会给你",[22,2406,2407],{},"字符串下标","（",[31,2410,1740],{},[31,2412,2413],{},"'1'","）而不是值，还会顺带翻出原型链上的可枚举属性，是教科书级的错误写法。数组和字符串用 ",[31,2416,2417],{},"for...of","（或数组方法，第九章展开），普通对象用 ",[31,2420,2403],{},[31,2422,2423],{},"Object.entries()",[36,2425,2427],{"className":383,"code":2426,"language":385,"meta":41,"style":41},"const user = { name: '阿蓝', age: 28 }\nfor (const [key, value] of Object.entries(user)) {\n  console.log(key, value)   \u002F\u002F 'name' '阿蓝' \u002F 'age' 28\n}\n",[31,2428,2429,2449,2484,2497],{"__ignoreMap":41},[45,2430,2431,2433,2435,2437,2439,2441,2444,2447],{"class":47,"line":48},[45,2432,393],{"class":392},[45,2434,782],{"class":396},[45,2436,400],{"class":392},[45,2438,787],{"class":58},[45,2440,790],{"class":79},[45,2442,2443],{"class":58},", age: ",[45,2445,2446],{"class":396},"28",[45,2448,793],{"class":58},[45,2450,2451,2453,2455,2457,2460,2463,2466,2469,2472,2475,2478,2481],{"class":47,"line":55},[45,2452,979],{"class":392},[45,2454,873],{"class":58},[45,2456,393],{"class":392},[45,2458,2459],{"class":58}," [",[45,2461,2462],{"class":396},"key",[45,2464,2465],{"class":58},", ",[45,2467,2468],{"class":396},"value",[45,2470,2471],{"class":58},"] ",[45,2473,2474],{"class":392},"of",[45,2476,2477],{"class":58}," Object.",[45,2479,2480],{"class":72},"entries",[45,2482,2483],{"class":58},"(user)) {\n",[45,2485,2486,2489,2491,2494],{"class":47,"line":95},[45,2487,2488],{"class":58},"  console.",[45,2490,441],{"class":72},[45,2492,2493],{"class":58},"(key, value)   ",[45,2495,2496],{"class":51},"\u002F\u002F 'name' '阿蓝' \u002F 'age' 28\n",[45,2498,2499],{"class":47,"line":102},[45,2500,910],{"class":58},[27,2502,2504,1852,2506],{"id":2503},"break-与-continue",[31,2505,2279],{},[31,2507,2508],{},"continue",[1094,2510,2511,2520],{},[1097,2512,2513,2515,2516,2519],{},[31,2514,2279],{},"：立即跳出",[22,2517,2518],{},"整个","循环（找到就收工的场景）；",[1097,2521,2522,2524],{},[31,2523,2508],{},"：跳过本轮剩余部分，直接进入下一轮（过滤场景）。",[36,2526,2528],{"className":383,"code":2527,"language":385,"meta":41,"style":41},"for (let i = 1; i \u003C= 60; i++) {\n  if (i % 3 === 0 && i % 5 === 1) { found = i; break }   \u002F\u002F 找到即停，不空转\n}\nfor (let i = 1; i \u003C= 20; i++) {\n  if (i % 2 !== 0) continue   \u002F\u002F 奇数跳过\n  evens.push(i)\n}\n",[31,2529,2530,2558,2603,2607,2634,2657,2667],{"__ignoreMap":41},[45,2531,2532,2534,2536,2538,2540,2542,2544,2546,2549,2552,2554,2556],{"class":47,"line":48},[45,2533,979],{"class":392},[45,2535,873],{"class":58},[45,2537,611],{"class":392},[45,2539,986],{"class":58},[45,2541,76],{"class":392},[45,2543,768],{"class":396},[45,2545,994],{"class":58},[45,2547,2548],{"class":392},"\u003C=",[45,2550,2551],{"class":396}," 60",[45,2553,1002],{"class":58},[45,2555,1005],{"class":392},[45,2557,879],{"class":58},[45,2559,2560,2562,2565,2568,2570,2572,2574,2577,2579,2581,2584,2586,2588,2591,2593,2596,2598,2600],{"class":47,"line":55},[45,2561,2171],{"class":392},[45,2563,2564],{"class":58}," (i ",[45,2566,2567],{"class":392},"%",[45,2569,999],{"class":396},[45,2571,1597],{"class":392},[45,2573,991],{"class":396},[45,2575,2576],{"class":392}," &&",[45,2578,986],{"class":58},[45,2580,2567],{"class":392},[45,2582,2583],{"class":396}," 5",[45,2585,1597],{"class":392},[45,2587,768],{"class":396},[45,2589,2590],{"class":58},") { found ",[45,2592,76],{"class":392},[45,2594,2595],{"class":58}," i; ",[45,2597,2279],{"class":392},[45,2599,1532],{"class":58},[45,2601,2602],{"class":51},"\u002F\u002F 找到即停，不空转\n",[45,2604,2605],{"class":47,"line":95},[45,2606,910],{"class":58},[45,2608,2609,2611,2613,2615,2617,2619,2621,2623,2625,2628,2630,2632],{"class":47,"line":102},[45,2610,979],{"class":392},[45,2612,873],{"class":58},[45,2614,611],{"class":392},[45,2616,986],{"class":58},[45,2618,76],{"class":392},[45,2620,768],{"class":396},[45,2622,994],{"class":58},[45,2624,2548],{"class":392},[45,2626,2627],{"class":396}," 20",[45,2629,1002],{"class":58},[45,2631,1005],{"class":392},[45,2633,879],{"class":58},[45,2635,2636,2638,2640,2642,2645,2648,2650,2652,2654],{"class":47,"line":108},[45,2637,2171],{"class":392},[45,2639,2564],{"class":58},[45,2641,2567],{"class":392},[45,2643,2644],{"class":396}," 2",[45,2646,2647],{"class":392}," !==",[45,2649,991],{"class":396},[45,2651,1627],{"class":58},[45,2653,2508],{"class":392},[45,2655,2656],{"class":51},"   \u002F\u002F 奇数跳过\n",[45,2658,2659,2662,2664],{"class":47,"line":137},[45,2660,2661],{"class":58},"  evens.",[45,2663,839],{"class":72},[45,2665,2666],{"class":58},"(i)\n",[45,2668,2669],{"class":47,"line":142},[45,2670,910],{"class":58},[15,2672,2673,2674,2408,2677,2680,2681,2684,2685,2687],{},"写循环时始终问自己两件事：",[22,2675,2676],{},"终止条件会到达吗",[31,2678,2679],{},"while (true)"," 必须配合 break 或明确的退出分支）；",[22,2682,2683],{},"循环变量在推进吗","（忘记 ",[31,2686,2121],{}," 就是无限循环）。这两类 bug 在浏览器里的表现都是页面直接卡死。",[10,2689,2691],{"id":2690},"六函数参数返回值与作用域","六、函数：参数、返回值与作用域",[15,2693,2694,2695,2698,2699,354],{},"函数是 JS 的基本复用单位：把一段逻辑包起来、给它起个名字、按需调用。HTML 教程里每个示例框能响应点击，背后都是函数在跑。这一章讲清函数的写法、参数与返回值的规则；作用域与 ",[31,2696,2697],{},"this"," 留到第七章，因为那要先消化一个事实：",[22,2700,2701],{},"函数是可以被传来传去的值",[27,2703,2705],{"id":2704},"三种写法与提升","三种写法与「提升」",[36,2707,2709],{"className":383,"code":2708,"language":385,"meta":41,"style":41},"\u002F\u002F ① 函数声明\nfunction double(n) { return n * 2 }\n\n\u002F\u002F ② 函数表达式：把匿名函数存进变量\nconst double = function (n) { return n * 2 }\n\n\u002F\u002F ③ 箭头函数：表达式场景的简写\nconst double = (n) => { return n * 2 }\nconst double = n => n * 2   \u002F\u002F 单参数省括号，单表达式省 {} 和 return\n",[31,2710,2711,2716,2740,2744,2749,2776,2780,2785,2814],{"__ignoreMap":41},[45,2712,2713],{"class":47,"line":48},[45,2714,2715],{"class":51},"\u002F\u002F ① 函数声明\n",[45,2717,2718,2720,2723,2725,2727,2729,2731,2734,2736,2738],{"class":47,"line":55},[45,2719,1515],{"class":392},[45,2721,2722],{"class":72}," double",[45,2724,409],{"class":58},[45,2726,1722],{"class":1523},[45,2728,1008],{"class":58},[45,2730,583],{"class":392},[45,2732,2733],{"class":58}," n ",[45,2735,2093],{"class":392},[45,2737,2644],{"class":396},[45,2739,793],{"class":58},[45,2741,2742],{"class":47,"line":95},[45,2743,99],{"emptyLinePlaceholder":98},[45,2745,2746],{"class":47,"line":102},[45,2747,2748],{"class":51},"\u002F\u002F ② 函数表达式：把匿名函数存进变量\n",[45,2750,2751,2753,2755,2757,2760,2762,2764,2766,2768,2770,2772,2774],{"class":47,"line":108},[45,2752,393],{"class":392},[45,2754,2722],{"class":72},[45,2756,400],{"class":392},[45,2758,2759],{"class":392}," function",[45,2761,873],{"class":58},[45,2763,1722],{"class":1523},[45,2765,1008],{"class":58},[45,2767,583],{"class":392},[45,2769,2733],{"class":58},[45,2771,2093],{"class":392},[45,2773,2644],{"class":396},[45,2775,793],{"class":58},[45,2777,2778],{"class":47,"line":137},[45,2779,99],{"emptyLinePlaceholder":98},[45,2781,2782],{"class":47,"line":142},[45,2783,2784],{"class":51},"\u002F\u002F ③ 箭头函数：表达式场景的简写\n",[45,2786,2787,2789,2791,2793,2795,2797,2799,2801,2804,2806,2808,2810,2812],{"class":47,"line":148},[45,2788,393],{"class":392},[45,2790,2722],{"class":72},[45,2792,400],{"class":392},[45,2794,873],{"class":58},[45,2796,1722],{"class":1523},[45,2798,1627],{"class":58},[45,2800,435],{"class":392},[45,2802,2803],{"class":58}," { ",[45,2805,583],{"class":392},[45,2807,2733],{"class":58},[45,2809,2093],{"class":392},[45,2811,2644],{"class":396},[45,2813,793],{"class":58},[45,2815,2816,2818,2820,2822,2825,2828,2830,2832,2834],{"class":47,"line":178},[45,2817,393],{"class":392},[45,2819,2722],{"class":72},[45,2821,400],{"class":392},[45,2823,2824],{"class":1523}," n",[45,2826,2827],{"class":392}," =>",[45,2829,2733],{"class":58},[45,2831,2093],{"class":392},[45,2833,2644],{"class":396},[45,2835,2836],{"class":51},"   \u002F\u002F 单参数省括号，单表达式省 {} 和 return\n",[15,2838,2839,2840,2842,2843,2845,2846,2848,2849,354],{},"函数声明有",[22,2841,714],{},"（hoisting）：整个函数被搬到作用域顶部，声明之前也能调用。函数表达式与箭头函数存进变量，遵守变量自己的规则——",[31,2844,393],{}," 声明的处于 TDZ，先调用直接报 ",[31,2847,950],{},"。实践中不要依赖提升，",[22,2850,2851],{},"先定义后使用",[356,2853],{"height":2854,"src":2855,"title":2856},660,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F06-functions.html","示例 06：函数的三种写法与参数机制",[27,2858,2860],{"id":2859},"参数默认值只在-undefined-时触发","参数：默认值只在 undefined 时触发",[36,2862,2864],{"className":383,"code":2863,"language":385,"meta":41,"style":41},"function greet(name = '游客') { return '你好，' + name }\n\ngreet()            \u002F\u002F '你好，游客' —— 没传参\ngreet(undefined)   \u002F\u002F '你好，游客' —— 传了 undefined\ngreet('')          \u002F\u002F '你好，'     —— 空串是一个「值」，不触发默认\ngreet(null)        \u002F\u002F '你好，null' —— null 也是一个「值」\n",[31,2865,2866,2894,2898,2908,2921,2934],{"__ignoreMap":41},[45,2867,2868,2870,2873,2875,2878,2880,2882,2884,2886,2889,2891],{"class":47,"line":48},[45,2869,1515],{"class":392},[45,2871,2872],{"class":72}," greet",[45,2874,409],{"class":58},[45,2876,2877],{"class":1523},"name",[45,2879,400],{"class":392},[45,2881,1908],{"class":79},[45,2883,1008],{"class":58},[45,2885,583],{"class":392},[45,2887,2888],{"class":79}," '你好，'",[45,2890,1591],{"class":392},[45,2892,2893],{"class":58}," name }\n",[45,2895,2896],{"class":47,"line":55},[45,2897,99],{"emptyLinePlaceholder":98},[45,2899,2900,2902,2905],{"class":47,"line":95},[45,2901,2036],{"class":72},[45,2903,2904],{"class":58},"()            ",[45,2906,2907],{"class":51},"\u002F\u002F '你好，游客' —— 没传参\n",[45,2909,2910,2912,2914,2916,2918],{"class":47,"line":102},[45,2911,2036],{"class":72},[45,2913,409],{"class":58},[45,2915,709],{"class":396},[45,2917,472],{"class":58},[45,2919,2920],{"class":51},"\u002F\u002F '你好，游客' —— 传了 undefined\n",[45,2922,2923,2925,2927,2929,2931],{"class":47,"line":108},[45,2924,2036],{"class":72},[45,2926,409],{"class":58},[45,2928,1974],{"class":79},[45,2930,1667],{"class":58},[45,2932,2933],{"class":51},"\u002F\u002F '你好，'     —— 空串是一个「值」，不触发默认\n",[45,2935,2936,2938,2940,2942,2945],{"class":47,"line":137},[45,2937,2036],{"class":72},[45,2939,409],{"class":58},[45,2941,349],{"class":396},[45,2943,2944],{"class":58},")        ",[45,2946,2947],{"class":51},"\u002F\u002F '你好，null' —— null 也是一个「值」\n",[15,2949,2950,2951,2953,2954,2960],{},"这条规则与第四章的 ",[31,2952,1942],{}," 完全一致：",[22,2955,2956,2957,2959],{},"只有 ",[31,2958,709],{}," 代表「没有」。"," 与之配套的两条机制：",[1094,2962,2963,2984],{},[1097,2964,2965,2968,2969,2972,2973,566,2976,2979,2980,2983],{},[22,2966,2967],{},"剩余参数"," ",[31,2970,2971],{},"function sum(...nums)","：把所有实参收成一个真数组（能用 ",[31,2974,2975],{},"map",[31,2977,2978],{},"reduce","）。旧代码里的 ",[31,2981,2982],{},"arguments"," 是类数组，新代码不要再用了。",[1097,2985,2986,2968,2989,2992,2993,2996],{},[22,2987,2988],{},"展开运算符",[31,2990,2991],{},"sum(...[1, 2, 3])","：反过来把数组摊成一个个实参。同一个 ",[31,2994,2995],{},"...","，放在形参位置是「收集」，放在实参位置是「展开」。",[27,2998,3000],{"id":2999},"返回值没有-return-就是-undefined","返回值：没有 return 就是 undefined",[1094,3002,3003,3014],{},[1097,3004,3005,3006,3008,3009,3011,3012,1110],{},"函数没有 ",[31,3007,583],{},"（或 ",[31,3010,583],{}," 后面没有值）→ 返回 ",[31,3013,709],{},[1097,3015,3016,3018,3019,3022],{},[31,3017,583],{}," 会",[22,3020,3021],{},"立即结束函数","，后面的代码不再执行——利用这一点写「守卫子句」，把异常情况提前挡掉：",[36,3024,3026],{"className":383,"code":3025,"language":385,"meta":41,"style":41},"function divide(a, b) {\n  if (b === 0) return '除数不能为 0'   \u002F\u002F 守卫：先处理掉不能算的情况\n  return a \u002F b                        \u002F\u002F 主干只有一行\n}\n",[31,3027,3028,3047,3068,3082],{"__ignoreMap":41},[45,3029,3030,3032,3035,3037,3040,3042,3045],{"class":47,"line":48},[45,3031,1515],{"class":392},[45,3033,3034],{"class":72}," divide",[45,3036,409],{"class":58},[45,3038,3039],{"class":1523},"a",[45,3041,2465],{"class":58},[45,3043,3044],{"class":1523},"b",[45,3046,879],{"class":58},[45,3048,3049,3051,3054,3056,3058,3060,3062,3065],{"class":47,"line":55},[45,3050,2171],{"class":392},[45,3052,3053],{"class":58}," (b ",[45,3055,1562],{"class":392},[45,3057,991],{"class":396},[45,3059,1627],{"class":58},[45,3061,583],{"class":392},[45,3063,3064],{"class":79}," '除数不能为 0'",[45,3066,3067],{"class":51},"   \u002F\u002F 守卫：先处理掉不能算的情况\n",[45,3069,3070,3072,3074,3076,3079],{"class":47,"line":95},[45,3071,2209],{"class":392},[45,3073,1413],{"class":58},[45,3075,2096],{"class":392},[45,3077,3078],{"class":58}," b                        ",[45,3080,3081],{"class":51},"\u002F\u002F 主干只有一行\n",[45,3083,3084],{"class":47,"line":102},[45,3085,910],{"class":58},[1094,3087,3088,3098],{},[1097,3089,3090,3091,3094,3095,354],{},"函数只能返回",[22,3092,3093],{},"一个","值；想返回多个数据就包成对象或数组，调用方解构：",[31,3096,3097],{},"const { total, count } = stats(list)",[1097,3099,3100,3102],{},[31,3101,583],{}," 关键字后面不要换行——ASI 会在行尾补分号，把「返回值」悄悄变成「返回 undefined」。",[27,3104,3105],{"id":3105},"箭头函数的适用边界",[15,3107,3108],{},"简写谱系（对应示例里的表格）：",[36,3110,3112],{"className":383,"code":3111,"language":385,"meta":41,"style":41},"(a, b) => { return a + b }   \u002F\u002F 完整形式\n(a, b) => a + b              \u002F\u002F 单表达式：{} 与 return 都能省\nn => n * 2                   \u002F\u002F 单参数：括号能省\n() => 42                     \u002F\u002F 无参数：括号不能省\nn => ({ id: n })             \u002F\u002F 想直接返回对象字面量：要包一层括号\n",[31,3113,3114,3142,3166,3181,3194],{"__ignoreMap":41},[45,3115,3116,3118,3120,3122,3124,3126,3128,3130,3132,3134,3136,3139],{"class":47,"line":48},[45,3117,409],{"class":58},[45,3119,3039],{"class":1523},[45,3121,2465],{"class":58},[45,3123,3044],{"class":1523},[45,3125,1627],{"class":58},[45,3127,435],{"class":392},[45,3129,2803],{"class":58},[45,3131,583],{"class":392},[45,3133,1413],{"class":58},[45,3135,765],{"class":392},[45,3137,3138],{"class":58}," b }   ",[45,3140,3141],{"class":51},"\u002F\u002F 完整形式\n",[45,3143,3144,3146,3148,3150,3152,3154,3156,3158,3160,3163],{"class":47,"line":55},[45,3145,409],{"class":58},[45,3147,3039],{"class":1523},[45,3149,2465],{"class":58},[45,3151,3044],{"class":1523},[45,3153,1627],{"class":58},[45,3155,435],{"class":392},[45,3157,1413],{"class":58},[45,3159,765],{"class":392},[45,3161,3162],{"class":58}," b              ",[45,3164,3165],{"class":51},"\u002F\u002F 单表达式：{} 与 return 都能省\n",[45,3167,3168,3170,3172,3174,3176,3178],{"class":47,"line":95},[45,3169,1722],{"class":1523},[45,3171,2827],{"class":392},[45,3173,2733],{"class":58},[45,3175,2093],{"class":392},[45,3177,2644],{"class":396},[45,3179,3180],{"class":51},"                   \u002F\u002F 单参数：括号能省\n",[45,3182,3183,3186,3188,3191],{"class":47,"line":102},[45,3184,3185],{"class":58},"() ",[45,3187,435],{"class":392},[45,3189,3190],{"class":396}," 42",[45,3192,3193],{"class":51},"                     \u002F\u002F 无参数：括号不能省\n",[45,3195,3196,3198,3200,3203],{"class":47,"line":108},[45,3197,1722],{"class":1523},[45,3199,2827],{"class":392},[45,3201,3202],{"class":58}," ({ id: n })             ",[45,3204,3205],{"class":51},"\u002F\u002F 想直接返回对象字面量：要包一层括号\n",[15,3207,3208,3209,3212,3213,3218,3219,3222,3223,3226],{},"选择建议：",[22,3210,3211],{},"需要名字、需要复用时用函数声明；作为回调「一行搞定」时用箭头函数","。箭头函数有两条硬限制，都和第七章的内容有关：它",[22,3214,3215,3216],{},"没有自己的 ",[31,3217,2697],{},"（捕获外层的），也",[22,3220,3221],{},"不能当构造函数","（不能 ",[31,3224,3225],{},"new","）。",[27,3228,3230],{"id":3229},"函数是一等公民","函数是「一等公民」",[15,3232,3233,3234,3237,3238,3241],{},"在 JS 里函数就是一种值：能存进变量、放进对象、当参数传、当返回值返回。把函数当参数传给另一个函数（",[22,3235,3236],{},"回调","）是最常见的用法——事件监听、",[31,3239,3240],{},"setTimeout","、数组方法都在用：",[36,3243,3245],{"className":383,"code":3244,"language":385,"meta":41,"style":41},"button.addEventListener('click', () => { … })   \u002F\u002F 点击时由浏览器回调\nsetTimeout(() => { … }, 1000)                     \u002F\u002F 1 秒后回调一次\n[1, 2, 3].map(n => n * 2)                         \u002F\u002F map 对每个元素回调\n",[31,3246,3247,3268,3289],{"__ignoreMap":41},[45,3248,3249,3252,3254,3256,3258,3260,3262,3265],{"class":47,"line":48},[45,3250,3251],{"class":58},"button.",[45,3253,486],{"class":72},[45,3255,409],{"class":58},[45,3257,491],{"class":79},[45,3259,494],{"class":58},[45,3261,435],{"class":392},[45,3263,3264],{"class":58}," { … })   ",[45,3266,3267],{"class":51},"\u002F\u002F 点击时由浏览器回调\n",[45,3269,3270,3272,3275,3277,3280,3283,3286],{"class":47,"line":55},[45,3271,3240],{"class":72},[45,3273,3274],{"class":58},"(() ",[45,3276,435],{"class":392},[45,3278,3279],{"class":58}," { … }, ",[45,3281,3282],{"class":396},"1000",[45,3284,3285],{"class":58},")                     ",[45,3287,3288],{"class":51},"\u002F\u002F 1 秒后回调一次\n",[45,3290,3291,3294,3296,3298,3301,3303,3306,3309,3311,3313,3315,3317,3319,3321,3323,3326],{"class":47,"line":95},[45,3292,3293],{"class":58},"[",[45,3295,1471],{"class":396},[45,3297,2465],{"class":58},[45,3299,3300],{"class":396},"2",[45,3302,2465],{"class":58},[45,3304,3305],{"class":396},"3",[45,3307,3308],{"class":58},"].",[45,3310,2975],{"class":72},[45,3312,409],{"class":58},[45,3314,1722],{"class":1523},[45,3316,2827],{"class":392},[45,3318,2733],{"class":58},[45,3320,2093],{"class":392},[45,3322,2644],{"class":396},[45,3324,3325],{"class":58},")                         ",[45,3327,3328],{"class":51},"\u002F\u002F map 对每个元素回调\n",[15,3330,3331,3332,566,3334,3337,3338,1267,3341,3344,3345,566,3348,3350,3351,3344,3354,3226],{},"命名也对号入座：普通函数用动词开头（",[31,3333,1109],{},[31,3335,3336],{},"formatDate","），判断类用 ",[31,3339,3340],{},"is",[31,3342,3343],{},"has"," 开头（",[31,3346,3347],{},"isEmpty",[31,3349,1131],{},"），事件处理用 ",[31,3352,3353],{},"handle",[31,3355,3356],{},"handleClick",[10,3358,3360],{"id":3359},"七作用域与闭包函数记住它的出生环境","七、作用域与闭包：函数记住它的出生环境",[15,3362,3363,3364,3367],{},"函数不只是「一段代码」，它还会",[22,3365,3366],{},"记住自己被定义时所处的环境","——定义时能访问哪些变量，之后无论被带到哪里调用，这份访问权都不会丢。这条性质叫词法作用域，它衍生的最著名的机制就是闭包。",[27,3369,3371],{"id":3370},"作用域链从里往外找","作用域链：从里往外找",[36,3373,3375],{"className":383,"code":3374,"language":385,"meta":41,"style":41},"const site = '我的博客'          \u002F\u002F ① 全局\n\nfunction outer() {\n  const author = '阿蓝'          \u002F\u002F ② outer 的局部\n\n  function inner() {\n    const title = '第一篇'       \u002F\u002F ③ inner 的局部\n    console.log(title, author, site)   \u002F\u002F 三个都能访问：由内向外逐层找\n  }\n  inner()\n}\n",[31,3376,3377,3392,3396,3406,3422,3426,3436,3452,3465,3470,3478],{"__ignoreMap":41},[45,3378,3379,3381,3384,3386,3389],{"class":47,"line":48},[45,3380,393],{"class":392},[45,3382,3383],{"class":396}," site",[45,3385,400],{"class":392},[45,3387,3388],{"class":79}," '我的博客'",[45,3390,3391],{"class":51},"          \u002F\u002F ① 全局\n",[45,3393,3394],{"class":47,"line":55},[45,3395,99],{"emptyLinePlaceholder":98},[45,3397,3398,3400,3403],{"class":47,"line":95},[45,3399,1515],{"class":392},[45,3401,3402],{"class":72}," outer",[45,3404,3405],{"class":58},"() {\n",[45,3407,3408,3411,3414,3416,3419],{"class":47,"line":102},[45,3409,3410],{"class":392},"  const",[45,3412,3413],{"class":396}," author",[45,3415,400],{"class":392},[45,3417,3418],{"class":79}," '阿蓝'",[45,3420,3421],{"class":51},"          \u002F\u002F ② outer 的局部\n",[45,3423,3424],{"class":47,"line":108},[45,3425,99],{"emptyLinePlaceholder":98},[45,3427,3428,3431,3434],{"class":47,"line":137},[45,3429,3430],{"class":392},"  function",[45,3432,3433],{"class":72}," inner",[45,3435,3405],{"class":58},[45,3437,3438,3441,3444,3446,3449],{"class":47,"line":142},[45,3439,3440],{"class":392},"    const",[45,3442,3443],{"class":396}," title",[45,3445,400],{"class":392},[45,3447,3448],{"class":79}," '第一篇'",[45,3450,3451],{"class":51},"       \u002F\u002F ③ inner 的局部\n",[45,3453,3454,3457,3459,3462],{"class":47,"line":148},[45,3455,3456],{"class":58},"    console.",[45,3458,441],{"class":72},[45,3460,3461],{"class":58},"(title, author, site)   ",[45,3463,3464],{"class":51},"\u002F\u002F 三个都能访问：由内向外逐层找\n",[45,3466,3467],{"class":47,"line":178},[45,3468,3469],{"class":58},"  }\n",[45,3471,3472,3475],{"class":47,"line":183},[45,3473,3474],{"class":72},"  inner",[45,3476,3477],{"class":58},"()\n",[45,3479,3480],{"class":47,"line":189},[45,3481,910],{"class":58},[15,3483,3484,3485,3488,3489,3491,3492,3495,3496,3499,3500,3503],{},"变量查找永远遵循「",[22,3486,3487],{},"由内向外","」：先在本地找，找不到去外层，再到全局，都没有就 ",[31,3490,950],{},"。反方向不行——",[31,3493,3494],{},"outer"," 里访问不到 ",[31,3497,3498],{},"inner"," 的 ",[31,3501,3502],{},"title","。函数每嵌套一层，就在作用域链上串一环。",[27,3505,3507],{"id":3506},"闭包内层函数带走了外层的变量","闭包：内层函数「带走」了外层的变量",[36,3509,3511],{"className":383,"code":3510,"language":385,"meta":41,"style":41},"function createCounter() {\n  let count = 0              \u002F\u002F 外层局部变量\n  return function () {\n    count += 1               \u002F\u002F 内层函数在用它\n    return count\n  }\n}\n\nconst next = createCounter()\nnext()   \u002F\u002F 1\nnext()   \u002F\u002F 2 —— count 没有在两次调用间重置\n",[31,3512,3513,3522,3535,3544,3557,3565,3569,3573,3577,3590,3601],{"__ignoreMap":41},[45,3514,3515,3517,3520],{"class":47,"line":48},[45,3516,1515],{"class":392},[45,3518,3519],{"class":72}," createCounter",[45,3521,3405],{"class":58},[45,3523,3524,3526,3528,3530,3532],{"class":47,"line":55},[45,3525,884],{"class":392},[45,3527,748],{"class":58},[45,3529,76],{"class":392},[45,3531,991],{"class":396},[45,3533,3534],{"class":51},"              \u002F\u002F 外层局部变量\n",[45,3536,3537,3539,3541],{"class":47,"line":95},[45,3538,2209],{"class":392},[45,3540,2759],{"class":392},[45,3542,3543],{"class":58}," () {\n",[45,3545,3546,3549,3552,3554],{"class":47,"line":102},[45,3547,3548],{"class":58},"    count ",[45,3550,3551],{"class":392},"+=",[45,3553,768],{"class":396},[45,3555,3556],{"class":51},"               \u002F\u002F 内层函数在用它\n",[45,3558,3559,3562],{"class":47,"line":108},[45,3560,3561],{"class":392},"    return",[45,3563,3564],{"class":58}," count\n",[45,3566,3567],{"class":47,"line":137},[45,3568,3469],{"class":58},[45,3570,3571],{"class":47,"line":142},[45,3572,910],{"class":58},[45,3574,3575],{"class":47,"line":148},[45,3576,99],{"emptyLinePlaceholder":98},[45,3578,3579,3581,3584,3586,3588],{"class":47,"line":178},[45,3580,393],{"class":392},[45,3582,3583],{"class":396}," next",[45,3585,400],{"class":392},[45,3587,3519],{"class":72},[45,3589,3477],{"class":58},[45,3591,3592,3595,3598],{"class":47,"line":183},[45,3593,3594],{"class":72},"next",[45,3596,3597],{"class":58},"()   ",[45,3599,3600],{"class":51},"\u002F\u002F 1\n",[45,3602,3603,3605,3607],{"class":47,"line":189},[45,3604,3594],{"class":72},[45,3606,3597],{"class":58},[45,3608,3609],{"class":51},"\u002F\u002F 2 —— count 没有在两次调用间重置\n",[15,3611,3612,3615,3616,3619,3620,3623,3624,354],{},[31,3613,3614],{},"createCounter"," 执行完毕后，按直觉它的局部变量该被回收；但因为返回出去的内层函数仍然引用着 ",[31,3617,3618],{},"count","，JS 让这个变量",[22,3621,3622],{},"继续存活","。这就是闭包的定义式描述：",[22,3625,3626],{},"闭包 = 函数 + 它定义时所在的环境",[15,3628,3629],{},"闭包的三个典型用途：",[3631,3632,3633,3646,3655],"ol",{},[1097,3634,3635,3638,3639,3641,3642,3645],{},[22,3636,3637],{},"私有状态","：上面的 ",[31,3640,3618],{}," 外部无法直接读写，只能通过 ",[31,3643,3644],{},"next()"," 推进——模块封装的最小版本；",[1097,3647,3648,1103,3651,3654],{},[22,3649,3650],{},"状态工厂",[31,3652,3653],{},"createCounter()"," 每次调用生成一个独立计数器，互不干扰；",[1097,3656,3657,1103,3660,3663,3664,3667],{},[22,3658,3659],{},"一次性 \u002F 记忆化",[31,3661,3662],{},"once(fn)"," 让函数只生效第一次；",[31,3665,3666],{},"memoize(fn)"," 缓存计算结果。",[356,3669],{"height":2854,"src":3670,"title":3671},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F07-closure.html","示例 07：闭包的三个用途与经典陷阱",[27,3673,3675],{"id":3674},"经典陷阱var-循环与-let-循环","经典陷阱：var 循环与 let 循环",[15,3677,3678,3679,1079,3681,3683],{},"第二章提过 ",[31,3680,611],{},[31,3682,979],{}," 循环里「每次迭代一个新绑定」，现在能说清它救的是什么场了：",[36,3685,3687],{"className":383,"code":3686,"language":385,"meta":41,"style":41},"for (var i = 0; i \u003C 3; i++) {\n  setTimeout(() => console.log(i), 0)   \u002F\u002F 3 3 3 —— 三段代码共享同一个 i\n}\n\nfor (let j = 0; j \u003C 3; j++) {\n  setTimeout(() => console.log(j), 0)   \u002F\u002F 0 1 2 —— 每轮 j 是独立绑定\n}\n",[31,3688,3689,3715,3738,3742,3746,3772,3794],{"__ignoreMap":41},[45,3690,3691,3693,3695,3697,3699,3701,3703,3705,3707,3709,3711,3713],{"class":47,"line":48},[45,3692,979],{"class":392},[45,3694,873],{"class":58},[45,3696,615],{"class":392},[45,3698,986],{"class":58},[45,3700,76],{"class":392},[45,3702,991],{"class":396},[45,3704,994],{"class":58},[45,3706,59],{"class":392},[45,3708,999],{"class":396},[45,3710,1002],{"class":58},[45,3712,1005],{"class":392},[45,3714,879],{"class":58},[45,3716,3717,3720,3722,3724,3726,3728,3731,3733,3735],{"class":47,"line":55},[45,3718,3719],{"class":72},"  setTimeout",[45,3721,3274],{"class":58},[45,3723,435],{"class":392},[45,3725,438],{"class":58},[45,3727,441],{"class":72},[45,3729,3730],{"class":58},"(i), ",[45,3732,1717],{"class":396},[45,3734,472],{"class":58},[45,3736,3737],{"class":51},"\u002F\u002F 3 3 3 —— 三段代码共享同一个 i\n",[45,3739,3740],{"class":47,"line":95},[45,3741,910],{"class":58},[45,3743,3744],{"class":47,"line":102},[45,3745,99],{"emptyLinePlaceholder":98},[45,3747,3748,3750,3752,3754,3756,3758,3760,3762,3764,3766,3768,3770],{"class":47,"line":108},[45,3749,979],{"class":392},[45,3751,873],{"class":58},[45,3753,611],{"class":392},[45,3755,1040],{"class":58},[45,3757,76],{"class":392},[45,3759,991],{"class":396},[45,3761,1047],{"class":58},[45,3763,59],{"class":392},[45,3765,999],{"class":396},[45,3767,1054],{"class":58},[45,3769,1005],{"class":392},[45,3771,879],{"class":58},[45,3773,3774,3776,3778,3780,3782,3784,3787,3789,3791],{"class":47,"line":137},[45,3775,3719],{"class":72},[45,3777,3274],{"class":58},[45,3779,435],{"class":392},[45,3781,438],{"class":58},[45,3783,441],{"class":72},[45,3785,3786],{"class":58},"(j), ",[45,3788,1717],{"class":396},[45,3790,472],{"class":58},[45,3792,3793],{"class":51},"\u002F\u002F 0 1 2 —— 每轮 j 是独立绑定\n",[45,3795,3796],{"class":47,"line":142},[45,3797,910],{"class":58},[15,3799,3800,3802,3803,3806,3807,3809,3810,3816,3817,3819],{},[31,3801,615],{}," 只有一份循环变量，等回调执行时循环早已结束、",[31,3804,3805],{},"i"," 停在 3；",[31,3808,611],{}," 每次迭代都创建一个新绑定，回调各自记住当轮的值。",[22,3811,3812,3813,3815],{},"用 ",[31,3814,611],{}," 写循环","就是这条坑的根治方案，",[31,3818,3240],{}," 换成事件监听、数组方法回调，现象完全一样。",[27,3821,3822],{"id":3822},"内存提醒",[15,3824,3825,3826,3829],{},"闭包让外层变量不被回收，这既是特性也是成本：被长期持有的闭包如果引用了很大的数据（大数组、DOM 节点），会阻止它们被释放。实践上不需要紧张，只需记住一条：",[22,3827,3828],{},"长生命周期的地方（全局、长期存在的定时器\u002F监听器）别无意中拖住大对象","；不再需要的监听器记得移除。",[10,3831,3833],{"id":3832},"八字符串模板字符串与常用方法","八、字符串：模板字符串与常用方法",[15,3835,3836,3837,3840,3841,3844,3845,3847],{},"字符串是不可变的原始值（第三章）：所有「修改」方法都返回",[22,3838,3839],{},"新字符串","，原字符串纹丝不动。",[31,3842,3843],{},"name.toUpperCase()"," 之后 ",[31,3846,2877],{}," 还是原样——想把结果留住，要赋给变量。",[27,3849,3851],{"id":3850},"模板字符串拼接的现代写法","模板字符串：拼接的现代写法",[36,3853,3855],{"className":383,"code":3854,"language":385,"meta":41,"style":41},"const user = { name: '阿蓝', points: 1280 }\n\n\u002F\u002F 旧：加号拼接，引号要数清楚\nconst msg1 = '用户 ' + user.name + ' 有 ' + user.points + ' 积分'\n\n\u002F\u002F 新：反引号模板，${} 里能放任意表达式\nconst msg2 = `用户 ${user.name} 有 ${user.points} 积分，下一级还差 ${2000 - user.points}`\n",[31,3856,3857,3877,3881,3886,3918,3922,3927],{"__ignoreMap":41},[45,3858,3859,3861,3863,3865,3867,3869,3872,3875],{"class":47,"line":48},[45,3860,393],{"class":392},[45,3862,782],{"class":396},[45,3864,400],{"class":392},[45,3866,787],{"class":58},[45,3868,790],{"class":79},[45,3870,3871],{"class":58},", points: ",[45,3873,3874],{"class":396},"1280",[45,3876,793],{"class":58},[45,3878,3879],{"class":47,"line":55},[45,3880,99],{"emptyLinePlaceholder":98},[45,3882,3883],{"class":47,"line":95},[45,3884,3885],{"class":51},"\u002F\u002F 旧：加号拼接，引号要数清楚\n",[45,3887,3888,3890,3893,3895,3898,3900,3903,3905,3908,3910,3913,3915],{"class":47,"line":102},[45,3889,393],{"class":392},[45,3891,3892],{"class":396}," msg1",[45,3894,400],{"class":392},[45,3896,3897],{"class":79}," '用户 '",[45,3899,1591],{"class":392},[45,3901,3902],{"class":58}," user.name ",[45,3904,765],{"class":392},[45,3906,3907],{"class":79}," ' 有 '",[45,3909,1591],{"class":392},[45,3911,3912],{"class":58}," user.points ",[45,3914,765],{"class":392},[45,3916,3917],{"class":79}," ' 积分'\n",[45,3919,3920],{"class":47,"line":108},[45,3921,99],{"emptyLinePlaceholder":98},[45,3923,3924],{"class":47,"line":137},[45,3925,3926],{"class":51},"\u002F\u002F 新：反引号模板，${} 里能放任意表达式\n",[45,3928,3929,3931,3934,3936,3939,3941,3944,3946,3949,3951,3953,3956,3959,3962,3964,3966,3968,3970],{"class":47,"line":142},[45,3930,393],{"class":392},[45,3932,3933],{"class":396}," msg2",[45,3935,400],{"class":392},[45,3937,3938],{"class":79}," `用户 ${",[45,3940,2164],{"class":58},[45,3942,3943],{"class":79},".",[45,3945,2877],{"class":58},[45,3947,3948],{"class":79},"} 有 ${",[45,3950,2164],{"class":58},[45,3952,3943],{"class":79},[45,3954,3955],{"class":58},"points",[45,3957,3958],{"class":79},"} 积分，下一级还差 ${",[45,3960,3961],{"class":396},"2000",[45,3963,1622],{"class":392},[45,3965,782],{"class":58},[45,3967,3943],{"class":79},[45,3969,3955],{"class":58},[45,3971,3972],{"class":79},"}`\n",[15,3974,3975,3976,3979,3980,3983],{},"模板字符串的三个能力：插入任意表达式（不是只能放变量）、跨行书写（换行不需要 ",[31,3977,3978],{},"\\n","）、标签模板（",[31,3981,3982],{},"String.raw"," 等高级用法，先了解即可）。拼接 HTML 片段时也用它。",[27,3985,3986],{"id":3986},"常用方法速查",[223,3988,3989,4002],{},[226,3990,3991],{},[229,3992,3993,3996,3999],{},[232,3994,3995],{},"方法",[232,3997,3998],{},"作用",[232,4000,4001],{},"示例",[245,4003,4004,4028,4048,4066,4085,4103,4121,4142,4166],{},[229,4005,4006,4017,4020],{},[250,4007,4008,1267,4011,1267,4014],{},[31,4009,4010],{},"includes",[31,4012,4013],{},"startsWith",[31,4015,4016],{},"endsWith",[250,4018,4019],{},"包含 \u002F 以…开头 \u002F 以…结尾",[250,4021,4022,4025,4026],{},[31,4023,4024],{},"'abc'.includes('b')"," → ",[31,4027,876],{},[229,4029,4030,4035,4041],{},[250,4031,4032],{},[31,4033,4034],{},"indexOf",[250,4036,4037,4038],{},"首次出现的下标，找不到返回 ",[22,4039,4040],{},"-1",[250,4042,4043,4025,4046],{},[31,4044,4045],{},"'a-b-c'.indexOf('-')",[31,4047,1471],{},[229,4049,4050,4055,4058],{},[250,4051,4052],{},[31,4053,4054],{},"slice(start, end)",[250,4056,4057],{},"截取，支持负数下标（从尾部算）",[250,4059,4060,4025,4063],{},[31,4061,4062],{},"'hello'.slice(1, 4)",[31,4064,4065],{},"'ell'",[229,4067,4068,4076,4079],{},[250,4069,4070,1267,4073],{},[31,4071,4072],{},"toUpperCase",[31,4074,4075],{},"toLowerCase",[250,4077,4078],{},"大小写转换",[250,4080,4081,4082],{},"比较前统一大小写：",[31,4083,4084],{},"a.toLowerCase() === b.toLowerCase()",[229,4086,4087,4092,4095],{},[250,4088,4089],{},[31,4090,4091],{},"trim",[250,4093,4094],{},"去掉首尾空白（表单输入必做）",[250,4096,4097,4025,4100],{},[31,4098,4099],{},"'  hi  '.trim()",[31,4101,4102],{},"'hi'",[229,4104,4105,4110,4113],{},[250,4106,4107],{},[31,4108,4109],{},"split",[250,4111,4112],{},"按分隔符切成数组",[250,4114,4115,4025,4118],{},[31,4116,4117],{},"'a,b,c'.split(',')",[31,4119,4120],{},"['a','b','c']",[229,4122,4123,4131,4134],{},[250,4124,4125,1267,4128],{},[31,4126,4127],{},"replace",[31,4129,4130],{},"replaceAll",[250,4132,4133],{},"替换，支持正则",[250,4135,4136,4025,4139],{},[31,4137,4138],{},"'a-b'.replaceAll('-', '+')",[31,4140,4141],{},"'a+b'",[229,4143,4144,4155,4158],{},[250,4145,4146,1267,4149,1267,4152],{},[31,4147,4148],{},"repeat",[31,4150,4151],{},"padStart",[31,4153,4154],{},"padEnd",[250,4156,4157],{},"重复 \u002F 补齐",[250,4159,4160,4025,4163],{},[31,4161,4162],{},"'7'.padStart(2, '0')",[31,4164,4165],{},"'07'",[229,4167,4168,4176,4179],{},[250,4169,4170,1267,4173],{},[31,4171,4172],{},"charAt(i)",[31,4174,4175],{},"at(-1)",[250,4177,4178],{},"取单个字符",[250,4180,4181,4025,4184],{},[31,4182,4183],{},"'你好'.at(-1)",[31,4185,4186],{},"'好'",[356,4188],{"height":2854,"src":4189,"title":4190},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F08-string.html","示例 08：字符串常用方法实操",[27,4192,4193],{"id":4193},"两个值得单独说的点",[15,4195,4196,2968,4202,4205,4206,4208,4209,612,4212,4214,4215,4217,4218,354],{},[22,4197,4198,4199,4201],{},"判断包含时 ",[31,4200,4034],{}," 的 -1。",[31,4203,4204],{},"if (str.indexOf('x'))"," 是错误写法：找不到返回 ",[31,4207,4040],{},"，是",[22,4210,4211],{},"真值",[31,4213,870],{}," 居然会通过。正确写法是用 ",[31,4216,4010],{},"，或者显式写 ",[31,4219,4220],{},"if (str.indexOf('x') !== -1)",[15,4222,4223,4232,4233,4236,4237,4239,4240,354],{},[22,4224,4225,1852,4228,4231],{},[31,4226,4227],{},"slice",[31,4229,4230],{},"substring"," 的区别。"," 负数下标时 ",[31,4234,4235],{},"slice(-3)"," 表示「从倒数第 3 个开始」；",[31,4238,4230],{}," 会把负数当 0，且参数顺序颠倒时自动交换——行为不一致且难记，",[22,4241,4242,4243],{},"统一用 ",[31,4244,4227],{},[10,4246,4248],{"id":4247},"九数组增删改查与-map-filter-reduce","九、数组：增删改查与 map \u002F filter \u002F reduce",[15,4250,4251,4252,4255,4256,1267,4258,4261,4262,1267,4264,354],{},"数组是有序的值的集合，日常写法是字面量 ",[31,4253,4254],{},"[1, 2, 3]","。它同时是「列表」也是「栈\u002F队列」两种用法的载体：一端进出用 ",[31,4257,839],{},[31,4259,4260],{},"pop","，两端进出用 ",[31,4263,839],{},[31,4265,4266],{},"shift",[27,4268,4269],{"id":4269},"创建与访问",[36,4271,4273],{"className":383,"code":4272,"language":385,"meta":41,"style":41},"const langs = ['HTML', 'CSS', 'JavaScript']   \u002F\u002F 字面量，最常用\nconst empty = []                               \u002F\u002F 空数组\nconst grid = Array.from({ length: 3 }, (_, i) => i * 2)   \u002F\u002F [0, 2, 4]，由长度生成\n\nlangs[0]        \u002F\u002F 'HTML'，下标从 0 开始\nlangs.at(-1)    \u002F\u002F 'JavaScript'，负数从尾部数，比 langs[langs.length - 1] 干净\nlangs.length    \u002F\u002F 3，长度是属性不是方法\n",[31,4274,4275,4305,4320,4364,4368,4381,4401],{"__ignoreMap":41},[45,4276,4277,4279,4282,4284,4286,4289,4291,4294,4296,4299,4302],{"class":47,"line":48},[45,4278,393],{"class":392},[45,4280,4281],{"class":396}," langs",[45,4283,400],{"class":392},[45,4285,2459],{"class":58},[45,4287,4288],{"class":79},"'HTML'",[45,4290,2465],{"class":58},[45,4292,4293],{"class":79},"'CSS'",[45,4295,2465],{"class":58},[45,4297,4298],{"class":79},"'JavaScript'",[45,4300,4301],{"class":58},"]   ",[45,4303,4304],{"class":51},"\u002F\u002F 字面量，最常用\n",[45,4306,4307,4309,4312,4314,4317],{"class":47,"line":55},[45,4308,393],{"class":392},[45,4310,4311],{"class":396}," empty",[45,4313,400],{"class":392},[45,4315,4316],{"class":58}," []                               ",[45,4318,4319],{"class":51},"\u002F\u002F 空数组\n",[45,4321,4322,4324,4327,4329,4332,4335,4338,4340,4343,4345,4347,4349,4351,4353,4355,4357,4359,4361],{"class":47,"line":95},[45,4323,393],{"class":392},[45,4325,4326],{"class":396}," grid",[45,4328,400],{"class":392},[45,4330,4331],{"class":58}," Array.",[45,4333,4334],{"class":72},"from",[45,4336,4337],{"class":58},"({ length: ",[45,4339,3305],{"class":396},[45,4341,4342],{"class":58}," }, (",[45,4344,1143],{"class":1523},[45,4346,2465],{"class":58},[45,4348,3805],{"class":1523},[45,4350,1627],{"class":58},[45,4352,435],{"class":392},[45,4354,986],{"class":58},[45,4356,2093],{"class":392},[45,4358,2644],{"class":396},[45,4360,472],{"class":58},[45,4362,4363],{"class":51},"\u002F\u002F [0, 2, 4]，由长度生成\n",[45,4365,4366],{"class":47,"line":102},[45,4367,99],{"emptyLinePlaceholder":98},[45,4369,4370,4373,4375,4378],{"class":47,"line":108},[45,4371,4372],{"class":58},"langs[",[45,4374,1717],{"class":396},[45,4376,4377],{"class":58},"]        ",[45,4379,4380],{"class":51},"\u002F\u002F 'HTML'，下标从 0 开始\n",[45,4382,4383,4386,4389,4391,4393,4395,4398],{"class":47,"line":137},[45,4384,4385],{"class":58},"langs.",[45,4387,4388],{"class":72},"at",[45,4390,409],{"class":58},[45,4392,2090],{"class":392},[45,4394,1471],{"class":396},[45,4396,4397],{"class":58},")    ",[45,4399,4400],{"class":51},"\u002F\u002F 'JavaScript'，负数从尾部数，比 langs[langs.length - 1] 干净\n",[45,4402,4403,4405,4407],{"class":47,"line":142},[45,4404,4385],{"class":58},[45,4406,2317],{"class":396},[45,4408,4409],{"class":51},"    \u002F\u002F 3，长度是属性不是方法\n",[15,4411,4412,4413,1110,4415,4417,4418,4421],{},"访问不存在的下标不报错，返回 ",[31,4414,709],{},[31,4416,2317],{}," 可以写入（",[31,4419,4420],{},"arr.length = 0"," 是清空数组的常用写法）。",[27,4423,4425],{"id":4424},"增删改记住哪端快","增删改：记住哪端快",[223,4427,4428,4442],{},[226,4429,4430],{},[229,4431,4432,4435,4437,4440],{},[232,4433,4434],{},"操作",[232,4436,3995],{},[232,4438,4439],{},"效果",[232,4441,1243],{},[245,4443,4444,4460,4475,4490,4505,4521],{},[229,4445,4446,4449,4454,4457],{},[250,4447,4448],{},"尾部加",[250,4450,4451],{},[31,4452,4453],{},"push(x)",[250,4455,4456],{},"返回新长度",[250,4458,4459],{},"改原数组",[229,4461,4462,4465,4470,4473],{},[250,4463,4464],{},"尾部删",[250,4466,4467],{},[31,4468,4469],{},"pop()",[250,4471,4472],{},"返回被删的元素",[250,4474,4459],{},[229,4476,4477,4480,4485,4487],{},[250,4478,4479],{},"头部加",[250,4481,4482],{},[31,4483,4484],{},"unshift(x)",[250,4486,4456],{},[250,4488,4489],{},"慢，整体后移",[229,4491,4492,4495,4500,4502],{},[250,4493,4494],{},"头部删",[250,4496,4497],{},[31,4498,4499],{},"shift()",[250,4501,4472],{},[250,4503,4504],{},"慢，整体前移",[229,4506,4507,4510,4515,4518],{},[250,4508,4509],{},"任意位置",[250,4511,4512],{},[31,4513,4514],{},"splice(start, delCount, ...items)",[250,4516,4517],{},"返回被删元素组成的数组",[250,4519,4520],{},"就地插入\u002F删除\u002F替换三合一",[229,4522,4523,4526,4530,4533],{},[250,4524,4525],{},"截取不改原数组",[250,4527,4528],{},[31,4529,4054],{},[250,4531,4532],{},"返回新数组",[250,4534,4535],{},"与字符串同名方法同语义",[36,4537,4539],{"className":383,"code":4538,"language":385,"meta":41,"style":41},"const list = ['a', 'b', 'c']\nlist.splice(1, 1, 'B', 'B2')   \u002F\u002F 从下标 1 删 1 个，插入两个 → ['a', 'B', 'B2', 'c']\nconst copy = list.slice()      \u002F\u002F 浅拷贝一份\n",[31,4540,4541,4568,4599],{"__ignoreMap":41},[45,4542,4543,4545,4548,4550,4552,4555,4557,4560,4562,4565],{"class":47,"line":48},[45,4544,393],{"class":392},[45,4546,4547],{"class":396}," list",[45,4549,400],{"class":392},[45,4551,2459],{"class":58},[45,4553,4554],{"class":79},"'a'",[45,4556,2465],{"class":58},[45,4558,4559],{"class":79},"'b'",[45,4561,2465],{"class":58},[45,4563,4564],{"class":79},"'c'",[45,4566,4567],{"class":58},"]\n",[45,4569,4570,4573,4576,4578,4580,4582,4584,4586,4589,4591,4594,4596],{"class":47,"line":55},[45,4571,4572],{"class":58},"list.",[45,4574,4575],{"class":72},"splice",[45,4577,409],{"class":58},[45,4579,1471],{"class":396},[45,4581,2465],{"class":58},[45,4583,1471],{"class":396},[45,4585,2465],{"class":58},[45,4587,4588],{"class":79},"'B'",[45,4590,2465],{"class":58},[45,4592,4593],{"class":79},"'B2'",[45,4595,472],{"class":58},[45,4597,4598],{"class":51},"\u002F\u002F 从下标 1 删 1 个，插入两个 → ['a', 'B', 'B2', 'c']\n",[45,4600,4601,4603,4606,4608,4611,4613,4616],{"class":47,"line":95},[45,4602,393],{"class":392},[45,4604,4605],{"class":396}," copy",[45,4607,400],{"class":392},[45,4609,4610],{"class":58}," list.",[45,4612,4227],{"class":72},[45,4614,4615],{"class":58},"()      ",[45,4617,4618],{"class":51},"\u002F\u002F 浅拷贝一份\n",[27,4620,4622],{"id":4621},"遍历forof-与经典的-for","遍历：for...of 与经典的 for",[15,4624,4625,4626,4632,4633,4635,4636,4639,4640,354,4643,4645],{},"第五章已定下规则：",[22,4627,4628,4629,4631],{},"数组用 ",[31,4630,2417],{}," 拿值","；需要下标时用经典 ",[31,4634,979],{},"，或者 ",[31,4637,4638],{},"arr.entries()"," 解构出 ",[31,4641,4642],{},"[index, value]",[31,4644,2403],{}," 在数组上给字符串下标，继续回避。",[36,4647,4649],{"className":383,"code":4648,"language":385,"meta":41,"style":41},"for (const lang of langs) console.log(lang)\n\nfor (const [i, lang] of langs.entries()) {\n  console.log(i, lang)          \u002F\u002F 0 'HTML' \u002F 1 'CSS' \u002F 2 'JavaScript'\n}\n",[31,4650,4651,4672,4676,4705,4717],{"__ignoreMap":41},[45,4652,4653,4655,4657,4659,4662,4664,4667,4669],{"class":47,"line":48},[45,4654,979],{"class":392},[45,4656,873],{"class":58},[45,4658,393],{"class":392},[45,4660,4661],{"class":396}," lang",[45,4663,2372],{"class":392},[45,4665,4666],{"class":58}," langs) console.",[45,4668,441],{"class":72},[45,4670,4671],{"class":58},"(lang)\n",[45,4673,4674],{"class":47,"line":55},[45,4675,99],{"emptyLinePlaceholder":98},[45,4677,4678,4680,4682,4684,4686,4688,4690,4693,4695,4697,4700,4702],{"class":47,"line":95},[45,4679,979],{"class":392},[45,4681,873],{"class":58},[45,4683,393],{"class":392},[45,4685,2459],{"class":58},[45,4687,3805],{"class":396},[45,4689,2465],{"class":58},[45,4691,4692],{"class":396},"lang",[45,4694,2471],{"class":58},[45,4696,2474],{"class":392},[45,4698,4699],{"class":58}," langs.",[45,4701,2480],{"class":72},[45,4703,4704],{"class":58},"()) {\n",[45,4706,4707,4709,4711,4714],{"class":47,"line":102},[45,4708,2488],{"class":58},[45,4710,441],{"class":72},[45,4712,4713],{"class":58},"(i, lang)          ",[45,4715,4716],{"class":51},"\u002F\u002F 0 'HTML' \u002F 1 'CSS' \u002F 2 'JavaScript'\n",[45,4718,4719],{"class":47,"line":108},[45,4720,910],{"class":58},[27,4722,4723],{"id":4723},"查找与判断",[36,4725,4727],{"className":383,"code":4726,"language":385,"meta":41,"style":41},"langs.includes('CSS')              \u002F\u002F true —— 包含某个值吗\nlangs.indexOf('Java')              \u002F\u002F -1 —— 找不到返回 -1（同样是那个坑）\nlangs.find(x => x.length > 4)      \u002F\u002F 'JavaScript'？不，返回第一个长度>4的元素\nlangs.findIndex(x => x === 'CSS')  \u002F\u002F 1\n",[31,4728,4729,4744,4760,4791],{"__ignoreMap":41},[45,4730,4731,4733,4735,4737,4739,4741],{"class":47,"line":48},[45,4732,4385],{"class":58},[45,4734,4010],{"class":72},[45,4736,409],{"class":58},[45,4738,4293],{"class":79},[45,4740,1682],{"class":58},[45,4742,4743],{"class":51},"\u002F\u002F true —— 包含某个值吗\n",[45,4745,4746,4748,4750,4752,4755,4757],{"class":47,"line":55},[45,4747,4385],{"class":58},[45,4749,4034],{"class":72},[45,4751,409],{"class":58},[45,4753,4754],{"class":79},"'Java'",[45,4756,1682],{"class":58},[45,4758,4759],{"class":51},"\u002F\u002F -1 —— 找不到返回 -1（同样是那个坑）\n",[45,4761,4762,4764,4767,4769,4772,4774,4777,4779,4782,4785,4788],{"class":47,"line":95},[45,4763,4385],{"class":58},[45,4765,4766],{"class":72},"find",[45,4768,409],{"class":58},[45,4770,4771],{"class":1523},"x",[45,4773,2827],{"class":392},[45,4775,4776],{"class":58}," x.",[45,4778,2317],{"class":396},[45,4780,4781],{"class":392}," >",[45,4783,4784],{"class":396}," 4",[45,4786,4787],{"class":58},")      ",[45,4789,4790],{"class":51},"\u002F\u002F 'JavaScript'？不，返回第一个长度>4的元素\n",[45,4792,4793,4795,4798,4800,4802,4804,4807,4809,4812,4815],{"class":47,"line":102},[45,4794,4385],{"class":58},[45,4796,4797],{"class":72},"findIndex",[45,4799,409],{"class":58},[45,4801,4771],{"class":1523},[45,4803,2827],{"class":392},[45,4805,4806],{"class":58}," x ",[45,4808,1562],{"class":392},[45,4810,4811],{"class":79}," 'CSS'",[45,4813,4814],{"class":58},")  ",[45,4816,3600],{"class":51},[15,4818,4819,4821,4822,4825,4826,4828,4829,4821,4831,4834,4835,4837,4838,1267,4840,1267,4843,4845],{},[31,4820,4766],{}," 返回",[22,4823,4824],{},"元素","（找不到 ",[31,4827,709],{},"），",[31,4830,4797],{},[22,4832,4833],{},"下标","（找不到 -1），",[31,4836,4034],{}," 严格相等比较、不做函数判断。以上是「查」，接下来一节的 ",[31,4839,2975],{},[31,4841,4842],{},"filter",[31,4844,2978],{}," 才是数组真正的主力。",[356,4847],{"height":2854,"src":4848,"title":4849},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F09a-arrays.html","示例 09a：数组的增删改查",[27,4851,4852],{"id":4852},"快速自查的三个坑",[1094,4854,4855,4873,4884],{},[1097,4856,4857,1103,4862,4865,4866,4869,4870,4872],{},[22,4858,4859,4861],{},[31,4860,839],{}," 返回的是长度不是数组",[31,4863,4864],{},"const r = arr.push(1)"," 后拿 ",[31,4867,4868],{},"r"," 继续链式调用必然错——",[31,4871,839],{}," 改的是原数组，不要接着用返回值。",[1097,4874,4875,4883],{},[22,4876,4877,4879,4880,4882],{},[31,4878,4575],{}," 改原数组、",[31,4881,4227],{}," 不改","：名字只差一个字母，行为相反，拿不准时先查这条表。",[1097,4885,4886,1103,4892,4895,4896,4899,4900,4903],{},[22,4887,4888,4891],{},[31,4889,4890],{},"sort"," 默认按字符串排序",[31,4893,4894],{},"[10, 2, 1].sort()"," 得到 ",[31,4897,4898],{},"[1, 10, 2]","！数字排序必须传比较函数 ",[31,4901,4902],{},"sort((a, b) => a - b)","，下一节展开。",[27,4905,4907],{"id":4906},"map-filter-reduce用回调加工数组","map \u002F filter \u002F reduce：用回调加工数组",[15,4909,4910,4911,4914,4915,4918],{},"数组的「遍历—加工」几乎都可以用这几个方法表达，它们都以",[22,4912,4913],{},"回调函数","为参数（第六章的一等公民），且都",[22,4916,4917],{},"不改原数组","（返回新数组\u002F新值）：",[36,4920,4922],{"className":383,"code":4921,"language":385,"meta":41,"style":41},"const nums = [1, 2, 3, 4, 5, 6]\n\nnums.map(n => n * n)              \u002F\u002F [1, 4, 9, 16, 25, 36]   每个元素变换 → 等长新数组\nnums.filter(n => n % 2 === 0)     \u002F\u002F [2, 4, 6]              按条件保留 → 变短的新数组\nnums.reduce((acc, n) => acc + n, 0)   \u002F\u002F 21                  从左到右汇总成一个值\n\nnums.find(n => n > 3)             \u002F\u002F 4       第一个满足的「元素」，没有则 undefined\nnums.findIndex(n => n > 3)        \u002F\u002F 3       第一个满足的「下标」，没有则 -1\nnums.some(n => n > 5)             \u002F\u002F true    「存在」满足的\nnums.every(n => n > 0)            \u002F\u002F true    「全部」满足\nnums.includes(4)                  \u002F\u002F true    是否包含某值（=== 比较）\n",[31,4923,4924,4961,4965,4988,5016,5051,5055,5079,5102,5126,5151],{"__ignoreMap":41},[45,4925,4926,4928,4931,4933,4935,4937,4939,4941,4943,4945,4947,4950,4952,4954,4956,4959],{"class":47,"line":48},[45,4927,393],{"class":392},[45,4929,4930],{"class":396}," nums",[45,4932,400],{"class":392},[45,4934,2459],{"class":58},[45,4936,1471],{"class":396},[45,4938,2465],{"class":58},[45,4940,3300],{"class":396},[45,4942,2465],{"class":58},[45,4944,3305],{"class":396},[45,4946,2465],{"class":58},[45,4948,4949],{"class":396},"4",[45,4951,2465],{"class":58},[45,4953,2272],{"class":396},[45,4955,2465],{"class":58},[45,4957,4958],{"class":396},"6",[45,4960,4567],{"class":58},[45,4962,4963],{"class":47,"line":55},[45,4964,99],{"emptyLinePlaceholder":98},[45,4966,4967,4970,4972,4974,4976,4978,4980,4982,4985],{"class":47,"line":95},[45,4968,4969],{"class":58},"nums.",[45,4971,2975],{"class":72},[45,4973,409],{"class":58},[45,4975,1722],{"class":1523},[45,4977,2827],{"class":392},[45,4979,2733],{"class":58},[45,4981,2093],{"class":392},[45,4983,4984],{"class":58}," n)              ",[45,4986,4987],{"class":51},"\u002F\u002F [1, 4, 9, 16, 25, 36]   每个元素变换 → 等长新数组\n",[45,4989,4990,4992,4994,4996,4998,5000,5002,5004,5006,5008,5010,5013],{"class":47,"line":102},[45,4991,4969],{"class":58},[45,4993,4842],{"class":72},[45,4995,409],{"class":58},[45,4997,1722],{"class":1523},[45,4999,2827],{"class":392},[45,5001,2733],{"class":58},[45,5003,2567],{"class":392},[45,5005,2644],{"class":396},[45,5007,1597],{"class":392},[45,5009,991],{"class":396},[45,5011,5012],{"class":58},")     ",[45,5014,5015],{"class":51},"\u002F\u002F [2, 4, 6]              按条件保留 → 变短的新数组\n",[45,5017,5018,5020,5022,5025,5028,5030,5032,5034,5036,5039,5041,5044,5046,5048],{"class":47,"line":108},[45,5019,4969],{"class":58},[45,5021,2978],{"class":72},[45,5023,5024],{"class":58},"((",[45,5026,5027],{"class":1523},"acc",[45,5029,2465],{"class":58},[45,5031,1722],{"class":1523},[45,5033,1627],{"class":58},[45,5035,435],{"class":392},[45,5037,5038],{"class":58}," acc ",[45,5040,765],{"class":392},[45,5042,5043],{"class":58}," n, ",[45,5045,1717],{"class":396},[45,5047,472],{"class":58},[45,5049,5050],{"class":51},"\u002F\u002F 21                  从左到右汇总成一个值\n",[45,5052,5053],{"class":47,"line":137},[45,5054,99],{"emptyLinePlaceholder":98},[45,5056,5057,5059,5061,5063,5065,5067,5069,5071,5073,5076],{"class":47,"line":142},[45,5058,4969],{"class":58},[45,5060,4766],{"class":72},[45,5062,409],{"class":58},[45,5064,1722],{"class":1523},[45,5066,2827],{"class":392},[45,5068,2733],{"class":58},[45,5070,2215],{"class":392},[45,5072,999],{"class":396},[45,5074,5075],{"class":58},")             ",[45,5077,5078],{"class":51},"\u002F\u002F 4       第一个满足的「元素」，没有则 undefined\n",[45,5080,5081,5083,5085,5087,5089,5091,5093,5095,5097,5099],{"class":47,"line":148},[45,5082,4969],{"class":58},[45,5084,4797],{"class":72},[45,5086,409],{"class":58},[45,5088,1722],{"class":1523},[45,5090,2827],{"class":392},[45,5092,2733],{"class":58},[45,5094,2215],{"class":392},[45,5096,999],{"class":396},[45,5098,2944],{"class":58},[45,5100,5101],{"class":51},"\u002F\u002F 3       第一个满足的「下标」，没有则 -1\n",[45,5103,5104,5106,5109,5111,5113,5115,5117,5119,5121,5123],{"class":47,"line":178},[45,5105,4969],{"class":58},[45,5107,5108],{"class":72},"some",[45,5110,409],{"class":58},[45,5112,1722],{"class":1523},[45,5114,2827],{"class":392},[45,5116,2733],{"class":58},[45,5118,2215],{"class":392},[45,5120,2583],{"class":396},[45,5122,5075],{"class":58},[45,5124,5125],{"class":51},"\u002F\u002F true    「存在」满足的\n",[45,5127,5128,5130,5133,5135,5137,5139,5141,5143,5145,5148],{"class":47,"line":183},[45,5129,4969],{"class":58},[45,5131,5132],{"class":72},"every",[45,5134,409],{"class":58},[45,5136,1722],{"class":1523},[45,5138,2827],{"class":392},[45,5140,2733],{"class":58},[45,5142,2215],{"class":392},[45,5144,991],{"class":396},[45,5146,5147],{"class":58},")            ",[45,5149,5150],{"class":51},"\u002F\u002F true    「全部」满足\n",[45,5152,5153,5155,5157,5159,5161,5164],{"class":47,"line":189},[45,5154,4969],{"class":58},[45,5156,4010],{"class":72},[45,5158,409],{"class":58},[45,5160,4949],{"class":396},[45,5162,5163],{"class":58},")                  ",[45,5165,5166],{"class":51},"\u002F\u002F true    是否包含某值（=== 比较）\n",[15,5168,5169],{},"三个主角的分工记住这张表：",[223,5171,5172,5187],{},[226,5173,5174],{},[229,5175,5176,5178,5181,5184],{},[232,5177,3995],{},[232,5179,5180],{},"一句话",[232,5182,5183],{},"返回",[232,5185,5186],{},"什么时候用",[245,5188,5189,5204,5219],{},[229,5190,5191,5195,5198,5201],{},[250,5192,5193],{},[31,5194,2975],{},[250,5196,5197],{},"逐个变换",[250,5199,5200],{},"等长新数组",[250,5202,5203],{},"「每个都要处理一遍」",[229,5205,5206,5210,5213,5216],{},[250,5207,5208],{},[31,5209,4842],{},[250,5211,5212],{},"逐个筛选",[250,5214,5215],{},"变短（可能空）的新数组",[250,5217,5218],{},"「挑出符合条件的」",[229,5220,5221,5225,5228,5231],{},[250,5222,5223],{},[31,5224,2978],{},[250,5226,5227],{},"逐个累计",[250,5229,5230],{},"任意类型的一个结果",[250,5232,5233],{},"「求和 \u002F 计数 \u002F 分组 \u002F 拼字符串」",[15,5235,5236,5238,5239,5242,5243,2968,5246,5248,5249,5251,5252,5254,5255,5257,5258,354],{},[31,5237,2978],{}," 的两个参数值得盯住：",[31,5240,5241],{},"(acc, n) => 新acc"," 的回调，加一个",[22,5244,5245],{},"初始值",[31,5247,1717],{},"。初始值不同，结果类型就不同——传 ",[31,5250,1717],{}," 求和、传 ",[31,5253,1367],{}," 收集、传 ",[31,5256,658],{}," 分组。不传初始值时以第一个元素为起点，空数组会直接报错，所以",[22,5259,5260],{},"请始终传初始值",[27,5262,5263],{"id":5263},"链式调用与短路顺序",[36,5265,5267],{"className":383,"code":5266,"language":385,"meta":41,"style":41},"const products = [\n  { name: '键盘', price: 399, onSale: true },\n  { name: '鼠标', price: 129, onSale: false },\n  { name: '显示器', price: 1499, onSale: true }\n]\n\nconst names = products\n  .filter(p => p.onSale)          \u002F\u002F 先筛\n  .map(p => `${p.name} ¥${p.price}`)  \u002F\u002F 再变换\n",[31,5268,5269,5281,5303,5321,5339,5343,5347,5359,5378],{"__ignoreMap":41},[45,5270,5271,5273,5276,5278],{"class":47,"line":48},[45,5272,393],{"class":392},[45,5274,5275],{"class":396}," products",[45,5277,400],{"class":392},[45,5279,5280],{"class":58}," [\n",[45,5282,5283,5286,5289,5292,5295,5298,5300],{"class":47,"line":55},[45,5284,5285],{"class":58},"  { name: ",[45,5287,5288],{"class":79},"'键盘'",[45,5290,5291],{"class":58},", price: ",[45,5293,5294],{"class":396},"399",[45,5296,5297],{"class":58},", onSale: ",[45,5299,876],{"class":396},[45,5301,5302],{"class":58}," },\n",[45,5304,5305,5307,5310,5312,5315,5317,5319],{"class":47,"line":95},[45,5306,5285],{"class":58},[45,5308,5309],{"class":79},"'鼠标'",[45,5311,5291],{"class":58},[45,5313,5314],{"class":396},"129",[45,5316,5297],{"class":58},[45,5318,1570],{"class":396},[45,5320,5302],{"class":58},[45,5322,5323,5325,5328,5330,5333,5335,5337],{"class":47,"line":102},[45,5324,5285],{"class":58},[45,5326,5327],{"class":79},"'显示器'",[45,5329,5291],{"class":58},[45,5331,5332],{"class":396},"1499",[45,5334,5297],{"class":58},[45,5336,876],{"class":396},[45,5338,793],{"class":58},[45,5340,5341],{"class":47,"line":108},[45,5342,4567],{"class":58},[45,5344,5345],{"class":47,"line":137},[45,5346,99],{"emptyLinePlaceholder":98},[45,5348,5349,5351,5354,5356],{"class":47,"line":142},[45,5350,393],{"class":392},[45,5352,5353],{"class":396}," names",[45,5355,400],{"class":392},[45,5357,5358],{"class":58}," products\n",[45,5360,5361,5364,5366,5368,5370,5372,5375],{"class":47,"line":148},[45,5362,5363],{"class":58},"  .",[45,5365,4842],{"class":72},[45,5367,409],{"class":58},[45,5369,15],{"class":1523},[45,5371,2827],{"class":392},[45,5373,5374],{"class":58}," p.onSale)          ",[45,5376,5377],{"class":51},"\u002F\u002F 先筛\n",[45,5379,5380,5382,5384,5386,5388,5390,5393,5395,5397,5399,5402,5404,5406,5408,5411,5413],{"class":47,"line":178},[45,5381,5363],{"class":58},[45,5383,2975],{"class":72},[45,5385,409],{"class":58},[45,5387,15],{"class":1523},[45,5389,2827],{"class":392},[45,5391,5392],{"class":79}," `${",[45,5394,15],{"class":58},[45,5396,3943],{"class":79},[45,5398,2877],{"class":58},[45,5400,5401],{"class":79},"} ¥${",[45,5403,15],{"class":58},[45,5405,3943],{"class":79},[45,5407,1159],{"class":58},[45,5409,5410],{"class":79},"}`",[45,5412,4814],{"class":58},[45,5414,5415],{"class":51},"\u002F\u002F 再变换\n",[356,5417],{"height":2283,"src":5418,"title":5419},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F09b-array-methods.html","示例 09b：map \u002F filter \u002F reduce 实战",[15,5421,5422,5423,5431,5432,5434],{},"链条顺序影响性能与可读性：",[22,5424,5425,5426,5428,5429],{},"先 ",[31,5427,4842],{}," 后 ",[31,5430,2975],{}," 能让 ",[31,5433,2975],{}," 处理更少的元素；链条不要超过三步，更长的逻辑抽成命名函数。",[27,5436,5438,5440],{"id":5437},"sort-与浅拷贝",[31,5439,4890],{}," 与「浅拷贝」",[36,5442,5444],{"className":383,"code":5443,"language":385,"meta":41,"style":41},"[10, 2, 1].sort()                     \u002F\u002F [1, 10, 2] ✗ 默认转字符串逐字符比较\n[10, 2, 1].sort((a, b) => a - b)      \u002F\u002F [1, 2, 10] ✓ 升序\n[10, 2, 1].sort((a, b) => b - a)      \u002F\u002F [10, 2, 1] ✓ 降序\n\nconst sorted = [...nums].sort((a, b) => a - b)   \u002F\u002F 想要新数组：先浅拷贝再排\n",[31,5445,5446,5471,5511,5551,5555],{"__ignoreMap":41},[45,5447,5448,5450,5453,5455,5457,5459,5461,5463,5465,5468],{"class":47,"line":48},[45,5449,3293],{"class":58},[45,5451,5452],{"class":396},"10",[45,5454,2465],{"class":58},[45,5456,3300],{"class":396},[45,5458,2465],{"class":58},[45,5460,1471],{"class":396},[45,5462,3308],{"class":58},[45,5464,4890],{"class":72},[45,5466,5467],{"class":58},"()                     ",[45,5469,5470],{"class":51},"\u002F\u002F [1, 10, 2] ✗ 默认转字符串逐字符比较\n",[45,5472,5473,5475,5477,5479,5481,5483,5485,5487,5489,5491,5493,5495,5497,5499,5501,5503,5505,5508],{"class":47,"line":55},[45,5474,3293],{"class":58},[45,5476,5452],{"class":396},[45,5478,2465],{"class":58},[45,5480,3300],{"class":396},[45,5482,2465],{"class":58},[45,5484,1471],{"class":396},[45,5486,3308],{"class":58},[45,5488,4890],{"class":72},[45,5490,5024],{"class":58},[45,5492,3039],{"class":1523},[45,5494,2465],{"class":58},[45,5496,3044],{"class":1523},[45,5498,1627],{"class":58},[45,5500,435],{"class":392},[45,5502,1413],{"class":58},[45,5504,2090],{"class":392},[45,5506,5507],{"class":58}," b)      ",[45,5509,5510],{"class":51},"\u002F\u002F [1, 2, 10] ✓ 升序\n",[45,5512,5513,5515,5517,5519,5521,5523,5525,5527,5529,5531,5533,5535,5537,5539,5541,5543,5545,5548],{"class":47,"line":95},[45,5514,3293],{"class":58},[45,5516,5452],{"class":396},[45,5518,2465],{"class":58},[45,5520,3300],{"class":396},[45,5522,2465],{"class":58},[45,5524,1471],{"class":396},[45,5526,3308],{"class":58},[45,5528,4890],{"class":72},[45,5530,5024],{"class":58},[45,5532,3039],{"class":1523},[45,5534,2465],{"class":58},[45,5536,3044],{"class":1523},[45,5538,1627],{"class":58},[45,5540,435],{"class":392},[45,5542,1425],{"class":58},[45,5544,2090],{"class":392},[45,5546,5547],{"class":58}," a)      ",[45,5549,5550],{"class":51},"\u002F\u002F [10, 2, 1] ✓ 降序\n",[45,5552,5553],{"class":47,"line":102},[45,5554,99],{"emptyLinePlaceholder":98},[45,5556,5557,5559,5562,5564,5566,5568,5571,5573,5575,5577,5579,5581,5583,5585,5587,5589,5592],{"class":47,"line":108},[45,5558,393],{"class":392},[45,5560,5561],{"class":396}," sorted",[45,5563,400],{"class":392},[45,5565,2459],{"class":58},[45,5567,2995],{"class":392},[45,5569,5570],{"class":58},"nums].",[45,5572,4890],{"class":72},[45,5574,5024],{"class":58},[45,5576,3039],{"class":1523},[45,5578,2465],{"class":58},[45,5580,3044],{"class":1523},[45,5582,1627],{"class":58},[45,5584,435],{"class":392},[45,5586,1413],{"class":58},[45,5588,2090],{"class":392},[45,5590,5591],{"class":58}," b)   ",[45,5593,5594],{"class":51},"\u002F\u002F 想要新数组：先浅拷贝再排\n",[15,5596,5597,5599,5600,5603,5604,5607,5608,5611,5612,1852,5615,5618],{},[31,5598,4890],{}," 是这张表里",[22,5601,5602],{},"唯一改原数组","的方法（",[31,5605,5606],{},"reverse"," 同理），所以复制一份再排序是最常见的写法：",[31,5609,5610],{},"[...nums]"," 是展开成新数组（浅拷贝），",[31,5613,5614],{},"Array.from(nums)",[31,5616,5617],{},"nums.slice()"," 等价。注意「浅」——数组元素若是对象，拷贝后两边的对象仍是同一批。",[27,5620,5621],{"id":5621},"什么时候不用函数式三件套",[15,5623,5624,5625,5627,5628,5630],{},"需要 ",[31,5626,2279],{},"、需要「边遍历边改原数组」、或者逻辑里有多个相互牵扯的状态时，老老实实写 ",[31,5629,2417],{},"。方法链是给「数据管道」用的，不是用来炫技的。",[10,5632,5634],{"id":5633},"十对象属性的读写遍历与复制","十、对象：属性的读写、遍历与复制",[15,5636,5637,5638,566,5640,5643,5644,566,5646,5649],{},"对象是「键值对的集合」，用来给一组相关的数据一个名字：",[31,5639,569],{},[31,5641,5642],{},"user.age"," 显然比 ",[31,5645,1106],{},[31,5647,5648],{},"userAge"," 两个散装变量好维护。",[27,5651,5653],{"id":5652},"字面量读写与点方括号","字面量、读写与点\u002F方括号",[36,5655,5657],{"className":383,"code":5656,"language":385,"meta":41,"style":41},"const user = {\n  name: '阿蓝',\n  age: 28,\n  'favorite-lang': 'JavaScript',   \u002F\u002F 键名含连字符：写字面量时必须加引号\n  greet() { return '你好' }        \u002F\u002F 方法简写（等价于 greet: function () {}）\n}\n\nuser.name              \u002F\u002F '阿蓝'，点访问：键名是合法标识符时用\nuser['favorite-lang']  \u002F\u002F 方括号访问：键名是变量或是非法标识符时必须用\nconst key = 'age'\nuser[key]              \u002F\u002F 28 —— 方括号里放表达式\n\nuser.city = '杭州'      \u002F\u002F 新增或改写\ndelete user.city       \u002F\u002F 删除（少用；改成置 null 或重建对象更清晰）\n'age' in user          \u002F\u002F true，判断键是否存在\nuser.email ?? '未填'    \u002F\u002F 读可能不存在的键 → undefined，用 ?? 兜底\n",[31,5658,5659,5670,5680,5689,5705,5724,5728,5732,5740,5754,5765,5773,5777,5790,5802,5816],{"__ignoreMap":41},[45,5660,5661,5663,5665,5667],{"class":47,"line":48},[45,5662,393],{"class":392},[45,5664,782],{"class":396},[45,5666,400],{"class":392},[45,5668,5669],{"class":58}," {\n",[45,5671,5672,5675,5677],{"class":47,"line":55},[45,5673,5674],{"class":58},"  name: ",[45,5676,790],{"class":79},[45,5678,5679],{"class":58},",\n",[45,5681,5682,5685,5687],{"class":47,"line":95},[45,5683,5684],{"class":58},"  age: ",[45,5686,2446],{"class":396},[45,5688,5679],{"class":58},[45,5690,5691,5694,5697,5699,5702],{"class":47,"line":102},[45,5692,5693],{"class":79},"  'favorite-lang'",[45,5695,5696],{"class":58},": ",[45,5698,4298],{"class":79},[45,5700,5701],{"class":58},",   ",[45,5703,5704],{"class":51},"\u002F\u002F 键名含连字符：写字面量时必须加引号\n",[45,5706,5707,5710,5713,5715,5718,5721],{"class":47,"line":108},[45,5708,5709],{"class":72},"  greet",[45,5711,5712],{"class":58},"() { ",[45,5714,583],{"class":392},[45,5716,5717],{"class":79}," '你好'",[45,5719,5720],{"class":58}," }        ",[45,5722,5723],{"class":51},"\u002F\u002F 方法简写（等价于 greet: function () {}）\n",[45,5725,5726],{"class":47,"line":137},[45,5727,910],{"class":58},[45,5729,5730],{"class":47,"line":142},[45,5731,99],{"emptyLinePlaceholder":98},[45,5733,5734,5737],{"class":47,"line":148},[45,5735,5736],{"class":58},"user.name              ",[45,5738,5739],{"class":51},"\u002F\u002F '阿蓝'，点访问：键名是合法标识符时用\n",[45,5741,5742,5745,5748,5751],{"class":47,"line":178},[45,5743,5744],{"class":58},"user[",[45,5746,5747],{"class":79},"'favorite-lang'",[45,5749,5750],{"class":58},"]  ",[45,5752,5753],{"class":51},"\u002F\u002F 方括号访问：键名是变量或是非法标识符时必须用\n",[45,5755,5756,5758,5760,5762],{"class":47,"line":183},[45,5757,393],{"class":392},[45,5759,2389],{"class":396},[45,5761,400],{"class":392},[45,5763,5764],{"class":79}," 'age'\n",[45,5766,5767,5770],{"class":47,"line":189},[45,5768,5769],{"class":58},"user[key]              ",[45,5771,5772],{"class":51},"\u002F\u002F 28 —— 方括号里放表达式\n",[45,5774,5775],{"class":47,"line":1538},[45,5776,99],{"emptyLinePlaceholder":98},[45,5778,5779,5782,5784,5787],{"class":47,"line":1547},[45,5780,5781],{"class":58},"user.city ",[45,5783,76],{"class":392},[45,5785,5786],{"class":79}," '杭州'",[45,5788,5789],{"class":51},"      \u002F\u002F 新增或改写\n",[45,5791,5793,5796,5799],{"class":47,"line":5792},14,[45,5794,5795],{"class":392},"delete",[45,5797,5798],{"class":58}," user.city       ",[45,5800,5801],{"class":51},"\u002F\u002F 删除（少用；改成置 null 或重建对象更清晰）\n",[45,5803,5805,5808,5810,5813],{"class":47,"line":5804},15,[45,5806,5807],{"class":79},"'age'",[45,5809,2392],{"class":392},[45,5811,5812],{"class":58}," user          ",[45,5814,5815],{"class":51},"\u002F\u002F true，判断键是否存在\n",[45,5817,5819,5822,5824,5827],{"class":47,"line":5818},16,[45,5820,5821],{"class":58},"user.email ",[45,5823,1942],{"class":392},[45,5825,5826],{"class":79}," '未填'",[45,5828,5829],{"class":51},"    \u002F\u002F 读可能不存在的键 → undefined，用 ?? 兜底\n",[27,5831,5832],{"id":5832},"属性的简写与解构",[36,5834,5836],{"className":383,"code":5835,"language":385,"meta":41,"style":41},"const name = '阿蓝', age = 28\nconst user = { name, age }        \u002F\u002F 属性简写：变量名即键名\n\nconst { name: nick, age = 18 } = user   \u002F\u002F 解构：取出并改名、给默认值\nconst { name, ...rest } = user          \u002F\u002F 剩余属性收成新对象（浅拷贝）\n",[31,5837,5838,5858,5872,5876,5909],{"__ignoreMap":41},[45,5839,5840,5842,5844,5846,5848,5850,5853,5855],{"class":47,"line":48},[45,5841,393],{"class":392},[45,5843,1898],{"class":396},[45,5845,400],{"class":392},[45,5847,3418],{"class":79},[45,5849,2465],{"class":58},[45,5851,5852],{"class":396},"age",[45,5854,400],{"class":392},[45,5856,5857],{"class":396}," 28\n",[45,5859,5860,5862,5864,5866,5869],{"class":47,"line":55},[45,5861,393],{"class":392},[45,5863,782],{"class":396},[45,5865,400],{"class":392},[45,5867,5868],{"class":58}," { name, age }        ",[45,5870,5871],{"class":51},"\u002F\u002F 属性简写：变量名即键名\n",[45,5873,5874],{"class":47,"line":95},[45,5875,99],{"emptyLinePlaceholder":98},[45,5877,5878,5880,5882,5884,5886,5889,5891,5893,5895,5898,5901,5903,5906],{"class":47,"line":102},[45,5879,393],{"class":392},[45,5881,2803],{"class":58},[45,5883,2877],{"class":1523},[45,5885,5696],{"class":58},[45,5887,5888],{"class":396},"nick",[45,5890,2465],{"class":58},[45,5892,5852],{"class":396},[45,5894,400],{"class":392},[45,5896,5897],{"class":396}," 18",[45,5899,5900],{"class":58}," } ",[45,5902,76],{"class":392},[45,5904,5905],{"class":58}," user   ",[45,5907,5908],{"class":51},"\u002F\u002F 解构：取出并改名、给默认值\n",[45,5910,5911,5913,5915,5917,5919,5921,5924,5926,5928,5930],{"class":47,"line":108},[45,5912,393],{"class":392},[45,5914,2803],{"class":58},[45,5916,2877],{"class":396},[45,5918,2465],{"class":58},[45,5920,2995],{"class":392},[45,5922,5923],{"class":396},"rest",[45,5925,5900],{"class":58},[45,5927,76],{"class":392},[45,5929,5812],{"class":58},[45,5931,5932],{"class":51},"\u002F\u002F 剩余属性收成新对象（浅拷贝）\n",[15,5934,5935,5936,5939,5940,5943],{},"解构可以嵌套（",[31,5937,5938],{},"const { profile: { city } } = user","），函数参数位置也常用：",[31,5941,5942],{},"function render({ title, date = '今天' }) { … }","——调用方传对象，顺序无关、可省略可读性好。",[27,5945,5946],{"id":5946},"遍历与键值对",[36,5948,5950],{"className":383,"code":5949,"language":385,"meta":41,"style":41},"Object.keys(user)      \u002F\u002F ['name', 'age', 'favorite-lang'] 键数组\nObject.values(user)    \u002F\u002F 值数组\nObject.entries(user)   \u002F\u002F [[键, 值], …]，配合 for...of 或 Object.fromEntries\nObject.assign({}, user)      \u002F\u002F 浅拷贝合并（老写法）\nconst clone = { ...user }    \u002F\u002F 展开浅拷贝，等价且更常用\n{ ...a, ...b }               \u002F\u002F 合并：同名键后者覆盖前者\n",[31,5951,5952,5966,5979,5991,6004,6023],{"__ignoreMap":41},[45,5953,5954,5957,5960,5963],{"class":47,"line":48},[45,5955,5956],{"class":58},"Object.",[45,5958,5959],{"class":72},"keys",[45,5961,5962],{"class":58},"(user)      ",[45,5964,5965],{"class":51},"\u002F\u002F ['name', 'age', 'favorite-lang'] 键数组\n",[45,5967,5968,5970,5973,5976],{"class":47,"line":55},[45,5969,5956],{"class":58},[45,5971,5972],{"class":72},"values",[45,5974,5975],{"class":58},"(user)    ",[45,5977,5978],{"class":51},"\u002F\u002F 值数组\n",[45,5980,5981,5983,5985,5988],{"class":47,"line":95},[45,5982,5956],{"class":58},[45,5984,2480],{"class":72},[45,5986,5987],{"class":58},"(user)   ",[45,5989,5990],{"class":51},"\u002F\u002F [[键, 值], …]，配合 for...of 或 Object.fromEntries\n",[45,5992,5993,5995,5998,6001],{"class":47,"line":102},[45,5994,5956],{"class":58},[45,5996,5997],{"class":72},"assign",[45,5999,6000],{"class":58},"({}, user)      ",[45,6002,6003],{"class":51},"\u002F\u002F 浅拷贝合并（老写法）\n",[45,6005,6006,6008,6011,6013,6015,6017,6020],{"class":47,"line":108},[45,6007,393],{"class":392},[45,6009,6010],{"class":396}," clone",[45,6012,400],{"class":392},[45,6014,2803],{"class":58},[45,6016,2995],{"class":392},[45,6018,6019],{"class":58},"user }    ",[45,6021,6022],{"class":51},"\u002F\u002F 展开浅拷贝，等价且更常用\n",[45,6024,6025,6028,6030,6033,6035,6038],{"class":47,"line":137},[45,6026,6027],{"class":58},"{ ",[45,6029,2995],{"class":392},[45,6031,6032],{"class":58},"a, ",[45,6034,2995],{"class":392},[45,6036,6037],{"class":58},"b }               ",[45,6039,6040],{"class":51},"\u002F\u002F 合并：同名键后者覆盖前者\n",[15,6042,6043,6044,6049,6050,6052,6053,354],{},"遍历对象的位置回忆第五章：",[22,6045,6046,6048],{},[31,6047,2417],{}," 不能直接用在对象上","（对象不是可迭代的），要 ",[31,6051,2423],{}," 转成数组再 ",[31,6054,2417],{},[27,6056,6057],{"id":6057},"引用语义的三个现实后果",[15,6059,6060],{},"第三章说过「对象传引用」，落到日常就是三件事：",[36,6062,6064],{"className":383,"code":6063,"language":385,"meta":41,"style":41},"const a = { n: 1 }\nconst b = a          \u002F\u002F 别名：不是拷贝！\nb.n = 2              \u002F\u002F a.n 也变成 2\n\nconst c = { ...a }   \u002F\u002F 浅拷贝：第一层分开，嵌套对象仍共享\na.inner === c.inner  \u002F\u002F 若 a 有嵌套对象，这是 true\n\nJSON.parse(JSON.stringify(obj))   \u002F\u002F 深拷贝的「穷人版」：丢函数、丢 undefined、日期变字符串\n",[31,6065,6066,6081,6096,6108,6112,6131,6144,6148],{"__ignoreMap":41},[45,6067,6068,6070,6073,6075,6077,6079],{"class":47,"line":48},[45,6069,393],{"class":392},[45,6071,6072],{"class":396}," a",[45,6074,400],{"class":392},[45,6076,1468],{"class":58},[45,6078,1471],{"class":396},[45,6080,793],{"class":58},[45,6082,6083,6085,6088,6090,6093],{"class":47,"line":55},[45,6084,393],{"class":392},[45,6086,6087],{"class":396}," b",[45,6089,400],{"class":392},[45,6091,6092],{"class":58}," a          ",[45,6094,6095],{"class":51},"\u002F\u002F 别名：不是拷贝！\n",[45,6097,6098,6101,6103,6105],{"class":47,"line":95},[45,6099,6100],{"class":58},"b.n ",[45,6102,76],{"class":392},[45,6104,2644],{"class":396},[45,6106,6107],{"class":51},"              \u002F\u002F a.n 也变成 2\n",[45,6109,6110],{"class":47,"line":102},[45,6111,99],{"emptyLinePlaceholder":98},[45,6113,6114,6116,6119,6121,6123,6125,6128],{"class":47,"line":108},[45,6115,393],{"class":392},[45,6117,6118],{"class":396}," c",[45,6120,400],{"class":392},[45,6122,2803],{"class":58},[45,6124,2995],{"class":392},[45,6126,6127],{"class":58},"a }   ",[45,6129,6130],{"class":51},"\u002F\u002F 浅拷贝：第一层分开，嵌套对象仍共享\n",[45,6132,6133,6136,6138,6141],{"class":47,"line":137},[45,6134,6135],{"class":58},"a.inner ",[45,6137,1562],{"class":392},[45,6139,6140],{"class":58}," c.inner  ",[45,6142,6143],{"class":51},"\u002F\u002F 若 a 有嵌套对象，这是 true\n",[45,6145,6146],{"class":47,"line":142},[45,6147,99],{"emptyLinePlaceholder":98},[45,6149,6150,6153,6155,6158,6160,6162,6164,6167,6170],{"class":47,"line":148},[45,6151,6152],{"class":396},"JSON",[45,6154,3943],{"class":58},[45,6156,6157],{"class":72},"parse",[45,6159,409],{"class":58},[45,6161,6152],{"class":396},[45,6163,3943],{"class":58},[45,6165,6166],{"class":72},"stringify",[45,6168,6169],{"class":58},"(obj))   ",[45,6171,6172],{"class":51},"\u002F\u002F 深拷贝的「穷人版」：丢函数、丢 undefined、日期变字符串\n",[15,6174,6175],{},"以及「不可变更新」的习惯：改状态时别原地改，创建一个改好的新对象。这一习惯对后面 DOM\u002F框架更新（以及本站 Vue 代码）至关重要：",[36,6177,6179],{"className":383,"code":6178,"language":385,"meta":41,"style":41},"\u002F\u002F 原地改：谁引用了它都会被牵连\nuser.age = 29\n\n\u002F\u002F 不可变更新：生成新对象，旧对象保持原样\nconst nextUser = { ...user, age: 29 }\n",[31,6180,6181,6186,6196,6200,6205],{"__ignoreMap":41},[45,6182,6183],{"class":47,"line":48},[45,6184,6185],{"class":51},"\u002F\u002F 原地改：谁引用了它都会被牵连\n",[45,6187,6188,6191,6193],{"class":47,"line":55},[45,6189,6190],{"class":58},"user.age ",[45,6192,76],{"class":392},[45,6194,6195],{"class":396}," 29\n",[45,6197,6198],{"class":47,"line":95},[45,6199,99],{"emptyLinePlaceholder":98},[45,6201,6202],{"class":47,"line":102},[45,6203,6204],{"class":51},"\u002F\u002F 不可变更新：生成新对象，旧对象保持原样\n",[45,6206,6207,6209,6212,6214,6216,6218,6221,6224],{"class":47,"line":108},[45,6208,393],{"class":392},[45,6210,6211],{"class":396}," nextUser",[45,6213,400],{"class":392},[45,6215,2803],{"class":58},[45,6217,2995],{"class":392},[45,6219,6220],{"class":58},"user, age: ",[45,6222,6223],{"class":396},"29",[45,6225,793],{"class":58},[356,6227],{"height":2854,"src":6228,"title":6229},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F10-objects.html","示例 10：对象的读写、解构与不可变更新",[27,6231,6233],{"id":6232},"什么时候用对象什么时候用-map","什么时候用对象、什么时候用 Map",[15,6235,6236,6237,6240,6241,6244,6245,1103],{},"键都是",[22,6238,6239],{},"字符串\u002F符号","、结构固定（像一个「记录」）→ 对象字面量；键是",[22,6242,6243],{},"动态的、任意类型的","，或频繁增删 → ",[31,6246,6247],{},"Map",[36,6249,6251],{"className":383,"code":6250,"language":385,"meta":41,"style":41},"const cache = new Map()\ncache.set('k', 1).set(otherObj, 2)   \u002F\u002F 键可以是对象，且清楚知道自己有几项\ncache.get('k')       \u002F\u002F 1\ncache.has(otherObj)  \u002F\u002F true\n",[31,6252,6253,6270,6302,6318],{"__ignoreMap":41},[45,6254,6255,6257,6260,6262,6265,6268],{"class":47,"line":48},[45,6256,393],{"class":392},[45,6258,6259],{"class":396}," cache",[45,6261,400],{"class":392},[45,6263,6264],{"class":392}," new",[45,6266,6267],{"class":72}," Map",[45,6269,3477],{"class":58},[45,6271,6272,6275,6278,6280,6283,6285,6287,6290,6292,6295,6297,6299],{"class":47,"line":55},[45,6273,6274],{"class":58},"cache.",[45,6276,6277],{"class":72},"set",[45,6279,409],{"class":58},[45,6281,6282],{"class":79},"'k'",[45,6284,2465],{"class":58},[45,6286,1471],{"class":396},[45,6288,6289],{"class":58},").",[45,6291,6277],{"class":72},[45,6293,6294],{"class":58},"(otherObj, ",[45,6296,3300],{"class":396},[45,6298,472],{"class":58},[45,6300,6301],{"class":51},"\u002F\u002F 键可以是对象，且清楚知道自己有几项\n",[45,6303,6304,6306,6309,6311,6313,6316],{"class":47,"line":95},[45,6305,6274],{"class":58},[45,6307,6308],{"class":72},"get",[45,6310,409],{"class":58},[45,6312,6282],{"class":79},[45,6314,6315],{"class":58},")       ",[45,6317,3600],{"class":51},[45,6319,6320,6322,6324,6327],{"class":47,"line":102},[45,6321,6274],{"class":58},[45,6323,3343],{"class":72},[45,6325,6326],{"class":58},"(otherObj)  ",[45,6328,6329],{"class":51},"\u002F\u002F true\n",[15,6331,6332,6334,6335,6338,6339,6342,6343,6346],{},[31,6333,6247],{}," 还保证",[22,6336,6337],{},"插入顺序","、有直接的 ",[31,6340,6341],{},"size"," 属性；对象要数数量得 ",[31,6344,6345],{},"Object.keys(obj).length","。日常优先对象，遇到「以对象为键」或大吞吐增删再换 Map。",[10,6348,6350],{"id":6349},"十一class把同类对象的模板写成代码","十一、class：把「同类对象」的模板写成代码",[15,6352,6353,6354,6357,6358,6360,6361,6364],{},"对象字面量适合",[22,6355,6356],{},"单件","数据；如果同样结构的对象要反复创建、还要带一批方法，每次都手写字面量就是重复粘贴。这时用 ",[31,6359,1150],{},"：先画一张图纸（类），再按图纸批量生产（实例）。",[31,6362,6363],{},"new User('阿蓝', 28)"," 读作「按 User 的图纸造一个阿蓝」。",[27,6366,6368],{"id":6367},"constructor-与-new实例是怎么诞生的","constructor 与 new：实例是怎么诞生的",[36,6370,6372],{"className":383,"code":6371,"language":385,"meta":41,"style":41},"class User {\n  constructor(name, age) {\n    this.name = name   \u002F\u002F this 指向正在被创建的那个实例\n    this.age = age\n  }\n  greet() {\n    return `你好，我是 ${this.name}`   \u002F\u002F 方法省略 function 关键字\n  }\n}\n\nconst alice = new User('阿蓝', 28)\nalice.greet()   \u002F\u002F '你好，我是阿蓝'\n",[31,6373,6374,6383,6398,6414,6426,6430,6436,6454,6458,6462,6466,6489],{"__ignoreMap":41},[45,6375,6376,6378,6381],{"class":47,"line":48},[45,6377,1150],{"class":392},[45,6379,6380],{"class":72}," User",[45,6382,5669],{"class":58},[45,6384,6385,6388,6390,6392,6394,6396],{"class":47,"line":55},[45,6386,6387],{"class":392},"  constructor",[45,6389,409],{"class":58},[45,6391,2877],{"class":1523},[45,6393,2465],{"class":58},[45,6395,5852],{"class":1523},[45,6397,879],{"class":58},[45,6399,6400,6403,6406,6408,6411],{"class":47,"line":95},[45,6401,6402],{"class":396},"    this",[45,6404,6405],{"class":58},".name ",[45,6407,76],{"class":392},[45,6409,6410],{"class":58}," name   ",[45,6412,6413],{"class":51},"\u002F\u002F this 指向正在被创建的那个实例\n",[45,6415,6416,6418,6421,6423],{"class":47,"line":102},[45,6417,6402],{"class":396},[45,6419,6420],{"class":58},".age ",[45,6422,76],{"class":392},[45,6424,6425],{"class":58}," age\n",[45,6427,6428],{"class":47,"line":108},[45,6429,3469],{"class":58},[45,6431,6432,6434],{"class":47,"line":137},[45,6433,5709],{"class":72},[45,6435,3405],{"class":58},[45,6437,6438,6440,6443,6445,6447,6449,6451],{"class":47,"line":142},[45,6439,3561],{"class":392},[45,6441,6442],{"class":79}," `你好，我是 ${",[45,6444,2697],{"class":396},[45,6446,3943],{"class":79},[45,6448,2877],{"class":58},[45,6450,5410],{"class":79},[45,6452,6453],{"class":51},"   \u002F\u002F 方法省略 function 关键字\n",[45,6455,6456],{"class":47,"line":148},[45,6457,3469],{"class":58},[45,6459,6460],{"class":47,"line":178},[45,6461,910],{"class":58},[45,6463,6464],{"class":47,"line":183},[45,6465,99],{"emptyLinePlaceholder":98},[45,6467,6468,6470,6473,6475,6477,6479,6481,6483,6485,6487],{"class":47,"line":189},[45,6469,393],{"class":392},[45,6471,6472],{"class":396}," alice",[45,6474,400],{"class":392},[45,6476,6264],{"class":392},[45,6478,6380],{"class":72},[45,6480,409],{"class":58},[45,6482,790],{"class":79},[45,6484,2465],{"class":58},[45,6486,2446],{"class":396},[45,6488,415],{"class":58},[45,6490,6491,6494,6496,6498],{"class":47,"line":1538},[45,6492,6493],{"class":58},"alice.",[45,6495,2036],{"class":72},[45,6497,3597],{"class":58},[45,6499,6500],{"class":51},"\u002F\u002F '你好，我是阿蓝'\n",[15,6502,6503,6505,6506,6509,6510,6512,6513,6516,6517,6519],{},[31,6504,3225],{}," 做四件事：创建一个空对象 → 把它的原型指向 ",[31,6507,6508],{},"User.prototype"," → 以这个对象为 ",[31,6511,2697],{}," 执行 ",[31,6514,6515],{},"constructor"," → 返回它。",[31,6518,6515],{}," 里放「每个实例各自不同」的数据（名字、年龄）；方法写在类体里，由所有实例共享。",[27,6521,6523],{"id":6522},"方法在原型上实例只装数据","方法在原型上：实例只装数据",[36,6525,6527],{"className":383,"code":6526,"language":385,"meta":41,"style":41},"Object.keys(alice)                             \u002F\u002F ['name', 'age']——实例自己的属性\nalice.hasOwnProperty('greet')                  \u002F\u002F false——greet 不在实例自己身上\n'greet' in alice                               \u002F\u002F true——顺着原型链找得到\nObject.getPrototypeOf(alice) === User.prototype  \u002F\u002F true\nObject.getOwnPropertyNames(User.prototype)     \u002F\u002F ['constructor', 'greet']\n",[31,6528,6529,6541,6558,6570,6592],{"__ignoreMap":41},[45,6530,6531,6533,6535,6538],{"class":47,"line":48},[45,6532,5956],{"class":58},[45,6534,5959],{"class":72},[45,6536,6537],{"class":58},"(alice)                             ",[45,6539,6540],{"class":51},"\u002F\u002F ['name', 'age']——实例自己的属性\n",[45,6542,6543,6545,6548,6550,6553,6555],{"class":47,"line":55},[45,6544,6493],{"class":58},[45,6546,6547],{"class":72},"hasOwnProperty",[45,6549,409],{"class":58},[45,6551,6552],{"class":79},"'greet'",[45,6554,5163],{"class":58},[45,6556,6557],{"class":51},"\u002F\u002F false——greet 不在实例自己身上\n",[45,6559,6560,6562,6564,6567],{"class":47,"line":95},[45,6561,6552],{"class":79},[45,6563,2392],{"class":392},[45,6565,6566],{"class":58}," alice                               ",[45,6568,6569],{"class":51},"\u002F\u002F true——顺着原型链找得到\n",[45,6571,6572,6574,6577,6580,6582,6584,6586,6589],{"class":47,"line":102},[45,6573,5956],{"class":58},[45,6575,6576],{"class":72},"getPrototypeOf",[45,6578,6579],{"class":58},"(alice) ",[45,6581,1562],{"class":392},[45,6583,6380],{"class":396},[45,6585,3943],{"class":58},[45,6587,6588],{"class":396},"prototype",[45,6590,6591],{"class":51},"  \u002F\u002F true\n",[45,6593,6594,6596,6599,6601,6604,6606,6608,6610],{"class":47,"line":108},[45,6595,5956],{"class":58},[45,6597,6598],{"class":72},"getOwnPropertyNames",[45,6600,409],{"class":58},[45,6602,6603],{"class":396},"User",[45,6605,3943],{"class":58},[45,6607,6588],{"class":396},[45,6609,5012],{"class":58},[45,6611,6612],{"class":51},"\u002F\u002F ['constructor', 'greet']\n",[15,6614,6615,6616,6619,6620,6622,6623,6626],{},"读 ",[31,6617,6618],{},"alice.greet"," 时，引擎先看实例自己有没有，没有就顺着 ",[31,6621,6508],{}," 找——找到才执行。这条「实例 → 类原型 → 父类原型 → Object.prototype → null」的查找路径就是",[22,6624,6625],{},"原型链","。方法只存一份放在原型上，而不是每个实例复制一份，这是它省内存的原因。",[27,6628,6630],{"id":6629},"getter把算出来的值伪装成属性","getter：把「算出来的值」伪装成属性",[36,6632,6634],{"className":383,"code":6633,"language":385,"meta":41,"style":41},"class User {\n  \u002F\u002F …constructor 同上\n  get profile() {\n    return `${this.name}（${this.age} 岁）`\n  }\n}\n\nalice.profile   \u002F\u002F '阿蓝（28 岁）'——写法像读属性，实际执行函数\n",[31,6635,6636,6644,6649,6659,6683,6687,6691,6695],{"__ignoreMap":41},[45,6637,6638,6640,6642],{"class":47,"line":48},[45,6639,1150],{"class":392},[45,6641,6380],{"class":72},[45,6643,5669],{"class":58},[45,6645,6646],{"class":47,"line":55},[45,6647,6648],{"class":51},"  \u002F\u002F …constructor 同上\n",[45,6650,6651,6654,6657],{"class":47,"line":95},[45,6652,6653],{"class":392},"  get",[45,6655,6656],{"class":72}," profile",[45,6658,3405],{"class":58},[45,6660,6661,6663,6665,6667,6669,6671,6674,6676,6678,6680],{"class":47,"line":102},[45,6662,3561],{"class":392},[45,6664,5392],{"class":79},[45,6666,2697],{"class":396},[45,6668,3943],{"class":79},[45,6670,2877],{"class":58},[45,6672,6673],{"class":79},"}（${",[45,6675,2697],{"class":396},[45,6677,3943],{"class":79},[45,6679,5852],{"class":58},[45,6681,6682],{"class":79},"} 岁）`\n",[45,6684,6685],{"class":47,"line":108},[45,6686,3469],{"class":58},[45,6688,6689],{"class":47,"line":137},[45,6690,910],{"class":58},[45,6692,6693],{"class":47,"line":142},[45,6694,99],{"emptyLinePlaceholder":98},[45,6696,6697,6700],{"class":47,"line":148},[45,6698,6699],{"class":58},"alice.profile   ",[45,6701,6702],{"class":51},"\u002F\u002F '阿蓝（28 岁）'——写法像读属性，实际执行函数\n",[15,6704,6705,6706,6709,6710,6713],{},"getter 适合",[22,6707,6708],{},"由已有数据派生","的值：不存副本、每次读取实时计算，也就不会出现「改了 name 却忘了同步副本」的不一致。setter（",[31,6711,6712],{},"set profile(v) { … }","）同理，把一次赋值变成校验或转换的入口。",[27,6715,6717,6718,6721],{"id":6716},"私有字段-开头","私有字段：",[31,6719,6720],{},"#"," 开头",[15,6723,6724,6725,6727],{},"第七章用闭包实现过「外部改不了」的私有状态，语言的现代方案是 ",[31,6726,6720],{}," 字段：",[36,6729,6731],{"className":383,"code":6730,"language":385,"meta":41,"style":41},"class Counter {\n  #count = 0                    \u002F\u002F 私有字段：# 开头\n  increment() { return ++this.#count }\n}\n\nconst c = new Counter()\nc.increment()   \u002F\u002F 1\nc.count         \u002F\u002F undefined——外部读不到 #count\nc.#count        \u002F\u002F 语法错误：类外部根本写不出这个表达式\n",[31,6732,6733,6742,6754,6771,6775,6779,6793,6805,6813],{"__ignoreMap":41},[45,6734,6735,6737,6740],{"class":47,"line":48},[45,6736,1150],{"class":392},[45,6738,6739],{"class":72}," Counter",[45,6741,5669],{"class":58},[45,6743,6744,6747,6749,6751],{"class":47,"line":55},[45,6745,6746],{"class":1523},"  #count",[45,6748,400],{"class":392},[45,6750,991],{"class":396},[45,6752,6753],{"class":51},"                    \u002F\u002F 私有字段：# 开头\n",[45,6755,6756,6759,6761,6763,6766,6768],{"class":47,"line":95},[45,6757,6758],{"class":72},"  increment",[45,6760,5712],{"class":58},[45,6762,583],{"class":392},[45,6764,6765],{"class":392}," ++",[45,6767,2697],{"class":396},[45,6769,6770],{"class":58},".#count }\n",[45,6772,6773],{"class":47,"line":102},[45,6774,910],{"class":58},[45,6776,6777],{"class":47,"line":108},[45,6778,99],{"emptyLinePlaceholder":98},[45,6780,6781,6783,6785,6787,6789,6791],{"class":47,"line":137},[45,6782,393],{"class":392},[45,6784,6118],{"class":396},[45,6786,400],{"class":392},[45,6788,6264],{"class":392},[45,6790,6739],{"class":72},[45,6792,3477],{"class":58},[45,6794,6795,6798,6801,6803],{"class":47,"line":142},[45,6796,6797],{"class":58},"c.",[45,6799,6800],{"class":72},"increment",[45,6802,3597],{"class":58},[45,6804,3600],{"class":51},[45,6806,6807,6810],{"class":47,"line":148},[45,6808,6809],{"class":58},"c.count         ",[45,6811,6812],{"class":51},"\u002F\u002F undefined——外部读不到 #count\n",[45,6814,6815,6818],{"class":47,"line":178},[45,6816,6817],{"class":58},"c.#count        ",[45,6819,6820],{"class":51},"\u002F\u002F 语法错误：类外部根本写不出这个表达式\n",[15,6822,6823,6825],{},[31,6824,6720],{}," 不是命名约定，是语法层面的封闭：外部看不见、改不动，只有类自己的方法能碰。余额、口令这类「绝不外泄」的数据就用它包装。",[27,6827,6829],{"id":6828},"static属于类整体不属于实例的成员","static：属于类整体、不属于实例的成员",[36,6831,6833],{"className":383,"code":6832,"language":385,"meta":41,"style":41},"class User {\n  static population = 0   \u002F\u002F 静态字段\n  constructor(name) {\n    this.name = name\n    User.population++\n  }\n}\n\nUser.population   \u002F\u002F 3——问类要，不是问实例要\nalice.population  \u002F\u002F undefined——实例身上没有\n",[31,6834,6835,6843,6858,6868,6879,6887,6891,6895,6899,6907],{"__ignoreMap":41},[45,6836,6837,6839,6841],{"class":47,"line":48},[45,6838,1150],{"class":392},[45,6840,6380],{"class":72},[45,6842,5669],{"class":58},[45,6844,6845,6848,6851,6853,6855],{"class":47,"line":55},[45,6846,6847],{"class":392},"  static",[45,6849,6850],{"class":1523}," population",[45,6852,400],{"class":392},[45,6854,991],{"class":396},[45,6856,6857],{"class":51},"   \u002F\u002F 静态字段\n",[45,6859,6860,6862,6864,6866],{"class":47,"line":95},[45,6861,6387],{"class":392},[45,6863,409],{"class":58},[45,6865,2877],{"class":1523},[45,6867,879],{"class":58},[45,6869,6870,6872,6874,6876],{"class":47,"line":102},[45,6871,6402],{"class":396},[45,6873,6405],{"class":58},[45,6875,76],{"class":392},[45,6877,6878],{"class":58}," name\n",[45,6880,6881,6884],{"class":47,"line":108},[45,6882,6883],{"class":58},"    User.population",[45,6885,6886],{"class":392},"++\n",[45,6888,6889],{"class":47,"line":137},[45,6890,3469],{"class":58},[45,6892,6893],{"class":47,"line":142},[45,6894,910],{"class":58},[45,6896,6897],{"class":47,"line":148},[45,6898,99],{"emptyLinePlaceholder":98},[45,6900,6901,6904],{"class":47,"line":178},[45,6902,6903],{"class":58},"User.population   ",[45,6905,6906],{"class":51},"\u002F\u002F 3——问类要，不是问实例要\n",[45,6908,6909,6912],{"class":47,"line":183},[45,6910,6911],{"class":58},"alice.population  ",[45,6913,6914],{"class":51},"\u002F\u002F undefined——实例身上没有\n",[15,6916,6917,6918,6921,6922,6925,6926,6929,6930,3226],{},"static 描述",[22,6919,6920],{},"类整体","的状态或工具：实例计数、",[31,6923,6924],{},"User.fromJSON()"," 这样的工厂方法、配置常量。判断标准：这件事跟某一个实例有关（",[31,6927,6928],{},"alice.greet()","），还是跟类整体有关（",[31,6931,6932],{},"User.population",[27,6934,6936],{"id":6935},"继承extends-与-super","继承：extends 与 super",[36,6938,6940],{"className":383,"code":6939,"language":385,"meta":41,"style":41},"class Admin extends User {\n  constructor(name, age, level) {\n    super(name, age)          \u002F\u002F 先调父类 constructor，把父类那半建好\n    this.level = level        \u002F\u002F 再补子类独有的属性\n  }\n  greet() {\n    return `${super.greet()}，也是管理员`   \u002F\u002F 覆盖父类方法，同时保留父类实现\n  }\n}\n\nconst admin = new Admin('青梧', 30, 3)\nadmin instanceof Admin   \u002F\u002F true\nadmin instanceof User    \u002F\u002F true——原型链把两代连在一起\nalice instanceof Admin   \u002F\u002F false——普通用户不是管理员\n",[31,6941,6942,6956,6975,6986,7001,7005,7011,7033,7037,7041,7045,7074,7087,7098],{"__ignoreMap":41},[45,6943,6944,6946,6949,6952,6954],{"class":47,"line":48},[45,6945,1150],{"class":392},[45,6947,6948],{"class":72}," Admin",[45,6950,6951],{"class":392}," extends",[45,6953,6380],{"class":72},[45,6955,5669],{"class":58},[45,6957,6958,6960,6962,6964,6966,6968,6970,6973],{"class":47,"line":55},[45,6959,6387],{"class":392},[45,6961,409],{"class":58},[45,6963,2877],{"class":1523},[45,6965,2465],{"class":58},[45,6967,5852],{"class":1523},[45,6969,2465],{"class":58},[45,6971,6972],{"class":1523},"level",[45,6974,879],{"class":58},[45,6976,6977,6980,6983],{"class":47,"line":95},[45,6978,6979],{"class":396},"    super",[45,6981,6982],{"class":58},"(name, age)          ",[45,6984,6985],{"class":51},"\u002F\u002F 先调父类 constructor，把父类那半建好\n",[45,6987,6988,6990,6993,6995,6998],{"class":47,"line":102},[45,6989,6402],{"class":396},[45,6991,6992],{"class":58},".level ",[45,6994,76],{"class":392},[45,6996,6997],{"class":58}," level        ",[45,6999,7000],{"class":51},"\u002F\u002F 再补子类独有的属性\n",[45,7002,7003],{"class":47,"line":108},[45,7004,3469],{"class":58},[45,7006,7007,7009],{"class":47,"line":137},[45,7008,5709],{"class":72},[45,7010,3405],{"class":58},[45,7012,7013,7015,7017,7020,7022,7024,7027,7030],{"class":47,"line":142},[45,7014,3561],{"class":392},[45,7016,5392],{"class":79},[45,7018,7019],{"class":396},"super",[45,7021,3943],{"class":79},[45,7023,2036],{"class":72},[45,7025,7026],{"class":79},"()",[45,7028,7029],{"class":79},"}，也是管理员`",[45,7031,7032],{"class":51},"   \u002F\u002F 覆盖父类方法，同时保留父类实现\n",[45,7034,7035],{"class":47,"line":148},[45,7036,3469],{"class":58},[45,7038,7039],{"class":47,"line":178},[45,7040,910],{"class":58},[45,7042,7043],{"class":47,"line":183},[45,7044,99],{"emptyLinePlaceholder":98},[45,7046,7047,7049,7052,7054,7056,7058,7060,7063,7065,7068,7070,7072],{"class":47,"line":189},[45,7048,393],{"class":392},[45,7050,7051],{"class":396}," admin",[45,7053,400],{"class":392},[45,7055,6264],{"class":392},[45,7057,6948],{"class":72},[45,7059,409],{"class":58},[45,7061,7062],{"class":79},"'青梧'",[45,7064,2465],{"class":58},[45,7066,7067],{"class":396},"30",[45,7069,2465],{"class":58},[45,7071,3305],{"class":396},[45,7073,415],{"class":58},[45,7075,7076,7079,7082,7084],{"class":47,"line":1538},[45,7077,7078],{"class":58},"admin ",[45,7080,7081],{"class":392},"instanceof",[45,7083,6948],{"class":72},[45,7085,7086],{"class":51},"   \u002F\u002F true\n",[45,7088,7089,7091,7093,7095],{"class":47,"line":1547},[45,7090,7078],{"class":58},[45,7092,7081],{"class":392},[45,7094,6380],{"class":72},[45,7096,7097],{"class":51},"    \u002F\u002F true——原型链把两代连在一起\n",[45,7099,7100,7103,7105,7107],{"class":47,"line":5792},[45,7101,7102],{"class":58},"alice ",[45,7104,7081],{"class":392},[45,7106,6948],{"class":72},[45,7108,7109],{"class":51},"   \u002F\u002F false——普通用户不是管理员\n",[15,7111,7112,7113,7122],{},"子类 constructor 里有一条硬规则：",[22,7114,7115,7116,7119,7120],{},"必须先 ",[31,7117,7118],{},"super()"," 才能使用 ",[31,7121,2697],{},"——父类那半没建好之前，this 还不存在。子类不写 constructor 时，会自动调用父类 constructor。",[27,7124,7126],{"id":7125},"什么时候用-class","什么时候用 class",[1094,7128,7129,7136,7142],{},[1097,7130,7131,7132,7135],{},"要",[22,7133,7134],{},"批量制造","结构相同、自带行为的对象（用户、订单、游戏角色）→ class",[1097,7137,7138,7139,7141],{},"只有一个存数据的",[22,7140,6356],{},"对象 → 对象字面量就够，不必包一层类",[1097,7143,7144],{},"一堆互相无状态的纯计算 → 普通函数或模块组织更合适",[15,7146,7147,7148,7150],{},"判断标准不是「高级不高级」，而是「有没有实例这回事」。没有 ",[31,7149,3225],{}," 的冲动，就用不着 class。",[356,7152],{"height":2283,"src":7153,"title":7154},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F11-class.html","示例 11：class、实例与原型链",[10,7156,7158],{"id":7157},"十二错误处理错了以后怎么办","十二、错误处理：错了以后怎么办",[15,7160,7161],{},"程序出错不可怕，怕的是出错后现场消失：页面白屏、按钮没反应，控制台一行红字，然后呢？错误处理就是给「出问题时」提前写好剧本——哪一步可能失败、失败后给什么替代结果、哪些错必须留证据给人看。",[27,7163,7165],{"id":7164},"错误是对象error-自带现场信息","错误是对象：Error 自带现场信息",[36,7167,7169],{"className":383,"code":7168,"language":385,"meta":41,"style":41},"try {\n  JSON.parse('{oops}')\n} catch (err) {\n  err.name      \u002F\u002F 'SyntaxError'\n  err.message   \u002F\u002F 'Unexpected token o in JSON…'（措辞各浏览器略有差异）\n  err.stack     \u002F\u002F 从出错点出发的完整调用栈快照\n}\n",[31,7170,7171,7178,7194,7205,7213,7221,7229],{"__ignoreMap":41},[45,7172,7173,7176],{"class":47,"line":48},[45,7174,7175],{"class":392},"try",[45,7177,5669],{"class":58},[45,7179,7180,7183,7185,7187,7189,7192],{"class":47,"line":55},[45,7181,7182],{"class":396},"  JSON",[45,7184,3943],{"class":58},[45,7186,6157],{"class":72},[45,7188,409],{"class":58},[45,7190,7191],{"class":79},"'{oops}'",[45,7193,415],{"class":58},[45,7195,7196,7199,7202],{"class":47,"line":95},[45,7197,7198],{"class":58},"} ",[45,7200,7201],{"class":392},"catch",[45,7203,7204],{"class":58}," (err) {\n",[45,7206,7207,7210],{"class":47,"line":102},[45,7208,7209],{"class":58},"  err.name      ",[45,7211,7212],{"class":51},"\u002F\u002F 'SyntaxError'\n",[45,7214,7215,7218],{"class":47,"line":108},[45,7216,7217],{"class":58},"  err.message   ",[45,7219,7220],{"class":51},"\u002F\u002F 'Unexpected token o in JSON…'（措辞各浏览器略有差异）\n",[45,7222,7223,7226],{"class":47,"line":137},[45,7224,7225],{"class":58},"  err.stack     ",[45,7227,7228],{"class":51},"\u002F\u002F 从出错点出发的完整调用栈快照\n",[45,7230,7231],{"class":47,"line":142},[45,7232,910],{"class":58},[15,7234,7235,7236,7238,7239,7242,7243,7246],{},"try 块里任意一句抛错，本块剩下的都不执行，立刻跳到 catch；catch 跑完，程序从整个 try\u002Fcatch 之后继续。",[31,7237,2877],{}," 是错误类型，",[31,7240,7241],{},"message"," 是人话描述，",[31,7244,7245],{},"stack"," 是调试时最有价值的部分——顺着它能找到谁一路调用到这个出错点。",[27,7248,7250],{"id":7249},"throw主动把错误扔出去","throw：主动把错误扔出去",[36,7252,7254],{"className":383,"code":7253,"language":385,"meta":41,"style":41},"function divide(a, b) {\n  if (b === 0) throw new RangeError('除数不能为 0')\n  return a \u002F b\n}\n",[31,7255,7256,7272,7299,7310],{"__ignoreMap":41},[45,7257,7258,7260,7262,7264,7266,7268,7270],{"class":47,"line":48},[45,7259,1515],{"class":392},[45,7261,3034],{"class":72},[45,7263,409],{"class":58},[45,7265,3039],{"class":1523},[45,7267,2465],{"class":58},[45,7269,3044],{"class":1523},[45,7271,879],{"class":58},[45,7273,7274,7276,7278,7280,7282,7284,7287,7289,7292,7294,7297],{"class":47,"line":55},[45,7275,2171],{"class":392},[45,7277,3053],{"class":58},[45,7279,1562],{"class":392},[45,7281,991],{"class":396},[45,7283,1627],{"class":58},[45,7285,7286],{"class":392},"throw",[45,7288,6264],{"class":392},[45,7290,7291],{"class":72}," RangeError",[45,7293,409],{"class":58},[45,7295,7296],{"class":79},"'除数不能为 0'",[45,7298,415],{"class":58},[45,7300,7301,7303,7305,7307],{"class":47,"line":95},[45,7302,2209],{"class":392},[45,7304,1413],{"class":58},[45,7306,2096],{"class":392},[45,7308,7309],{"class":58}," b\n",[45,7311,7312],{"class":47,"line":102},[45,7313,910],{"class":58},[15,7315,7316,7318,7319,7322,7323,7326],{},[31,7317,7286],{}," 后面可以跟任何值，但",[22,7320,7321],{},"永远抛 Error 实例","：只有它自带 name \u002F message \u002F stack，能被工具链识别。抛裸字符串（",[31,7324,7325],{},"throw '出错啦'","）会让 catch 方无从下手。",[27,7328,7330],{"id":7329},"try-catch-finally完整形态","try \u002F catch \u002F finally：完整形态",[36,7332,7334],{"className":383,"code":7333,"language":385,"meta":41,"style":41},"function readSetting(key) {\n  let text\n  try {\n    text = localStorage.getItem(key)\n    return text === null ? null : JSON.parse(text)\n  } catch (err) {\n    console.warn('读设置失败，用默认值', err.message)\n    return null\n  } finally {\n    console.log('无论成败都会执行')   \u002F\u002F 释放资源、收尾的工作放这里\n  }\n}\n",[31,7335,7336,7349,7356,7363,7379,7407,7416,7431,7438,7447,7463,7467],{"__ignoreMap":41},[45,7337,7338,7340,7343,7345,7347],{"class":47,"line":48},[45,7339,1515],{"class":392},[45,7341,7342],{"class":72}," readSetting",[45,7344,409],{"class":58},[45,7346,2462],{"class":1523},[45,7348,879],{"class":58},[45,7350,7351,7353],{"class":47,"line":55},[45,7352,884],{"class":392},[45,7354,7355],{"class":58}," text\n",[45,7357,7358,7361],{"class":47,"line":95},[45,7359,7360],{"class":392},"  try",[45,7362,5669],{"class":58},[45,7364,7365,7368,7370,7373,7376],{"class":47,"line":102},[45,7366,7367],{"class":58},"    text ",[45,7369,76],{"class":392},[45,7371,7372],{"class":58}," localStorage.",[45,7374,7375],{"class":72},"getItem",[45,7377,7378],{"class":58},"(key)\n",[45,7380,7381,7383,7386,7388,7391,7393,7395,7397,7400,7402,7404],{"class":47,"line":108},[45,7382,3561],{"class":392},[45,7384,7385],{"class":58}," text ",[45,7387,1562],{"class":392},[45,7389,7390],{"class":396}," null",[45,7392,2220],{"class":392},[45,7394,7390],{"class":396},[45,7396,2226],{"class":392},[45,7398,7399],{"class":396}," JSON",[45,7401,3943],{"class":58},[45,7403,6157],{"class":72},[45,7405,7406],{"class":58},"(text)\n",[45,7408,7409,7412,7414],{"class":47,"line":137},[45,7410,7411],{"class":58},"  } ",[45,7413,7201],{"class":392},[45,7415,7204],{"class":58},[45,7417,7418,7420,7423,7425,7428],{"class":47,"line":142},[45,7419,3456],{"class":58},[45,7421,7422],{"class":72},"warn",[45,7424,409],{"class":58},[45,7426,7427],{"class":79},"'读设置失败，用默认值'",[45,7429,7430],{"class":58},", err.message)\n",[45,7432,7433,7435],{"class":47,"line":148},[45,7434,3561],{"class":392},[45,7436,7437],{"class":396}," null\n",[45,7439,7440,7442,7445],{"class":47,"line":178},[45,7441,7411],{"class":58},[45,7443,7444],{"class":392},"finally",[45,7446,5669],{"class":58},[45,7448,7449,7451,7453,7455,7458,7460],{"class":47,"line":183},[45,7450,3456],{"class":58},[45,7452,441],{"class":72},[45,7454,409],{"class":58},[45,7456,7457],{"class":79},"'无论成败都会执行'",[45,7459,472],{"class":58},[45,7461,7462],{"class":51},"\u002F\u002F 释放资源、收尾的工作放这里\n",[45,7464,7465],{"class":47,"line":189},[45,7466,3469],{"class":58},[45,7468,7469],{"class":47,"line":1538},[45,7470,910],{"class":58},[15,7472,7473,7474,7477],{},"finally 的特别之处：",[22,7475,7476],{},"即使 try 里写了 return，它也先执行完","（返回值先被暂存，finally 跑完才真正返回）。连接、文件句柄这类「用完必须关」的东西放 finally 里。",[27,7479,7480],{"id":7480},"内置错误类型与自定义错误",[15,7482,7483,7484,7487,7488,7490,7491,7494,7495,7498,7499,7502,7503,7506],{},"常见类型：",[31,7485,7486],{},"TypeError","（对 null 读属性、调用了非函数）、",[31,7489,950],{},"（变量不存在）、",[31,7492,7493],{},"RangeError","（数值越界）、",[31,7496,7497],{},"SyntaxError","。要区分两种情况：",[22,7500,7501],{},"代码本体","的语法错误在解析阶段就挂了，try\u002Fcatch 根本拦不到；而 ",[31,7504,7505],{},"JSON.parse"," 收到坏数据时抛的 SyntaxError 是运行时的，能拦。",[15,7508,7509],{},"业务错误则继承 Error 自己定义：",[36,7511,7513],{"className":383,"code":7512,"language":385,"meta":41,"style":41},"class ValidationError extends Error {\n  constructor(field, message) {\n    super(message)                 \u002F\u002F Error 负责装好 message \u002F stack\n    this.name = 'ValidationError'  \u002F\u002F 覆盖默认的 'Error'\n    this.field = field             \u002F\u002F 自定义信息：出错的是哪个字段\n  }\n}\n\ntry {\n  validate(input)\n} catch (err) {\n  if (err instanceof ValidationError) {\n    showFieldError(err.field, err.message)   \u002F\u002F 认识的错误 → 就地处理\n  } else {\n    throw err                                \u002F\u002F 不认识的 → 原样再抛\n  }\n}\n",[31,7514,7515,7529,7544,7554,7568,7583,7587,7591,7595,7601,7609,7617,7630,7641,7650,7661,7665],{"__ignoreMap":41},[45,7516,7517,7519,7522,7524,7527],{"class":47,"line":48},[45,7518,1150],{"class":392},[45,7520,7521],{"class":72}," ValidationError",[45,7523,6951],{"class":392},[45,7525,7526],{"class":72}," Error",[45,7528,5669],{"class":58},[45,7530,7531,7533,7535,7538,7540,7542],{"class":47,"line":55},[45,7532,6387],{"class":392},[45,7534,409],{"class":58},[45,7536,7537],{"class":1523},"field",[45,7539,2465],{"class":58},[45,7541,7241],{"class":1523},[45,7543,879],{"class":58},[45,7545,7546,7548,7551],{"class":47,"line":95},[45,7547,6979],{"class":396},[45,7549,7550],{"class":58},"(message)                 ",[45,7552,7553],{"class":51},"\u002F\u002F Error 负责装好 message \u002F stack\n",[45,7555,7556,7558,7560,7562,7565],{"class":47,"line":102},[45,7557,6402],{"class":396},[45,7559,6405],{"class":58},[45,7561,76],{"class":392},[45,7563,7564],{"class":79}," 'ValidationError'",[45,7566,7567],{"class":51},"  \u002F\u002F 覆盖默认的 'Error'\n",[45,7569,7570,7572,7575,7577,7580],{"class":47,"line":108},[45,7571,6402],{"class":396},[45,7573,7574],{"class":58},".field ",[45,7576,76],{"class":392},[45,7578,7579],{"class":58}," field             ",[45,7581,7582],{"class":51},"\u002F\u002F 自定义信息：出错的是哪个字段\n",[45,7584,7585],{"class":47,"line":137},[45,7586,3469],{"class":58},[45,7588,7589],{"class":47,"line":142},[45,7590,910],{"class":58},[45,7592,7593],{"class":47,"line":148},[45,7594,99],{"emptyLinePlaceholder":98},[45,7596,7597,7599],{"class":47,"line":178},[45,7598,7175],{"class":392},[45,7600,5669],{"class":58},[45,7602,7603,7606],{"class":47,"line":183},[45,7604,7605],{"class":72},"  validate",[45,7607,7608],{"class":58},"(input)\n",[45,7610,7611,7613,7615],{"class":47,"line":189},[45,7612,7198],{"class":58},[45,7614,7201],{"class":392},[45,7616,7204],{"class":58},[45,7618,7619,7621,7624,7626,7628],{"class":47,"line":1538},[45,7620,2171],{"class":392},[45,7622,7623],{"class":58}," (err ",[45,7625,7081],{"class":392},[45,7627,7521],{"class":72},[45,7629,879],{"class":58},[45,7631,7632,7635,7638],{"class":47,"line":1547},[45,7633,7634],{"class":72},"    showFieldError",[45,7636,7637],{"class":58},"(err.field, err.message)   ",[45,7639,7640],{"class":51},"\u002F\u002F 认识的错误 → 就地处理\n",[45,7642,7643,7645,7648],{"class":47,"line":5792},[45,7644,7411],{"class":58},[45,7646,7647],{"class":392},"else",[45,7649,5669],{"class":58},[45,7651,7652,7655,7658],{"class":47,"line":5804},[45,7653,7654],{"class":392},"    throw",[45,7656,7657],{"class":58}," err                                ",[45,7659,7660],{"class":51},"\u002F\u002F 不认识的 → 原样再抛\n",[45,7662,7663],{"class":47,"line":5818},[45,7664,3469],{"class":58},[45,7666,7668],{"class":47,"line":7667},17,[45,7669,910],{"class":58},[15,7671,7672,7673,7675],{},"自定义错误的价值在于「抛出方和捕获方有了约定」：",[31,7674,7081],{}," 一判，字段名、错误文案都在，提示就能精确落到那一个输入框上。",[27,7677,7679],{"id":7678},"捕获的分寸只处理你懂的错","捕获的分寸：只处理你懂的错",[1094,7681,7682,7688,7697,7706],{},[1097,7683,7684,7687],{},[22,7685,7686],{},"认识的错误就处理","：给用户一个能走下去的结果（默认值、字段提示）",[1097,7689,7690,7696],{},[22,7691,7692,7693],{},"不认识的错误原样 ",[31,7694,7695],{},"throw err","：里面可能藏着程序 bug，别假装没事",[1097,7698,7699,1103,7702,7705],{},[22,7700,7701],{},"绝不空 catch",[31,7703,7704],{},"catch {}"," 把现场直接抹掉，是日后最难查的 bug 来源",[1097,7707,7708,7709,7712],{},"能提前判断的别拿异常当流程控制：",[31,7710,7711],{},"if (b === 0) return null"," 比 try\u002Fcatch 更直白，异常留给「意外」",[356,7714],{"height":7715,"src":7716,"title":7717},700,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F12-errors.html","示例 12：try \u002F catch \u002F throw 与自定义错误",[10,7719,7721],{"id":7720},"十三dom-查询怎么在页面里找到元素","十三、DOM 查询：怎么在页面里找到元素",[15,7723,7724],{},"第一章讲过浏览器把 HTML 解析成 DOM 树，从这一章开始 JavaScript 正式接管这棵树。第一步是「找到元素」——找不到，后面什么都改不了。",[27,7726,7728],{"id":7727},"两个主力queryselector-与-queryselectorall","两个主力：querySelector 与 querySelectorAll",[36,7730,7732],{"className":383,"code":7731,"language":385,"meta":41,"style":41},"document.querySelector('.card')       \u002F\u002F 第一个匹配 → 元素；找不到 → null\ndocument.querySelectorAll('.card')    \u002F\u002F 全部匹配 → NodeList；找不到 → 空集合\ndocument.getElementById('staff-list') \u002F\u002F 按 id 直取\n",[31,7733,7734,7751,7767],{"__ignoreMap":41},[45,7735,7736,7739,7741,7743,7746,7748],{"class":47,"line":48},[45,7737,7738],{"class":58},"document.",[45,7740,406],{"class":72},[45,7742,409],{"class":58},[45,7744,7745],{"class":79},"'.card'",[45,7747,6315],{"class":58},[45,7749,7750],{"class":51},"\u002F\u002F 第一个匹配 → 元素；找不到 → null\n",[45,7752,7753,7755,7758,7760,7762,7764],{"class":47,"line":55},[45,7754,7738],{"class":58},[45,7756,7757],{"class":72},"querySelectorAll",[45,7759,409],{"class":58},[45,7761,7745],{"class":79},[45,7763,4397],{"class":58},[45,7765,7766],{"class":51},"\u002F\u002F 全部匹配 → NodeList；找不到 → 空集合\n",[45,7768,7769,7771,7774,7776,7779,7781],{"class":47,"line":95},[45,7770,7738],{"class":58},[45,7772,7773],{"class":72},"getElementById",[45,7775,409],{"class":58},[45,7777,7778],{"class":79},"'staff-list'",[45,7780,1627],{"class":58},[45,7782,7783],{"class":51},"\u002F\u002F 按 id 直取\n",[15,7785,7786,7787,566,7790,566,7793,566,7796,7799,7800,7803],{},"括号里写的就是 CSS 教程里那套选择器，原样搬来：",[31,7788,7789],{},".tag",[31,7791,7792],{},"#list",[31,7794,7795],{},"li.card",[31,7797,7798],{},"[data-role=\"backend\"]","，还能组合 ",[31,7801,7802],{},"ul > li:first-child","。CSS 学过的选择器知识在这里全额复用。",[15,7805,7806],{},"两个高频要点：",[1094,7808,7809,7823],{},[1097,7810,7811,7813,7814,7816,7817,7820,7821],{},[31,7812,406],{}," 找不到返回 ",[31,7815,349],{},"——习惯写法是先判空再操作（",[31,7818,7819],{},"if (!el) return","），或者随手 ",[31,7822,2014],{},[1097,7824,7825,7827,7828,7831,7832,7835,7836,7838,7839,1267,7841,7843,7844],{},[31,7826,7757],{}," 返回的 ",[22,7829,7830],{},"NodeList 不是数组","：有 ",[31,7833,7834],{},"forEach","、能 ",[31,7837,2417],{},"，但没有 ",[31,7840,2975],{},[31,7842,4842],{},"；要加工先 ",[31,7845,7846],{},"Array.from()",[27,7848,7850],{"id":7849},"顺着关系走别只会从-document-全局查","顺着关系走，别只会从 document 全局查",[36,7852,7854],{"className":383,"code":7853,"language":385,"meta":41,"style":41},"const list = document.getElementById('staff-list')\nlist.children.length         \u002F\u002F 子元素个数（只算元素，不含文本节点）\nlist.firstElementChild       \u002F\u002F 第一个子元素\ncard.parentElement           \u002F\u002F 父元素\ncard.nextElementSibling      \u002F\u002F 下一个兄弟元素\n",[31,7855,7856,7874,7884,7892,7900],{"__ignoreMap":41},[45,7857,7858,7860,7862,7864,7866,7868,7870,7872],{"class":47,"line":48},[45,7859,393],{"class":392},[45,7861,4547],{"class":396},[45,7863,400],{"class":392},[45,7865,403],{"class":58},[45,7867,7773],{"class":72},[45,7869,409],{"class":58},[45,7871,7778],{"class":79},[45,7873,415],{"class":58},[45,7875,7876,7879,7881],{"class":47,"line":55},[45,7877,7878],{"class":58},"list.children.",[45,7880,2317],{"class":396},[45,7882,7883],{"class":51},"         \u002F\u002F 子元素个数（只算元素，不含文本节点）\n",[45,7885,7886,7889],{"class":47,"line":95},[45,7887,7888],{"class":58},"list.firstElementChild       ",[45,7890,7891],{"class":51},"\u002F\u002F 第一个子元素\n",[45,7893,7894,7897],{"class":47,"line":102},[45,7895,7896],{"class":58},"card.parentElement           ",[45,7898,7899],{"class":51},"\u002F\u002F 父元素\n",[45,7901,7902,7905],{"class":47,"line":108},[45,7903,7904],{"class":58},"card.nextElementSibling      ",[45,7906,7907],{"class":51},"\u002F\u002F 下一个兄弟元素\n",[15,7909,7910,7911,7914],{},"从一个大容器出发逐层缩小范围，比从 ",[31,7912,7913],{},"document"," 全查更准（页面别处有同名 class 也不会串）也更快。",[27,7916,7918],{"id":7917},"closest-与-matches向上找与自我判断","closest 与 matches：向上找与自我判断",[36,7920,7922],{"className":383,"code":7921,"language":385,"meta":41,"style":41},"span.closest('.card')       \u002F\u002F 从自己出发逐层向上，返回第一个匹配的祖先；找不到 → null\ncard.matches('.highlight')  \u002F\u002F 我匹配这个选择器吗 → true \u002F false\n",[31,7923,7924,7941],{"__ignoreMap":41},[45,7925,7926,7929,7932,7934,7936,7938],{"class":47,"line":48},[45,7927,7928],{"class":58},"span.",[45,7930,7931],{"class":72},"closest",[45,7933,409],{"class":58},[45,7935,7745],{"class":79},[45,7937,6315],{"class":58},[45,7939,7940],{"class":51},"\u002F\u002F 从自己出发逐层向上，返回第一个匹配的祖先；找不到 → null\n",[45,7942,7943,7946,7949,7951,7954,7956],{"class":47,"line":55},[45,7944,7945],{"class":58},"card.",[45,7947,7948],{"class":72},"matches",[45,7950,409],{"class":58},[45,7952,7953],{"class":79},"'.highlight'",[45,7955,4814],{"class":58},[45,7957,7958],{"class":51},"\u002F\u002F 我匹配这个选择器吗 → true \u002F false\n",[15,7960,7961,7963,7964,7967],{},[31,7962,7931],{}," 是第十五章「事件委托」的核心工具：不管点中卡片内部多深的小元素，",[31,7965,7966],{},"closest('.card')"," 总能把你带回那张卡。",[27,7969,7971],{"id":7970},"live-集合与静态快照别记混","live 集合与静态快照，别记混",[36,7973,7975],{"className":383,"code":7974,"language":385,"meta":41,"style":41},"const live = list.getElementsByClassName('card')  \u002F\u002F live：DOM 变了它跟着变\nconst snap = list.querySelectorAll('.card')       \u002F\u002F 静态快照：查询那一刻定死\n",[31,7976,7977,8001],{"__ignoreMap":41},[45,7978,7979,7981,7984,7986,7988,7991,7993,7996,7998],{"class":47,"line":48},[45,7980,393],{"class":392},[45,7982,7983],{"class":396}," live",[45,7985,400],{"class":392},[45,7987,4610],{"class":58},[45,7989,7990],{"class":72},"getElementsByClassName",[45,7992,409],{"class":58},[45,7994,7995],{"class":79},"'card'",[45,7997,4814],{"class":58},[45,7999,8000],{"class":51},"\u002F\u002F live：DOM 变了它跟着变\n",[45,8002,8003,8005,8008,8010,8012,8014,8016,8018,8020],{"class":47,"line":55},[45,8004,393],{"class":392},[45,8006,8007],{"class":396}," snap",[45,8009,400],{"class":392},[45,8011,4610],{"class":58},[45,8013,7757],{"class":72},[45,8015,409],{"class":58},[45,8017,7745],{"class":79},[45,8019,6315],{"class":58},[45,8021,8022],{"class":51},"\u002F\u002F 静态快照：查询那一刻定死\n",[15,8024,8025,8026,1267,8028,8031,8032,8035,8036,4821,8038,8041,8042,8044],{},"老接口（",[31,8027,7990],{},[31,8029,8030],{},"getElementsByTagName","）返回",[22,8033,8034],{},"实时联动","的 HTMLCollection，",[31,8037,7757],{},[22,8039,8040],{},"定格快照","的 NodeList。示例里追加一张卡片就能看到两种行为分道扬镳。日常首选 ",[31,8043,7757],{},"，行为可预期；需要最新结果重新查询一次就好。",[356,8046],{"height":2283,"src":8047,"title":8048},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F13-dom-select.html","示例 13：DOM 查询——找到页面上的元素",[10,8050,8052],{"id":8051},"十四修改-dom怎么把页面改出内容","十四、修改 DOM：怎么把页面改出内容",[15,8054,8055,8056,354],{},"找到元素之后就是「改」。手段按重要性排序：创建与挂载、改内容、改状态、删除，最后组装成日常最常用的结构——",[22,8057,8058],{},"渲染函数",[27,8060,8062],{"id":8061},"创建拼装挂载三步造一个节点","创建、拼装、挂载：三步造一个节点",[36,8064,8066],{"className":383,"code":8065,"language":385,"meta":41,"style":41},"const li = document.createElement('li')   \u002F\u002F ① 造：游离在内存，还没上树\nli.className = 'item'\nli.textContent = '写周报'\n\nconst list = document.querySelector('#todo-list')\nlist.append(li)      \u002F\u002F ② 挂到子节点末尾；prepend 挂到开头\n",[31,8067,8068,8092,8102,8112,8116,8135],{"__ignoreMap":41},[45,8069,8070,8072,8075,8077,8079,8082,8084,8087,8089],{"class":47,"line":48},[45,8071,393],{"class":392},[45,8073,8074],{"class":396}," li",[45,8076,400],{"class":392},[45,8078,403],{"class":58},[45,8080,8081],{"class":72},"createElement",[45,8083,409],{"class":58},[45,8085,8086],{"class":79},"'li'",[45,8088,472],{"class":58},[45,8090,8091],{"class":51},"\u002F\u002F ① 造：游离在内存，还没上树\n",[45,8093,8094,8097,8099],{"class":47,"line":55},[45,8095,8096],{"class":58},"li.className ",[45,8098,76],{"class":392},[45,8100,8101],{"class":79}," 'item'\n",[45,8103,8104,8107,8109],{"class":47,"line":95},[45,8105,8106],{"class":58},"li.textContent ",[45,8108,76],{"class":392},[45,8110,8111],{"class":79}," '写周报'\n",[45,8113,8114],{"class":47,"line":102},[45,8115,99],{"emptyLinePlaceholder":98},[45,8117,8118,8120,8122,8124,8126,8128,8130,8133],{"class":47,"line":108},[45,8119,393],{"class":392},[45,8121,4547],{"class":396},[45,8123,400],{"class":392},[45,8125,403],{"class":58},[45,8127,406],{"class":72},[45,8129,409],{"class":58},[45,8131,8132],{"class":79},"'#todo-list'",[45,8134,415],{"class":58},[45,8136,8137,8139,8142,8145],{"class":47,"line":137},[45,8138,4572],{"class":58},[45,8140,8141],{"class":72},"append",[45,8143,8144],{"class":58},"(li)      ",[45,8146,8147],{"class":51},"\u002F\u002F ② 挂到子节点末尾；prepend 挂到开头\n",[15,8149,8150,8151,1267,8154,8157,8158,8161,8162,8165,8166,354],{},"先在内存里把子树拼装好、最后一步挂树，只触发一次布局；边挂边补会反复触发，规模一大就卡。插入全家桶还有 ",[31,8152,8153],{},"before",[31,8155,8156],{},"after","（自己的前\u002F后）、",[31,8159,8160],{},"replaceWith","；删除用 ",[31,8163,8164],{},"el.remove()","；清空子节点用 ",[31,8167,8168],{},"list.replaceChildren()",[27,8170,8172],{"id":8171},"改内容textcontent-与-innerhtml-的分界","改内容：textContent 与 innerHTML 的分界",[36,8174,8176],{"className":383,"code":8175,"language":385,"meta":41,"style":41},"el.textContent = '\u003Cb>加粗\u003C\u002Fb>'   \u002F\u002F 原样当纯文本：页面上就是这串字符\nel.innerHTML  = '\u003Cb>加粗\u003C\u002Fb>'    \u002F\u002F 交给浏览器解析：真的变成加粗文字\n",[31,8177,8178,8191],{"__ignoreMap":41},[45,8179,8180,8183,8185,8188],{"class":47,"line":48},[45,8181,8182],{"class":58},"el.textContent ",[45,8184,76],{"class":392},[45,8186,8187],{"class":79}," '\u003Cb>加粗\u003C\u002Fb>'",[45,8189,8190],{"class":51},"   \u002F\u002F 原样当纯文本：页面上就是这串字符\n",[45,8192,8193,8196,8198,8200],{"class":47,"line":55},[45,8194,8195],{"class":58},"el.innerHTML  ",[45,8197,76],{"class":392},[45,8199,8187],{"class":79},[45,8201,8202],{"class":51},"    \u002F\u002F 交给浏览器解析：真的变成加粗文字\n",[15,8204,8205,8206,8209,8210,8213],{},"铁律：",[22,8207,8208],{},"挂用户输入永远走 textContent","。innerHTML 只用于你完全掌控的模板字符串——评论框里塞一段 ",[31,8211,8212],{},"\u003Cimg onerror=…>","，innerHTML 会当场把脚本执行了，这就是 XSS 的入口。",[27,8215,8217],{"id":8216},"改状态classlist-与-dataset","改状态：classList 与 dataset",[36,8219,8221],{"className":383,"code":8220,"language":385,"meta":41,"style":41},"el.classList.add('done')       \u002F\u002F 加\nel.classList.remove('done')    \u002F\u002F 删\nel.classList.toggle('done')    \u002F\u002F 切换，返回最终是否含有\nel.classList.contains('done')  \u002F\u002F 查\n\nel.dataset.index = 2           \u002F\u002F 写入 → 页面上是 data-index=\"2\"\nel.dataset.index               \u002F\u002F 读回——永远是字符串\n",[31,8222,8223,8241,8257,8273,8289,8293,8305],{"__ignoreMap":41},[45,8224,8225,8228,8231,8233,8236,8238],{"class":47,"line":48},[45,8226,8227],{"class":58},"el.classList.",[45,8229,8230],{"class":72},"add",[45,8232,409],{"class":58},[45,8234,8235],{"class":79},"'done'",[45,8237,6315],{"class":58},[45,8239,8240],{"class":51},"\u002F\u002F 加\n",[45,8242,8243,8245,8248,8250,8252,8254],{"class":47,"line":55},[45,8244,8227],{"class":58},[45,8246,8247],{"class":72},"remove",[45,8249,409],{"class":58},[45,8251,8235],{"class":79},[45,8253,4397],{"class":58},[45,8255,8256],{"class":51},"\u002F\u002F 删\n",[45,8258,8259,8261,8264,8266,8268,8270],{"class":47,"line":95},[45,8260,8227],{"class":58},[45,8262,8263],{"class":72},"toggle",[45,8265,409],{"class":58},[45,8267,8235],{"class":79},[45,8269,4397],{"class":58},[45,8271,8272],{"class":51},"\u002F\u002F 切换，返回最终是否含有\n",[45,8274,8275,8277,8280,8282,8284,8286],{"class":47,"line":102},[45,8276,8227],{"class":58},[45,8278,8279],{"class":72},"contains",[45,8281,409],{"class":58},[45,8283,8235],{"class":79},[45,8285,4814],{"class":58},[45,8287,8288],{"class":51},"\u002F\u002F 查\n",[45,8290,8291],{"class":47,"line":108},[45,8292,99],{"emptyLinePlaceholder":98},[45,8294,8295,8298,8300,8302],{"class":47,"line":137},[45,8296,8297],{"class":58},"el.dataset.index ",[45,8299,76],{"class":392},[45,8301,2644],{"class":396},[45,8303,8304],{"class":51},"           \u002F\u002F 写入 → 页面上是 data-index=\"2\"\n",[45,8306,8307,8310],{"class":47,"line":142},[45,8308,8309],{"class":58},"el.dataset.index               ",[45,8311,8312],{"class":51},"\u002F\u002F 读回——永远是字符串\n",[15,8314,8315,8316,8319,8320,8323],{},"样式交给 class、附加状态记进 ",[31,8317,8318],{},"data-*","、真正的数据放 JS 变量——「三权分立」。不必用 ",[31,8321,8322],{},"el.style.xxx"," 一个个敷（除非是真正动态计算的值），换一批 class 更整洁。",[27,8325,8327],{"id":8326},"渲染函数数据-dom变了就重建","渲染函数：数据 → DOM，变了就重建",[36,8329,8331],{"className":383,"code":8330,"language":385,"meta":41,"style":41},"let tasks = [ … ]\n\nfunction renderTasks() {\n  list.replaceChildren()                 \u002F\u002F 清空\n  tasks.forEach((task, index) => {\n    const li = document.createElement('li')\n    li.dataset.index = index\n    li.textContent = task.text\n    list.append(li)\n  })\n}\n",[31,8332,8333,8345,8349,8358,8372,8395,8413,8423,8433,8443,8448],{"__ignoreMap":41},[45,8334,8335,8337,8340,8342],{"class":47,"line":48},[45,8336,611],{"class":392},[45,8338,8339],{"class":58}," tasks ",[45,8341,76],{"class":392},[45,8343,8344],{"class":58}," [ … ]\n",[45,8346,8347],{"class":47,"line":55},[45,8348,99],{"emptyLinePlaceholder":98},[45,8350,8351,8353,8356],{"class":47,"line":95},[45,8352,1515],{"class":392},[45,8354,8355],{"class":72}," renderTasks",[45,8357,3405],{"class":58},[45,8359,8360,8363,8366,8369],{"class":47,"line":102},[45,8361,8362],{"class":58},"  list.",[45,8364,8365],{"class":72},"replaceChildren",[45,8367,8368],{"class":58},"()                 ",[45,8370,8371],{"class":51},"\u002F\u002F 清空\n",[45,8373,8374,8377,8379,8381,8384,8386,8389,8391,8393],{"class":47,"line":108},[45,8375,8376],{"class":58},"  tasks.",[45,8378,7834],{"class":72},[45,8380,5024],{"class":58},[45,8382,8383],{"class":1523},"task",[45,8385,2465],{"class":58},[45,8387,8388],{"class":1523},"index",[45,8390,1627],{"class":58},[45,8392,435],{"class":392},[45,8394,5669],{"class":58},[45,8396,8397,8399,8401,8403,8405,8407,8409,8411],{"class":47,"line":137},[45,8398,3440],{"class":392},[45,8400,8074],{"class":396},[45,8402,400],{"class":392},[45,8404,403],{"class":58},[45,8406,8081],{"class":72},[45,8408,409],{"class":58},[45,8410,8086],{"class":79},[45,8412,415],{"class":58},[45,8414,8415,8418,8420],{"class":47,"line":142},[45,8416,8417],{"class":58},"    li.dataset.index ",[45,8419,76],{"class":392},[45,8421,8422],{"class":58}," index\n",[45,8424,8425,8428,8430],{"class":47,"line":148},[45,8426,8427],{"class":58},"    li.textContent ",[45,8429,76],{"class":392},[45,8431,8432],{"class":58}," task.text\n",[45,8434,8435,8438,8440],{"class":47,"line":178},[45,8436,8437],{"class":58},"    list.",[45,8439,8141],{"class":72},[45,8441,8442],{"class":58},"(li)\n",[45,8444,8445],{"class":47,"line":183},[45,8446,8447],{"class":58},"  })\n",[45,8449,8450],{"class":47,"line":189},[45,8451,910],{"class":58},[15,8453,8454,8455,8458,8459,8462,8463,8465],{},"数据变了就改数组、然后 ",[31,8456,8457],{},"renderTasks()"," 整块重建。看着「笨」（全删重造），换来一个关键好处：",[22,8460,8461],{},"任何时刻 DOM 都严格等于数据的投影","，不存在「忘了同步某处」的状态不一致——这正是 Vue 等框架的思路源头。反过来，用 ",[31,8464,8164],{}," 直接删节点、绕过数据，下一次重建它就会复活：数据才是真相源。",[27,8467,8469],{"id":8468},"事件委托预告重建后按钮还能点","事件委托预告：重建后按钮还能点",[15,8471,8472,8473,8475,8476,8479,8480,8483],{},"整块重建后，旧节点上的监听器全部作废。如果每次重建都重新绑定，代码会碎成一地 ",[31,8474,486],{},"。更省事的做法是",[22,8477,8478],{},"把监听器挂在容器上","，用 ",[31,8481,8482],{},"e.target.closest('.item')"," 判断点击落点——容器绑一次，子孙随便换。它的完整原理就是下一章。",[356,8485],{"height":7715,"src":8486,"title":8487},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F14-dom-modify.html","示例 14：修改 DOM——从 createElement 到渲染函数",[10,8489,8491],{"id":8490},"十五事件用户和页面的对话","十五、事件：用户和页面的对话",[15,8493,8494,8495,8497],{},"前面几章一直在用 ",[31,8496,486],{},"，现在把它的全貌补齐：事件对象里有什么、一次点击到底走了哪条路、为什么监听器该挂在容器上而不是每个按钮上。",[27,8499,8501],{"id":8500},"监听移除与选项","监听、移除与选项",[36,8503,8505],{"className":383,"code":8504,"language":385,"meta":41,"style":41},"const onClick = (e) => { … }\nbtn.addEventListener('click', onClick)\n\nbtn.addEventListener('click', onClick, { once: true })     \u002F\u002F 触发一次后自动移除\nbtn.addEventListener('click', onClick, { capture: true })  \u002F\u002F 在捕获阶段接\nbtn.removeEventListener('click', onClick)  \u002F\u002F 移除：必须传「同一个函数引用」\n",[31,8506,8507,8528,8541,8545,8566,8587],{"__ignoreMap":41},[45,8508,8509,8511,8514,8516,8518,8521,8523,8525],{"class":47,"line":48},[45,8510,393],{"class":392},[45,8512,8513],{"class":72}," onClick",[45,8515,400],{"class":392},[45,8517,873],{"class":58},[45,8519,8520],{"class":1523},"e",[45,8522,1627],{"class":58},[45,8524,435],{"class":392},[45,8526,8527],{"class":58}," { … }\n",[45,8529,8530,8532,8534,8536,8538],{"class":47,"line":55},[45,8531,424],{"class":58},[45,8533,486],{"class":72},[45,8535,409],{"class":58},[45,8537,491],{"class":79},[45,8539,8540],{"class":58},", onClick)\n",[45,8542,8543],{"class":47,"line":95},[45,8544,99],{"emptyLinePlaceholder":98},[45,8546,8547,8549,8551,8553,8555,8558,8560,8563],{"class":47,"line":102},[45,8548,424],{"class":58},[45,8550,486],{"class":72},[45,8552,409],{"class":58},[45,8554,491],{"class":79},[45,8556,8557],{"class":58},", onClick, { once: ",[45,8559,876],{"class":396},[45,8561,8562],{"class":58}," })     ",[45,8564,8565],{"class":51},"\u002F\u002F 触发一次后自动移除\n",[45,8567,8568,8570,8572,8574,8576,8579,8581,8584],{"class":47,"line":108},[45,8569,424],{"class":58},[45,8571,486],{"class":72},[45,8573,409],{"class":58},[45,8575,491],{"class":79},[45,8577,8578],{"class":58},", onClick, { capture: ",[45,8580,876],{"class":396},[45,8582,8583],{"class":58}," })  ",[45,8585,8586],{"class":51},"\u002F\u002F 在捕获阶段接\n",[45,8588,8589,8591,8593,8595,8597,8600],{"class":47,"line":137},[45,8590,424],{"class":58},[45,8592,546],{"class":72},[45,8594,409],{"class":58},[45,8596,491],{"class":79},[45,8598,8599],{"class":58},", onClick)  ",[45,8601,8602],{"class":51},"\u002F\u002F 移除：必须传「同一个函数引用」\n",[15,8604,8605,8606,8608],{},"移除有个高频坑：",[31,8607,546],{}," 靠函数引用识别，传一个新写的箭头函数毫无作用——处理函数要先存成变量，注册和移除传的是同一个。",[27,8610,8612],{"id":8611},"事件对象target-与-currenttarget","事件对象：target 与 currentTarget",[36,8614,8616],{"className":383,"code":8615,"language":385,"meta":41,"style":41},"list.addEventListener('click', (e) => {\n  e.target          \u002F\u002F 实际被点中的元素（可能很深，比如按钮里的文字）\n  e.currentTarget   \u002F\u002F 监听器挂着的元素（这里就是 list）\n  e.type            \u002F\u002F 'click'\n})\n",[31,8617,8618,8639,8647,8655,8663],{"__ignoreMap":41},[45,8619,8620,8622,8624,8626,8628,8631,8633,8635,8637],{"class":47,"line":48},[45,8621,4572],{"class":58},[45,8623,486],{"class":72},[45,8625,409],{"class":58},[45,8627,491],{"class":79},[45,8629,8630],{"class":58},", (",[45,8632,8520],{"class":1523},[45,8634,1627],{"class":58},[45,8636,435],{"class":392},[45,8638,5669],{"class":58},[45,8640,8641,8644],{"class":47,"line":55},[45,8642,8643],{"class":58},"  e.target          ",[45,8645,8646],{"class":51},"\u002F\u002F 实际被点中的元素（可能很深，比如按钮里的文字）\n",[45,8648,8649,8652],{"class":47,"line":95},[45,8650,8651],{"class":58},"  e.currentTarget   ",[45,8653,8654],{"class":51},"\u002F\u002F 监听器挂着的元素（这里就是 list）\n",[45,8656,8657,8660],{"class":47,"line":102},[45,8658,8659],{"class":58},"  e.type            ",[45,8661,8662],{"class":51},"\u002F\u002F 'click'\n",[45,8664,8665],{"class":47,"line":108},[45,8666,8667],{"class":58},"})\n",[15,8669,8670,8671,8674],{},"事件从 document 向下「捕获」、到达目标、再向上「冒泡」。监听器默认在冒泡阶段触发；加了 ",[31,8672,8673],{},"{ capture: true }"," 就在下行阶段触发。点内层元素时祖孙三代的监听器都会收到——示例里把这条链完整打印了一遍，对照着看一遍就不会再记混。",[27,8676,8678],{"id":8677},"事件委托一器多用","事件委托：一器多用",[15,8680,8681,8682,1103],{},"冒泡带来一个工程上的大招。列表有 100 个条目，逐个绑监听器就是 100 个函数，重建后还要全部重绑。换成",[22,8683,8684],{},"只在容器上绑一个",[36,8686,8688],{"className":383,"code":8687,"language":385,"meta":41,"style":41},"ul.addEventListener('click', (e) => {\n  const li = e.target.closest('.item')   \u002F\u002F 不管点得多深，先回到条目\n  if (!li) return\n  if (e.target.closest('.del')) { li.remove(); return }   \u002F\u002F 点中删除按钮\n  li.classList.toggle('done')                              \u002F\u002F 否则算切换完成\n})\n",[31,8689,8690,8711,8734,8748,8777,8794],{"__ignoreMap":41},[45,8691,8692,8695,8697,8699,8701,8703,8705,8707,8709],{"class":47,"line":48},[45,8693,8694],{"class":58},"ul.",[45,8696,486],{"class":72},[45,8698,409],{"class":58},[45,8700,491],{"class":79},[45,8702,8630],{"class":58},[45,8704,8520],{"class":1523},[45,8706,1627],{"class":58},[45,8708,435],{"class":392},[45,8710,5669],{"class":58},[45,8712,8713,8715,8717,8719,8722,8724,8726,8729,8731],{"class":47,"line":55},[45,8714,3410],{"class":392},[45,8716,8074],{"class":396},[45,8718,400],{"class":392},[45,8720,8721],{"class":58}," e.target.",[45,8723,7931],{"class":72},[45,8725,409],{"class":58},[45,8727,8728],{"class":79},"'.item'",[45,8730,472],{"class":58},[45,8732,8733],{"class":51},"\u002F\u002F 不管点得多深，先回到条目\n",[45,8735,8736,8738,8740,8742,8745],{"class":47,"line":95},[45,8737,2171],{"class":392},[45,8739,873],{"class":58},[45,8741,2176],{"class":392},[45,8743,8744],{"class":58},"li) ",[45,8746,8747],{"class":392},"return\n",[45,8749,8750,8752,8755,8757,8759,8762,8765,8767,8770,8772,8774],{"class":47,"line":102},[45,8751,2171],{"class":392},[45,8753,8754],{"class":58}," (e.target.",[45,8756,7931],{"class":72},[45,8758,409],{"class":58},[45,8760,8761],{"class":79},"'.del'",[45,8763,8764],{"class":58},")) { li.",[45,8766,8247],{"class":72},[45,8768,8769],{"class":58},"(); ",[45,8771,583],{"class":392},[45,8773,1532],{"class":58},[45,8775,8776],{"class":51},"\u002F\u002F 点中删除按钮\n",[45,8778,8779,8782,8784,8786,8788,8791],{"class":47,"line":108},[45,8780,8781],{"class":58},"  li.classList.",[45,8783,8263],{"class":72},[45,8785,409],{"class":58},[45,8787,8235],{"class":79},[45,8789,8790],{"class":58},")                              ",[45,8792,8793],{"class":51},"\u002F\u002F 否则算切换完成\n",[45,8795,8796],{"class":47,"line":137},[45,8797,8667],{"class":58},[15,8799,8800,8801,8804,8805,8808,8809,8812],{},"好处一次说完：",[22,8802,8803],{},"动态新增的条目自动生效","（不用补绑）、",[22,8806,8807],{},"重建后不失效","（容器没换）、",[22,8810,8811],{},"监听器数量与条目数无关","（省内存）。这也是上一章渲染函数敢「全删重造」的底气。",[27,8814,8816],{"id":8815},"preventdefault把浏览器的默认动作拦下来","preventDefault：把浏览器的默认动作拦下来",[15,8818,8819],{},"浏览器对很多事件有内置动作：点链接会跳转、提交表单会刷新页面、右键弹菜单。想接管就先拦：",[36,8821,8823],{"className":383,"code":8822,"language":385,"meta":41,"style":41},"link.addEventListener('click', (e) => {\n  e.preventDefault()   \u002F\u002F 取消默认动作\n  \u002F\u002F 校验、确认框、异步提交……自己安排\n})\n",[31,8824,8825,8846,8859,8864],{"__ignoreMap":41},[45,8826,8827,8830,8832,8834,8836,8838,8840,8842,8844],{"class":47,"line":48},[45,8828,8829],{"class":58},"link.",[45,8831,486],{"class":72},[45,8833,409],{"class":58},[45,8835,491],{"class":79},[45,8837,8630],{"class":58},[45,8839,8520],{"class":1523},[45,8841,1627],{"class":58},[45,8843,435],{"class":392},[45,8845,5669],{"class":58},[45,8847,8848,8851,8854,8856],{"class":47,"line":55},[45,8849,8850],{"class":58},"  e.",[45,8852,8853],{"class":72},"preventDefault",[45,8855,3597],{"class":58},[45,8857,8858],{"class":51},"\u002F\u002F 取消默认动作\n",[45,8860,8861],{"class":47,"line":95},[45,8862,8863],{"class":51},"  \u002F\u002F 校验、确认框、异步提交……自己安排\n",[45,8865,8866],{"class":47,"line":102},[45,8867,8667],{"class":58},[15,8869,8870,8871,8873,8874,8877,8878,8873,8881,8884],{},"两个「拦」别用混：",[31,8872,8853],{}," 拦",[22,8875,8876],{},"默认动作","（事件本身还能继续冒泡）；",[31,8879,8880],{},"stopPropagation",[22,8882,8883],{},"事件传播","（默认动作不受影响）。",[27,8886,8887],{"id":8887},"常见事件类型速查",[223,8889,8890,8903],{},[226,8891,8892],{},[229,8893,8894,8897,8900],{},[232,8895,8896],{},"事件",[232,8898,8899],{},"触发时机",[232,8901,8902],{},"常用场景",[245,8904,8905,8918,8931,8944,8957,8977,8993],{},[229,8906,8907,8912,8915],{},[250,8908,8909],{},[31,8910,8911],{},"click",[250,8913,8914],{},"点击（键盘在按钮上回车也算）",[250,8916,8917],{},"几乎所有交互",[229,8919,8920,8925,8928],{},[250,8921,8922],{},[31,8923,8924],{},"input",[250,8926,8927],{},"每次输入、粘贴、删除",[250,8929,8930],{},"实时校验、搜索建议",[229,8932,8933,8938,8941],{},[250,8934,8935],{},[31,8936,8937],{},"change",[250,8939,8940],{},"值确定改变且失焦（select \u002F checkbox 立即）",[250,8942,8943],{},"下拉选择、勾选",[229,8945,8946,8951,8954],{},[250,8947,8948],{},[31,8949,8950],{},"submit",[250,8952,8953],{},"表单提交",[250,8955,8956],{},"拦默认、校验通过再放行",[229,8958,8959,8967,8974],{},[250,8960,8961,1267,8964],{},[31,8962,8963],{},"keydown",[31,8965,8966],{},"keyup",[250,8968,8969,8970,8973],{},"按下 \u002F 松开键，",[31,8971,8972],{},"e.key"," 拿键名",[250,8975,8976],{},"回车提交、快捷键",[229,8978,8979,8987,8990],{},[250,8980,8981,1267,8984],{},[31,8982,8983],{},"mouseenter",[31,8985,8986],{},"mouseleave",[250,8988,8989],{},"鼠标进入 \u002F 离开（不冒泡）",[250,8991,8992],{},"悬停效果",[229,8994,8995,9000,9003],{},[250,8996,8997],{},[31,8998,8999],{},"scroll",[250,9001,9002],{},"滚动",[250,9004,9005],{},"吸顶、懒加载",[15,9007,9008],{},"用到再查、查了记住用法即可；找不到名字时去 MDN 搜「事件名 + MDN」是最快的路。",[356,9010],{"height":9011,"src":9012,"title":9013},720,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F15-events.html","示例 15：事件——冒泡、委托与默认行为",[10,9015,9017],{"id":9016},"十六表单读取校验与提交","十六、表单：读取、校验与提交",[15,9019,9020],{},"表单是页面上唯一「用户往里写数据」的地方。把它拆成四件事：读取控件、实时反馈、拦下默认提交、校验通过后组装数据——按顺序走一遍，表单就没有神秘感了。",[27,9022,9024],{"id":9023},"读取控件value-与-checked","读取控件：value 与 checked",[36,9026,9028],{"className":38,"code":9027,"language":40,"meta":41,"style":41},"\u003Cinput id=\"f-name\" type=\"text\">\n\u003Cselect id=\"f-city\">\u003Coption>杭州\u003C\u002Foption>…\u003C\u002Fselect>\n\u003Cinput type=\"checkbox\" name=\"interest\" value=\"js\">\n",[31,9029,9030,9053,9084],{"__ignoreMap":41},[45,9031,9032,9034,9036,9039,9041,9044,9046,9048,9051],{"class":47,"line":48},[45,9033,59],{"class":58},[45,9035,8924],{"class":62},[45,9037,9038],{"class":72}," id",[45,9040,76],{"class":58},[45,9042,9043],{"class":79},"\"f-name\"",[45,9045,206],{"class":72},[45,9047,76],{"class":58},[45,9049,9050],{"class":79},"\"text\"",[45,9052,92],{"class":58},[45,9054,9055,9057,9060,9062,9064,9067,9069,9072,9075,9077,9080,9082],{"class":47,"line":55},[45,9056,59],{"class":58},[45,9058,9059],{"class":62},"select",[45,9061,9038],{"class":72},[45,9063,76],{"class":58},[45,9065,9066],{"class":79},"\"f-city\"",[45,9068,66],{"class":58},[45,9070,9071],{"class":62},"option",[45,9073,9074],{"class":58},">杭州\u003C\u002F",[45,9076,9071],{"class":62},[45,9078,9079],{"class":58},">…\u003C\u002F",[45,9081,9059],{"class":62},[45,9083,92],{"class":58},[45,9085,9086,9088,9090,9092,9094,9097,9099,9101,9104,9107,9109,9112],{"class":47,"line":95},[45,9087,59],{"class":58},[45,9089,8924],{"class":62},[45,9091,206],{"class":72},[45,9093,76],{"class":58},[45,9095,9096],{"class":79},"\"checkbox\"",[45,9098,1898],{"class":72},[45,9100,76],{"class":58},[45,9102,9103],{"class":79},"\"interest\"",[45,9105,9106],{"class":72}," value",[45,9108,76],{"class":58},[45,9110,9111],{"class":79},"\"js\"",[45,9113,92],{"class":58},[36,9115,9117],{"className":383,"code":9116,"language":385,"meta":41,"style":41},"nameInput.value        \u002F\u002F 文本框、密码框、下拉框：都用 .value（字符串）\ncheckbox.checked       \u002F\u002F 复选框、单选框：用 .checked（true \u002F false）\nform.querySelectorAll('input[name=\"interest\"]:checked')   \u002F\u002F 只挑勾上的\n",[31,9118,9119,9127,9135],{"__ignoreMap":41},[45,9120,9121,9124],{"class":47,"line":48},[45,9122,9123],{"class":58},"nameInput.value        ",[45,9125,9126],{"class":51},"\u002F\u002F 文本框、密码框、下拉框：都用 .value（字符串）\n",[45,9128,9129,9132],{"class":47,"line":55},[45,9130,9131],{"class":58},"checkbox.checked       ",[45,9133,9134],{"class":51},"\u002F\u002F 复选框、单选框：用 .checked（true \u002F false）\n",[45,9136,9137,9140,9142,9144,9147,9149],{"class":47,"line":95},[45,9138,9139],{"class":58},"form.",[45,9141,7757],{"class":72},[45,9143,409],{"class":58},[45,9145,9146],{"class":79},"'input[name=\"interest\"]:checked'",[45,9148,472],{"class":58},[45,9150,9151],{"class":51},"\u002F\u002F 只挑勾上的\n",[15,9153,9154,9157,9158,9161,9162,9165],{},[31,9155,9156],{},":checked"," 是 CSS 伪类选择器在查询里的又一次复用。整表一把抓用 ",[31,9159,9160],{},"new FormData(form)","：可遍历出「键 → 值」序列。两个细节要记住：",[22,9163,9164],{},"没勾的复选框根本不在 FormData 里","；同名多选会各占一条。",[15,9167,9168],{},"手工组装对象 vs FormData 各有场景：手工版本只收想要的字段、能把值转成数字、勾选项收成数组；FormData 原貌打包，适合整个表单原样发给后端。",[27,9170,9172],{"id":9171},"input-事件输入时实时反馈","input 事件：输入时实时反馈",[36,9174,9176],{"className":383,"code":9175,"language":385,"meta":41,"style":41},"emailInput.addEventListener('input', () => {\n  const ok = \u002F^\\S+@\\S+\\.\\S+$\u002F.test(emailInput.value.trim())\n  hint.textContent = ok ? '格式看起来没问题' : '需要 名称@域名 的形式'\n})\n",[31,9177,9178,9196,9248,9269],{"__ignoreMap":41},[45,9179,9180,9183,9185,9187,9190,9192,9194],{"class":47,"line":48},[45,9181,9182],{"class":58},"emailInput.",[45,9184,486],{"class":72},[45,9186,409],{"class":58},[45,9188,9189],{"class":79},"'input'",[45,9191,494],{"class":58},[45,9193,435],{"class":392},[45,9195,5669],{"class":58},[45,9197,9198,9200,9203,9205,9208,9211,9214,9216,9220,9222,9224,9228,9230,9233,9235,9237,9240,9243,9245],{"class":47,"line":55},[45,9199,3410],{"class":392},[45,9201,9202],{"class":396}," ok",[45,9204,400],{"class":392},[45,9206,9207],{"class":79}," \u002F",[45,9209,9210],{"class":392},"^",[45,9212,9213],{"class":396},"\\S",[45,9215,765],{"class":392},[45,9217,9219],{"class":9218},"sns5M","@",[45,9221,9213],{"class":396},[45,9223,765],{"class":392},[45,9225,9227],{"class":9226},"sRjNt","\\.",[45,9229,9213],{"class":396},[45,9231,9232],{"class":392},"+$",[45,9234,2096],{"class":79},[45,9236,3943],{"class":58},[45,9238,9239],{"class":72},"test",[45,9241,9242],{"class":58},"(emailInput.value.",[45,9244,4091],{"class":72},[45,9246,9247],{"class":58},"())\n",[45,9249,9250,9253,9255,9258,9261,9264,9266],{"class":47,"line":95},[45,9251,9252],{"class":58},"  hint.textContent ",[45,9254,76],{"class":392},[45,9256,9257],{"class":58}," ok ",[45,9259,9260],{"class":392},"?",[45,9262,9263],{"class":79}," '格式看起来没问题'",[45,9265,2226],{"class":392},[45,9267,9268],{"class":79}," '需要 名称@域名 的形式'\n",[45,9270,9271],{"class":47,"line":102},[45,9272,8667],{"class":58},[15,9274,9275,9277,9278,9280],{},[31,9276,8924],{}," 每次改动都触发（打一个字也触发），用来做即时提示；",[31,9279,8937],{}," 在失焦且值改变后才触发，适合「确定后再生效」的场景。",[27,9282,9284],{"id":9283},"submit拦下默认提交统一校验","submit：拦下默认提交，统一校验",[36,9286,9288],{"className":383,"code":9287,"language":385,"meta":41,"style":41},"form.addEventListener('submit', (e) => {\n  e.preventDefault()      \u002F\u002F 不拦的话：页面刷新，你所有的 JS 状态清零\n  const errors = {}       \u002F\u002F 「字段 → 提示语」的错误对象\n  if (!name.value.trim()) errors.name = '名字不能为空'\n  if (!\u002F^\\S+@\\S+\\.\\S+$\u002F.test(email.value)) errors.email = '邮箱格式不对'\n  if (!agree.checked) errors.agree = '需要先同意条款'\n  renderErrors(errors)    \u002F\u002F 用 Object.entries 逐个写到字段旁边\n  if (Object.keys(errors).length) return   \u002F\u002F 有错就停\n  \u002F\u002F 校验通过：组装数据，交给后端（第十九章 fetch）\n})\n",[31,9289,9290,9311,9322,9337,9358,9400,9416,9427,9448,9453],{"__ignoreMap":41},[45,9291,9292,9294,9296,9298,9301,9303,9305,9307,9309],{"class":47,"line":48},[45,9293,9139],{"class":58},[45,9295,486],{"class":72},[45,9297,409],{"class":58},[45,9299,9300],{"class":79},"'submit'",[45,9302,8630],{"class":58},[45,9304,8520],{"class":1523},[45,9306,1627],{"class":58},[45,9308,435],{"class":392},[45,9310,5669],{"class":58},[45,9312,9313,9315,9317,9319],{"class":47,"line":55},[45,9314,8850],{"class":58},[45,9316,8853],{"class":72},[45,9318,4615],{"class":58},[45,9320,9321],{"class":51},"\u002F\u002F 不拦的话：页面刷新，你所有的 JS 状态清零\n",[45,9323,9324,9326,9329,9331,9334],{"class":47,"line":95},[45,9325,3410],{"class":392},[45,9327,9328],{"class":396}," errors",[45,9330,400],{"class":392},[45,9332,9333],{"class":58}," {}       ",[45,9335,9336],{"class":51},"\u002F\u002F 「字段 → 提示语」的错误对象\n",[45,9338,9339,9341,9343,9345,9348,9350,9353,9355],{"class":47,"line":102},[45,9340,2171],{"class":392},[45,9342,873],{"class":58},[45,9344,2176],{"class":392},[45,9346,9347],{"class":58},"name.value.",[45,9349,4091],{"class":72},[45,9351,9352],{"class":58},"()) errors.name ",[45,9354,76],{"class":392},[45,9356,9357],{"class":79}," '名字不能为空'\n",[45,9359,9360,9362,9364,9366,9368,9370,9372,9374,9376,9378,9380,9382,9384,9386,9388,9390,9392,9395,9397],{"class":47,"line":108},[45,9361,2171],{"class":392},[45,9363,873],{"class":58},[45,9365,2176],{"class":392},[45,9367,2096],{"class":79},[45,9369,9210],{"class":392},[45,9371,9213],{"class":396},[45,9373,765],{"class":392},[45,9375,9219],{"class":9218},[45,9377,9213],{"class":396},[45,9379,765],{"class":392},[45,9381,9227],{"class":9226},[45,9383,9213],{"class":396},[45,9385,9232],{"class":392},[45,9387,2096],{"class":79},[45,9389,3943],{"class":58},[45,9391,9239],{"class":72},[45,9393,9394],{"class":58},"(email.value)) errors.email ",[45,9396,76],{"class":392},[45,9398,9399],{"class":79}," '邮箱格式不对'\n",[45,9401,9402,9404,9406,9408,9411,9413],{"class":47,"line":137},[45,9403,2171],{"class":392},[45,9405,873],{"class":58},[45,9407,2176],{"class":392},[45,9409,9410],{"class":58},"agree.checked) errors.agree ",[45,9412,76],{"class":392},[45,9414,9415],{"class":79}," '需要先同意条款'\n",[45,9417,9418,9421,9424],{"class":47,"line":142},[45,9419,9420],{"class":72},"  renderErrors",[45,9422,9423],{"class":58},"(errors)    ",[45,9425,9426],{"class":51},"\u002F\u002F 用 Object.entries 逐个写到字段旁边\n",[45,9428,9429,9431,9434,9436,9439,9441,9443,9445],{"class":47,"line":148},[45,9430,2171],{"class":392},[45,9432,9433],{"class":58}," (Object.",[45,9435,5959],{"class":72},[45,9437,9438],{"class":58},"(errors).",[45,9440,2317],{"class":396},[45,9442,1627],{"class":58},[45,9444,583],{"class":392},[45,9446,9447],{"class":51},"   \u002F\u002F 有错就停\n",[45,9449,9450],{"class":47,"line":178},[45,9451,9452],{"class":51},"  \u002F\u002F 校验通过：组装数据，交给后端（第十九章 fetch）\n",[45,9454,9455],{"class":47,"line":183},[45,9456,8667],{"class":58},[15,9458,9459,9460,9463,9464,9467,9468,9470,9471,9474],{},"三个要点：",[31,9461,9462],{},"e.preventDefault()"," 必须是第一步；校验结果用「字段 → 提示语」的对象表达，",[31,9465,9466],{},"Object.entries"," 就能派位到界面（第十章学的遍历在这里有了正经工作）；最后一个 ",[31,9469,583],{}," 把「不通过」挡在门外。想再加体验细节，可以 ",[31,9472,9473],{},"focus()"," 到第一个出错字段。",[27,9476,9477],{"id":9477},"提交按钮与回车是同一个事件",[15,9479,9480,9483,9484,9486,9487,2968,9490,9493,9494,9497],{},[31,9481,9482],{},"\u003Cbutton type=\"submit\">"," 和输入框里的回车，触发的都是 ",[31,9485,8950],{},"——两个入口，一份逻辑。反过来要小心：form 里的 ",[31,9488,9489],{},"\u003Cbutton>",[22,9491,9492],{},"不写 type 时默认就是 submit","，想当普通按钮用必须显式写 ",[31,9495,9496],{},"type=\"button\"","，这个默认值坑过无数人。",[356,9499],{"height":9500,"src":9501,"title":9502},760,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F16-form.html","示例 16：表单——读取、实时校验与提交",[10,9504,9506],{"id":9505},"十七bom-与浏览器存储","十七、BOM 与浏览器存储",[15,9508,9509],{},"到上一章为止，我们干的活都发生在「文档」里。但 JS 的宿主是浏览器，文档之外还有一整套围绕浏览器窗口的 API——常被叫作 BOM（Browser Object Model）。这一章认识四样东西：window、navigator、location、history，再加一个绕不开的实用主题：把数据存在用户的浏览器里。",[27,9511,9513],{"id":9512},"window全局对象也是窗口","window：全局对象，也是窗口",[15,9515,9516],{},"第七章讲过作用域链：变量从里往外找，链的最后一站是全局作用域。在浏览器里，这个「最后一站」有个具体的名字——window。它身兼两职：",[1094,9518,9519,9525],{},[1097,9520,9521,9522,9524],{},"它是全局对象：",[31,9523,615],{}," 声明的变量、函数声明，都会自动变成 window 的属性",[1097,9526,9527],{},"它是浏览器窗口：视口尺寸、弹窗、定时器（下一章）都挂在它名下",[36,9529,9531],{"className":383,"code":9530,"language":385,"meta":41,"style":41},"var appName = 'demo'\nfunction boot() {}\nwindow.appName       \u002F\u002F 'demo'：var 和函数声明自动成为 window 的属性\nlet secret = 1\nwindow.secret        \u002F\u002F undefined：let\u002Fconst 不上 window\n\nwindow.innerWidth    \u002F\u002F 视口宽（CSS 像素）\nwindow.alert('!')    \u002F\u002F 这些全局函数，其实都是 window 的方法\nalert('!')           \u002F\u002F 平时省略前缀\n",[31,9532,9533,9545,9555,9563,9574,9582,9586,9594,9612],{"__ignoreMap":41},[45,9534,9535,9537,9540,9542],{"class":47,"line":48},[45,9536,615],{"class":392},[45,9538,9539],{"class":58}," appName ",[45,9541,76],{"class":392},[45,9543,9544],{"class":79}," 'demo'\n",[45,9546,9547,9549,9552],{"class":47,"line":55},[45,9548,1515],{"class":392},[45,9550,9551],{"class":72}," boot",[45,9553,9554],{"class":58},"() {}\n",[45,9556,9557,9560],{"class":47,"line":95},[45,9558,9559],{"class":58},"window.appName       ",[45,9561,9562],{"class":51},"\u002F\u002F 'demo'：var 和函数声明自动成为 window 的属性\n",[45,9564,9565,9567,9570,9572],{"class":47,"line":102},[45,9566,611],{"class":392},[45,9568,9569],{"class":58}," secret ",[45,9571,76],{"class":392},[45,9573,1418],{"class":396},[45,9575,9576,9579],{"class":47,"line":108},[45,9577,9578],{"class":58},"window.secret        ",[45,9580,9581],{"class":51},"\u002F\u002F undefined：let\u002Fconst 不上 window\n",[45,9583,9584],{"class":47,"line":137},[45,9585,99],{"emptyLinePlaceholder":98},[45,9587,9588,9591],{"class":47,"line":142},[45,9589,9590],{"class":58},"window.innerWidth    ",[45,9592,9593],{"class":51},"\u002F\u002F 视口宽（CSS 像素）\n",[45,9595,9596,9599,9602,9604,9607,9609],{"class":47,"line":148},[45,9597,9598],{"class":58},"window.",[45,9600,9601],{"class":72},"alert",[45,9603,409],{"class":58},[45,9605,9606],{"class":79},"'!'",[45,9608,4397],{"class":58},[45,9610,9611],{"class":51},"\u002F\u002F 这些全局函数，其实都是 window 的方法\n",[45,9613,9614,9616,9618,9620,9623],{"class":47,"line":178},[45,9615,9601],{"class":72},[45,9617,409],{"class":58},[45,9619,9606],{"class":79},[45,9621,9622],{"class":58},")           ",[45,9624,9625],{"class":51},"\u002F\u002F 平时省略前缀\n",[27,9627,9629],{"id":9628},"navigator环境信息与别做浏览器侦探","navigator：环境信息与「别做浏览器侦探」",[15,9631,9632,9633,9636,9637,9640,9641,9644],{},"navigator 提供宿主环境的信息：",[31,9634,9635],{},"navigator.language","（界面语言）、",[31,9638,9639],{},"navigator.onLine","（粗略的联网状态）、",[31,9642,9643],{},"navigator.userAgent","（浏览器标识字符串）。",[15,9646,9647],{},"UA 字符串是历史包袱：浏览器们互相伪装，字符串里塞满了别人的名字，解析它来判断「用户用什么浏览器」非常不可靠。正确姿势是特性检测——不看你是谁，看你有什么能力：",[36,9649,9651],{"className":383,"code":9650,"language":385,"meta":41,"style":41},"if ('geolocation' in navigator) {\n  \u002F\u002F 有定位能力才调用，没有就换方案\n}\n\nwindow.matchMedia('(prefers-color-scheme: dark)').matches\n\u002F\u002F 系统偏好深色？本站的明暗主题就是靠它读初始偏好的\n",[31,9652,9653,9667,9672,9676,9680,9695],{"__ignoreMap":41},[45,9654,9655,9657,9659,9662,9664],{"class":47,"line":48},[45,9656,870],{"class":392},[45,9658,873],{"class":58},[45,9660,9661],{"class":79},"'geolocation'",[45,9663,2392],{"class":392},[45,9665,9666],{"class":58}," navigator) {\n",[45,9668,9669],{"class":47,"line":55},[45,9670,9671],{"class":51},"  \u002F\u002F 有定位能力才调用，没有就换方案\n",[45,9673,9674],{"class":47,"line":95},[45,9675,910],{"class":58},[45,9677,9678],{"class":47,"line":102},[45,9679,99],{"emptyLinePlaceholder":98},[45,9681,9682,9684,9687,9689,9692],{"class":47,"line":108},[45,9683,9598],{"class":58},[45,9685,9686],{"class":72},"matchMedia",[45,9688,409],{"class":58},[45,9690,9691],{"class":79},"'(prefers-color-scheme: dark)'",[45,9693,9694],{"class":58},").matches\n",[45,9696,9697],{"class":47,"line":137},[45,9698,9699],{"class":51},"\u002F\u002F 系统偏好深色？本站的明暗主题就是靠它读初始偏好的\n",[27,9701,9703],{"id":9702},"location地址栏的入口","location：地址栏的入口",[15,9705,9706,9707,354],{},"location 描述当前页面地址，",[22,9708,9709],{},"改它就等于导航",[36,9711,9713],{"className":383,"code":9712,"language":385,"meta":41,"style":41},"location.href       \u002F\u002F 完整地址：读是字符串，写是跳转\nlocation.pathname   \u002F\u002F 路径部分\nlocation.search     \u002F\u002F '?page=2'\nlocation.hash       \u002F\u002F '#who=alan'，井号起\n\nlocation.hash = 'who=alan'   \u002F\u002F 只改 hash：不刷新页面、会进历史记录\nlocation.reload()            \u002F\u002F 重新加载（慎用，页面状态全丢）\n",[31,9714,9715,9723,9731,9739,9747,9751,9764],{"__ignoreMap":41},[45,9716,9717,9720],{"class":47,"line":48},[45,9718,9719],{"class":58},"location.href       ",[45,9721,9722],{"class":51},"\u002F\u002F 完整地址：读是字符串，写是跳转\n",[45,9724,9725,9728],{"class":47,"line":55},[45,9726,9727],{"class":58},"location.pathname   ",[45,9729,9730],{"class":51},"\u002F\u002F 路径部分\n",[45,9732,9733,9736],{"class":47,"line":95},[45,9734,9735],{"class":58},"location.search     ",[45,9737,9738],{"class":51},"\u002F\u002F '?page=2'\n",[45,9740,9741,9744],{"class":47,"line":102},[45,9742,9743],{"class":58},"location.hash       ",[45,9745,9746],{"class":51},"\u002F\u002F '#who=alan'，井号起\n",[45,9748,9749],{"class":47,"line":108},[45,9750,99],{"emptyLinePlaceholder":98},[45,9752,9753,9756,9758,9761],{"class":47,"line":137},[45,9754,9755],{"class":58},"location.hash ",[45,9757,76],{"class":392},[45,9759,9760],{"class":79}," 'who=alan'",[45,9762,9763],{"class":51},"   \u002F\u002F 只改 hash：不刷新页面、会进历史记录\n",[45,9765,9766,9769,9772,9774],{"class":47,"line":142},[45,9767,9768],{"class":58},"location.",[45,9770,9771],{"class":72},"reload",[45,9773,2904],{"class":58},[45,9775,9776],{"class":51},"\u002F\u002F 重新加载（慎用，页面状态全丢）\n",[15,9778,9779,9780,9783],{},"地址栏参数交给 URLSearchParams 解析，比自己 ",[31,9781,9782],{},"split('&')"," 稳：",[36,9785,9787],{"className":383,"code":9786,"language":385,"meta":41,"style":41},"\u002F\u002F 地址为 \u002Fsearch?page=2&tag=js&tag=dom 时\nconst params = new URLSearchParams(location.search)\nparams.get('page')     \u002F\u002F '2'——注意永远是字符串，要算数得自己转\nparams.get('tag')      \u002F\u002F 'js'：同名参数只返回第一个\nparams.getAll('tag')   \u002F\u002F ['js', 'dom']：全都要用 getAll\nparams.has('page')     \u002F\u002F true\n",[31,9788,9789,9794,9811,9828,9844,9860],{"__ignoreMap":41},[45,9790,9791],{"class":47,"line":48},[45,9792,9793],{"class":51},"\u002F\u002F 地址为 \u002Fsearch?page=2&tag=js&tag=dom 时\n",[45,9795,9796,9798,9801,9803,9805,9808],{"class":47,"line":55},[45,9797,393],{"class":392},[45,9799,9800],{"class":396}," params",[45,9802,400],{"class":392},[45,9804,6264],{"class":392},[45,9806,9807],{"class":72}," URLSearchParams",[45,9809,9810],{"class":58},"(location.search)\n",[45,9812,9813,9816,9818,9820,9823,9825],{"class":47,"line":95},[45,9814,9815],{"class":58},"params.",[45,9817,6308],{"class":72},[45,9819,409],{"class":58},[45,9821,9822],{"class":79},"'page'",[45,9824,5012],{"class":58},[45,9826,9827],{"class":51},"\u002F\u002F '2'——注意永远是字符串，要算数得自己转\n",[45,9829,9830,9832,9834,9836,9839,9841],{"class":47,"line":102},[45,9831,9815],{"class":58},[45,9833,6308],{"class":72},[45,9835,409],{"class":58},[45,9837,9838],{"class":79},"'tag'",[45,9840,4787],{"class":58},[45,9842,9843],{"class":51},"\u002F\u002F 'js'：同名参数只返回第一个\n",[45,9845,9846,9848,9851,9853,9855,9857],{"class":47,"line":108},[45,9847,9815],{"class":58},[45,9849,9850],{"class":72},"getAll",[45,9852,409],{"class":58},[45,9854,9838],{"class":79},[45,9856,472],{"class":58},[45,9858,9859],{"class":51},"\u002F\u002F ['js', 'dom']：全都要用 getAll\n",[45,9861,9862,9864,9866,9868,9870,9872],{"class":47,"line":137},[45,9863,9815],{"class":58},[45,9865,3343],{"class":72},[45,9867,409],{"class":58},[45,9869,9822],{"class":79},[45,9871,5012],{"class":58},[45,9873,6329],{"class":51},[27,9875,9877],{"id":9876},"history前进与后退","history：前进与后退",[15,9879,9880,9883,9884,1267,9887,9890,9891,9894],{},[31,9881,9882],{},"history.length"," 是当前会话的历史条数；",[31,9885,9886],{},"back()",[31,9888,9889],{},"forward()"," 等价于点浏览器的前进后退。更常用的是 pushState \u002F replaceState——",[22,9892,9893],{},"不刷新页面也能改地址","，前端路由（比如本站切换页面）的地基：",[36,9896,9898],{"className":383,"code":9897,"language":385,"meta":41,"style":41},"history.pushState({ page: 1 }, '', '\u002Flist?page=1')    \u002F\u002F 新增一条历史：地址变了，页面没动\nhistory.replaceState({ page: 2 }, '', '\u002Flist?page=2') \u002F\u002F 替换当前这条，不加新记录\n",[31,9899,9900,9928],{"__ignoreMap":41},[45,9901,9902,9905,9908,9911,9913,9916,9918,9920,9923,9925],{"class":47,"line":48},[45,9903,9904],{"class":58},"history.",[45,9906,9907],{"class":72},"pushState",[45,9909,9910],{"class":58},"({ page: ",[45,9912,1471],{"class":396},[45,9914,9915],{"class":58}," }, ",[45,9917,1974],{"class":79},[45,9919,2465],{"class":58},[45,9921,9922],{"class":79},"'\u002Flist?page=1'",[45,9924,4397],{"class":58},[45,9926,9927],{"class":51},"\u002F\u002F 新增一条历史：地址变了，页面没动\n",[45,9929,9930,9932,9935,9937,9939,9941,9943,9945,9948,9950],{"class":47,"line":55},[45,9931,9904],{"class":58},[45,9933,9934],{"class":72},"replaceState",[45,9936,9910],{"class":58},[45,9938,3300],{"class":396},[45,9940,9915],{"class":58},[45,9942,1974],{"class":79},[45,9944,2465],{"class":58},[45,9946,9947],{"class":79},"'\u002Flist?page=2'",[45,9949,1627],{"class":58},[45,9951,9952],{"class":51},"\u002F\u002F 替换当前这条，不加新记录\n",[15,9954,9955],{},"这对 API 受同源策略严格约束：本章示例跑在沙箱 iframe 里（没有自己的「源」），一调 pushState 就抛 SecurityError——所以示例里只演示 hash 这种「改地址不刷新」的手段。",[27,9957,9959],{"id":9958},"alert-confirm-prompt内置弹窗","alert \u002F confirm \u002F prompt：内置弹窗",[36,9961,9963],{"className":383,"code":9962,"language":385,"meta":41,"style":41},"alert('知道了')                            \u002F\u002F 只有「确定」，无返回值\nconst ok = confirm('确定删除吗？')          \u002F\u002F 确定 true \u002F 取消 false\nconst nick = prompt('你叫什么？', '默认值')  \u002F\u002F 确定 → 字符串（可为空串）；取消 → null\n",[31,9964,9965,9980,10001],{"__ignoreMap":41},[45,9966,9967,9969,9971,9974,9977],{"class":47,"line":48},[45,9968,9601],{"class":72},[45,9970,409],{"class":58},[45,9972,9973],{"class":79},"'知道了'",[45,9975,9976],{"class":58},")                            ",[45,9978,9979],{"class":51},"\u002F\u002F 只有「确定」，无返回值\n",[45,9981,9982,9984,9986,9988,9991,9993,9996,9998],{"class":47,"line":55},[45,9983,393],{"class":392},[45,9985,9202],{"class":396},[45,9987,400],{"class":392},[45,9989,9990],{"class":72}," confirm",[45,9992,409],{"class":58},[45,9994,9995],{"class":79},"'确定删除吗？'",[45,9997,1667],{"class":58},[45,9999,10000],{"class":51},"\u002F\u002F 确定 true \u002F 取消 false\n",[45,10002,10003,10005,10008,10010,10013,10015,10018,10020,10023,10025],{"class":47,"line":95},[45,10004,393],{"class":392},[45,10006,10007],{"class":396}," nick",[45,10009,400],{"class":392},[45,10011,10012],{"class":72}," prompt",[45,10014,409],{"class":58},[45,10016,10017],{"class":79},"'你叫什么？'",[45,10019,2465],{"class":58},[45,10021,10022],{"class":79},"'默认值'",[45,10024,4814],{"class":58},[45,10026,10027],{"class":51},"\u002F\u002F 确定 → 字符串（可为空串）；取消 → null\n",[15,10029,10030,10031,10034],{},"三个都是",[22,10032,10033],{},"阻塞式","：脚本会停住等你点完，而且样式不可定制。正式产品里基本不用（替换成页面内的对话框），但调试和小工具里很顺手。",[27,10036,10038],{"id":10037},"存储三件套localstoragesessionstoragecookie","存储三件套：localStorage、sessionStorage、cookie",[15,10040,10041],{},"浏览器会在本地为用户存数据，三个容器，用途完全不同：",[223,10043,10044,10059],{},[226,10045,10046],{},[229,10047,10048,10050,10053,10056],{},[232,10049],{},[232,10051,10052],{},"localStorage",[232,10054,10055],{},"sessionStorage",[232,10057,10058],{},"cookie",[245,10060,10061,10075,10089,10102,10117],{},[229,10062,10063,10066,10069,10072],{},[250,10064,10065],{},"生命周期",[250,10067,10068],{},"永久，除非主动清除",[250,10070,10071],{},"标签页关掉即失",[250,10073,10074],{},"可设过期时间",[229,10076,10077,10080,10083,10086],{},[250,10078,10079],{},"作用范围",[250,10081,10082],{},"同源的所有标签页",[250,10084,10085],{},"只在当前标签页",[250,10087,10088],{},"同源 + 路径规则",[229,10090,10091,10094,10097,10099],{},[250,10092,10093],{},"容量",[250,10095,10096],{},"约 5MB",[250,10098,10096],{},[250,10100,10101],{},"4KB",[229,10103,10104,10107,10110,10112],{},[250,10105,10106],{},"会发给服务器吗",[250,10108,10109],{},"不会",[250,10111,10109],{},[250,10113,10114],{},[22,10115,10116],{},"每次请求都带上",[229,10118,10119,10122,10125,10128],{},[250,10120,10121],{},"典型用途",[250,10123,10124],{},"主题、草稿、偏好",[250,10126,10127],{},"一次性的流程状态",[250,10129,10130],{},"会话凭证（给后端看）",[15,10132,10133,10134],{},"口诀：",[22,10135,10136],{},"前端自己用的数据放 localStorage \u002F sessionStorage；要让后端看到的才用 cookie。",[15,10138,10139],{},"两种 storage 的 API 一模一样（第十章末尾见过 Map——这套是它的老前辈）：",[36,10141,10143],{"className":383,"code":10142,"language":385,"meta":41,"style":41},"localStorage.setItem('theme', 'dark')   \u002F\u002F 存\nlocalStorage.getItem('theme')           \u002F\u002F 'dark'；键不存在返回 null（不是 undefined）\nlocalStorage.removeItem('theme')        \u002F\u002F 删一条\nlocalStorage.clear()                    \u002F\u002F 清空——会波及同源下的所有代码，慎用\nlocalStorage.length                     \u002F\u002F 现有几条\n",[31,10144,10145,10168,10183,10199,10212],{"__ignoreMap":41},[45,10146,10147,10150,10153,10155,10158,10160,10163,10165],{"class":47,"line":48},[45,10148,10149],{"class":58},"localStorage.",[45,10151,10152],{"class":72},"setItem",[45,10154,409],{"class":58},[45,10156,10157],{"class":79},"'theme'",[45,10159,2465],{"class":58},[45,10161,10162],{"class":79},"'dark'",[45,10164,472],{"class":58},[45,10166,10167],{"class":51},"\u002F\u002F 存\n",[45,10169,10170,10172,10174,10176,10178,10180],{"class":47,"line":55},[45,10171,10149],{"class":58},[45,10173,7375],{"class":72},[45,10175,409],{"class":58},[45,10177,10157],{"class":79},[45,10179,9622],{"class":58},[45,10181,10182],{"class":51},"\u002F\u002F 'dark'；键不存在返回 null（不是 undefined）\n",[45,10184,10185,10187,10190,10192,10194,10196],{"class":47,"line":95},[45,10186,10149],{"class":58},[45,10188,10189],{"class":72},"removeItem",[45,10191,409],{"class":58},[45,10193,10157],{"class":79},[45,10195,2944],{"class":58},[45,10197,10198],{"class":51},"\u002F\u002F 删一条\n",[45,10200,10201,10203,10206,10209],{"class":47,"line":102},[45,10202,10149],{"class":58},[45,10204,10205],{"class":72},"clear",[45,10207,10208],{"class":58},"()                    ",[45,10210,10211],{"class":51},"\u002F\u002F 清空——会波及同源下的所有代码，慎用\n",[45,10213,10214,10216,10218],{"class":47,"line":108},[45,10215,10149],{"class":58},[45,10217,2317],{"class":396},[45,10219,10220],{"class":51},"                     \u002F\u002F 现有几条\n",[27,10222,10224],{"id":10223},"只认字符串json-来中转","只认字符串：JSON 来中转",[15,10226,10227,10228],{},"两个必记的点：",[22,10229,10230],{},"存进去什么，取出来都是字符串；想存对象\u002F数组要用 JSON 中转。",[36,10232,10234],{"className":383,"code":10233,"language":385,"meta":41,"style":41},"localStorage.setItem('count', 7)\nconst got = localStorage.getItem('count')\ntypeof got                \u002F\u002F 'string'！拿到的是 '7'，要算数得 Number(got)\n\nconst user = { name: '阿蓝', visits: 3 }\nlocalStorage.setItem('app:user', JSON.stringify(user))     \u002F\u002F 序列化进去\nconst saved = JSON.parse(localStorage.getItem('app:user')) \u002F\u002F 反序列化出来\nsaved.name                \u002F\u002F '阿蓝'，类型保住了\n\nJSON.stringify({ fn() {}, u: undefined })  \u002F\u002F '{}'——函数和 undefined 直接消失\nJSON.stringify(new Date())                 \u002F\u002F 日期变字符串；要用 Date 得自己 new\n",[31,10235,10236,10254,10273,10283,10287,10306,10331,10361,10369,10373,10397],{"__ignoreMap":41},[45,10237,10238,10240,10242,10244,10247,10249,10252],{"class":47,"line":48},[45,10239,10149],{"class":58},[45,10241,10152],{"class":72},[45,10243,409],{"class":58},[45,10245,10246],{"class":79},"'count'",[45,10248,2465],{"class":58},[45,10250,10251],{"class":396},"7",[45,10253,415],{"class":58},[45,10255,10256,10258,10261,10263,10265,10267,10269,10271],{"class":47,"line":55},[45,10257,393],{"class":392},[45,10259,10260],{"class":396}," got",[45,10262,400],{"class":392},[45,10264,7372],{"class":58},[45,10266,7375],{"class":72},[45,10268,409],{"class":58},[45,10270,10246],{"class":79},[45,10272,415],{"class":58},[45,10274,10275,10277,10280],{"class":47,"line":95},[45,10276,1239],{"class":392},[45,10278,10279],{"class":58}," got                ",[45,10281,10282],{"class":51},"\u002F\u002F 'string'！拿到的是 '7'，要算数得 Number(got)\n",[45,10284,10285],{"class":47,"line":102},[45,10286,99],{"emptyLinePlaceholder":98},[45,10288,10289,10291,10293,10295,10297,10299,10302,10304],{"class":47,"line":108},[45,10290,393],{"class":392},[45,10292,782],{"class":396},[45,10294,400],{"class":392},[45,10296,787],{"class":58},[45,10298,790],{"class":79},[45,10300,10301],{"class":58},", visits: ",[45,10303,3305],{"class":396},[45,10305,793],{"class":58},[45,10307,10308,10310,10312,10314,10317,10319,10321,10323,10325,10328],{"class":47,"line":137},[45,10309,10149],{"class":58},[45,10311,10152],{"class":72},[45,10313,409],{"class":58},[45,10315,10316],{"class":79},"'app:user'",[45,10318,2465],{"class":58},[45,10320,6152],{"class":396},[45,10322,3943],{"class":58},[45,10324,6166],{"class":72},[45,10326,10327],{"class":58},"(user))     ",[45,10329,10330],{"class":51},"\u002F\u002F 序列化进去\n",[45,10332,10333,10335,10338,10340,10342,10344,10346,10349,10351,10353,10355,10358],{"class":47,"line":142},[45,10334,393],{"class":392},[45,10336,10337],{"class":396}," saved",[45,10339,400],{"class":392},[45,10341,7399],{"class":396},[45,10343,3943],{"class":58},[45,10345,6157],{"class":72},[45,10347,10348],{"class":58},"(localStorage.",[45,10350,7375],{"class":72},[45,10352,409],{"class":58},[45,10354,10316],{"class":79},[45,10356,10357],{"class":58},")) ",[45,10359,10360],{"class":51},"\u002F\u002F 反序列化出来\n",[45,10362,10363,10366],{"class":47,"line":148},[45,10364,10365],{"class":58},"saved.name                ",[45,10367,10368],{"class":51},"\u002F\u002F '阿蓝'，类型保住了\n",[45,10370,10371],{"class":47,"line":178},[45,10372,99],{"emptyLinePlaceholder":98},[45,10374,10375,10377,10379,10381,10384,10387,10390,10392,10394],{"class":47,"line":183},[45,10376,6152],{"class":396},[45,10378,3943],{"class":58},[45,10380,6166],{"class":72},[45,10382,10383],{"class":58},"({ ",[45,10385,10386],{"class":72},"fn",[45,10388,10389],{"class":58},"() {}, u: ",[45,10391,709],{"class":396},[45,10393,8583],{"class":58},[45,10395,10396],{"class":51},"\u002F\u002F '{}'——函数和 undefined 直接消失\n",[45,10398,10399,10401,10403,10405,10407,10409,10412,10415],{"class":47,"line":189},[45,10400,6152],{"class":396},[45,10402,3943],{"class":58},[45,10404,6166],{"class":72},[45,10406,409],{"class":58},[45,10408,3225],{"class":392},[45,10410,10411],{"class":72}," Date",[45,10413,10414],{"class":58},"())                 ",[45,10416,10417],{"class":51},"\u002F\u002F 日期变字符串；要用 Date 得自己 new\n",[15,10419,10420,10421,566,10424,10427],{},"键名建议带统一前缀（",[31,10422,10423],{},"app:user",[31,10425,10426],{},"app:theme","），避免和同源下其他代码撞名。这套 JSON 往返，也是下一章 fetch 与后端交换数据的预演。",[27,10429,10431],{"id":10430},"存不进去怎么办先探测再兜底","存不进去怎么办：先探测，再兜底",[15,10433,10434,10435,10438],{},"localStorage 并非总能用：隐私模式、浏览器设置、",[22,10436,10437],{},"沙箱 iframe"," 都会让它一写就抛异常（SecurityError）；写爆容量（约 5MB）会抛 QuotaExceededError。所以正经代码不直接假设它可用：",[36,10440,10442],{"className":383,"code":10441,"language":385,"meta":41,"style":41},"try {\n  localStorage.setItem('__probe__', '1')\n  localStorage.removeItem('__probe__')\n  \u002F\u002F 没抛异常：可用\n} catch (err) {\n  \u002F\u002F SecurityError 等：不可用，降级到内存 Map——程序换个方式继续跑\n}\n",[31,10443,10444,10450,10468,10480,10485,10493,10498],{"__ignoreMap":41},[45,10445,10446,10448],{"class":47,"line":48},[45,10447,7175],{"class":392},[45,10449,5669],{"class":58},[45,10451,10452,10455,10457,10459,10462,10464,10466],{"class":47,"line":55},[45,10453,10454],{"class":58},"  localStorage.",[45,10456,10152],{"class":72},[45,10458,409],{"class":58},[45,10460,10461],{"class":79},"'__probe__'",[45,10463,2465],{"class":58},[45,10465,2413],{"class":79},[45,10467,415],{"class":58},[45,10469,10470,10472,10474,10476,10478],{"class":47,"line":95},[45,10471,10454],{"class":58},[45,10473,10189],{"class":72},[45,10475,409],{"class":58},[45,10477,10461],{"class":79},[45,10479,415],{"class":58},[45,10481,10482],{"class":47,"line":102},[45,10483,10484],{"class":51},"  \u002F\u002F 没抛异常：可用\n",[45,10486,10487,10489,10491],{"class":47,"line":108},[45,10488,7198],{"class":58},[45,10490,7201],{"class":392},[45,10492,7204],{"class":58},[45,10494,10495],{"class":47,"line":137},[45,10496,10497],{"class":51},"  \u002F\u002F SecurityError 等：不可用，降级到内存 Map——程序换个方式继续跑\n",[45,10499,10500],{"class":47,"line":142},[45,10501,910],{"class":58},[15,10503,10504,10505,10508],{},"本章示例就跑在沙箱 iframe 里，你会亲眼看到探测失败、代码自动降级到内存的过程——正是第十二章的原则：",[22,10506,10507],{},"预判得到的错误就处理掉，让程序活着","。大文件（超过 5MB）不该硬塞 localStorage，那是 IndexedDB 的地盘。",[15,10510,10511,10512,10515],{},"最后一个安全习惯：",[22,10513,10514],{},"localStorage 里的一切，同源脚本都能读","。所以密码、登录凭证不要放进去——第十四章说过，XSS 注入的脚本第一件事就是翻 localStorage。",[15,10517,10518],{},"下一章换个维度：前面所有代码都是「立刻执行」的，下一章解决「等一等再执行」——定时器、回调、Promise 与 async \u002F await。",[356,10520],{"height":10521,"src":10522,"title":10523},740,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F17-bom.html","示例 17：BOM 环境信息与带兜底的存储",[10,10525,10527],{"id":10526},"十八异步等一等再执行","十八、异步：等一等再执行",[15,10529,10530,10531,10534],{},"前十七章的代码都是「一行做完再做下一行」。但浏览器里很多事急不来：网络请求要几百毫秒，定时器要等够时间，用户什么时候点按钮完全没谱。JS 的策略是不等——",[22,10532,10533],{},"先往下走，事情完成时回头叫一个函数","。这就是异步。",[27,10536,10538],{"id":10537},"定时器把函数预约到未来","定时器：把函数「预约」到未来",[15,10540,10541,10543,10544,10547,10548,10551,10552,1103],{},[31,10542,3240],{}," 把函数预约到「至少 N 毫秒后」执行，返回一个编号 id；",[31,10545,10546],{},"clearTimeout(id)"," 可以反悔。",[31,10549,10550],{},"setInterval"," 每隔 N 毫秒重复执行，配 ",[31,10553,10554],{},"clearInterval",[36,10556,10558],{"className":383,"code":10557,"language":385,"meta":41,"style":41},"const id = setTimeout(() => { console.log('1 秒后见') }, 1000)\nclearTimeout(id)          \u002F\u002F 反悔：还没到点就取消\n\nconst clockId = setInterval(() => { console.log(new Date()) }, 1000)\nclearInterval(clockId)    \u002F\u002F 停止\n",[31,10559,10560,10592,10603,10607,10640],{"__ignoreMap":41},[45,10561,10562,10564,10566,10568,10571,10573,10575,10578,10580,10582,10585,10588,10590],{"class":47,"line":48},[45,10563,393],{"class":392},[45,10565,9038],{"class":396},[45,10567,400],{"class":392},[45,10569,10570],{"class":72}," setTimeout",[45,10572,3274],{"class":58},[45,10574,435],{"class":392},[45,10576,10577],{"class":58}," { console.",[45,10579,441],{"class":72},[45,10581,409],{"class":58},[45,10583,10584],{"class":79},"'1 秒后见'",[45,10586,10587],{"class":58},") }, ",[45,10589,3282],{"class":396},[45,10591,415],{"class":58},[45,10593,10594,10597,10600],{"class":47,"line":55},[45,10595,10596],{"class":72},"clearTimeout",[45,10598,10599],{"class":58},"(id)          ",[45,10601,10602],{"class":51},"\u002F\u002F 反悔：还没到点就取消\n",[45,10604,10605],{"class":47,"line":95},[45,10606,99],{"emptyLinePlaceholder":98},[45,10608,10609,10611,10614,10616,10619,10621,10623,10625,10627,10629,10631,10633,10636,10638],{"class":47,"line":102},[45,10610,393],{"class":392},[45,10612,10613],{"class":396}," clockId",[45,10615,400],{"class":392},[45,10617,10618],{"class":72}," setInterval",[45,10620,3274],{"class":58},[45,10622,435],{"class":392},[45,10624,10577],{"class":58},[45,10626,441],{"class":72},[45,10628,409],{"class":58},[45,10630,3225],{"class":392},[45,10632,10411],{"class":72},[45,10634,10635],{"class":58},"()) }, ",[45,10637,3282],{"class":396},[45,10639,415],{"class":58},[45,10641,10642,10644,10647],{"class":47,"line":108},[45,10643,10554],{"class":72},[45,10645,10646],{"class":58},"(clockId)    ",[45,10648,10649],{"class":51},"\u002F\u002F 停止\n",[15,10651,10652,10653,10656],{},"关键词是「至少」：",[22,10654,10655],{},"定时器不会打断正在执行的代码","。如果一个循环要跑 3 秒，你设置的 1 秒定时器也得排队等它跑完：",[36,10658,10660],{"className":383,"code":10659,"language":385,"meta":41,"style":41},"setTimeout(() => console.log('定时器'), 0)\nfor (let i = 0; i \u003C 3; i++) console.log('循环 ' + i)\n\u002F\u002F 输出：循环 0、循环 1、循环 2、定时器\n\u002F\u002F 0 毫秒也不是立刻：先让当前这轮同步代码跑完\n",[31,10661,10662,10686,10725,10730],{"__ignoreMap":41},[45,10663,10664,10666,10668,10670,10672,10674,10676,10679,10682,10684],{"class":47,"line":48},[45,10665,3240],{"class":72},[45,10667,3274],{"class":58},[45,10669,435],{"class":392},[45,10671,438],{"class":58},[45,10673,441],{"class":72},[45,10675,409],{"class":58},[45,10677,10678],{"class":79},"'定时器'",[45,10680,10681],{"class":58},"), ",[45,10683,1717],{"class":396},[45,10685,415],{"class":58},[45,10687,10688,10690,10692,10694,10696,10698,10700,10702,10704,10706,10708,10710,10713,10715,10717,10720,10722],{"class":47,"line":55},[45,10689,979],{"class":392},[45,10691,873],{"class":58},[45,10693,611],{"class":392},[45,10695,986],{"class":58},[45,10697,76],{"class":392},[45,10699,991],{"class":396},[45,10701,994],{"class":58},[45,10703,59],{"class":392},[45,10705,999],{"class":396},[45,10707,1002],{"class":58},[45,10709,1005],{"class":392},[45,10711,10712],{"class":58},") console.",[45,10714,441],{"class":72},[45,10716,409],{"class":58},[45,10718,10719],{"class":79},"'循环 '",[45,10721,1591],{"class":392},[45,10723,10724],{"class":58}," i)\n",[45,10726,10727],{"class":47,"line":95},[45,10728,10729],{"class":51},"\u002F\u002F 输出：循环 0、循环 1、循环 2、定时器\n",[45,10731,10732],{"class":47,"line":102},[45,10733,10734],{"class":51},"\u002F\u002F 0 毫秒也不是立刻：先让当前这轮同步代码跑完\n",[27,10736,10738],{"id":10737},"回调结果出来时叫我","回调：结果出来时叫我",[15,10740,10741],{},"早期 JS 的异步全写回调：把「拿到结果后要做的事」包成函数传进去。约定俗成的回调风格是错误优先——第一个参数放着错误：",[36,10743,10745],{"className":383,"code":10744,"language":385,"meta":41,"style":41},"orderTea((err, result) => {\n  if (err) {\n    console.log('失败：' + err.message)\n    return\n  }\n  console.log('成功：' + result)\n})\n",[31,10746,10747,10768,10774,10790,10795,10799,10815],{"__ignoreMap":41},[45,10748,10749,10752,10754,10757,10759,10762,10764,10766],{"class":47,"line":48},[45,10750,10751],{"class":72},"orderTea",[45,10753,5024],{"class":58},[45,10755,10756],{"class":1523},"err",[45,10758,2465],{"class":58},[45,10760,10761],{"class":1523},"result",[45,10763,1627],{"class":58},[45,10765,435],{"class":392},[45,10767,5669],{"class":58},[45,10769,10770,10772],{"class":47,"line":55},[45,10771,2171],{"class":392},[45,10773,7204],{"class":58},[45,10775,10776,10778,10780,10782,10785,10787],{"class":47,"line":95},[45,10777,3456],{"class":58},[45,10779,441],{"class":72},[45,10781,409],{"class":58},[45,10783,10784],{"class":79},"'失败：'",[45,10786,1591],{"class":392},[45,10788,10789],{"class":58}," err.message)\n",[45,10791,10792],{"class":47,"line":102},[45,10793,10794],{"class":392},"    return\n",[45,10796,10797],{"class":47,"line":108},[45,10798,3469],{"class":58},[45,10800,10801,10803,10805,10807,10810,10812],{"class":47,"line":137},[45,10802,2488],{"class":58},[45,10804,441],{"class":72},[45,10806,409],{"class":58},[45,10808,10809],{"class":79},"'成功：'",[45,10811,1591],{"class":392},[45,10813,10814],{"class":58}," result)\n",[45,10816,10817],{"class":47,"line":142},[45,10818,8667],{"class":58},[15,10820,10821],{},"能跑，但有著名的「回调地狱」问题：上一步的结果是下一步的输入，只能一层套一层，代码向右缩进成金字塔，错误处理还要每层重复。",[27,10823,10825],{"id":10824},"promise给未来的结果一个盒子","Promise：给「未来的结果」一个盒子",[15,10827,10828,10829,10832],{},"Promise 是一个代表未来结果的对象，三态：",[22,10830,10831],{},"pending（等结果）→ fulfilled（成功）或 rejected（失败）","，状态一旦落定就不可逆。",[36,10834,10836],{"className":383,"code":10835,"language":385,"meta":41,"style":41},"function orderTea() {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.3) resolve('一杯珍珠奶茶')      \u002F\u002F 兑现\n      else reject(new Error('打烊了'))                       \u002F\u002F 拒付\n    }, 800)\n  })\n}\n\norderTea()\n  .then((tea) => console.log('拿到：' + tea))       \u002F\u002F 成功走这里\n  .catch((err) => console.log('出错：' + err.message)) \u002F\u002F 失败走这里\n  .finally(() => console.log('收尾'))                \u002F\u002F 都走这里\n",[31,10837,10838,10847,10872,10883,10914,10939,10949,10953,10957,10961,10967,11000,11031],{"__ignoreMap":41},[45,10839,10840,10842,10845],{"class":47,"line":48},[45,10841,1515],{"class":392},[45,10843,10844],{"class":72}," orderTea",[45,10846,3405],{"class":58},[45,10848,10849,10851,10853,10856,10858,10861,10863,10866,10868,10870],{"class":47,"line":55},[45,10850,2209],{"class":392},[45,10852,6264],{"class":392},[45,10854,10855],{"class":396}," Promise",[45,10857,5024],{"class":58},[45,10859,10860],{"class":1523},"resolve",[45,10862,2465],{"class":58},[45,10864,10865],{"class":1523},"reject",[45,10867,1627],{"class":58},[45,10869,435],{"class":392},[45,10871,5669],{"class":58},[45,10873,10874,10877,10879,10881],{"class":47,"line":95},[45,10875,10876],{"class":72},"    setTimeout",[45,10878,3274],{"class":58},[45,10880,435],{"class":392},[45,10882,5669],{"class":58},[45,10884,10885,10888,10891,10894,10896,10898,10900,10902,10904,10906,10909,10911],{"class":47,"line":102},[45,10886,10887],{"class":392},"      if",[45,10889,10890],{"class":58}," (Math.",[45,10892,10893],{"class":72},"random",[45,10895,3185],{"class":58},[45,10897,2215],{"class":392},[45,10899,1600],{"class":396},[45,10901,1627],{"class":58},[45,10903,10860],{"class":72},[45,10905,409],{"class":58},[45,10907,10908],{"class":79},"'一杯珍珠奶茶'",[45,10910,4787],{"class":58},[45,10912,10913],{"class":51},"\u002F\u002F 兑现\n",[45,10915,10916,10919,10922,10924,10926,10928,10930,10933,10936],{"class":47,"line":108},[45,10917,10918],{"class":392},"      else",[45,10920,10921],{"class":72}," reject",[45,10923,409],{"class":58},[45,10925,3225],{"class":392},[45,10927,7526],{"class":72},[45,10929,409],{"class":58},[45,10931,10932],{"class":79},"'打烊了'",[45,10934,10935],{"class":58},"))                       ",[45,10937,10938],{"class":51},"\u002F\u002F 拒付\n",[45,10940,10941,10944,10947],{"class":47,"line":137},[45,10942,10943],{"class":58},"    }, ",[45,10945,10946],{"class":396},"800",[45,10948,415],{"class":58},[45,10950,10951],{"class":47,"line":142},[45,10952,8447],{"class":58},[45,10954,10955],{"class":47,"line":148},[45,10956,910],{"class":58},[45,10958,10959],{"class":47,"line":178},[45,10960,99],{"emptyLinePlaceholder":98},[45,10962,10963,10965],{"class":47,"line":183},[45,10964,10751],{"class":72},[45,10966,3477],{"class":58},[45,10968,10969,10971,10974,10976,10979,10981,10983,10985,10987,10989,10992,10994,10997],{"class":47,"line":189},[45,10970,5363],{"class":58},[45,10972,10973],{"class":72},"then",[45,10975,5024],{"class":58},[45,10977,10978],{"class":1523},"tea",[45,10980,1627],{"class":58},[45,10982,435],{"class":392},[45,10984,438],{"class":58},[45,10986,441],{"class":72},[45,10988,409],{"class":58},[45,10990,10991],{"class":79},"'拿到：'",[45,10993,1591],{"class":392},[45,10995,10996],{"class":58}," tea))       ",[45,10998,10999],{"class":51},"\u002F\u002F 成功走这里\n",[45,11001,11002,11004,11006,11008,11010,11012,11014,11016,11018,11020,11023,11025,11028],{"class":47,"line":1538},[45,11003,5363],{"class":58},[45,11005,7201],{"class":72},[45,11007,5024],{"class":58},[45,11009,10756],{"class":1523},[45,11011,1627],{"class":58},[45,11013,435],{"class":392},[45,11015,438],{"class":58},[45,11017,441],{"class":72},[45,11019,409],{"class":58},[45,11021,11022],{"class":79},"'出错：'",[45,11024,1591],{"class":392},[45,11026,11027],{"class":58}," err.message)) ",[45,11029,11030],{"class":51},"\u002F\u002F 失败走这里\n",[45,11032,11033,11035,11037,11039,11041,11043,11045,11047,11050,11053],{"class":47,"line":1547},[45,11034,5363],{"class":58},[45,11036,7444],{"class":72},[45,11038,3274],{"class":58},[45,11040,435],{"class":392},[45,11042,438],{"class":58},[45,11044,441],{"class":72},[45,11046,409],{"class":58},[45,11048,11049],{"class":79},"'收尾'",[45,11051,11052],{"class":58},"))                ",[45,11054,11055],{"class":51},"\u002F\u002F 都走这里\n",[15,11057,11058],{},"三个值得记住的点：",[1094,11060,11061,11074,11084],{},[1097,11062,11063,11066,11067,11069,11070,11073],{},[31,11064,11065],{},".then"," 里 ",[31,11068,583],{}," 的东西会被包成新的 Promise，所以可以",[22,11071,11072],{},"链式","往下串——这才解决了回调嵌套",[1097,11075,11076,11079,11080,11083],{},[31,11077,11078],{},"new Promise(executor)"," 里的执行器函数是",[22,11081,11082],{},"同步立刻执行","的，真正异步的是它注册进去的定时器\u002F网络回调",[1097,11085,11086,11087,11090,11091],{},"忘了 ",[31,11088,11089],{},".catch","，失败会变成「未处理的拒绝」，控制台报 ",[31,11092,11093],{},"Uncaught (in promise)",[27,11095,11097],{"id":11096},"async-await把-promise-写成人话","async \u002F await：把 Promise 写成人话",[15,11099,11100,11102,11103,11106],{},[31,11101,306],{}," 函数里可以 ",[31,11104,11105],{},"await"," 一个 Promise：等它落定，成功直接取出值，失败抛成异常——于是 try\u002Fcatch 就能管异步错误了：",[36,11108,11110],{"className":383,"code":11109,"language":385,"meta":41,"style":41},"async function buyTea() {\n  try {\n    const tea = await orderTea()   \u002F\u002F 等它落定；期间页面照常响应\n    console.log('拿到：' + tea)\n  } catch (err) {\n    console.log('出错：' + err.message)\n  }\n}\n",[31,11111,11112,11123,11129,11148,11163,11171,11185,11189],{"__ignoreMap":41},[45,11113,11114,11116,11118,11121],{"class":47,"line":48},[45,11115,306],{"class":392},[45,11117,2759],{"class":392},[45,11119,11120],{"class":72}," buyTea",[45,11122,3405],{"class":58},[45,11124,11125,11127],{"class":47,"line":55},[45,11126,7360],{"class":392},[45,11128,5669],{"class":58},[45,11130,11131,11133,11136,11138,11141,11143,11145],{"class":47,"line":95},[45,11132,3440],{"class":392},[45,11134,11135],{"class":396}," tea",[45,11137,400],{"class":392},[45,11139,11140],{"class":392}," await",[45,11142,10844],{"class":72},[45,11144,3597],{"class":58},[45,11146,11147],{"class":51},"\u002F\u002F 等它落定；期间页面照常响应\n",[45,11149,11150,11152,11154,11156,11158,11160],{"class":47,"line":102},[45,11151,3456],{"class":58},[45,11153,441],{"class":72},[45,11155,409],{"class":58},[45,11157,10991],{"class":79},[45,11159,1591],{"class":392},[45,11161,11162],{"class":58}," tea)\n",[45,11164,11165,11167,11169],{"class":47,"line":108},[45,11166,7411],{"class":58},[45,11168,7201],{"class":392},[45,11170,7204],{"class":58},[45,11172,11173,11175,11177,11179,11181,11183],{"class":47,"line":137},[45,11174,3456],{"class":58},[45,11176,441],{"class":72},[45,11178,409],{"class":58},[45,11180,11022],{"class":79},[45,11182,1591],{"class":392},[45,11184,10789],{"class":58},[45,11186,11187],{"class":47,"line":142},[45,11188,3469],{"class":58},[45,11190,11191],{"class":47,"line":148},[45,11192,910],{"class":58},[15,11194,11195,11196,11199,11200,11203],{},"注意：",[22,11197,11198],{},"await「暂停」的只是这个 async 函数，不是整个页面","——函数在 await 处让出控制权，其他事件照常跑，Promise 落定后它从下一行接着走。另外，async 函数永远返回 Promise，所以 ",[31,11201,11202],{},"buyTea()"," 拿到的是 Promise 而不是值；想在别处拿值，就继续 await 它。",[27,11205,11207],{"id":11206},"串行还是并行别让-await-排队","串行还是并行：别让 await 排队",[15,11209,11210],{},"常见翻车点：三个互不依赖的任务，写成三个 await 串着等，总耗时是三者之和：",[36,11212,11214],{"className":383,"code":11213,"language":385,"meta":41,"style":41},"await task('洗菜', 600)   \u002F\u002F 一个接着一个……总共约 1800ms\nawait task('切菜', 600)\nawait task('炒菜', 600)\n\nconst names = await Promise.all([task('烧水', 600), task('蒸饭', 600), task('备菜', 600)])\n\u002F\u002F 同时发车，约 600ms 拿到全部结果\n",[31,11215,11216,11238,11255,11272,11276,11336],{"__ignoreMap":41},[45,11217,11218,11220,11223,11225,11228,11230,11233,11235],{"class":47,"line":48},[45,11219,11105],{"class":392},[45,11221,11222],{"class":72}," task",[45,11224,409],{"class":58},[45,11226,11227],{"class":79},"'洗菜'",[45,11229,2465],{"class":58},[45,11231,11232],{"class":396},"600",[45,11234,472],{"class":58},[45,11236,11237],{"class":51},"\u002F\u002F 一个接着一个……总共约 1800ms\n",[45,11239,11240,11242,11244,11246,11249,11251,11253],{"class":47,"line":55},[45,11241,11105],{"class":392},[45,11243,11222],{"class":72},[45,11245,409],{"class":58},[45,11247,11248],{"class":79},"'切菜'",[45,11250,2465],{"class":58},[45,11252,11232],{"class":396},[45,11254,415],{"class":58},[45,11256,11257,11259,11261,11263,11266,11268,11270],{"class":47,"line":95},[45,11258,11105],{"class":392},[45,11260,11222],{"class":72},[45,11262,409],{"class":58},[45,11264,11265],{"class":79},"'炒菜'",[45,11267,2465],{"class":58},[45,11269,11232],{"class":396},[45,11271,415],{"class":58},[45,11273,11274],{"class":47,"line":102},[45,11275,99],{"emptyLinePlaceholder":98},[45,11277,11278,11280,11282,11284,11286,11288,11290,11293,11296,11298,11300,11303,11305,11307,11309,11311,11313,11316,11318,11320,11322,11324,11326,11329,11331,11333],{"class":47,"line":108},[45,11279,393],{"class":392},[45,11281,5353],{"class":396},[45,11283,400],{"class":392},[45,11285,11140],{"class":392},[45,11287,10855],{"class":396},[45,11289,3943],{"class":58},[45,11291,11292],{"class":72},"all",[45,11294,11295],{"class":58},"([",[45,11297,8383],{"class":72},[45,11299,409],{"class":58},[45,11301,11302],{"class":79},"'烧水'",[45,11304,2465],{"class":58},[45,11306,11232],{"class":396},[45,11308,10681],{"class":58},[45,11310,8383],{"class":72},[45,11312,409],{"class":58},[45,11314,11315],{"class":79},"'蒸饭'",[45,11317,2465],{"class":58},[45,11319,11232],{"class":396},[45,11321,10681],{"class":58},[45,11323,8383],{"class":72},[45,11325,409],{"class":58},[45,11327,11328],{"class":79},"'备菜'",[45,11330,2465],{"class":58},[45,11332,11232],{"class":396},[45,11334,11335],{"class":58},")])\n",[45,11337,11338],{"class":47,"line":137},[45,11339,11340],{"class":51},"\u002F\u002F 同时发车，约 600ms 拿到全部结果\n",[15,11342,11343,11346,11347,354],{},[31,11344,11345],{},"Promise.all"," 一荣俱荣：任一失败，整体立刻以第一个错误 reject。口诀：",[22,11348,11349],{},"有依赖才串行，没依赖用 Promise.all",[27,11351,11353],{"id":11352},"微任务与宏任务够用版","微任务与宏任务（够用版）",[15,11355,11356],{},"记住一个执行顺序就够用了：同步代码 → 微任务（Promise 回调、await 的续行）→ 宏任务（setTimeout \u002F setInterval 回调）：",[36,11358,11360],{"className":383,"code":11359,"language":385,"meta":41,"style":41},"setTimeout(() => console.log('定时器 0'), 0)\nPromise.resolve().then(() => console.log('Promise'))\nconsole.log('同步代码')\n\u002F\u002F 输出：同步代码 → Promise → 定时器 0\n",[31,11361,11362,11385,11414,11427],{"__ignoreMap":41},[45,11363,11364,11366,11368,11370,11372,11374,11376,11379,11381,11383],{"class":47,"line":48},[45,11365,3240],{"class":72},[45,11367,3274],{"class":58},[45,11369,435],{"class":392},[45,11371,438],{"class":58},[45,11373,441],{"class":72},[45,11375,409],{"class":58},[45,11377,11378],{"class":79},"'定时器 0'",[45,11380,10681],{"class":58},[45,11382,1717],{"class":396},[45,11384,415],{"class":58},[45,11386,11387,11390,11392,11394,11397,11399,11401,11403,11405,11407,11409,11412],{"class":47,"line":55},[45,11388,11389],{"class":396},"Promise",[45,11391,3943],{"class":58},[45,11393,10860],{"class":72},[45,11395,11396],{"class":58},"().",[45,11398,10973],{"class":72},[45,11400,3274],{"class":58},[45,11402,435],{"class":392},[45,11404,438],{"class":58},[45,11406,441],{"class":72},[45,11408,409],{"class":58},[45,11410,11411],{"class":79},"'Promise'",[45,11413,507],{"class":58},[45,11415,11416,11418,11420,11422,11425],{"class":47,"line":95},[45,11417,915],{"class":58},[45,11419,441],{"class":72},[45,11421,409],{"class":58},[45,11423,11424],{"class":79},"'同步代码'",[45,11426,415],{"class":58},[45,11428,11429],{"class":47,"line":102},[45,11430,11431],{"class":51},"\u002F\u002F 输出：同步代码 → Promise → 定时器 0\n",[15,11433,11434],{},"它能解释大部分「为什么日志顺序和我想的不一样」；更深的机制（事件循环、任务队列）用到再补。",[356,11436],{"height":11437,"src":11438,"title":11439},800,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F18-async.html","示例 18：定时器、回调、Promise、async \u002F await 与串并行对比",[10,11441,11443],{"id":11442},"十九fetch-与-json和服务器说上话","十九、fetch 与 JSON：和服务器说上话",[15,11445,11446],{},"上一章的 Promise 和 async \u002F await，最大的用武之地就是网络请求。这一章把「页面的数据从哪来」这件事说透：新前端的第一口粮，就是从服务器 fetch 回来的 JSON。",[27,11448,11450],{"id":11449},"json跨语言的数据交换格式","JSON：跨语言的数据交换格式",[15,11452,11453,11454,11457,11458,11460,11461,11463],{},"JSON（JavaScript Object Notation）长得像 JS 对象字面量，但它是",[22,11455,11456],{},"格式","不是代码：键必须双引号、没有注释、没有函数，值只允许字符串、数字、布尔、null、数组、对象。你要说规则就一句：能 ",[31,11459,1574],{}," 出去、能 ",[31,11462,7505],{}," 回来，才算合法。",[36,11465,11467],{"className":383,"code":11466,"language":385,"meta":41,"style":41},"JSON.stringify({ name: '阿蓝', tags: ['js'] })   \u002F\u002F '{\"name\":\"阿蓝\",\"tags\":[\"js\"]}'\nJSON.parse('{\"page\": 2}')                        \u002F\u002F { page: 2 }\n",[31,11468,11469,11494],{"__ignoreMap":41},[45,11470,11471,11473,11475,11477,11480,11482,11485,11488,11491],{"class":47,"line":48},[45,11472,6152],{"class":396},[45,11474,3943],{"class":58},[45,11476,6166],{"class":72},[45,11478,11479],{"class":58},"({ name: ",[45,11481,790],{"class":79},[45,11483,11484],{"class":58},", tags: [",[45,11486,11487],{"class":79},"'js'",[45,11489,11490],{"class":58},"] })   ",[45,11492,11493],{"class":51},"\u002F\u002F '{\"name\":\"阿蓝\",\"tags\":[\"js\"]}'\n",[45,11495,11496,11498,11500,11502,11504,11507,11510],{"class":47,"line":55},[45,11497,6152],{"class":396},[45,11499,3943],{"class":58},[45,11501,6157],{"class":72},[45,11503,409],{"class":58},[45,11505,11506],{"class":79},"'{\"page\": 2}'",[45,11508,11509],{"class":58},")                        ",[45,11511,11512],{"class":51},"\u002F\u002F { page: 2 }\n",[15,11514,11515,11517],{},[31,11516,7505],{}," 遇到非法文本会抛 SyntaxError——内容来自外部（网络、存储、用户）时，必须包 try\u002Fcatch，这是第十二章就定下的纪律。",[27,11519,11521],{"id":11520},"fetch-的第一个反直觉404-也算成功","fetch 的第一个反直觉：404 也算「成功」",[15,11523,11524,11527,11528,11531,11532,11538],{},[31,11525,11526],{},"fetch(url)"," 返回 Promise，但它 ",[22,11529,11530],{},"resolve 的条件是「服务器有应答」","——404、500 都是有应答，照样 resolve；只有断网、DNS 失败这类网络层错误才 reject。所以",[22,11533,11534,11535],{},"拿到结果后必须自己检查 ",[31,11536,11537],{},"res.ok","（2xx 为 true）：",[1094,11540,11541,11544],{},[1097,11542,11543],{},"resolve ≠ 请求成功，只等于「有回音」",[1097,11545,11546,11547,11549,11550,11553],{},"没查 ",[31,11548,11537],{}," 的代码，会在 404 时拿着错误页去 ",[31,11551,11552],{},"json()","，然后以一种莫名其妙的方式崩掉",[15,11555,11556],{},"一个完整的「三段式」模板，以后所有 GET 都可以照抄：",[36,11558,11560],{"className":383,"code":11559,"language":385,"meta":41,"style":41},"async function getUsers() {\n  try {\n    const res = await fetch('\u002Fapi\u002Fusers')\n    if (!res.ok) throw new Error('HTTP ' + res.status)\n    return await res.json()\n  } catch (err) {\n    console.error('加载用户失败：', err.message)\n    return []\n  }\n}\n",[31,11561,11562,11573,11579,11600,11628,11642,11650,11664,11671,11675],{"__ignoreMap":41},[45,11563,11564,11566,11568,11571],{"class":47,"line":48},[45,11565,306],{"class":392},[45,11567,2759],{"class":392},[45,11569,11570],{"class":72}," getUsers",[45,11572,3405],{"class":58},[45,11574,11575,11577],{"class":47,"line":55},[45,11576,7360],{"class":392},[45,11578,5669],{"class":58},[45,11580,11581,11583,11586,11588,11590,11593,11595,11598],{"class":47,"line":95},[45,11582,3440],{"class":392},[45,11584,11585],{"class":396}," res",[45,11587,400],{"class":392},[45,11589,11140],{"class":392},[45,11591,11592],{"class":72}," fetch",[45,11594,409],{"class":58},[45,11596,11597],{"class":79},"'\u002Fapi\u002Fusers'",[45,11599,415],{"class":58},[45,11601,11602,11605,11607,11609,11612,11614,11616,11618,11620,11623,11625],{"class":47,"line":102},[45,11603,11604],{"class":392},"    if",[45,11606,873],{"class":58},[45,11608,2176],{"class":392},[45,11610,11611],{"class":58},"res.ok) ",[45,11613,7286],{"class":392},[45,11615,6264],{"class":392},[45,11617,7526],{"class":72},[45,11619,409],{"class":58},[45,11621,11622],{"class":79},"'HTTP '",[45,11624,1591],{"class":392},[45,11626,11627],{"class":58}," res.status)\n",[45,11629,11630,11632,11634,11637,11640],{"class":47,"line":108},[45,11631,3561],{"class":392},[45,11633,11140],{"class":392},[45,11635,11636],{"class":58}," res.",[45,11638,11639],{"class":72},"json",[45,11641,3477],{"class":58},[45,11643,11644,11646,11648],{"class":47,"line":137},[45,11645,7411],{"class":58},[45,11647,7201],{"class":392},[45,11649,7204],{"class":58},[45,11651,11652,11654,11657,11659,11662],{"class":47,"line":142},[45,11653,3456],{"class":58},[45,11655,11656],{"class":72},"error",[45,11658,409],{"class":58},[45,11660,11661],{"class":79},"'加载用户失败：'",[45,11663,7430],{"class":58},[45,11665,11666,11668],{"class":47,"line":148},[45,11667,3561],{"class":392},[45,11669,11670],{"class":58}," []\n",[45,11672,11673],{"class":47,"line":178},[45,11674,3469],{"class":58},[45,11676,11677],{"class":47,"line":183},[45,11678,910],{"class":58},[15,11680,11681,11682,11685,11686,11689,11690,11693,11694,1103,11697,11699,11700,11703],{},"读响应体有三个常用方法：",[31,11683,11684],{},"res.json()","（解析成对象）、",[31,11687,11688],{},"res.text()","（原始字符串）、",[31,11691,11692],{},"res.blob()","（二进制）。注意响应体是",[22,11695,11696],{},"一次性流",[31,11698,11552],{}," 读过了再 ",[31,11701,11702],{},"text()"," 会报错，二选一。",[27,11705,11707],{"id":11706},"post把数据送上去","POST：把数据送上去",[15,11709,11710,11711,11714,11715,1110,11717,11720],{},"发数据用第二个参数：method、headers、body。body 必须是",[22,11712,11713],{},"字符串","——对象要先 ",[31,11716,1574],{},[31,11718,11719],{},"Content-Type: application\u002Fjson"," 相当于告诉服务器「我给你的是 JSON，请按这个解」：",[36,11722,11724],{"className":383,"code":11723,"language":385,"meta":41,"style":41},"await fetch('\u002Fapi\u002Fusers', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application\u002Fjson' },\n  body: JSON.stringify({ name: '阿蓝', role: 'member' }),\n})\n",[31,11725,11726,11739,11749,11764,11788],{"__ignoreMap":41},[45,11727,11728,11730,11732,11734,11736],{"class":47,"line":48},[45,11729,11105],{"class":392},[45,11731,11592],{"class":72},[45,11733,409],{"class":58},[45,11735,11597],{"class":79},[45,11737,11738],{"class":58},", {\n",[45,11740,11741,11744,11747],{"class":47,"line":55},[45,11742,11743],{"class":58},"  method: ",[45,11745,11746],{"class":79},"'POST'",[45,11748,5679],{"class":58},[45,11750,11751,11754,11757,11759,11762],{"class":47,"line":95},[45,11752,11753],{"class":58},"  headers: { ",[45,11755,11756],{"class":79},"'Content-Type'",[45,11758,5696],{"class":58},[45,11760,11761],{"class":79},"'application\u002Fjson'",[45,11763,5302],{"class":58},[45,11765,11766,11769,11771,11773,11775,11777,11779,11782,11785],{"class":47,"line":102},[45,11767,11768],{"class":58},"  body: ",[45,11770,6152],{"class":396},[45,11772,3943],{"class":58},[45,11774,6166],{"class":72},[45,11776,11479],{"class":58},[45,11778,790],{"class":79},[45,11780,11781],{"class":58},", role: ",[45,11783,11784],{"class":79},"'member'",[45,11786,11787],{"class":58}," }),\n",[45,11789,11790],{"class":47,"line":108},[45,11791,8667],{"class":58},[15,11793,11794],{},"查询参数用第十七章的 URLSearchParams 拼，省得手写转义：",[36,11796,11798],{"className":383,"code":11797,"language":385,"meta":41,"style":41},"const qs = new URLSearchParams({ q: '闭包', page: 2 })\nfetch('\u002Fapi\u002Fsearch?' + qs)   \u002F\u002F \u002Fapi\u002Fsearch?q=%E9%97%AD%E5%8C%85&page=2\n",[31,11799,11800,11827],{"__ignoreMap":41},[45,11801,11802,11804,11807,11809,11811,11813,11816,11819,11822,11824],{"class":47,"line":48},[45,11803,393],{"class":392},[45,11805,11806],{"class":396}," qs",[45,11808,400],{"class":392},[45,11810,6264],{"class":392},[45,11812,9807],{"class":72},[45,11814,11815],{"class":58},"({ q: ",[45,11817,11818],{"class":79},"'闭包'",[45,11820,11821],{"class":58},", page: ",[45,11823,3300],{"class":396},[45,11825,11826],{"class":58}," })\n",[45,11828,11829,11832,11834,11837,11839,11842],{"class":47,"line":55},[45,11830,11831],{"class":72},"fetch",[45,11833,409],{"class":58},[45,11835,11836],{"class":79},"'\u002Fapi\u002Fsearch?'",[45,11838,1591],{"class":392},[45,11840,11841],{"class":58}," qs)   ",[45,11843,11844],{"class":51},"\u002F\u002F \u002Fapi\u002Fsearch?q=%E9%97%AD%E5%8C%85&page=2\n",[27,11846,11847],{"id":11847},"新手翻车清单",[1094,11849,11850,11859,11865,11874,11879,11894],{},[1097,11851,11086,11852,11854,11855,11858],{},[31,11853,11105],{},"：拿到的是 Promise 不是数据（",[31,11856,11857],{},"users.length"," 是 undefined）",[1097,11860,11861,11862,11864],{},"忘了查 ",[31,11863,11537],{},"：404 当成功处理",[1097,11866,11867,11868,11870,11871],{},"body 忘 ",[31,11869,1574],{},"：服务器收到 ",[31,11872,11873],{},"[object Object]",[1097,11875,11876,11878],{},[31,11877,11552],{}," 当同步方法用：它是 async 的，要 await",[1097,11880,11881,11882,11885,11886,11889,11890,11893],{},"路径写相对 ",[31,11883,11884],{},"\u002Fapi\u002F..."," 时和页面不同源会被 CORS 拦：跨源需要",[22,11887,11888],{},"服务器","在响应头里点头（",[31,11891,11892],{},"Access-Control-Allow-Origin","），这是后端配置问题，不是 fetch 写法问题",[1097,11895,11896,11897,11900],{},"拿网络数据直接塞 ",[31,11898,11899],{},"innerHTML","：XSS 老问题，回顾第十四章",[27,11902,11903],{"id":11903},"本章示例的说明",[15,11905,11906,11907,1267,11910,1267,11913,11915,11916],{},"示例跑在沙箱 iframe 里，没有可用的网络源，所以内置了一个「长得和 fetch 一样」的 fakeFetch（同样 600ms 延迟、同样返回带 ",[31,11908,11909],{},"status",[31,11911,11912],{},"ok",[31,11914,11552],{}," 的响应对象）来完整演示流程：200 \u002F 404 \u002F 500 \u002F 断网四种结局、POST 提交、重新拉取列表。",[22,11917,11918],{},"真实项目里把它换成 fetch，流程一字不差。",[15,11920,11921],{},"下一章换个话题：代码越写越多，全塞一个文件迟早失控——模块化怎么拆、怎么 import \u002F export，以及交付前的工程习惯。",[356,11923],{"height":9500,"src":11924,"title":11925},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F19-fetch.html","示例 19：fetch 流程——状态码、404\u002F500\u002F断网与 POST",[10,11927,11929],{"id":11928},"二十模块与工程习惯","二十、模块与工程习惯",[15,11931,11932,11933,11936],{},"到上一章为止，代码都写在单个 script 里。真实项目动辄几千行，全塞一个文件有三宗罪：",[22,11934,11935],{},"找不到、会撞名、没法复用","。这一章解决「代码怎么组织」，顺路把交付前该有的工程习惯过一遍。",[27,11938,11940],{"id":11939},"模块是什么自带作用域的文件","模块是什么：自带作用域的文件",[15,11942,11943],{},"一个 JS 文件加上几个 export，就是「模块」（ES Module）。浏览器原生支持，机制有四个要点：",[1094,11945,11946,11952,11958,11964],{},[1097,11947,11948,11951],{},[22,11949,11950],{},"自带作用域","：模块顶层的变量不会挂到 window（对比：传统 script 的顶层 var 和函数声明全是全局的，本章示例能看到这个差别的现场）",[1097,11953,11954,11957],{},[22,11955,11956],{},"显式接口","：只有 export 出去的东西，外面才拿得到",[1097,11959,11960,11963],{},[22,11961,11962],{},"单例","：同一个模块被多个地方 import，也只执行一次，共享同一份内部状态",[1097,11965,11966,11969,11970,11973],{},[22,11967,11968],{},"严格模式","：模块里自动开启 ",[31,11971,11972],{},"'use strict'","，第二章那些「别这么干」从建议变成强制",[36,11975,11977],{"className":38,"code":11976,"language":40,"meta":41,"style":41},"\u003Cscript type=\"module\" src=\".\u002Fmain.js\">\u003C\u002Fscript>\n",[31,11978,11979],{"__ignoreMap":41},[45,11980,11981,11983,11985,11987,11989,11991,11993,11995,11998,12000,12002],{"class":47,"line":48},[45,11982,59],{"class":58},[45,11984,69],{"class":62},[45,11986,206],{"class":72},[45,11988,76],{"class":58},[45,11990,211],{"class":79},[45,11992,73],{"class":72},[45,11994,76],{"class":58},[45,11996,11997],{"class":79},"\".\u002Fmain.js\"",[45,11999,83],{"class":58},[45,12001,69],{"class":62},[45,12003,92],{"class":58},[15,12005,11195,12006,12009],{},[31,12007,12008],{},"file:\u002F\u002F"," 直接双击打开会因为 CORS 报错——模块必须经 HTTP 访问（本地起个静态服务，或直接用下节说的构建工具）。",[27,12011,12013],{"id":12012},"export-与-import-的几种写法","export 与 import 的几种写法",[36,12015,12017],{"className":383,"code":12016,"language":385,"meta":41,"style":41},"\u002F\u002F utils.js —— 输出方\nexport function formatTime(date) { … }    \u002F\u002F 具名导出：一个文件可以有很多个\nexport const MAX = 10\nexport default function render() { … }    \u002F\u002F 默认导出：一个模块至多一个\n",[31,12018,12019,12024,12045,12060],{"__ignoreMap":41},[45,12020,12021],{"class":47,"line":48},[45,12022,12023],{"class":51},"\u002F\u002F utils.js —— 输出方\n",[45,12025,12026,12029,12031,12034,12036,12039,12042],{"class":47,"line":55},[45,12027,12028],{"class":392},"export",[45,12030,2759],{"class":392},[45,12032,12033],{"class":72}," formatTime",[45,12035,409],{"class":58},[45,12037,12038],{"class":1523},"date",[45,12040,12041],{"class":58},") { … }    ",[45,12043,12044],{"class":51},"\u002F\u002F 具名导出：一个文件可以有很多个\n",[45,12046,12047,12049,12052,12055,12057],{"class":47,"line":95},[45,12048,12028],{"class":392},[45,12050,12051],{"class":392}," const",[45,12053,12054],{"class":396}," MAX",[45,12056,400],{"class":392},[45,12058,12059],{"class":396}," 10\n",[45,12061,12062,12064,12067,12069,12072,12075],{"class":47,"line":102},[45,12063,12028],{"class":392},[45,12065,12066],{"class":392}," default",[45,12068,2759],{"class":392},[45,12070,12071],{"class":72}," render",[45,12073,12074],{"class":58},"() { … }    ",[45,12076,12077],{"class":51},"\u002F\u002F 默认导出：一个模块至多一个\n",[36,12079,12081],{"className":383,"code":12080,"language":385,"meta":41,"style":41},"\u002F\u002F main.js —— 使用方\nimport { formatTime, MAX } from '.\u002Futils.js'    \u002F\u002F 具名导入：名字要对上\nimport { formatTime as fmt } from '.\u002Futils.js'  \u002F\u002F 重命名\nimport render from '.\u002Futils.js'                 \u002F\u002F 默认导入：名字自己起\nimport * as utils from '.\u002Futils.js'             \u002F\u002F 整个模块收成命名空间对象\n\nimport('.\u002Fheavy.js').then((mod) => { … })       \u002F\u002F 动态导入：用到才加载，返回 Promise\n",[31,12082,12083,12088,12104,12124,12138,12158,12162],{"__ignoreMap":41},[45,12084,12085],{"class":47,"line":48},[45,12086,12087],{"class":51},"\u002F\u002F main.js —— 使用方\n",[45,12089,12090,12093,12096,12098,12101],{"class":47,"line":55},[45,12091,12092],{"class":392},"import",[45,12094,12095],{"class":58}," { formatTime, MAX } ",[45,12097,4334],{"class":392},[45,12099,12100],{"class":79}," '.\u002Futils.js'",[45,12102,12103],{"class":51},"    \u002F\u002F 具名导入：名字要对上\n",[45,12105,12106,12108,12111,12114,12117,12119,12121],{"class":47,"line":95},[45,12107,12092],{"class":392},[45,12109,12110],{"class":58}," { formatTime ",[45,12112,12113],{"class":392},"as",[45,12115,12116],{"class":58}," fmt } ",[45,12118,4334],{"class":392},[45,12120,12100],{"class":79},[45,12122,12123],{"class":51},"  \u002F\u002F 重命名\n",[45,12125,12126,12128,12131,12133,12135],{"class":47,"line":102},[45,12127,12092],{"class":392},[45,12129,12130],{"class":58}," render ",[45,12132,4334],{"class":392},[45,12134,12100],{"class":79},[45,12136,12137],{"class":51},"                 \u002F\u002F 默认导入：名字自己起\n",[45,12139,12140,12142,12145,12148,12151,12153,12155],{"class":47,"line":108},[45,12141,12092],{"class":392},[45,12143,12144],{"class":396}," *",[45,12146,12147],{"class":392}," as",[45,12149,12150],{"class":58}," utils ",[45,12152,4334],{"class":392},[45,12154,12100],{"class":79},[45,12156,12157],{"class":51},"             \u002F\u002F 整个模块收成命名空间对象\n",[45,12159,12160],{"class":47,"line":137},[45,12161,99],{"emptyLinePlaceholder":98},[45,12163,12164,12166,12168,12171,12173,12175,12177,12180,12182,12184,12187],{"class":47,"line":142},[45,12165,12092],{"class":392},[45,12167,409],{"class":58},[45,12169,12170],{"class":79},"'.\u002Fheavy.js'",[45,12172,6289],{"class":58},[45,12174,10973],{"class":72},[45,12176,5024],{"class":58},[45,12178,12179],{"class":1523},"mod",[45,12181,1627],{"class":58},[45,12183,435],{"class":392},[45,12185,12186],{"class":58}," { … })       ",[45,12188,12189],{"class":51},"\u002F\u002F 动态导入：用到才加载，返回 Promise\n",[15,12191,12192],{},"两条硬规矩：",[1094,12194,12195,12209],{},[1097,12196,12197,12198,1971,12201,12204,12205,12208],{},"路径必须写 ",[31,12199,12200],{},".\u002F",[31,12202,12203],{},"..\u002F"," 开头——写成 ",[31,12206,12207],{},"'utils.js'"," 会被当作 npm 包名去找，然后报「找不到模块」",[1097,12210,12211],{},"import 会被提升到模块顶部，写在哪里都一样（别指望靠位置控制顺序）",[15,12213,12214],{},"default 还是具名？一个模块只做一件事、只有一个主出口 → default；工具集合（一排平级函数）→ 全部具名（更好搜、重构更安全）。团队里保持一致，比纠结选哪个更重要。",[27,12216,12218],{"id":12217},"拆文件的原则一条链三个角色","拆文件的原则：一条链，三个角色",[15,12220,12221,12222,1103],{},"小型项目够用的分法——",[22,12223,12224],{},"utils（纯工具）→ store（数据与业务）→ main（入口\u002F界面）",[1094,12226,12227,12230,12233],{},[1097,12228,12229],{},"utils：不碰 DOM、不管业务，进什么出什么，最好测",[1097,12231,12232],{},"store：管数据和规则（比如「count 不能超过 limit」），也不碰 DOM",[1097,12234,12235],{},"main：只当接线员——把用户事件接到 store，把结果渲染到 DOM",[15,12237,12238],{},"依赖方向单向：main → store → utils。反过来（工具函数里 import 业务状态）就是循环依赖噩梦的开端。本章示例把第十四章的迷你待办按这个原则拆成三个文件，每个几十行，看完文件名就知道该去哪改。",[27,12240,12242],{"id":12241},"命名与注释改代码的人会谢你","命名与注释：改代码的人会谢你",[1094,12244,12245,12265,12278],{},[1097,12246,12247,12248,12251,12252,566,12255,12257,12258,566,12261,12264],{},"文件名小写连字符（",[31,12249,12250],{},"user-store.js","）；函数名用动词短语（",[31,12253,12254],{},"createUser",[31,12256,3336],{},"），布尔用 is\u002Fhas\u002Fcan 开头（",[31,12259,12260],{},"isValid",[31,12262,12263],{},"hasNext","）",[1097,12266,12267,1103,12270,12273,12274,12277],{},[22,12268,12269],{},"注释写「为什么」，不写「是什么」",[31,12271,12272],{},"i++ \u002F\u002F i 加一"," 是噪音；",[31,12275,12276],{},"\u002F\u002F 兼容 iOS 15 的旧行为"," 才是信息",[1097,12279,12280],{},"删掉注释掉的死代码——git 记着呢，不差这一份",[27,12282,12284],{"id":12283},"devtools出-bug-时的三件套","DevTools：出 bug 时的三件套",[1094,12286,12287,12304,12314],{},[1097,12288,12289,1103,12292,12295,12296,12299,12300,12303],{},[22,12290,12291],{},"console 家族",[31,12293,12294],{},"console.table(arr)"," 把对象数组排成表、",[31,12297,12298],{},"console.time \u002F timeEnd"," 掐耗时、",[31,12301,12302],{},"console.error"," 让日志带颜色和调用栈（本站示例的「迷你控制台」就是它的仿制品）",[1097,12305,12306,12309,12310,12313],{},[22,12307,12308],{},"断点","：Sources 面板点行号打断点，鼠标悬停看变量值；条件断点只在条件成立时停下；在代码里写 ",[31,12311,12312],{},"debugger"," 语句也会停",[1097,12315,12316,12319],{},[22,12317,12318],{},"Network 面板","：上一章的请求全在这：状态码、请求头、响应体——接口挂没挂一眼便知",[27,12321,12323],{"id":12322},"从手写-script-到工程化缺的那一环","从手写 script 到工程化：缺的那一环",[15,12325,12326,12327,12330,12331,12334,12335,12338],{},"现在我们手动引入每个文件：",[31,12328,12329],{},"\u003Cscript type=\"module\" src=\".\u002Fmain.js\">","，main.js 再 import 其他。项目再大就会遇到新问题：几百个模块逐个写标签不现实、新语法旧浏览器不认、npm 上的包怎么用。这就是",[22,12332,12333],{},"构建工具","（现在的默认答案是 Vite）出场的原因：你安心写模块和装包，它负责打包、压缩、兼容旧浏览器。学完下一章的收尾实战，你就具备了看懂一个 Vite 项目 ",[31,12336,12337],{},"src\u002F"," 目录的全部前置知识。",[356,12340],{"height":12341,"src":12342,"title":12343},780,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F20-modules.html","示例 20：模块作用域、三文件拆分与运行效果",[10,12345,12347],{"id":12346},"二十一综合实战从能跑到像样","二十一、综合实战：从「能跑」到「像样」",[15,12349,12350,12351,12354],{},"毕业考试不用另出题：把 21 章的技能串起来做一个",[22,12352,12353],{},"待办清单","就够了。需求列清楚——添加（含校验）、勾选完成、删除、按状态筛选、显示剩余数量、刷新后数据还在。先看一版「能跑但毛病一堆」的实现，再逐条改造成能进产品的水准。",[27,12356,12358],{"id":12357},"第一版能跑就行","第一版：「能跑」就行？",[15,12360,12361,12362,12365],{},"下面的版本功能真的都能用：回车添加、勾选、删除。但代码里有六个毛病，每一个都是前面某章点名讲过的——先玩一玩，看你能认出几个（提示：输入框里输入 ",[31,12363,12364],{},"\u003Cb>我有格式\u003C\u002Fb>"," 再回车，看看会发生什么）。",[356,12367],{"height":12368,"src":12369,"title":12370},560,"\u002Ftutorials\u002Fjs\u002Fdemos\u002F21-before.html","示例 21（改造前）：能跑但毛病一堆的待办清单",[15,12372,12373],{},"对照下面的清单找一找，看你说中了几个。",[223,12375,12376,12389],{},[226,12377,12378],{},[229,12379,12380,12383,12386],{},[232,12381,12382],{},"「能跑版」的毛病",[232,12384,12385],{},"改造版怎么治",[232,12387,12388],{},"出处",[245,12390,12391,12402,12413,12424,12435,12446],{},[229,12392,12393,12396,12399],{},[250,12394,12395],{},"全局变量满天飞",[250,12397,12398],{},"用闭包把状态藏起来，对外只暴露操作函数",[250,12400,12401],{},"七、十",[229,12403,12404,12407,12410],{},[250,12405,12406],{},"innerHTML 拼字符串",[250,12408,12409],{},"createElement + textContent，用户输入永远当文本",[250,12411,12412],{},"十四",[229,12414,12415,12418,12421],{},[250,12416,12417],{},"onclick 属性、函数挂 window",[250,12419,12420],{},"addEventListener + 事件委托，一个监听管全部条目",[250,12422,12423],{},"十五",[229,12425,12426,12429,12432],{},[250,12427,12428],{},"不 trim、不校验",[250,12430,12431],{},"trim + 空值\u002F长度拦截 + 错误提示",[250,12433,12434],{},"八、十六",[229,12436,12437,12440,12443],{},[250,12438,12439],{},"刷新即失",[250,12441,12442],{},"JSON + localStorage，先探测再兜底",[250,12444,12445],{},"十七",[229,12447,12448,12451,12454],{},[250,12449,12450],{},"数据与界面搅在一起",[250,12452,12453],{},"分出数据层 \u002F 渲染层：一切改动 = 改数据 + 重新 render",[250,12455,12456],{},"十四、二十",[15,12458,12459,12460,12463],{},"改造版还顺手加了一处毛病清单之外的东西：保存状态用 ",[31,12461,12462],{},"async \u002F await"," 模拟异步落盘（真实项目里这里就是一次 fetch POST），右上角能看到「保存中… → 已保存」的流转。",[27,12465,12467],{"id":12466},"第二版改造版","第二版：改造版",[356,12469],{"height":7715,"src":12470,"title":12471},"\u002Ftutorials\u002Fjs\u002Fdemos\u002F21-after.html","示例 21（改造后）：综合实战·待办清单",[27,12473,12475],{"id":12474},"改造版的骨架其实就三件事","改造版的骨架，其实就三件事",[15,12477,12478],{},"数据层用闭包藏住状态，对外只有一组操作函数——你会注意到所有修改都是「不可变更新」，而数组的 map \u002F filter \u002F reduce 全用上了：",[36,12480,12482],{"className":383,"code":12481,"language":385,"meta":41,"style":41},"function createStore(initial) {\n  let todos = initial\n  let seq = todos.reduce((max, t) => Math.max(max, t.id), 0)\n  return {\n    add(text) { todos = [...todos, { id: ++seq, text: text, done: false }] },\n    toggle(id) { todos = todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) },\n    remove(id) { todos = todos.filter((t) => t.id !== id) },\n    get() { return todos },\n  }\n}\n",[31,12483,12484,12498,12510,12550,12556,12588,12642,12675,12687,12691],{"__ignoreMap":41},[45,12485,12486,12488,12491,12493,12496],{"class":47,"line":48},[45,12487,1515],{"class":392},[45,12489,12490],{"class":72}," createStore",[45,12492,409],{"class":58},[45,12494,12495],{"class":1523},"initial",[45,12497,879],{"class":58},[45,12499,12500,12502,12505,12507],{"class":47,"line":55},[45,12501,884],{"class":392},[45,12503,12504],{"class":58}," todos ",[45,12506,76],{"class":392},[45,12508,12509],{"class":58}," initial\n",[45,12511,12512,12514,12517,12519,12522,12524,12526,12529,12531,12534,12536,12538,12541,12543,12546,12548],{"class":47,"line":95},[45,12513,884],{"class":392},[45,12515,12516],{"class":58}," seq ",[45,12518,76],{"class":392},[45,12520,12521],{"class":58}," todos.",[45,12523,2978],{"class":72},[45,12525,5024],{"class":58},[45,12527,12528],{"class":1523},"max",[45,12530,2465],{"class":58},[45,12532,12533],{"class":1523},"t",[45,12535,1627],{"class":58},[45,12537,435],{"class":392},[45,12539,12540],{"class":58}," Math.",[45,12542,12528],{"class":72},[45,12544,12545],{"class":58},"(max, t.id), ",[45,12547,1717],{"class":396},[45,12549,415],{"class":58},[45,12551,12552,12554],{"class":47,"line":102},[45,12553,2209],{"class":392},[45,12555,5669],{"class":58},[45,12557,12558,12561,12563,12566,12569,12571,12573,12575,12578,12580,12583,12585],{"class":47,"line":108},[45,12559,12560],{"class":72},"    add",[45,12562,409],{"class":58},[45,12564,12565],{"class":1523},"text",[45,12567,12568],{"class":58},") { todos ",[45,12570,76],{"class":392},[45,12572,2459],{"class":58},[45,12574,2995],{"class":392},[45,12576,12577],{"class":58},"todos, { id: ",[45,12579,1005],{"class":392},[45,12581,12582],{"class":58},"seq, text: text, done: ",[45,12584,1570],{"class":396},[45,12586,12587],{"class":58}," }] },\n",[45,12589,12590,12593,12595,12598,12600,12602,12604,12606,12608,12610,12612,12614,12617,12619,12622,12624,12626,12628,12631,12633,12636,12639],{"class":47,"line":137},[45,12591,12592],{"class":72},"    toggle",[45,12594,409],{"class":58},[45,12596,12597],{"class":1523},"id",[45,12599,12568],{"class":58},[45,12601,76],{"class":392},[45,12603,12521],{"class":58},[45,12605,2975],{"class":72},[45,12607,5024],{"class":58},[45,12609,12533],{"class":1523},[45,12611,1627],{"class":58},[45,12613,435],{"class":392},[45,12615,12616],{"class":58}," (t.id ",[45,12618,1562],{"class":392},[45,12620,12621],{"class":58}," id ",[45,12623,9260],{"class":392},[45,12625,2803],{"class":58},[45,12627,2995],{"class":392},[45,12629,12630],{"class":58},"t, done: ",[45,12632,2176],{"class":392},[45,12634,12635],{"class":58},"t.done } ",[45,12637,12638],{"class":392},":",[45,12640,12641],{"class":58}," t)) },\n",[45,12643,12644,12647,12649,12651,12653,12655,12657,12659,12661,12663,12665,12667,12670,12672],{"class":47,"line":142},[45,12645,12646],{"class":72},"    remove",[45,12648,409],{"class":58},[45,12650,12597],{"class":1523},[45,12652,12568],{"class":58},[45,12654,76],{"class":392},[45,12656,12521],{"class":58},[45,12658,4842],{"class":72},[45,12660,5024],{"class":58},[45,12662,12533],{"class":1523},[45,12664,1627],{"class":58},[45,12666,435],{"class":392},[45,12668,12669],{"class":58}," t.id ",[45,12671,1855],{"class":392},[45,12673,12674],{"class":58}," id) },\n",[45,12676,12677,12680,12682,12684],{"class":47,"line":148},[45,12678,12679],{"class":72},"    get",[45,12681,5712],{"class":58},[45,12683,583],{"class":392},[45,12685,12686],{"class":58}," todos },\n",[45,12688,12689],{"class":47,"line":178},[45,12690,3469],{"class":58},[45,12692,12693],{"class":47,"line":183},[45,12694,910],{"class":58},[15,12696,12697],{},"渲染层一切从数据出发，事件用一个委托全部接管——数据变了就重新 render，界面永远和数据对得上，不存在「哪里忘了同步」：",[36,12699,12701],{"className":383,"code":12700,"language":385,"meta":41,"style":41},"function render() {\n  list.replaceChildren(...visible().map(renderItem))\n}\n\nlist.addEventListener('click', (e) => {\n  const item = e.target.closest('.item')\n  if (!item) return\n  const id = Number(item.dataset.id)\n  if (e.target.matches('.del')) { store.remove(id); commit('删除') }\n  else if (e.target.matches('input[type=\"checkbox\"]')) { store.toggle(id); commit('更新') }\n})\n",[31,12702,12703,12711,12731,12735,12739,12759,12777,12790,12804,12835,12867],{"__ignoreMap":41},[45,12704,12705,12707,12709],{"class":47,"line":48},[45,12706,1515],{"class":392},[45,12708,12071],{"class":72},[45,12710,3405],{"class":58},[45,12712,12713,12715,12717,12719,12721,12724,12726,12728],{"class":47,"line":55},[45,12714,8362],{"class":58},[45,12716,8365],{"class":72},[45,12718,409],{"class":58},[45,12720,2995],{"class":392},[45,12722,12723],{"class":72},"visible",[45,12725,11396],{"class":58},[45,12727,2975],{"class":72},[45,12729,12730],{"class":58},"(renderItem))\n",[45,12732,12733],{"class":47,"line":95},[45,12734,910],{"class":58},[45,12736,12737],{"class":47,"line":102},[45,12738,99],{"emptyLinePlaceholder":98},[45,12740,12741,12743,12745,12747,12749,12751,12753,12755,12757],{"class":47,"line":108},[45,12742,4572],{"class":58},[45,12744,486],{"class":72},[45,12746,409],{"class":58},[45,12748,491],{"class":79},[45,12750,8630],{"class":58},[45,12752,8520],{"class":1523},[45,12754,1627],{"class":58},[45,12756,435],{"class":392},[45,12758,5669],{"class":58},[45,12760,12761,12763,12765,12767,12769,12771,12773,12775],{"class":47,"line":137},[45,12762,3410],{"class":392},[45,12764,2369],{"class":396},[45,12766,400],{"class":392},[45,12768,8721],{"class":58},[45,12770,7931],{"class":72},[45,12772,409],{"class":58},[45,12774,8728],{"class":79},[45,12776,415],{"class":58},[45,12778,12779,12781,12783,12785,12788],{"class":47,"line":142},[45,12780,2171],{"class":392},[45,12782,873],{"class":58},[45,12784,2176],{"class":392},[45,12786,12787],{"class":58},"item) ",[45,12789,8747],{"class":392},[45,12791,12792,12794,12796,12798,12801],{"class":47,"line":148},[45,12793,3410],{"class":392},[45,12795,9038],{"class":396},[45,12797,400],{"class":392},[45,12799,12800],{"class":72}," Number",[45,12802,12803],{"class":58},"(item.dataset.id)\n",[45,12805,12806,12808,12810,12812,12814,12816,12819,12821,12824,12827,12829,12832],{"class":47,"line":178},[45,12807,2171],{"class":392},[45,12809,8754],{"class":58},[45,12811,7948],{"class":72},[45,12813,409],{"class":58},[45,12815,8761],{"class":79},[45,12817,12818],{"class":58},")) { store.",[45,12820,8247],{"class":72},[45,12822,12823],{"class":58},"(id); ",[45,12825,12826],{"class":72},"commit",[45,12828,409],{"class":58},[45,12830,12831],{"class":79},"'删除'",[45,12833,12834],{"class":58},") }\n",[45,12836,12837,12840,12843,12845,12847,12849,12852,12854,12856,12858,12860,12862,12865],{"class":47,"line":183},[45,12838,12839],{"class":392},"  else",[45,12841,12842],{"class":392}," if",[45,12844,8754],{"class":58},[45,12846,7948],{"class":72},[45,12848,409],{"class":58},[45,12850,12851],{"class":79},"'input[type=\"checkbox\"]'",[45,12853,12818],{"class":58},[45,12855,8263],{"class":72},[45,12857,12823],{"class":58},[45,12859,12826],{"class":72},[45,12861,409],{"class":58},[45,12863,12864],{"class":79},"'更新'",[45,12866,12834],{"class":58},[45,12868,12869],{"class":47,"line":189},[45,12870,8667],{"class":58},[15,12872,12873],{},"剩下的都是普通代码。把示例在「在新标签页打开」后对着源码再读一遍——每一行都能对应回某一章。",[27,12875,12876],{"id":12876},"你已经走完了什么",[15,12878,12879],{},"从第一章「script 放在哪」到能独立写出上面这个应用：变量、类型、函数、闭包、对象、类、错误、DOM、事件、表单、BOM、异步、fetch、模块——21 章没有一章是多余的，它们都在你刚才玩的这个页面里出现了。",[15,12881,12882,12883,12886],{},"接下来去哪：想直接上框架就学 Vue 或 React（你在读的这个站点就是 Vue 写的——回头看你写的 ",[31,12884,12885],{},"render()","，是不是已经有点「响应式」的形状了）；想补类型上 TypeScript；想吃透 npm 生态就认真学 Vite；想碰后端就顺着 Node.js 往下走。工具会换，但变量、函数、异步、模块这些地基不会。",[27,12888,12889],{"id":12889},"最后一份检查清单",[15,12891,12892,12893,12896],{},"下面这些如果都能",[22,12894,12895],{},"不看教程写出来、并且讲给别人听明白","，这门课就毕业了：",[1094,12898,12899,12902,12905,12908,12911,12914,12917,12920,12923,12926,12929,12932,12935,12938],{},[1097,12900,12901],{},"说清 var \u002F let \u002F const 的区别，以及「const 锁的是绑定不是值」",[1097,12903,12904],{},"分清原始值和对象传参的差别，说得出什么叫浅拷贝",[1097,12906,12907],{},"用解构、展开、可选链把日常代码写短而不晦涩",[1097,12909,12910],{},"写好箭头函数，解释闭包为什么能「记住」外层变量",[1097,12912,12913],{},"熟练用 map \u002F filter \u002F reduce \u002F find 加工数组",[1097,12915,12916],{},"用 class 加私有字段写一个小型数据结构",[1097,12918,12919],{},"用 throw \u002F try \u002F catch \u002F finally 搭起错误处理",[1097,12921,12922],{},"用 querySelector \u002F createElement \u002F textContent 安全地操作 DOM",[1097,12924,12925],{},"用事件委托让动态列表「一器多用」，说清 preventDefault 和 stopPropagation 的分工",[1097,12927,12928],{},"用 FormData 与 submit 事件读写、校验一张表单",[1097,12930,12931],{},"用 setTimeout \u002F setInterval 排列时间，用 Promise 与 async \u002F await 消费异步",[1097,12933,12934],{},"写 fetch 时记得查 res.ok，POST 时记得 JSON.stringify",[1097,12936,12937],{},"用 JSON 往返在 localStorage 里存对象，并做可用性探测与兜底",[1097,12939,12940],{},"把代码拆成 utils \u002F store \u002F main，理顺 import \u002F export",[15,12942,12943],{},"清单里每一条，正文里都配了示例。以后哪条卡住了，回来翻对应那一章就好。",[27,12945,12946],{"id":12946},"写在最后",[15,12948,12949],{},"教程到这儿就结束了，但「会 JS」这件事只能靠写出来。去找一个你自己每天真用得上的小需求——记账、单词卡、值班表，什么都行——用这 21 章的全部本事把它做出来。过程中卡住的每一处，都比再读一遍教程管用。",[12951,12952,12953],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}",{"title":41,"searchDepth":55,"depth":55,"links":12955},[12956,12962,12969,12975,12983,12991,12998,13004,13009,13021,13028,13038,13045,13051,13058,13065,13071,13081,13089,13096,13104],{"id":12,"depth":55,"text":13,"children":12957},[12958,12960,12961],{"id":29,"depth":95,"text":12959},"\u003Cscript> 放哪里：四种写法与各自的执行时机",{"id":376,"depth":95,"text":377},{"id":554,"depth":95,"text":555},{"id":598,"depth":55,"text":599,"children":12963},[12964,12965,12966,12967,12968],{"id":623,"depth":95,"text":623},{"id":822,"depth":95,"text":823},{"id":851,"depth":95,"text":852},{"id":963,"depth":95,"text":964},{"id":1089,"depth":95,"text":1089},{"id":1182,"depth":55,"text":1183,"children":12970},[12971,12972,12973,12974],{"id":1197,"depth":95,"text":1198},{"id":1400,"depth":95,"text":1401},{"id":1578,"depth":95,"text":1578},{"id":1691,"depth":95,"text":1691},{"id":1764,"depth":55,"text":1765,"children":12976},[12977,12979,12980,12982],{"id":1771,"depth":95,"text":12978},"== 会做类型转换，=== 不会",{"id":1872,"depth":95,"text":1873},{"id":2010,"depth":95,"text":12981},"可选链 ?.：深层访问不再逐层判断",{"id":2078,"depth":95,"text":2078},{"id":2133,"depth":55,"text":2134,"children":12984},[12985,12986,12988,12989],{"id":2137,"depth":95,"text":2138},{"id":2249,"depth":95,"text":12987},"switch：=== 比较与 break",{"id":2288,"depth":95,"text":2288},{"id":2503,"depth":95,"text":12990},"break 与 continue",{"id":2690,"depth":55,"text":2691,"children":12992},[12993,12994,12995,12996,12997],{"id":2704,"depth":95,"text":2705},{"id":2859,"depth":95,"text":2860},{"id":2999,"depth":95,"text":3000},{"id":3105,"depth":95,"text":3105},{"id":3229,"depth":95,"text":3230},{"id":3359,"depth":55,"text":3360,"children":12999},[13000,13001,13002,13003],{"id":3370,"depth":95,"text":3371},{"id":3506,"depth":95,"text":3507},{"id":3674,"depth":95,"text":3675},{"id":3822,"depth":95,"text":3822},{"id":3832,"depth":55,"text":3833,"children":13005},[13006,13007,13008],{"id":3850,"depth":95,"text":3851},{"id":3986,"depth":95,"text":3986},{"id":4193,"depth":95,"text":4193},{"id":4247,"depth":55,"text":4248,"children":13010},[13011,13012,13013,13014,13015,13016,13017,13018,13020],{"id":4269,"depth":95,"text":4269},{"id":4424,"depth":95,"text":4425},{"id":4621,"depth":95,"text":4622},{"id":4723,"depth":95,"text":4723},{"id":4852,"depth":95,"text":4852},{"id":4906,"depth":95,"text":4907},{"id":5263,"depth":95,"text":5263},{"id":5437,"depth":95,"text":13019},"sort 与「浅拷贝」",{"id":5621,"depth":95,"text":5621},{"id":5633,"depth":55,"text":5634,"children":13022},[13023,13024,13025,13026,13027],{"id":5652,"depth":95,"text":5653},{"id":5832,"depth":95,"text":5832},{"id":5946,"depth":95,"text":5946},{"id":6057,"depth":95,"text":6057},{"id":6232,"depth":95,"text":6233},{"id":6349,"depth":55,"text":6350,"children":13029},[13030,13031,13032,13033,13035,13036,13037],{"id":6367,"depth":95,"text":6368},{"id":6522,"depth":95,"text":6523},{"id":6629,"depth":95,"text":6630},{"id":6716,"depth":95,"text":13034},"私有字段：# 开头",{"id":6828,"depth":95,"text":6829},{"id":6935,"depth":95,"text":6936},{"id":7125,"depth":95,"text":7126},{"id":7157,"depth":55,"text":7158,"children":13039},[13040,13041,13042,13043,13044],{"id":7164,"depth":95,"text":7165},{"id":7249,"depth":95,"text":7250},{"id":7329,"depth":95,"text":7330},{"id":7480,"depth":95,"text":7480},{"id":7678,"depth":95,"text":7679},{"id":7720,"depth":55,"text":7721,"children":13046},[13047,13048,13049,13050],{"id":7727,"depth":95,"text":7728},{"id":7849,"depth":95,"text":7850},{"id":7917,"depth":95,"text":7918},{"id":7970,"depth":95,"text":7971},{"id":8051,"depth":55,"text":8052,"children":13052},[13053,13054,13055,13056,13057],{"id":8061,"depth":95,"text":8062},{"id":8171,"depth":95,"text":8172},{"id":8216,"depth":95,"text":8217},{"id":8326,"depth":95,"text":8327},{"id":8468,"depth":95,"text":8469},{"id":8490,"depth":55,"text":8491,"children":13059},[13060,13061,13062,13063,13064],{"id":8500,"depth":95,"text":8501},{"id":8611,"depth":95,"text":8612},{"id":8677,"depth":95,"text":8678},{"id":8815,"depth":95,"text":8816},{"id":8887,"depth":95,"text":8887},{"id":9016,"depth":55,"text":9017,"children":13066},[13067,13068,13069,13070],{"id":9023,"depth":95,"text":9024},{"id":9171,"depth":95,"text":9172},{"id":9283,"depth":95,"text":9284},{"id":9477,"depth":95,"text":9477},{"id":9505,"depth":55,"text":9506,"children":13072},[13073,13074,13075,13076,13077,13078,13079,13080],{"id":9512,"depth":95,"text":9513},{"id":9628,"depth":95,"text":9629},{"id":9702,"depth":95,"text":9703},{"id":9876,"depth":95,"text":9877},{"id":9958,"depth":95,"text":9959},{"id":10037,"depth":95,"text":10038},{"id":10223,"depth":95,"text":10224},{"id":10430,"depth":95,"text":10431},{"id":10526,"depth":55,"text":10527,"children":13082},[13083,13084,13085,13086,13087,13088],{"id":10537,"depth":95,"text":10538},{"id":10737,"depth":95,"text":10738},{"id":10824,"depth":95,"text":10825},{"id":11096,"depth":95,"text":11097},{"id":11206,"depth":95,"text":11207},{"id":11352,"depth":95,"text":11353},{"id":11442,"depth":55,"text":11443,"children":13090},[13091,13092,13093,13094,13095],{"id":11449,"depth":95,"text":11450},{"id":11520,"depth":95,"text":11521},{"id":11706,"depth":95,"text":11707},{"id":11847,"depth":95,"text":11847},{"id":11903,"depth":95,"text":11903},{"id":11928,"depth":55,"text":11929,"children":13097},[13098,13099,13100,13101,13102,13103],{"id":11939,"depth":95,"text":11940},{"id":12012,"depth":95,"text":12013},{"id":12217,"depth":95,"text":12218},{"id":12241,"depth":95,"text":12242},{"id":12283,"depth":95,"text":12284},{"id":12322,"depth":95,"text":12323},{"id":12346,"depth":55,"text":12347,"children":13105},[13106,13107,13108,13109,13110,13111],{"id":12357,"depth":95,"text":12358},{"id":12466,"depth":95,"text":12467},{"id":12474,"depth":95,"text":12475},{"id":12876,"depth":95,"text":12876},{"id":12889,"depth":95,"text":12889},{"id":12946,"depth":95,"text":12946},"\u002Fimg\u002Fjs-cover.svg","2026-10-04","零基础入门 JavaScript：变量、函数、闭包、DOM、事件与异步编程，21 章循序渐进，每章配可运行示例。","md",{},"\u002Ftutorials\u002Fjs",{"title":5,"description":13114},{"loc":13117},"tutorials\u002Fjs","luxOyhcDWoTYPEh0irrjEhyeFzC0WgVn_ZBWtSeZjJc",[13123,13125,13129,13133,13137,13141,13145,13149,13153,13157,13161,13165,13169,13173,13177,13181,13185,13188,13192,13196,13200,13204,13207,13211,13215,13219,13223,13227,13231,13235,13239,13243,13247,13251,13255,13259,13263,13267,13271,13275,13279,13283,13287,13291,13295,13299,13303,13307,13311,13315,13319,13323,13327,13331,13335,13339,13343,13347,13351,13355,13359,13363,13367,13371,13375,13379,13382,13386,13390,13394,13398,13402,13405,13409,13413,13417,13421,13425,13429,13433,13437,13441,13445,13449,13453,13457,13461,13465,13469,13472,13476,13480,13484,13488,13491,13495,13499,13503,13507,13511,13515,13519,13523,13527,13531,13535,13539,13543,13547,13551,13554,13558,13562,13566,13570,13574,13578,13582,13586,13590,13594,13598,13602,13606,13610,13613,13617,13621,13625],{"id":13117,"title":5,"titles":13124,"content":13114,"level":48},[],{"id":13126,"title":13,"titles":13127,"content":13128,"level":55},"\u002Ftutorials\u002Fjs#一javascript-是什么脚本的执行时机与绑定方式",[5],"HTML 决定「这是什么内容」，CSS 决定「它怎么呈现」，JavaScript 决定「它怎么响应」。前两者是声明式的描述，JS 则是真正的编程语言：变量、分支、循环、函数，还自带操作页面本身的能力。它在浏览器里由 JS 引擎（V8、SpiderMonkey 等）逐行执行；同一门语言搬到服务器就是 Node.js——本教程聚焦浏览器这一侧。 学 JS 之前先建立一条直觉：页面在浏览器里是「边解析边执行」的。HTML 教程第一章讲过解析过程，其中顺序地读标签这一点，直接决定了脚本的行为。",{"id":13130,"title":12959,"titles":13131,"content":13132,"level":95},"\u002Ftutorials\u002Fjs#script-放哪里四种写法与各自的执行时机",[5,13],"\u003C!-- ① 放在 head 里：先下载执行，再解析后面的 HTML -->\n\u003Chead>\u003Cscript src=\"\u002Fjs\u002Fapp.js\">\u003C\u002Fscript>\u003C\u002Fhead>\n\n\u003C!-- ② 放在 \u003C\u002Fbody> 之前：前面的 DOM 已经解析完 -->\n\u003Cbody>…\u003Cscript src=\"\u002Fjs\u002Fapp.js\">\u003C\u002Fscript>\u003C\u002Fbody>\n\n\u003C!-- ③ head 里加 defer：并行下载，解析完再按顺序执行 -->\n\u003Chead>\u003Cscript src=\"\u002Fjs\u002Fapp.js\" defer>\u003C\u002Fscript>\u003C\u002Fhead>\n\n\u003C!-- ④ type=\"module\"：文件成为模块，自带 defer 行为与独立作用域 -->\n\u003Chead>\u003Cscript src=\"\u002Fjs\u002Fapp.js\" type=\"module\">\u003C\u002Fscript>\u003C\u002Fhead> 写法 阻塞解析吗 何时执行 顺序保证 普通脚本（默认） 是，下载并执行完才继续解析 下载完立即执行 按出现顺序，但都在解析中 \u003C\u002Fbody> 之前 前面的内容已解析完，无所谓 立即执行 按出现顺序 defer 否，并行下载 整个文档解析完成后 严格按出现顺序 async 否 下载完立即执行（可能打断解析） 不保证，谁先下完谁先跑 type=\"module\" 否 同 defer 按出现顺序（第二十章展开） 关键结论只有一条：脚本执行时，只有它前面已经解析过的 HTML 才能被拿到。写在 head 里的普通脚本去操作 #late 必然拿到 null，然后报出新手遇到的第一个错误 Cannot read properties of null。 defer 还藏着一个细节：脚本在 DOMContentLoaded 之前执行，而 DOMContentLoaded 与 window 的 load 事件是两回事——前者在 DOM 树就绪时触发，后者要等图片、样式等全部资源加载完。想「DOM 一好就干活」，监听 DOMContentLoaded 或直接用 defer 即可，别用 load。",{"id":13134,"title":377,"titles":13135,"content":13136,"level":95},"\u002Ftutorials\u002Fjs#事件绑定addeventlistener-与-onclick-的差别",[5,13],"给元素挂行为有两种常见写法，差别值得先知道： const btn = document.querySelector('#btn')\n\nbtn.onclick = () => console.log('第一个')\nbtn.onclick = () => console.log('第二个')   \u002F\u002F 覆盖了上一个！最终只有这条生效\n\nbtn.addEventListener('click', () => console.log('第一个'))\nbtn.addEventListener('click', () => console.log('第二个'))   \u002F\u002F 两个都会执行 onclick 是「属性赋值」，同名的只能存一个；addEventListener 是「列表登记」，可以叠多个、还能按需移除（removeEventListener，要求传同一函数引用）。还有第三种——HTML 属性内联 onclick=\"…\"——把结构、行为写在一起，属于早期教程的写法，新代码不要再用了。事件的完整机制（冒泡、捕获、委托）在第十五章。",{"id":13138,"title":555,"titles":13139,"content":13140,"level":95},"\u002Ftutorials\u002Fjs#表达式语句与分号",[5,13],"JS 里有两类语法单位：表达式产生一个值（1 + 2、user.name、函数调用），语句是一个动作（const x = 1、if (…) {…}、return）。分号在多数情况可以省略（自动分号插入，ASI），但 ASI 的规则不直观，有两条实践建议：团队内统一风格（本站统一不写行尾分号），以及不要依赖 ASI 救场——像 return 后换行这类位置，ASI 的插入点常常和你的意图相反。",{"id":13142,"title":599,"titles":13143,"content":13144,"level":55},"\u002Ftutorials\u002Fjs#二变量与声明const-优先let-备用",[5],"现代 JS 的选型建议只有一句：默认用 const，确实需要重新赋值时才把它改成 let，var 基本不再使用。这不是审美偏好，var 有几条历史遗留规则会持续制造 bug，下面逐个看。",{"id":13146,"title":623,"titles":13147,"content":13148,"level":95},"\u002Ftutorials\u002Fjs#三兄弟对比",[5,599],"维度 const let var（老） 作用域 块级 {} 块级 {} 函数级 重新赋值 禁止（对绑定而言） 允许 允许 重复声明 报错 报错 静默覆盖 声明前访问 报错（TDZ） 报错（TDZ） 得到 undefined 提升 有，但在 TDZ 中 有，但在 TDZ 中 提升且初始化为 undefined const PI = 3.14\nlet count = 0\ncount = count + 1        \u002F\u002F ✓ let 可以重新赋值\n\nconst user = { name: '阿蓝' }\nuser.name = '小青'        \u002F\u002F ✓ 合法！改的是对象的属性\nuser = {}                \u002F\u002F ✗ TypeError：向常量赋值（改的是绑定本身）",{"id":13150,"title":823,"titles":13151,"content":13152,"level":95},"\u002Ftutorials\u002Fjs#const-锁定的是绑定不是值",[5,599],"这是 const 最容易被误解的一点：它保证变量永远指向同一个值，但完全不关心这个值内部能不能变。所以 const 声明对象后照样能改属性、push 数组。真想要「整个对象冻住」，用 Object.freeze()——注意它是浅冻结，嵌套的对象不受影响。",{"id":13154,"title":852,"titles":13155,"content":13156,"level":95},"\u002Ftutorials\u002Fjs#块级作用域与-tdz",[5,599],"let 和 const 的作用域是「最近的这一对花括号」： if (true) {\n  let inner = '块内'\n  var leak = '泄漏了'\n}\nconsole.log(inner)   \u002F\u002F ✗ ReferenceError：块外根本不存在\nconsole.log(leak)    \u002F\u002F '泄漏了'，var 是函数级作用域，从 if 里跑了出来 「声明前访问报错」这条叫暂时性死区（TDZ）：从进入作用域到执行到声明语句之间，变量处于不可访问状态。对比 var 的「提前访问得到 undefined」——undefined 会装作没事继续跑，ReferenceError 则会立刻炸给你看，早报错远好过带病运行，这也是推荐 const\u002Flet 的原因之一。",{"id":13158,"title":964,"titles":13159,"content":13160,"level":95},"\u002Ftutorials\u002Fjs#var-的历史包袱重复声明与循环泄漏",[5,599],"var 时代最常见的两个 bug：同名变量被第二次声明悄悄覆盖（不报任何错），以及 for 循环变量泄漏到外面： for (var i = 0; i \u003C 3; i++) { \u002F* … *\u002F }\nconsole.log(i)   \u002F\u002F 3 —— i 泄漏到了循环外面！\n\nfor (let j = 0; j \u003C 3; j++) { \u002F* … *\u002F }\nconsole.log(j)   \u002F\u002F ✗ ReferenceError：let 的 j 只在循环体内 let 在 for 里还有一条更微妙的规则——每次迭代都会创建一个新的绑定，这让「循环里创建函数、函数记住当次的值」成为自然行为。这一点在第七章讲闭包时会看到它的完整价值。",{"id":13162,"title":1089,"titles":13163,"content":13164,"level":95},"\u002Ftutorials\u002Fjs#命名规范",[5,599],"一条好名字省下十行注释。约定速查： 变量与函数用 camelCase：userName、fetchList； 真正常量用 UPPER_SNAKE_CASE：MAX_RETRY、API_BASE； 布尔值带前缀：isReady、hasPermission、canSubmit； 合法字符：字母、数字、$、_，不能数字开头，不能是保留字（class、return…）；中文标识符合法但别用； 名字要有语义：price 好于 p，list2、temp、data1 这种名字说明还没想清楚它是什么； 一行只声明一个变量，别写 let a = 1, b = 2 的逗号链——日后增删都容易漏。",{"id":13166,"title":1183,"titles":13167,"content":13168,"level":55},"\u002Ftutorials\u002Fjs#三数据类型原始值对象与类型判断",[5],"JS 的值分两大家族：原始值（primitive，不可再分）与对象（object，属性的集合）。理解这条分界，等于拿到后面所有「为什么它没变」「为什么它跟着变了」类问题的钥匙。",{"id":13170,"title":1198,"titles":13171,"content":13172,"level":95},"\u002Ftutorials\u002Fjs#八种类型与-typeof",[5,1183],"原始值有七种：string、number、boolean、undefined、null、symbol、bigint；加上 object 一族（含数组、函数、日期等）就是全部。 值 typeof 结果 备注 'abc' 'string' 42 \u002F 3.14 'number' 只有一种数字类型，不区分整数浮点 true 'boolean' undefined 'undefined' 「未定义」，变量声明未赋值的默认值 null 'object' 历史 bug，官方承认但为了兼容不改；判空用 === null Symbol('id') 'symbol' 独一无二的标记 10n 'bigint' 超出安全范围的大整数 {} \u002F [] 'object' 数组也是 object，判定要用 Array.isArray() function () {} 'function' 可调用对象，但归类上仍是对象",{"id":13174,"title":1401,"titles":13175,"content":13176,"level":95},"\u002Ftutorials\u002Fjs#原始值传拷贝对象传引用",[5,1183],"let a = 1\nlet b = a\nb = 2\nconsole.log(a)   \u002F\u002F 1 —— 原始值各存各的\n\nconst x = { n: 1 }\nconst y = x\ny.n = 2\nconsole.log(x.n) \u002F\u002F 2 —— 两个名字指向同一个对象！\n\nfunction bump(obj) { obj.n++ }   \u002F\u002F 参数也是引用传递\nbump(x)\nconsole.log(x.n) \u002F\u002F 3 —— 函数里改的就是外面那个对象 所以「对象比较」不能靠 === 判断内容：{n:1} === {n:1} 永远是 false，因为比的是「是不是同一个对象」。判断内容相等需要逐字段比较，或用 JSON.stringify 做近似的深比较（有顺序敏感等局限）。",{"id":13178,"title":1578,"titles":13179,"content":13180,"level":95},"\u002Ftutorials\u002Fjs#数字的两个经典雷区",[5,1183],"0.1 + 0.2 === 0.3          \u002F\u002F false！IEEE 754 双精度浮点的固有限制\nMath.abs(0.1 + 0.2 - 0.3) \u003C Number.EPSILON   \u002F\u002F 浮点比较的正确姿势\n\nNaN === NaN                \u002F\u002F false！NaN 不等于任何值，包括自己\nNumber.isNaN(NaN)          \u002F\u002F true，判断 NaN 要用它\nisNaN('12px')              \u002F\u002F true —— 老 isNaN 会先做类型转换，基本别用 金额场景不要用浮点数直接算账（乘 100 转整数分，或交给后端），这是行业铁律。",{"id":13182,"title":1691,"titles":13183,"content":13184,"level":95},"\u002Ftutorials\u002Fjs#假值只有八个位置",[5,1183],"if 里当条件用时，只有下面这些值算「假」，其余全是真： false   0   -0   0n   ''   null   undefined   NaN 最容易踩的是「长得像假、其实是真」的成员：字符串 '0'、'false'、空格串 ' '、空数组 []、空对象 {}——统统为真。所以校验「用户填没填」应该判 value === ''，而不是 if (value)。",{"id":13186,"title":1765,"titles":13187,"content":1768,"level":55},"\u002Ftutorials\u002Fjs#四运算符相等短路与可选链",[5],{"id":13189,"title":12978,"titles":13190,"content":13191,"level":95},"\u002Ftutorials\u002Fjs#会做类型转换-不会",[5,1765],"1 == '1'        \u002F\u002F true —— 字符串被转成数字\n1 === '1'       \u002F\u002F false —— 类型不同\nnull == undefined      \u002F\u002F true —— 特例，唯一值得保留 == 的用法\n[] == false            \u002F\u002F true —— 空数组转成 '' 再转 0……\n'0' == false           \u002F\u002F true —— 新手最常踩的「字符串 0」 规则只有一条：日常一律用 === 与 !==；唯一例外是 x == null，它同时命中 null 和 undefined，读作「x 是空的吗」，写起来比 x === null || x === undefined 干净。",{"id":13193,"title":1873,"titles":13194,"content":13195,"level":95},"\u002Ftutorials\u002Fjs#逻辑运算的返回值是操作数短路是特性",[5,1765],"&& 和 || 不返回布尔值，而是返回决定结果的那一侧操作数；并且左侧一旦能定结果，右侧就不执行（短路）： const name = input || '游客'      \u002F\u002F input 为假值时用默认值\nisReady && start()                \u002F\u002F 常用简写：条件成立才执行\nconst count = data ?? 0           \u002F\u002F ?? 只在 null \u002F undefined 时兜底 || 与 ?? 的区别是高频考点：|| 把 0 和空串也当成「没有值」，?? 只认 null \u002F undefined。想保留用户填的 0 或 '' 时，必须用 ??： 0 || 100   \u002F\u002F 100  ✗ 0 是假值，被兜底了\n0 ?? 100   \u002F\u002F 0    ✓ 0 是合法数字，保留",{"id":13197,"title":12981,"titles":13198,"content":13199,"level":95},"\u002Ftutorials\u002Fjs#可选链-深层访问不再逐层判断",[5,1765],"user?.profile?.name        \u002F\u002F 中途任何一环是 null\u002Fundefined 就返回 undefined，不报错\nuser.greet?.()             \u002F\u002F 方法存在才调用\nuser.tags?.[0]             \u002F\u002F 数组下标同样适用 配套的逻辑赋值运算符 ??= \u002F ||= \u002F &&= 用来「只在满足条件时写入」：补配置默认值用 ??= 最精准——已设的值（哪怕是 0 或空串）不会被覆盖。",{"id":13201,"title":2078,"titles":13202,"content":13203,"level":95},"\u002Ftutorials\u002Fjs#两个小符号",[5,1765],"+ 既是加法也是字符串拼接：只要一侧是字符串，另一侧就被转成字符串（'1' + 1 === '11'）；而 -、*、\u002F 只做数学，会把字符串强制转成数字再算（'6' \u002F '2' === 3）。字符串和数字混算前，先显式用 Number() 转换，比依赖隐式规则可靠。 ++ 有前缀后缀之分（++i 先加再用、i++ 先用再加），但不要在表达式里使用自增——arr[i++] = arr[i] 这种代码只有作者读得懂，拆成两行。",{"id":13205,"title":2134,"titles":13206,"content":41,"level":55},"\u002Ftutorials\u002Fjs#五流程控制分支与循环",[5],{"id":13208,"title":2138,"titles":13209,"content":13210,"level":95},"\u002Ftutorials\u002Fjs#if-else-与提前-return",[5,2134],"if 的语法不必多讲，值得刻意练习的是守卫子句写法：把「不满足就退出」的条件提前处理，主干逻辑就永远不会缩进三层以上。 function getDiscount(user) {\n  if (!user) return 0           \u002F\u002F 守卫：没有用户，直接返回\n  if (!user.isVip) return 0     \u002F\u002F 守卫：不是会员，直接返回\n  return user.years > 3 ? 0.8 : 0.9   \u002F\u002F 主干清晰，只有一行\n} 三元表达式 条件 ? 值1 : 值2 适合「二选一取值」的场合，不要嵌套超过一层——两层以上就该改写成 if 或映射表。",{"id":13212,"title":12987,"titles":13213,"content":13214,"level":95},"\u002Ftutorials\u002Fjs#switch-比较与-break",[5,2134],"switch 适合「一个值对多种情况」的分支，两条规则：比较用的是严格相等 ===（字符串 '5' 不会命中数字 5）；每个 case 结尾要 break，否则会从命中的位置一路「穿透」执行到底。穿透有时是特性——多个值共用一段逻辑时让 case 连续排列即可。",{"id":13216,"title":2288,"titles":13217,"content":13218,"level":95},"\u002Ftutorials\u002Fjs#三种循环与两种遍历",[5,2134],"for (let i = 0; i \u003C arr.length; i++) { }   \u002F\u002F 经典 for：需要下标或跳步时用\nwhile (条件) { }                            \u002F\u002F 先判断，可能一次都不执行\ndo { } while (条件)                        \u002F\u002F 先执行一遍，再判断\n\nfor (const item of arr) { }                \u002F\u002F ✓ 遍历数组\u002F字符串\u002FMap\u002FSet 用 for...of，拿值\nfor (const key in obj) { }                 \u002F\u002F 遍历「键」；对象用 in，数组千万别用（见下） for...in 用在数组上会给你字符串下标（'0'、'1'）而不是值，还会顺带翻出原型链上的可枚举属性，是教科书级的错误写法。数组和字符串用 for...of（或数组方法，第九章展开），普通对象用 for...in 或 Object.entries()： const user = { name: '阿蓝', age: 28 }\nfor (const [key, value] of Object.entries(user)) {\n  console.log(key, value)   \u002F\u002F 'name' '阿蓝' \u002F 'age' 28\n}",{"id":13220,"title":12990,"titles":13221,"content":13222,"level":95},"\u002Ftutorials\u002Fjs#break-与-continue",[5,2134],"break：立即跳出整个循环（找到就收工的场景）； continue：跳过本轮剩余部分，直接进入下一轮（过滤场景）。 for (let i = 1; i \u003C= 60; i++) {\n  if (i % 3 === 0 && i % 5 === 1) { found = i; break }   \u002F\u002F 找到即停，不空转\n}\nfor (let i = 1; i \u003C= 20; i++) {\n  if (i % 2 !== 0) continue   \u002F\u002F 奇数跳过\n  evens.push(i)\n} 写循环时始终问自己两件事：终止条件会到达吗（while (true) 必须配合 break 或明确的退出分支）；循环变量在推进吗（忘记 i++ 就是无限循环）。这两类 bug 在浏览器里的表现都是页面直接卡死。",{"id":13224,"title":2691,"titles":13225,"content":13226,"level":55},"\u002Ftutorials\u002Fjs#六函数参数返回值与作用域",[5],"函数是 JS 的基本复用单位：把一段逻辑包起来、给它起个名字、按需调用。HTML 教程里每个示例框能响应点击，背后都是函数在跑。这一章讲清函数的写法、参数与返回值的规则；作用域与 this 留到第七章，因为那要先消化一个事实：函数是可以被传来传去的值。",{"id":13228,"title":2705,"titles":13229,"content":13230,"level":95},"\u002Ftutorials\u002Fjs#三种写法与提升",[5,2691],"\u002F\u002F ① 函数声明\nfunction double(n) { return n * 2 }\n\n\u002F\u002F ② 函数表达式：把匿名函数存进变量\nconst double = function (n) { return n * 2 }\n\n\u002F\u002F ③ 箭头函数：表达式场景的简写\nconst double = (n) => { return n * 2 }\nconst double = n => n * 2   \u002F\u002F 单参数省括号，单表达式省 {} 和 return 函数声明有提升（hoisting）：整个函数被搬到作用域顶部，声明之前也能调用。函数表达式与箭头函数存进变量，遵守变量自己的规则——const 声明的处于 TDZ，先调用直接报 ReferenceError。实践中不要依赖提升，先定义后使用。",{"id":13232,"title":2860,"titles":13233,"content":13234,"level":95},"\u002Ftutorials\u002Fjs#参数默认值只在-undefined-时触发",[5,2691],"function greet(name = '游客') { return '你好，' + name }\n\ngreet()            \u002F\u002F '你好，游客' —— 没传参\ngreet(undefined)   \u002F\u002F '你好，游客' —— 传了 undefined\ngreet('')          \u002F\u002F '你好，'     —— 空串是一个「值」，不触发默认\ngreet(null)        \u002F\u002F '你好，null' —— null 也是一个「值」 这条规则与第四章的 ?? 完全一致：只有 undefined 代表「没有」。 与之配套的两条机制： 剩余参数 function sum(...nums)：把所有实参收成一个真数组（能用 map、reduce）。旧代码里的 arguments 是类数组，新代码不要再用了。 展开运算符 sum(...[1, 2, 3])：反过来把数组摊成一个个实参。同一个 ...，放在形参位置是「收集」，放在实参位置是「展开」。",{"id":13236,"title":3000,"titles":13237,"content":13238,"level":95},"\u002Ftutorials\u002Fjs#返回值没有-return-就是-undefined",[5,2691],"函数没有 return（或 return 后面没有值）→ 返回 undefined； return 会立即结束函数，后面的代码不再执行——利用这一点写「守卫子句」，把异常情况提前挡掉： function divide(a, b) {\n  if (b === 0) return '除数不能为 0'   \u002F\u002F 守卫：先处理掉不能算的情况\n  return a \u002F b                        \u002F\u002F 主干只有一行\n} 函数只能返回一个值；想返回多个数据就包成对象或数组，调用方解构：const { total, count } = stats(list)。 return 关键字后面不要换行——ASI 会在行尾补分号，把「返回值」悄悄变成「返回 undefined」。",{"id":13240,"title":3105,"titles":13241,"content":13242,"level":95},"\u002Ftutorials\u002Fjs#箭头函数的适用边界",[5,2691],"简写谱系（对应示例里的表格）： (a, b) => { return a + b }   \u002F\u002F 完整形式\n(a, b) => a + b              \u002F\u002F 单表达式：{} 与 return 都能省\nn => n * 2                   \u002F\u002F 单参数：括号能省\n() => 42                     \u002F\u002F 无参数：括号不能省\nn => ({ id: n })             \u002F\u002F 想直接返回对象字面量：要包一层括号 选择建议：需要名字、需要复用时用函数声明；作为回调「一行搞定」时用箭头函数。箭头函数有两条硬限制，都和第七章的内容有关：它没有自己的 this（捕获外层的），也不能当构造函数（不能 new）。",{"id":13244,"title":3230,"titles":13245,"content":13246,"level":95},"\u002Ftutorials\u002Fjs#函数是一等公民",[5,2691],"在 JS 里函数就是一种值：能存进变量、放进对象、当参数传、当返回值返回。把函数当参数传给另一个函数（回调）是最常见的用法——事件监听、setTimeout、数组方法都在用： button.addEventListener('click', () => { … })   \u002F\u002F 点击时由浏览器回调\nsetTimeout(() => { … }, 1000)                     \u002F\u002F 1 秒后回调一次\n[1, 2, 3].map(n => n * 2)                         \u002F\u002F map 对每个元素回调 命名也对号入座：普通函数用动词开头（fetchList、formatDate），判断类用 is \u002F has 开头（isEmpty、hasPermission），事件处理用 handle 开头（handleClick）。",{"id":13248,"title":3360,"titles":13249,"content":13250,"level":55},"\u002Ftutorials\u002Fjs#七作用域与闭包函数记住它的出生环境",[5],"函数不只是「一段代码」，它还会记住自己被定义时所处的环境——定义时能访问哪些变量，之后无论被带到哪里调用，这份访问权都不会丢。这条性质叫词法作用域，它衍生的最著名的机制就是闭包。",{"id":13252,"title":3371,"titles":13253,"content":13254,"level":95},"\u002Ftutorials\u002Fjs#作用域链从里往外找",[5,3360],"const site = '我的博客'          \u002F\u002F ① 全局\n\nfunction outer() {\n  const author = '阿蓝'          \u002F\u002F ② outer 的局部\n\n  function inner() {\n    const title = '第一篇'       \u002F\u002F ③ inner 的局部\n    console.log(title, author, site)   \u002F\u002F 三个都能访问：由内向外逐层找\n  }\n  inner()\n} 变量查找永远遵循「由内向外」：先在本地找，找不到去外层，再到全局，都没有就 ReferenceError。反方向不行——outer 里访问不到 inner 的 title。函数每嵌套一层，就在作用域链上串一环。",{"id":13256,"title":3507,"titles":13257,"content":13258,"level":95},"\u002Ftutorials\u002Fjs#闭包内层函数带走了外层的变量",[5,3360],"function createCounter() {\n  let count = 0              \u002F\u002F 外层局部变量\n  return function () {\n    count += 1               \u002F\u002F 内层函数在用它\n    return count\n  }\n}\n\nconst next = createCounter()\nnext()   \u002F\u002F 1\nnext()   \u002F\u002F 2 —— count 没有在两次调用间重置 createCounter 执行完毕后，按直觉它的局部变量该被回收；但因为返回出去的内层函数仍然引用着 count，JS 让这个变量继续存活。这就是闭包的定义式描述：闭包 = 函数 + 它定义时所在的环境。 闭包的三个典型用途： 私有状态：上面的 count 外部无法直接读写，只能通过 next() 推进——模块封装的最小版本； 状态工厂：createCounter() 每次调用生成一个独立计数器，互不干扰； 一次性 \u002F 记忆化：once(fn) 让函数只生效第一次；memoize(fn) 缓存计算结果。",{"id":13260,"title":3675,"titles":13261,"content":13262,"level":95},"\u002Ftutorials\u002Fjs#经典陷阱var-循环与-let-循环",[5,3360],"第二章提过 let 在 for 循环里「每次迭代一个新绑定」，现在能说清它救的是什么场了： for (var i = 0; i \u003C 3; i++) {\n  setTimeout(() => console.log(i), 0)   \u002F\u002F 3 3 3 —— 三段代码共享同一个 i\n}\n\nfor (let j = 0; j \u003C 3; j++) {\n  setTimeout(() => console.log(j), 0)   \u002F\u002F 0 1 2 —— 每轮 j 是独立绑定\n} var 只有一份循环变量，等回调执行时循环早已结束、i 停在 3；let 每次迭代都创建一个新绑定，回调各自记住当轮的值。用 let 写循环就是这条坑的根治方案，setTimeout 换成事件监听、数组方法回调，现象完全一样。",{"id":13264,"title":3822,"titles":13265,"content":13266,"level":95},"\u002Ftutorials\u002Fjs#内存提醒",[5,3360],"闭包让外层变量不被回收，这既是特性也是成本：被长期持有的闭包如果引用了很大的数据（大数组、DOM 节点），会阻止它们被释放。实践上不需要紧张，只需记住一条：长生命周期的地方（全局、长期存在的定时器\u002F监听器）别无意中拖住大对象；不再需要的监听器记得移除。",{"id":13268,"title":3833,"titles":13269,"content":13270,"level":55},"\u002Ftutorials\u002Fjs#八字符串模板字符串与常用方法",[5],"字符串是不可变的原始值（第三章）：所有「修改」方法都返回新字符串，原字符串纹丝不动。name.toUpperCase() 之后 name 还是原样——想把结果留住，要赋给变量。",{"id":13272,"title":3851,"titles":13273,"content":13274,"level":95},"\u002Ftutorials\u002Fjs#模板字符串拼接的现代写法",[5,3833],"const user = { name: '阿蓝', points: 1280 }\n\n\u002F\u002F 旧：加号拼接，引号要数清楚\nconst msg1 = '用户 ' + user.name + ' 有 ' + user.points + ' 积分'\n\n\u002F\u002F 新：反引号模板，${} 里能放任意表达式\nconst msg2 = `用户 ${user.name} 有 ${user.points} 积分，下一级还差 ${2000 - user.points}` 模板字符串的三个能力：插入任意表达式（不是只能放变量）、跨行书写（换行不需要 \\n）、标签模板（String.raw 等高级用法，先了解即可）。拼接 HTML 片段时也用它。",{"id":13276,"title":3986,"titles":13277,"content":13278,"level":95},"\u002Ftutorials\u002Fjs#常用方法速查",[5,3833],"方法 作用 示例 includes \u002F startsWith \u002F endsWith 包含 \u002F 以…开头 \u002F 以…结尾 'abc'.includes('b') → true indexOf 首次出现的下标，找不到返回 -1 'a-b-c'.indexOf('-') → 1 slice(start, end) 截取，支持负数下标（从尾部算） 'hello'.slice(1, 4) → 'ell' toUpperCase \u002F toLowerCase 大小写转换 比较前统一大小写：a.toLowerCase() === b.toLowerCase() trim 去掉首尾空白（表单输入必做） '  hi  '.trim() → 'hi' split 按分隔符切成数组 'a,b,c'.split(',') → ['a','b','c'] replace \u002F replaceAll 替换，支持正则 'a-b'.replaceAll('-', '+') → 'a+b' repeat \u002F padStart \u002F padEnd 重复 \u002F 补齐 '7'.padStart(2, '0') → '07' charAt(i) \u002F at(-1) 取单个字符 '你好'.at(-1) → '好'",{"id":13280,"title":4193,"titles":13281,"content":13282,"level":95},"\u002Ftutorials\u002Fjs#两个值得单独说的点",[5,3833],"判断包含时 indexOf 的 -1。 if (str.indexOf('x')) 是错误写法：找不到返回 -1，是真值，if 居然会通过。正确写法是用 includes，或者显式写 if (str.indexOf('x') !== -1)。 slice 与 substring 的区别。 负数下标时 slice(-3) 表示「从倒数第 3 个开始」；substring 会把负数当 0，且参数顺序颠倒时自动交换——行为不一致且难记，统一用 slice。",{"id":13284,"title":4248,"titles":13285,"content":13286,"level":55},"\u002Ftutorials\u002Fjs#九数组增删改查与-map-filter-reduce",[5],"数组是有序的值的集合，日常写法是字面量 [1, 2, 3]。它同时是「列表」也是「栈\u002F队列」两种用法的载体：一端进出用 push \u002F pop，两端进出用 push \u002F shift。",{"id":13288,"title":4269,"titles":13289,"content":13290,"level":95},"\u002Ftutorials\u002Fjs#创建与访问",[5,4248],"const langs = ['HTML', 'CSS', 'JavaScript']   \u002F\u002F 字面量，最常用\nconst empty = []                               \u002F\u002F 空数组\nconst grid = Array.from({ length: 3 }, (_, i) => i * 2)   \u002F\u002F [0, 2, 4]，由长度生成\n\nlangs[0]        \u002F\u002F 'HTML'，下标从 0 开始\nlangs.at(-1)    \u002F\u002F 'JavaScript'，负数从尾部数，比 langs[langs.length - 1] 干净\nlangs.length    \u002F\u002F 3，长度是属性不是方法 访问不存在的下标不报错，返回 undefined；length 可以写入（arr.length = 0 是清空数组的常用写法）。",{"id":13292,"title":4425,"titles":13293,"content":13294,"level":95},"\u002Ftutorials\u002Fjs#增删改记住哪端快",[5,4248],"操作 方法 效果 备注 尾部加 push(x) 返回新长度 改原数组 尾部删 pop() 返回被删的元素 改原数组 头部加 unshift(x) 返回新长度 慢，整体后移 头部删 shift() 返回被删的元素 慢，整体前移 任意位置 splice(start, delCount, ...items) 返回被删元素组成的数组 就地插入\u002F删除\u002F替换三合一 截取不改原数组 slice(start, end) 返回新数组 与字符串同名方法同语义 const list = ['a', 'b', 'c']\nlist.splice(1, 1, 'B', 'B2')   \u002F\u002F 从下标 1 删 1 个，插入两个 → ['a', 'B', 'B2', 'c']\nconst copy = list.slice()      \u002F\u002F 浅拷贝一份",{"id":13296,"title":4622,"titles":13297,"content":13298,"level":95},"\u002Ftutorials\u002Fjs#遍历forof-与经典的-for",[5,4248],"第五章已定下规则：数组用 for...of 拿值；需要下标时用经典 for，或者 arr.entries() 解构出 [index, value]。for...in 在数组上给字符串下标，继续回避。 for (const lang of langs) console.log(lang)\n\nfor (const [i, lang] of langs.entries()) {\n  console.log(i, lang)          \u002F\u002F 0 'HTML' \u002F 1 'CSS' \u002F 2 'JavaScript'\n}",{"id":13300,"title":4723,"titles":13301,"content":13302,"level":95},"\u002Ftutorials\u002Fjs#查找与判断",[5,4248],"langs.includes('CSS')              \u002F\u002F true —— 包含某个值吗\nlangs.indexOf('Java')              \u002F\u002F -1 —— 找不到返回 -1（同样是那个坑）\nlangs.find(x => x.length > 4)      \u002F\u002F 'JavaScript'？不，返回第一个长度>4的元素\nlangs.findIndex(x => x === 'CSS')  \u002F\u002F 1 find 返回元素（找不到 undefined），findIndex 返回下标（找不到 -1），indexOf 严格相等比较、不做函数判断。以上是「查」，接下来一节的 map \u002F filter \u002F reduce 才是数组真正的主力。",{"id":13304,"title":4852,"titles":13305,"content":13306,"level":95},"\u002Ftutorials\u002Fjs#快速自查的三个坑",[5,4248],"push 返回的是长度不是数组：const r = arr.push(1) 后拿 r 继续链式调用必然错——push 改的是原数组，不要接着用返回值。 splice 改原数组、slice 不改：名字只差一个字母，行为相反，拿不准时先查这条表。 sort 默认按字符串排序：[10, 2, 1].sort() 得到 [1, 10, 2]！数字排序必须传比较函数 sort((a, b) => a - b)，下一节展开。",{"id":13308,"title":4907,"titles":13309,"content":13310,"level":95},"\u002Ftutorials\u002Fjs#map-filter-reduce用回调加工数组",[5,4248],"数组的「遍历—加工」几乎都可以用这几个方法表达，它们都以回调函数为参数（第六章的一等公民），且都不改原数组（返回新数组\u002F新值）： const nums = [1, 2, 3, 4, 5, 6]\n\nnums.map(n => n * n)              \u002F\u002F [1, 4, 9, 16, 25, 36]   每个元素变换 → 等长新数组\nnums.filter(n => n % 2 === 0)     \u002F\u002F [2, 4, 6]              按条件保留 → 变短的新数组\nnums.reduce((acc, n) => acc + n, 0)   \u002F\u002F 21                  从左到右汇总成一个值\n\nnums.find(n => n > 3)             \u002F\u002F 4       第一个满足的「元素」，没有则 undefined\nnums.findIndex(n => n > 3)        \u002F\u002F 3       第一个满足的「下标」，没有则 -1\nnums.some(n => n > 5)             \u002F\u002F true    「存在」满足的\nnums.every(n => n > 0)            \u002F\u002F true    「全部」满足\nnums.includes(4)                  \u002F\u002F true    是否包含某值（=== 比较） 三个主角的分工记住这张表： 方法 一句话 返回 什么时候用 map 逐个变换 等长新数组 「每个都要处理一遍」 filter 逐个筛选 变短（可能空）的新数组 「挑出符合条件的」 reduce 逐个累计 任意类型的一个结果 「求和 \u002F 计数 \u002F 分组 \u002F 拼字符串」 reduce 的两个参数值得盯住：(acc, n) => 新acc 的回调，加一个初始值 0。初始值不同，结果类型就不同——传 0 求和、传 [] 收集、传 {} 分组。不传初始值时以第一个元素为起点，空数组会直接报错，所以请始终传初始值。",{"id":13312,"title":5263,"titles":13313,"content":13314,"level":95},"\u002Ftutorials\u002Fjs#链式调用与短路顺序",[5,4248],"const products = [\n  { name: '键盘', price: 399, onSale: true },\n  { name: '鼠标', price: 129, onSale: false },\n  { name: '显示器', price: 1499, onSale: true }\n]\n\nconst names = products\n  .filter(p => p.onSale)          \u002F\u002F 先筛\n  .map(p => `${p.name} ¥${p.price}`)  \u002F\u002F 再变换 链条顺序影响性能与可读性：先 filter 后 map 能让 map 处理更少的元素；链条不要超过三步，更长的逻辑抽成命名函数。",{"id":13316,"title":13019,"titles":13317,"content":13318,"level":95},"\u002Ftutorials\u002Fjs#sort-与浅拷贝",[5,4248],"[10, 2, 1].sort()                     \u002F\u002F [1, 10, 2] ✗ 默认转字符串逐字符比较\n[10, 2, 1].sort((a, b) => a - b)      \u002F\u002F [1, 2, 10] ✓ 升序\n[10, 2, 1].sort((a, b) => b - a)      \u002F\u002F [10, 2, 1] ✓ 降序\n\nconst sorted = [...nums].sort((a, b) => a - b)   \u002F\u002F 想要新数组：先浅拷贝再排 sort 是这张表里唯一改原数组的方法（reverse 同理），所以复制一份再排序是最常见的写法：[...nums] 是展开成新数组（浅拷贝），Array.from(nums) 与 nums.slice() 等价。注意「浅」——数组元素若是对象，拷贝后两边的对象仍是同一批。",{"id":13320,"title":5621,"titles":13321,"content":13322,"level":95},"\u002Ftutorials\u002Fjs#什么时候不用函数式三件套",[5,4248],"需要 break、需要「边遍历边改原数组」、或者逻辑里有多个相互牵扯的状态时，老老实实写 for...of。方法链是给「数据管道」用的，不是用来炫技的。",{"id":13324,"title":5634,"titles":13325,"content":13326,"level":55},"\u002Ftutorials\u002Fjs#十对象属性的读写遍历与复制",[5],"对象是「键值对的集合」，用来给一组相关的数据一个名字：user.name、user.age 显然比 userName、userAge 两个散装变量好维护。",{"id":13328,"title":5653,"titles":13329,"content":13330,"level":95},"\u002Ftutorials\u002Fjs#字面量读写与点方括号",[5,5634],"const user = {\n  name: '阿蓝',\n  age: 28,\n  'favorite-lang': 'JavaScript',   \u002F\u002F 键名含连字符：写字面量时必须加引号\n  greet() { return '你好' }        \u002F\u002F 方法简写（等价于 greet: function () {}）\n}\n\nuser.name              \u002F\u002F '阿蓝'，点访问：键名是合法标识符时用\nuser['favorite-lang']  \u002F\u002F 方括号访问：键名是变量或是非法标识符时必须用\nconst key = 'age'\nuser[key]              \u002F\u002F 28 —— 方括号里放表达式\n\nuser.city = '杭州'      \u002F\u002F 新增或改写\ndelete user.city       \u002F\u002F 删除（少用；改成置 null 或重建对象更清晰）\n'age' in user          \u002F\u002F true，判断键是否存在\nuser.email ?? '未填'    \u002F\u002F 读可能不存在的键 → undefined，用 ?? 兜底",{"id":13332,"title":5832,"titles":13333,"content":13334,"level":95},"\u002Ftutorials\u002Fjs#属性的简写与解构",[5,5634],"const name = '阿蓝', age = 28\nconst user = { name, age }        \u002F\u002F 属性简写：变量名即键名\n\nconst { name: nick, age = 18 } = user   \u002F\u002F 解构：取出并改名、给默认值\nconst { name, ...rest } = user          \u002F\u002F 剩余属性收成新对象（浅拷贝） 解构可以嵌套（const { profile: { city } } = user），函数参数位置也常用：function render({ title, date = '今天' }) { … }——调用方传对象，顺序无关、可省略可读性好。",{"id":13336,"title":5946,"titles":13337,"content":13338,"level":95},"\u002Ftutorials\u002Fjs#遍历与键值对",[5,5634],"Object.keys(user)      \u002F\u002F ['name', 'age', 'favorite-lang'] 键数组\nObject.values(user)    \u002F\u002F 值数组\nObject.entries(user)   \u002F\u002F [[键, 值], …]，配合 for...of 或 Object.fromEntries\nObject.assign({}, user)      \u002F\u002F 浅拷贝合并（老写法）\nconst clone = { ...user }    \u002F\u002F 展开浅拷贝，等价且更常用\n{ ...a, ...b }               \u002F\u002F 合并：同名键后者覆盖前者 遍历对象的位置回忆第五章：for...of 不能直接用在对象上（对象不是可迭代的），要 Object.entries() 转成数组再 for...of。",{"id":13340,"title":6057,"titles":13341,"content":13342,"level":95},"\u002Ftutorials\u002Fjs#引用语义的三个现实后果",[5,5634],"第三章说过「对象传引用」，落到日常就是三件事： const a = { n: 1 }\nconst b = a          \u002F\u002F 别名：不是拷贝！\nb.n = 2              \u002F\u002F a.n 也变成 2\n\nconst c = { ...a }   \u002F\u002F 浅拷贝：第一层分开，嵌套对象仍共享\na.inner === c.inner  \u002F\u002F 若 a 有嵌套对象，这是 true\n\nJSON.parse(JSON.stringify(obj))   \u002F\u002F 深拷贝的「穷人版」：丢函数、丢 undefined、日期变字符串 以及「不可变更新」的习惯：改状态时别原地改，创建一个改好的新对象。这一习惯对后面 DOM\u002F框架更新（以及本站 Vue 代码）至关重要： \u002F\u002F 原地改：谁引用了它都会被牵连\nuser.age = 29\n\n\u002F\u002F 不可变更新：生成新对象，旧对象保持原样\nconst nextUser = { ...user, age: 29 }",{"id":13344,"title":6233,"titles":13345,"content":13346,"level":95},"\u002Ftutorials\u002Fjs#什么时候用对象什么时候用-map",[5,5634],"键都是字符串\u002F符号、结构固定（像一个「记录」）→ 对象字面量；键是动态的、任意类型的，或频繁增删 → Map： const cache = new Map()\ncache.set('k', 1).set(otherObj, 2)   \u002F\u002F 键可以是对象，且清楚知道自己有几项\ncache.get('k')       \u002F\u002F 1\ncache.has(otherObj)  \u002F\u002F true Map 还保证插入顺序、有直接的 size 属性；对象要数数量得 Object.keys(obj).length。日常优先对象，遇到「以对象为键」或大吞吐增删再换 Map。",{"id":13348,"title":6350,"titles":13349,"content":13350,"level":55},"\u002Ftutorials\u002Fjs#十一class把同类对象的模板写成代码",[5],"对象字面量适合单件数据；如果同样结构的对象要反复创建、还要带一批方法，每次都手写字面量就是重复粘贴。这时用 class：先画一张图纸（类），再按图纸批量生产（实例）。new User('阿蓝', 28) 读作「按 User 的图纸造一个阿蓝」。",{"id":13352,"title":6368,"titles":13353,"content":13354,"level":95},"\u002Ftutorials\u002Fjs#constructor-与-new实例是怎么诞生的",[5,6350],"class User {\n  constructor(name, age) {\n    this.name = name   \u002F\u002F this 指向正在被创建的那个实例\n    this.age = age\n  }\n  greet() {\n    return `你好，我是 ${this.name}`   \u002F\u002F 方法省略 function 关键字\n  }\n}\n\nconst alice = new User('阿蓝', 28)\nalice.greet()   \u002F\u002F '你好，我是阿蓝' new 做四件事：创建一个空对象 → 把它的原型指向 User.prototype → 以这个对象为 this 执行 constructor → 返回它。constructor 里放「每个实例各自不同」的数据（名字、年龄）；方法写在类体里，由所有实例共享。",{"id":13356,"title":6523,"titles":13357,"content":13358,"level":95},"\u002Ftutorials\u002Fjs#方法在原型上实例只装数据",[5,6350],"Object.keys(alice)                             \u002F\u002F ['name', 'age']——实例自己的属性\nalice.hasOwnProperty('greet')                  \u002F\u002F false——greet 不在实例自己身上\n'greet' in alice                               \u002F\u002F true——顺着原型链找得到\nObject.getPrototypeOf(alice) === User.prototype  \u002F\u002F true\nObject.getOwnPropertyNames(User.prototype)     \u002F\u002F ['constructor', 'greet'] 读 alice.greet 时，引擎先看实例自己有没有，没有就顺着 User.prototype 找——找到才执行。这条「实例 → 类原型 → 父类原型 → Object.prototype → null」的查找路径就是原型链。方法只存一份放在原型上，而不是每个实例复制一份，这是它省内存的原因。",{"id":13360,"title":6630,"titles":13361,"content":13362,"level":95},"\u002Ftutorials\u002Fjs#getter把算出来的值伪装成属性",[5,6350],"class User {\n  \u002F\u002F …constructor 同上\n  get profile() {\n    return `${this.name}（${this.age} 岁）`\n  }\n}\n\nalice.profile   \u002F\u002F '阿蓝（28 岁）'——写法像读属性，实际执行函数 getter 适合由已有数据派生的值：不存副本、每次读取实时计算，也就不会出现「改了 name 却忘了同步副本」的不一致。setter（set profile(v) { … }）同理，把一次赋值变成校验或转换的入口。",{"id":13364,"title":13034,"titles":13365,"content":13366,"level":95},"\u002Ftutorials\u002Fjs#私有字段-开头",[5,6350],"第七章用闭包实现过「外部改不了」的私有状态，语言的现代方案是 # 字段： class Counter {\n  #count = 0                    \u002F\u002F 私有字段：# 开头\n  increment() { return ++this.#count }\n}\n\nconst c = new Counter()\nc.increment()   \u002F\u002F 1\nc.count         \u002F\u002F undefined——外部读不到 #count\nc.#count        \u002F\u002F 语法错误：类外部根本写不出这个表达式 # 不是命名约定，是语法层面的封闭：外部看不见、改不动，只有类自己的方法能碰。余额、口令这类「绝不外泄」的数据就用它包装。",{"id":13368,"title":6829,"titles":13369,"content":13370,"level":95},"\u002Ftutorials\u002Fjs#static属于类整体不属于实例的成员",[5,6350],"class User {\n  static population = 0   \u002F\u002F 静态字段\n  constructor(name) {\n    this.name = name\n    User.population++\n  }\n}\n\nUser.population   \u002F\u002F 3——问类要，不是问实例要\nalice.population  \u002F\u002F undefined——实例身上没有 static 描述类整体的状态或工具：实例计数、User.fromJSON() 这样的工厂方法、配置常量。判断标准：这件事跟某一个实例有关（alice.greet()），还是跟类整体有关（User.population）。",{"id":13372,"title":6936,"titles":13373,"content":13374,"level":95},"\u002Ftutorials\u002Fjs#继承extends-与-super",[5,6350],"class Admin extends User {\n  constructor(name, age, level) {\n    super(name, age)          \u002F\u002F 先调父类 constructor，把父类那半建好\n    this.level = level        \u002F\u002F 再补子类独有的属性\n  }\n  greet() {\n    return `${super.greet()}，也是管理员`   \u002F\u002F 覆盖父类方法，同时保留父类实现\n  }\n}\n\nconst admin = new Admin('青梧', 30, 3)\nadmin instanceof Admin   \u002F\u002F true\nadmin instanceof User    \u002F\u002F true——原型链把两代连在一起\nalice instanceof Admin   \u002F\u002F false——普通用户不是管理员 子类 constructor 里有一条硬规则：必须先 super() 才能使用 this——父类那半没建好之前，this 还不存在。子类不写 constructor 时，会自动调用父类 constructor。",{"id":13376,"title":7126,"titles":13377,"content":13378,"level":95},"\u002Ftutorials\u002Fjs#什么时候用-class",[5,6350],"要批量制造结构相同、自带行为的对象（用户、订单、游戏角色）→ class 只有一个存数据的单件对象 → 对象字面量就够，不必包一层类 一堆互相无状态的纯计算 → 普通函数或模块组织更合适 判断标准不是「高级不高级」，而是「有没有实例这回事」。没有 new 的冲动，就用不着 class。",{"id":13380,"title":7158,"titles":13381,"content":7161,"level":55},"\u002Ftutorials\u002Fjs#十二错误处理错了以后怎么办",[5],{"id":13383,"title":7165,"titles":13384,"content":13385,"level":95},"\u002Ftutorials\u002Fjs#错误是对象error-自带现场信息",[5,7158],"try {\n  JSON.parse('{oops}')\n} catch (err) {\n  err.name      \u002F\u002F 'SyntaxError'\n  err.message   \u002F\u002F 'Unexpected token o in JSON…'（措辞各浏览器略有差异）\n  err.stack     \u002F\u002F 从出错点出发的完整调用栈快照\n} try 块里任意一句抛错，本块剩下的都不执行，立刻跳到 catch；catch 跑完，程序从整个 try\u002Fcatch 之后继续。name 是错误类型，message 是人话描述，stack 是调试时最有价值的部分——顺着它能找到谁一路调用到这个出错点。",{"id":13387,"title":7250,"titles":13388,"content":13389,"level":95},"\u002Ftutorials\u002Fjs#throw主动把错误扔出去",[5,7158],"function divide(a, b) {\n  if (b === 0) throw new RangeError('除数不能为 0')\n  return a \u002F b\n} throw 后面可以跟任何值，但永远抛 Error 实例：只有它自带 name \u002F message \u002F stack，能被工具链识别。抛裸字符串（throw '出错啦'）会让 catch 方无从下手。",{"id":13391,"title":7330,"titles":13392,"content":13393,"level":95},"\u002Ftutorials\u002Fjs#try-catch-finally完整形态",[5,7158],"function readSetting(key) {\n  let text\n  try {\n    text = localStorage.getItem(key)\n    return text === null ? null : JSON.parse(text)\n  } catch (err) {\n    console.warn('读设置失败，用默认值', err.message)\n    return null\n  } finally {\n    console.log('无论成败都会执行')   \u002F\u002F 释放资源、收尾的工作放这里\n  }\n} finally 的特别之处：即使 try 里写了 return，它也先执行完（返回值先被暂存，finally 跑完才真正返回）。连接、文件句柄这类「用完必须关」的东西放 finally 里。",{"id":13395,"title":7480,"titles":13396,"content":13397,"level":95},"\u002Ftutorials\u002Fjs#内置错误类型与自定义错误",[5,7158],"常见类型：TypeError（对 null 读属性、调用了非函数）、ReferenceError（变量不存在）、RangeError（数值越界）、SyntaxError。要区分两种情况：代码本体的语法错误在解析阶段就挂了，try\u002Fcatch 根本拦不到；而 JSON.parse 收到坏数据时抛的 SyntaxError 是运行时的，能拦。 业务错误则继承 Error 自己定义： class ValidationError extends Error {\n  constructor(field, message) {\n    super(message)                 \u002F\u002F Error 负责装好 message \u002F stack\n    this.name = 'ValidationError'  \u002F\u002F 覆盖默认的 'Error'\n    this.field = field             \u002F\u002F 自定义信息：出错的是哪个字段\n  }\n}\n\ntry {\n  validate(input)\n} catch (err) {\n  if (err instanceof ValidationError) {\n    showFieldError(err.field, err.message)   \u002F\u002F 认识的错误 → 就地处理\n  } else {\n    throw err                                \u002F\u002F 不认识的 → 原样再抛\n  }\n} 自定义错误的价值在于「抛出方和捕获方有了约定」：instanceof 一判，字段名、错误文案都在，提示就能精确落到那一个输入框上。",{"id":13399,"title":7679,"titles":13400,"content":13401,"level":95},"\u002Ftutorials\u002Fjs#捕获的分寸只处理你懂的错",[5,7158],"认识的错误就处理：给用户一个能走下去的结果（默认值、字段提示） 不认识的错误原样 throw err：里面可能藏着程序 bug，别假装没事 绝不空 catch：catch {} 把现场直接抹掉，是日后最难查的 bug 来源 能提前判断的别拿异常当流程控制：if (b === 0) return null 比 try\u002Fcatch 更直白，异常留给「意外」",{"id":13403,"title":7721,"titles":13404,"content":7724,"level":55},"\u002Ftutorials\u002Fjs#十三dom-查询怎么在页面里找到元素",[5],{"id":13406,"title":7728,"titles":13407,"content":13408,"level":95},"\u002Ftutorials\u002Fjs#两个主力queryselector-与-queryselectorall",[5,7721],"document.querySelector('.card')       \u002F\u002F 第一个匹配 → 元素；找不到 → null\ndocument.querySelectorAll('.card')    \u002F\u002F 全部匹配 → NodeList；找不到 → 空集合\ndocument.getElementById('staff-list') \u002F\u002F 按 id 直取 括号里写的就是 CSS 教程里那套选择器，原样搬来：.tag、#list、li.card、[data-role=\"backend\"]，还能组合 ul > li:first-child。CSS 学过的选择器知识在这里全额复用。 两个高频要点： querySelector 找不到返回 null——习惯写法是先判空再操作（if (!el) return），或者随手 ?. querySelectorAll 返回的 NodeList 不是数组：有 forEach、能 for...of，但没有 map \u002F filter；要加工先 Array.from()",{"id":13410,"title":7850,"titles":13411,"content":13412,"level":95},"\u002Ftutorials\u002Fjs#顺着关系走别只会从-document-全局查",[5,7721],"const list = document.getElementById('staff-list')\nlist.children.length         \u002F\u002F 子元素个数（只算元素，不含文本节点）\nlist.firstElementChild       \u002F\u002F 第一个子元素\ncard.parentElement           \u002F\u002F 父元素\ncard.nextElementSibling      \u002F\u002F 下一个兄弟元素 从一个大容器出发逐层缩小范围，比从 document 全查更准（页面别处有同名 class 也不会串）也更快。",{"id":13414,"title":7918,"titles":13415,"content":13416,"level":95},"\u002Ftutorials\u002Fjs#closest-与-matches向上找与自我判断",[5,7721],"span.closest('.card')       \u002F\u002F 从自己出发逐层向上，返回第一个匹配的祖先；找不到 → null\ncard.matches('.highlight')  \u002F\u002F 我匹配这个选择器吗 → true \u002F false closest 是第十五章「事件委托」的核心工具：不管点中卡片内部多深的小元素，closest('.card') 总能把你带回那张卡。",{"id":13418,"title":7971,"titles":13419,"content":13420,"level":95},"\u002Ftutorials\u002Fjs#live-集合与静态快照别记混",[5,7721],"const live = list.getElementsByClassName('card')  \u002F\u002F live：DOM 变了它跟着变\nconst snap = list.querySelectorAll('.card')       \u002F\u002F 静态快照：查询那一刻定死 老接口（getElementsByClassName \u002F getElementsByTagName）返回实时联动的 HTMLCollection，querySelectorAll 返回定格快照的 NodeList。示例里追加一张卡片就能看到两种行为分道扬镳。日常首选 querySelectorAll，行为可预期；需要最新结果重新查询一次就好。",{"id":13422,"title":8052,"titles":13423,"content":13424,"level":55},"\u002Ftutorials\u002Fjs#十四修改-dom怎么把页面改出内容",[5],"找到元素之后就是「改」。手段按重要性排序：创建与挂载、改内容、改状态、删除，最后组装成日常最常用的结构——渲染函数。",{"id":13426,"title":8062,"titles":13427,"content":13428,"level":95},"\u002Ftutorials\u002Fjs#创建拼装挂载三步造一个节点",[5,8052],"const li = document.createElement('li')   \u002F\u002F ① 造：游离在内存，还没上树\nli.className = 'item'\nli.textContent = '写周报'\n\nconst list = document.querySelector('#todo-list')\nlist.append(li)      \u002F\u002F ② 挂到子节点末尾；prepend 挂到开头 先在内存里把子树拼装好、最后一步挂树，只触发一次布局；边挂边补会反复触发，规模一大就卡。插入全家桶还有 before \u002F after（自己的前\u002F后）、replaceWith；删除用 el.remove()；清空子节点用 list.replaceChildren()。",{"id":13430,"title":8172,"titles":13431,"content":13432,"level":95},"\u002Ftutorials\u002Fjs#改内容textcontent-与-innerhtml-的分界",[5,8052],"el.textContent = '\u003Cb>加粗\u003C\u002Fb>'   \u002F\u002F 原样当纯文本：页面上就是这串字符\nel.innerHTML  = '\u003Cb>加粗\u003C\u002Fb>'    \u002F\u002F 交给浏览器解析：真的变成加粗文字 铁律：挂用户输入永远走 textContent。innerHTML 只用于你完全掌控的模板字符串——评论框里塞一段 \u003Cimg onerror=…>，innerHTML 会当场把脚本执行了，这就是 XSS 的入口。",{"id":13434,"title":8217,"titles":13435,"content":13436,"level":95},"\u002Ftutorials\u002Fjs#改状态classlist-与-dataset",[5,8052],"el.classList.add('done')       \u002F\u002F 加\nel.classList.remove('done')    \u002F\u002F 删\nel.classList.toggle('done')    \u002F\u002F 切换，返回最终是否含有\nel.classList.contains('done')  \u002F\u002F 查\n\nel.dataset.index = 2           \u002F\u002F 写入 → 页面上是 data-index=\"2\"\nel.dataset.index               \u002F\u002F 读回——永远是字符串 样式交给 class、附加状态记进 data-*、真正的数据放 JS 变量——「三权分立」。不必用 el.style.xxx 一个个敷（除非是真正动态计算的值），换一批 class 更整洁。",{"id":13438,"title":8327,"titles":13439,"content":13440,"level":95},"\u002Ftutorials\u002Fjs#渲染函数数据-dom变了就重建",[5,8052],"let tasks = [ … ]\n\nfunction renderTasks() {\n  list.replaceChildren()                 \u002F\u002F 清空\n  tasks.forEach((task, index) => {\n    const li = document.createElement('li')\n    li.dataset.index = index\n    li.textContent = task.text\n    list.append(li)\n  })\n} 数据变了就改数组、然后 renderTasks() 整块重建。看着「笨」（全删重造），换来一个关键好处：任何时刻 DOM 都严格等于数据的投影，不存在「忘了同步某处」的状态不一致——这正是 Vue 等框架的思路源头。反过来，用 el.remove() 直接删节点、绕过数据，下一次重建它就会复活：数据才是真相源。",{"id":13442,"title":8469,"titles":13443,"content":13444,"level":95},"\u002Ftutorials\u002Fjs#事件委托预告重建后按钮还能点",[5,8052],"整块重建后，旧节点上的监听器全部作废。如果每次重建都重新绑定，代码会碎成一地 addEventListener。更省事的做法是把监听器挂在容器上，用 e.target.closest('.item') 判断点击落点——容器绑一次，子孙随便换。它的完整原理就是下一章。",{"id":13446,"title":8491,"titles":13447,"content":13448,"level":55},"\u002Ftutorials\u002Fjs#十五事件用户和页面的对话",[5],"前面几章一直在用 addEventListener，现在把它的全貌补齐：事件对象里有什么、一次点击到底走了哪条路、为什么监听器该挂在容器上而不是每个按钮上。",{"id":13450,"title":8501,"titles":13451,"content":13452,"level":95},"\u002Ftutorials\u002Fjs#监听移除与选项",[5,8491],"const onClick = (e) => { … }\nbtn.addEventListener('click', onClick)\n\nbtn.addEventListener('click', onClick, { once: true })     \u002F\u002F 触发一次后自动移除\nbtn.addEventListener('click', onClick, { capture: true })  \u002F\u002F 在捕获阶段接\nbtn.removeEventListener('click', onClick)  \u002F\u002F 移除：必须传「同一个函数引用」 移除有个高频坑：removeEventListener 靠函数引用识别，传一个新写的箭头函数毫无作用——处理函数要先存成变量，注册和移除传的是同一个。",{"id":13454,"title":8612,"titles":13455,"content":13456,"level":95},"\u002Ftutorials\u002Fjs#事件对象target-与-currenttarget",[5,8491],"list.addEventListener('click', (e) => {\n  e.target          \u002F\u002F 实际被点中的元素（可能很深，比如按钮里的文字）\n  e.currentTarget   \u002F\u002F 监听器挂着的元素（这里就是 list）\n  e.type            \u002F\u002F 'click'\n}) 事件从 document 向下「捕获」、到达目标、再向上「冒泡」。监听器默认在冒泡阶段触发；加了 { capture: true } 就在下行阶段触发。点内层元素时祖孙三代的监听器都会收到——示例里把这条链完整打印了一遍，对照着看一遍就不会再记混。",{"id":13458,"title":8678,"titles":13459,"content":13460,"level":95},"\u002Ftutorials\u002Fjs#事件委托一器多用",[5,8491],"冒泡带来一个工程上的大招。列表有 100 个条目，逐个绑监听器就是 100 个函数，重建后还要全部重绑。换成只在容器上绑一个： ul.addEventListener('click', (e) => {\n  const li = e.target.closest('.item')   \u002F\u002F 不管点得多深，先回到条目\n  if (!li) return\n  if (e.target.closest('.del')) { li.remove(); return }   \u002F\u002F 点中删除按钮\n  li.classList.toggle('done')                              \u002F\u002F 否则算切换完成\n}) 好处一次说完：动态新增的条目自动生效（不用补绑）、重建后不失效（容器没换）、监听器数量与条目数无关（省内存）。这也是上一章渲染函数敢「全删重造」的底气。",{"id":13462,"title":8816,"titles":13463,"content":13464,"level":95},"\u002Ftutorials\u002Fjs#preventdefault把浏览器的默认动作拦下来",[5,8491],"浏览器对很多事件有内置动作：点链接会跳转、提交表单会刷新页面、右键弹菜单。想接管就先拦： link.addEventListener('click', (e) => {\n  e.preventDefault()   \u002F\u002F 取消默认动作\n  \u002F\u002F 校验、确认框、异步提交……自己安排\n}) 两个「拦」别用混：preventDefault 拦默认动作（事件本身还能继续冒泡）；stopPropagation 拦事件传播（默认动作不受影响）。",{"id":13466,"title":8887,"titles":13467,"content":13468,"level":95},"\u002Ftutorials\u002Fjs#常见事件类型速查",[5,8491],"事件 触发时机 常用场景 click 点击（键盘在按钮上回车也算） 几乎所有交互 input 每次输入、粘贴、删除 实时校验、搜索建议 change 值确定改变且失焦（select \u002F checkbox 立即） 下拉选择、勾选 submit 表单提交 拦默认、校验通过再放行 keydown \u002F keyup 按下 \u002F 松开键，e.key 拿键名 回车提交、快捷键 mouseenter \u002F mouseleave 鼠标进入 \u002F 离开（不冒泡） 悬停效果 scroll 滚动 吸顶、懒加载 用到再查、查了记住用法即可；找不到名字时去 MDN 搜「事件名 + MDN」是最快的路。",{"id":13470,"title":9017,"titles":13471,"content":9020,"level":55},"\u002Ftutorials\u002Fjs#十六表单读取校验与提交",[5],{"id":13473,"title":9024,"titles":13474,"content":13475,"level":95},"\u002Ftutorials\u002Fjs#读取控件value-与-checked",[5,9017],"\u003Cinput id=\"f-name\" type=\"text\">\n\u003Cselect id=\"f-city\">\u003Coption>杭州\u003C\u002Foption>…\u003C\u002Fselect>\n\u003Cinput type=\"checkbox\" name=\"interest\" value=\"js\"> nameInput.value        \u002F\u002F 文本框、密码框、下拉框：都用 .value（字符串）\ncheckbox.checked       \u002F\u002F 复选框、单选框：用 .checked（true \u002F false）\nform.querySelectorAll('input[name=\"interest\"]:checked')   \u002F\u002F 只挑勾上的 :checked 是 CSS 伪类选择器在查询里的又一次复用。整表一把抓用 new FormData(form)：可遍历出「键 → 值」序列。两个细节要记住：没勾的复选框根本不在 FormData 里；同名多选会各占一条。 手工组装对象 vs FormData 各有场景：手工版本只收想要的字段、能把值转成数字、勾选项收成数组；FormData 原貌打包，适合整个表单原样发给后端。",{"id":13477,"title":9172,"titles":13478,"content":13479,"level":95},"\u002Ftutorials\u002Fjs#input-事件输入时实时反馈",[5,9017],"emailInput.addEventListener('input', () => {\n  const ok = \u002F^\\S+@\\S+\\.\\S+$\u002F.test(emailInput.value.trim())\n  hint.textContent = ok ? '格式看起来没问题' : '需要 名称@域名 的形式'\n}) input 每次改动都触发（打一个字也触发），用来做即时提示；change 在失焦且值改变后才触发，适合「确定后再生效」的场景。",{"id":13481,"title":9284,"titles":13482,"content":13483,"level":95},"\u002Ftutorials\u002Fjs#submit拦下默认提交统一校验",[5,9017],"form.addEventListener('submit', (e) => {\n  e.preventDefault()      \u002F\u002F 不拦的话：页面刷新，你所有的 JS 状态清零\n  const errors = {}       \u002F\u002F 「字段 → 提示语」的错误对象\n  if (!name.value.trim()) errors.name = '名字不能为空'\n  if (!\u002F^\\S+@\\S+\\.\\S+$\u002F.test(email.value)) errors.email = '邮箱格式不对'\n  if (!agree.checked) errors.agree = '需要先同意条款'\n  renderErrors(errors)    \u002F\u002F 用 Object.entries 逐个写到字段旁边\n  if (Object.keys(errors).length) return   \u002F\u002F 有错就停\n  \u002F\u002F 校验通过：组装数据，交给后端（第十九章 fetch）\n}) 三个要点：e.preventDefault() 必须是第一步；校验结果用「字段 → 提示语」的对象表达，Object.entries 就能派位到界面（第十章学的遍历在这里有了正经工作）；最后一个 return 把「不通过」挡在门外。想再加体验细节，可以 focus() 到第一个出错字段。",{"id":13485,"title":9477,"titles":13486,"content":13487,"level":95},"\u002Ftutorials\u002Fjs#提交按钮与回车是同一个事件",[5,9017],"\u003Cbutton type=\"submit\"> 和输入框里的回车，触发的都是 submit——两个入口，一份逻辑。反过来要小心：form 里的 \u003Cbutton> 不写 type 时默认就是 submit，想当普通按钮用必须显式写 type=\"button\"，这个默认值坑过无数人。",{"id":13489,"title":9506,"titles":13490,"content":9509,"level":55},"\u002Ftutorials\u002Fjs#十七bom-与浏览器存储",[5],{"id":13492,"title":9513,"titles":13493,"content":13494,"level":95},"\u002Ftutorials\u002Fjs#window全局对象也是窗口",[5,9506],"第七章讲过作用域链：变量从里往外找，链的最后一站是全局作用域。在浏览器里，这个「最后一站」有个具体的名字——window。它身兼两职： 它是全局对象：var 声明的变量、函数声明，都会自动变成 window 的属性 它是浏览器窗口：视口尺寸、弹窗、定时器（下一章）都挂在它名下 var appName = 'demo'\nfunction boot() {}\nwindow.appName       \u002F\u002F 'demo'：var 和函数声明自动成为 window 的属性\nlet secret = 1\nwindow.secret        \u002F\u002F undefined：let\u002Fconst 不上 window\n\nwindow.innerWidth    \u002F\u002F 视口宽（CSS 像素）\nwindow.alert('!')    \u002F\u002F 这些全局函数，其实都是 window 的方法\nalert('!')           \u002F\u002F 平时省略前缀",{"id":13496,"title":9629,"titles":13497,"content":13498,"level":95},"\u002Ftutorials\u002Fjs#navigator环境信息与别做浏览器侦探",[5,9506],"navigator 提供宿主环境的信息：navigator.language（界面语言）、navigator.onLine（粗略的联网状态）、navigator.userAgent（浏览器标识字符串）。 UA 字符串是历史包袱：浏览器们互相伪装，字符串里塞满了别人的名字，解析它来判断「用户用什么浏览器」非常不可靠。正确姿势是特性检测——不看你是谁，看你有什么能力： if ('geolocation' in navigator) {\n  \u002F\u002F 有定位能力才调用，没有就换方案\n}\n\nwindow.matchMedia('(prefers-color-scheme: dark)').matches\n\u002F\u002F 系统偏好深色？本站的明暗主题就是靠它读初始偏好的",{"id":13500,"title":9703,"titles":13501,"content":13502,"level":95},"\u002Ftutorials\u002Fjs#location地址栏的入口",[5,9506],"location 描述当前页面地址，改它就等于导航。 location.href       \u002F\u002F 完整地址：读是字符串，写是跳转\nlocation.pathname   \u002F\u002F 路径部分\nlocation.search     \u002F\u002F '?page=2'\nlocation.hash       \u002F\u002F '#who=alan'，井号起\n\nlocation.hash = 'who=alan'   \u002F\u002F 只改 hash：不刷新页面、会进历史记录\nlocation.reload()            \u002F\u002F 重新加载（慎用，页面状态全丢） 地址栏参数交给 URLSearchParams 解析，比自己 split('&') 稳： \u002F\u002F 地址为 \u002Fsearch?page=2&tag=js&tag=dom 时\nconst params = new URLSearchParams(location.search)\nparams.get('page')     \u002F\u002F '2'——注意永远是字符串，要算数得自己转\nparams.get('tag')      \u002F\u002F 'js'：同名参数只返回第一个\nparams.getAll('tag')   \u002F\u002F ['js', 'dom']：全都要用 getAll\nparams.has('page')     \u002F\u002F true",{"id":13504,"title":9877,"titles":13505,"content":13506,"level":95},"\u002Ftutorials\u002Fjs#history前进与后退",[5,9506],"history.length 是当前会话的历史条数；back() \u002F forward() 等价于点浏览器的前进后退。更常用的是 pushState \u002F replaceState——不刷新页面也能改地址，前端路由（比如本站切换页面）的地基： history.pushState({ page: 1 }, '', '\u002Flist?page=1')    \u002F\u002F 新增一条历史：地址变了，页面没动\nhistory.replaceState({ page: 2 }, '', '\u002Flist?page=2') \u002F\u002F 替换当前这条，不加新记录 这对 API 受同源策略严格约束：本章示例跑在沙箱 iframe 里（没有自己的「源」），一调 pushState 就抛 SecurityError——所以示例里只演示 hash 这种「改地址不刷新」的手段。",{"id":13508,"title":9959,"titles":13509,"content":13510,"level":95},"\u002Ftutorials\u002Fjs#alert-confirm-prompt内置弹窗",[5,9506],"alert('知道了')                            \u002F\u002F 只有「确定」，无返回值\nconst ok = confirm('确定删除吗？')          \u002F\u002F 确定 true \u002F 取消 false\nconst nick = prompt('你叫什么？', '默认值')  \u002F\u002F 确定 → 字符串（可为空串）；取消 → null 三个都是阻塞式：脚本会停住等你点完，而且样式不可定制。正式产品里基本不用（替换成页面内的对话框），但调试和小工具里很顺手。",{"id":13512,"title":10038,"titles":13513,"content":13514,"level":95},"\u002Ftutorials\u002Fjs#存储三件套localstoragesessionstoragecookie",[5,9506],"浏览器会在本地为用户存数据，三个容器，用途完全不同： localStorage sessionStorage cookie 生命周期 永久，除非主动清除 标签页关掉即失 可设过期时间 作用范围 同源的所有标签页 只在当前标签页 同源 + 路径规则 容量 约 5MB 约 5MB 4KB 会发给服务器吗 不会 不会 每次请求都带上 典型用途 主题、草稿、偏好 一次性的流程状态 会话凭证（给后端看） 口诀：前端自己用的数据放 localStorage \u002F sessionStorage；要让后端看到的才用 cookie。 两种 storage 的 API 一模一样（第十章末尾见过 Map——这套是它的老前辈）： localStorage.setItem('theme', 'dark')   \u002F\u002F 存\nlocalStorage.getItem('theme')           \u002F\u002F 'dark'；键不存在返回 null（不是 undefined）\nlocalStorage.removeItem('theme')        \u002F\u002F 删一条\nlocalStorage.clear()                    \u002F\u002F 清空——会波及同源下的所有代码，慎用\nlocalStorage.length                     \u002F\u002F 现有几条",{"id":13516,"title":10224,"titles":13517,"content":13518,"level":95},"\u002Ftutorials\u002Fjs#只认字符串json-来中转",[5,9506],"两个必记的点：存进去什么，取出来都是字符串；想存对象\u002F数组要用 JSON 中转。 localStorage.setItem('count', 7)\nconst got = localStorage.getItem('count')\ntypeof got                \u002F\u002F 'string'！拿到的是 '7'，要算数得 Number(got)\n\nconst user = { name: '阿蓝', visits: 3 }\nlocalStorage.setItem('app:user', JSON.stringify(user))     \u002F\u002F 序列化进去\nconst saved = JSON.parse(localStorage.getItem('app:user')) \u002F\u002F 反序列化出来\nsaved.name                \u002F\u002F '阿蓝'，类型保住了\n\nJSON.stringify({ fn() {}, u: undefined })  \u002F\u002F '{}'——函数和 undefined 直接消失\nJSON.stringify(new Date())                 \u002F\u002F 日期变字符串；要用 Date 得自己 new 键名建议带统一前缀（app:user、app:theme），避免和同源下其他代码撞名。这套 JSON 往返，也是下一章 fetch 与后端交换数据的预演。",{"id":13520,"title":10431,"titles":13521,"content":13522,"level":95},"\u002Ftutorials\u002Fjs#存不进去怎么办先探测再兜底",[5,9506],"localStorage 并非总能用：隐私模式、浏览器设置、沙箱 iframe 都会让它一写就抛异常（SecurityError）；写爆容量（约 5MB）会抛 QuotaExceededError。所以正经代码不直接假设它可用： try {\n  localStorage.setItem('__probe__', '1')\n  localStorage.removeItem('__probe__')\n  \u002F\u002F 没抛异常：可用\n} catch (err) {\n  \u002F\u002F SecurityError 等：不可用，降级到内存 Map——程序换个方式继续跑\n} 本章示例就跑在沙箱 iframe 里，你会亲眼看到探测失败、代码自动降级到内存的过程——正是第十二章的原则：预判得到的错误就处理掉，让程序活着。大文件（超过 5MB）不该硬塞 localStorage，那是 IndexedDB 的地盘。 最后一个安全习惯：localStorage 里的一切，同源脚本都能读。所以密码、登录凭证不要放进去——第十四章说过，XSS 注入的脚本第一件事就是翻 localStorage。 下一章换个维度：前面所有代码都是「立刻执行」的，下一章解决「等一等再执行」——定时器、回调、Promise 与 async \u002F await。",{"id":13524,"title":10527,"titles":13525,"content":13526,"level":55},"\u002Ftutorials\u002Fjs#十八异步等一等再执行",[5],"前十七章的代码都是「一行做完再做下一行」。但浏览器里很多事急不来：网络请求要几百毫秒，定时器要等够时间，用户什么时候点按钮完全没谱。JS 的策略是不等——先往下走，事情完成时回头叫一个函数。这就是异步。",{"id":13528,"title":10538,"titles":13529,"content":13530,"level":95},"\u002Ftutorials\u002Fjs#定时器把函数预约到未来",[5,10527],"setTimeout 把函数预约到「至少 N 毫秒后」执行，返回一个编号 id；clearTimeout(id) 可以反悔。setInterval 每隔 N 毫秒重复执行，配 clearInterval： const id = setTimeout(() => { console.log('1 秒后见') }, 1000)\nclearTimeout(id)          \u002F\u002F 反悔：还没到点就取消\n\nconst clockId = setInterval(() => { console.log(new Date()) }, 1000)\nclearInterval(clockId)    \u002F\u002F 停止 关键词是「至少」：定时器不会打断正在执行的代码。如果一个循环要跑 3 秒，你设置的 1 秒定时器也得排队等它跑完： setTimeout(() => console.log('定时器'), 0)\nfor (let i = 0; i \u003C 3; i++) console.log('循环 ' + i)\n\u002F\u002F 输出：循环 0、循环 1、循环 2、定时器\n\u002F\u002F 0 毫秒也不是立刻：先让当前这轮同步代码跑完",{"id":13532,"title":10738,"titles":13533,"content":13534,"level":95},"\u002Ftutorials\u002Fjs#回调结果出来时叫我",[5,10527],"早期 JS 的异步全写回调：把「拿到结果后要做的事」包成函数传进去。约定俗成的回调风格是错误优先——第一个参数放着错误： orderTea((err, result) => {\n  if (err) {\n    console.log('失败：' + err.message)\n    return\n  }\n  console.log('成功：' + result)\n}) 能跑，但有著名的「回调地狱」问题：上一步的结果是下一步的输入，只能一层套一层，代码向右缩进成金字塔，错误处理还要每层重复。",{"id":13536,"title":10825,"titles":13537,"content":13538,"level":95},"\u002Ftutorials\u002Fjs#promise给未来的结果一个盒子",[5,10527],"Promise 是一个代表未来结果的对象，三态：pending（等结果）→ fulfilled（成功）或 rejected（失败），状态一旦落定就不可逆。 function orderTea() {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() > 0.3) resolve('一杯珍珠奶茶')      \u002F\u002F 兑现\n      else reject(new Error('打烊了'))                       \u002F\u002F 拒付\n    }, 800)\n  })\n}\n\norderTea()\n  .then((tea) => console.log('拿到：' + tea))       \u002F\u002F 成功走这里\n  .catch((err) => console.log('出错：' + err.message)) \u002F\u002F 失败走这里\n  .finally(() => console.log('收尾'))                \u002F\u002F 都走这里 三个值得记住的点： .then 里 return 的东西会被包成新的 Promise，所以可以链式往下串——这才解决了回调嵌套 new Promise(executor) 里的执行器函数是同步立刻执行的，真正异步的是它注册进去的定时器\u002F网络回调 忘了 .catch，失败会变成「未处理的拒绝」，控制台报 Uncaught (in promise)",{"id":13540,"title":11097,"titles":13541,"content":13542,"level":95},"\u002Ftutorials\u002Fjs#async-await把-promise-写成人话",[5,10527],"async 函数里可以 await 一个 Promise：等它落定，成功直接取出值，失败抛成异常——于是 try\u002Fcatch 就能管异步错误了： async function buyTea() {\n  try {\n    const tea = await orderTea()   \u002F\u002F 等它落定；期间页面照常响应\n    console.log('拿到：' + tea)\n  } catch (err) {\n    console.log('出错：' + err.message)\n  }\n} 注意：await「暂停」的只是这个 async 函数，不是整个页面——函数在 await 处让出控制权，其他事件照常跑，Promise 落定后它从下一行接着走。另外，async 函数永远返回 Promise，所以 buyTea() 拿到的是 Promise 而不是值；想在别处拿值，就继续 await 它。",{"id":13544,"title":11207,"titles":13545,"content":13546,"level":95},"\u002Ftutorials\u002Fjs#串行还是并行别让-await-排队",[5,10527],"常见翻车点：三个互不依赖的任务，写成三个 await 串着等，总耗时是三者之和： await task('洗菜', 600)   \u002F\u002F 一个接着一个……总共约 1800ms\nawait task('切菜', 600)\nawait task('炒菜', 600)\n\nconst names = await Promise.all([task('烧水', 600), task('蒸饭', 600), task('备菜', 600)])\n\u002F\u002F 同时发车，约 600ms 拿到全部结果 Promise.all 一荣俱荣：任一失败，整体立刻以第一个错误 reject。口诀：有依赖才串行，没依赖用 Promise.all。",{"id":13548,"title":11353,"titles":13549,"content":13550,"level":95},"\u002Ftutorials\u002Fjs#微任务与宏任务够用版",[5,10527],"记住一个执行顺序就够用了：同步代码 → 微任务（Promise 回调、await 的续行）→ 宏任务（setTimeout \u002F setInterval 回调）： setTimeout(() => console.log('定时器 0'), 0)\nPromise.resolve().then(() => console.log('Promise'))\nconsole.log('同步代码')\n\u002F\u002F 输出：同步代码 → Promise → 定时器 0 它能解释大部分「为什么日志顺序和我想的不一样」；更深的机制（事件循环、任务队列）用到再补。",{"id":13552,"title":11443,"titles":13553,"content":11446,"level":55},"\u002Ftutorials\u002Fjs#十九fetch-与-json和服务器说上话",[5],{"id":13555,"title":11450,"titles":13556,"content":13557,"level":95},"\u002Ftutorials\u002Fjs#json跨语言的数据交换格式",[5,11443],"JSON（JavaScript Object Notation）长得像 JS 对象字面量，但它是格式不是代码：键必须双引号、没有注释、没有函数，值只允许字符串、数字、布尔、null、数组、对象。你要说规则就一句：能 JSON.stringify 出去、能 JSON.parse 回来，才算合法。 JSON.stringify({ name: '阿蓝', tags: ['js'] })   \u002F\u002F '{\"name\":\"阿蓝\",\"tags\":[\"js\"]}'\nJSON.parse('{\"page\": 2}')                        \u002F\u002F { page: 2 } JSON.parse 遇到非法文本会抛 SyntaxError——内容来自外部（网络、存储、用户）时，必须包 try\u002Fcatch，这是第十二章就定下的纪律。",{"id":13559,"title":11521,"titles":13560,"content":13561,"level":95},"\u002Ftutorials\u002Fjs#fetch-的第一个反直觉404-也算成功",[5,11443],"fetch(url) 返回 Promise，但它 resolve 的条件是「服务器有应答」——404、500 都是有应答，照样 resolve；只有断网、DNS 失败这类网络层错误才 reject。所以拿到结果后必须自己检查 res.ok（2xx 为 true）： resolve ≠ 请求成功，只等于「有回音」 没查 res.ok 的代码，会在 404 时拿着错误页去 json()，然后以一种莫名其妙的方式崩掉 一个完整的「三段式」模板，以后所有 GET 都可以照抄： async function getUsers() {\n  try {\n    const res = await fetch('\u002Fapi\u002Fusers')\n    if (!res.ok) throw new Error('HTTP ' + res.status)\n    return await res.json()\n  } catch (err) {\n    console.error('加载用户失败：', err.message)\n    return []\n  }\n} 读响应体有三个常用方法：res.json()（解析成对象）、res.text()（原始字符串）、res.blob()（二进制）。注意响应体是一次性流：json() 读过了再 text() 会报错，二选一。",{"id":13563,"title":11707,"titles":13564,"content":13565,"level":95},"\u002Ftutorials\u002Fjs#post把数据送上去",[5,11443],"发数据用第二个参数：method、headers、body。body 必须是字符串——对象要先 JSON.stringify；Content-Type: application\u002Fjson 相当于告诉服务器「我给你的是 JSON，请按这个解」： await fetch('\u002Fapi\u002Fusers', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application\u002Fjson' },\n  body: JSON.stringify({ name: '阿蓝', role: 'member' }),\n}) 查询参数用第十七章的 URLSearchParams 拼，省得手写转义： const qs = new URLSearchParams({ q: '闭包', page: 2 })\nfetch('\u002Fapi\u002Fsearch?' + qs)   \u002F\u002F \u002Fapi\u002Fsearch?q=%E9%97%AD%E5%8C%85&page=2",{"id":13567,"title":11847,"titles":13568,"content":13569,"level":95},"\u002Ftutorials\u002Fjs#新手翻车清单",[5,11443],"忘了 await：拿到的是 Promise 不是数据（users.length 是 undefined） 忘了查 res.ok：404 当成功处理 body 忘 JSON.stringify：服务器收到 [object Object] json() 当同步方法用：它是 async 的，要 await 路径写相对 \u002Fapi\u002F... 时和页面不同源会被 CORS 拦：跨源需要服务器在响应头里点头（Access-Control-Allow-Origin），这是后端配置问题，不是 fetch 写法问题 拿网络数据直接塞 innerHTML：XSS 老问题，回顾第十四章",{"id":13571,"title":11903,"titles":13572,"content":13573,"level":95},"\u002Ftutorials\u002Fjs#本章示例的说明",[5,11443],"示例跑在沙箱 iframe 里，没有可用的网络源，所以内置了一个「长得和 fetch 一样」的 fakeFetch（同样 600ms 延迟、同样返回带 status \u002F ok \u002F json() 的响应对象）来完整演示流程：200 \u002F 404 \u002F 500 \u002F 断网四种结局、POST 提交、重新拉取列表。真实项目里把它换成 fetch，流程一字不差。 下一章换个话题：代码越写越多，全塞一个文件迟早失控——模块化怎么拆、怎么 import \u002F export，以及交付前的工程习惯。",{"id":13575,"title":11929,"titles":13576,"content":13577,"level":55},"\u002Ftutorials\u002Fjs#二十模块与工程习惯",[5],"到上一章为止，代码都写在单个 script 里。真实项目动辄几千行，全塞一个文件有三宗罪：找不到、会撞名、没法复用。这一章解决「代码怎么组织」，顺路把交付前该有的工程习惯过一遍。",{"id":13579,"title":11940,"titles":13580,"content":13581,"level":95},"\u002Ftutorials\u002Fjs#模块是什么自带作用域的文件",[5,11929],"一个 JS 文件加上几个 export，就是「模块」（ES Module）。浏览器原生支持，机制有四个要点： 自带作用域：模块顶层的变量不会挂到 window（对比：传统 script 的顶层 var 和函数声明全是全局的，本章示例能看到这个差别的现场） 显式接口：只有 export 出去的东西，外面才拿得到 单例：同一个模块被多个地方 import，也只执行一次，共享同一份内部状态 严格模式：模块里自动开启 'use strict'，第二章那些「别这么干」从建议变成强制 \u003Cscript type=\"module\" src=\".\u002Fmain.js\">\u003C\u002Fscript> 注意：file:\u002F\u002F 直接双击打开会因为 CORS 报错——模块必须经 HTTP 访问（本地起个静态服务，或直接用下节说的构建工具）。",{"id":13583,"title":12013,"titles":13584,"content":13585,"level":95},"\u002Ftutorials\u002Fjs#export-与-import-的几种写法",[5,11929],"\u002F\u002F utils.js —— 输出方\nexport function formatTime(date) { … }    \u002F\u002F 具名导出：一个文件可以有很多个\nexport const MAX = 10\nexport default function render() { … }    \u002F\u002F 默认导出：一个模块至多一个 \u002F\u002F main.js —— 使用方\nimport { formatTime, MAX } from '.\u002Futils.js'    \u002F\u002F 具名导入：名字要对上\nimport { formatTime as fmt } from '.\u002Futils.js'  \u002F\u002F 重命名\nimport render from '.\u002Futils.js'                 \u002F\u002F 默认导入：名字自己起\nimport * as utils from '.\u002Futils.js'             \u002F\u002F 整个模块收成命名空间对象\n\nimport('.\u002Fheavy.js').then((mod) => { … })       \u002F\u002F 动态导入：用到才加载，返回 Promise 两条硬规矩： 路径必须写 .\u002F 或 ..\u002F 开头——写成 'utils.js' 会被当作 npm 包名去找，然后报「找不到模块」 import 会被提升到模块顶部，写在哪里都一样（别指望靠位置控制顺序） default 还是具名？一个模块只做一件事、只有一个主出口 → default；工具集合（一排平级函数）→ 全部具名（更好搜、重构更安全）。团队里保持一致，比纠结选哪个更重要。",{"id":13587,"title":12218,"titles":13588,"content":13589,"level":95},"\u002Ftutorials\u002Fjs#拆文件的原则一条链三个角色",[5,11929],"小型项目够用的分法——utils（纯工具）→ store（数据与业务）→ main（入口\u002F界面）： utils：不碰 DOM、不管业务，进什么出什么，最好测 store：管数据和规则（比如「count 不能超过 limit」），也不碰 DOM main：只当接线员——把用户事件接到 store，把结果渲染到 DOM 依赖方向单向：main → store → utils。反过来（工具函数里 import 业务状态）就是循环依赖噩梦的开端。本章示例把第十四章的迷你待办按这个原则拆成三个文件，每个几十行，看完文件名就知道该去哪改。",{"id":13591,"title":12242,"titles":13592,"content":13593,"level":95},"\u002Ftutorials\u002Fjs#命名与注释改代码的人会谢你",[5,11929],"文件名小写连字符（user-store.js）；函数名用动词短语（createUser、formatDate），布尔用 is\u002Fhas\u002Fcan 开头（isValid、hasNext） 注释写「为什么」，不写「是什么」：i++ \u002F\u002F i 加一 是噪音；\u002F\u002F 兼容 iOS 15 的旧行为 才是信息 删掉注释掉的死代码——git 记着呢，不差这一份",{"id":13595,"title":12284,"titles":13596,"content":13597,"level":95},"\u002Ftutorials\u002Fjs#devtools出-bug-时的三件套",[5,11929],"console 家族：console.table(arr) 把对象数组排成表、console.time \u002F timeEnd 掐耗时、console.error 让日志带颜色和调用栈（本站示例的「迷你控制台」就是它的仿制品） 断点：Sources 面板点行号打断点，鼠标悬停看变量值；条件断点只在条件成立时停下；在代码里写 debugger 语句也会停 Network 面板：上一章的请求全在这：状态码、请求头、响应体——接口挂没挂一眼便知",{"id":13599,"title":12323,"titles":13600,"content":13601,"level":95},"\u002Ftutorials\u002Fjs#从手写-script-到工程化缺的那一环",[5,11929],"现在我们手动引入每个文件：\u003Cscript type=\"module\" src=\".\u002Fmain.js\">，main.js 再 import 其他。项目再大就会遇到新问题：几百个模块逐个写标签不现实、新语法旧浏览器不认、npm 上的包怎么用。这就是构建工具（现在的默认答案是 Vite）出场的原因：你安心写模块和装包，它负责打包、压缩、兼容旧浏览器。学完下一章的收尾实战，你就具备了看懂一个 Vite 项目 src\u002F 目录的全部前置知识。",{"id":13603,"title":12347,"titles":13604,"content":13605,"level":55},"\u002Ftutorials\u002Fjs#二十一综合实战从能跑到像样",[5],"毕业考试不用另出题：把 21 章的技能串起来做一个待办清单就够了。需求列清楚——添加（含校验）、勾选完成、删除、按状态筛选、显示剩余数量、刷新后数据还在。先看一版「能跑但毛病一堆」的实现，再逐条改造成能进产品的水准。",{"id":13607,"title":12358,"titles":13608,"content":13609,"level":95},"\u002Ftutorials\u002Fjs#第一版能跑就行",[5,12347],"下面的版本功能真的都能用：回车添加、勾选、删除。但代码里有六个毛病，每一个都是前面某章点名讲过的——先玩一玩，看你能认出几个（提示：输入框里输入 \u003Cb>我有格式\u003C\u002Fb> 再回车，看看会发生什么）。 对照下面的清单找一找，看你说中了几个。 「能跑版」的毛病 改造版怎么治 出处 全局变量满天飞 用闭包把状态藏起来，对外只暴露操作函数 七、十 innerHTML 拼字符串 createElement + textContent，用户输入永远当文本 十四 onclick 属性、函数挂 window addEventListener + 事件委托，一个监听管全部条目 十五 不 trim、不校验 trim + 空值\u002F长度拦截 + 错误提示 八、十六 刷新即失 JSON + localStorage，先探测再兜底 十七 数据与界面搅在一起 分出数据层 \u002F 渲染层：一切改动 = 改数据 + 重新 render 十四、二十 改造版还顺手加了一处毛病清单之外的东西：保存状态用 async \u002F await 模拟异步落盘（真实项目里这里就是一次 fetch POST），右上角能看到「保存中… → 已保存」的流转。",{"id":13611,"title":12467,"titles":13612,"content":41,"level":95},"\u002Ftutorials\u002Fjs#第二版改造版",[5,12347],{"id":13614,"title":12475,"titles":13615,"content":13616,"level":95},"\u002Ftutorials\u002Fjs#改造版的骨架其实就三件事",[5,12347],"数据层用闭包藏住状态，对外只有一组操作函数——你会注意到所有修改都是「不可变更新」，而数组的 map \u002F filter \u002F reduce 全用上了： function createStore(initial) {\n  let todos = initial\n  let seq = todos.reduce((max, t) => Math.max(max, t.id), 0)\n  return {\n    add(text) { todos = [...todos, { id: ++seq, text: text, done: false }] },\n    toggle(id) { todos = todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) },\n    remove(id) { todos = todos.filter((t) => t.id !== id) },\n    get() { return todos },\n  }\n} 渲染层一切从数据出发，事件用一个委托全部接管——数据变了就重新 render，界面永远和数据对得上，不存在「哪里忘了同步」： function render() {\n  list.replaceChildren(...visible().map(renderItem))\n}\n\nlist.addEventListener('click', (e) => {\n  const item = e.target.closest('.item')\n  if (!item) return\n  const id = Number(item.dataset.id)\n  if (e.target.matches('.del')) { store.remove(id); commit('删除') }\n  else if (e.target.matches('input[type=\"checkbox\"]')) { store.toggle(id); commit('更新') }\n}) 剩下的都是普通代码。把示例在「在新标签页打开」后对着源码再读一遍——每一行都能对应回某一章。",{"id":13618,"title":12876,"titles":13619,"content":13620,"level":95},"\u002Ftutorials\u002Fjs#你已经走完了什么",[5,12347],"从第一章「script 放在哪」到能独立写出上面这个应用：变量、类型、函数、闭包、对象、类、错误、DOM、事件、表单、BOM、异步、fetch、模块——21 章没有一章是多余的，它们都在你刚才玩的这个页面里出现了。 接下来去哪：想直接上框架就学 Vue 或 React（你在读的这个站点就是 Vue 写的——回头看你写的 render()，是不是已经有点「响应式」的形状了）；想补类型上 TypeScript；想吃透 npm 生态就认真学 Vite；想碰后端就顺着 Node.js 往下走。工具会换，但变量、函数、异步、模块这些地基不会。",{"id":13622,"title":12889,"titles":13623,"content":13624,"level":95},"\u002Ftutorials\u002Fjs#最后一份检查清单",[5,12347],"下面这些如果都能不看教程写出来、并且讲给别人听明白，这门课就毕业了： 说清 var \u002F let \u002F const 的区别，以及「const 锁的是绑定不是值」 分清原始值和对象传参的差别，说得出什么叫浅拷贝 用解构、展开、可选链把日常代码写短而不晦涩 写好箭头函数，解释闭包为什么能「记住」外层变量 熟练用 map \u002F filter \u002F reduce \u002F find 加工数组 用 class 加私有字段写一个小型数据结构 用 throw \u002F try \u002F catch \u002F finally 搭起错误处理 用 querySelector \u002F createElement \u002F textContent 安全地操作 DOM 用事件委托让动态列表「一器多用」，说清 preventDefault 和 stopPropagation 的分工 用 FormData 与 submit 事件读写、校验一张表单 用 setTimeout \u002F setInterval 排列时间，用 Promise 与 async \u002F await 消费异步 写 fetch 时记得查 res.ok，POST 时记得 JSON.stringify 用 JSON 往返在 localStorage 里存对象，并做可用性探测与兜底 把代码拆成 utils \u002F store \u002F main，理顺 import \u002F export 清单里每一条，正文里都配了示例。以后哪条卡住了，回来翻对应那一章就好。",{"id":13626,"title":12946,"titles":13627,"content":13628,"level":95},"\u002Ftutorials\u002Fjs#写在最后",[5,12347],"教程到这儿就结束了，但「会 JS」这件事只能靠写出来。去找一个你自己每天真用得上的小需求——记账、单词卡、值班表，什么都行——用这 21 章的全部本事把它做出来。过程中卡住的每一处，都比再读一遍教程管用。 html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}",1793263560708]