JavaScript 这门语言有个很有意思的特点:几乎所有人都在用,但真正系统读过 MDN 文档的人少之又少。大多数人是从某个视频教程或者项目实战里"摸"出来的语法,能跑就行,遇到边界情况再临时查。我自己早期也是这样,直到有次写一个视频旋转的小功能,一行document.querySelector('video').style.rotate = '-90deg'在 Chrome 里跑得好好的,到了另一个环境就出问题,排查半天才发现是对 DOM 属性和 CSS 属性的理解有偏差。从那以后我开始认真啃 MDN,才发现很多"坑"其实文档里写得清清楚楚,只是没人愿意花时间看。
这篇内容就是围绕 MDN 这个 JavaScript 最权威的参考文档来展开的。我会从"怎么用 MDN"这个角度切入,把 JavaScript 的核心知识体系——数据类型、运算符、对象、数组、循环、字符串、BOM、函数、剩余参数、动态调用、静态资源检测、跨语言调用等——串成一条线,同时把那些热搜词里高频出现的疑问点逐个拆开讲。不管你是刚入门的新手,还是写了几年代码但基础不牢的老手,都能从里面找到对自己有用的东西。MDN 不只是一本"查手册",它其实是一套完整的学习路径,关键看你会不会用。
1. 为什么 MDN 是 JavaScript 学习绕不开的一站
1.1 MDN 到底是什么,和普通教程有什么本质区别
MDN 全称 Mozilla Developer Network,现在叫 MDN Web Docs,是 Mozilla 基金会维护的 Web 技术文档平台。它覆盖的不只是 JavaScript,还有 HTML、CSS、Web API、HTTP、SVG 等等,但 JavaScript 部分无疑是其中最核心、最完整的一块。很多人第一次打开 MDN 会觉得"太干了",全是术语和表格,不如视频教程直观。但恰恰是这种"干",让它成为最可靠的信息源。
普通教程的问题在于,作者会根据自己的理解做取舍,把一些"看起来不重要"的细节省略掉。比如讲var和let的区别,很多教程只说"let 是块级作用域,var 是函数作用域",但不会告诉你变量提升(hoisting)在两者之间的具体差异,也不会告诉你暂时性死区(TDZ)到底在什么时刻生效。而 MDN 会把这些边界情况一条条列出来,甚至给出规范层面的解释。
我个人的经验是:教程用来入门,MDN 用来精进和查证。当你对某个 API 的行为不确定时,第一反应应该是去 MDN 查,而不是去搜索引擎里翻博客。因为博客可能过时,可能抄来抄去,而 MDN 的更新是跟着规范走的。
1.2 热搜词背后暴露的真实学习痛点
看一下那些热搜词,其实能看出很多人的学习状态。"javascript 运行时报错"说明大家卡在调试上;"javascript 检查静态资源是否加载完成"说明在做实际项目时遇到了异步加载的问题;"javascript 通过字符串调用函数"说明在写一些动态逻辑时不知道怎么处理;"oc和javascript互相调用"说明在做混合开发;"javascript:void(0)"这个老古董还在被搜,说明很多人对链接默认行为的处理还是一知半解。
这些搜索行为的共同点是:遇到具体问题才去查,缺乏体系化的知识框架。而 MDN 的价值恰恰在于,它把这些零散的知识点组织成了一个有层次的体系。你查一个querySelector,文档会告诉你它返回什么、什么时候返回 null、和getElementById有什么区别、性能上有什么考量。这些信息在你只搜"怎么旋转视频"的时候是拿不到的。
1.3 用 MDN 的正确姿势:从"查"到"读"
大部分人用 MDN 的方式是:遇到问题,搜索,找到对应页面,复制示例代码,关掉。这种方式能用,但浪费了 MDN 最大的价值。我的建议是分三层来用:
第一层是查 API,这是最基础的用法,遇到不认识的函数或属性就去查它的签名、参数、返回值、浏览器兼容性。
第二层是读指南,MDN 除了参考文档,还有大量 Guide 类型的文章,比如"JavaScript 指南""表达式和运算符""函数"这些,它们是按主题组织的,适合系统学习。
第三层是看规范链接,每个 API 页面底部通常会有指向 ECMAScript 规范或 WHATWG 规范的链接,虽然规范读起来很痛苦,但当你遇到"为什么这个行为是这样的"这种问题时,规范是唯一能给出确定答案的地方。
我自己的习惯是,每周抽时间精读一个 MDN 的 Guide 页面,不求快,求透。坚持几个月下来,对语言的理解会有质的提升。
2. 数据类型与运算符:MDN 里最容易被跳过的地基
2.1 七种原始类型和它们的"坑"
JavaScript 的数据类型分两大类:原始类型(Primitive)和对象类型(Object)。原始类型有七种:string、number、boolean、null、undefined、symbol、bigint。这个列表看起来简单,但每一个都有坑。
number是双精度浮点数,所以0.1 + 0.2 !== 0.3这个经典问题一直存在。MDN 在 Number 页面里明确说明了这一点,并给出了Number.EPSILON作为比较的容差方案。很多人写金额计算时直接用浮点数,结果对不上账,这就是没看文档的代价。
null和undefined的区别也是高频问题。简单说,undefined表示"变量声明了但没赋值"或者"访问了不存在的属性",是引擎自动给的;null表示"这里本该有个值,但现在是空的",是人为赋的。MDN 里有一句话我印象很深:null是"有意为之的空值",undefined是"意外的空值"。理解这句话,很多判断逻辑就清楚了。
symbol和bigint是后来加的,用得少但要知道它们存在。symbol主要用来做对象的唯一键,避免属性名冲突;bigint用来处理超过Number.MAX_SAFE_INTEGER的大整数。
2.2 类型转换的隐式规则
JavaScript 的类型转换是自动的,这既是它的灵活之处,也是 bug 的温床。'1' + 1得到'11','1' - 1得到0,这种不一致的行为让很多人抓狂。MDN 在"类型转换"页面里把转换规则整理成了表格,包括ToPrimitive、ToNumber、ToString这些抽象操作的触发条件。
我建议每个写 JavaScript 的人都把那张表看一遍。比如[] + {}得到'[object Object]',{} + []得到0(在某些解析环境下),这些反直觉的结果背后都是明确的规则。理解了规则,你就能预测结果,而不是靠试。
实际开发中,我的原则是显式转换优先。需要字符串就用String(x)或模板字符串,需要数字就用Number(x)或parseInt(x, 10),不要依赖隐式转换。这样代码可读性高,也不容易出意外。
2.3 运算符的优先级和短路求值
运算符这块,MDN 的"运算符优先级"表格是必须收藏的。很多人写复杂表达式时靠猜,结果a || b && c和(a || b) && c结果不一样。记住&&优先级高于||,??(空值合并)和||的行为差异也要清楚:||会把0、''、false都当成假值,而??只在null和undefined时取右边。
短路求值是个很实用的特性。a && a.b && a.b.c这种写法在可选链?.出现之前是标准做法。现在有了a?.b?.c,更简洁,但要注意?.不能用于赋值左侧。MDN 在可选链页面里明确说了这一点,很多人踩过这个坑。
3. 对象、数组与字符串:日常编码的三大件
3.1 对象:从字面量到原型链
对象是 JavaScript 的核心。MDN 的 Object 页面内容极其丰富,从创建对象的各种方式(字面量、构造函数、Object.create、类)到静态方法(Object.keys、Object.assign、Object.freeze、Object.entries)都有详细说明。
我重点说几个容易被忽略的点。Object.freeze是浅冻结,嵌套对象还是可以改的,要深冻结得递归处理。Object.assign是浅拷贝,而且会触发 setter,如果目标对象有 setter 会有意外行为。Object.create(null)创建的对象没有原型,适合做纯粹的字典,避免原型链上的属性干扰。
原型链这块,MDN 的"对象原型"页面讲得很清楚。__proto__是访问器属性,Object.getPrototypeOf是标准方法,prototype是函数才有的属性。这三个概念经常被混淆,建议对着文档把例子敲一遍。
3.2 数组:那些你以为会用其实没用对的方法
数组方法太多了,map、filter、reduce、forEach、find、some、every、flat、flatMap……MDN 的 Array 页面把它们按功能分类,还标注了是否改变原数组。这个标注非常重要,push、pop、splice、sort、reverse会改变原数组,map、filter、slice、concat不会。
reduce是最容易被误用的。很多人用它做累加,但其实它能做更复杂的事,比如按条件分组、扁平化、去重。MDN 的示例里有一个用reduce实现map和filter组合的例子,看完会对它的能力有新的认识。
sort的坑也值得一提。默认排序是按字符串比较的,[10, 1, 2].sort()得到[1, 10, 2],不是[1, 2, 10]。要按数字排序必须传比较函数(a, b) => a - b。这个坑每年都有新人踩。
3.3 字符串:不可变但有丰富方法
字符串是不可变的,所有"修改"字符串的方法都是返回新字符串。MDN 的 String 页面列出了所有方法,常用的有slice、substring、substr(已废弃)、split、replace、replaceAll、includes、startsWith、endsWith、padStart、padEnd、trim、trimStart、trimEnd。
replace和replaceAll的区别要注意:replace只替换第一个匹配,除非用正则加g标志;replaceAll替换所有匹配,但要求参数是字符串或带g标志的正则。MDN 里写得很清楚,但很多人还是习惯性用replace然后发现只换了一个。
模板字符串是 ES6 引入的,用反引号包裹,支持插值和多行。MDN 的"模板字符串"页面还介绍了标签模板(tagged template),这是一个高级但很有用的特性,可以用来做 SQL 拼接的安全处理、国际化等。
4. 函数、剩余参数与动态调用:进阶必备
4.1 函数的多种形态
JavaScript 里函数是一等公民,可以赋值给变量、作为参数传递、作为返回值。MDN 的 Function 页面涵盖了函数声明、函数表达式、箭头函数、立即执行函数表达式(IIFE)、生成器函数、异步函数等多种形态。
箭头函数和普通函数的最大区别是this的绑定。箭头函数没有自己的this,它捕获定义时所在作用域的this。这个特性在回调里很有用,但在对象方法里就要小心。MDN 的箭头函数页面专门有一节讲"this 的绑定",建议反复看。
函数的length属性返回形参个数,name属性返回函数名,arguments对象在非严格模式下可用但已不推荐。剩余参数...args是更好的替代方案,它返回真正的数组,而arguments是类数组对象。
4.2 剩余参数与展开运算符
剩余参数和展开运算符用的是同一个语法...,但场景不同。剩余参数用在函数定义里收集参数,展开运算符用在调用或数组/对象字面量里展开。
// 剩余参数 function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } // 展开运算符 const arr = [1, 2, 3]; const arr2 = [...arr, 4, 5]; const obj = { a: 1 }; const obj2 = { ...obj, b: 2 };MDN 的"剩余参数"和"展开语法"是两个独立页面,内容有交叉但侧重点不同。剩余参数页面会讲它和arguments的对比,展开语法页面会讲它在函数调用、数组构造、对象构造中的用法。对象展开是 ES2018 才加的,之前只能用Object.assign。
一个容易忽略的点:剩余参数必须是最后一个参数,后面不能再有参数。展开运算符在对象里做的是浅拷贝,嵌套对象还是引用共享。
4.3 通过字符串调用函数
"javascript 通过字符串调用函数"是个高频搜索词,说明很多人有动态调用的需求。常见场景是根据配置或用户输入决定调用哪个函数。
最直接的方式是用window或globalThis上的属性访问:
function foo() { console.log('foo'); } function bar() { console.log('bar'); } const name = 'foo'; globalThis[name](); // 输出 foo但这种方式有安全风险,如果字符串来自用户输入,可能调用到不该调用的函数。更安全的做法是用一个映射表:
const handlers = { foo: () => console.log('foo'), bar: () => console.log('bar') }; const name = 'foo'; if (Object.prototype.hasOwnProperty.call(handlers, name)) { handlers[name](); }MDN 在"属性访问器"页面里讲了点号和方括号两种访问方式的区别,方括号可以用变量作为属性名,这是动态调用的基础。另外eval也能做到,但 MDN 明确不推荐使用eval,因为它有性能和安全问题。
5. 循环、条件与流程控制:写对逻辑的前提
5.1 循环语句的选择
JavaScript 的循环有for、for...in、for...of、while、do...while,还有数组方法forEach、map等。MDN 的"循环与迭代"页面把它们放在一起对比,很直观。
for...in遍历的是可枚举属性,包括原型链上的,所以遍历数组时可能拿到非索引属性,不推荐用于数组。for...of遍历的是可迭代对象的值,数组、字符串、Map、Set 都可以,是遍历数组的推荐方式。
forEach不能break或continue,也不能return出整个函数,只能return出当前回调。如果需要提前退出,用for...of或者some/every。
性能上,现代引擎对for循环优化得很好,forEach也不差,选择主要看语义清晰度。我个人的习惯是:需要索引和提前退出用for,只遍历值用for...of,需要链式操作(map/filter/reduce)用数组方法。
5.2 条件语句与真值假值
if...else、switch、三元运算符、??、?.这些条件相关的语法,MDN 都有专门页面。重点是理解"真值"和"假值"的概念。
假值只有六个:false、0、-0、0n、''、null、undefined、NaN。注意0n是 BigInt 的零,-0和0在真值判断上一样。除了这六个,其他都是真值,包括空数组[]、空对象{}、字符串'0'、字符串'false'。
这个知识点看起来简单,但实际写代码时经常出错。比如判断一个数组是否为空,不能写if (arr),因为空数组也是真值,要写if (arr.length === 0)。
switch用的是严格相等===,所以switch(1)不会匹配case '1'。每个case后面要记得break,否则会穿透。MDN 的 switch 页面有详细说明。
5.3 异常处理与运行时报错
"javascript 运行时报错"是高频搜索词,说明调试是大家的痛点。MDN 的"错误"页面列出了 JavaScript 的所有内置错误类型:Error、SyntaxError、TypeError、ReferenceError、RangeError、URIError、EvalError、AggregateError。
TypeError最常见,通常是"访问了 undefined 或 null 的属性"或者"调用了不是函数的东西"。ReferenceError是"使用了未声明的变量"。SyntaxError是语法错误,通常在解析阶段就抛出。
try...catch...finally是标准的异常处理结构。catch捕获异常,finally无论是否异常都会执行。MDN 里还讲了throw语句和自定义错误类,通过继承Error可以创建自己的错误类型,方便区分处理。
调试时,我建议先看错误类型和错误信息,再去 MDN 搜对应的错误页面,通常能找到原因和解决方案。比盲目搜索高效得多。
6. BOM 与 DOM:浏览器环境下的 JavaScript
6.1 BOM 是什么,和 DOM 什么关系
BOM(Browser Object Model)是浏览器对象模型,DOM(Document Object Model)是文档对象模型。BOM 的核心是window对象,它代表浏览器窗口,同时也是全局对象。DOM 的核心是document对象,它是window的一个属性,代表当前页面的文档。
MDN 的"Window"页面和"Document"页面是两块独立的内容。window上有location、navigator、history、screen、localStorage、sessionStorage、setTimeout、setInterval、fetch、alert、confirm、prompt等。document上有querySelector、querySelectorAll、getElementById、getElementsByClassName、createElement、appendChild等。
热搜词里的document.querySelector('video').style.rotate = '-90deg'就是典型的 DOM 操作。querySelector返回第一个匹配的元素,没有匹配返回null。所以直接链式调用.style有风险,如果页面上没有 video 元素就会报TypeError。稳妥的写法是先判断:
const video = document.querySelector('video'); if (video) { video.style.rotate = '-90deg'; }style.rotate是 CSS 的rotate属性,现代浏览器支持。如果要兼容老浏览器,得用transform: rotate(-90deg)。MDN 的 CSSrotate页面有兼容性表格,可以查。
6.2 检查静态资源是否加载完成
"javascript 检查静态资源是否加载完成"这个需求通常出现在需要等图片、脚本、样式加载完再执行某些逻辑的场景。
对于单个图片,可以用img.complete属性或者onload/onerror事件:
const img = new Image(); img.onload = () => console.log('加载完成'); img.onerror = () => console.log('加载失败'); img.src = 'path/to/image.jpg'; // 或者检查已存在的图片 if (img.complete) { console.log('已加载'); } else { img.onload = () => console.log('加载完成'); }对于整个页面的资源,可以用window.onload事件,它会在所有资源(包括图片、样式、脚本)加载完后触发。而DOMContentLoaded只在 DOM 解析完就触发,不等资源。MDN 的"Document: DOMContentLoaded 事件"和"Window: load 事件"页面讲得很清楚。
如果要动态检测多个资源,可以用Promise.all包装:
function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } Promise.all([loadImage('a.jpg'), loadImage('b.jpg')]) .then(() => console.log('全部加载完成')) .catch(() => console.log('有加载失败的'));6.3 javascript:void(0) 的来龙去脉
javascript:void(0)这个写法在老代码里很常见,用在<a href="javascript:void(0)">里,目的是让链接不跳转。void运算符会计算表达式并返回undefined,所以void(0)就是undefined,链接就不会跳转。
但现在这种写法已经不推荐了。更好的做法是用<button>代替<a>,或者用href="#"配合event.preventDefault()。MDN 的"void 运算符"页面有说明,void的主要用途就是阻止链接跳转,但这个需求本身可以用更语义化的方式解决。
如果非要用<a>,可以这样:
<a href="#" onclick="event.preventDefault(); doSomething();">点击</a>或者用role="button"加上键盘事件处理,保证可访问性。
7. 跨语言调用与扩展:JavaScript 的边界
7.1 OC 和 JavaScript 互相调用
"oc和javascript互相调用"这个搜索词说明有人在 iOS 开发中需要在 Objective-C 和 JavaScript 之间通信。这通常发生在UIWebView或WKWebView里加载网页,然后原生代码和网页脚本需要交换数据。
在WKWebView里,JavaScript 调用原生可以用window.webkit.messageHandlers.xxx.postMessage(data),原生端通过WKScriptMessageHandler接收。原生调用 JavaScript 用evaluateJavaScript(_:completionHandler:)。
MDN 本身不涉及 iOS 原生开发,但 JavaScript 侧的postMessage和addEventListener('message')是标准的 Web API,MDN 有详细文档。跨语言调用的核心是序列化,通常用 JSON 字符串传递数据,因为两边都能解析。
需要注意的是,postMessage有安全考量,要验证消息来源,避免接收不可信的数据。MDN 的"Window.postMessage"页面有安全建议。
7.2 JavaScript 扩展插件
"javascript扩展插件"可能指浏览器扩展,也可能指编辑器插件。浏览器扩展方面,Chrome 扩展用manifest.json配置,背景脚本、内容脚本、弹出页面都可以用 JavaScript 写。MDN 有"浏览器扩展"的专题,虽然主要面向 Firefox,但很多概念是通用的。
内容脚本(content script)运行在网页上下文里,可以操作 DOM,但和页面脚本的变量是隔离的。要通过chrome.runtime.sendMessage或window.postMessage通信。背景脚本(background script)运行在扩展的上下文里,可以访问扩展的 API。
写扩展时要注意权限声明,manifest.json里的permissions字段决定了扩展能做什么。权限要最小化,避免申请不必要的权限,否则审核可能不通过,用户也会警惕。
7.3 WebGPU 与 3D 高斯泼溅
热搜词里出现了 "splat.js 纯 javascript + webgpu 的 3d 高斯泼溅处理方案",这是一个比较前沿的方向。3D 高斯泼溅(Gaussian Splatting)是一种三维重建和渲染技术,用大量高斯分布的点来表示场景,渲染速度快,效果逼真。传统上这类计算用 CUDA 或 WebGL,现在 WebGPU 提供了更底层的 GPU 访问能力,JavaScript 也能做高性能计算了。
MDN 有 WebGPU API 的文档,虽然还比较新,但基本概念和用法都有。WebGPU 的核心是navigator.gpu.requestAdapter()获取适配器,然后adapter.requestDevice()获取设备,再创建缓冲区、着色器、管线等。和 WebGL 相比,WebGPU 更接近现代图形 API(如 Vulkan、Metal),性能更好,但学习曲线也更陡。
如果你对这块感兴趣,建议先看 MDN 的 WebGPU 基础教程,再去看 splat.js 的源码,理解它怎么把高斯泼溅的算法映射到 WebGPU 的管线上。
8. 把 MDN 用成自己的知识库
8.1 建立自己的查阅习惯
MDN 内容太多,不可能全部记住。关键是建立查阅习惯。我的做法是:遇到任何不确定的 API,先查 MDN,看完再写代码。查的时候重点看四块:语法签名、参数说明、返回值、浏览器兼容性。这四块看完,基本就不会用错了。
浏览器兼容性表格特别重要。有些 API 在旧浏览器上不支持,MDN 会标注支持的最低版本。如果项目需要兼容旧环境,就得准备降级方案或者 polyfill。MDN 通常会推荐对应的 polyfill 链接。
8.2 用 MDN 的示例代码验证理解
MDN 每个 API 页面都有"示例"部分,这些示例是可以在浏览器控制台直接跑的。我建议不要只看,要动手改。改参数、改输入、看输出变化,这样能加深理解。比如看Array.prototype.reduce的示例时,把初始值去掉,看看会发生什么;把回调的返回值改掉,看看结果怎么变。
这种"折腾式学习"比被动阅读有效得多。MDN 的示例通常很简洁,正好适合做实验。
8.3 关注 MDN 的更新和规范动态
MDN 会跟着规范更新。新的 JavaScript 特性(比如Array.prototype.at、Object.hasOwn、structuredClone)在规范确定后不久就会出现在 MDN 上。关注 MDN 的更新,能让你及时了解语言的新能力。
我个人的习惯是每隔一段时间看看 MDN 的"JavaScript 参考"首页,看看有没有新增的页面。另外,MDN 的 GitHub 仓库是开源的,可以关注它的 issue 和 PR,了解文档的变动。
8.4 从 MDN 出发构建知识网络
MDN 的页面之间有大量交叉链接。查Array.prototype.map时,会看到它链接到"迭代器""生成器""函数式编程"等相关页面。顺着链接读下去,就能把零散的知识点连成网。
我建议在查一个 API 时,至少点开两三个相关链接看看。比如查Promise时,点开async/await、then、catch、finally、Promise.all、Promise.race这些页面,把异步编程的整个体系过一遍。这样查一次,收获的是一整块知识,而不是一个点。
MDN 的搜索功能也很好用,支持按 API 名、按概念、按错误信息搜索。遇到报错时,直接把错误信息粘进去搜,通常能找到对应的页面。
说到底,MDN 不是一本需要从头读到尾的书,而是一个可以反复查阅、逐步深入的工具。用得越多,越能体会到它的价值。我自己的经验是,每次认真读一个 MDN 页面,都能发现之前不知道的细节。这些细节积累起来,就是和别人拉开差距的地方。