
JavaScript这门语言说简单是真的简单浏览器里写个console.log(hello world)就能跑说难也真的难闭包、原型链、事件循环、类型隐式转换任何一个拿出来都能让刚入门的朋友懵半天。我这些年带过不少新人也帮别人排查过无数线上问题发现绝大多数报错和诡异行为归根结底都落在基础知识点不扎实上。所以我一直觉得JS基础知识点合集这种东西不是给新手背的而是给所有写JS的人在踩坑之后回头翻的案头手册。这篇内容我会按照实际开发中最常碰到的几个方向来梳理类型判断与转换、函数与事件、数值精度与Canvas实操、运行时错误排查、以及性能优化思路。里面既有原理层面的解释也有可以直接复制去用的代码片段还会穿插一些我踩过的坑和当时的处理过程。适合刚学完JS语法、准备写真实项目的初学者也适合写过一阵子但总被类型和报错折磨的进阶选手。你不用按顺序读完挑自己最近遇到的问题去查就可以。1. 先理清JavaScript基础到底学什么1.1 我理解中的核心关键词如果只允许我用几个词来概括JavaScript基础我会选数据类型、函数、事件、作用域、异步、DOM操作、错误处理。这几个方向几乎覆盖了日常开发95%以上的场景。比如数据类型直接关系到后端返回的JSON你能否正确解析函数决定了你写出来的业务代码能不能复用事件是所有交互效果的基础异步则关系到接口请求和页面渲染的节奏。我在面试新人时比较喜欢问一个问题typeof null的结果是什么很多人脱口而出null但正确答案是object。这个知识点看似刁钻其实背后牵扯到JavaScript中null作为空指针标记的历史设计也牵扯到类型判断时不写防御性代码就会踩雷的现状。类似这样的小坑才是基础知识点真正有价值的地方。它们不会出现在教程的前三章但会出现在你加班的每个深夜里。另外我还想强调一点JavaScript的基础不只是语法还包括运行环境。同样的代码在浏览器里跑和Node.js里跑作用域、全局对象、模块加载方式都不一样。所以我在整理知识点时会刻意区分哪些是语言本身的东西哪些是宿主环境提供的能力比如window、document只在浏览器里存在。这样你在不同场景下手写代码才不会张冠李戴。1.2 基础知识点总结的思路很多朋友也问过我像Python基础知识点总结那种清单式的整理方法能不能套用到JavaScript上。我的回答是可以而且应该这么做。语言千差万别但知识体系整理的核心思路是一样的先分模块再找每个模块的最小可用知识点然后通过一两个典型场景把知识点串起来。我自己的做法比较简单。我会按数据怎么存、逻辑怎么组织、交互怎么实现、错误怎么处理四个维度来建自己的知识树。数据怎么存就是变量、类型、引用逻辑怎么组织就是函数、对象、模块交互怎么实现就是DOM、事件、动画错误怎么处理就是异常捕获、调试工具、边界场景。每次学到新东西就往这四个分类里塞。这样积累半年你会发现自己的知识树越来越茂密遇到问题也能很快定位到是哪个分支的知识不够用。2. 数据类型判断和转换是绕不开的坎2.1 typeOf的局限和真正的判断套路JavaScript的数据类型可以分为原始类型和引用类型两大类。原始类型包括string、number、boolean、null、undefined、symbol、bigint引用类型主要是object以及它的各种子类型比如数组、函数、日期、正则等。基础中的基础就是能快速判断一个变量到底是什么类型。typeof是最直觉的工具但它有三个明显的坑。第一typeof null object这是语言设计时留下的历史问题我们无法改变它只能在使用时多做一层判断。第二typeof对数组、日期、正则这些对象统一返回object你无法区分具体是哪种子类型。第三对于NaNtypeof返回number但NaN表示的是一个非法数值不能用常规的相等判断来识别。所以在生产环境里我推荐用Object.prototype.toString.call()这个办法。它可以返回类似[object Array]、[object Date]、[object RegExp]这样的精确信息。我封装过一个很小的工具函数function getType(value) { return Object.prototype.toString.call(value).slice(8, -1); } console.log(getType(null)); // Null console.log(getType([1, 2])); // Array console.log(getType(new Date())); // Date console.log(getType(/test/)); // RegExp这段代码看起来简单但我建议你把.slice(8, -1)这一步背下来因为很多三方库内部就是这样判断类型的。另外如果你只需要判断是否是数组直接用Array.isArray就好这是目前最稳妥的数组判断方法比instanceof Array更可靠尤其是遇到跨iframe场景时。跨iframe时数组的原型链可能和你当前环境的Array.prototype不一致instanceof就会误判但Array.isArray不受影响。2.2 常见类型转换的坑类型转换是JavaScript里最容易出诡异问题的地方。我见过最典型的一个是后端返回的字段偶尔是数字偶尔是字符串前端直接用去判断结果数字和字符串被隐式转换等于判断失效。我的建议很简单全程使用不要给隐式转换留下机会。这条规则适用于几乎所有业务代码。还有一个高频问题是用加号拼接字符串时数字被意外转换。比如1 2的结果是12而不是3。这不是Bug而是语言规格里定义的行为当加号两边有一个是字符串时另一个会被转成字符串。知道这个规则以后写代码时就会主动用模板字符串或明确调用String()来转换而不是依赖隐式行为。布尔值转换同样有很多误用。if (value)这种写法看起来简洁但空数组、空对象、字符串false都会被视为真值。我通常会用更显式的判断// 判断一个值是否为“有效”的字符串 function isValidString(value) { return typeof value string value.trim().length 0; }这样写虽然啰嗦但阅读代码的人一眼就能看出意图不用再去猜变量到底是什么类型。类型判断的最终目的不是让代码更短而是让代码在半年后还能被人读明白。2.3 类型判断在生产环境中的推荐写法生产环境里的类型判断往往不是单打独斗而是组合拳。我举一个从接口拿到用户信息的例子。function formatUserInfo(rawData) { if (!rawData || typeof rawData ! object) { return null; } const id rawData.id; const name typeof rawData.name string ? rawData.name.trim() : ; const tags Array.isArray(rawData.tags) ? rawData.tags : []; const age Number.isFinite(rawData.age) ? rawData.age : 0; return { id, name, tags, age }; }这里面每一项判断都有目的。typeof rawData object先拦住非对象数据Array.isArray确保后面的tags.map操作不报错Number.isFinite则把字符串形式的数字、Infinity、NaN都排除掉比typeof rawData.age number更严格。推荐你把这种防御性取值的写法当作默认习惯因为接口数据永远不像文档里写的那么干净。我接手过的项目里线上问题至少有三成是接口字段类型变化导致的提前在入口处做类型约束能省掉大量后患。3. 函数与事件把逻辑组织起来的两种方式3.1 函数声明与函数表达式的差异函数是JavaScript里组织逻辑的最小单位但很多人分不清函数声明和函数表达式的区别。简单说函数声明会整体提升到当前作用域顶部所以你在声明之前调用它是合法的函数表达式则不行你必须在赋值之后才能调用。看这个例子sayHello(); // 正常输出因为函数声明被提升 function sayHello() { console.log(hello); } const sayBye function () { console.log(bye); }; sayBye(); // 这行没问题但把调用挪到const之前就会报错看起来只是顺序问题但在复杂模块里这会导致一种很隐蔽的Bug你在文件靠前的某个函数里调用了后面声明的一个函数页面初始化时一切正常一旦某个模块加载顺序变化就会出现函数未被定义的报错。我的习惯是优先使用const加箭头函数来声明业务函数偶尔用普通函数声明来组织工具函数。箭头函数还有一个额外好处它没有自己的this在回调函数里能直接访问外层作用域的this这在React类组件和事件回调里特别顺手。3.2 事件绑定与事件委托JavaScript的事件系统是整个页面交互的基础。addEventListener是现代浏览器推荐的绑定方式它可以绑定多个同一类型的事件处理函数也支持捕获阶段监听。顺便提一句onclick这种属性赋值方式并不是不能用而是它会覆盖之前的处理函数而且难以移除维护起来比较被动。事件委托是我特别推荐每个前端掌握的技巧。它的原理是利用事件冒泡机制把子元素的事件统一交给父元素处理。典型的例子是一个动态列表的点击事件。document.querySelector(#list).addEventListener(click, function (event) { const target event.target.closest(.item); if (!target) return; console.log(点击了, target.dataset.id); });这段代码有几个细节值得注意。event.target总是最深的那个被点击元素所以用closest(.item)往上找最近匹配的祖先找不到匹配就提前返回数据通过>function callNative() { if (window.webkit window.webkit.messageHandlers) { window.webkit.messageHandlers.openPage.postMessage({ pageId: home }); } }反过来OC调用JS就方便很多直接通过WKWebView的evaluateJavaScript方法执行一段JS字符串即可比如回调页面上的某个函数。这里我要提醒一个实战问题调用时机。WebView加载页面是异步的如果原生在DOM还没准备好的时候就调用JS会静默失败。我通常会在页面加载完成后由JS主动通知原生我准备好了原生拿到这个信号再发起调用。这种先握手再通信的思路能省掉很多玄学式的不响应问题。4. 数值精度与Canvas动手写几个实用小效果4.1 保留两位小数的正确做法网上搜javascript保留两位小数你可能会看到五花八门的回答toFixed(2)、Math.round()、正则替换等等。我用实际踩坑经验告诉你toFixed(2)不是万能的因为它返回值是字符串而且对部分小数存在四舍五入不稳定的情况。console.log((1.005).toFixed(2)); // 1.00预期可能是1.01这是因为1.005在二进制浮点数里的表示精度问题。所以处理金额、百分比这类需要精确计算的场景我建议先把数值放大成整数计算完成后再缩小回来function roundToTwo(num) { return Math.round((num Number.EPSILON) * 100) / 100; } console.log(roundToTwo(1.005)); // 1.01如果你需要字符串结果且要保持两位小数再配合调用toFixed(2)。这里最关键的一点是区分计算结果和展示结果。数值计算用整数或Number.EPSILON修正UI展示时才转成字符串千万不要在计算过程中反复使用toFixed否则你累加的金额会在某一天突然差出一分钱。做电商的朋友看到这里应该有共鸣。4.2 用Canvas画一幅云彩素材关于javascript素材 云彩很多人想要的是现成素材文件但作为一个前端掌握用Canvas直接生成云彩素材的能力会更实用。你可以把它用在网页背景、H5海报或者游戏场景里。Canvas画云彩的常见思路是用多个半透明圆形叠加。任何一个云都可以拆解成一个大圆底加上几个小圆凸起给它们统一填充白色再降低全局透明度边缘就会呈现出蓬松感。我这里写一个最小可用版本function drawCloud(ctx, x, y, scale) { ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale); ctx.fillStyle rgba(255, 255, 255, 0.8); ctx.beginPath(); ctx.arc(0, 0, 40, 0, Math.PI * 2); ctx.arc(40, -15, 30, 0, Math.PI * 2); ctx.arc(-40, -10, 25, 0, Math.PI * 2); ctx.arc(15, -30, 25, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } const canvas document.getElementById(sky); const ctx canvas.getContext(2d); drawCloud(ctx, 100, 80, 1);这里有个Canvas新手容易忽略的点同一路径内多个arc之间会有填充重叠如果你用的是半透明颜色重叠区域颜色会更深云朵会显得脏。解决办法有两个要么计算好各个圆的分布让重叠区域保持平滑要么把多个arc放进同一个beginPath里统一填充让它们合成一个整体路径而不是叠加透明度。我个人更习惯第二种方式视觉上更干净。你可以把这段代码运行起来再配合requestAnimationFrame做缓慢漂移动画一个简单的动态云彩效果就出来了。4.3 借助工具库解决跨浏览器兼容问题以前做前端时最头疼的问题就是同一个事件、同一个API在不同浏览器里行为不一样。后来社区把这些兼容性处理统一封装成了工具库流行的jQuery里大量代码就是干这个的。现在原生API已经很完善但遇到老版本浏览器或者需要统一处理特殊行为时工具函数库依然有存在价值。我的建议是不要把工具库当依赖而要把它当答案书。比如jQuery的$.ajax封装了XMLHttpRequest的各种细节你可以去看它的源码理解它如何做JSON解析、如何处理超时和错误状态。再看lodash的_.get函数它解决的就是嵌套对象取值时容易抛TypeError的问题。学会阅读这些工具库的实现远比自己瞎写Edge Case处理要靠谱。在跨浏览器兼容性上还有一条实用建议永远先查文档的可使用性表格。Can I Use这样的网站会把每个API支持哪些浏览器版本列得很清楚。你在写某个特性前花30秒查一下就能决定自己是直接用还是加polyfill还是换一个保守方案。大部分兼容性Bug不是写代码造成的而是没有在最开始确认支持范围。5. 运行时错误与性能问题排查5.1 几类高频运行时错误速查JavaScript运行时报错是每个开发者每天都可能遇到的事。我总结了几类最高频的错误连同它们的典型场景和排查思路整理成了一张速查表。错误类型常见场景排查思路SyntaxError括号不匹配、来自接口的JSON字符串不合法eval了一段乱数据先看报错行号再检查模板字符串和对象字面量重点排查动态拼接代码TypeError访问undefined的属性比如obj.name而obj是undefined顺着调用栈找到最初的数据来源用typeof或getType确认变量类型ReferenceError变量未定义、函数名拼写错误、在块级作用域外访问let变量在控制台输入变量名看是否返回undefined检查作用域链RangeError递归调用过深、数组长度负数、toFixed参数超出范围检查递归的出口条件留意循环里是否不断创建数据URIErrordecodeURIComponent遇到非法编码的字符串对来自URL的参数做异常捕获必要时加try...catch这里我想多提一句TypeError因为它是实际项目里的头号杀手。一个最常见的场景是接口返回的数据结构里有个嵌套对象后端在某个异常分支下没返回这个对象前端直接data.list.items去取页面瞬间白屏。解决这类问题一方面要在入口处做数据兜底另一方面要养成写防御性代码的习惯const items data?.list?.items ?? [];可选链和空值合并运算符是我近些年最推荐的语法特性它们能在不牺牲可读性的前提下把大量判空代码压缩成一行。不过要注意这两个语法在很老的浏览器里不原生支持需要借助构建工具做语法转换。5.2 用一段书签脚本快速检查页面状态我看到网上有一段流传很广的代码结构大致是这样的javascript:((){try{let xdocument.getelementsbyclassname(...); ...})()。这是一个典型的书签脚本浏览器收藏栏里保存一段以javascript:开头的代码点击后在当前页面执行。它的作用通常是快速操作页面元素比如高亮某种类型的节点、读取页面数据、模拟点击等。对开发者来说学会写这种小脚本能极大提升调试效率。我自己经常用书签脚本做的一件事是检查页面到底加载了多少个特定节点以及它们是否可见。核心方法依赖两个基础APIdocument.getElementsByClassName和document.querySelectorAll。前者返回的是HTMLCollection注意它是动态集合边遍历边操作DOM时会无限循环后者返回的是静态NodeList不会受后续DOM变更影响所以常规遍历我更推荐querySelectorAll。要提醒各位的是书签脚本的代码有一个环境限制.js文件可以放在博客或GitHub上但浏览器地址栏里的javascript:协议在某些现代浏览器中会被拦截尤其是涉及粘贴和网络请求时。所以调试性脚本主要用于本地开发生产环境排查时我更建议直接在DevTools的Console里执行代码片段。Console里还可以用$0表示当前选中的DOM元素这比书签脚本更灵活。5.3 高负载JavaScript的优化方向提到屏蔽高负载javascript很多人第一反应是屏蔽某些恼人的功能。但从开发者的视角来看这句话更应该理解为你的页面里可能存在大量占用CPU的JavaScript代码它们把主线程拖得很慢让页面卡顿、甚至无响应。高负载JavaScript通常来自三个方向高频触发的事件回调、过重的同步计算、以及无节制的DOM操作。高频触发最典型的场景是scroll事件和mousemove事件。我在做一个滚动动画时一开始直接在回调里计算元素位置并修改样式结果页面在低端手机上掉帧严重。后来加上了requestAnimationFrame限频把实际的计算和绘制频率降下来卡顿立刻缓解。因为事件回调可能每秒触发几十上百次而屏幕的刷新频率只有60帧中间大量计算是白做的。另一个高负载来源是同步执行大型循环或复杂的字符串拼接。遇到这种情况可以考虑把任务拆片用setTimeout分批次执行或者放到Web Worker里。Web Worker是浏览器提供的一个独立线程适合做数据计算、大列表排序这类CPU密集型任务但它不能直接操作DOM所以计算完成后仍然要回到主线程来更新页面。这个知识点在高性能项目里很加分。关于DOM操作核心原则是批量修改、减少回流。每次读写布局属性都会触发浏览器的重排和重绘代价很高。更好的方式是用DocumentFragment先把要插入的节点都准备好再一次性挂到DOM树上或者用字符串模板先拼好再统一写入容器。这些优化思路本质都是减少主线程的无效劳动。5.4 FullCalendar集成中的常见坑FullCalendar是一个功能非常强大的JavaScript日历库支持月、周、日等多种视图可以拖拽事件、加载远程数据。我在一个项目管理工具里用过它总体很顺手但有几个坑值得提前避开。第一坑是JSON日期格式解析。FullCalendar的event.date经常会收到字符串形式的ISO时间比如2025-05-01T10:00:00但如果你直接把它当作Date对象去做加减运算会因为不同浏览器对ISO字符串的解析差异而出现偏移。我的做法是进入组件前统一用moment或dayjs把时间字符串parse一遍再交给日历组件涉及自定义字段的排序也都在这个parse阶段完成不要让日历组件自己处理原始字符串。第二坑是事件的动态更新。FullCalendar在初始化时传入events数组后如果后续接口数据变了你可能会直接改数组再调用rerender。但更稳妥的方式是使用getEventSourceById找到对应事件源然后调用refetchEvents让日历重新拉取数据。这里涉及的知识点其实是数据和视图分离你修改的应该数据源而不是手动删掉DOM元素再重新添加后者很容易出现重复事件或残留事件。第三坑是资源化场景下的性能问题。当你一次性加载几百个事件并启用拖拽时FullCalendar在低端设备上会有明显延迟。我会把事件按时间范围拆分加载默认只看当前月切换视图再补发接口请求。这个做法对任何日历类组件都通用核心思想是把数据量控制在渲染能够轻松承受的范围内。做完这个优化之后切换月份基本没有卡顿感。日历库用起来不难难的是理解它的数据流和生命周期。我的体会是先用官方示例跑通一小段功能别急着接复杂的业务逻辑等你理解了一个事件从接口到日历视图的完整旅程再慢慢叠加拖拽、权限控制这些高级特性。这个循序渐进的过程其实也是学JavaScript本身的最优路径。