JS作用域于变量声明机制详解

作用域

作用域是指当前可执行的上下文,其中的值和表达式都是可见的。

作用域类型

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 生成块作用域
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容