执行模型与作用域
执行上下文与词法作用域决定变量在何处可访问、闭包如何形成,以及 var / let / const 的差异。语言概览见 JavaScript 基础。
执行上下文
执行上下文是 JavaScript 代码执行时的环境。它包含了代码执行所需的所有信息:变量、函数声明、this 指向等。
类型
- 全局执行上下文:
- 这是默认的、最基础的上下文。
- 任何不在函数内部的代码都在全局上下文中。
- 在浏览器中,全局对象的
this绑定为window(严格模式模块中顶层this为undefined);在 Node.js 中为global;统一可用globalThis。
- 函数执行上下文:
- 每当一个函数被调用时,都会为该函数创建一个新的执行上下文。
- 每个函数都有自己的上下文栈。
eval执行上下文:- 在
eval()内执行的代码拥有独立上下文;生产代码应避免eval(见 安全)。
- 在
执行上下文的生命周期(创建阶段与执行阶段)
JavaScript 引擎在执行代码时,主要分为两个阶段:
- 创建阶段:
- 创建变量对象:初始化函数的参数、变量声明、函数声明。
- 建立作用域链:确定对变量的访问权限。
- 确定
this指向。 - 注:此时变量会被赋值为
undefined(变量提升)。
- 执行阶段:
- 逐行执行代码,为变量赋值,引用函数。
执行上下文栈
引擎用**调用栈(call stack)**管理执行上下文:脚本开始全局上下文入栈;函数调用则入栈新上下文;函数返回后出栈。
作用域
作用域是指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。作用域是在代码编写时(定义时)确定的,而不是运行时确定的,这被称为“词法作用域”。
类型
- 全局作用域:
- 在代码任何地方都能访问到的变量。
- 拥有全局作用域的变量是全局变量。
- 函数作用域:
- 在函数内部声明的变量,只能在函数内部访问。
- 外部无法访问函数内部的变量。
- 块级作用域:
- ES6 的
let/const使{}块可形成独立作用域。 var不具备块级作用域(它在{}外依然可见,除非在函数内)。
- ES6 的
作用域链
当访问一个变量时,JavaScript 引擎会先在当前作用域查找,如果没有找到,就向上一级作用域查找,直到全局作用域。这种层层向上的链式查找机制就是作用域链。
闭包
闭包是 JavaScript 中最强大的特性之一,也是面试常考点。
定义
简单来说,闭包就是函数能够记住并访问它的词法作用域,即使该函数是在其词法作用域之外执行。
原理
当一个函数内部定义了另一个函数,并把这个内部函数返回或传递到外部时,内部函数会持有外部函数的变量对象的引用。即使外部函数已经执行完毕退出了执行栈,其变量对象依然不会被销毁,因为内部函数还在引用它。
代码示例
function outer() {
let a = 10;
return function inner() {
console.log(a);
};
}
const closureFunc = outer(); // outer 执行完毕,通常认为 a 会被销毁
closureFunc(); // 输出 10,依然能访问 a,这就是闭包
应用场景
- 数据私有化(模拟私有方法)。
- 柯里化。
- 保持状态(如防抖、节流函数)。
变量提升与暂时性死区
这是理解 var、let 和 const 行为差异的关键。
变量提升
在 ES6 之前,使用 var 声明的变量,无论在函数的何处声明,都会被“提升”到函数的顶部。但是,只有声明被提升,赋值操作留在原地。
- var 的表现:
console.log(myVar); // 输出 undefined,不会报错
var myVar = 10;
// 等价于:
// var myVar;
// console.log(myVar);
// myVar = 10;
- 函数声明也会被提升:
sayHello(); // 可以正常调用,输出 "Hello"
function sayHello() {
console.log("Hello");
}
暂时性死区
ES6 引入了 let 和 const,它们也存在变量提升,但是行为不同:它们被放入了“暂时性死区”(TDZ)。
- TDZ 的含义:从代码块(作用域)的开始,直到变量声明语句运行完成之前,该变量都处于 TDZ。在此期间访问该变量会导致 ReferenceError。
- 目的:为了减少运行时错误,强制开发者先声明再使用。
console.log(myLet); // 报错:ReferenceError: Cannot access 'myLet' before initialization
let myLet = 20;
{
// TDZ 开始
tmp = "abc"; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ 结束,tmp 被初始化为 undefined
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
总结对比
| 特性 | var | let / const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 变量提升 | 是,提升并初始化为 undefined | 是,提升但进入暂时性死区 (TDZ) |
| 重复声明 | 允许 | 不允许 (报错) |
| 全局挂载 | 会挂载到 window (浏览器) | 不会挂载到 window |
综合示例
为了把这些概念串起来,看下面这个例子:
var globalVar = "I am global";
function createCounter() {
let count = 0; // 局部变量,createCounter 的作用域
// 返回一个闭包
return function () {
count++; // 访问外部作用域的 count
console.log(count);
console.log(globalVar); // 通过作用域链访问全局变量
};
}
const counter = createCounter();
counter(); // 输出 1, "I am global"
counter(); // 输出 2, "I am global"
在这个例子中:
- 执行上下文:代码开始运行创建全局上下文;调用
createCounter创建函数上下文。 - 作用域:
count拥有函数作用域,globalVar拥有全局作用域。 - 闭包:返回的匿名函数形成了闭包,使得
createCounter执行完毕后,count依然保存在内存中。 - 变量提升:
let声明的count在声明前处于 TDZ;若改用var则提升为undefined。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| MDN:闭包 | 闭包与词法环境 |
| MDN:let | 块级作用域与 TDZ |
| MDN:var | 函数作用域与提升 |
| MDN:this | this 与执行上下文 |
| ECMA-262:执行上下文 | 规范术语(英文) |
相关文章
错误处理与调试
结构化错误类型、try/catch 与全局未捕获处理是健壮应用的基础。
浏览器相关 API
DOM 事件、存储、fetch、Worker、Observer 等由浏览器提供的 Web API(非 ECMAScript 核心)。HTML 结构见 标签。
性能优化
防抖/节流、渲染与内存、资源预加载及引擎行为是前端性能的主要抓手。相关 CSS 见 视觉效果、Head 资源提示。
模块化与工程化
ESM、CommonJS 与打包工具(Tree Shaking、代码分割)是前端工程化的基础。入门见 JavaScript 基础 · 模块。
数据结构与集合增强
Map / Set、WeakMap / WeakSet、数组高阶方法、TypedArray 与 Symbol 等扩展了内置数据结构能力。见 语言基础。
JavaScript 基础
JavaScript(ECMAScript)是 Web 页面与 Node.js 等运行时的脚本语言:在浏览器中操作 DOM、发起网络请求;在服务端处理 I/O。语法与 HTML、CSS 配合构成前端技术栈。
Series
js
14 / 14