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

最佳实践

永远使用 let 或 const,不要使用 var。


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

问题说明

let 和 const 声明的变量在声明之前不可访问,这个区域称为"暂时性死区"(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 声明的变量属于函数作用域 ,不受 if、for 等代码块的限制;let 和 const 声明的变量属于块级作用域。

踩坑示例

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 = 1 或 let 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...of 或 forEach
默认声明 var const(优先)/ let(需重赋值时)
相关推荐
Dream-Y.ocean10 分钟前
鸿蒙PC平台逻控 PLC 编辑器适配实战:基于 Electron 壳方案的纯 JavaScript 梯形图编程与仿真引擎开发
javascript·编辑器·harmonyos
nice_lcj52017 分钟前
ThreadLocal 入门:它是怎么隔离数据的、为什么会导致内存泄漏
java·开发语言·后端·面试·并发编程·内存泄漏·threadlocal
紧跟先前的步伐29 分钟前
Go AI Agent 实战 28|Agent 的预算、超时与停止条件
开发语言·人工智能·后端·ai·golang
bksczm2 小时前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库
春涧草茶2 小时前
慢就是快16-1协程
开发语言·前端·python
前端小D2 小时前
JS核心底层
前端·javascript
Dovis(誓平步青云)2 小时前
启元 T1 Pro 人机相声搭档:语音-触摸双通道的启元机器人节奏引擎设计
开发语言·前端·javascript·microsoft·交互
做科研的周师兄2 小时前
【R语言实战】MixSIAR 氢氧同位素水源解析全流程:从混合空间校验到贝叶斯 MCMC 收敛诊断(后附完整代码)
java·大数据·开发语言
codeejun4 小时前
每日一Go·MySQL-11、覆盖索引 & 回表原理
javascript·数据库·mysql
.痞老板.4 小时前
java ee--计算机是如何工作的
java·开发语言·java-ee