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(需重赋值时) |