news 2026/10/13 17:22:06

JS进阶三硬核:闭包、原型链与事件循环全面解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS进阶三硬核:闭包、原型链与事件循环全面解析

今天是自学路线的第13天,正好也是进阶专题的第三篇。前12天我从零开始,依次过完了HTML/CSS布局、JavaScript基础语法、DOM事件处理,进阶前两篇则分别啃了作用域与函数式编程。Day 13 进阶3给自己定的主题是三块公认的硬骨头:闭包、原型链、事件循环。说实话,这三块在各类面试题里出现率极高,但在日常写页面时又容易被一带而过,属于典型的“不考不知道,一考全露馅”。这篇笔记把这三大块彻底讲透,配了可直接运行的代码和练习,适合刚学完JS基础准备进阶的读者,也适合想系统复习一遍的开发者。

1. Day 13 进阶3的学习定位:为什么它是整个入门阶段的关键转折

进阶这个系列不是随便定的,它是把JavaScript从“会用”推向“理解”的必经之路。Day 13 进阶3之所以重要,是因为闭包、原型链、事件循环这三块内容恰好覆盖了JS语言最底层的三条主线:函数与作用域的关系、对象与继承的实现机制、异步代码的执行调度。只要这三条线通了,后续看框架源码、写组件封装、排查线上问题都会顺手很多。

1.1 前12天的路线复盘:进阶是搭在基础上的

先把我前面12天的安排完整摊开,这样你能理解为什么进阶3敢直接上这些硬核内容。我的学习路线不是按教科书章节排的,而是按“先用起来,再理解原理”的顺序来的。

天数主要内容掌握程度
Day 1-3HTML基础标签、CSS选择器、盒模型、Flex布局能独立还原静态页面
Day 4-6CSS Grid、响应式布局、动画过渡能写出自适应页面
Day 7-9JS变量、数据类型、运算符、流程控制、函数基础能写简单交互逻辑
Day 10DOM操作、事件监听、表单处理能完成常见页面交互
Day 11进阶1:作用域、变量提升、暂时性死区能解释代码执行顺序
Day 12进阶2:高阶函数、柯里化、函数式思想能写出复用性更高的函数
Day 13进阶3:闭包、原型链、事件循环能解释框架底层机制

Day 11 讲作用域的时候,我发现很多代码问题是“变量莫名其妙被改了”或者“循环里拿不到想要的索引”,这类问题最终都指向闭包。Day 12 讲高阶函数时,柯里化和函数工厂本质上也在利用闭包保存状态。所以闭包不是凭空冒出来的概念,它是前面作用域知识的自然延伸。

原型链则承接了“对象”这个主线。前7天我只知道用对象字面量存数据,遇到需要多个相似对象时只会复制粘贴。进阶3里会把“构造函数、原型、实例”这三者的关系彻底讲清楚,让对象创建和继承从“复制”变成“委托”。

事件循环是Day 13里最偏“运行时”的一块。前面写异步代码时,只知道用setTimeout或者事件回调,但为什么setTimeout的延迟时间不准、为什么Promise排在setTimeout前面执行,这些都需要看事件循环的调度规则。

1.2 进阶3三个主题的内在联系:语法骨架与运行骨架

很多人把这三大块当独立知识点记,其实它们之间有非常强的联动关系。

先看闭包和原型链:闭包是函数与作用域的组合,原型链是对象与对象之间的链接。JS里的函数本身就是对象,所以一个函数既可以通过闭包保存变量,也可以作为构造函数创建实例,还能被挂到原型上被其他对象共享。这两者在写类、写模块、做封装时经常同时出现。

再看原型链与事件循环:浏览器里的DOM事件机制依赖对象体系。比如事件对象、事件目标,它们都是通过原型链继承来的。而事件回调是异步执行的,回调里的this指向、参数捕获,都和闭包、原型链直接相关。

我给Day 13定了一个总目标:用闭包保护状态,用原型链组织对象关系,用事件循环理解异步触发时机。三个知识点合在一起,才是JS这门语言的完整运行图景。

2. 闭包:先解决一个真实问题,再背定义

