
上周在 Code Review 时看到同事写的一行代码const { default } this.props。他想从一个配置对象里取出默认配置于是直接在解构赋值里用了default做变量名。编辑器没标红但项目一跑起来控制台直接砸下一行SyntaxError: Unexpected token default。他挺委屈我查过default确实是保留关键字这不是已经“避开”了吗这句话暴露了一个常见误区很多人对 JavaScript 保留关键字的理解停留在“背单词表”的阶段知道有这回事但不知道它在变量声明、对象解构、类成员、严格模式、模块作用域这些真实场景里分别怎么生效。今天这篇就围绕 JavaScript 保留关键字彻底聊透从规范分类、真实报错现场到严格模式的隐形雷区再到工具链自查和面试延伸一次说清楚。全程会用我踩过的坑和修过的 bug 做案例保证看完能直接用上。1. 先分清四类“禁词”关键词、未来保留字、字面量、上下文敏感词保留关键字这个问题第一层认知是JavaScript 里的“禁词”不是一个单一名单而是好几类。很多人只知道if、for、return不能当变量名但没搞懂true、null、undefined、await、static这些名字的规则并不一样。先把分类理清后面所有报错就都能看懂。1.1 当前规范里的完整关键词列表ECMAScript 规范里有一个概念叫ReservedWord由Keyword和FutureReservedWord组成。其中Keyword就是当前版本语法里真正被占用的词出现的位置已经被语法固定你不能拿它做标识符比如变量名、函数名、参数名。完整的当前关键字列表如下// ECMAScript 当前规范中的完整关键字按字母序 break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof new return super switch this throw try typeof var void while with yield这份列表里的词绝大多数在写业务代码时不会有人拿来做变量名真正容易翻车的是default、delete、class、new、in、typeof这几个原因很简单它们和日常英语词汇高度重合比如接口返回一个default字段、算法里写一个new节点、配置对象里要取一个delete标记下意识就成了变量名。还有一对容易混淆的undefined和NaN、Infinity。它们不是保留关键字而是全局对象上的属性。这意味着理论上你可以写出var undefined 1这种代码老环境的经典坑严格模式赋值会抛TypeError而var null 1这种写都不用写解析器直接炸。1.2 严格模式和模块环境里额外禁用的名字除了上述核心关键词还有一批词在严格模式下才被禁止作为标识符使用。ES5 时代它们叫“未来保留字”意思是给未来语法预留的位置现在虽然没有语法意义但标准不允许你拿来命名。这批词包括// 严格模式下禁用作标识符ES5 FutureReservedWord implements interface let package private protected public static yield注意let和yield现在已经是真正有关键词语义的符号了但在不同上下文里的限制还有细微差别。let在非严格模式下可以写var let 1老代码里有这种写法但严格模式下直接禁止yield在生成器函数内部以及严格模式下都不能作为变量名。也就是说即使你打开了.js文件发现let package 1能跑一旦切到use strict或者被构建工具按 ES Module 处理立刻变SyntaxError。除了这些ES Module 顶层环境还会额外禁用await作为标识符使用。另外规范里有一批从 Java 风格继承过来的历史保留词abstract、boolean、byte、char、double、final、float、goto、int、long、native、short、synchronized、throws、transient、volatile。它们在 ES3 时代是正儿八经的保留字后来被移出了保留列表但为了兼容老引擎和旧代码不要用它们做标识符依然是最稳妥的选择。1.3 上下文敏感词async、await、static、get、set 不能一概而论真正让“保留字背也背不完”的是第三类上下文敏感词。它们的报错与否取决于出现在什么语法位置同一个词在这里合法、在那里就爆。async是一个典型。作为函数声明的前缀async function foo() {}合法但如果你写var async 1在现代浏览器里它居然能跑因为async不是全局保留字只有出现在特定语法上下文比如作为异步函数标志时才有特殊含义。同理get、set在对象字面量里是访问器属性的修饰符但obj.get 1也完全合法。static在类声明里是静态成员修饰符但在普通对象里它就是一个普通属性名。of在for...of循环里是关键字平时写const of 1却可以。所以我对团队的建议一直是不要试图把每个单词的合法位置都记住而是记住一条铁律——凡是规范里定义为Keyword的词任何位置都不要用来声明标识符凡是只在特定上下文生效的词命名时默认也避开因为它今天合法不代表三年后合法。这条规则能帮你躲掉绝大多数智障报错。2. 翻车现场变量、函数、类成员和解构里的真实报错理论部分过完了下面进入真正有价值的模块这些保留字在实际代码里是怎么报错的报错长什么样以及怎么绕过。2.1 变量声明踩过的三种死法变量声明是保留字最密集的雷区。我见过三种典型死法报错方式和原因完全不同。第一种直接声明使用核心关键字。比如const default 1; // SyntaxError: Unexpected token default let delete 2; // SyntaxError: Unexpected token delete var class 3; // 老环境直接炸新环境部分引擎能跑但绝对不该这么写这种最直观解析器一看就知道你这个标识符是关键词直接拒绝。注意const和let声明比var更严格很多词在var宽松模式下能通过换成const立刻炸。第二种let自己和自己撞车let let 1; // SyntaxError: let is disallowed as a lexically bound name这行代码在 Chrome 里会报错因为let本身是词法声明的引导词不能再作为被声明的名称出现。有些老教程里会写var let 1在新规范的非严格模式下其实是能用但没有任何理由把自己置于这种恶心境地。第三种在解构声明的绑定位置使用保留字。这个我在开头已经举例了const { default } obj; // SyntaxError const { delete } obj; // SyntaxError解构赋值的左侧是“绑定标识符”不是“属性名”所以它遵守变量命名规则。这和第 3 章要讲的“对象属性名例外”正好是相反逻辑很多人就是在这里栽跟头。2.2 函数名、参数名的历史包袱函数声明里函数名和参数名同样受保留字约束function new() {} // SyntaxError: Unexpected token new function f(delete) {} // SyntaxError: Unexpected token delete参数名踩坑的概率其实比函数名更高。因为业务代码里常常想写function process(default) {}表示“处理默认值”这种想法几乎必炸。正确做法是改成defaultValue或dft。这里还有个历史包袱在 ES5 严格模式下eval和arguments也被限制为不能做绑定名称不能赋值甚至不能作为函数参数名。所以老代码里如果有function foo(eval) {}这种写法在加上use strict后会直接报SyntaxError: Unexpected eval or arguments in strict mode。这不算传统意义的保留关键字但实际效果就是“换一个环境就多了一批禁词”迁移老代码的时候容易在这里栽个措手不及。2.3 类成员里最隐蔽的坑static 和字段声明类语法是保留字问题的新型事故高发区。先说结论类的方法名允许使用保留字因为方法名走的是属性名解析规则不是标识符解析规则。所以这些代码都是合法的class A { default() {} delete() {} class() {} }但类字段语法Class Fields和static修饰符混在一起就有坑了。最典型的是这个class A { static 1; // SyntaxError: Unexpected token }你可能会想这明明是把static当作字段名赋值啊为什么报错因为解析器认为static后面必须跟一个静态成员的方法名或字段名结果它看到直接懵了。这本质上就是static在类上下文里是修饰符而不是可被声明的名称。解决办法是不用static做字段名或者换一个词。类似的历史 bug 还有class A { static static() {} }这类写法在部分 V8 版本上会解析异常。虽然规范理论上允许方法名和修饰符同名但不同引擎对待这种边界情况的态度并不一致我建议团队直接约定类成员命名不做这种挑战解析器的行为。还有一个小细节class A { get() {} }是合法的它定义了一个名字叫get的实例方法但如果你写class A { get() {} }后又想同时写访问器get foo() {}这两者不会冲突因为一个是普通方法名一个是属性访问器的修饰符。这种上下文敏感性就是上一节说的“上下文敏感词”的典型体现。理解了这一点你看到报错时就不会慌。2.4 解构赋值时的一行代码报错解构赋值是保留字出镜率极高的场景尤其是前后端对接时拿接口数据。最常见的需求是接口返回一个对象{ default: true, name: x }你想把default字段取出来。第一次写的人大概率会这样const { default } res; // SyntaxError: Unexpected token default为什么对象字面量里{ default: true }合法但解构到变量名时不合法因为解构赋值左侧{ default }里的default不是属性名而是“变量声明名”它必须遵守标识符规则。正确姿势是把变量重命名const { default: defaultFlag } res; // 把 res.default 取出命名为 defaultFlag console.log(defaultFlag);同理缩写的对象字面量{ default }同样报错因为简写属性会生成一个标识符引用。函数参数解构也有同样问题function f({ default: d }) { // 可以属性名 default绑定名 d return d; } function f({ default }) { // SyntaxError return default; }我建议团队约定一个通用写法凡是解构接口字段里有保留字命名的一律用“属性名: 别名”的语法显式改名既是规避语法错误也让代码后续可读性更强——default这个词在业务代码里太模糊了即使没有语法问题也应该给它一个更有业务含义的名字。3. 对象属性名为什么是例外obj.default 合法var default 却报错很多人第一次遇到obj.default能跑而var default炸裂时会觉得 JavaScript 的规则自相矛盾。其实完全不矛盾这背后是规范里两个不同的语法类别在起作用。3.1 属性名走的是 IdentifierName不是 Identifier规范对于一个名字出现的位置有严格区分。在“标识符”位置变量名、函数名、参数名、绑定名它必须满足Identifier规则而Identifier不能在保留字集合里。但在“属性名”位置对象字面量的键名、成员访问符后的名称、类的方法名它只需要满足IdentifierName规则IdentifierName允许任何关键词。所以下面这些代码在现代浏览器里全部合法const obj { default: 1, delete: 2, class: 3, if: 4 }; obj.default; // 1 obj[delete]; // 2这也解释了一个很多人没想明白的问题为什么for (const key in obj)遍历出来的key如果是default脚本也不会报错因为key是字符串不是标识符。保留字的冲突只发生在“你需要一个变量来装这个名字”的时候而不发生在“这个名字作为属性键存在”的时候。给一个更容易理解的生活类比你可以在班级花名册里把某个学生起名叫“冬”也可以喊他“冬天”但你不能把自己的身份证名称字段写成“冬天”——花名册的类别允许特殊字符和任意文字身份证的类别则要求符合固定命名规范。属性名就是花名册变量名就是身份证。3.2 JSON 和第三方 API 字段的现实处理理解了属性名例外规则后对接第三方 API 的很多困惑会立刻消失。服务端返回的 JSON 数据经常出现default、class、delete、status当然status不是保留字只是举例这样的字段这是完全合法的因为 JSON 的键本质是字符串。推荐处理方式是逐层解构并重命名而不是直接点号访问// 假设接口返回 const res { default: true, delete: /api/delete, class: primary }; // 解构时重命名避免后续代码里到处 obj.default const { default: isDefault, delete: deleteUrl, class: className } res;这里尤其需要提醒一点res.default在普通函数里能跑但如果你的脚本是严格模式或 ES Module也不能用res.default去声明一个新变量。换句话说属性访问没问题把属性值拿出来赋给一个叫default的变量才是问题所在。每次报错时先判断一下错误是出在取属性名还是在声明变量名排查方向完全不同。3.3 老 IE 时代的历史包袱与方括号习惯为什么很多老代码里访问对象字段一律用中括号obj[class]而不是点号obj.class这不是风格偏好而是历史兼容需求。在 IE 8 及更早的 JScript 引擎里保留字作为属性名同样不被支持obj.class会直接抛语法错误。当时 JavaScript 社区的标准规避方式是统一用方括号访问// 老 IE 兼容写法 var value obj[class]; var value2 obj[default];这个习惯一直延续到今天。如果你在维护老项目的代码看到满屏中括号不要觉得奇怪那是工程师用血泪换来的兼容经验。而现代浏览器和 Node.js 里obj.class完全没问题TypeScript 的类型定义里写interface Foo { class: string }也是合法的。我在日常开发中会优先使用点号访问只有遇到字段名确实和关键字撞了、或者需要对接老环境时才用方括号这样代码更简洁也符合现代规范。4. 严格模式和 ES Module两块隐形的“雷区”很多代码在开发本地跑得好好的一到 CI、打包或者 SSR 环境就报SyntaxError十有八九是触碰了这两块隐形的雷区严格模式和模块作用域。因为保留字的判定规则在这两种环境下会变得更严格。4.1 use strict 到底额外禁了哪些字在普通脚本里以下代码是可以被非严格模式容忍的var implements 1; var private 2; var public 3; var interface 4; var static 5;如果把它改成下面这样就会原地爆炸use strict; var implements 1; // SyntaxError: Unexpected strict mode reserved word var private 2; // 同样报错 var public 3; // 报错原因就是我第 1 节提到的严格模式额外保留词。严格模式把implements、interface、package、private、protected、public、static、yield、let这 9 个词划进了禁用于标识符的范围和 ES6 的FutureReservedWord表完全对齐。除此之外严格模式还对eval和arguments做了限制不能作为变量名、参数名不能赋值不能作为函数名。它还禁用了with语句。很多人以为with是浏览器兼容性问题才被禁的其实严格模式下直接就是语法错误use strict; with (obj) { // SyntaxError: Strict mode code may not include a with statement console.log(a); }所以当你决定在项目里全面启用use strict时不要以为只是“规范一点”它意味着整个代码库的标识符命名空间都收紧了一圈。老项目迁移时请先全局搜索\b(let|yield|package|implements|private|public|static)\b这些词把老代码里的裸名字改掉再开严格模式。4.2 async/await 与模块顶层的作用域限制ES Module 的默认行为是严格模式而且模块顶层会自动把await保留。这意味着一个.js文件一旦被import引入立即变成严格模式环境。最经典的坑是这样的// module.js var await 1; // SyntaxError: Unexpected reserved word let package 2; // SyntaxError: Unexpected strict mode reserved word这些报错在没有use strict声明的情况下也会出现因为模块本身就是严格模式。很多人在 Node.js 里升级 package.json 的type: module之后突然发现一批变量声明报错就是撞上了这堵墙。async function内部对await的保护也类似async function load() { var await 1; // SyntaxError: Unexpected reserved word }注意这里有个很别扭的细节在非 async 的普通函数里var await 1在部分环境中是合法的。所以同一个文件名、同一行代码把它改成async function后立刻炸这种“看起来一样的代码换个上下文就报错”的情况最容易让人困惑。解决办法还是那句话上下文敏感词一律不用来命名省心。4.3 老代码迁移时的兼容判断老代码迁到新环境的兼容问题我实际处理过很多次。最典型的是远古代码// 这段代码在 ES5 时代是合法的 var let 10; var static abc; var package x;在浏览器里跑得好好的一旦进入构建工具比如 Vite、Webpack就报错因为构建工具预设的代码处理环境要么是 ES Module 严格模式要么是在严格模式下解析。排查流程我总结成三步先看报错类型。SyntaxError: Unexpected strict mode reserved word说明问题明确指向严格模式保留词直接全局搜索对应单词。再看文件头。有没有use strict、export、import关键字只要有import/export基本就是模块环境一切都按严格模式来。最后看目标运行环境。Kettle 这类工具里跑 JavaScript 的老引擎可能还是 ES5 的保留字集合和 Node 20 的判断结果不一样要以实际执行引擎为准。这里我再补充一个实际案例有次我把一段老脚本写进某定时任务里那段代码里用了var yield 1。在我的 Chrome 控制台测试没问题进了任务执行器就报SyntaxError排查了一下午才发现执行器内部给整个文件包了一层生成器上下文yield在那个位置成了生成器关键字。所以说不用上下文敏感词做标识符是跨环境兼容的发动机。5. 自查、规避和快捷键工具链里怎么提前拦住当项目里保留字报错出现的频率超过一次两次就该把检查机制前置了。靠人肉眼背列表不现实下面几个工具和方法是我自己一直在用的配合起来基本能做到“写的时候就拦住”。5.1 ESLint 规则与 IDE 内置报错第一个防线是 IDE 的语义检查和 ESLint。VS Code 里 JavaScript 文件启用typescript.validate或者安装 ESLint 插件后非法标识符一般会直接标红。但 ESLint 也有覆盖不到的地方比如像undefined、NaN、Infinity这种全局属性你在非严格模式下声明同名变量编辑器并不会标红只有运行时才会有诡异行为。这就要用到 ESLint 规则no-shadow-restricted-names它专门禁止把受限的名字作为变量名、参数名或函数名。配置非常简单{ rules: { no-shadow-restricted-names: error, no-restricted-syntax: [ error, { selector: WithStatement, message: with 语句已禁用请改用解构赋值。 } ] } }no-restricted-syntax里的选择器很灵活除了禁with也可以用来禁eval、new Function这类常见问题。如果你的团队想对保留字使用做更精细的控制还可以在.eslintrc.js里用自定义规则检查标识符命名。不过以我的经验ESLint 的no-shadow-restricted-names加 IDE 高亮已经能拦住 90% 的坑。5.2 Node 侧语法检查与构建工具的改名策略有些时候问题不发生在开发环境而是发生在脚本执行或构建流水线里。这时候可以用 Node 的语法检查命令快速定位不用打开项目node --check app.js它能检测出一个文件的语法错误包括保留字导致的SyntaxError而且不会真正执行代码非常安全。注意node --check对 ES Module 文件的行为会受.mjs后缀或 package.json 的type字段影响检查前先确认文件按什么模块类型解析。构建工具那一层压缩器的变量名重写机制也值得一提。你用 Terser、UglifyJS、esbuild 压缩代码时它们会把变量重命名为t、e、n这类短名重命名过程会自动避开保留字因为它们从合法标识符字符集里挑选名字根本不会撞上if、for。如果你见过压缩产物里出现class这类词那大概率不是变量名而是对象属性名压缩工具默认不重写属性名除非开启mangle.properties之类的配置。5.3 命名策略避开保留字的三条实用原则工具能拦住一步但最好的方案是根本不要写出这些代码。我在团队里推行过几条简单粗暴的原则第一核心关键字后缀化。拿default举例所有表示“默认配置”的变量统一叫defaultConfig、defaultVal或dftFlag而不是default。delete字段统一叫deleteUrl、deleteAction。给关键字套一个业务后缀既避开了语法问题也让变量含义更清楚——老实说一个叫default的变量在代码评审里根本不知道是干嘛的。第二解构时显式改名。凡是接口数据里出现的保留字字段解构赋值时一律用{ default: defaultFlag }的别名语法建立一条铁律“解构对象只产出合法标识符”。第三建立团队禁用词清单。我把这类词和维护规范一起写进 README包括全部核心关键字、严格模式保留词、历史 Java 风格保留字。命名评审时直接对照清单查比靠记忆靠谱。我自己还养成了一个习惯写代码时如果心里对某个词“到底能不能用”有一丝犹豫就直接换一个表达。因为纠结本身说明这个词在边缘地带边缘地带的危险不值得用几分钟琢磨时间交换。6. 从保留字延伸到面试和日常排错的三件事聊完了原理和工具最后讲讲和保留字相关、实际工作中经常被问到的三件事。这三件事在网上热度一直很高也和很多前端面试题、日常报错定位直接相关。6.1 typeof、instanceof、undefined 为什么不算保留字热搜词里“javascript判断数据类型”常年占据高位而谈判断数据类型就绕不开typeof、instanceof、undefined这三兄弟。很多人会误以为它们都是“函数”或“关键字”实际上区分它们对理解保留字很有帮助。typeof和instanceof在规范里确实是运算符也是关键字。但注意它们不是函数typeof(x)这种写法其实是把(x)当成了一个表达式与typeof x没有区别。undefined、NaN、Infinity则完全不是关键字它们是全局对象上的属性。这也是为什么老环境里能写出undefined 1这种代码而严格模式会直接抛TypeError: Cannot assign to read only property undefined of object。有一个几乎必考的陷阱是typeof null object。这不是保留字问题是typeof运算符的历史遗留 bug规范为了兼容老代码一直没改。如果你在面试中被问到判断数组typeof []返回的也是object正确做法是用Array.isArray()或者Object.prototype.toString.call()。这些知识点和保留字一样核心都在于“识别语言规范的边界而不是凭感觉猜”。6.2 SyntaxError 与 ReferenceError 的区别保留字引发的大多数报错都属于SyntaxError但很多新手会把所有这种红字统称为“报错了”这给排错带来很大障碍。我把两者区别给你捋清楚SyntaxError解析阶段的错误。代码还没开始运行就被识别出语法不合法常见于保留字作标识符、括号不匹配、字符串未闭合。报错特征通常是Unexpected token xxx。ReferenceError运行阶段的错误。代码语法完全合法但某个标识符在当前作用域里不存在。报错特征是xxx is not defined。TypeError运行阶段的错误。语法合法、变量存在但操作方式不对。比如对null或undefined调属性报错是Cannot read properties of null。举几个对照例子const default 1; // SyntaxError发生在解析阶段 console.log(foo); // ReferenceError发生在运行阶段 null.toString(); // TypeError发生在运行阶段实际排查时看到SyntaxError: Unexpected token基本可以确定是保留字或者语法结构问题根本不需要去看运行时逻辑。如果你在一个函数内部看到private报SyntaxError第一反应就应该是检查这个文件是否处于严格模式或模块环境而不是去调试业务逻辑。6.3 遇到“看起来能跑、换环境就炸”的老引擎问题最后一类日常高频问题叫“环境差异”。同一个文件浏览器里跑得好好的但到了 Kettle、奥多比脚本、旧版 Node、或者某些低版本 WebView 里就报保字错误。原因很简单不同引擎实现了不同时期的 ECMAScript 规范保留字集合不完全一致。老引擎可能保留更多 Java 风格词也可能对let、yield的态度和现代引擎不同。比如在旧版 JScript 中var class 1会直接报错而在现代浏览器的非严格模式下这段代码居然能跑。这种“这个环境合法、那个环境不合法”的差异恰恰说明保留字是一个跟随规范演进的动态集合。我处理这类问题时有一个非常管用的笨办法把代码里所有可能和关键字撞车的命名全部列出来统一替换成“加前缀/加后缀”的版本。比如class改成clsdefault改成defaultValuedelete改成del。这种命名也许不是最优雅的但换来的是跨环境的最大兼容性。如果让我给一个最朴素的建议把所有拿不准的词都当成保留字处理。JavaScript 里有成千上万个可用的命名你永远不缺“xxxConfig”“xxxValue”“getXxx”这类表达方式没必要去赌某个名字在当前引擎里恰好合法。少一次踩坑比什么都强。