
写代码这件事入门容易写明白难。很多人最开始接触JavaScript都是从照着demo敲一遍开始的但真正到了自己上手写业务逻辑、处理数据、调试报错的时候才发现最基础的东西没吃透处处都是坑。这篇文章我就把web前端JS的基本语法这部分从实际开发的角度重新捋一遍。不是教科书式的概念罗列而是把那些日常写代码真正用得到、面试也经常问、但很多人其实没完全搞懂的知识点掰开揉碎讲清楚。不管你是刚入门的新手还是写了一两年JS但基础不够扎实的开发者这篇文章都适合你花半小时认真过一遍。1. 内容整体设计与思路拆解1.1 为什么基础语法值得重新学一遍我见过不少开发者JS写了很久能看懂代码也能改bug但一问到闭包、作用域、原型链这些基础概念就含糊。这不是他们不努力而是基础语法这块当初学得太糙靠的是多写几遍自然就会了的经验主义。但实际上JS的基础语法决定了你后面能用这个语言走到什么深度——它关联着你如何组织代码结构如何在团队协作中写出别人能维护的代码甚至决定着你排查bug时是瞎试还是精准定位。基础语法这东西看起来简单但恰恰是那些看起来简单的地方藏着最深的坑。比如变量提升、类型隐式转换、this指向这些你要是没真正理解背后的机制写着写着就会踩中莫名其妙的问题。所以这篇内容从最核心的语法机制讲起一层层把JS的性格剖析清楚。这里有个常见的认知误区很多人以为快速入门就应该从框架开始React、Vue上手写页面多快啊。但框架本身是建立在JS语言特性之上的如果你连基本的数据处理都容易出类型错误连函数参数传递和回调的执行时机都不清楚框架只会放大这些问题而不是帮你解决。我经常说JS的基本功就好比盖楼的地基地面上的人可能只看到漂亮的楼层但楼能盖多高看的全是地基的深度。1.2 这套语法体系在开发中的位置要从全局看JS的基本语法在真实开发里的位置我们可以做一个类比。如果把一个前端项目比作一座房子那HTML是墙体结构CSS是装修风格而JS是水电管网——它负责让整个空间真正活起来响应点击、发送请求、动态渲染数据。基本语法中的变量与数据类型决定了管网用什么材质函数与作用域决定了水电路线的走向对象与数组的操作决定了不同房间之间的联动逻辑。在实际开发中你写的每一段业务代码最终都可以拆解成几类基础动作声明变量存数据、处理数据、控制流程、封装函数复用逻辑、用事件或异步机制与外界交互。而这些基础动作全部落脚于基本语法。很多人写代码觉得不顺手根源就在于对语言底层行为的不理解写一行代码之前要先想这行到底干什么、结果会是什么而不是靠打字肌肉记忆。所以我更愿意把这篇内容定义为实战前置课。学完以后你再去看框架源码、去写组件逻辑会发现原来那些看不懂的语法其实就是几个核心机制的叠加组合。基本功不仅仅是入门的第一步更是职业生涯中排错效率的决定性因素。2. 变量声明与数据类型2.1 var、let、const三兄弟的真正区别这是最基础但也最容易被轻视的内容。很多新手背结论var能重复声明、let和const不能const定义常量不能改。但你要是真的写业务代码会发现光背结论不够你得理解为什么这样设计以及这三个关键词对应什么样的编程场景。先看变量提升。var声明的变量会提升到代码块顶部并且初始值为undefined。这意味着你可以在声明之前使用这个变量——它不会报错只是值为undefined。这个特性其实是JS早期设计的历史遗留极易造成误用。let和const则不存在这个问题它们在声明之前访问会直接抛出ReferenceError错误这个区域被称为暂时性死区。再说说作用域。var只有函数作用域没有块级作用域。所以你在if代码块里用var声明一个变量出了if块它还在这个变量就泄漏到了外部作用域。let和const是块级作用域的出了花括号就访问不到。在团队协作中变量泄漏是很多隐蔽bug的来源比如循环里注册事件用var就会出经典问题for (var i 0; i 3; i) { setTimeout(function() { console.log(i); // 输出3, 3, 3 }, 100); } for (let j 0; j 3; j) { setTimeout(function() { console.log(j); // 输出0, 1, 2 }, 100); }原因很简单var声明的是同一个全局变量i循环结束后i已经是3三个定时器回调拿到的都是同一个i。而let每次循环创建独立的块级绑定每个回调捕获的是当次循环的j。很多新手在这个问题上卡很久就是因为不理解作用域绑定的机制。还有一个实践建议。优先使用const只有当变量的值确实需要被重新赋值时才用let。var就尽量不要用了。这样写的好处是代码的可预期性更强一个变量从出生到销毁都是固定引用别人接手你的代码时不需要去追踪这个变量的赋值历史。老有人觉得这不过是代码风格问题但真正到了维护阶段你才会发现const带来的安心感有多值钱。2.2 数据类型与typeof的边界陷阱JS的基本数据类型有七种undefined、null、number、string、boolean、symbol、bigint。再加上object构成了整个语言类型体系。这里有一个在开发中非常容易踩坑的点就是typeof运算符对null的处理typeof undefined; // undefined typeof null; // object —— 这是个历史bug但官方明确不会修为什么会这样因为JS最初的版本中类型的标签位设计存在问题null被错误地标识为对象类型。很多框架和工具库为了解决这个问题都会做一个增强判断function isNull(value) { return value null; }注意这里要用全等运算符因为宽松相等运算符会在null和undefined之间做隐式转换判断导致null undefined为true。如果你就是想精确判断一个变量是否为null是最安全的。number类型也有不少坑。JS的number是双精度浮点数所以0.10.2并不等于0.3这在涉及金额计算的时候会引发严重问题。实际开发中我建议涉及精确计算的场景使用整数运算比如以分为单位存储金额或者使用专门的库来处理十进制运算。这也是面试时经常考察的一个点原理就是IEEE 754标准的二进制浮点数无法精确表示所有十进制小数。还有NaN这个特殊值。它代表不是数字但它的类型却是number。判断一个值是否为NaN不能直接用因为NaN NaN的结果是false。需要用全局的isNaN函数或更严格的Number.isNaN。这里又有个细节isNaN会对待判断的值做隐式类型转换所以isNaN(hello)返回true而Number.isNaN不会做转换Number.isNaN(hello)返回false。日常开发中用Number.isNaN更可靠。2.3 类型转换与宽松/严格相等JS是一门弱类型语言所以类型转换无处不在。你写一个if判断系统会自动把值转成布尔值你做字符串拼接数字会自动变成字符串。这种便利性在某些时候是福音在某些时候就是炸弹。用一个小例子说明隐式转换的危险性console.log(1 2); // 12 console.log(1 - 2); // -1 console.log(5 * 2); // 10 console.log([] []); // console.log([] {}); // [object Object]看起来毫无规律其实背后有规则加法运算中如果有一边是字符串会优先做字符串拼接减法和乘法运算会强制把两边转为数字对象转原始值时会调用toString方法。这些规则在JavaScript规范中有详细定义但日常开发中很少有人真正去记。我给你的建议是不要依赖这些怪异的隐式转换保持类型明确是避免线上事故的第一原则。宽松相等和严格相等的选择也值得多说两句。很多老一辈面试官爱考的比较规则但实践层面ESLint规范里有一条规则叫eqeqeq就是强制要求必须用代替。这不是矫情因为带来的隐式转换在复杂的业务场景里极难排查。你判断一个接口返回的字段是否为空它可能返回null、可能返回undefined、可能返回空字符串如果用null判断以上三种情况全部命中但你根本不知道具体是哪种后续的逻辑很难写。当然有一个场景我是主动用的判断某个值是null还是undefined时用value null一行搞定这是JS社区普遍认可的技巧。除此之外全用。3. 函数、作用域与闭包3.1 函数声明的几种方式以及差异函数是JS的一等公民。这意味着函数可以像普通变量一样赋值、传参、被返回也可以作为对象的属性存在。函数声明主要有三种方式函数声明、函数表达式、箭头函数。三者看起来差不多实际差别很大。函数声明有提升特性可以在声明之前调用sayHello(); // 正常运行 function sayHello() { console.log(hello); }函数表达式没有提升它把函数赋值给一个变量变量本身遵守let的规则不能在声明之前使用。这个差异在模块化编程中很关键决定了你组织代码的顺序是否自由。箭头函数是ES6引入的语法更简洁但有两个重要差异必须清楚。第一箭头函数没有自己的this它继承定义时所在作用域的this第二箭头函数不能用作构造函数不能使用new调用所以也没有prototype属性。在实际开发中什么时候用箭头函数什么时候用普通函数我的习惯是这样需要在回调中访问外层的this时用箭头函数需要动态绑定this、或者需要用作构造函数时用普通函数。比如在对象方法中普通函数的this指向调用它的对象本身而箭头函数的this则会指向外层上下文——很多人在这里栽跟头以为对象方法里的this就是对象结果用箭头函数一写this指向变成了undefined。const obj { name: 示例, greet: function() { console.log(this.name); // 示例 }, badGreet: () { console.log(this.name); // undefinedthis指向外层 } };还有一个细节是rest参数和arguments。普通函数内部可以使用arguments对象获取所有实参它是一个类数组。箭头函数没有自己的arguments但可以通过rest参数实现同样的功能function oldWay() { console.log(arguments[0], arguments[1]); } const newWay (...args) { console.log(args[0], args[1]); };rest参数返回的是真正的数组可以直接使用map、filter这些数组方法而arguments需要先转换。3.2 作用域链与闭包的真实面貌作用域链是JS引擎在执行代码时查找变量的路径规则。当你在一个函数内部访问一个变量时引擎会先在当前函数作用域里找找不到就去外层的嵌套作用域里找一直找到全局作用域。这条查找链路就是作用域链。闭包的本质就是函数记住并访问了其定义时的词法作用域。即使这个函数后来在别的地方执行了它仍然可以通过作用域链访问到定义时的变量。听起来很抽象用代码说明function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); counter(); // 1 counter(); // 2createCounter执行完后按说局部变量count应该被销毁了但因为内部函数持有对count的引用count的生命周期被延长了这就是闭包的力量。这个特性可以用来做数据私有化可以把一些状态“锁”在函数内部外部无法直接访问和修改只能通过返回的方法操作。闭包也是面试高频考点的核心但实际开发中我更关心的是闭包的内存管理问题。如果不小心让闭包长期存在或者无意中在循环里创建了大量闭包内存泄漏算是比较典型的性能隐患。现代浏览器引擎对闭包内存有一定的优化但你不能指望它去优化你代码设计层面的大问题。正确的做法是确认闭包不再需要时主动解除引用或者让大对象不进入闭包作用域链。事件监听里也处处是闭包。每次绑定监听器回调函数都持有外部作用域的引用。如果在单页应用中频繁绑定而不解绑内存占用只增不减。很多前端性能问题排查到最后发现就是这类基本语法特性的误用看着不高深影响却是实打实的。3.3 this绑定的四种规则this的指向是JS里最让人头疼的语法点之一但本质上它遵循的规则是有限的掌握了规则就没有模糊空间了。默认绑定函数独立调用时this指向undefined严格模式或全局对象非严格模式。在浏览器的全局环境中非严格模式下的this是window。隐式绑定当函数作为对象的方法调用时this指向该对象。上面obj.greet()的例子就是隐式绑定。显式绑定使用call、apply、bind可以显式指定函数执行时的this指向。new绑定使用new调用构造函数时新创建的对象会成为函数执行时的this。这四者之间还有优先级new绑定 显式绑定 隐式绑定 默认绑定。我在项目中总结的最常用技巧是如果一个函数里的this经常搞混干脆不要用this直接把它需要的值作为参数传进去。这是工程化的思维方式比起死记硬背this的每一种规则让代码本身不依赖this才是长期维护最省心的方案。当然如果团队约定用面向对象风格写代码老老实实理解上述四条规则就是必修课了。涉及事件处理函数的this指向记住一个口诀函数怎么被调用this就指向谁。4. 数组与对象的核心操作4.1 数组常用高阶方法与不变量数组的很多方法和操作都是日常开发中高频率使用的map、filter、reduce、forEach、find、some、every这是现代前端开发中真正支撑起函数式编程风格的基础设施。map用一个函数作用于数组的每个元素返回一个新数组长度与原数组一致。filter根据条件过滤返回的新数组可能更短。reduce更灵活它可以把一个数组累计成一个值。这三个方法用好就足以处理绝大多数的数据转换场景。用reduce实现一个对象分组的功能可以直观看到它的威力const students [ { class: A, name: 张三 }, { class: B, name: 李四 }, { class: A, name: 王五 } ]; const groupByClass students.reduce((result, student) { (result[student.class] || []).push(student); return result; }, {});这里我用到了逻辑或赋值运算符||它等价于result[student.class] || (result[student.class] [])。这是ES2021新增的语法简洁了不少。reduce在处理接口数据聚合时非常顺手一旦你习惯用它会发现循环代码会少很多。高阶方法使用中有一个高频错误map和forEach的区别很多人会踩。map返回一个新数组forEach只负责遍历没有返回值。如果你想基于原数组生成一个新数组用map如果你只是想做循环副作用操作比如打印日志或修改外部变量用forEach。前者是纯函数风格后者是命令式风格。业务代码中应尽量减少forEach的使用因为它的副作用会让逻辑变得隐蔽。还有一个必须注意的细节这些方法都不会改变原数组它们返回新的数组。如果你想做原地操作比如删除某个元素反而需要用splice这类方法它修改原数组。到底用哪种取决于你希望保持不可变性还是追求性能。在React等框架驱动的开发模式中保持原始数据不可变是一个重要约定因为框架的依赖追踪依赖于引用变化直接修改原数组可能导致不可预期的渲染问题。4.2 对象操作要点与边界情况对象是JS数据组织的基础单位。对象的创建、属性读取、属性遍历、浅拷贝深拷贝这些操作看着简单但细节极其丰富。属性读取有两种方式点号和方括号。点号要求属性名是合法的标识符方括号可以使用任意字符串甚至变量。从接口文件读取字段时经常会遇到字段名包含横线或者数字开头的情况比如{user-name: 张三}此时必须用方括号获取。对象的遍历方式有几种for...in可以遍历对象自身的可枚举属性以及原型链上继承的可枚举属性。实际开发中如果你只想遍历自身属性最好配合hasOwnProperty判断。Object.keys返回自身可枚举的属性名数组Object.values返回对应的值数组Object.entries返回[key, value]对的数组。这三兄弟是日常数据遍历的利器它们返回的都是数组可以直接用map、filter等方法继续处理。拷贝的问题就更常见了。浅拷贝只拷贝一层如果对象里嵌套了对象嵌套对象仍然共享引用。深拷贝则要递归处理所有层级。一个常见的开发误区是认为Object.assign({}, obj)就是深拷贝。实际上它只做了浅拷贝顶层属性如果是引用类型复制的是引用而不是值。你真的需要深拷贝时日常开发有很多处理方式比如利用JSON序列化const deepCopy JSON.parse(JSON.stringify(original));这种方式的局限性在于函数、undefined、Symbol、循环引用这些JSON不支持的格式会被丢掉或者报错。如果你的数据结构里有这些特殊值就得用递归手写深拷贝或者借助工具库了。在实际开发中我建议你在写拷贝逻辑前先问自己一个问题我真的需要深拷贝吗很多场景下使用不可变更新就够了即创建一个新对象只改变需要修改的字段其他字段通过展开运算符保留引用这样既避免了深拷贝的性能开销又保持了数据的引用关系。尤其在构建大型前端应用时深拷贝的开销会影响渲染性能能避免就尽量避免。4.3 解构赋值与展开运算符的实战用法解构赋值是ES6最常用的语法糖之一它可以让你从数组或对象中快速提取数据。它不仅仅是语法简洁更是表达了我只关心这个结构中的哪几个字段的意图。对象解构的基本形式是const { name, age } user;数组解构则按顺序取值const [first, second] list;解构赋值有一个非常重要的应用场景是函数参数的定义。函数如果接收一个配置对象直接在参数位置解构调用方不需要关心参数的顺序function showMessage({ title 默认标题, content } {}) { console.log(title, content); }这里我给解构的参数加了默认值同时给整个参数对象也加了默认值避免传入undefined时报错。这是一套比较健壮的写法在写组件和工具库时非常常用。展开运算符则用于把可迭代对象展开成元素列表或者把对象的属性复制到另一个对象中。最典型的使用场景是合并对象时不改变原对象const newConfig { ...defaultConfig, ...userConfig };这行的作用是以defaultConfig为基础用userConfig覆盖其中同名属性生成一个新对象。由于userConfig中不存在的属性取默认值调用方只要传自己要改动的字段就行。这套模式在组件参数合并、请求配置扩展中太常用了。还有一个细节值得注意展开运算符是浅操作的和Object.assign行为一致嵌套对象仍然是引用共享。别以为展开运算符展开一层就自动深拷贝了。5. 流程控制、错误处理与调试技巧5.1 条件分支与短路求值JS的条件判断、逻辑运算符经常藏着一些意料之外情理之中的行为。if的条件表达式会被隐式转换为布尔值以下值会被判为falsefalse、0、-0、NaN、空字符串、null、undefined。其他值包括空数组、空对象、字符串0都会判为true。记住这个规则能避免很多if判断的隐性bug。逻辑运算符和||在JS中具有短路特性并且返回的是参与运算的值本身而不一定是布尔值。这个特性可以被用来写非常简洁的代码const name user.name || 匿名用户; const isLoggedIn user.token user.expiresAt Date.now(); const canShow isAdmin isLoggedIn;第一种写法在配置默认值时很方便但要注意如果user.name本身可能是空字符串或0这种写法就会误用默认值。在这些边缘场景下建议使用空值合并运算符??而不是||const name user.name ?? 匿名用户;??只有左侧是null或undefined时才使用右侧的默认值空字符串和0都能正常传递。这个运算符是ES2020增加的现在已经被广泛支持日常开发完全可以用起来。JS还有一个非常实用的三元运算符condition ? a : b它在简单的二选一场景下可读性很好。但如果嵌套过多代码就会变得难以阅读。我通常在分支超过两个或逻辑复杂时坚持使用if...else或switch因为代码是写给团队里其他人看的简洁不是唯一目标清晰才是。5.2 循环执行与迭代器的选择JS的循环方式主要分为三类传统for循环、for...in循环、for...of循环。它们在语义和适用场景上完全不同。传统for循环适用于你知道要用索引访问数组元素、或者需要自行控制迭代步长的场景。它的性能通常不差但因为代码噪音多可读性不如高阶方法。for...in用于遍历对象的可枚举属性但千万不要用在数组遍历上。因为for...in遍历的是属性名而不是值数组的下标索引是字符串类型的0、1等而且它还会遍历到数组对象上自定义的属性。一次没注意就可能出现诡异的bug。for...of是ES6引入的用于遍历可迭代对象比如数组、字符串、Set、Map。它在语义上更接近依次取每个元素不像for...in那样遍历属性名。日常开发中如果不需要索引值for...of比传统for循环更清晰for (const item of list) { process(item); }这里有个小提醒for...of不能直接用于遍历普通对象的键值因为普通对象不是可迭代的。你需要使用Object.keys、Object.values或Object.entries转换后遍历。之前提到的数组高阶方法map/filter/reduce其实也是一种循环它们在语义上更强调数据变换而非遍历。选择哪种方式我的建议是数据转换优先用高阶方法副作用操作优先用for...of或常规循环。团队代码Review时这也是容易产生分歧的地方提前统一风格能省下不少沟通成本。5.3 异常捕获与错误信息的有效利用JS的错误处理用try...catch...finally。实际开发中很多人只在入口处包一个大的try...catch出了问题也只是console.error一下诊断价值很低。我建议的方式是分层的错误处理策略。底层函数只负责抛出异常不吞掉不处理上层调用方根据自己的语义catch异常决定是重试、降级还是上报。比如请求接口时网络错误、业务错误、数据格式错误应该分开处理而不是一竿子打尽。catch块里的err对象有几个重要属性err.message是错误描述err.stack是调用栈。排查问题时stack的信息价值远超message它告诉你错误在哪一行被抛出、调用链路是什么。所以在console.error时至少要把这两个都打出来。对于预期内的错误场景我更倾向于主动抛出异常而不是返回错误码。因为在JS的生态里异常传播比返回值判断更顺手也更能保证调用方不漏处理。function validateAge(age) { if (typeof age ! number) { throw new TypeError(age必须是数字); } if (age 0 || age 150) { throw new RangeError(age超出合理范围); } return age; }利用JS内置的错误类型可以让错误的语义更加明确定位问题的时候省很多事。5.4 浏览器调试工具的实用技巧编写代码只是工作的一部分调试才是真正暴露基本功的时间。按F12打开开发者工具Sources面板里可以打断点。断点的价值在于当你对一个函数的执行过程不确定时单步执行能逐行看到变量的实时变化比console.log高效得多。这里分享一个我经常用的技巧在console面板里直接输入表达式求值比如在断点暂停时可以直接在控制台里输入this看它指向谁输入某个变量名看它当前的值甚至调用当前作用域内的函数测试输出。这是一种交互式调试的工作方式很多人写前端写了很久却不习惯挺可惜的。如果你在排查一个每次结果都不一样的诡异bug最快速的方法往往是二分法注释代码而不是从头到尾硬看。把可能出问题的代码范围逐步缩小很快就能定位到具体的某一行。这个方法和编程语言无关但在JS的弱类型环境下尤其好用因为隐式类型转换造成的诡异bug非常多。还有一种情况是数据结构和预期不一致我一般直接在接口返回的关键处执行console.table()来可视化展示数组对象。table的输出格式在查看对象数组时比console.log清晰太多字段名和值对齐摆放一眼就能看出哪条数据不对。6. 常见问题与排查技巧实录6.1 高频报错速查表下面这张表整理了我日常开发中遇到频率最高的几类JS基础报错、报错场景和排查思路。建议遇到问题时先对照这张表自查一遍能省下不少搜索时间。报错信息常见触发场景核心排查思路ReferenceError: xxx is not defined变量名拼错、在声明前访问let/const变量、作用域外访问变量检查拼写检查变量声明位置检查是否在正确的代码块内TypeError: Cannot read property xx of undefined调用了undefined/null的属性先确认数据是否为undefined再做属性访问用可选链?.防御TypeError: xxx is not a function把非函数类型当作函数调用检查变量实际类型确认导入导出是否正确检查函数名是否被覆盖NaN出现在计算结果中非数字类型参与了算术运算检查数据来源用Number.isNaN判断并做类型防御Maximum call stack size exceeded无限递归检查递归终止条件确认是否误传了自引用数据Unexpected token语法错误常见于括号不匹配、字符串引号未闭合打开IDE的语法检查面板定位具体行列6.2 经典案例复盘一个隐藏的数据类型问题之前排查过一个线上问题场景比较典型拿出来分享。当时某公司的一个内部运营系统里列表页数据筛选功能出现异常当列表为空时页面显示暂无数据但在某个特定条件下空列表居然渲染出了一个旧数据模块。排查了半天定位到问题出在一个filter方法的判断条件上。代码如下const filtered allItems.filter(item item.status statusFilter);statusFilter是从接口查询参数里取出来的类型是字符串0。item.status虽然数据库里存的是数字0但因为经过接口序列化实际返回变成了字符串0。用比较字符串0和数字0时会做隐式转换结果为true所以筛出了一个本应被过滤掉的数据。这个问题的根源就是使用了而不是。如果当时第一版代码就严格按照约束自己这个问题根本不会出现。修法的思路也很简单做一次显式类型归一化把比较的双方都统一到同一类型const statusFilter String(requestParam.status ?? ); const filtered allItems.filter(item String(item.status) statusFilter);这类问题在真实业务中出现的频率远比想象中高。接口字段的类型在不同的接口提供方那里可能会出现不完全一致的情况有的返回数字、有的返回字符串关键就是你的判断代码要对类型敏感度有明确预期。这是基本功扎实与否最直观的体现——不是代码写得多花哨而是能否预见数据在传输过程中的类型漂移。6.3 基本功训练的落地路径基础语法这个概念很大但学习路径是清晰的。我在实际辅导团队成员时通常建议按以下顺序自测基本功哪一环薄弱就补哪一环。变量部分你是否能准确说出var、let、const三者的全部差异能在什么场景下分别使用闭包的原理你是否能用自己的话解释清楚并且能手动写一个应用闭包的例子类型部分typeof可能返回哪些值如何判断一个值是不是数组如何判断一个对象是不是空对象你能否在不运行代码的情况下手动推导出几个表达式的返回值函数部分函数声明和表达式在提升上的差异是什么箭头函数的this有什么特点call、apply、bind三个方法各有什么适用场景数组对象部分map和forEach的区别是什么reduce能实现哪些复杂的数据聚合如何实现一个对象深拷贝会遇到什么限制流程部分如何写一个健壮的错误处理链你在排查问题时最快的定位手段是什么如果你能流畅回答以上问题并且能随手写出对应的代码示例那JS的基本功算是真正过关了。如果哪一个问题让你要思考半天那就值得单独花时间去补因为这些问题在真实的开发中几乎每一天都会用到。我自己的习惯是每隔一段时间就把基础语法重新翻一遍每次都会有新的理解。这不是客套话是因为当你积累了更多工程经验后那些当初背下来的规则会在新的场景里被重新解释。基本功不是学一次就结束的它是常读常新的东西。