闭包的定义网上有一堆,但我建议你别急着背“函数返回函数”“内部函数引用外部变量”这类结论。先看它到底解决什么问题,定义自然就懂了。

2.1 从一段最朴素的计数器说起

假设你要做一个计数器,每点一次按钮数字加一。初级写法通常是这样:

let count = 0; function increment() { count++; return count; } console.log(increment()); // 1 console.log(increment()); // 2

这个写法能跑,但count是全局变量。项目一旦复杂,任何地方都能修改count,你根本不知道它是在哪一步被改坏的。更专业的做法是把count藏起来,只暴露increment函数:

function createCounter() { let count = 0; return function increment() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2

这里createCounter执行完后,局部变量count按理说应该被回收。但实际上increment函数还在使用count,所以JS引擎会把这个变量保留在increment函数的“背包装备”里。这个组合就叫闭包:一个函数加上它创建时所在的作用域。

关键点在于:increment不仅能访问count,而且每次调用它,访问的都是同一个count。如果你再创建一个counter2,它会拥有独立的count。这种“一个函数一份私有状态”的能力,正是闭包最大的价值。

2.2 闭包的生活类比:每个函数都背着一个小背包

我常用一个类比来帮助理解闭包:把函数想象成一个员工,函数声明时所在的上下文是一个工位。工位上的抽屉(局部变量)在员工离开后会清空,但如果这个员工手里握着一把抽屉钥匙,并且把这个钥匙封装进了自己的技能里,那不管他走到哪,都能打开那个抽屉。

闭包就是这把“钥匙”。它让一个函数在离开创建环境之后,依然能访问创建环境里的变量。

这个类比还能解释一个常见的注意点:闭包保存的不是变量的“快照”,而是变量的“引用”。换句话说,如果闭包引用的变量值后来变了,闭包里拿到的也是新值,不是旧值。这个特性在循环里最容易被坑到。

2.3 闭包的三个高价值应用场景:封装、柯里化、循环捕获

闭包最常见的应用场景有三个,每一个都值得手写一遍。

第一个是数据私有化。借助闭包,你可以实现类似其他语言中“private”的效果。上面计数器的例子就是最简单的私有状态。更进一步,可以把多个方法包进同一个闭包,形成一个模块:

function createUser(name) { let _name = name; return { getName: function () { return _name; }, setName: function (newName) { _name = newName; } }; } const user = createUser('小明'); console.log(user.getName()); // 小明 user.setName('小红'); console.log(user.getName()); // 小红

外部无法直接访问_name,只能通过getName和setName操作,这就保证了数据的安全边界。

第二个是柯里化。柯里化指把多参数函数转换成一系列单参数函数。它本质上也是在用闭包保存参数:

function multiply(a) { return function (b) { return a * b; }; } const double = multiply(2); console.log(double(5)); // 10 console.log(double(8)); // 16

double函数一直“记得”a等于2,这全靠闭包在背后支撑。

第三个是循环中的变量捕获。这是实际开发里踩得最多的坑:

for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 100); }

这段代码的输出是3、3、3,而不是0、1、2。原因是setTimeout的回调函数在100毫秒后执行,那时循环早已结束,i的最终值是3,所有回调共享同一个i。用闭包可以修复:

for (var i = 0; i < 3; i++) { (function (num) { setTimeout(function () { console.log(num); }, 100); })(i); }

每次循环执行IIFE时,把当前的i作为参数num传入,闭包保存的是每个回合的num副本。这样输出就是0、1、2。当然现在用let也能直接解决,但理解闭包捕获机制依然是排查这类问题的基本功。

2.4 闭包的坑与注意点:不是所有闭包都要用

闭包好用,但代价也要说清楚。

第一,闭包会让变量长期驻留内存。因为外部函数执行完后,内部函数还持有外部变量的引用,垃圾回收器不会回收这块内存。滥用闭包,尤其在循环里大量创建闭包,会导致内存占用上升。我在写代码时,会先问自己:这里真的需要保存状态吗?如果只需要一次性的逻辑,就没必要用闭包。

第二,闭包保存的是引用而不是值。如果闭包指向一个对象,对象属性被修改,闭包读到的也是修改后的结果。这不是bug,而是预期行为,但新手容易在对象配置、事件监听时理解偏差。

