
js--14是我给自己整理的第十四份JavaScript阶段笔记。跟前面的编号一样这不是某个具体的开源项目而是把我最近半年在前端项目、面试准备、日常脚本里反复踩到和用到的JS知识点做了一次大扫除。内容跨度确实有点大从htmlcssjs基础语法一路到WPS里的js宏、音源脚本、Vite构建报错、Promise并发控制、原型链、Vue响应式、TypeScript对比基本把我手头接触到的JS相关场景都装进去了。如果你自学前端有一段时间想系统查漏补缺或者正在准备面试想快速过一遍高频考点这份笔记应该能帮你省不少时间。我按七个大块来写每个部分独立成章感兴趣可以直接跳到对应位置。1. 基础语法与高频API先把手上的活干利索很多人一上来就追框架、追工程化反而把最基础的原生JS语法用得磕磕绊绊。我在面试候选人和帮同事review代码时发现大量问题都出在字符串、数字、数组这几个最常用的API上。这一节不聊虚的全是日常开发里真正高频的写法。1.1 字符串包含判断includes与indexOf的取舍判断一个字符串里有没有子串最常见的就是includes和indexOf。两者核心区别只有一个includes返回布尔值indexOf返回找到的下标找不到就返回-1。const text Hello, JavaScript!; console.log(text.includes(JavaScript)); // true console.log(text.indexOf(JavaScript)); // 7 console.log(text.includes(Vue)); // false console.log(text.indexOf(Vue)); // -1那到底用哪个我个人的习惯是只需要知道“有没有”的时候用includes代码意图更清晰需要知道“在哪个位置”或者要根据位置做截取、替换的时候用indexOf。还有个容易忽略的细节includes是ES2016引入的理论上比indexOf在非常老的浏览器里兼容性差一点但现在还在维护的项目基本不用担心这个。另外这两个方法都是区分大小写的要做不区分大小写的匹配一般是先toLowerCase()再判断。如果你要匹配的是复杂模式比如邮箱格式、手机号格式那就别用字符串方法了直接上正则。text.includes()只能判断有没有符号但/^[^\s][^\s]\.[^\s]$/才能判断是不是一个像样的邮箱地址。记住一个原则精确匹配用字符串方法模式匹配用正则别混着用。1.2 数字处理保留两位小数、判断整数细节比想象中多先说保留两位小数。大多数人的第一反应是num.toFixed(2)这个方法确实是常用方案但它有个坑四舍五入的精度在某些边界值上不符合直觉。console.log((1.005).toFixed(2)); // 1.00 而不是 1.01 console.log((2.55).toFixed(1)); // 2.5 而不是 2.6原因很简单浮点数在二进制里不是精确表示的1.005实际存储的值比1.005略小一点所以toFixed四舍五入时掉下去了。如果对精度要求高比如金额计算建议先转成整数再处理或者用Math.round(num * 100) / 100这种偏移方式再配合toFixed(2)补位function formatMoney(num) { return (Math.round((num Number.EPSILON) * 100) / 100).toFixed(2); }Number.EPSILON是ES6提供的“最小精度差”补偿浮点误差很实用。再说判断整数。Number.isInteger(3)是trueNumber.isInteger(3.0)也是true但Number.isInteger(3)是falseNumber.isInteger(3.1)是false。这里要注意它和parseInt的区别parseInt(3.9)返回3但它不是“判断整数”而是“截断取整”。如果你要判断一个变量是不是整数优先用Number.isInteger。如果输入可能来自表单字符串那要先转成number再判断const input 42; console.log(Number.isInteger(input)); // false console.log(Number.isInteger(Number(input))); // true1.3 数组三件套map、filter、forEach的思维差异数组方法里最容易混的就是map、filter、forEach我在代码review时见过不少人把三者写混。记住它们的本质区别map把数组每个元素映射成新值返回一个等长度的新数组。filter把不满足条件的元素过滤掉返回一个长度小于等于原数组的新数组。forEach只遍历不关心返回值没有新数组。const nums [1, 2, 3, 4, 5]; const doubled nums.map(n n * 2); // [2, 4, 6, 8, 10] const evens nums.filter(n n % 2 0); // [2, 4] nums.forEach(n console.log(n)); // 遍历打印没有返回值常见错误是在forEach里收集结果时忘了push或者在map里只做赋值不return结果得到一堆undefined。另一个常见误区是想“边遍历边过滤”用forEach加上if判断再push到新数组虽然能实现但完全可以用filter一步到位代码更简洁。React里渲染列表时data.filter(...).map(...)这种链式写法非常实用先过滤再映射一气呵成。forEach还有一个特点不能用break或return跳出整个循环return只是结束当前这次回调。想提前终止循环要么用for...of要么用some/every配合短路逻辑。1.4 forEach后怎么知道“循环完了”从回调到Promise的改造搜索“js foearch怎么判断循环完了”的人大概率是把forEach拼错了真正的名字是forEach。但这个问题本身很经典forEach遍历完同步代码后函数就结束了它没有返回值也没有类似“遍历完成”的回调。你可能会试这样写let done false; arr.forEach(item { // 处理item }); done true;如果里面的处理都是同步的那没问题forEach执行完下一行done true立刻执行。但如果在forEach里做了异步操作比如发请求、读文件这个done就会在请求还没返回时被置为true因为forEach不会等异步回调完成。要解决这个问题最简单的办法是改用for...of循环配合await或者把每个异步任务收集成Promise数组再用Promise.all统一等待const tasks arr.map(async item { await doSomething(item); }); await Promise.all(tasks); console.log(全部完成);这里其实引出了一个更本质的问题JavaScript的异步流程控制。下面专门说。2. 异步编程Promise、async/await与那条隐藏的“传染链”异步是JavaScript区别于其他语言最明显的一块。新手写回调老手写Promise高手在理解事件循环的前提下合理选择。这一节讲的都是实际项目里高频出现的点和那些容易绕进去的弯。2.1 Promise基础与Promise.all的真实应用场景Promise三个状态pending进行中、fulfilled已成功、rejected已失败。状态一旦从pending变成fulfilled或rejected就不能再变了。这是Promise设计的核心也是它比回调可靠的原因——回调可以被调用多次Promise只能落定一次。日常最常见的是fetch发请求fetch(/api/user) .then(res res.json()) .then(data { console.log(data); }) .catch(err { console.error(err); });真正体现Promise威力的是Promise.all。它的场景是这样的一个页面需要同时拿到用户信息、订单列表、商品列表三个接口的数据三个接口互不依赖就可以并行发起全部返回后再一起处理。const [user, orders, products] await Promise.all([ fetch(/api/user).then(r r.json()), fetch(/api/orders).then(r r.json()), fetch(/api/products).then(r r.json()), ]);Promise.all是“全成功才成功一个失败就失败”。如果你需要等所有请求结束但不想被某一个失败拖垮那就用Promise.allSettled它会等所有Promise都落定然后返回每个结果的完整状态。在批量上传文件、批量导入数据这种场景里allSettled比all更实用因为你能知道哪些成功、哪些失败而不是一个失败就全军覆没。2.2 async“传染性”到底是什么“async传染性”这个说法在社区里越来越常见。意思是一个函数只要被标成async它的返回值就变成一个Promise调用方就得用await或.then来接一旦调用方用了await调用函数本身又可能变成async……就像病毒一样一层层往上传染。async function getData() { return data; } function outer() { // 这里拿不到 data只能拿到 Promise const result getData(); // Promise { data } }为什么会有传染性因为async函数内部如果有await代码执行到那里会暂停外部根本无法同步拿到最终值。JavaScript是单线程的异步结果只能通过回调、Promise这些“将来”的通道传递。所以“传染”不是设计缺陷而是单线程模型下必然的选择。实际开发里怎么应对在项目入口把异步边界限制住不要让async无限向上传染。比如一个React组件里事件处理函数可以写成async但渲染函数不能初始化数据的调用放在useEffect里而不是组件体顶层同步执行。理解了传染性的来源就知道该在哪里主动切回同步边界而不是到处await一团乱。2.3 手写sleep函数从setTimeout到同步等待思维转变js sleep搜索频率一直很高因为很多人想用Java里Thread.sleep()的思路在JS里“睡一会儿”。但浏览器和Node都是单线程的不可能真正同步阻塞主线程不然整个页面就卡死了。我们说的sleep实际是“延迟继续执行”用Promise包装setTimeout最直接function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); } // 使用 (async () { console.log(开始); await sleep(2000); console.log(2秒后); })();如果你想要一个带“超时中断”的sleep可以结合Promise.racefunction sleepWithTimeout(ms, timeout) { return Promise.race([ sleep(ms).then(() done), sleep(timeout).then(() timeout), ]); }在轮询场景里sleep非常常用比如每5秒检查一次某个任务状态直到完成或超时。记住sleep返回的是Promise只能在async函数里用await等不能“同步睡”。想明白这一层就不容易被它的“传染性”带偏。2.4 异步任务并发控制从一个请求排队的小需求说起有一回我做个批量上传工具一次要传几十张图片如果直接Promise.all把所有图片塞给后端后端扛不住浏览器也会同时开太多连接。这种时候就需要并发控制同时只跑N个任务跑完一个从队列里补一个。一个简单的并发池实现思路是维护一个结果数组和一个当前活跃计数每次启动任务时判断活跃数是否达到上限没达到就继续启动达到就等某个任务结束再启动下一个。async function runWithConcurrency(tasks, limit) { const results []; const executing new Set(); for (const task of tasks) { const promise Promise.resolve().then(task); results.push(promise); executing.add(promise); const clean () executing.delete(promise); promise.then(clean, clean); if (executing.size limit) { await Promise.race(executing); } } return Promise.all(results); }这个代码里最关键的是Promise.race(executing)它会在任意一个任务完成时立刻返回让循环有机会继续往里塞新任务。实际项目中也可以直接用现成的库但我建议自己写一遍这个逻辑因为并发控制的思路在好多场景都能复用比如爬虫限速、数据分片上传、队列消费。3. 核心机制与横向对比原型链、Vue响应式、TypeScript基础API是“术”这一节是“道”。搞不清楚原型链很多JS行为在你眼里就是玄学不理解响应式Vue的更新机制总是隔层纱不弄明白TypeScript和JavaScript的分工很容易在项目里把类型系统用成一堆any。3.1 原型链构造函数、原型对象与实例之间的关系原型链是JS面试的常客也是很多人最懵的一块。其实拆开看就三样东西构造函数、原型对象、实例。function Person(name) { this.name name; } Person.prototype.sayHi function () { console.log(Hi, I am ${this.name}); }; const p new Person(张三); p.sayHi(); // Hi, I am 张三这里的关系是Person是构造函数它有一个prototype属性指向原型对象Person.prototypep是实例它有一个__proto__属性也指向Person.prototype原型对象Person.prototype上有一个constructor属性指回构造函数Person。所以p.sayHi找不到时JS引擎会沿着p.__proto__也就是Person.prototype去找找到了就调用。数组方法同理。为什么[1,2,3].map(...)能用因为Array.prototype上定义了map实例数组通过__proto__链找到了它。这就是“继承”的本质——不是复制而是顺着原型链查找。理解这一点后那些“为什么这个对象上有这个方法”的问题基本都能自己推出来了。instanceof运算符的原理也跟原型链有关它检查右边构造函数的prototype是否出现在左边的原型链上。3.2 用“依赖收集”理解Vue响应式js深入浅出vue这类搜索表明很多人想啃Vue原理。Vue的响应式核心其实就四个字依赖收集。原理是当读取一个响应式数据时把当前正在执行的“副作用函数”比如组件的渲染函数收集到这个数据的依赖列表里当数据改变时再把这个列表里的副作用函数全部重新执行一遍。Vue2用Object.defineProperty逐属性拦截get和setVue3用Proxy代理整个对象。两者目标一样但Proxy能拦截新增属性、删除属性、数组索引变更这些Vue2处理起来很别扭的操作。const raw { count: 0 }; const deps new Set(); const proxy new Proxy(raw, { get(target, key) { if (currentEffect) deps.add(currentEffect); return target[key]; }, set(target, key, value) { target[key] value; deps.forEach(effect effect()); return true; } });上面这个迷你版currentEffect就是当前注册的副作用函数。读取proxy.count时它被收集进deps修改proxy.count时deps里的函数全部重新执行。Vue的computed、watch、组件更新底层都是这套逻辑的延伸。理解了依赖收集Vue的性能优化手段比如合理划分组件、用computed缓存中间结果也就能讲出道理了。3.3 TypeScript到底比JS多了什么经常有人问typescript和js的区别。一句话TypeScript是JavaScript的超集它在JS的基础上加了类型系统写完之后编译成纯JS运行。区别不在运行时而在编译期——TS能帮你在代码跑起来之前发现很多低级错误。interface User { id: number; name: string; age?: number; } function greet(user: User): string { return Hello, ${user.name}; }JS里的user可能来自接口、从对象解构、被同事改来改去没有类型约束时稍不注意就把user.name拼成了user.nmae运行时才报错。TS让这种错误在编辑器和编译阶段就暴露出来。但这不意味着TS比JS“高级”——运行时执行的都是JSTS只是给你加了一道安全网。什么时候值得上TS我的经验是项目超过五千行、多人协作、接口数据结构复杂就非常值得。如果只是写个几十行的脚本、写个临时demo用TS反而增加成本。选择依据是“要不要长期维护”和“几个人一起写”而不是“别人都在用”。4. 浏览器端实战打印、剪贴板、图片压缩与页面生命周期前几节偏理论这里全部是能直接抄走的页面功能代码。每一项都是我实际在业务系统里做过的包括打印、复制文字、图片压缩、防止页面误关、iframe父子页面通信。4.1 实现局部打印window.print与iframe方案浏览器打印最常见的需求是用户点一个按钮只打印某个区域的单据/表格而不是整个页面。window.print()会打印当前整个页面配合CSS可以控制显示范围media print { body * { visibility: hidden; } .print-area, .print-area * { visibility: visible; } .print-area { position: absolute; left: 0; top: 0; } }这个方案的好处是不用改DOM结构坏处是在某些浏览器里布局容易错位。我更喜欢另一种方案动态创建隐藏iframe把要打印的内容写进去再调用iframe里的print()。function printHTML(html) { const iframe document.createElement(iframe); iframe.style.position fixed; iframe.style.right 0; iframe.style.bottom 0; iframe.style.width 0; iframe.style.height 0; iframe.style.border 0; document.body.appendChild(iframe); const doc iframe.contentWindow.document; doc.open(); doc.write(html); doc.close(); iframe.onload () { iframe.contentWindow.focus(); iframe.contentWindow.print(); setTimeout(() document.body.removeChild(iframe), 1000); }; }注意向iframe写内容时要带上完整的html和head包括样式和字符集否则中文容易乱码、样式全丢。打印完成后再移除iframe免得页面残留隐藏节点。4.2 保存网页文字到剪贴板Clipboard API与兼容方案“手机 保存网页文字 js代码”这类搜索本质是想实现一个“点击复制”按钮。现代浏览器推荐用Clipboard APIasync function copyText(text) { try { await navigator.clipboard.writeText(text); console.log(复制成功); } catch (err) { console.error(复制失败, err); } }但navigator.clipboard只在https或localhost环境下可用而且需要用户授权。老项目或者非https环境就得用兼容方案创建一个隐藏的textarea选中它再执行document.execCommand(copy)。function copyTextFallback(text) { const textarea document.createElement(textarea); textarea.value text; textarea.style.position fixed; textarea.style.opacity 0; document.body.appendChild(textarea); textarea.select(); document.execCommand(copy); document.body.removeChild(textarea); }execCommand虽然被标记废弃但兼容性极好很多扫码、H5页面还在用。我通常是先尝试navigator.clipboard失败再走fallback两条路都包一层错误处理这样用户在iOS微信内置浏览器里也不会点一下没反应。4.3 canvas压缩图片转WebP参数与坑前端压缩图片是上传场景的刚需比如用户拿手机拍了一张5MB的照片直接上传体验和带宽都扛不住。常见做法是用FileReader读文件创建Image对象加载再画到canvas上最后用canvas.toDataURL(image/webp, quality)导出压缩数据。function compressImage(file, quality 0.7) { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload e { const img new Image(); img.onload () { const canvas document.createElement(canvas); canvas.width img.width; canvas.height img.height; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0); canvas.toBlob(blob { if (blob) resolve(blob); else reject(new Error(压缩失败)); }, image/webp, quality); }; img.onerror reject; img.src e.target.result; }; reader.onerror reject; reader.readAsDataURL(file); }); }这里有三个坑我反复踩过。第一如果图片来自其他域名且没有CORS头canvas会被污染调用toBlob或toDataURL会直接报错这种场景要先确保服务器返回Access-Control-Allow-Origin图片也要加crossOriginanonymous。第二超大图片在部分安卓机上会超过canvas尺寸上限导致压缩出来是黑图稳妥做法是先按比例把长边限制到比如2560以内。第三WebP格式不是所有浏览器都支持导出前最好判断一下canvas.toDataURL(image/webp).startsWith(data:image/webp)不支持就回退到jpeg。4.4 防止页面自动关闭beforeunload与在线状态统计“js如何防止页面自动关闭”这个问题有两种理解一种是用户不小心点了关闭希望弹窗确认另一种是页面在后台被浏览器自动回收。前者用beforeunload事件window.addEventListener(beforeunload, e { e.preventDefault(); e.returnValue ; });注意现代浏览器出于用户体验考虑已经不允许自定义弹窗文案只会显示默认的“可能离开此网站吗”。这个方案适合有未保存表单的场景比如在线编辑页面。不要滥用不然用户会觉得页面很烦。至于页面因为长时间处于后台被浏览器“冻结”这其实是浏览器的省电策略JS在后台被挂起是正常行为。可以用visibilitychange监听页面是不是重新可见了然后做数据刷新document.addEventListener(visibilitychange, () { if (document.visibilityState visible) { // 页面重新可见刷新数据或重新登录校验 } });顺带说一个高频搜索“js打开手机根目录怎么打开”。浏览器出于安全沙箱限制JS不能直接访问手机文件系统根目录。你能做的是通过input[typefile]让用户主动选择文件或者用File System Access API在用户授权后访问特定目录。如果是你们自己的App里的WebView可以通过原生桥接暴露一个获取真实路径的接口那不在纯web技术范围内。4.5 iframe jQuery父子页面通信的旧项目维护实录现在新项目很少用jQuery和iframe了但存量系统的维护里这两个组合还是经常出现。我前段时间就接手了一个老后台父页面有一个列表子页面在iframe里子页面操作完要关掉自身并刷新父页面的列表。子页面里这样调父页面方法window.parent.refreshList(); // 或者带参数 window.parent.handleAfterEdit(data);父页面里要定义好这些全局函数并且注意调用时机。如果两个页面域名不同那就涉及跨域必须走postMessage// 父页面监听 window.addEventListener(message, e { if (e.data.type refresh) { loadList(); } }); // 子页面发送 window.parent.postMessage({ type: refresh }, https://parent-domain.com);老项目另一个常见问题是分不清jquery的js下载——准确的表述应该是“jQuery库文件的本地引入”因为jQuery就是一个js文件以前很多系统习惯下载到本地静态目录然后script srcjquery.min.js/script引入。维护老代码时还要注意IE兼容js 打开 ie这种需求本质上是因为部分老旧网站还依赖IE的ActiveX控件但IE早已停止支持现在主流的做法是让用户使用Edge浏览器的“IE模式”或者逐步改造系统别再基于IE做新功能了。5. 框架、构建与常见报错Vite、Element、CSV导出、URL校验工程化工具的报错是最让人头疼的因为信息往往不直观。这一节记录了最近遇到的构建报错、组件库用法、数据导出和校验的实战处理都是可以直接拿来对照的。5.1 Vite构建报错[vite:esbuild-transpile]排查实录搜到[vite:esbuild-transpile] transform failed with 2 errors: static/js/general-9这种报错说明Vite在构建时用esbuild对某个js文件做语法转译失败了。我遇到的那次具体表现是本地开发正常一执行npm run build就挂报错文件指向static/js/general-9。排查步骤我建议按这个顺序来先看报错信息里是否带有行号和具体语法片段如果有顺着文件打开那一行检查语法。esbuild对语法错误比较敏感常见的原因包括多括号少括号、模板字符串里嵌套了没转义的字符、某个依赖包里残留了TS语法却没被插件处理。如果报错文件是压缩后的静态文件先找到生成它的源码。static/js/general-9这种命名一般是某个应用自身打包产物可能是旧版本缓存。清空dist目录、node_modules里的.vite缓存再重新安装依赖试试。锁定esbuild版本。esbuild是Vite的重要依赖版本升级偶尔会带来语法兼容变化如果项目里直接依赖了esbuild把它固定到和Vite匹配的版本。用npx vite build --debug跑一次拿到更详细的日志。那次我最终定位到是node_modules里某个库的新版本用了一个较新的JS语法特性而项目配置的构建目标太低esbuild拒绝转译。解决方案是在vite.config.js里把build.target调到es2020同时把那个库升级到兼容版本。遇到这类问题别直接删node_modules重装先确认报错文件是源码还是第三方库方向对了很快就能解开。5.2 Vue Element日历组件禁用未来月份的两种实现看到了js vue element 日历判断月份大于当前月的月份不能选择这个需求在业务系统里很常见比如报销日期不能填未来、预约只能选当天之前。Element UI的DatePicker用picker-options里的disabledDate就能实现disabledDate(time) { const today new Date(); today.setHours(0, 0, 0, 0); return time.getTime() today.getTime(); }disabledDate接收一个time参数即当前正在遍历的日期返回true表示禁用。上面的写法会把今天之后的所有日期禁用但今天本身可选。如果你还想禁用今天只允许选昨天及以前把判断改成time.getTime() today.getTime()就行。如果是Element Plus属性名变成了disabled-date写法类似。注意比较时要先把时间归零否则时分秒会影响结果。还有一个坑disabledDate接收的Date对象是当地时区的不要在比较时随意调用toISOString()会出现时区偏移导致日期判断差一天。把new Date()和time都用setHours(0,0,0,0)归一化是最稳妥的。5.3 前端导出CSV三种方案对比与中文乱码处理“js中导出csv的方法”在后台管理系统里特别常见查出来一条条渲染成表格太麻烦直接导成CSV用Excel打开最省事。纯前端导出CSV最直接的方法是拼字符串然后用Blob下载function exportCSV(rows) { const header Object.keys(rows[0]).join(,); const body rows.map(row Object.values(row).map(v ${String(v).replace(//g, )}).join(,) ).join(\n); const csv ${header}\n${body}; const blob new Blob([\uFEFF${csv}], { type: text/csv;charsetutf-8; }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download export.csv; a.click(); URL.revokeObjectURL(url); }这里最容易被坑的是乱码。CSV本质是纯文本Excel默认用ANSI解析你用UTF-8生成的CSV直接双击打开经常是乱码。解决办法就是在文本前面加一个UTF-8的BOM\uFEFF上面代码里的\uFEFF${csv}就是干这个的。三个方案对比一下方案一是纯前端拼字符串适合小数据量零依赖方案二是后端生成文件返回下载链接适合大数据量不卡浏览器方案三是用SheetJS这类库直接生成xlsx功能更强但体积较大。我的建议是数据量千行以内用纯前端CSV因为轻、简单、可控数据量大或者要带样式、多sheet还是让后端出文件更靠谱。5.4 URL有效性校验与省市区三级联动“js验证url有效性”也是一个常被搜索的基础需求。最现代的正规方式是直接用new URL()构造函数function isValidUrl(url) { try { new URL(url); return true; } catch (err) { return false; } }new URL()能解析比较完整的URL格式但注意它比较宽容比如new URL(baidu.com)在浏览器里会报错因为缺少协议。如果你希望用户必须填完整的https://开头再补一个协议校验function isValidHttpUrl(url) { try { const u new URL(url); return u.protocol http: || u.protocol https:; } catch (err) { return false; } }还有一类的“验证URL有效性”是指能不能访问通那就得用fetch去请求但会受跨域限制通常需要后端代劳。正则校验虽然也能写但维护成本高而且容易漏掉边界情况能用URL构造函数就少写正则。省市区三级联动则是一个经典的前端交互题。核心思路是准备好省市区数据用map和filter做级联过滤const cityList provinces.find(p p.code selectedProvince)?.cities || []; const areaList cityList.find(c c.code selectedCity)?.areas || [];联动组件要注意数据量全国省市区数据一般几百KB做成一次性加载没问题但搜索、切换要有缓存别每次选省都去请求一遍。用computedVue或useMemoReact缓存中间结果交互会顺滑很多。5.5 ArcGIS API for JS二三维切换的踩坑记录arcgis js api for javascript 4.x 二三维切换显示这类需求通常是在一张地图上让用户自由切换2D和3D视角。ArcGIS API for JS 4.x本身就支持二三维一体化关键是用同一个Map对象但view不同2D用MapView3D用SceneView。const map new Map({ basemap: topo-vector }); const view2D new MapView({ container: viewDiv, map: map, center: [116.3, 39.9], zoom: 10 }); const view3D new SceneView({ container: viewDiv3D, map: map, center: [116.3, 39.9], zoom: 10 });切换的时候可以在同一个container上交替设置view.container和view.viewpoint来同步相机位置。我踩过的主要坑有三个一是切换后图层渲染异常因为3D场景下部分图层比如某些栅格服务默认不在SceneView显示需要手动把图层加到map二是相机同步从2D切到3D后视角抖动需要把原来的中心点、缩放级别同步过去必要时用viewpoint对象整体传递三是性能3D场景对显卡要求高不要在移动端同时加载太多图层。这类GIS调试没法纯靠浏览器控制台建议多打印view.viewpoint来定位同步问题。6. 特殊场景与扩展能力WPS JS宏、脚本注入、前端安全JavaScript的生态早就超出浏览器范畴了。WPS支持用JS写宏不少播放器和工具类应用把JS当作规则引擎浏览器插件本身也离不开JS。这一节聊几个渗透在生活里的JS场景顺便说说前端安全那些事。6.1 WPS JS宏用JS写办公自动化顺带去重“js宏”和“wps js宏”搜的人越来越多因为WPS默认支持两种宏语言VBA和JS。对于不熟悉VBA但有前端基础的办公人员用JS写宏确实更友好。在WPS表格里按AltF11打开宏编辑器选择JS环境就能用类似ActiveSheet的对象模型操作工作表。举个例子WPS表格里有一列数据要把重复记录去掉。用JS宏可以这样写function removeDuplicates() { const sheet ActiveSheet; const range sheet.Range(A1:A100); const values range.Value2; const seen new Set(); const unique []; for (let i 0; i values.length; i) { const val values[i][0]; if (!seen.has(val)) { seen.add(val); unique.push([val]); } } range.ClearContents(); const target sheet.Range(A1:A unique.length); target.Value2 unique; }这里就是先把整列值读出来用Set去重再一次性写回。操作Excel/WPS对象时尽量用Range级别的批量读写不要一个单元格一个单元格地set否则数据量大时会非常慢。JS宏和VBA宏的对象模型基本一致只是语法不同会JS的人学这个几乎零成本。顺带说一句宏功能虽然强大但运行未知来源的宏有安全风险只运行自己写的或可信来源的代码。6.2 音源脚本与数据源适配JS不止跑在网页里很多人搜到“lxmusic音源js在线”“野草音源js文件”其实是指某些音乐播放器通过外置JavaScript脚本把不同平台的音乐接口适配成统一的数据源格式。这类脚本本质是一个“数据源适配层”播放器定义好了一个统一的接口约定比如搜索返回什么字段、获取播放链接返回什么字段脚本只需要把这个约定实现出来就能把任意数据源接进来。这个思路在很多领域都能看到视频网站播放器解析脚本、小说阅读器书源脚本、插件系统的生命周期脚本。核心能力是理解宿主环境提供的API按照约定的数据结构输出结果。这类脚本通常运行在受限的JS运行时里没有DOM、没有window但提供了http、crypto这类扩展函数。写这类脚本最需要关注的是接口字段的映射和请求参数的处理比如搜索关键词要URL编码、响应数据要做容错处理。需要提醒的是音源脚本涉及音乐版权问题作为技术爱好者了解一下适配思路没问题但如果用来抓取和传播无授权内容就有合规风险。我这里聊的是“数据源适配”这个通用技术模型不是鼓励去搞盗版源。6.3 Alook等浏览器的JS插件与轻量自动化“alook浏览器js插件大全”这类搜索背后是很多人想给浏览器加装一些轻量脚本实现页面去广告、自动展开全文、保存网页文字这类操作。Alook这类支持脚本注入的浏览器本质上提供了一个迷你扩展环境每种脚本用**和CSS选择器匹配页面元素再执行自定义JS。比如屏蔽页面广告弹窗可以写一个简单脚本定时查找带有ad、banner这类class的节点并隐藏function removeAds() { const selectors [.ad, .banner, .popup, [id*ad-]]; selectors.forEach(sel { document.querySelectorAll(sel).forEach(el { el.style.display none; }); }); } setInterval(removeAds, 2000);这类脚本写起来不难难点在于不同站点的广告节点命名五花八门维护选择器列表比写脚本本身更费时间。另外一个更通用的做法是学习一下MutationObserver它能在DOM变动时触发回调比固定间隔轮询更高效、更优雅const observer new MutationObserver(() { removeAds(); }); observer.observe(document.body, { childList: true, subtree: true });用浏览器脚本做自动化时注意别破坏页面的正常功能比如误隐藏掉表单、按钮。还有一种必坑场景是页面是单页应用路由切换后DOM重新渲染脚本可能失效所以要么保持轮询要么用MutationObserver持续监听。6.4 前端代码保护与数据校验关于JS逆向与反爬的认知这两年“js逆向”“js反爬实战”特别火我也经常被问到。这是个需要摆正认知的话题前端代码是公开下发给浏览器的任何人按F12都能看到源码所以前端永远做不到绝对保密。所谓“逆向”和“反爬”本质上是加固与绕过的猫鼠游戏。我自己的态度是了解逆向技术有助于排查线上问题、分析第三方SDK行为、保护自有代码但我反对把精力花在非法抓取别人数据上。从实操上看前端工程师真正需要掌握的安全加固手段有这么几类代码压缩混淆让阅读难度提高常用工具如JavaScript obfuscator。接口参数签名前端用国密算法比如SM2、SM3、SM4对关键参数做加密或加签服务端验签。国密算法在js侧可以直接用sm-crypto之类的库实现Java后端也有对应用法两端配合才能保证参数不被篡改。请求频率限制、验证码、设备指纹等风控手段放在服务端而非前端。这里要明确一点前端利用国密算法对数据做加密能防止明文传输、防止参数被随意篡改但密钥如果暴露在前端代码里仍然可以被提取。所以前端加密的正确目标是“增加攻击成本”而不是“绝对阻止攻击”。真正的安全边界必须在服务端。做这行别迷信“前端加密就安全了”也别钻牛角尖非要把自己代码搞得谁都看不懂——那样自己维护也是灾难。7. 面试与算法从吉利JS面试题说起搜“吉利js面试算法题”的人应该是在准备车企前端岗位的面试。JS方向的面试题虽然花样多但翻来覆去其实就几类手写实现、输出题、异步题、算法题。我结合亲身经历和网上公开信息把高频的考点拉了一遍顺便给一份自查清单。7.1 手写实现、输出题与异步题JS面试三类常客手写实现类主要考察基本功。比如手写数组去重至少能写出三种方法Set、filter indexOf、Map。Set最简单const unique arr [...new Set(arr)];再比如手写防抖和节流这两个概念经常被混淆。防抖是“事件停止触发后一段时间才执行”节流是“固定时间间隔内最多执行一次”。区分方法很简单防抖适合输入框搜索你连续打字的时候不会一直发请求停下来了才搜节流适合滚动加载、按钮连点保证固定频率执行。function debounce(fn, delay 300) { let timer null; return function (...args) { clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; } function throttle(fn, interval 300) { let last 0; return function (...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }输出题考察的是对闭包、作用域、事件循环的理解。经典题目是for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出 3 3 3原因在于var没有块级作用域循环结束后i已经是3三个定时器回调共用同一个i。把var改成let输出就是0 1 2因为let每次循环创建了新绑定。这题能延伸出很多变体比如改成用闭包立即执行函数保存值。理解了原理变体怎么变都不怕。异步题考的是Promise执行顺序。比如console.log(A); setTimeout(() console.log(B), 0); Promise.resolve().then(() console.log(C)); console.log(D); // 输出 A D C B原因是微任务Promise.then在当前宏任务结束后、下一个宏任务之前执行宏任务setTimeout永远排在微任务后面。掌握了事件循环的优先级这类题几乎就是送分题。7.2 自查清单从闭包到函数式组合以及关于“js散度”的冷知识如果你准备JS面试我建议对着这份清单自查一遍缺哪块补哪块变量提升、暂时性死区、闭包的定义与使用场景。深拷贝与浅拷贝的区别手写一个考虑函数、Date、RegExp的深拷贝。call、apply、bind的区别与手写实现。原型链、new关键字底层做了什么。事件循环宏任务、微任务、async/await的执行顺序。Promise常用静态方法all、allSettled、race、any。ES6常用新特性let/const、解构、扩展运算符、可选链、空值合并。手写数组的map、filter、reduce理解它们的行为。函数柯里化与组合函数能说出实际应用场景。这份清单是我自己在带人时常用的。面试不一定要全部答得出但至少不能出现“会用框架但不知道JS基础”的情况。框架更新快语言基础相对稳定把基础夯实了框架换个新的也能快速上手。最后分享一个搜索冷知识搜“js”时还经常出现“js散度”。这里的JS是Jensen-Shannon divergence信息论里用来衡量两个概率分布相似度的一种指标常用于文本相似度计算、模型蒸馏跟JavaScript没有半点关系。同一个缩写在不同领域指代完全不同的东西这也是技术人检索资料时特别容易踩的坑看到不明词汇先确认领域再深挖内容。整理js--14这份笔记的过程中我最大的感受是JavaScript这个语言看起来松松散散真正深入下去每一个知识点都能挖出一串关联内容比如从forEach出发会碰到异步从异步会碰到事件循环从事件循环又会碰到Promise和async。如果只靠背API列表很容易学了后面忘了前面换成“问题驱动”的学习方式遇到问题就顺着链条往下追知识反而记得牢。我个人还有一个习惯就是每隔一段时间把踩过的坑、搜过的高频词、看过的源码心得汇总成编号笔记js--14就是这样诞生的。下次再聊我争取把Vue响应式源码和Node侧的性能调优继续写透。