
做了这么多年前端我一直觉得JavaScript的核心语法才是真正拉开差距的地方。框架可以换Vue换React再换Svelte都没问题但一旦碰到复杂业务逻辑比如异步任务编排、深拷贝、数组各种变换、this指向丢失很多三五年经验的人照样会卡壳。这不是记不住API的问题而是核心语法没有形成体系。这篇文章我打算把JS核心语法彻底梳理一遍不绕弯子从数据类型到函数、对象、数组、异步流程控制再到事件循环和实战场景每一块都讲清楚“为什么”尽量让基础一般的同学也能直接拿去做项目复盘。1. 先说清楚JS核心语法到底在学什么很多初学者会把“JavaScript核心语法”和“API大全”混为一谈以为背下了数组方法、字符串方法就算学会了。真不是这样。API是工具语法是内功。核心语法解决的是语言本身怎么组织逻辑、怎么管理变量、怎么描述行为和状态。框架的源码、复杂业务的抽象、性能优化最后都落在这套底层的语法拼图上。1.1 为什么核心语法比框架API更重要框架API的更新频率非常快。拿前端圈子来说jQuery时代大家背$.ajax后来Vue 2时代背this.$emit到了Vue 3又变成setup和ref。你今天死记硬背的框架API三年后大概率变成历史包袱。但核心语法不一样从ES5到ES2024语言层面的核心机制基本稳定只会渐进增强不会推倒重来。我举个特别实际的例子。热词里有“js深入浅出vue”不管你是看Vue源码还是做二次开发核心依赖的就是Object.defineProperty、Proxy、发布订阅模式、闭包、异步更新队列。这些都是核心语法和原生对象能力的组合不把底子打牢源码分析就是看天书。再比如做JS逆向和反爬的同学天天面对压缩混淆过的代码你如果没有扎实的语法功底连变量提升和闭包都认不出来更别说还原逻辑了。1.2 从ES5到ES6语法演进的底层逻辑ES6之后JavaScript的语法变化不是单纯地“加糖”而是补上了很多语言层面的短板。理解这个演进逻辑比死记新特性列表重要得多。之前用var声明变量最大的问题是作用域太宽松。函数级作用域加上变量提升容易在循环和条件分支里踩坑。所以ES6引入了let和const用块级作用域把变量的生命周期锁死。之前回调地狱让人痛不欲生所以Promise和async/await出现了把嵌套回调拍平成了顺序逻辑。之前对象的原型继承写起来很绕所以class语法和extend关键字出现了虽然本质还是原型链但表达方式友好太多。你会发现一个规律语法演进永远是为了解决开发者的真实痛点。理解了这一点你再去看新特性就不会觉得是零散的知识点而是有一条清晰的主线。1.3 变量声明与数据类型最基础也最容易翻车很多人觉得变量声明太简单不值得看。但我面试过不少候选人一问到var和let的底层区别能说清的人真不多。JavaScript的数据类型分为两大类原始类型和引用类型。原始类型包括 string、number、boolean、null、undefined、symbol、bigint它们存的是值本身。引用类型包括 object、array、function它们存的是内存地址。这个差别直接决定了赋值和比较的行为let a 10; let b a; b 20; console.log(a); // 10原始类型赋值互不影响 let obj1 { name: Tom }; let obj2 obj1; obj2.name Jerry; console.log(obj1.name); // Jerry引用类型赋值拷贝的是地址这个例子解释了开发中最常见的“为什么我改了某个对象其他地方也跟着变了”。理解了值和引用的区别你就知道什么时候该深拷贝什么时候浅拷贝就够了。再来看var、let、const的选择。我的习惯是默认用const需要重新赋值时用let几乎不用var。原因有三点const是块级作用域且声明时就要初始化能提前暴露“变量被意外重新赋值”的问题。let也有块级作用域不存在var那样到处乱串的问题。var的变量提升机制会让代码阅读顺序和执行顺序不一致调试时很容易产生认知负担。注意const变量指向的对象其属性是可修改的。const obj {}然后obj.name x完全合法只是不能重新赋值obj本身。还有一个所有前端都会碰到的经典问题0.1 0.2 为什么不等于 0.3因为JS里的number是IEEE 754双精度浮点数二进制无法精确表示0.1和0.2所以计算结果会有浮点误差。处理金额计算时要么用整数分做单位要么用专门的计算库千万别直接拿小数去做相等判断。2. 函数就是JS的命根子说JavaScript函数式一等公民很多人只是听了这句话却不知道它意味着什么。函数在JS里可以赋值给变量、可以当作参数传递、可以被另一个函数返回这就衍生出了一整套灵活的编程方式。想写明白JS函数这块必须吃透。2.1 函数声明、函数表达式与箭头函数的区别这是热词“js函数”里绕不开的考点。三者写法不同行为也不同// 函数声明 function sum(a, b) { return a b; } // 函数表达式 const sum function(a, b) { return a b; }; // 箭头函数 const sum (a, b) a b;最大的区别在于“提升”。函数声明会被提升到作用域顶部所以你可以在声明之前调用它。函数表达式不行它必须等执行到赋值语句之后才能用。箭头函数就是纯语法糖吗也不是。它没有自己的this、没有arguments对象、不能作为构造函数。我实际开发中的经验是定义工具函数、普通方法优先用函数声明代码结构更清晰。需要保存this上下文时比如在事件回调或定时器里访问外层this用箭头函数最省事。不要滥用箭头函数在需要动态this的场景比如对象方法、构造函数、事件监听器里需要拿到当前元素该用function还得用。2.2 闭包到底是什么用一个生活中的类比讲透闭包这个概念劝退了无数初学者。我用一个生活场景来解释闭包就好比你把自己的日记本藏在一个只有你自己知道的抽屉里外部世界看不到这个抽屉但你可以随时打开它。函数就是那个抽屉函数内部声明的变量就是日记本而外部作用域无法直接访问。function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2这里的匿名函数“记住”了createCounter作用域里的count变量每次调用都能修改它。这个机制在实际开发中有三个典型场景防抖和节流函数。把timer变量藏在闭包里每次调用都复用同一个定时器ID。模拟私有变量。比如一个对象内部的状态不通过暴露方法就无法从外部修改。循环中的异步回调去绑定正确的作用域。2.3 this指向四条规则就能记住this是JS里最大的坑之一。我见过很多写了几年代码的人遇到this就在控制台里一遍遍console.log。其实this的绑定就四句话默认绑定独立函数调用时非严格模式下this指向全局对象浏览器里是window严格模式下是undefined。隐式绑定作为对象的方法调用时this指向这个对象。显式绑定通过call、apply、bind调用时this指向你传进去的那个目标。new绑定通过new关键字调用构造函数时this指向新创建的那个实例。优先级上new 显式绑定 隐式绑定 默认绑定。注意箭头函数不遵守这四条规则它的this是在定义时从外层作用域继承的而且之后不能被改变。有一个非常典型的坑把对象方法当作回调传给setTimeoutthis就丢了。const user { name: Tom, greet() { console.log(Hello, ${this.name}); } }; setTimeout(user.greet, 1000); // Hello, undefinedthis丢成了undefined解决办法就是箭头函数setTimeout(() user.greet(), 1000)或者用bind显式绑定setTimeout(user.greet.bind(user), 1000)。3. 对象、数组与数据操作的十八般武艺前端日常开发80%的时间都在处理数据从后端拿数据、转换数据、再渲染到页面上。对象和数组是整个数据流转的核心。热词里“js 数组”、“js对象”、“javascript学习手册四js对象”、“javascript学习手册五js数组”被反复提到说明这套东西确实是大家最常用的。3.1 对象操作的关键API与原型的理解对象的访问有两种方式点语法和方括号语法。前者适合属性名是固定标识符的情况后者适合属性名是动态计算的情况比如要读取user[某个变量]这种场景。现代开发中下面这几个对象方法是必须会的const person { name: Tom, age: 20, city: Shanghai }; // 获取键、值、键值对数组 console.log(Object.keys(person)); // [name, age, city] console.log(Object.values(person)); // [Tom, 20, Shanghai] console.log(Object.entries(person)); // [[name, Tom], [age, 20], [city, Shanghai]] // 合并对象 const extraInfo { hobby: coding }; const fullInfo Object.assign({}, person, extraInfo); // 或者用展开运算符更简洁 const fullInfo2 { ...person, ...extraInfo };about原型链我不建议死记硬背那套“沿原型链向上查找”的教科书描述。你只需要记住三个事实每个对象都有一个内部属性指向它的原型访问一个属性时先在自身找找不到就顺着原型链往上找直到找到Object.prototype为止对象的共享方法写在原型上可以节省内存class语法是原型继承的语法糖。3.2 数组高阶方法map/filter/reduce组合拳热词里“js数组合并去重”、“js函数”、“操作excel的js工具库”这些场景本质上都是数组处理。我觉得数组方法里最核心的就是map、filter、reduce掌握了这三个你就能把传统的for循环压成一行。const numbers [1, 2, 3, 4, 5]; const doubled numbers.map(n n * 2); // [2, 4, 6, 8, 10] const evens numbers.filter(n n % 2 0); // [2, 4] const total numbers.reduce((sum, n) sum n, 0); // 15map生成一个新数组和原数组一一映射filter筛选出符合条件的元素reduce把数组归约为一个值这个值可以是数字、对象甚至可以是另一个数组。我写复杂业务时最常用的套路是先用filter过滤无效数据再用map做字段映射最后用reduce分组或统计。这个组合能解决大多数数据清洗需求。forEach和map不是一回事。forEach只负责遍历执行副作用没有返回值map必须返回新数组。如果你只是想在遍历里做点事情比如打印日志用forEach。如果要对每个元素做变换并生成新数组用map。用错了语义会让代码很难维护。3.3 数组合并去重每种写法背后的代价“数组合并去重”这个热词我没少在面试题里看到。先看合并有concat和展开运算符两种常见方式const arr1 [1, 2]; const arr2 [3, 4]; const merged [...arr1, ...arr2]; // [1, 2, 3, 4]再看去重。简单数据类型去重Set是最优雅的const arr [1, 2, 2, 3, 3, 4]; const unique [...new Set(arr)]; // [1, 2, 3, 4]但对象数组去重就不能这么玩因为对象的比较是引用比较就算两个对象内容完全一样在Set里也认为是不同的元素。这时候你得制定一个“唯一性规则”比如按id去重const users [ { id: 1, name: Tom }, { id: 2, name: Jerry }, { id: 1, name: Tom } ]; const uniqueUsers users.filter((user, index, self) self.findIndex(u u.id user.id) index );也可以遍历去重边遍历边用一个Map或者对象做记录性能更好。选择哪种方案取决于数据量。如果数组只有几十条写清楚最重要如果上万条就要考虑时间复杂度了。3.4 字符串操作包含判断、大小写与常用工具热词里“js判断字符串是否包含”、“js 忽略大小写”都是字符串命中的高频需求。最基本的包含判断最早用indexOf现在直接用includes语义更清晰const str Hello World; str.includes(World); // true str.startsWith(Hello); // true str.endsWith(World); // true忽略大小写的方法也简单比较前统一转成小写const target javascript; const keyword JavaScript; target.toLowerCase().includes(keyword.toLowerCase()); // true除此之外字符串的截取和替换需要区分substring、substr和slice的差异。我的建议是只用slice和substring不要用substr因为它已经被废弃了。slice支持负数索引从尾部开始数实际编码时非常方便。const str abcdef; str.slice(0, 3); // abc str.slice(-3); // def str.substring(0, 3); // abc4. 条件、循环与流程控制代码的骨架函数是零件对象和数组是数据容器而条件语句和循环语句决定代码的执行路径。逻辑写得乱不乱清晰不清晰全看这一层。4.1 条件判断的多种写法与可读性权衡if/else是最基础的条件判断。热词里“javascript学习手册六js条件语句”被搜索说明这仍然是很多人的学习重点。但实际开发中我发现几个常见问题嵌套太深、分支太多、判断条件冗长。嵌套太深的if我的习惯是“早退”。把异常情况在前面先return掉后面的主流程就不需要包裹在else里了function process(data) { if (!data) { console.warn(data不能为空); return; } if (!Array.isArray(data)) { console.warn(data必须是数组); return; } // 主流程直接写在这里不需要else }如果条件是单个变量的多值判断switch比一堆if更清晰。但还有一个更好的替代方案用对象做映射。比如根据状态码拿文案const statusMap { PENDING: 待处理, SUCCESS: 成功, FAILED: 失败 }; const text statusMap[status] || 未知状态;这种方式比switch更紧凑也更符合数据驱动的思路。逻辑分支的精髓不是“把条件写对”而是“把条件的可读性保持住”。4.2 循环的不再只会用for遍历方式的进化数组遍历的选择太多了很多人分不清。我的判断标准是需要知道索引用for循环或forEach的第二个参数。需要提前终止循环用for或for...of配合break。需要遍历对象用for...in配合hasOwnProperty检查。只需要遍历数组的值优先用for...of。需要对每个元素做变换、过滤、汇总回到3.2里说的map/filter/reduce。注意for...in和for...of的区别非常关键。for...in遍历的是键名对数组来说就是下标for...of遍历的是值。对对象来说for...in会连原型链上可枚举的属性一起遍历出来所以最好加上hasOwnProperty判断或者直接用Object.keys去遍历。// 对象遍历推荐写法 Object.keys(obj).forEach(key { console.log(key, obj[key]); });4.3 异步流程控制从回调到async/await的演进JS的异步是语言设计的核心。早期只有回调函数多层异步就形成了“回调地狱”代码横向发展几乎没法维护。后来Promise出现用链式调用解决了横向嵌套问题。再后来async/await让异步代码写起来和同步代码一样。热词里“js promise”单独被搜出来说明很多人对它还有理解障碍。Promise本质上是一个状态机pending进行中、fulfilled已成功、rejected已失败状态一旦从pending变到fulfilled或rejected就不可再变。Promise构造函数接收一个执行器函数里面做异步操作function fetchUser(id) { return new Promise((resolve, reject) { setTimeout(() { if (id 0) { resolve({ id, name: Tom }); } else { reject(new Error(id无效)); } }, 1000); }); } fetchUser(1) .then(user console.log(user)) .catch(err console.error(err));书写Promise时最常犯的错误是忘记return。在then链里做异步操作必须return一个新的Promise否则下一步拿不到结果fetchUser(1) .then(user { // 这里必须 return下面的then才能拿到结果 return fetchUser(user.id 1); }) .then(nextUser console.log(nextUser)) .catch(err console.error(err));async/await是Promise的语法糖让异步代码更像同步代码。它的核心准则是只有被async声明的函数内部才能使用await。await会阻塞当前函数继续执行直到后面的Promise完成但不会阻塞整个线程。对于依赖某个接口返回后再请求下一个接口的串联场景这样的写法最直观。async function loadUserWithFriend(id) { try { const user await fetchUser(id); const friend await fetchUser(user.id 1); return friend; } catch (err) { console.error(err); } }5. 同步与异步事件循环机制拆解理解了Promise和async/await还必须知道它们在一个更大的框架里如何运行。这个框架就是事件循环Event Loop。热词“js event loop过程”被点出来说明这是绕不开的核心难点。我之前写过不少调试代码很多时候报错原因不在于业务逻辑而在于代码的执行顺序被事件循环的机制改变了。5.1 从一段代码看懂事件循环直接看例子console.log(1); setTimeout(() { console.log(2); }, 0); Promise.resolve().then(() { console.log(3); }); console.log(4);执行结果是 1、4、3、2。原因在于同步代码先执行Promise的回调属于微任务会在当前宏任务结束后立刻执行setTimeout的回调属于宏任务即使延迟是0也要等到下一个宏任务阶段才执行。我把事件循环的过程简化为三步执行一个宏任务执行所有微任务更新渲染。宏任务包括script整体代码、setTimeout、setInterval、事件回调微任务包括Promise.then、MutationObserver、queueMicrotask。每次执行完一个宏任务都要把微任务队列清空微任务里如果又生成了新的微任务会继续执行直到清空为止。5.2 常见异步坑与排查方法最大的坑有两个。一个是“异步中的for循环变量捕获”。注意这里对应热词“循环中的异步回调绑定”很多老代码里用var声明循环变量闭包捕获的是同一个变量for (var i 0; i 3; i) { setTimeout(() console.log(i), 0); } // 输出 3 3 3因为var是函数级作用域三次循环共用同一个i等setTimeout执行时i已经变成3了。改成let就解决了因为let是块级作用域每次循环都会重新绑定一个i。或者用闭包包一层也可以。另一个是Promise的异常被吞掉。Promise的rejection如果没有对应的catch处理错误不会直接抛出来而是静默丢失排查起来非常痛苦。我的习惯是每个Promise链都至少有一个.catch或try/catch生产环境还要监听unhandledrejection事件把漏网的异常统一记录下来。5.3 Promise组合的进阶用法掌握Promise.all、Promise.allSettled、Promise.race、Promise.any这四个组合方法能解决大部分并发场景。Promise.all等所有Promise全部成功返回结果数组任何一个失败整体失败。适用于并发请求多个接口且相互依赖全部结果的场景。Promise.allSettled等所有Promise状态都确定不管成功失败返回每个任务的状态和结果。适用于关心部分成功结果的场景。Promise.race谁先完成就用谁的结果。适用于超时控制的场景。Promise.any谁先成功就用谁的全部失败才报错。适用于多个候选请求谁先返回可用的用谁。比如做超时控制function withTimeout(promise, ms) { const timeout new Promise((resolve, reject) { setTimeout(() reject(new Error(请求超时)), ms); }); return Promise.race([promise, timeout]); }这样的写法在接口慢、资源加载、扫码登录轮询等场景都可以直接用。6. 实战把语法串起来做点小东西学了这么多核心语法最终还是要落到项目上。我挑了热词里出现过的几个典型场景演示怎么把前面这些知识串起来。这些场景不大但里面全都是核心语法的组合应用。6.1 三级联动数据处理与事件绑定的综合应用“js三级联动”是前端面试和简历上最常见的功能之一多半是省市区选择。核心思路不复杂选择省根据省拿市列表选择市根据市拿区列表。纯JS实现时最关键的是用change事件监听select的变化再动态重设后面的select选项。const regionData { 广东省: { 广州市: [天河区, 越秀区], 深圳市: [南山区, 福田区] }, 江苏省: { 南京市: [玄武区, 鼓楼区], 苏州市: [姑苏区, 工业园区] } }; function updateSelect(selectEl, options) { selectEl.innerHTML ; options.forEach(item { const opt document.createElement(option); opt.value item; opt.textContent item; selectEl.appendChild(opt); }); } // 监听省变化 provinceSelect.addEventListener(change, (e) { const cityMap regionData[e.target.value] || {}; const cities Object.keys(cityMap); updateSelect(citySelect, cities); // 同时触发一次市变化把区列表同步出来 citySelect.dispatchEvent(new Event(change)); });这里用到了对象遍历Object.keys、数组遍历forEach、模板字符串虽然没有显示、事件监听和自定义事件。数据驱动的思想也很明显先把数据组织好再根据数据去渲染页面避免了一堆复杂的DOM操作。实际的省市区数据往往是一个巨大的JSON文件你可以提前把数据按省、市两级组织成嵌套对象这样取子集时直接按key取值性能非常可观。6.2 复制粘贴功能从execCommand到Clipboard API热词里有“js实现复制粘贴”这个需求说简单也简单说坑也坑。老方案是用document.execCommand(copy)配合一个隐藏的textarea或者input选中内容再复制function copyText(text) { const textarea document.createElement(textarea); textarea.value text; document.body.appendChild(textarea); textarea.select(); document.execCommand(copy); document.body.removeChild(textarea); }新方案是使用Clipboard API也就是navigator.clipboard.writeText。它在HTTPS或者localhost环境下才可用而且需要用户授权。好在复制粘贴通常都是在用户点击按钮之后才触发权限这一关一般没问题。async function copyText(text) { try { await navigator.clipboard.writeText(text); console.log(复制成功); } catch (err) { console.error(复制失败, err); // 降级到execCommand方案 } }这里面综合运用了异步函数、try/catch错误处理、Promise和条件分支。很多人只记住了execCommand一种方案遇到不支持的环境就抓瞎。两种方案配合降级才是生产环境该有的姿势。6.3 打开URL与工具库集成的思路“js 打开url”是另一个高频需求。最稳的方式是通过创建a标签并模拟点击function openUrl(url) { const a document.createElement(a); a.href url; a.target _blank; a.rel noopener noreferrer; document.body.appendChild(a); a.click(); document.body.removeChild(a); }注意加relnoopener noreferrer是为了安全防止新页面通过window.opener拿到原页面的控制权。如果只是想当前页跳转直接window.location.href url即可。至于热词里提到的xlsx处理、在线音乐源接入等场景本质上都是“核心语法 第三方库”的组合。使用xlsx库读取表格核心逻辑就是把工作簿文件解析成一个二维数组再用map和filter把数据变换成前端需要的格式。库帮你把复杂性藏起来了但底层的数组变换和对象操作还是得靠核心语法。6.4 用原生JS写防抖函数闭包和this的综合演练防抖debounce是核心语法的高频应用代码量不到十行但能同时考到闭包、this绑定、箭头函数、rest参数这几个核心知识点function debounce(fn, delay 300) { let timer null; return function(...args) { const context this; if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(context, args); }, delay); }; }返回的函数每次被调用时都会重置定时器。只有停止操作超过delay毫秒后fn才会真正执行。这里有三个细节值得注意timer放在闭包里不会污染外部作用域。用...args收集参数再用apply传入原函数保证回调的参数不丢失。用箭头函数而非function来包定时器回调让this能透传到外层所以fn.apply(context, args)里的context就是event listener绑定时的this。防抖在输入框实时搜索、窗口resize、按钮重复提交等场景都极其有用。同样的逻辑改成节流throttle控制执行频率不低于某个阈值则是另一种思路。7. 常见问题与排查技巧实录这部分是我在实际开发中反复踩过的坑也是我帮别人排查代码时最常见的几种问题。整理了对应的原因和解决方案更像是速查表方便你遇到问题时直接对照。7.1 高频错误与解决方案速查错误现象根本原因解决方案Cannot read property x of undefined访问了undefined对象的属性多半是接口返回结构变了用可选链.或提前判空this在回调里变成undefined函数作为独立回调时this丢失箭头函数、bind、闭包保存this数组去重没生效对象数组不能用Set直接比较内容按唯一字段用Map或filter去重0.10.2 ! 0.3浮点数二进制存储精度有限转整数计算或使用库setTimeout循环打印同一个值var变量提升且被闭包共享用let声明循环变量异步请求的数据渲染不上数据在异步回调里赋值渲染时机不对在回调里触发渲染或用Promise/asyncconst obj {}后来又给obj重新赋值报错const不能重新赋值用let声明需要重新赋值的变量Promise没有执行也没报错忘记return Promise链式断了补上return或检查resolve/reject是否被调用7.2 字符串包含判断的兼容问题早期浏览器里没有String.prototype.includes有些老项目还在用indexOf。我建议在兼容老环境时自己封装一个判断函数而不是到处改调用方function contains(str, keyword) { return str.indexOf(keyword) ! -1; }如果想忽略大小写就统一toLowerCase后再判断。这个函数可以挂在公共工具里后续项目升级到ES6时再统一替换成includes。这种渐进式兼容的思路比一次性大改代码安全得多。7.3 异步编程的三条经验我做前端这几年的血的教训总结成三条经验所有接口请求都建议做错误处理不要依赖“假设后端一定不会出错”。最简单的方式是封装一个request函数内部统一拦截错误和超时。一旦一个函数用了async那么后续所有调用它的代码都要按异步处理不要在async函数外面同步等待结果。事件循环里的顺序问题永远不要靠“猜”去解决。写复杂异步逻辑时直接在关键节点打印日志把执行顺序看一遍比自己脑内模拟快得多。7.4 JS验证文件类型时的安全补充热词里“前端js验证”相关的内容也常有。前端验证只能提升用户体验不能作为唯一的安全防线。比如文件上传时前端判断文件类型就不要只看文件扩展名还要看MIME类型甚至看文件头字节。更严格的做法是把类型校验放到服务端。前端的职责是提前发现明显错误、减少无效请求而不是替后端做安全决策。8. 整理一份核心语法自查清单聊到这里我觉得有必要把这次梳理的语法知识收敛成一份可以自我检测的清单。你可以在面试前、做项目前、或者带新人的时候直接拿这组问题去检验掌握程度。每一条想清楚“是什么”和“为什么”之后再去看实际代码思路会清晰很多。8.1 拿到代码先问自己的几个问题看到一段含变量的JS代码先判断它的作用域是这个变量的可见范围。看到函数定义先想它的调用时机和调用者是谁this会指向哪里。看到一个对象结构先想它是数据容器还是业务组件属性可能从哪里来。看到循环想清楚是在遍历对象、数组还是可迭代对象遍历中途是否可能提前退出。看到异步操作想清楚它属于宏任务还是微任务后续代码执行顺序会不会受到影响。这套思考方式实际是我平时代码review的基础。我review别人的代码不会只看表面逻辑是否正确而是看这五个层面是否都有意识地设计过。很多后来爆出来的线上bug比如数据没到位导致渲染错乱this指向偏移导致事件无效都能在这五个问题里找到蛛丝马迹。8.2 关于工具链与原生语法学习的平衡热词里有“前端js大全”、“js深入浅出vue”这样的内容我见过很多人收藏了一堆工具库和框架文章但真正自己动手写的时候还是不会。原因很简单工具和框架是在语法之上做的抽象抽象用久了你会慢慢忘记底层是怎么组织的。我的建议是每学一个框架API都去想一想它底层对应的原生语法是什么。Vue的响应式对应Object.defineProperty或ProxyReact的setState批量更新对应任务队列的概念事件总线对应发布订阅模式。当你把框架和语法连接起来的时候你就不再是“会用框架”而是“理解框架”。最后的最后我再分享一点个人体会。搞前端这些年我见过太多人急于求成去追新框架、新技术结果兜兜转转回来补基础反而浪费了更多时间。JS的核心语法就是这个行业永不贬值的内功你早一天把它吃透早一天写代码不慌。要是能把这篇文章里的每个例子动手敲一遍再对照你自己的项目想想哪里能用上收获会比看十篇零散教程都大。