第三,事件监听器里使用闭包要小心。如果给DOM元素绑定了闭包回调,但移除元素前没有解绑,回调会一直持有元素和相关变量的引用,造成“游离DOM”内存泄漏。页面长列表频繁增删节点时尤其明显。

我个人的做法是:闭包优先用于模块封装和参数预置,避免在循环体内做不必要的闭包嵌套。需要动态取索引时,优先用let声明循环变量,既清爽又安全。

3. 原型链:对象继承不是复制,而是委托

再来看第二个硬骨头:原型链。很多初学者把JS的继承理解成“复制父类的方法到子类”,这个方向从头就偏了。JS的对象继承机制不是复制,而是委托。

3.1 “继承”的真相:找不到属性就去问原型

抛开书上的术语,用日常逻辑解释。

当你访问一个对象的属性时,JS引擎先在这个对象自身找。如果找不到,它会顺着对象的原型对象接着找。原型对象也可能有自己的原型,这样一层层往上走,直到某层的原型为null。这条链路就是原型链。

举个例子:

const person = { name: '小明', greet: function () { console.log('你好,我是' + this.name); } }; const student = Object.create(person); student.name = '小红'; student.greet(); // 你好,我是小红

student自身有name属性,但greet方法在student身上找不到,于是顺着原型链去person对象上找,找到了greet就调用。这里student没有复制greet,它只是“委托”person来响应greet这个请求。这就是委托式继承的核心。

3.2 构造函数、原型、实例三者的关系

实际项目里不会老用Object.create,更多是用构造函数加new关键字创建对象。这时候会涉及三个角色:构造函数、原型对象、实例对象。

function Person(name) { this.name = name; } Person.prototype.greet = function () { console.log('你好,我是' + this.name); }; const p1 = new Person('小明'); const p2 = new Person('小红'); p1.greet(); // 你好,我是小明 p2.greet(); // 你好,我是小红 console.log(p1.greet === p2.greet); // true

这里有几个关键结论:

  • Person.prototype是原型对象,它身上定义的greet方法会被所有实例共享。
  • p1和p2自身没有greet方法,但它们通过原型链都能访问到Person.prototype.greet,所以两个实例调用的是同一个函数。
  • 实例有一个内部指针指向构造函数的prototype对象,这个指针在浏览器控制台里一般显示为[[Prototype]],在代码里可以通过Object.getPrototypeOf()来获取。
  • 构造函数本身有一个prototype属性,而实例没有这个属性,只有指向原型的内部指针。

用表格可以把这三者的关系列得更直观:

角色如何访问原型相关内容
构造函数通过fn.prototype访问它创建实例时使用的原型对象
原型对象通过protoObj.constructor指回构造函数
实例对象通过Object.getPrototypeOf(instance)访问它的原型

3.3 手写继承的几种方式对比:组合继承和Class的区别

理解了原型与实例的关系后,就能看懂各种继承写法了。这里我不啰嗦每一种的历史细节,直接对比几种主流方式的优缺点。

第一种是经典的原型链继承:

function Animal(name) { this.name = name; } Animal.prototype.eat = function () { console.log(this.name + '在吃东西'); }; function Dog(name) { this.name = name; } Dog.prototype = new Animal(); // 让Dog的原型指向Animal实例 Dog.prototype.constructor = Dog; const dog = new Dog('旺财'); dog.eat(); // 旺财在吃东西

这种方式的问题是:所有Dog实例共享同一个Animal实例里的引用类型属性。如果Animal的实例属性里有数组,修改一个Dog实例的数组,其他Dog实例也会看到变化。

第二种是组合继承,即构造函数继承加原型链共享:

