尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript高频实战:从基础函数到逆向防护的五个层次

JavaScript高频实战:从基础函数到逆向防护的五个层次 直接说结论这个标题我第一眼看到的时候也愣了一下“js--5”五个字符看起来像是随手起的代号。但结合近期的热搜词来看它对应的就是JavaScript生态里那些高频出现、绕不开的实战场景。“5”可以理解为我在整理这些零散热词时发现的一条隐藏线索——从最基础的函数、数组、字符串操作到时间格式化再到页面交互、文件处理最后到反爬逆向这类进阶领域恰好由浅入深串成了五个层次。这篇文章就按这个顺序来把每个层次里最常被问到、最常踩坑的点拎出来配上可直接复制的代码和完整的思路拆解。无论你是刚写完第一行console.log的新手还是已经被动态Cookie折磨过几天的老手应该都能从中找到点有用的东西。1. 内容整体设计与思路拆解1.1 为什么从五个层次入手整理热搜词的时候有个很明显的感受凡是被反复搜索的JS问题基本都集中在几个共性场景里——某个数组方法记不住用法、某个字符串操作不知道叫什么API、某个交互需求找不到现成方案、某个诡异报错查了半天才明白原因。这些内容单独看很零碎但把它们按“数据操作-时间处理-页面交互-文件进阶-安全防护”这条主线串起来其实就是一个前端开发者从入门到进阶的完整成长路径。“5”这个数字不是硬凑的而是我在梳理热词时发现的热度分层。搜索量最高的永远是js函数、js foreach、js map方法这类基础问题因为它们最常用、最容易记混稍微进阶一点的是js时间戳转时间格式、js判断字符串是否包含、js数组方法这种带明确场景的工具型需求再往上就是js三级联动、iframe关闭jquery并刷新父页面js这类综合性的页面交互然后是docxtemplater js替换word、前端js解码h264、apple musickit js这种特定的业务集成最后才是js逆向、js反爬实战、js反混淆工具这类偏安全方向的高级话题。五个层次对应五种不同的技术诉求。1.2 这篇文章的适用对象与阅读建议如果你是刚入行的前端新人建议从第2章开始把字符串和数组那部分认真看一遍尤其是我给的对比表格能帮你少走很多弯路。如果你是做了两三年的业务开发可以直接跳到第3章和第4章时间格式化和页面交互的实战代码基本可以抄了就改。如果你已经对基础API很熟练正在往逆向或安全方向发展第5章里那部分内容可能更对你胃口。有一点要提前说明这篇文章里的所有代码都基于ES6语法运行环境是现代浏览器和Node.js 14以上的版本。关于js反爬实战、js逆向这类内容我会讲原理、讲思路、讲对抗逻辑但不会提供任何可以直接用于破解商业网站的完整代码。原因很简单——技术本身是中性的但批量抓取数据、绕过付费限制这些行为涉及法律风险我希望这篇文章能帮助你理解防护方是怎么思考的而不是教你怎么突破别人的防线。这也是一个从业者该有的底线。2. 基础不牢地动山摇字符串、数组与对象的核心操作实战2.1 这类基础操作怎么学才记得住很多新人学JS有个误区喜欢把MDN当字典从头到尾翻一遍结果翻完就忘。我个人的经验是学基础API最好的方式不是背而是带着场景去搜。“怎么判断一个字符串里有没有某个字”、“怎么把两个数组合并成一个”、“怎么把数组里符合条件的内容挑出来”——带着这些具体问题去查查到一个记一个用不了几次就熟了。热搜词里恰好就有几个最典型的需求js判断字符串是否包含、js字符串数组取交集、js怎么用扩展运算符把一个数组里面的值都添加到另外一个数组、js foreach和js map方法。这几个问题背后对应的API几乎是日常工作里使用频率最高的JS基础能力。我先把它们一次性讲透。2.2 字符串包含判断的四种姿势判断一个字符串是否包含另一个子串ES6之前最常用的是indexOfES6之后我们可以用更语义化的includes。这俩的区别很多人搞不清楚我放个对比方法返回值是否区分大小写能否传起始位置兼容性str.indexOf(subStr)子串首次出现的索引找不到返回-1区分可以全兼容str.includes(subStr)返回布尔值区分可以IE不支持str.startsWith(subStr)返回布尔值区分可以IE不支持str.search(regexp)匹配到的索引找不到返回-1按正则规则不能直接传位置全兼容实际开发中如果是简单的“判断是否包含”我推荐直接用includes因为代码语义最清晰——if (str.includes(目标))一眼就能看懂。但有个坑必须提醒includes在IE里完全没有兼容性如果你的项目还要支持IE老老实实用indexOf ! -1。// 推荐做法现代浏览器直接用 includes const content JavaScript 是目前最流行的编程语言之一; console.log(content.includes(编程语言)); // true // 兼容写法老项目里用 indexOf console.log(content.indexOf(编程语言) ! -1); // true // 需要判断开头结尾时 const fileName report_2024.pdf; console.log(fileName.startsWith(report)); // true console.log(fileName.endsWith(.pdf)); // true还有一个容易被忽略的细节includes是区分大小写的如果需要不区分大小写得先把两个字符串统一转成小写或大写再判断。这个技巧在处理用户输入校验、搜索过滤时非常常用。2.3 数组取交集、合并与遍历的高频写法关于数组热搜词里同时出现了js数组方法、js map、js foreach、js repeat方法、js 字符串数组取交集、js怎么用扩展运算符把一个数组里面的值都添加到另外一个数组这背后是几类完全不同的需求。先说扩展运算符合并数组这是ES6里最实用的语法糖之一。它解决的问题是把数组A的所有元素添加到数组B里。老写法是concat或者循环push新写法一行搞定const baseList [1, 2, 3]; const extraList [4, 5, 6]; // 用扩展运算符创建一个新数组 const merged1 [...baseList, ...extraList]; console.log(merged1); // [1, 2, 3, 4, 5, 6] // 如果有去重需求可以先合并再转 Set const dedupMerged [...new Set([...baseList, ...extraList])]; // 注意扩展运算符是浅拷贝数组里若存放的是对象复制的是引用 const objArr1 [{ name: 张三 }]; const objArr2 [...objArr1]; objArr2[0].name 李四; console.log(objArr1[0].name); // 李四原数组也被改了再说取交集。面试里经常考实际业务里也经常会遇到——比如两个标签列表要找出同时存在的标签。经典解法是用filter配合includes或者用Set提高性能// 简单可读的写法filter includes const listA [苹果, 香蕉, 西瓜, 葡萄]; const listB [香蕉, 葡萄, 橘子]; const intersection listA.filter(item listB.includes(item)); console.log(intersection); // [香蕉, 葡萄] // 大数据量时的优化写法Set filter const setB new Set(listB); const intersectionFast listA.filter(item setB.has(item)); console.log(intersectionFast); // [香蕉, 葡萄]includes在数组上的原理就是遍历如果数组B很大每次判断都要完整走一遍时间复杂度和O(n*m)没什么区别。而Set.has()的时间复杂度是O(1)数据量大的时候性能差距非常明显。写代码不能只看功能还得考虑数据规模。至于foreach和map的区别我已经解释过无数次了forEach只是遍历不返回新数组map会基于回调的返回值创建新数组。什么时候用哪个判断标准只有一个——你到底需不需要一个新数组。需要就map不需要就forEach。千万别用forEachpush到一个空数组里来实现map的功能那是反模式。3. 逃不掉的时间处理时间戳转格式与日期校验3.1 手写格式化函数还是引入dayjsjs时间戳转时间格式yyyy-mm-dd这个搜索词精准地命中了前端开发的日常刚需。很多项目里都会遇到“后端返回了一个时间戳前端要在页面上显示成2024-06-18 14:30:00这种格式”的需求。第一反应是引入moment.js或dayjs之类的库但如果是很小的页面、不想引入额外依赖手写一个函数也完全够用。我个人的建议是追求开发效率就直接用dayjs它体积只有2KB左右API和moment几乎一致但如果只是格式化时间戳这一个需求实在没必要为一个函数引入整个库。手写并不难关键是处理好补零。function formatTimestamp(timestamp, pattern YYYY-MM-DD HH:mm:ss) { const date new Date(timestamp); const year date.getFullYear(); const month String(date.getMonth() 1).padStart(2, 0); const day String(date.getDate()).padStart(2, 0); const hours String(date.getHours()).padStart(2, 0); const minutes String(date.getMinutes()).padStart(2, 0); const seconds String(date.getSeconds()).padStart(2, 0); return pattern .replace(YYYY, year) .replace(MM, month) .replace(DD, day) .replace(HH, hours) .replace(mm, minutes) .replace(ss, seconds); } console.log(formatTimestamp(Date.now())); // 输出示例2024-06-18 14:30:00 console.log(formatTimestamp(Date.now(), YYYY年MM月DD日)); // 输出示例2024年06月18日这个写法用的是padStart补零比传统的三元判断更简洁。有个细节需要注意getMonth()返回的是0-11所以必须加1getHours()返回的是24小时制的0-23如果产品要求显示“下午2点”这种12小时制需要自己再处理一下。3.2 时间戳的隐式转换与时间戳的“秒”和“毫秒”陷阱写这个函数的时候有一个高频坑要提前说清楚new Date(timestamp)能解析的是毫秒级的时间戳。但很多后端接口返回的时间戳是秒级的比如1718700000。如果你直接把秒级时间戳扔给new Date()生成的时间会是1970年1月然后你会得到一个完全对不上的日期。检测方法很简单一个合法的时间戳new Date(timestamp)生成的时间年份在1970到2000之间那大概率就是秒级时间戳。解决办法是判断时间戳的位数——13位的是毫秒10位的是秒。function normalizeTimestamp(timestamp) { // 10位数字说明是秒级乘以1000转为毫秒 return String(timestamp).length 10 ? timestamp * 1000 : timestamp; } // 使用 const safeDate new Date(normalizeTimestamp(1718700000));顺带说一个日常开发中很实用的技巧如果你想格式化的是“今天”“昨天”这种相对时间光靠Date对象是搞不定的得自己计算时间差。逻辑也不复杂取当前时间戳减去目标时间戳除以一天的毫秒数86400000根据结果判断是今天、昨天还是更早。function getRelativeTime(timestamp) { const diff Date.now() - normalizeTimestamp(timestamp); const day 24 * 60 * 60 * 1000; if (diff day) return 今天; if (diff day * 2) return 昨天; return formatTimestamp(timestamp, YYYY-MM-DD); }4. 页面交互实战三级联动、新窗口下载与父页面刷新4.1 三级联动一次讲透js三级联动是一个超经典的需求最常见的场景就是省市区选择。整套逻辑的核心就是“数据的级联关系”——选完省市的下拉框更新选完市区的下拉框更新。热搜词里专门有人搜这个说明很多人卡在了“选中变化后如何用JS重新渲染下拉框”这一环。先给一个简化版的数据结构实际业务里这种数据一般直接来自后端接口const regionData { 北京市: { 北京市: [东城区, 西城区, 朝阳区] }, 广东省: { 广州市: [天河区, 越秀区, 海珠区], 深圳市: [南山区, 福田区, 罗湖区] } };核心思路分三步第一步页面初始化时先把省的下拉框option渲染好第二步监听省下拉框的change事件根据当前选中值找到对应的市级数据清空并重填市下拉框同时清空区下拉框第三步监听市下拉框的change事件同理更新区下拉框。// 获取元素 const provinceSelect document.getElementById(province); const citySelect document.getElementById(city); const districtSelect document.getElementById(district); // 初始化省级下拉框 function initProvince() { Object.keys(regionData).forEach(province { const option document.createElement(option); option.value province; option.textContent province; provinceSelect.appendChild(option); }); } // 根据省份更新城市下拉框 function updateCities(province) { citySelect.innerHTML option value请选择城市/option; districtSelect.innerHTML option value请选择区县/option; const cities Object.keys(regionData[province] || {}); cities.forEach(city { const option document.createElement(option); option.value city; option.textContent city; citySelect.appendChild(option); }); } // 根据城市更新区县下拉框 function updateDistricts(province, city) { districtSelect.innerHTML option value请选择区县/option; const districts regionData[province]?.[city] || []; districts.forEach(district { const option document.createElement(option); option.value district; option.textContent district; districtSelect.appendChild(option); }); } // 绑定事件 provinceSelect.addEventListener(change, (e) updateCities(e.target.value)); citySelect.addEventListener(change, (e) updateDistricts(provinceSelect.value, e.target.value)); // 启动 initProvince();这里有个新手很容易忽略的点更新城市下拉框之前一定要先把区下拉框也清空。否则会出现这种情况——你原本选了“广东省-深圳市-南山区”现在把省改成“北京市”区里还留着“南山区”明显是脏数据。联动逻辑讲究的是每层变化都要重置所有下级这是完整性的关键。4.2 新窗口打开并POST提交下载文件js 打开新界面 post请求下载文件这个需求乍一看有点绕其实描述的是这样一个场景点击“导出报表”按钮需要携带一些参数向后端发起POST请求后端返回一个文件流浏览器下载文件。为什么要用POST因为参数可能很长、可能有复杂结构GET请求的URL根本塞不下。最直接的做法是动态创建一个form表单设置methodpost和action为后端接口地址然后target_blank触发下载function downloadWithPost(url, params) { // createElement 创建表单 const form document.createElement(form); form.method POST; form.action url; form.target _blank; // 新窗口打开避免当前页面跳走 // 把参数塞进隐藏的 input Object.entries(params).forEach(([key, value]) { const input document.createElement(input); input.type hidden; input.name key; input.value value; form.appendChild(input); }); // 必须把表单添加到 body 上才能成功 submit document.body.appendChild(form); form.submit(); document.body.removeChild(form); } downloadWithPost(/api/export, { startDate: 2024-01-01, endDate: 2024-06-18, type: xlsx });原理其实很简单浏览器解析到form.submit()时会按表单的method和action发起请求target_blank让响应在新窗口打开对于Content-Disposition: attachment的文件流响应来说新窗口不会打扰当前页面文件就会自动下载。4.3 iframe内联页面操作父页面刷新iframe关闭jquery并刷新父页面js这种搜索词一看就是在做老后台系统或富文本编辑器嵌入页面时的需求。典型场景是父页面打开了一个iframe弹窗你在iframe里完成了新增或修改操作点“关闭”按钮时需要通知父页面刷新列表。最基础的方式是直接用window.parent访问父页面的全局函数// iframe 子页面里 function closeAndRefreshParent() { const isModal window.self ! window.top; if (isModal) { // 调用父页面的全局方法刷新列表 window.parent.refreshList(); } // 如果是 window.open 打开的新窗口用 window.opener if (window.opener) { window.opener.refreshList(); } // 关闭当前窗口或 iframe window.close(); }如果父页面用的是jQuery那更简单的写法是通过jQuery的全局对象往里塞一个函数// 父页面里定义 $.extend({ refreshList: function() { // 重新请求列表 loadList(); } });有个细节值得注意如果你在iframe里直接调用window.close()有些浏览器会因为“非脚本打开的页面不能被关闭”的限制而报错。最稳妥的方式是先把关闭动作交给父页面处理——在父页面里提供一个专门用于关闭弹窗的函数iframe只负责通知父页面让父页面来关闭自己。// 父页面 function closeIframeModal() { $(#myModal).modal(hide); // bootstrap modal 示例 } // iframe 子页面 window.parent.closeIframeModal(); window.parent.refreshList();5. 文件与前端进阶应用FileReader、Word替换与视频解码5.1 FileReader应用场景js filereader对应的是浏览器读取本地文件的能力。最常见的场景是用户上传图片前端不经过后端直接预览或者用户选择CSV文件前端解析成表格展示。FileReader的用法比较固定核心就三步拿File对象、创建FileReader实例、绑定onload读取结果。// input 选择的文件 const input document.getElementById(fileInput); input.addEventListener(change, function (e) { const file e.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload function (ev) { // 根据需求选择结果类型 // 如果是图片预览 const img document.getElementById(preview); img.src ev.target.result; }; // readAsDataURL 适合图片、音频等需要展示或播放的场景 reader.readAsDataURL(file); });FileReader还有几个读取模式readAsText读文本文件、readAsArrayBuffer读二进制数据、readAsDataURL读成Base64字符串。不同模式适合不同场景比如解析CSV用readAsText图片预览用readAsDataURL处理音频波形之类的需求用readAsArrayBuffer。5.2 docxtemplater替换Word模板docxtemplater js替换word是办公自动化领域很典型的需求。核心思路是先准备一个带占位符的.docx模板比如合同模板里写{companyName}然后通过docxtemplater这个库将占位符替换成实际数据最后生成新的Word文件供下载。基本用法如下import PizZip from pizzip; import Docxtemplater from docxtemplater; import { saveAs } from file-saver; // 读取模板文件二进制内容 const templateFile await fetch(/template.docx).then(res res.arrayBuffer()); const zip new PizZip(templateFile); const doc new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, }); // 要替换的数据 doc.render({ companyName: 某某科技有限公司, projectName: 智慧园区管理平台, contractAmount: 1,280,000元, signDate: 2024年06月18日 }); // 生成并下载新文件 const blob doc.getZip().generate({ type: blob, mimeType: application/vnd.openxmlformats-officedocument.wordprocessingml.document }); saveAs(blob, 合同_某某科技有限公司.docx);这个方案的优点是模板完全由非技术人员用Word编辑业务人员可以直接控制版式开发人员只负责传数据分工很干净。常见的坑有两个一是docxtemplater对模板里占位符的格式要求严格{companyName}如果被Word自动修正成花括号字体不一致替换会失败二是如果模板里有表格要做循环表格还得额外配置loop语法不能只做简单的文本替换。5.3 前端解码H264的可能性与限制前端js解码h264是一个比较硬核的方向。为什么要前端解码因为某些直播流的编码格式原生video标签的HLS或MP4支持无法直接播放比如低延迟的HTTP-FLV流或者特殊的webrtc传输场景。这时候前端就得上软解或者硬解方案。目前比较成熟的方案有Broadway纯JS写的H.264解码器性能一般适合低分辨率或测试场景。WASM版本的ffmpeg复杂但能力最强通过WebAssembly在浏览器里跑ffmpeg解码可以处理各种编码格式。优先利用浏览器原生能力大多数现代浏览器对H.264的支持已经很好了直接用video srcxxx.mp4或者MediaSource加载分段视频就够了没必要强行上软解。我的建议是除非业务有特殊需求比如实时性要求极高的低延迟播放否则不要轻易走上“前端解码”这条路。它涉及大量底层知识调优难度大而且性能受用户设备影响严重。折腾半天可能还不如让后端转封装成浏览器原生支持的格式来得省事。6. 防护与逆向基础认知JS混淆、反爬与动态Cookie的原理6.1 为什么会有JS反爬和JS混淆js逆向、js反爬实战、js反混淆工具、5秒盾返回的js怎么解密这些热词背后其实是同一个问题Web站点的代码是跑在用户浏览器里的只要你想看就能看到源码。但对于服务方来说有些逻辑和数据需要保护比如下方的接口不能被人批量爬取于是就用混淆、加密、动态生成Cookie等手段增加逆向成本。这里要先明确一个“攻”与“守”的边界从防护方的角度看JS混淆、动态Cookie、浏览器指纹都是合理的反爬手段目的是提高机器人批量请求的成本从研究者的角度看理解这些技术能帮助你做好自己的安全防护或者处理一些合法授权场景下的调试需求。这篇文章只讲原理层面的内容。JS混淆分几个级别最低级的是压缩混淆把变量名改成单个字母去掉注释和空格中级的是控制流平坦化把原本顺序执行的代码拆散用switch分发高级的是各种花指令插入、字符串加密、代码虚拟化。级别越高逆向难度越大但代码体积膨胀越严重性能损耗也越大。6.2 动态Cookie与5秒盾的基本逻辑不少反爬场景里你第一次请求某个网页时会发现返回的不是页面内容而是一段JS。这段JS会在浏览器里执行一些操作可能设置一个Cookie也可能计算出一个动态Token然后带着结果重新请求一次才能拿到真实页面。这就是俗称的“5秒盾”或“Cookie反爬”。对于正常用户来说整个过程在零点几秒内完成完全感知不到但对于脚本爬虫来说如果不模拟这些JS逻辑就拿不到真正的数据。动态Cookie的常见做法是服务端下发一段包含摘要算法或加密逻辑的JS让代码在浏览器端生成一个随时间和指纹变化的Cookie值再带这个Cookie去访问真实资源。这样做的好处是每次请求的Cookie值都不同纯静态伪造很难成功坏处是服务端要维护校验逻辑计算逻辑也不能太复杂否则正常用户的等待时间会变长。6.3 前端开发者如何正确看待逆向关于js逆向我给你一个比较成熟的从业者建议把重点放在理解原理和防护上而不是绕过别人的防护上。一个前端开发者如果能看懂混淆后的代码、能理解动态Cookie的生成流程、能自己写一段简单的反爬逻辑这对职业发展是有帮助的——因为你知道了攻击者的思路才能写出更健壮的防护代码。但在实际工作中要守住底线——没有任何授权的情况下逆向别人的接口去抓数据轻则违反网站服务条款重则触犯法律。正确的做法是遇到反爬需求先确认数据是否有公开API或官方接口没有的话和对方团队沟通获取授权实在不行换一个数据源。技术方案永远是服务于业务的别为了炫技把自己搭进去。7. 常见问题与排查技巧实录7.1 deprecation warninglegacy-js-apideprecation warning [legacy-js-api]: the legacy js api is deprecated这个东西在Google Maps JavaScript API里经常出现。如果你在控制台看到这条警告说明你还在用Google Maps JavaScript API的旧版调用方式比如google.maps.Map、google.maps.Marker这些而Google已经推出了新的API版本旧接口即将被移除。解决办法是查一下官方文档看你的项目中用了哪些旧接口然后迁移到新版本。迁移过程一般涉及类名变更比如google.maps.Marker变成了google.maps.marker.AdvancedMarkerElement、初始化方式变更等。警告本身不影响当前功能但从维护角度看早晚得改越晚改越痛苦。7.2 foreach与map的三大记混场景js foreach和js map的混淆是新手最常见的问题复盘一下最容易出错的三个场景第一想在forEach里返回一个新的处理后的数组然后发现得到的是undefined。因为你把forEach当成map用了它压根不返回东西。第二想让map执行一些有副作用的操作比如打印日志、修改外部变量然后发现返回值没用上。这种场景合理但容易造成代码可读性下降。第三在一个链式调用链中forEach后面接不了方法因为它的返回值是undefined。如果后面还想继续筛选或排序就得改用map或者先遍历再操作。7.3 repeat、replace、replaceAll的边界情况关于js repeat方法有一个容易摔跤的地方当你用abc.repeat(-1)或者abc.repeat(Infinity)时会直接抛RangeError。所以做文本拼接之前最好先确认次数是合理的非负整数。js replace方法也有个经典坑默认只替换字符串中的第一个匹配项如果你要替换全部需要用正则加g标志或者用ES2021新增的replaceAllconst text aaa-bbb-ccc; console.log(text.replace(-, /)); // aaa/bbb-ccc只替换了第一处 console.log(text.replace(/-/g, /)); // aaa/bbb/ccc传统全局替换 console.log(text.replaceAll(-, /)); // aaa/bbb/ccc更语义化7.4 数据类型转换的几个隐性坑与js字符串长度、js对象、js宏相关的坑有一个共同点类型不匹配。比如你好.length得到2但.length得到4——这是因为emoji在JavaScript里是代理对一个字符占两个UTF-16码元。处理用户输入时如果你用length做长度限制很容易把emoji截断成乱码。正确做法是用Array.from(s)或[...s]来按字符拆分const s ab; console.log(s.length); // 4不直观 console.log(Array.from(s).length); // 3按实际字符计js宏这个词要注意区分场景在浏览器环境它可能指宏任务(setTimeout、setInterval)在Excel的VBA里它又是另一套概念。如果是前端面试问“宏任务和微任务”那问的是事件循环机制在JS开发中我们要记住的是执行顺序同步代码先执行然后微任务Promise.then、MutationObserver最后宏任务setTimeout等。用这个顺序理解异步代码的执行结果基本可以解决90%的疑惑。8. 最后再分享几个实用技巧第一遇到不会写的JS基础方法与其背API文档不如用console.log把返回值打印出来看看。很多方法记不住是正常的记住“console.log能帮你理解一切”就够了。第二关于js验证url有效性不要再写复杂的正则了。用new URL(str)包一层try-catch比什么正则都可靠。function isValidUrl(str) { try { new URL(str); return true; } catch { return false; } }第三如果你觉得typescript和js的区别还搞不清楚我的建议是先扎实学好JS再学TS。TS是JS的超集它解决的是大型项目的类型安全问题但前提是你得先理解JS里的动态特性否则TS只会让你觉得处处受限制而感受不到它的价值。这篇文章能覆盖的肯定还是有限的JS生态太庞大了。但如果你能把我上面提到的这些高频场景都跑通基础能力基本就够用了。剩下的就是在真实项目里边做边学慢慢积累。
返回列表