1. 从基础语法到核心机制:Day 6该往哪里使劲
坚持到第六天,说实话已经淘汰掉一批人了。前五天我们通常把 HTML 结构、CSS 样式、JS 基础语法过了一遍,能写点静态页面、能做点简单的交互,比如点击按钮弹个提示框、切换个 Tab 页签、做个图片轮播。但这时候很多人会卡在一个很尴尬的阶段——代码能跑,但一遇到稍微复杂点的逻辑就懵,不知道为什么 var 和 let 行为不一样,不知道为什么回调函数里的 this 不是自己想象的那个对象,更搞不清楚异步任务到底是怎么排队的。
这个阶段我称之为"前端新手的分水岭":说白了就是开始从"能写"走向"懂为什么这么写"。Day 6 这个节点,我建议把精力全部砸在 JavaScript 的核心机制上,而不是急着去学 Vue、React 或者各种构建工具。原因很简单——框架天天变,但 JS 引擎里那套"作用域、闭包、原型链、事件循环"的机制,十年了基本没变过。你有没有想过,为什么面试题翻来覆去就是那几道:防抖节流怎么实现?[1,2,3].map(parseInt) 输出什么?Promise 和 setTimeout 谁先执行?这些问题的底层,全部指向今天我们要聊的这几个核心概念。
所以今天这篇文章,不教你写什么炫酷的页面,也不贴大段大段的框架代码。我们就老老实实把 JavaScript 最硬核的几块骨头啃一遍:作用域与提升、闭包、this 指向、原型链、事件循环。每块我都会结合实际的代码案例和常见的面试场景来讲,保证你能看懂、能复现、能用在实战里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 作用域与提升机制:var、let、const 的底层差异
2.1 执行上下文到底是什么
很多初学者学到作用域的时候,第一反应是"作用域不就是变量的有效范围吗",这句话对,但不够。真正理解作用域,得先理解一个概念——执行上下文(Execution Context)。你可以把执行上下文想象成一个"代码运行时的环境快照",JavaScript 引擎在执行一段代码之前,会先创建一个上下文,然后把变量声明、函数声明、this 的值、作用域链等信息都挂到这个上下文上。
JavaScript 有三种执行上下文:全局执行上下文、函数执行上下文、eval 执行上下文。我们平时写的代码,大部分时间就在全局上下文和函数上下文之间切换。每次调用一个函数,引擎都会给这个函数创建一个全新的执行上下文,并把它压入调用栈(Call Stack)。函数执行完,上下文弹出,相关的变量如果没有被外部引用,就会被垃圾回收。
这就解释了一个新手特别容易困惑的问题:为什么局部变量在函数外部访问不到?因为那个变量只存在于函数执行上下文的变量环境里,外部上下文根本看不到它。
2.2 变量提升的真相与暂时性死区
看一段代码:
javascript复制console.log(a); // undefined
var a = 10;
很多新手第一次看到这个输出 undefined 而不是报错,都会觉得莫名其妙。这个现象的底层机制就是变量提升(Hoisting)。var 声明的变量,在编译阶段会被"提升"到当前作用域的顶部,但只是声明被提升,赋值操作还留在原地。所以上面的代码实际等价于:
javascript复制var a; // 声明被提升,默认值为 undefined
console.log(a); // undefined
a = 10; // 赋值还在原地
但如果你把 var 换成 let:
javascript复制console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;
let 和 const 也会被提升,但在赋值之前访问它们会直接报错。这个"声明了但还没初始化"的区域,就叫暂时性死区(Temporal Dead Zone, TDZ)。为什么 JS 要搞一个 TDZ 出来?根本原因是 let 和 const 的设计目标就是弥补 var 的缺陷——var 的提升行为极其容易造成隐式 bug,比如忘记初始化就使用变量,结果拿到一个 undefined,排查半天才发现是声明位置的问题。
从实战角度我给你的建议是:全面使用 const,只在变量确实需要重新赋值时才用 let,永远不要用 var。这不仅仅是风格偏好,而是能实实在在减少一类很难排查的 bug。我自己在 code review 的时候,看到有人还在用 var 都会问一句为什么,基本没有人能给出非用不可的理由。
2.3 块级作用域为什么是革命性的
在 ES6 之前,JavaScript 只有全局作用域和函数作用域,没有块级作用域。这意味着 if 块、for 循环块里面的 var 变量,在外面照样能访问到。
javascript复制for (var i = 0; i < 5; i++) {
// 循环体
}
console.log(i); // 5,var 没有块级作用域,i 泄漏到全局
这个特性在早期的前端代码里造成过无数经典 bug,最著名的就是循环里绑定事件的闭包问题。let 和 const 引入之后,块级作用域才真正落地:
javascript复制for (let j = 0; j < 5; j++) {
// 循环体
}
console.log(j); // ReferenceError: j is not defined
注意 let 在 for 循环里还有一个特殊待遇——每次迭代都会创建一个新的绑定。也就是说循环体里的 j,在每次迭代中都是独立的一个变量。这个特性在解决"循环绑定事件拿到错误索引"这类问题时,简直是杀手锏。后面讲闭包的时候我还会回到这个案例。
3. 闭包:不只是面试题,更是前端架构的基石
3.1 闭包产生的条件和本质
闭包(Closure)是 JavaScript 中被讨论最多、也最容易被误解的概念之一。很多初学者觉得闭包很玄乎,甚至有人把闭包等同于"函数里返回函数"。其实不对。
闭包的本质是:当一个内部函数引用外部函数的变量时,即使外部函数已经执行完毕,这个内部函数依然保留着对外部函数作用域的引用能力。换句话说,闭包是"函数 + 函数定义时的词法作用域"的组合体。
看这个最经典的例子:
javascript复制function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(coun
