JavaScript进阶自学记录:this指向、原型链与事件循环实战
发布时间:2026/9/26 17:55:10来源:尧图网络
说实话写这篇记录之前我盯着“IT自学第三十四天”这个数字愣了好一会儿。三十四天说长不长说短也不短但它恰好卡在一个很有意思的节点上基础语法基本见过了能写出一点能跑的小玩意但距离“真正会编程”总觉得隔着一层窗户纸。今天就是捅窗户纸的日子。我把这天的核心学习内容——JavaScript里的this指向、原型链、事件循环和异步编程——按自己的节奏啃了一遍中间一度自闭最后靠调试器和一块白板把思路捋顺了。这篇文章就把第三十四天的完整过程、踩过的坑和悟出来的东西都写下来给同样在自学路上的人一个参考。不管你是刚学到变量函数还是已经卡在异步编程门口这篇记录应该都能让你少走几步弯路。1. 第三十四天的学习路线规划为什么偏偏是这一天1.1 前期进度盘点第三十四天前的积累自学最怕的就是“东一榔头西一棒子”今天看Python明天学Java折腾一个月啥也没学会。我给自己定的路线是走前端方向前面三十三天大概做了这些事1~7天HTML和CSS基础能照着设计稿切一个静态页面对盒模型、Flex布局、定位这些概念有了初步感知。 8~15天JavaScript基础语法包括变量、数据类型、运算符、流程控制、函数、数组和对象这个阶段最磨人很多知识点看懂了但一写就报错。 16~25天开始碰DOM操作和事件学会了querySelector、addEventListener这些常用API能做出简单的交互效果比如点击按钮切换样式、动态渲染列表。 26~33天深入函数和作用域接触了闭包、立即执行函数、高阶函数这些概念说实话有一部分是囫囵吞枣隐隐约约觉得后面肯定会用上但当时并没有真正吃透。到第三十三天晚上我做了一个简单的待办事项应用功能是能加能删能勾选用的还全是面向过程的写法。代码确实跑通了但是文件里到处是全局变量函数之间互相调用全靠传参写的时候没问题改起来就想骂人。这时候我才意识到该系统的学一学JavaScript进阶的东西了。1.2 为什么这个时间节点要学“this、原型链和事件循环”很多自学者会犯一个错误刚学会if/else和for循环就开始刷算法题或者急着去学框架结果被this和异步搞到怀疑人生。我倒是觉得像这种比较虚的概念学得太早容易劝退学得太晚浪费时间第三十天到第四十天左右刚好是个黄金窗口期因为这时候你已经有足够的代码量做铺垫了。所以我把第三十四天定义为“进阶概念突破日”目标很明确一是搞懂this到底是什么二是理解原型链的作用三是把事件循环和异步编程打通。这三个东西是前端进阶的基石初次接触会觉得每个字都认识但连起来不知道什么意思但今天必须把它们拆碎了、嚼烂了。1.3 当天学习目标拆解与时间分配我给自己列了一个详细到小时的学习计划上午 2.5 小时集中火力攻this的四种绑定规则配合代码示例和打断点观察。 下午 1.5 小时理解原型链搞明白对象和函数之间的关系。 下午 2 小时看事件循环的经典视频和可视化工具搞懂微任务和宏任务的区别。 晚上 2 小时用两个实战题目检验白天的学习效果包括一个防抖函数的实现和一个异步加载数据的模拟。这个安排是有讲究的最难的部分放在精力最好的上午原型链和异步属于概念性内容需要反复琢磨间隔一段时间回头再想会有种豁然开朗的感觉所以下午和晚上分开学习留出消化时间。2. this指向的四种规则与实操验证2.1 默认绑定和隐式绑定先搞清楚函数被谁调用学this最忌死记硬背关键其实就一句话this的值是在函数被调用的时候确定的而不是在函数定义的时候确定的。谁调用了这个函数大部分时候this就指向谁。默认绑定是最简单的一种情况直接调用一个普通函数在非严格模式下this指向window浏览器环境在严格模式下指向undefined。比如这段代码function showThis() { console.log(this); } showThis(); // 非严格模式下打印 window严格模式下打印 undefined当时我不理解为什么严格模式下不一样后来明白了严格模式下故意把this设为undefined是为了避免误操作全局对象这是一种安全保护机制。隐式绑定就更常用了比如有一个对象对象的方法里用到了this那这个this就指向这个对象。我写了个简单的例子const user { name: 自学小张, getName: function() { console.log(this.name); } }; user.getName(); // 打印 自学小张这个好理解因为getName是作为user的方法被调用的。但坑爹的地方在于如果把这个方法单独拿出来再调用const fn user.getName; fn(); // 打印 undefined因为此时 fn 在全局被调用this 指向 window这就是自学者最容易踩的坑。你把方法赋值给一个变量方法里的this就丢了因为调用者变成了全局对象。这时候就需要用后面的显式绑定来救场。2.2 显式绑定call、apply、bind到底怎么用call、apply和bind就是用来强行指定this指向的工具。三者的区别很简单call和apply都是立即调用函数区别仅在于传参方式call是一个一个传apply是传一个数组bind不会立即调用而是返回一个新的函数新函数的this被永久绑死。我在控制台里反复试了几组代码const user1 { name: 小张 }; const user2 { name: 小李 }; function introduce(age, city) { console.log(${this.name}${age}岁来自${city}); } introduce.call(user1, 25, 武汉); introduce.apply(user2, [26, 长沙]); const bound introduce.bind(user1, 25, 武汉); bound(湖北); // 这里再传参会放在武汉后面有个小细节值得注意bind是“参数合并”的第一次绑定时传的参数会固定下来之后调用时再传的参数会追加在后面。这一点在写偏函数的时候很好用但在日常调用中如果不小心容易参数顺序错乱。还有一个容易忽略的点call和apply的第一个参数如果传null或undefined在非严格模式下会被忽略this指向window。这个知识点在第三部分讲数组方法时经常用到比如[].slice.call(arguments)就是把类数组转成真数组的经典操作。2.3 new绑定构造函数里的this指向实例对象用new关键字调用函数时this指向新创建的那个对象。new做了什么我总结为四步创建一个空对象把空对象的原型指向构造函数的prototype执行构造函数并让this指向这个空对象如果函数没有返回对象就自动返回这个新对象。function Person(name) { this.name name; this.sayName function() { console.log(我是${this.name}); }; } const p1 new Person(小张); const p2 new Person(小李); p1.sayName(); // 我是小张 p2.sayName(); // 我是小李这里我第一次理解为什么所有实例都能有自己的name属性因为每次new都会执行一遍构造函数每次都生成了新的对象和新的属性。2.4 箭头函数为什么不适用这些规则之前学箭头函数的时候只知道它写法简洁今天才真正明白它的本质箭头函数本身没有this它会捕获定义时外层作用域的this。这带来一个巨大优势就是不需要像普通函数那样担心调用者变了导致this丢失。const obj { name: 箭头函数测试, delayLog: function() { setTimeout(function() { console.log(this.name); // undefined因为回调被全局调用 }, 100); }, delayLogArrow: function() { setTimeout(() { console.log(this.name); // 箭头函数测试捕获外层 this }, 100); } }; obj.delayLog(); obj.delayLogArrow();看这个例子就很清楚了。以前我写定时器里面访问外层this总是报错后来要么在外面存一个that this要么用箭头函数。明白原理之后思路就清晰了到底该用普通函数还是箭头函数取决于你是否需要自己的this。3. 原型链从“万物皆对象”到代码复用3.1 理解函数对象的双重身份刚接触原型链时最绕的就是函数和对象的关系。实际上每个函数在创建时都会自带一个prototype属性这个属性指向一个对象而每个由构造函数创建出来的实例内部都有一个[[Prototype]]属性指向构造函数的prototype。在浏览器里实例的__proto__就相当于这个内部属性的等价获取方式。我自己动手在控制台里打印查看function Animal(name) { this.name name; } Animal.prototype.sayName function() { console.log(this.name); }; const dog new Animal(旺财); console.log(dog.__proto__ Animal.prototype); // true console.log(Animal.prototype.constructor Animal); // true为什么要把方法放在prototype上而不是构造函数里面因为如果写在构造函数里每创建一个实例就会重新创建一个函数浪费内存放在prototype上所有实例共享同一个方法通过原型链访问即可。我拿手机类比了一下prototype就像手机说明书所有同型号手机共看同一本说明书不用每台手机都自带一份。3.2 原型链的查找机制所谓的原型链说白了就是一条“向上找”的链路。当我们访问一个对象的属性时如果对象本身没有就沿着__proto__逐级往上找一直找到Object.prototype如果还没有就是undefined。把之前的Animal和dog串起来看dog.toString();dog本身没有toStringAnimal.prototype上也没有因为只定义了sayName于是再往上找到Object.prototype发现toString在那里就拿来用了。整个过程就是一次一次的属性查找这跟作用域链的查找逻辑很相似本质上都是一种就近寻址的机制。理解了这个之后我再回头看“万物皆对象”这句话有了新的体会并不是说所有东西都是Object的实例而是说几乎所有东西最终都能通过原型链找到Object.prototype这个公共祖先。3.3 一个经典原型链题目的完整拆解网上有一道很经典的面试题function Foo() { this.value 42; } Foo.prototype.getValue function() { return this.value; }; const foo new Foo(); console.log(foo instanceof Foo); // true console.log(foo instanceof Object); // true console.log(foo instanceof Function); // false为什么foo instanceof Foo是true因为instanceof检查的是右边的构造函数的prototype是否出现在左边实例的原型链上Foo.prototype确实在foo的原型链上。为什么foo instanceof Function是false因为Function.prototype根本不在foo的原型链上。这个检查机制搞懂了以后看instanceof就不会觉得玄学了。原型链这个知识点的实操价值在于它解释了为什么每个数组都能用map/filter每个字符串都能用trim/split。因为数组和字符串都是构造函数的实例方法全部挂载在它们的构造函数的prototype上平时调用方法其实就是沿着原型链找到了对应的方法。4. 事件循环与异步编程从玄学走向可视化4.1 同步代码和异步代码的真实执行顺序以前写代码总遇到一种情况明明setTimeout写在前面后边的同步代码反而先执行。当时以为是不是定时器出问题了今天学了事件循环才明白这是JavaScript的单线程执行模型决定的。简单来说JavaScript是单线程的同一时间只能干一件事。遇到异步任务比如定时器、网络请求、DOM事件时不会一直等它完成而是先把它放到一边继续执行后面的同步代码。等到同步代码执行完了主线程空了再回头处理那些异步任务。可以做一个生活化的类比你去奶茶店点单收银员收完钱后立刻接待下一位顾客而不是在柜台等你这杯做好了再接待下一位。奶茶做好了就放到取餐口等收银员有空了再喊你取餐。这就是事件循环的精髓。4.2 宏任务与微任务setTimeout真的只等100毫秒吗这里有个特别关键的概念异步任务之间还分宏任务和微任务。我一开始也搞混了后来靠一个经典例子彻底弄明白console.log(1); setTimeout(function() { console.log(2); }, 0); Promise.resolve().then(function() { console.log(3); }); console.log(4);猜猜打印顺序是什么我当时猜的是1、4、2、3因为setTimeout是0毫秒定时器Promise的then也是异步的总得有先后。但答案是1、4、3、2。原因就是微任务和宏任务的优先级不同。Promise的then属于微任务setTimeout属于宏任务。在一次事件循环中先执行宏任务当前同步代码也算一个宏任务然后执行所有微任务之后再去取下一个宏任务。所以then里的3比setTimeout里的2先打印出来。这个知识点看着像概念题实际上在真实项目中特别重要。比如你写了一个页面加载数据后要更新列表如果数据更新逻辑写在链式调用的微任务里那DOM渲染可能在微任务之前就已经完成了导致你看到的是旧数据。搞清楚微任务的执行时机对排查这类“看起来像bug但又不是必现”的问题很有帮助。4.3 从回调地狱到Promise再到async/await异步编程最痛苦的阶段就是回调地狱。比如我要连续请求三个接口第一个返回后请求第二个第二个返回后请求第三个如果全部用回调函数写requestData(function(response1) { requestMoreData(response1, function(response2) { requestMoreData(response2, function(response3) { console.log(response3); }); }); });这样的代码但凡多加两层改起来心态直接崩。后来学了Promise用链式调用把嵌套拍平requestData() .then(response1 requestMoreData(response1)) .then(response2 requestMoreData(response2)) .then(response3 console.log(response3));代码结构立刻清晰了很多每一层只干一件事出错也好看。而async/await是在Promise之上再套一层糖让异步代码看起来像同步代码一样直观async function getData() { const response1 await requestData(); const response2 await requestMoreData(response1); const response3 await requestMoreData(response2); console.log(response3); }我用一句非常直白的话来总结Promise解决了回调嵌套的问题async/await解决了Promise链过长的问题本质上都是为了让异步代码更容易被人类理解。4.4 异步在实战题目中的完整应用为了验证这一天的学习效果我给自己出了一个题目模拟异步加载用户列表然后根据用户ID再加载详情最后把信息渲染到页面上。核心代码如下const API { fetchUserList() { return new Promise(resolve { setTimeout(() { resolve([{ id: 1, name: 小张 }, { id: 2, name: 小李 }]); }, 500); }); }, fetchUserDetail(id) { return new Promise(resolve { setTimeout(() { resolve({ id, desc: 这是${id}号的详情 }); }, 500); }); } }; async function renderUserList() { const list await API.fetchUserList(); for (const user of list) { const detail await API.fetchUserDetail(user.id); console.log(detail.desc); } } renderUserList();这里要注意一个问题await不能直接用在普通循环里并行执行上面的写法导致第二个请求要等第一个完成才开始是一种顺序请求。如果希望并行请求应该用Promise.all。实际项目里到底用哪个取决于接口之间有没有前后依赖没有依赖的尽量并行速度会快很多。今天我把这个知识点也顺手验证了一下const [list, detail] await Promise.all([ API.fetchUserList(), API.fetchUserDetail(1) ]);两个请求同时发起总耗时不会翻倍。这个细节在很多性能优化的小技巧里都会用到理解了原理用起来就心里有数。5. 一晚上的调试实录用断点和调用栈解决this丢失问题5.1 现场还原为什么这个按钮点击没有反应晚上练习时我写了一个简单的计数器页面点击按钮数字加一。代码写完之后打开浏览器点击按钮没有任何反应控制台也没有报错。我当时的代码是这样的const counter { count: 0, increment: function() { this.count; render(); } }; const btn document.querySelector(#btn); btn.addEventListener(click, counter.increment);第一眼看上去没毛病counter.increment里this.count点击按钮应该让count加一。但实际测试后this已经丢了因为addEventListener在调用回调时会把回调里的this指向触发事件的元素也就是那个按钮。所以this.count读取的是按钮上的count属性默认是undefined自增之后变成NaN页面自然不会更新。这就是前面学过的隐式绑定丢失问题实战里非常常见完全不看理论很容易被坑。5.2 用断点确认this的实时值我没有肉眼猜而是打开浏览器的开发者工具在this.count这一行打了一个断点。刷新页面点击按钮代码执行到断点时自动暂停此时我鼠标悬停在this上面看到的确实是一个button元素而不是counter对象。这比任何讲解都直观一次断点操作胜过十遍看书。顺带学会了“调用栈”面板的用法可以看到当前执行到哪一层函数、这层函数是谁调用的把调用关系理出来this丢失的原因一目了然。如果按钮的点击回调被调用时调用栈里有一层是dispatchEvent说明回调是被事件系统触发的调用者就是DOM元素而不是counter对象。5.3 三种解决方案对比bind、闭包、箭头函数找到问题之后我试了三种方案各有取舍方案一用bind显式绑定btn.addEventListener(click, counter.increment.bind(counter));这个方案最明确直接把counter绑成this。但注意bind会返回新函数如果之后想移除监听器需要保存引用不然移除不了。方案二用闭包缓存外部变量const that counter; btn.addEventListener(click, function() { that.increment(); });这是老派写法现在其实不推荐了因为箭尾函数完全可以替代但遇到老代码时你要能看懂。方案三用箭头函数btn.addEventListener(click, () counter.increment());箭头函数没有自己的this所以counter.increment()作为表达式调用this依然指向counter。这是我个人最喜欢的方案因为写法最简洁语义也清晰。5.4 控制台、断点、网络面板的三件套配合这一天晚上我发现排查前端问题光靠console.log是不够的。遇到数据请求问题的时候网络面板能告诉你接口到底有没有发出、返回什么状态码、响应体长什么样遇到逻辑问题时断点能把执行过程一步步拆开控制台还能手动修改变量的值来验证猜想比如我在控制台直接输入counter.count 5再点击按钮发现页面刷新成6了从而确认逻辑链路是通的问题只出在this上。这种三层配合很像侦探破案控制台给你线索断点帮你复盘过程网络面板帮你排除外部干扰。自学者一定要尽早养成这套调试习惯不要只会打console.log。6. 自学三十四天来的五个大坑与应对方法6.1 只看不敲视频看了一整天代码写了不到十行这是初期最容易犯的错误。B站收藏夹吃灰的教程倒是不少但很多时候看完一个视频觉得自己全都懂了关上网页要自己写的时候就傻眼。我后来定了一个铁律每看一个视频必须把里面所有代码自己在编辑器里敲一遍并且跑出结果才允许看下一个视频。敲的时候出错更好出错才能把问题的原因搞明白。6.2 笔记记得太多代码敲得太少我的早期笔记简直是复制粘贴大典把文档和教学视频里的代码原封不动抄过来就算完事结果复习的时候什么都看不懂因为没有参与感。后来我改成了“问题驱动笔记”只记录当天遇到的实际报错、排查思路和解决方案比如“点击按钮无效原因this指向DOM元素解决箭头函数调用”。这样的笔记复习起来效率极高因为每条都是曾经真实遇到的问题看了一眼就能触发记忆。6.3 过度追新框架满天飞基础空空如也自学圈里有一种浮躁的风气今天觉得Vue火就学Vue明天觉得React红就学React后天又冒出来一个Svelte结果基础还没打牢就想着上框架写出来的代码Bug满屏跑还不明白为什么。我的态度是如果JavaScript本身的变量、类型、函数、作用域、异步这些核心概念都还糊里糊涂那学任何框架都会变成背API换个版本就懵。倒是把原型链和this吃透了回头再看框架的源码很多设计就能看出门道。6.4 不会描述问题搜索引擎都救不了你很多初学者问问题只会说“这个不运行”“那个报错”别人根本看不明白。我学到的最有价值的一个习惯是把报错信息原封不动地复制到搜索引擎里如果搜不到就把问题的代码、期望结果、实际结果一并写清楚。中文搜不到就换英文搜Stack Overflow上的回答质量高得吓人往往能顺藤摸瓜学到很多额外的知识点。6.5 没有复盘学了就忘忘了就慌自学的节奏其实是螺旋式上升的今天学的东西过两周回头再看一定会有新的理解。我坚持每天晚上花十几分钟复盘当天学习内容怎么写用大白话把当天学到的核心概念讲给自己听讲得出来才算真懂。比如今晚复盘时我就写this就是函数被调用时的主人普通人找不到主人就顺手抓了全局当主人懂了这个后面的一切都好办。7. 第三十五天起的学习安排与资料推荐7.1 后七天的核心目标第三十四天属于“基础设施建设”接下来第三十五天到第四十一天就该打实战了。我给自己安排了一个小项目做一个带数据请求的新闻列表页涉及HTML结构、CSS样式、JavaScript的数据请求与渲染、事件委托、防抖处理、加载状态展示。这个项目不引入任何框架完全用原生JavaScript实现目的是把前面学的所有知识点串起来尤其是异步和DOM操作的配合。7.2 值得反复翻阅的几类资料我这几天的资料主要就三类MDN文档查API权威且免费、现代JavaScript教程重点看异步那部分讲得很通透、以及一个可以在线可视化事件循环的网站叫Loupe把代码贴进去就能看到执行流程适合理解微任务和宏任务。自学者一定克服“看英文文档恐惧症”不要一上来就找中文二手翻译。MDN的英文版内容最全中文版有些页面也会滞后学会直接读英文文档是专业路上的一步早晚要迈出去。7.3 一个小建议项目别贪大做完比做完美重要我见过太多自学的人立志“三十天做一个某某系统”最后项目死在第二周原因是范围太大、需求模糊、完美主义上头。我的建议很简单第一个综合项目能把增删改查做明白、能把异步数据渲染清楚、能在控制台看到网络请求记录就算合格了。做完之后立刻复盘哪里卡住了、为什么卡的、下一次怎么做能更快这样的收获比多做一个“大项目”强得多。我个人的体会是自学这件事最大的敌人不是难度而是失控感。今天把this、原型链、事件循环这三个“老朋友”真真正正地搞清晰了晚上睡觉都踏实了不少。说句掏心窝的话之前的三十三天里也踩过不少坑但回头看凡是后来让我融会贯通的地方几乎都是当时那段时间反复琢磨、反复动手试错留下的。第三十四天是一个转折点因为从这一天起写出能跑的代码已经不够了我开始理解代码为什么会这样跑。如果这篇记录能给同样在自学的朋友带来一点启发我就很开心了。
网站建设高端定制企业官网