ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript核心语法实战:从字符串处理到异步编程的必备技巧

JavaScript核心语法实战:从字符串处理到异步编程的必备技巧 这几年的前端面试有个特别有意思的现象候选人简历上写着“精通 JavaScript”可一问reduce怎么用、Promise和微任务到底啥关系、数组去重有哪几种写法就开始支支吾吾。反而是那些踏踏实实把基础语法吃透的人遇到复杂问题能给出更漂亮的方案。我写这篇东西的初衷很简单想把我日常开发中真正高频用到、也经常被问到的 JS 核心语法做一个系统梳理。这里没有“茴香豆的茴有几种写法”那种考据癖只有能直接落地的代码片段、踩坑经验和取舍逻辑。1. 数据操作基本功字符串、数组与对象的高频语法作为前端每天打交道最多的就是数据。接口返回的 JSON、页面表单收集的值、本地缓存里的结构本质上都是字符串、数组和对象这三种类型在来回倒腾。很多看起来复杂的业务拆解到最后就是这三种核心数据结构的增删改查。把这块练熟了写代码的速度和容错率会明显上一个大台阶。1.1 字符串判断包含、忽略大小写与 URL 校验先聊字符串。热搜词里就有“js判断字符串是否包含”这确实是最高频的场景之一。最常见的写法有三种indexOf、includes、search。我个人的习惯是优先用includes因为它的语义最清晰返回值是布尔值不用再跟-1做比较。const str Hello, JavaScript World; // 基本包含判断 console.log(str.includes(JavaScript)); // true // indexOf 的老式写法很多老项目里能看到 console.log(str.indexOf(JavaScript) ! -1); // true // search 支持正则适合更复杂的匹配 console.log(str.search(/javascript/i) ! -1); // true那句“js 忽略大小写”的需求最简单的处理方式是把两边都转成同一种格式再比。我习惯统一toLowerCase()因为它在绝大多数场景下够用而且语义直白。const keyword javascript; console.log(str.toLowerCase().includes(keyword.toLowerCase())); // true注意includes支持从指定位置开始搜索的第二个参数但是不支持正则。如果需要用正则做模糊匹配老老实实切到search或者match。再说一个经常在表单校验里遇到的场景——URL 有效性验证。很多人第一反应是写一个复杂的正则但正则很难覆盖所有边界情况。我实测下来最稳的方案是利用浏览器内置的URL构造函数加上try/catch。function isValidUrl(url) { try { new URL(url); return true; } catch (e) { return false; } } console.log(isValidUrl(https://example.com)); // true console.log(isValidUrl(not-a-url)); // false这个方法的好处是浏览器已经帮你处理了各种协议解析的边角情况不用自己去维护一套正则规则。不过它会把ftp://这类协议也判成合法如果业务上只允许 HTTP/HTTPS那就得再补一道协议白名单校验。1.2 数组的合并、去重与元素操作数组应该是 JS 里最常用的数据结构了。热搜词里“js数组合并去重”出现频率很高这里我把合并和去重分开说因为它们的应用场景其实完全不同。合并数组最简单的是用展开运算符const arr1 [1, 2, 3]; const arr2 [4, 5, 6]; const merged [...arr1, ...arr2]; console.log(merged); // [1, 2, 3, 4, 5, 6]展开运算符做合并代码清晰而且可以很方便地在任意位置插入元素。如果想在合并的同时做去重配合Set是最优雅的写法const arr3 [1, 2, 2, 3]; const arr4 [3, 4, 5, 5]; const uniqueMerged [...new Set([...arr3, ...arr4])]; console.log(uniqueMerged); // [1, 2, 3, 4, 5]Set天然去重配合展开运算符转回数组一行代码搞定。但这里有个隐藏的坑Set的去重基于严格相等所以NaN和NaN会被认为是同一个值而{}和{}会被当成不同对象。如果数组里是对象想按某个字段去重Set就无能为力了得换个思路。对象数组按属性去重我常用的方案是reduce加一个 Mapconst users [ { id: 1, name: 张三 }, { id: 2, name: 李四 }, { id: 1, name: 张三三 }, ]; const uniqueUsers [...new Map(users.map(u [u.id, u])).values()]; console.log(uniqueUsers); // [{ id: 1, name: 张三三 }, { id: 2, name: 李四 }]这段代码的精髓在于用Map的 key 来保证唯一性后面的同名 id 会覆盖前面的。注意这里输出的是后面那个name: 张三三的对象因为Map的 key 相同时后面的值会覆盖前面的。如果你业务上想保留第一个就得换个遍历方向。数组的操作方法实在太多了map、filter、find、some、every这些属于必会的。值得一提的“js三级联动”这种场景本质就是根据选中的上一级去filter下一级的数组理解了数组过滤联动就水到渠成。1.3 对象遍历与字典取值对象在 JS 里地位特殊它既是普通数据结构也能当字典Map用。热搜词里的“js 获取字典的所有值”就很典型。遍历对象的方法有好几个我最常用的是Object.entries()因为它能同时拿到 key 和 value。const dict { apple: 5, banana: 3, orange: 8 }; // 获取所有 key const keys Object.keys(dict); console.log(keys); // [apple, banana, orange] // 获取所有 value const values Object.values(dict); console.log(values); // [5, 3, 8] // 同时遍历 key 和 value for (const [key, value] of Object.entries(dict)) { console.log(${key}: ${value}); }注意到上面代码里for...of结合解构赋值这是遍历对象条目的推荐写法。相比for...inObject.entries()不会遍历到原型链上的可枚举属性更安全可靠。关于对象取值现在的新语法?.可选链和??空值合并非常值得一用能明显减少防御性判断const userInfo { profile: { email: testexample.com } }; // 没有可选链时代码长这样 const email userInfo.profile userInfo.profile.email; // 用可选链 const email2 userInfo.profile?.email; // 配合空值合并给默认值 const nickname userInfo.nickname ?? 匿名用户;??和||的区别在于??只在左侧是null或undefined时取右侧值0、、false这些合法值不会被误判为空。这一点在业务开发里经常造成隐蔽的 bug尤其是处理数字 0 的时候。2. 函数与作用域从声明方式到闭包陷阱函数是 JS 的一等公民几乎所有逻辑都封装在函数里。很多入门者一开始都会被三种函数写法搞晕函数声明、函数表达式、箭头函数。这三者看着差不多实际行为差别很大选错了会在this绑定上吃大亏。2.1 三种函数写法的差异对比先说结论我的项目规范里是这样定的需要this就用普通函数不需要this且代码简短就用箭头函数定义类方法或构造函数用class和普通方法。下面把三种写法的差异铺开讲。// 函数声明有提升 function add(a, b) { return a b; } // 函数表达式无提升 const subtract function(a, b) { return a - b; }; // 箭头函数无提升、无 arguments、无自己的 this const multiply (a, b) a * b;函数声明会被提升到作用域顶部所以你在声明之前调用也不会报错。函数表达式和箭头函数则不具备这个特性必须先定义后使用。这个差异在模块化代码里影响不大但在老式脚本里可能造成让人挠头的 bug。箭头函数最大的特点是“没有自己的 this”它继承的是定义时所在上下文的 this。这个特性在处理事件回调和异步回调时特别好用const counter { count: 0, increment() { // 这里用箭头函数this 指向 counter setInterval(() { this.count; console.log(this.count); }, 1000); } }; counter.increment();如果用普通函数写setInterval的回调this会指向全局对象this.count永远undefined。过去我们得const self this或者用.bind(this)现在箭头函数直接从根上解决了这个问题。不过要注意箭头函数不能当构造函数用也没有arguments对象。遇到需要动态获取参数个数的情况得用剩余参数const sumAll (...nums) nums.reduce((total, n) total n, 0); console.log(sumAll(1, 2, 3, 4)); // 102.2 闭包的作用与常见陷阱闭包是 JS 里绕不开的概念也是面试题的重灾区。简单来说闭包就是函数能记住自己定义时所处作用域的能力。这个能力让函数可以访问外部变量也让外部可以间接操作函数内部的私有状态。我最常用的闭包场景是封装私有变量function createCounter() { let count 0; return { increment() { count; }, decrement() { count--; }, getCount() { return count; } }; } const myCounter createCounter(); myCounter.increment(); myCounter.increment(); console.log(myCounter.getCount()); // 2这里的count变量外部无法直接访问只能通过返回的三个方法操作这就是最朴素的“封装”思想。但闭包也有经典陷阱最典型的就是循环中配合var的问题// 这个循环输出什么 for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); } // 输出3 3 3原因是var声明的i是函数作用域循环结束时i已经变成 3三个定时器回调共享同一个i。解决办法是用let声明i让每次循环拥有独立的块级作用域或者用立即执行函数把值传进去。现在写代码我会直接用let从语法层面规避这类问题。我自己的体会是理解闭包的关键在于理解“变量存在的地方而不是变量被使用的地方”。当你在一个函数里嵌套另一个函数内层函数对外层函数变量的引用就让外层变量无法被垃圾回收。所以在写长生命周期的对象、事件监听时要注意避免无意识产生不必要的闭包防止内存泄漏。2.3 onclick 在 JS 中的用法与事件绑定热搜词里有个“onclick在js中的用法”这个看着基础实际深挖也有门道。纯原生环境下给元素绑定点击事件有三种姿势。第一种是在 HTML 里直接写onclick属性button onclickhandleClick()点我/button这种写法最直观但缺点也很明显逻辑和结构耦合在一起而且handleClick必须挂载到全局作用域容易造成全局污染。我不建议在正经项目里用仅适合快速验证 Demo。第二种是 DOM 属性绑定const btn document.getElementById(myButton); btn.onclick function() { console.log(按钮被点击); };这种写法比内联干净但onclick属性只能赋值一个回调后赋值的会覆盖之前的。第三种是我最推荐的addEventListenerconst btn document.getElementById(myButton); const handleClick () console.log(点击事件触发); btn.addEventListener(click, handleClick); // 需要移除时传入同一个函数引用 btn.removeEventListener(click, handleClick);addEventListener支持多个监听器共存还能控制事件冒泡和捕获阶段使用{ once: true }可以自动执行一次后移除。如果你在处理动态渲染的列表还可以用事件委托的方式在父容器上统一监听减少内存占用。事件这块还有一个高频坑在循环里给多个元素绑定事件如果直接使用var索引会遇到和闭包陷阱一样的“全是最后一个值”的问题。现在推荐的做法是直接在事件回调里调用带参数的函数或者用>function getAction(type) { switch (type) { case create: return 创建; case update: return 更新; case delete: return 删除; default: return 未知操作; } }这里switch的每个分支都用了return所以不需要break。如果没写return记得在case尾加break否则会一路穿透到底这可是新手最容易踩的坑。如果判断条件比较多且是判断区间用if/else更合适。举个例子根据分数返回评级function getGrade(score) { if (score 90) return A; if (score 80) return B; if (score 70) return C; return D; }像这种提前return的写法能避免多层嵌套比用else if链更清爽。我在 code review 时经常建议别人用这种“早退出”风格。再说三目运算符适合那种“根据条件二选一”的场景const statusText isActive ? 启用 : 禁用;但三目一旦嵌套可读性会断崖式下跌。比如a ? b : c ? d : e这种我强烈不建议写阅读成本太高。遇到多种条件分支用if/else或switch更合适没必要为了炫技牺牲可读性。另外还有个“对象映射”的技巧适合纯取值场景比一串if/else更简洁const statusMap { active: 启用, disabled: 禁用, pending: 待审核, }; const text statusMap[status] || 未知状态;这种方式本质上是用对象做查找表逻辑清晰扩展性也好。3.2 循环语句for、for...of、forEach 与 map 的区别JS 的循环写法多得让人眼花缭乱。先分类for循环、while循环属于传统命令式for...of和for...in属于迭代式forEach、map、filter、reduce等属于数组方法。数组遍历上我的经验是分场景看待需要拿到索引用for循环或forEach((item, index) {})。只需要元素值用for...of或forEach。需要把数组“变”成另一个数组优先map。需要筛选数组用filter。需要把数组“汇总”成一个值用reduce。写一段映射的例子const prices [100, 200, 300]; // 对每个元素做变换 const withTax prices.map(p p * 1.1); console.log(withTax); // [110, 220, 330] // 筛选大于 150 的元素 const expensive prices.filter(p p 150); console.log(expensive); // [200, 300] // 累加求和 const total prices.reduce((sum, p) sum p, 0); console.log(total); // 600这里最值得说的就是map和forEach的区别。map会返回一个新数组适合“变换”操作forEach返回undefined适合“执行副作用”操作。很多初学者把map当循环用白白创建了新数组又不接收返回值浪费了性能也失去了语义。for...of相比forEach有个明显优势 —— 支持break和continue。如果你需要在遍历过程中提前终止循环for...of是更好的选择const arr [1, 2, 3, 4, 5]; for (const num of arr) { if (num 3) break; console.log(num); // 1, 2 }而forEach里用break会直接报语法错误用return也只能跳过当前迭代不能终止整个循环。这是一个非常隐蔽的坑代码不报错但逻辑不对。3.3 对象遍历与数组遍历的差异看到热搜词里同时有“js对象”和“js数组”我把对象和数组遍历放在一起对比因为写法上很容易混。对象遍历最常见的坑是for...in会把原型链上的可枚举属性也遍历出来这在很多继承场景下会造成意外结果。所以我现在遍历对象默认用Object.keys()/Object.values()/Object.entries()三件套配合for...of使用干净且可控。const obj { a: 1, b: 2, c: 3 }; // 推荐只遍历自身属性 Object.keys(obj).forEach(key { console.log(key, obj[key]); }); // 或者 for...of entries for (const [key, value] of Object.entries(obj)) { console.log(key, value); }数组用for...in也是一个常见错误。for...in遍历的是“索引键名”字符串类型顺序不保证而且会把数组上自定义的属性也遍历出来。数组遍历就应该用for...of、forEach或map系列各司其职。4. 异步编程Promise、Event Loop 与同步异步热搜词里“js promise”“js event loop过程”“js同步和异步”这几个词经常一起出现说明大家到了异步这块问题比较集中。JS 是单线程语言但通过事件循环机制实现了非阻塞的异步处理。不理解这一层写出来的代码容易出现“数据还没拿到就使用”的毛病或者回调地狱。4.1 同步与异步的本质区别同步代码按照书写顺序依次执行遇到耗时操作比如大循环会阻塞后续代码。异步代码则不会等待耗时操作完成它会先把后面的代码执行完等耗时操作出结果后再回来执行回调。先看一个很经典的例子console.log(开始); setTimeout(() { console.log(定时器回调); }, 0); console.log(结束); // 输出顺序开始 - 结束 - 定时器回调这里setTimeout的延迟时间是 0但它仍然不会在“结束”之前打印。原因是setTimeout注册的回调会进入任务队列要等当前宏任务执行完才轮到它。理解这个顺序是理解 JS 异步的第一步。同步和异步的选择直接决定了你处理接口请求、事件监听、定时任务的方式。比如你从服务端拉数据就必须用异步的方式处理响应否则页面会卡死。4.2 Promise 的核心用法Promise是解决异步问题的标准方案。它代表一个尚未完成、但预期会在未来完成的操作。一个 Promise 对象有三种状态pending进行中、fulfilled已成功、rejected已失败。状态只能从pending转到fulfilled或rejected且一旦改变就不会再变。最基本的用法function fetchData() { return new Promise((resolve, reject) { setTimeout(() { const success Math.random() 0.3; if (success) { resolve({ data: 接口数据 }); } else { reject(new Error(请求失败)); } }, 1000); }); } fetchData() .then(data { console.log(成功拿到, data); }) .catch(error { console.error(出错了, error.message); }) .finally(() { console.log(无论成败都会执行); });catch的链式位置很有讲究。如果你有几个.then把catch放在最后它能捕获前面任意一个.then里抛出的错误。这比在每个.then里单独写try/catch省事太多。实际项目里Promise经常配合async/await使用写法更接近同步思维async function loadData() { try { const data await fetchData(); console.log(data); } catch (error) { console.error(error); } }注意await必须在async函数里使用try/catch捕获的是异步操作中抛出的异常。这套组合拳目前是前端异步的主流写法。4.3 事件循环 Event Loop 机制Event Loop是 JS 异步的底层机制。简单来说JS 引擎有一个调用栈还有一个任务队列。同步代码直接进调用栈执行异步任务如setTimeout、网络请求、Promise回调会被挂起等时机成熟后进入任务队列。调用栈清空后事件循环会从任务队列里取回调来执行。这里要区分“宏任务”和“微任务”。setTimeout、setInterval、I/O 操作属于宏任务Promise.then、MutationObserver、queueMicrotask属于微任务。每一次事件循环先执行一个宏任务然后清空所有微任务再进行下一个宏任务。来看一个综合的例子console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve() .then(() { console.log(promise1); }) .then(() { console.log(promise2); }); console.log(script end); // 输出顺序 // script start // script end // promise1 // promise2 // setTimeout面试时这个题几乎必考。我记它的口诀是同步先跑微任务紧随宏任务垫底。但要注意宏任务和微任务各自也有多次排队的情况。深入理解这套机制后你在排查“为什么接口返回后页面没更新”这类问题时会有更清晰的思路。在我自己的开发经验里Event Loop理解不到位最典型的症状是用setInterval节流、在setTimeout里拿最新状态却拿到旧值、多个接口并发时回调顺序和预期不符。这些都是“异步时序”在作祟。5. 浏览器实战复制粘贴、打开 URL 与 JS 引入机制前面讲的都是语言本身这一节落到浏览器场景因为有相当多的核心语法是被浏览器 API 触发的。热搜词里“js实现复制粘贴”“js 打开url”“js引入”这些都属于业务开发里每天都在碰的活儿。5.1 原生实现复制粘贴功能复制粘贴是前端高频需求比如点击按钮把一段文本复制到剪贴板。早年间大家都在用document.execCommand(copy)配合一个临时创建的textarea元素。这个方法虽然能用但已经进入废弃流程官方推荐用的是 Clipboard API。新版 API 的使用方式async function copyText(text) { try { await navigator.clipboard.writeText(text); console.log(复制成功); } catch (err) { console.error(复制失败, err); } }注意navigator.clipboard只在安全上下文HTTPS 或 localhost里可用而且需要用户授权。如果你的站点在 HTTP 环境下或者浏览器版本较老得回退到execCommand方案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(); try { document.execCommand(copy); console.log(复制成功兼容方案); } catch (err) { console.error(复制失败, err); } finally { document.body.removeChild(textarea); } }一个实操心得在移动端 Safari 上execCommand可能会有“复制不生效”的问题通常是textarea没有聚焦或者没有设置readonly导致的。我实际踩坑后现在的策略是优先 Clipboard API失败再降级到execCommand并且把临时元素设为只读。5.2 打开 URL 与多标签页控制“js 打开 url”这个需求也很好理解。最常见的场景是点击按钮后跳转到新页面。最直接的方式是// 当前页面直接跳转 window.location.href https://example.com; // 新标签页打开 window.open(https://example.com, _blank);但window.open有个经常被忽略的坑如果它是在异步回调里被调用很容易被浏览器的弹窗拦截器拦掉因为浏览器只信任用户操作同步触发的window.open。如果确实需要异步获取 URL 后再打开可以先打开一个空窗口等拿到 URL 再赋值const newWindow window.open(, _blank); fetch(/api/get-redirect-url) .then(res res.json()) .then(data { newWindow.location.href data.url; });还有一个细节控制新窗口的位置和尺寸。用window.open的第三个参数可以设定width、height、noopener等特性。但现代浏览器对弹窗尺寸限制很严格很多属性已经不生效了。在单页应用里我更推荐用路由跳转而不是开新窗口。5.3 引入外部 JS 的三种方式与选型“js引入”看着基础但这里的坑还真不少。常见的引入方式有script标签直接引入、动态创建script标签、ES Module 的import。经典方式script srchttps://cdn.example.com/libs/sdk.js/script这是最简单的方式但要注意加载顺序。默认的script标签是同步加载、阻塞渲染的。如果脚本体积很大页面会出现白屏。现代浏览器的解决方案是加defer或async属性!-- deferHTML 解析完后按顺序执行 -- script defer srcscript.js/script !-- async下载完立即执行不保证顺序 -- script async srcscript.js/script我个人在页面性能优化的实践中习惯把普通脚本加上defer让它在 DOM 解析完毕后再执行不阻塞渲染。如果多个脚本之间有依赖关系用defer能保证顺序没有依赖的独立脚本用async可以更快加载。动态加载 JS 的场景也很多比如用户点击某个按钮后才加载对应的功能模块function loadScript(src) { return new Promise((resolve, reject) { const script document.createElement(script); script.src src; script.onload resolve; script.onerror reject; document.head.appendChild(script); }); } async function handleOpenEditor() { try { await loadScript(/vendor/editor.js); // 此时 editor.js 已加载完成可以调用它暴露的全局变量 window.Editor.init(); } catch (e) { console.error(编辑器加载失败, e); } }这种按需加载的方式能显著减少首屏体积。如果项目已经用了打包工具那直接用import()动态导入更省心构建工具会帮你处理代码分割。5.4 iframe 与父页面联动关闭、刷新与通信热搜词里“iframe关闭jquery并刷新父页面js”是个蛮具体的场景。前端的应用里有时候会嵌入 iframe 作为子应用或者弹窗。父子页面之间经常需要联动。先解决“关闭 iframe 并刷新父页面”。如果 iframe 本身是嵌入在父页面的 DOM 里关闭它可以直接移除或隐藏// 在 iframe 内部关闭自身并通知父页面刷新 window.parent.document.getElementById(myIframe).style.display none; window.parent.location.reload();如果 iframe 是作为弹窗组件比如 jQuery UI 的 dialog打开的刷新父页面的代码是window.parent.location.reload();子页面访问父页面的前提是同源。跨域情况下直接操作window.parent.document会被浏览器拦截。这种情况下需要用postMessage做安全的跨域通信。父页面监听 iframe 发来的消息window.addEventListener(message, function(event) { // 务必校验 event.origin防止跨域消息伪造 if (event.origin ! https://trusted-site.com) return; const data event.data; if (data.type done) { // 关闭 iframe、刷新页面等操作 document.getElementById(myIframe).style.display none; location.reload(); } });子页面发送消息window.parent.postMessage({ type: done, payload: 操作完成 }, https://parent-site.com);注意postMessage的第二个参数要指向父页面的确切源才能确保消息只被目标窗口接收。很多安全事件都源于对event.origin的校验缺失这个细节我会建议所有团队都要严格执行。5.5 浏览器插件如何调用页面内的 JS 函数“浏览器插件如何调用页面js函数”这条热搜说明不少开发者接到了浏览器插件的需求。这里稍微展开说一下原理层面上的方案。浏览器插件以 Manifest V3 为例主要分三个部分background后台脚本、content scripts内容脚本、popup弹出页面。内容脚本和页面共享同一个 DOM但它们运行在“隔离世界”里不能直接访问页面中的 JS 变量和函数。要调用页面里的函数通常需要把代码注入到页面上下文。最常用的做法是使用chrome.scripting.executeScript// background 或 popup 中 chrome.scripting.executeScript({ target: { tabId: tab.id }, func: (arg) { // 这里的代码运行在页面上下文中 if (typeof window.pageFunction function) { const result window.pageFunction(arg); return result; } return null; }, args: [参数] }, (results) { console.log(调用结果, results[0]?.result); });如果是通过content_scripts注入的脚本要向页面注入新函数可以创建一个script标签然后把源码塞进去// content script 中 const script document.createElement(script); script.textContent window.injectedFn function(val) { return window.somePageGlobalFn(val); }; ; document.documentElement.appendChild(script); script.remove();这段代码把injectedFn注入到页面上下文content script 和页面各自维护一份作用域通过 DOM 事件或postMessage互相通信。我最深的体会是插件开发和常规前端开发的心智模型完全不同。你写的代码可能运行在多个隔离世界里需要时刻想清楚“代码运行在哪个上下文”。调试时也要善用插件的devtools面板我经常在 content script 和页面 console 之间来回切换边看边试。6. 实用工具库与框架整合从 Excel 处理到 Vue 引入核心语法可以在实际项目里产生生产效率但很多时候我们也依赖第三方工具库来放大能力。热搜词里“操作excel的js工具库 - xlsx的使用方法”和“js深入浅出vue”这几条正好说明大家在基本语法之上还想知道怎么用库、怎么和框架搭配。6.1 使用 xlsx 库处理 Excel 文件前端处理 Excel最常用的库就是 SheetJS 的xlsx。我经常用它做导入导出功能比如把表格里的数据生成 Excel 文件下载或者读取用户上传的 Excel 并解析成 JSON。先看导出 Excel 的核心逻辑import * as XLSX from xlsx; const rows [ { name: 张三, age: 20 }, { name: 李四, age: 25 }, ]; // 把 JSON 数组转换成工作表 const worksheet XLSX.utils.json_to_sheet(rows); // 创建工作簿并写入工作表 const workbook XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 用户表); // 生成并触发下载 XLSX.writeFile(workbook, 用户数据.xlsx);这里需要注意json_to_sheet默认会用对象的 key 作为表头如果表头顺序不符合预期可以用aoa_to_sheet配合二维数组精确控制。再看解析上传的 Excelasync function parseExcel(file) { const data await file.arrayBuffer(); const workbook XLSX.read(data, { type: array }); // 读取第一个工作表 const firstSheetName workbook.SheetNames[0]; const worksheet workbook.Sheets[firstSheetName]; // 转成 JSON const jsonData XLSX.utils.sheet_to_json(worksheet); console.log(jsonData); return jsonData; }用sheet_to_json时默认会把第一行当表头。如果你的表格第一行不是表头可以加参数{ header: 1 }这样返回的就是二维数组每一行都是数组元素出错率会小一些。实际操作里常遇到的一个问题是“日期字段在 Excel 里是数字”。这其实是 Excel 内部存储日期的方式需要在解析时设置cellDates: true让 xlsx 自动转换。6.2 在前端做文件上传的 JS 校验文件上传是另一个非常典型的场景热搜词“ctfhub 文件上传 - js前端验证”虽然源自 CTF 题目但现实中前端同学也会遇到“前端 JS 校验文件类型”的需求。前端校验的目的是改善用户体验不能作为安全边界这个定位一定要清晰。常规的逻辑分成三步校验文件类型、校验文件大小、预览内容。function validateFile(file) { // 1. 校验类型 const allowedTypes [image/jpeg, image/png, application/pdf]; if (!allowedTypes.includes(file.type)) { alert(不支持的文件格式); return false; } // 2. 校验大小限制 5MB const maxSize 5 * 1024 * 1024; if (file.size maxSize) { alert(文件不能超过 5MB); return false; } return true; } // 配合上传控件 fileInput.addEventListener(change, (e) { const file e.target.files[0]; if (file validateFile(file)) { // 执行上传 uploadFile(file); } });注意file.type是浏览器根据文件内容推断的 MIME 类型但如果用户改了扩展名file.type可能仍然可信度不够。如果对安全要求更高可以在拿到文件后用 FileReader 读取文件头几个字节对比魔数比如 JPEG 以FF D8 FF开头。不过前端校验始终只能作为体验层面的拦截真正的合法性必须后端把关。6.3 JS 与 Vue 等框架的衔接“js深入浅出vue”这条热搜的出现很有意思。很多人一开始以为 Vue 是一套全新的语法其实 Vue 底层就是对原生 JS 的封装和增强。模板语法最终会被编译成 render 函数而 render 函数本质就是 JS 函数。理解这一点对排查框架里的问题特别有帮助。比如 Vue 的响应式原理其实就是用原生 JS 的Object.definePropertyVue 2或ProxyVue 3对数据对象做拦截。搜索“js和监听、”和“依赖收集”这些概念底层都是 JS 对象操作的进阶。如果你把 Vue 生命周期里的 mounted、computed、watch 这些概念拆开看会发现它们背后用到的还是事件、函数、异步队列这些核心语法。event loop 跑得顺不顺直接决定了 Vue 异步更新 DOM 的时序对不对。深入学习框架时我的习惯是先写原生 JS 实现同样功能看卡在哪一步再去查框架的文档。这样理解最深而不是一味背 API。7. 常见问题与排查技巧实录写代码的时间久了踩过的坑多了就会积累一套排查问题的思路。这一节把我在处理 JS 核心语法相关问题时的高频 Bug 和排查方法做个总结列入一张速查表方便你平时对照。7.1 典型异常与解决方案速查表现象可能原因排查方向函数未定义控制台报ReferenceError脚本加载顺序不对函数声明在未被加载的脚本中检查script顺序改用defer用typeof fn function判断数组方法执行报错xxx is not a function获取到的数据不是数组而是对象或字符串用Array.isArray()先校验检查接口返回的数据结构this在回调中变成 undefined普通函数里的 this 指向问题改用箭头函数或.bind(this)复制粘贴不生效非安全上下文或浏览器权限限制用 Clipboard API 前判断navigator.clipboard是否存在降级到execCommand循环里绑定的点击事件都是最后一个值var的作用域问题改用let声明循环变量或用闭包保存索引Promise 链没执行 catch忘记在 async/await 中使用 try/catch检查 Promise 链尾部是否有catchasync 函数内部是否统一 try/catch页面加载慢脚本阻塞渲染script标签是同步加载添加defer或async考虑动态按需加载脚本这张表不能穷尽所有问题但覆盖了我平时最高频遇到的几类。写代码时我建议多用控制台打印关键节点然后“从后往前推”逐步缩小出错范围。7.2 排查思路与调试技巧排查 JS 问题我的流程固定如下。第一先看控制台报错信息。报错信息里通常有文件名、行号和错误类型。ReferenceError大概率是变量或函数未定义TypeError大概是调用了不存在的方法。先把报错位置找到再去看上下文。第二善用console.log和断点。小项目直接用console.log打印可疑变量的值和类型。复杂逻辑就用浏览器开发者工具的断点调试在 Source 面板给关键行打断点程序执行到那里会停下这时可以在 Scope 面板看到所有变量当前的值还能单步执行逐行查看逻辑走向。第三验证“同步还是异步”的问题。如果发现输出顺序和预期不符注意检查代码里是否有setTimeout、Promise、await。异步时序导致的 bug 往往是“偶发”的比如接口快的时候没问题、慢的时候出问题这种问题console.log的时序能帮你快速定位。第四善用搜索和社区资源。我一开始调试浏览器插件的executeScript函数明明存在却调用不到最后是在社区讨论里看到“内容脚本和页面脚本隔离”这个概念才恍然大悟。别硬撑搜索引擎和社区的力量能帮你节省大量时间。7.3 两个值得记住的实操心得最后分享两个我个人的实操心得。第一个是关于数据结构转换。做前端久了你会发现很多业务 bug 的本质是数据结构不匹配。后端给了数组但组件需要对象接口返回了字符串但代码里当数字用。所以遇到莫名其妙的报错先打印typeof、Array.isArray()、Object.keys()把数据结构看清楚一半问题就消失了。第二个是关于“最小复现”。遇到复杂问题我会先把无关逻辑全部删掉只留最小可复现的代码片段。比如判断一个字符串包含某段内容可以只写 3 行代码在控制台里跑。这么做的好处是能快速区分问题出在“语法逻辑”还是“环境关联”。控制台就是最好的实验场随手写个console.log都比瞎猜快。我在实际开发中还有一个很深的感受JS 核心语法的掌握程度直接决定了你排 bug 的速度。语法熟的人写完代码心里大概有数语法不熟的人写一百行代码出十个 bug改不完。建议你把字符串、数组、对象、函数、Promise 这几块单独强化一下用真实的业务需求练手不用特意刷题。代码是练出来的不是看出来的。
返回列表