JavaScript核心机制详解:作用域、闭包、异步与原型链实战

发布时间:2026/10/14 8:30:07
JavaScript核心机制详解:作用域、闭包、异步与原型链实战 从入行到现在我一直在跟 JavaScript 打交道。早年写页面特效后来做完整的前端工程再到现在帮团队梳理基础培训材料每一次回头重新读这些最基础的概念都会有新的体会。很多人觉得 JavaScript 门槛低写起来随意但恰恰是那些“好像懂了”的知识点会在某一天突然变成线上事故的根源。这篇笔记不是什么系统教程而是我自己积累下来的一套理解框架涵盖了作用域、闭包、异步、原型链这些绕不开的核心主题也记录了不少实际踩坑后的修正认知。如果你正在系统梳理 JavaScript 基础或者被某些概念绕得头疼这份笔记应该能给你一些启发。1. 变量声明不是小事var、let、const背后的作用域规则1.1 var带来的历史包袱我刚开始写 JavaScript 的时候用的还是 var。当时没人觉得有什么问题直到某天排查一个循环绑定点击事件的 bug才发现 var 的坑远比自己想象的大。var 有两个非常独特的行为变量提升和函数作用域。变量提升指的是声明会被 JavaScript 引擎在处理代码时提前到当前作用域的顶部但赋值仍然留在原位置。这就导致了一种很反直觉的现象console.log(a); // 输出 undefined而不是报错 var a 10;这里没有抛错是因为引擎把var a提升到了顶部相当于先声明了 a默认值为 undefined然后才执行 console.log。很多初学者会被这个现象迷惑以为 JavaScript 是边读边执行的实际它是先编译再执行的编译阶段完成声明绑定执行阶段再跑赋值语句。函数作用域意味着 var 声明的变量不属于块级作用域只属于当前函数。for 循环里用 var 声明的计数器循环结束后依然能访问到for (var i 0; i 5; i) { setTimeout(() console.log(i)); // 输出5次5不是0、1、2、3、4 }这个经典问题本质上是 setTimeout 的回调函数在循环结束后才执行而它读到的 i 是同一个、已经被累加到 5 的变量。ES6 之前开发者只能用闭包来绕过这个坑。1.2 let 和 const 引入了块级作用域与暂时性死区let 和 const 的出现补齐了块级作用域的能力。它们只在所在的块{}内生效同时也带来了另一个重要机制——暂时性死区TDZ。在变量声明之前的区域内访问该变量会直接抛 ReferenceErrorconsole.log(b); // ReferenceError: Cannot access b before initialization let b 20;TDZ 的设定看起来很“碍事”但它其实是在帮我们提早暴露问题。使用 var 时变量提升把错误掩盖了你只能在运行时读到 undefined然后各种判断逻辑可能因此偏离预期。而 let/const 的报错能督促我们养成“先声明后使用”的习惯。const 与 let 的唯一区别是const 声明时必须赋值且该绑定不能被重新赋值。注意我说的是绑定不能被重新赋值不是说值不可变。const 声明的对象其属性仍然可以修改const obj { name: hello }; obj.name world; // 正常执行 obj { name: world }; // TypeError: Assignment to constant variable在实际项目中我推荐默认使用 const只有当确实需要重新赋值时才用 let。这能让代码的意图更清晰看到 const 就知道这个变量在整个生命周期里不会重新指向别的值。1.3 作用域链自由变量的查找规则理解了声明方式就能更顺畅地理解作用域链。JavaScript 的作用域是词法作用域函数在定义时就确定了它的“外层环境”而不是在调用时确定。查找变量时引擎会从当前作用域开始逐层向外查找直到全局作用域沿途找到第一个匹配就停下来。我用一个简单的类比来解释作用域链就像一条向上回溯的绳子每个函数定义时都拽着一条指向其外层作用域的引用这个引用在函数创建时就已经定死了。const name global; function outer() { const name outer; function inner() { console.log(name); // 输出 outer } inner(); } outer();inner 函数内部没有定义 name于是沿着作用域链往外找找到 outer 函数作用域里的 name输出 outer。这里的关键是inner 定义在 outer 内部所以它的外层是 outer与调用位置无关。很多开发者会把词法作用域与动态作用域搞混。JavaScript 不关心函数从哪里被调用只关心函数在哪里被定义。这与 this 的绑定规则形成鲜明对比后面我们会单独展开 this 的问题。2. 闭包不是玄学从循环问题到内存释放的实战整理2.1 闭包的形成条件与本质闭包是 JavaScript 里最被误解的概念之一。很多人把它描述成“函数内部返回一个函数”但这只是表象。闭包的本质是一个函数在定义时捕获了其词法作用域中的变量引用即使这个函数在自己的词法作用域之外被调用依然能访问这些变量。形成闭包需要满足三个条件存在函数嵌套、内部函数引用外部函数的变量、内部函数在外部函数执行完之后仍被使用。当一个变量被闭包引用时它不会随着外部函数执行完就被垃圾回收而是继续活在内存里。function createCounter() { let count 0; return function() { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2createCounter 执行后按理说局部变量 count 应该被销毁。但因为返回的内部函数持有 count 的引用count 被保留下来这个内部函数连同它捕获的环境一起就是闭包。2.2 经典案例复盘一键解决循环闭包问题前面我们看到了 var 在循环里配合 setTimeout 翻车的情况。现在用闭包来解思路是给每次循环都创建一个独立的作用域副本for (var i 0; i 5; i) { (function(j) { setTimeout(() console.log(j), 100); })(i); } // 输出 0、1、2、3、4立即执行函数IIFE每次接收当前的 i把它作为参数 j 保存在自己的作用域里。setTimeout 回调捕获的是 j而不是循环变量 i于是每个回调拿到了不同的值。当然现在的代码已经不需要这么绕了直接用 let 声明循环变量就能解决问题。因为 let 在每次迭代时会创建一个新的绑定每个回调捕获的是该次迭代的独立绑定。2.3 闭包与内存何时需要主动释放闭包能保留变量的引用这也意味着它可能造成内存泄漏。最常见的泄漏场景是不必要地持有大型数据或者事件监听器没有被正确移除。比如一个组件在挂载时注册了全局事件监听内部使用了组件里的数据卸载时没有移除监听器那么组件虽然从 UI 上被销毁了但其闭包仍然被事件系统引用整个组件的数据都无法被回收。这种问题在单页应用里特别容易出现解决办法是始终在卸载阶段移除监听器或者使用 AbortController 控制事件信号。我处理过的一个真实排查经历某个页面在反复点击进入退出后内存占用持续攀升最终定位到是一张图表组件的 resize 监听器没有在销毁时解绑窗口每次变化都会触发那些已经离开的页面实例里的回调闭包里的旧数据越积越多。修复方式很简单在销毁时调用移除方法就行但排查过程花了不少时间。关于闭包还有一个常见疑问是不是“返回函数”就是闭包并非如此。只要内部函数引用了外部函数的变量且这个引用被保留形成闭包的条件就成立。比如function init() { let name hello; function display() { console.log(name); } window.showName display; // 把函数挂到全局闭包形成 }display 被赋给全局变量它引用了 init 里的 name这就是闭包尽管 init 没有返回任何东西。3. 异步编程的真相事件循环如何驱动单线程的 JavaScript3.1 单线程模型与调用栈JavaScript 是单线程语言意味着同一时刻只能执行一段代码。这个设计最初是为了简化浏览器环境下 DOM 操作的并发问题——如果两个线程同时修改同一个元素谁来合并结果干脆只用一个线程执行所有任务。既然是单线程为什么还能处理网络请求、定时器这类耗时操作而不阻塞页面答案是JavaScript 引擎本身只负责执行代码耗时任务由浏览器或 Node.js 的其他线程处理完成后通过消息队列通知主线程。核心的运行时模型由三部分组成调用栈Call Stack、任务队列Task Queue、微任务队列Microtask Queue。同步代码在调用栈中后进先出地执行而异步任务在条件满足后被放入队列等待栈清空。3.2 宏任务与微任务的执行顺序理解事件循环最重要的是分清宏任务和微任务。宏任务包括整体脚本代码、setTimeout、setInterval、I/O 操作、UI 渲染等。微任务包括Promise 的 then/catch/finally 回调、queueMicrotask 注册的回调等。事件循环的规则是每轮循环从宏任务队列里取出一个宏任务执行执行完后再把整个微任务队列清空然后进入下一轮。微任务队列中一旦有新任务产生会继续在当前清空阶段执行直到队列为空。看这段代码console.log(script start); setTimeout(() { console.log(timeout); }, 0); Promise.resolve().then(() { console.log(promise1); }).then(() { console.log(promise2); }); console.log(script end);执行顺序是script start - script end - promise1 - promise2 - timeout。因为整体脚本本身就是一个宏任务先同步打印 script start 和 script endPromise 的 then 回调作为微任务进入微任务队列。脚本这个宏任务执行完后事件循环清空微任务队列依次输出 promise1、promise2。然后才取出 setTimeout 注册的宏任务执行。很多面试题喜欢在这种基础上再叠加多层嵌套 setTimeout 或 Promise但核心判断依据永远只有一个当前宏任务结束后优先清空整个微任务队列然后再取下一个宏任务。3.3 async/await 不是魔法只是 Promise 的语法糖async 函数总是返回一个 Promise。await 会阻塞当前 async 函数内部的后续代码但不会阻塞外部代码。它的底层是生成器加 Promise 的封装把“等待 Promise 完成后再继续执行”的流程视为同步写法的形态。看一个容易踩坑的例子async function test() { console.log(start); await Promise.resolve(); console.log(end); } console.log(before); test(); console.log(after);输出顺序是before - start - after - end。原因在于test() 被调用时函数体同步执行到第一个 await 处打印 start。遇到 await 后函数让出控制权返回一个 Promise外层的脚本继续执行打印 after。此时微任务队列中已经有恢复 test 函数的任务等当前栈清空后执行打印 end。我见过不少同事以为 await 是“等待结果后才继续”于是把整段逻辑都当成同步忽略了外部代码不受阻塞这个事实。理解 async/await 的关键就是await 是放弃当前函数对栈的控制权同时注册一个微任务等栈空了再回来继续跑。3.4 实际排查经验定时器不准的问题怎么定位某次线上页面有个倒计时功能统计用户操作时长总是偏慢。最终定位到的原因是 setTimeout 的延迟时间并不能保证精确。因为事件循环的前面可能排着耗时的同步任务或者大量微任务setTimeout 的回调必须等栈空才能执行。倒计时功能更适合用时间戳差值来计算const startTime Date.now(); setInterval(() { const elapsed Date.now() - startTime; updateDisplay(elapsed); }, 1000);每次更新时都拿当前时间减去开始时间而不是累加固定的间隔。这样即使某个周期的回调被延迟显示的时间依然是真实流逝的时间。更精确的方案是用performance.now()它返回从页面导航开始到当前时刻的高精度时间并且避免了系统时间调整的影响。对于需要精细计时的应用这个 API 比 Date.now() 更可靠。4. this 的四条绑定规则与箭头函数如何改变游戏规则4.1 默认绑定与隐式绑定this 是 JavaScript 里最容易让人困惑的概念之一因为它不是在函数定义时确定的而是在函数调用时根据调用方式确定的。也就是说this 的指向由“函数如何被调用”决定。默认绑定发生在不带任何修饰地调用普通函数时。非严格模式下this 指向全局对象严格模式下this 是 undefined。function showThis() { use strict; console.log(this); } showThis(); // undefined隐式绑定发生在对象方法调用时this 指向该对象const obj { name: obj, say: function() { console.log(this.name); } }; obj.say(); // obj这里的重点在于this 指向的是调用时点左侧的对象。如果把方法单独取出来再调用this 就会丢失const fn obj.say; fn(); // undefined严格模式下因为调用时没有对象上下文这个现象在把方法作为回调传入其他函数时极其常见比如button.addEventListener(click, obj.say)事件触发时 this 指向按钮而不是 obj。4.2 显式绑定与 new 绑定使用 call、apply、bind 可以显式指定 thisfunction greet() { console.log(Hello, ${this.name}); } greet.call({ name: alice }); // Hello, alice greet.apply({ name: bob }); // Hello, bob const boundGreet greet.bind({ name: carol }); boundGreet(); // Hello, carolcall 和 apply 的差别只在传参方式call 使用参数列表apply 使用数组。bind 则不一样它返回一个新函数新函数内部的 this 被永久绑定为传入的对象。new 绑定也很特殊。当函数被 new 调用时JavaScript 引擎会创建一个新对象把这个新对象作为 this 传入函数如果函数没有显式返回对象类型则 new 表达式返回这个新对象。注意new 绑定的优先级高于显式绑定function Animal(name) { this.name name; } const dog new Animal(dog); console.log(dog.name); // dog如果先 bind 再 newthis 仍然指向新创建的对象bind 的绑定会被忽略。4.3 箭头函数的“去 this 化”箭头函数没有自己的 this它会捕获定义时所在上下文的 this 值。它不能作为构造函数使用也不能用 call/apply/bind 改变 this。这里有一个值得一提的细节箭头函数的 this 是词法性的和普通函数在调用时确定 this 完全不同。看这个代码const obj { name: obj, sayLater: function() { setTimeout(() { console.log(this.name); }, 100); } }; obj.sayLater(); // obj箭头函数在定义时捕获了 sayLater 的 this而 sayLater 在调用时以 obj 方法的形式被调用this 指向 obj所以箭头函数内部访问 this.name 拿到的是 obj。如果这里不用箭头函数改用普通函数结果就会变成 undefined因为 setTimeout 回调执行时函数是在全局上下文里被调用的this 不再指向 obj。箭头函数非常适用于回调场景它避免了var self this或bind(this)的繁琐写法。但它不合适用作对象的方法定义因为此时 this 不会指向对象而是指向外层作用域。4.4 优先级总结与防坑建议优先级从低到高分别是默认绑定 隐式绑定 显式绑定 new 绑定。箭头函数的 this 独立于这些规则。在实际开发中唯一需要警惕的是回调函数里的 this。我给团队定的一条规矩是回调函数凡是需要访问外层 this 的一律用箭头函数普通方法凡是需要动态 this 的不要用箭头函数否则还容易出 bug。5. 原型链与继承new 的背后和 class 的实质5.1 prototype、constructor 与proto的三角关系每次用 new 调用构造函数时JavaScript 引擎做了四件事创建新的空对象、将这个对象的原型指向构造函数的 prototype 属性、将构造函数内部的 this 绑定到该对象并执行、判断构造函数是否有显式返回对象。function Person(name) { this.name name; } const p new Person(tom); console.log(p.name); // tom console.log(p.__proto__ Person.prototype); // true实例对象 p 内部有一个__proto__指针指向 Person.prototype。访问 p 上不存在的属性时引擎会顺着__proto__链一直往上找直到 Object.prototype再往上就是 null。这条链就是原型链。Person.prototype 自身也有一个 constructor 属性指向 Person 函数形成一个闭环。5.2 基于原型链的继承演进最传统的方式是原型链继承function Student(name, grade) { Person.call(this, name); this.grade grade; } Student.prototype Object.create(Person.prototype); Student.prototype.constructor Student;关键一步是Object.create(Person.prototype)它创建一个新对象其原型是 Person.prototype同时保留自己的 constructor 指向 Student。如果不做这一步把 Student.prototype 直接赋值为 Person.prototype那么 Student 和 Person 的实例方法会完全共享后续修改 Student.prototype 会影响 Person 的实例。ES6 引入了 class 语法底层依然是基于原型链实现的只是写法上更接近传统面向对象语言class Student extends Person { constructor(name, grade) { super(name); this.grade grade; } }extends 相当于把 Student.prototype 的原型指向 Person.prototypesuper 则负责调用父类构造函数来初始化 this。注意在子类的构造函数里访问 this 之前必须先调用 super这是语法强制要求原因在于父类构造函数负责创建并初始化 this 对象。5.3 实战中的原型链误区一个容易踩的坑是使用Array.prototype.includes等方法时误以为数组自身拥有这些方法。实际上数组实例通过原型链从Array.prototype继承了这些方法数组本身只存储元素。另一个常见的误区是扩展原生原型对象比如直接给Object.prototype添加方法。这种做法在多人协作的项目里会引发不可预期的冲突而且所有对象的属性遍历都可能受到影响强烈不建议在生产代码中这样做。理解了原型链很多“莫名奇妙”的报错就能解释了。比如一个对象调用 toUpperCase 报错就是因为该对象不是字符串不存在这个属性顺着原型链找遍了也找不到。6. 数组与对象的实用技巧解构、展开与现代工具方法6.1 解构赋值的第一性原理解构赋值本质上是“模式匹配”把数组或对象中的属性/元素按照对应的模式提取到变量中。它不只是语法糖还支持默认值、嵌套解构和剩余元素收集。数组解构按位置匹配const [first, second 10, ...rest] [1, undefined, 3, 4]; console.log(first); // 1 console.log(second); // 10因为 undefined 触发默认值 console.log(rest); // [3, 4]注意默认值只在数组元素严格等于 undefined 时生效null 不会触发默认值。对象解构按属性名匹配const obj { name: demo, info: { age: 3 } }; const { name, info: { age }, extra default } obj; console.log(name); // demo console.log(age); // 3 console.log(extra); // default解构在函数参数的处理上尤其好用可以给复杂配置对象提供默认值层次结构让函数签名一目了然。6.2 展开运算符的浅拷贝边界展开运算符...能简化数组和对象的合并复制操作但它只做一层浅拷贝。也就是说嵌套的对象和数组仍然是引用关系const original { list: [{ id: 1 }] }; const copy { ...original }; copy.list.push({ id: 2 }); console.log(original.list.length); // 2说明共享了同一个数组引用如果需要对多层结构做深拷贝可以使用 structuredClone 方法它比 JSON.parse(JSON.stringify(...)) 更可靠能够正确处理 Date、Map、Set 等类型并且不会丢 undefined 和函数JSON 方案会把它们忽略掉。6.3 数组方法的选择逻辑数组的现代方法非常多我在实际项目里的选择逻辑是需要根据条件转换数组且长度不变用 map需要筛选出符合条件的子集用 filter需要遍历但不关心返回值用 forEach需要把数组归约为单值用 reduce需要找到第一个符合条件的元素用 find需要判断是否全部/部分元素满足条件用 every/some。reduce 是很多开发者最容易绕晕的方法但它的威力确实大。看一个典型的对象分组场景const people [ { name: a, age: 20 }, { name: b, age: 20 }, { name: c, age: 30 } ]; const grouped people.reduce((acc, person) { (acc[person.age] || []).push(person); return acc; }, {}); console.log(grouped); // { 20: [{name:a,age:20}, {name:b,age:20}], 30: [{name:c,age:30}] }reduce 的第一个参数是累积器第二个参数是当前元素。每次迭代都必须返回累积器的更新结果否则下一轮就拿不到之前的数据。这种模式在做数据转换、统计、分组时非常高效。6.4 可选链与空值合并的配合使用访问深层对象属性时最怕的是中间某一层为 null 或 undefined。以前要层层判断现在可以用可选链操作符?.const data { user: null }; const name data.user?.profile?.name; // 返回 undefined不报错?.还可以用于方法调用和索引访问但要注意它只在左侧为 null/undefined 时短路不区分 false、0 或空字符串。空值合并操作符??与||的区别也值得理清。??只在左侧为 null 或 undefined 时返回右侧值而||会在左侧为任意假值时返回右侧值。如果某个配置项的默认值是 0 或空字符串用||就会把合法值覆盖成默认值const config { retryCount: 0 }; const count1 config.retryCount || 5; // 5不是预期 const count2 config.retryCount ?? 5; // 0符合预期这是我在项目里进行配置项默认值处理时总结出来的经验凡是可能合法的空值、0、空字符串默认值逻辑统一用??||只用于布尔语义的兜底。7. 原生应用的性能细节减少垃圾回收压力与优化实践7.1 避免不必要的对象创建JavaScript 的自动垃圾回收虽然省心但创建和销毁对象的成本并不为零。在某些高频执行的逻辑里频繁创建临时对象会造成 GC垃圾回收压力导致应用出现卡顿。一个典型的场景是循环里拼接字符串每次使用模板字符串都会产生新的字符串但字符串拼接本身在引擎层面有优化问题不大真正需要注意的反而是在高频渲染或动画循环里创建闭包、数组和对象。更好的做法是尽量复用对象结构或者使用函数式的方法链减少中间变量的产生。V8 引擎对代码的优化程度与代码形态有关保持对象形状一致每次属性顺序一致有利于隐藏类的优化。7.2 事件委托减少监听器数量页面上有大量元素需要绑定事件时逐个绑定会创建大量监听器既占用内存又影响初始化的性能。事件委托把监听器挂到共同的祖先元素上利用事件冒泡机制统一处理document.querySelector(#list).addEventListener(click, (e) { const target e.target.closest(.item); if (!target) return; console.log(target.dataset.id); });这里用closest找到实际的点击目标如果点击在元素内部也能正确捕获。事件委托不仅减少了监听器数量还让动态插入的元素自动拥有事件能力不需要新元素绑定。7.3 防抖与节流的区别防抖debounce和节流throttle是前端性能优化中两个高频使用的技术。很多人分不清楚我提供一个记忆方法防抖关注“事件停止后执行一次”节流关注“固定时间内最多执行一次”。防抖适合输入框实时搜索function debounce(fn, delay 300) { let timer null; return function(...args) { clearTimeout(timer); timer setTimeout(() fn.apply(this, args), delay); }; }每次输入都会重置定时器只有当用户停止输入超过 delay 毫秒后函数才会执行。节流适合滚动事件、拖拽、窗口尺寸调整function throttle(fn, interval 200) { let last 0; return function(...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }第一次触发会立即执行后续只有在超过间隔时间后才再次执行。我在实际项目中还注意过闭包在这些优化技巧里保存了 timer 的状态所以每次生成的防抖/节流函数必须是单例的避免重复创建导致状态丢失。8. 常见坑位汇总写给自己的排错清单8.1 比较运算中的类型转换与的区别是面试常客但实际开发中真的能引发问题。会做类型转换比如1 1为 true。这种隐式转换可能导致判断结果完全不符合预期。我自己的习惯是绝大多数情况下只用只有判断某个值是不是 null 或 undefined 时才用 null这种写法因为它同时覆盖两种情况。除此之外不用。8.2 浮点数精度问题JavaScript 的浮点数基于 IEEE 754 标准0.1 0.2 的结果不等于 0.3。这在金额计算、百分比统计等场景中很致命。解决方案涉及金额的计算优先用整数以“分”为单位计算避免浮点数运算必须用浮点数时通过toFixed控制显示精度。这里有个需要注意的细节toFixed返回的是字符串需要转换回数字且某些特殊数值的舍入行为与直觉不同。8.3 作用域链性能不要制造过深的链虽然作用域链的查找在现代引擎中做了大量优化但过深的作用域链仍然会降低访问速度更重要的是它让代码的可读性严重下降。我建议一个函数不要嵌套超过两层尽量把嵌套逻辑提取成命名函数。这既是为了性能也是为了让代码不至于变成“洋葱型”。8.4 模块化与命名空间从全局变量满天飞到 CommonJS、ES ModuleJavaScript 的模块化演进非常明显。ES Module 的import/export是目前的主流方案它的静态分析特性让打包工具能够做 tree-shaking只保留实际用到的代码。在写模块时有一个容易忘记的细节是循环依赖问题。两个模块互相引用彼此在某些执行顺序下会导致某个模块在初始化前就被访问从而出现Cannot access before initialization的报错。解决方案是调整模块的依赖方向把共享状态抽到独立模块或者改用动态import()延迟加载。9. 关于调试与工具链的几点心得9.1 善用 console 的升级用法console.log很多人都会用但调试复杂结构时console.table能够把对象数组渲染成表格视觉效果直观很多。console.time和console.timeEnd可以快速测速console.trace打印当前调用栈在定位“这个函数是谁调进来的”这个问题时非常有效。我排查问题时习惯先console.trace再看堆栈有时候问题的根源根本不在当前函数内部而是调用者的执行顺序有误。9.2 浏览器 DevTools 的断点调试断点调试和 console 输出相比的优势在于可以在任何作用域位置暂停实时查看闭包中变量的值甚至修改它们。我调试异步问题时经常在 Promise 的 then 回调里打断点配合 Sources 面板的 Call Stack 区域查看完整调用链。必要时还可以使用条件断点避免特定值出现时才断住减少调试干扰。9.3 构建工具对代码的影响现代前端工程几乎离不开构建工具。了解打包器的工作原理对写出高效的 JavaScript 有直接帮助。比如 tree-shaking 依赖 ES Module 的静态结构如果代码里使用require或者副作用较多的模块tree-shaking 效果就会大打折扣。有一个细节是ES Module 的import * as ns from ./x.js在打包时不如明确具名导入效果好。因为打包器要保留整个目标模块的导出可能无法删除多余代码体积会变大。我会尽量使用具名导入既清晰又能优化打包体积。10. 坚持整理笔记的意义写这篇 JavaScript 笔记的过程其实也是我重新审视自己的过程。很多概念在项目中用过无数遍但真正把原理写下来的时候才发现有些认识是模糊的。比如事件循环的微任务清空时机我在纸上推导和实际调试验证之后才算真正理解。如果你也在学习 JavaScript我建议你也做同样的事把每一个知识点用自己的话写一遍配合最小可运行示例验证再想想它和实际开发中的什么场景相关。这个过程本身比看十篇教程都有效。遇到不确定的地方打开 DevTools 实测理由和结论要有证据支撑。这些基础概念虽然不是新东西但它们是所有框架、工具链、性能优化的地基。地基稳了上面盖什么楼都踏实。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询