作用域与变量提升

1. 变量提升

JavaScript 引擎在执行代码之前,会先进行编译。在编译阶段,变量声明函数声明会被从它们在代码中的位置移动到当前作用域的顶部。

(1)var 提升

console.log(a); // undefined, 不报错
var a = 2;

等价于:

var a; // 提升,初始值为 undefined
console.log(a);
a = 2;

(2) 函数声明提升

函数声明 会整体提升(不仅仅是变量名)。

foo(); // "bar"

function foo() {
  console.log("bar");
}

(3) 函数表达式不会提升

foo(); // TypeError: foo is not a function

var foo = function bar() {
  console.log("bar");
};

这里的 var foo 被提升为 undefined,调用 undefined() 当然报错。

2. 块级作用域与 TDZ (暂时性死区)

ES6 引入了 letconst,它们使得 {} 变为块级作用域。

(1) 什么是 TDZ ?

TDZ (Temporal Dead Zone) 是指在使用 letconst 声明变量之前的代码区域。 在这段区域内,如果访问该变量,会抛出 ReferenceError

目的强制让开发人员在声明变量后再使用它,避免像 var 那样依赖变量提升导致的不可预测行为。

(2) 为什么会有 TDZ ?

在 JavaScript 中,let/const 也是存在变量提升的。 JS 引擎在编译阶段就已经知道变量的存在(及其作用域)。

  • var: 声明被提升,且会被直接初始化为 undefined。所以你可以在声明前访问它(值为 undefined)。
  • let/const: 声明被提升,但不会被初始化。直到执行流运行到声明的那一行代码时,变量才会被初始化。在初始化之前,变量处于“死区”。

(3) 常见陷阱与示例

基础示例

{
  // console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a = 1;
}

赋值陷阱

看似合法的赋值,实则是 TDZ。

let x = x; // Uncaught ReferenceError
// 解析:右侧的 x 在赋值操作执行前就已经处于 TDZ 中(因为 let x 已经提升了)。

函数参数默认值

function bar(x = y, y = 2) { // Uncaught ReferenceError: Cannot access 'y' before initialization
  return [x, y];
}
bar();
// 解析:参数也是有顺序的。x 的默认值用到了 y,但此时 y 还没被声明(在后面)。

typeof 陷阱

在 TDZ 中,连以前被认为是“安全”的操作 typeof 也会报错。

// 如果 a 没被声明 (此时是真正的 undeclared)
typeof a; // "undefined" (不报错)

// 如果下面有 let a
{
  typeof a; // Uncaught ReferenceError
  let a;
}

Class 同样受影响

类声明也不会像函数声明那样整体提升,必须先定义后使用。

const p = new Person(); // ReferenceError
class Person {}

3. 作用域链

在查找变量时,JS 引擎会先在当前作用域找,找不到就去外层作用域找,直到全局作用域。如果在全局还找不到,非严格模式下会自动创建全局变量(写入赋值),或者报错(读取)。

面试题:词法作用域 函数的作用域在定义时就确定了,而不是调用时。

function foo() {
  console.log(a);
}

function bar() {
  var a = 3;
  foo();
}

var a = 2;

bar(); // ?

答案:输出 2。因为 foo 是在全局作用域定义的,它的上层作用域是全局作用域(a=2),而不是 bar 的作用域。

4. 面试考点

Q:var,let,const 区别?

  • 作用域var 是函数作用域,let/const 是块级作用域。
  • 提升var 提升并初始化为 undefinedlet/const 提升但不初始化(TDZ)。
  • 重复声明var 允许,let/const 报错。
  • 全局对象挂载var a = 1 会导致 window.a === 1let a = 1 不会挂载到 window

Q: 立即执行函数 (IIFE) 的作用?

创建一个独立的函数作用域,避免污染全局变量。现代开发中多用模块化或块级作用域代替。