function Animal(name) { this.name = name; this.foods = []; } Animal.prototype.eat = function () { console.log(this.name + '在吃' + this.foods.join(',')); }; function Dog(name) { Animal.call(this, name); // 每个实例都有一份自己的引用类型属性 } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; const dog1 = new Dog('旺财'); dog1.foods.push('骨头'); const dog2 = new Dog('来福'); console.log(dog2.foods); // [],互不影响

组合继承是ES6之前最常用的可靠方案,解决了引用类型属性共享问题,同时保留了方法共享的能力。

第三种是ES6的class语法:

class Animal { constructor(name) { this.name = name; this.foods = []; } eat() { console.log(this.name + '在吃' + this.foods.join(',')); } } class Dog extends Animal { constructor(name) { super(name); } } const dog1 = new Dog('旺财'); dog1.foods.push('骨头'); const dog2 = new Dog('来福'); console.log(dog2.foods); // []

class语法是组合继承的语法糖,写法更清晰,但底层机制依然是原型链委托,而不是真正的类复制。理解原型链再看class,才能明白extends关键字和super的调用背后到底发生了什么。

3.4 项目里的原型链技巧:判断、枚举、扩展

除了写继承,原型链在代码排查中也很常用。

判断某个属性是实例自身的还是原型上的,用hasOwnProperty:

function Person(name) { this.name = name; } Person.prototype.greet = function () {}; const p = new Person('小明'); console.log(p.hasOwnProperty('name')); // true console.log(p.hasOwnProperty('greet')); // false

判断一个对象是谁的实例,用instanceof:

console.log(p instanceof Person); // true console.log(p instanceof Object); // true

instanceof的原理其实就是顺着原型链找构造函数的prototype对象。如果找到就返回true,找不到返回false。所以深入理解原型链后,instanceof的结果一眼就能看出来。

还有一个重要注意点:不要轻易给内置对象的原型添加方法。比如给Array.prototype加一个myUnique方法,在小型Demo里很爽,但会污染全局环境,和其他库冲突的风险很高。如果非要在项目里做数组去重这类操作,优先写成独立工具函数,或者用class继承内置类型再扩展,而不是直接修改原型。

4. 事件循环:JS异步执行的调度真相

闭包和原型链处理的是“写代码”的层面,事件循环处理的是“代码跑起来”的层面。我学到这里的时候,才真正明白为什么setTimeout不可靠,为什么Promise回调不是立刻执行。

4.1 单线程的JS如何异步:先看整体调度逻辑

JS运行时是单线程的,这意味着同一时刻只能执行一段代码。但浏览器里有很多事不能干等,比如请求接口、点击事件、定时器,这些不能阻塞主线程。于是运行时引入了事件循环机制。

可以把整个执行过程抽象成三个角色:调用栈、任务队列、事件循环。

调用栈是当前正在执行的任务队列。函数调用时进栈,函数返回时出栈。任务队列里放的是等待执行的回调任务。事件循环是一个永不停止的循环,它的职责就是不断检查调用栈是否为空,如果为空,就从任务队列里取一个任务放进调用栈执行。

当代码执行到setTimeout或者发起网络请求时,这类操作不会原地等待,而是交给浏览器的其他模块去计时或通信。计时结束或请求返回后,对应的回调会被放进任务队列。主线程的调用栈一空,事件循环就把回调取出来执行。

这样解释就能理解为什么计时器不准:setTimeout只是把回调“排进队列”,实际执行时间取决于队列前面还有多少任务、每个任务执行了多久。

4.2 宏任务与微任务:执行顺序的完整规则

任务队列里其实还分两类:宏任务和微任务。这个区分非常关键。

宏任务包括:同步代码整体、setTimeout、setInterval、I/O操作、UI渲染。微任务包括:Promise的then/catch/finally回调、queueMicrotask、MutationObserver。

执行规则是:每执行完一个宏任务,事件循环会清空整个微任务队列,然后再执行下一个宏任务。注意,是清空整个微任务队列,而不是只执行一个。

看一个经典示例:

console.log('1'); setTimeout(function () { console.log('2'); }, 0); Promise.resolve().then(function () { console.log('3'); }); console.log('4');

输出结果是1、4、3、2,而不是1、2、3、4。推演过程是这样的:

  • 同步代码依次执行,输出1和4。
  • setTimeout把回调作为宏任务排进队列。
  • Promise的then把回调作为微任务排进队列。
  • 同步代码全部执行完,调用栈清空。
  • 事件循环先处理微任务队列,输出3。
  • 微任务队列清空后,才处理宏任务队列,输出2。

这个规则在实际开发中非常重要。比如你用Promise封装了一个接口请求,后续的then回调会在当前宏任务结束前优先执行,而不是等到所有宏任务都跑完。

再看一个微任务产生微任务的例子:

Promise.resolve().then(function () { console.log('A'); Promise.resolve().then(function () { console.log('B'); }); }).then(function () { console.log('C'); });

输出顺序是A、B、C。第一个then执行时又触发了新的微任务,因为当前还在“清空微任务队列”阶段,所以新产生的微任务也会在本轮内被执行。这也是agent task源的一个核心特性,深入调试异步逻辑时会经常用到。

4.3 Promise与async/await:更顺手的异步写法

Promise是微任务的主要来源,async/await则是Promise的语法糖。这两个必须放在一起理解。

先看Promise链条:

function fetchData(ms) { return new Promise(function (resolve) { setTimeout(function () { resolve('数据已返回'); }, ms); }); } fetchData(1000) .then(function (msg) { console.log('第一次获取:', msg); return fetchData(500); }) .then(function (msg) { console.log('第二次获取:', msg); }) .catch(function (err) { console.error(err); });

这里then里返回一个新Promise,后续的then就会等这个Promise完成后才执行,形成了串行异步流程。

同样的逻辑用async/await写更直观:

async function main() { try { const msg1 = await fetchData(1000); console.log('第一次获取:', msg1); const msg2 = await fetchData(500); console.log('第二次获取:', msg2); } catch (err) { console.error(err); } } main();

await就是“暂停当前异步函数,等待Promise完成”,但它并没有阻塞主线程。这个暂停只是把async函数后续代码包装成微任务继续执行。所以async/await写起来像同步代码,本质依然是异步的。

我做并发请求时有一个习惯:没有依赖关系的请求,不要傻傻地一个个await,而是用Promise.all并行。只有前后依赖时才用串行await。这样能明显减少总等待时间。

async function loadParallel() { const [userInfo, listData] = await Promise.all([ fetchData(800), fetchData(1200) ]); console.log(userInfo, listData); }

4.4 长任务与渲染卡顿的实战建议

理解了事件循环,还要理解它对页面渲染的影响。浏览器渲染新帧是有节奏的,比如60Hz屏幕大约每16.7毫秒渲染一帧。如果主线程连续执行一个超长任务,比如一次处理上万条数据,渲染就会被卡住,页面出现肉眼可见的掉帧和卡顿。

常见的解决方案是把长任务拆分成多个短任务。比如用setTimeout片段处理大数组:

function processChunk(items, chunkSize, handler) { let index = 0; function next() { if (index >= items.length) { return; } const end = Math.min(index + chunkSize, items.length); for (; index < end; index++) { handler(items[index]); } setTimeout(next, 0); // 把下一块任务排入宏任务队列 } next(); }

每次处理完一小块数据,就主动让出主线程,让浏览器有机会渲染。虽然整体耗时可能变长,但页面不会卡死。另一个思路是用requestAnimationFrame和任务分片,或者用Web Worker把计算挪到独立线程。不管用哪种,前提都是理解事件循环的调度时机。

5. 进阶3的落地练习:三个能直接跑起来的小实验

光看不练等于白看。Day 13 进阶3我给自己布置了三个练习,每个都综合了当天的知识。你照着跑一遍,理解深度完全不一样。

5.1 实验1:用闭包加原型链封装一个带私有状态的任务队列

这个练习的目标是做一个只能在队列尾部添加任务、从头部取出任务的小工具。队列数据不能被外部直接修改。

function createTaskQueue() { let tasks = []; // 闭包保护的私有状态 const queue = { add(task) { tasks.push(task); return queue; }, take() { return tasks.shift(); }, size() { return tasks.length; } }; return queue; } const queue = createTaskQueue(); queue.add('写文章').add('写代码').add('调试'); console.log(queue.size()); // 3 console.log(queue.take()); // 写文章 console.log(queue.size()); // 2 console.log(queue.tasks); // undefined,外部拿不到真实数组

这里闭包负责隐藏tasks数组,对象方法负责暴露操作接口。如果你试了直接访问queue.tasks,会发现结果是undefined,因为tasks根本不在queue对象上。

在此基础上,你可以给queue对象挂一个自定义方法,再通过原型链共享给多个队列实例。比如用工厂函数配合Object.create,或者直接用class重写。我自己会再写一版class,对比两遍之后对“类就是构造函数的语法糖”这句话的体会特别深。

5.2 实验2:手写一个简单的并发控制器

进阶到最后,事件循环的知识要能用起来。这个练习实现一个并发控制器,限制同一时间最多执行两个异步任务。

function createConcurrentRunner(limit) { const queue = []; let activeCount = 0; function next() { if (activeCount >= limit || queue.length === 0) { return; } const task = queue.shift(); activeCount++; task().finally(function () { activeCount--; next(); }); } return { add(task) { queue.push(task); next(); return this; } }; } function createAsyncTask(name, delay) { return function () { return new Promise(function (resolve) { setTimeout(function () { console.log('完成:', name); resolve(); }, delay); }); }; } const runner = createConcurrentRunner(2); runner .add(createAsyncTask('任务A', 1000)) .add(createAsyncTask('任务B', 500)) .add(createAsyncTask('任务C', 300)) .add(createAsyncTask('任务D', 400));

运行输出大致是:

完成:任务B 完成:任务C 完成:任务A 完成:任务D

推演过程:A和B先启动;B在500ms后完成,空出一个槽位,C进入;C在300ms后完成,D进入;A在1000ms后完成。这个练习既用到了闭包保护队列,也用到了Promise的finally和微任务调度机制,是Day 13很合适的综合题。

5.3 实验3:预测事件循环输出顺序

最后一个练习不需要写代码,而是读代码,把执行顺序一步步写下来。给你一段无删减的代码:

console.log('start'); setTimeout(() => { console.log('timeout1'); Promise.resolve().then(() => console.log('promise-in-timeout')); }, 0); Promise.resolve().then(() => { console.log('promise1'); }).then(() => { console.log('promise2'); }); console.log('end');

先自己推一遍,再放到浏览器控制台跑。预期输出是:

start end promise1 promise2 timeout1 promise-in-timeout

推演关键点:同步代码输出start、end;微任务队列里有promise1和promise2;宏任务队列里有timeout1。微任务队列先清空,输出promise1、promise2。然后宏任务timeout1开始执行,它内部又产生了一个新的微任务promise-in-timeout。注意,这个微任务是在timeout1宏任务执行过程中产生的,并不会影响后续宏任务的顺序,因为当前宏任务还没结束,内部的微任务会在当前宏任务结束后立即清空,所以promise-in-timeout先输出,之后才轮到下一个宏任务。

6. 常见问题与避坑实录

学完三大主题,实操中必然踩坑。我把这段时间遇到的典型问题整理成速查表,方便你以后排查。

问题现象可能原因解决方法
循环里setTimeout输出全是最后一项闭包捕获了循环变量的最终值用let声明循环变量,或用IIFE传入当前索引
修改一个实例的数组,其他实例也变了引用类型属性被原型链共享在构造函数里用this.xxx = []初始化,不要挂到原型上
用instanceof判断子类实例返回false子类原型没有被正确设置,或prototype被覆盖检查是否用了Object.create(SuperClass.prototype)
Promise的then有时不执行忘了调用resolve或reject,Promise状态永远pending在异步操作完成回调里显式调用resolve
大量DOM操作导致页面卡顿主线程被长任务阻塞,渲染掉帧拆分任务、减少同步大循环、必要时用requestAnimationFrame
接口并发时总耗时等于所有请求之和串行await了没有依赖关系的请求改用Promise.all并行请求

排查思路我一般分三步。第一步,在关键位置打印日志或者用断点,确认数据流和调用栈。第二步,缩小范围,把可疑代码抽成最小可复现的Demo,在控制台单独测。第三步,用浏览器性能面板看主线程的任务切片,识别长任务和密集的微任务。

闭包相关的bug最隐蔽,因为它不报错,只是行为不符合预期。我实际排查过一次计数器一直跳两位数的现象,最后发现是某个地方把createCounter返回的函数存了两份,两个闭包各自维护了独立状态,互相覆盖导致混乱。这种问题靠读逻辑很难发现,直接在控制台执行并展开函数的[[Scopes]]属性,能看到闭包里到底保存了哪些变量。

原型链相关的bug则集中在“方法不生效”和“属性莫名共享”。方法不生效先检查prototype是否被整体覆盖了;属性共享则检查是否在原型上定义了引用类型字段。

事件循环相关的bug最容易让人抓狂的是执行顺序不可控。遇到这种情况,别猜,把宏任务和微任务分别列出来,按“同步代码 → 微任务清空 → 宏任务 → 微任务清空”的时序推一遍,十次能中九次。

Day 13 进阶3学完,我最大的感受是这三块内容组合起来,整个JS的“运行时画面”终于完整了。以前写代码是靠记忆拼凑语法,现在写代码是知道每一段代码在引擎里如何被调度、如何被检索、如何被回收。这种状态上的转变,比记住多少API都重要。对于正在学进阶系列的人,我的建议很直接:不要停留在看文章,把上面的三个练习逐一跑起来,再自己改参数观察输出变化。跑一遍代码,比读十遍概念都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 17:20:32

零成本抓取tick级实时行情:Python+WebSocket实战指南

在行情数据这件事上&#xff0c;“免费”和“实时”向来是冤家。日线数据满大街都是&#xff0c;分钟线也能找到不少免费接口&#xff0c;但一旦你开口要 tick 级行情——每笔成交都逐条推送的那种数据——几乎所有数据商都会先跟你谈钱。原因不难理解&#xff1a;tick 数据才是…

作者头像 李华
网站建设 2026/10/13 17:19:50

cc-safety-net:基于npx的前端安全前置诊断工具

1. 项目概述&#xff1a;这不是一个“装完就跑”的 CLI 工具&#xff0c;而是一套可落地的安全防护前置校验体系cc-safety-net这个名字乍看像某个开源库的缩写&#xff0c;但实际它是一个面向现代前端工程与本地开发环境的安全基线检查工具——不是杀毒软件&#xff0c;也不是运…

作者头像 李华
网站建设 2026/10/13 17:19:26

AI编程代理技能驱动工作流:v0.1.5框架实战解析

最近半年我一直在折腾一个叫"AI-Infra-Auto-Driven-SKILLS"的框架项目&#xff0c;目前迭代到 v0.1.5。老读者可能记得我吐槽过 Codex 和 Claude Code 这类终端 AI 编程代理"脑子不够使"的问题——不是模型笨&#xff0c;而是每次任务进来都像没头苍蝇&…

作者头像 李华
网站建设 2026/10/13 17:18:48

ab压测工具:HTTP性能验证与并发测试入门核心指南

1. 项目概述&#xff1a;为什么ab不是“过时的玩具”&#xff0c;而是压测入门不可绕开的第一块磨刀石很多人看到“ab压测工具”这五个字&#xff0c;第一反应是&#xff1a;“这玩意儿2002年就随Apache发布了&#xff0c;现在还讲&#xff1f;是不是该直接上JMeter、k6或者Loc…

作者头像 李华
网站建设 2026/10/13 17:18:39

Python语法核心:从变量本质到异常处理的工程实践

1. 变量与数据类型&#xff1a;Python的"动态"到底动在哪里很多初学者学Python语法&#xff0c;第一感觉是"简单"&#xff0c;写起来像在写伪代码。但真正开始做项目&#xff0c;尤其是我自己带过几个新人之后&#xff0c;发现大家对Python"动态类型&…

作者头像 李华
网站建设 2026/10/13 17:16:45

拿下决策权:让LLM只做语义判断,代码掌控确定性逻辑

1. 为什么我们急着把决策权从 LLM 手里收回来 先说结论&#xff1a;我最近大半个月都在折腾一套叫 Embabel 的思路——把 Agent 的决策权从 LLM 手里拿回来。听起来有点反直觉&#xff0c;毕竟现在大家都在堆模型、喂提示词、让大模型“自由发挥”&#xff0c;怎么反而要收权&a…

作者头像 李华