JavaScript / TypeScript 易踩坑知识点 —— 作用域与变量类

JavaScript / TypeScript 易踩坑知识点 ------ 作用域与变量类

一、var 变量提升(Hoisting)

问题说明

var 声明的变量会被"提升"到函数(或全局)作用域的顶部,但只提升声明,不提升初始化

踩坑示例

javascript 复制代码
console.log(a)   // undefined(不是报错!)
var a = 1

// 实际执行顺序等价于:
var a
console.log(a)   // undefined
a = 1
javascript 复制代码
function foo() {
  console.log(x)  // undefined(⚠️ 不是 ReferenceError)
  var x = 1
}
foo()

与 let/const 的区别

javascript 复制代码
// var:提升声明,初始化为 undefined
console.log(a)   // undefined
var a = 1

// let/const:提升但进入"暂时性死区"(TDZ),访问直接报错
console.log(b)   // ReferenceError: Cannot access 'b' before initialization
let b = 1

console.log(c)   // ReferenceError: Cannot access 'c' before initialization
const c = 1

最佳实践

永远使用 letconst,不要使用 var


二、let/const 暂时性死区(TDZ)

问题说明

letconst 声明的变量在声明之前不可访问,这个区域称为"暂时性死区"(Temporal Dead Zone)。

踩坑示例

javascript 复制代码
// 块级作用域中的 TDZ
if (true) {
  console.log(temp)  // ReferenceError
  let temp = 'hello'
}

// 函数参数默认值中的 TDZ
function foo(x = temp) {  // ReferenceError
  let temp = 'hello'
}

// 类字段初始化中的 TDZ
class Foo {
  bar = this.x  // ReferenceError(this 尚未初始化)
  x = 1
}

最佳实践

  • 使用 const 作为默认声明方式,只有在需要重新赋值时才用 let
  • 避免在 TDZ 范围内访问变量

三、var 的函数作用域 vs let/const 的块级作用域

问题说明

var 声明的变量属于函数作用域 ,不受 iffor 等代码块的限制;letconst 声明的变量属于块级作用域

踩坑示例

javascript 复制代码
// var 不受块级作用域限制
if (true) {
  var a = 1
}
console.log(a)  // 1(⚠️ a 泄露到了外层作用域)

// let 受块级作用域限制
if (true) {
  let b = 1
}
console.log(b)  // ReferenceError(⚠️ b 在块外不可访问)

for 循环中的经典坑

javascript 复制代码
// var 导致的经典闭包坑
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// 输出:3, 3, 3(⚠️ 全部输出 3)

// 修复方案1:用 let(块级作用域)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// 输出:0, 1, 2(✅)

// 修复方案2:用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 100)
  })(i)
}
// 输出:0, 1, 2(✅)

四、for...in 遍历原型链

问题说明

for...in 会遍历对象自身和原型链上的所有可枚举属性。

踩坑示例

javascript 复制代码
const obj = { a: 1, b: 2 }

// 正常遍历
for (let key in obj) {
  console.log(key)  // 'a', 'b'
}

// 如果原型链上有属性,也会被遍历到
Object.prototype.c = 3
for (let key in obj) {
  console.log(key)  // 'a', 'b', 'c'(⚠️ c 来自原型链)
}

// 修复:使用 hasOwnProperty 过滤
for (let key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key)  // 只输出自身属性
  }
}

// 更推荐:Object.keys() / Object.entries()
Object.keys(obj).forEach(key => console.log(key))  // 只遍历自身可枚举属性

遍历数组的注意事项

javascript 复制代码
// ⚠️ for...in 不适合遍历数组
const arr = [1, 2, 3]
arr.customProp = 'hello'
for (let i in arr) {
  console.log(i)  // '0', '1', '2', 'customProp'(⚠️ 包含了原型属性)
}

// ✅ 推荐:for...of 或 forEach
for (let val of arr) { console.log(val) }
arr.forEach(val => console.log(val))

五、闭包与循环的经典坑

问题说明

闭包会捕获变量的引用而非值,在循环中使用时容易出问题。

踩坑示例

javascript 复制代码
// 经典闭包坑
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 100)
}
// 输出:5, 5, 5, 5, 5(⚠️ 所有闭包共享同一个 i)

// 修复1:用 let(ES6 块级作用域)
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), i * 100)
}
// 输出:0, 1, 2, 3, 4(✅)

// 修复2:用 IIFE
for (var i = 0; i < 5; i++) {
  ((j) => {
    setTimeout(() => console.log(j), j * 100)
  })(i)
}
// 输出:0, 1, 2, 3, 4(✅)

// 修复3:用 Promise + for...of(适合异步场景)
async function asyncLoop() {
  for (let i = 0; i < 5; i++) {
    await new Promise(resolve => setTimeout(resolve, i * 100))
    console.log(i)
  }
}

六、函数作用域的隐藏坑

问题说明

函数内部的 var 会泄露到函数外部,且同名变量会覆盖外层变量。

踩坑示例

javascript 复制代码
function foo() {
  var x = 1
  if (x === 1) {
    var y = 2  // y 泄露到了函数作用域顶部
  }
  console.log(y)  // 2(✅ 可以访问,因为 var 提升到函数顶部)
}
foo()
console.log(y)  // ReferenceError(⚠️ y 不在全局作用域中)

箭头函数的作用域

javascript 复制代码
// 箭头函数没有自己的 this,继承外层 this
const obj = {
  name: 'Alice',
  regular: function() {
    console.log(this.name)  // 'Alice'
  },
  arrow: () => {
    console.log(this.name)  // undefined(⚠️ 箭头函数没有自己的 this)
  }
}
obj.regular()
obj.arrow()

总结对照表

坑点 错误写法 正确写法
变量提升 var a = 1 const a = 1let a = 1
块级作用域 var 在 if/for 中 let / const
for 循环闭包 for(var i...) for(let i...)
for...in 遍历原型 for(let k in obj) for(let k in obj) if(obj.hasOwnProperty(k))
遍历数组 for...in for...offorEach
默认声明 var const(优先)/ let(需重赋值时)
相关推荐
YYYing.1 小时前
【C++进阶系列 (一)】关于线程堆栈的那些事 (上篇)
c语言·开发语言·c++·线程堆栈
zh73141 小时前
Go 1.23 → 1.26 升级检查文档
开发语言·chrome·golang
CRMEB系统商城1 小时前
汽车养护连锁的数字化底座CRMEB 多门店系统实战方案
java·开发语言·小程序·开源·汽车
努力努力再努力wz1 小时前
【Redis入门系列】:从 RESP 协议到 redis-plus-plus:Redis 客户端编程与 C++ 接口设计
开发语言·数据库·c++·redis·分布式·缓存·架构
变与不变8061 小时前
JS作用域,作用域链
前端·javascript·es6
雪芽蓝域zzs1 小时前
二十一节:进阶:用户列表批量删除功能
前端·javascript·vue.js
研☆香2 小时前
可选链运算符和空值合并运算符的使用
前端·javascript·vue.js
后台模板学习2 小时前
Rust vs Python 在用户认证模块中的实现:底层
开发语言·python·rust
雪芽蓝域zzs2 小时前
第二十七节:进阶:路由权限控制 + 404 / 401 全局异常页面开发
前端·javascript·html