作用域
作用域是指当前可执行的上下文,其中的值和表达式都是可见的。
作用域类型
1、全局作用域
2、函数作用域
3、块级作用域(由{} 包裹的区域 ES6新增)
作用域链
当访问一个变量时,JS会:
1、先在当前作用域找;
2、找不到就往上一层作用域找;
3、一直找到全局作用域;
4、如果全局作用域也找不到就-> ReferenceError
变量提升
变量提升并不是代码真的被搬到上面去了,而是js引擎在执行代码前,会做一个预扫描(编译阶段),在这个阶段中:
1、它会先收集当前作用域中的所有变量声明和函数声明
2、然后在执行阶段再去执行代码。
预扫描阶段就会让这些变量名提前注册到当前作用域中,这就是提升
举例:
var
console.log(a) // undefined
var a = 1;
上面代码的执行顺序相当于
var a; //声明被提升+初始化为 undefined
console.log(a);
a = 1
所以上面的输出结果没问题,只是值还没赋上
let
console.log(b) // ReferenceError
let b = 2;
执行顺序是这样
// 在作用域建立时,b 已经被登记在作用域中了 但是此时还没有被初始化
TDZ b 处于暂时性死区
console.log(b) // 在初始化之前访问->报错 Reference
let b = 2; // 执行到这一步 完成初始化
所以let 声明的变量在初始化之前不能访问,这一段区域叫做暂时性死区。
const
const 和let 一样 只是要求声明时必须同时赋值
console.log(c);
const c = 3
var let const 区别
| 对比项 | var | let | const |
|---|---|---|---|
| 🧭 作用域类型 | 函数作用域(function scope) | 块级作用域(block scope) | 块级作用域(block scope) |
| 🪄 变量提升(Hoisting) | ✅ 会提升,初始化为 undefined
|
✅ 会提升,但存在“暂时性死区(TDZ)”,未初始化前不可访问 | ✅ 会提升,但存在“暂时性死区(TDZ)”,未初始化前不可访问 |
| ⚠️ 暂时性死区(TDZ) | ❌ 不存在 | ✅ 存在 | ✅ 存在 |
| 🧱 是否能重复声明 | ✅ 可以在同一作用域重复声明 | ❌ 不可以 | ❌ 不可以 |
| 🔄 是否可重新赋值 | ✅ 可以 | ✅ 可以 | ❌ 不可以(常量) |
| 🧩 是否必须初始化 | ❌ 不需要 | ❌ 不需要 | ✅ 声明时必须赋值 |
| 🌍 是否绑定到 window | ✅ 会变成全局对象属性(window.a) |
❌ 不会 | ❌ 不会 |
| 🧮 使用场景 | 定义函数作用域内变量(旧写法) | 定义可变的块级变量 | 定义常量(不可更改引用) |
函数声明与块级作用域
function f() { console.log('I am outside!'); }
(function () {
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
上述代码在ES5和ES6规范中输出不同
ES5规范--函数级提升
函数声明 (function f() {...})总是提升到包含函数作用域顶部(不管写在块里if while {})
所以上述代码实际的运行是这样的
function f() { console.log('I am outside!'); }
(function () {
// ES5 的提升效果(把 if 内的 function 提升到函数顶部)
function f() { console.log('I am inside!'); } // 被提升
if (false) {
// 原 if 体不执行
}
f(); // 调用提升后的内部 f -> "I am inside!"
}());
ES6规范--块级函数声明
函数声明若写在块内,按规范是块级声明:
1、它在块级作用域内提升(即该块顶部可用)
2、在块外不可见
3、若块未执行(如 if(false)),该块级声明不会被初始化 → 在块外访问会产生 ReferenceError(或至少不能看到外层函数)
所以实际上等同于下面的代码
function f() { console.log('I am outside!'); }
(function () {
// 进入 IIFE:这里不会把 if 内的 function 提升到 IIFE 顶部(它属于 if 的块级作用域)
if (false) {
function f() { console.log('I am inside!'); } // 这个 f 被视作块内绑定,提升到 if 块顶部
}
f(); // 块内 f 屏蔽了外层,但块未运行内 f 未初始化 -> ReferenceError
}());
块级声明语法陷阱
不合法写法
if(true) let x =1 // 语法错误
合法写法:
if(true){let x = 1}
原因是if (true) 后面必须是「语句(statement)」;
但 let 是「声明语句(declaration)」,不能直接跟在控制语句后。
全局变量与window绑定
| 情况 | window.a 是否有值 | 说明 |
|---|---|---|
全局作用域下 var a = 1
|
✅ 是(window.a = 1) | 全局 var 会绑定到 window |
全局作用域下 let a = 1
|
❌ 否 | let/const 不会挂载 window |
函数内部 var a = 1
|
❌ 否 | 局部作用域,不会绑定 window |
闭包与作用域链陷阱
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function() { console.log(i); };
}
a[6](); // 输出 10 ❗
因为 var 是函数作用域,所有函数共享同一个 i;
循环结束时 i = 10,所以每个函数输出 10
解决办法:
for (let i = 0; i < 10; i++) {
a[i] = function() { console.log(i); };
}
a[6](); // 输出 6 ✅
let 在每次循环都会创建一个新的块级作用域,
每个闭包函数都捕获自己当次的 i 之所以会输出6 是因为js引擎有自己机制可以记住每次循环的值。
常见陷阱
| 题目 | 输出结果 | 关键点 |
|---|---|---|
console.log(a); var a=1; |
undefined | var 提升 |
console.log(b); let b=1; |
ReferenceError | TDZ |
if(true) let x=1; |
SyntaxError | 控制语句后不能直接用 let |
var a=1; console.log(window.a); |
1 | 全局 var 挂在 window 上 |
let a=1; console.log(window.a); |
undefined | let 不挂 window |
for(var i=0;i<3;i++){ setTimeout(()=>console.log(i),0)} |
3 3 3 | var 没有块作用域 |
for(let i=0;i<3;i++){ setTimeout(()=>console.log(i),0)} |
0 1 2 | let 生成块作用域 |