JavaScript的初始化是指在代码执行过程中为变量、函数、类等分配内存并赋予初始值的过程。理解这一过程对于掌握代码执行顺序、调试作用域问题以及编写健壮的程序至关重要。

在ECMAScript规范中,初始化是指为变量在内存中分配空间并设置初始值的过程。这个过程与变量声明的方式(var、let、const、function等)密切相关,不同声明方式的初始化时机和行为有所不同。

ECMAScript规范中的声明类型分类

一、变量初始化

JavaScript中变量的完整生命周期可分为三个阶段:

1. 声明(Declaration)

在编译阶段,引擎在作用域中注册变量标识符,并为其分配内存空间,但此时尚未赋值。var、let、const、函数和类声明都会被提升,但具体行为不同。

声明阶段

  • 行为:在作用域中注册变量标识符。

  • 时机:代码执行前(编译阶段)。

  • 内存影响:分配内存空间,但值未定义。

示例:声明阶段

function example() {
    // 编译阶段:引擎在这里"看到"了声明
    // var a;  // 声明,分配内存
    // let b;  // 声明,但进入TDZ
    // const c; // 声明,但必须立即初始化(错误)
    
    console.log("执行开始");
    // 运行时才会真正处理下面的声明
    var a;
    let b;
    const c = 10;
}

2. 初始化(Initialization)

在代码执行阶段,为变量赋予初始值。 var 变量在提升时被初始化为 undefined;let 和 const 变量则在执行到声明语句时才被初始化,此前处于 “暂时性死区(TDZ)”。

定义/初始化阶段

  • 行为:给变量分配初始值。

  • 时机:执行到声明语句时。

  • 内存影响:内存空间被写入初始值

示例:定义/初始化阶段

function initExample() {
    // 阶段1:声明(编译时)
    // var x; (提升)
    // let y; (进入TDZ)
    
    // 阶段2:定义/初始化(运行时)
    console.log(typeof x); // undefined - 已定义初始值undefined
    var x = 5; // 这里实际是初始化为5(不是先定义undefined再赋值)
    
    // console.log(y); // ReferenceError - 在TDZ中,未初始化
    let y = 10; // 初始化为10
    
    const z = 15; // 声明和初始化同时完成
}

3. 赋值(Assignment)

修改变量的值,可在初始化后多次进行。const 变量不允许重新赋值,但其指向的对象或数组内容可以修改。

赋值阶段

  • 行为:改变已初始化变量的值。

  • 时机:执行到赋值语句时。

  • 内存影响:内存中的值被更新。

示例:赋值阶段

function assignmentExample() {
    let count = 0; // 声明并初始化为0
    
    count = 1;      // 赋值:将内存中的0改为1
    count = count + 1; // 赋值:读取当前值,计算新值,写入内存
    count++;        // 赋值:递增操作
    
    const MAX = 100; // 声明并初始化
    // MAX = 200;    // 错误:const变量不能重新赋值
}

不同声明方式的初始化行为:

1. var声明的初始化

  • 声明阶段:var声明的变量会被提升到作用域顶部(全局或函数作用域)。

  • 初始化阶段:初始化与声明 同时发生(在代码执行前),默认初始值为 undefined。

  • 赋值阶段:在代码执行到赋值语句时完成。

console.log(a); // undefined(声明+初始化已完成)
var a = 10;     // 赋值阶段

2. let/const声明的初始化

  • 声明阶段:let/const声明的变量也会被提升,但进入 暂时性死区(Temporal Dead Zone, TDZ)

  • 初始化阶段:初始化在代码执行到 声明语句位置  时才完成(与 var 的区别核心)。

    • let变量初始值为 undefined。

    • const 变量必须在声明时立即初始化(否则抛出语法错误)。

  • 赋值阶段:

    • let 变量可在初始化后重新赋值。

    • const 变量一旦初始化就不能修改(引用不可变)。

console.log(b); // 报错:Cannot access 'b' before initialization(仍在TDZ)
let b = 20;     // 执行到此处完成初始化+赋值

const c = 30;   // 声明时必须初始化
c = 40;         // 报错:Assignment to constant variable

let变量的初始值分析

  • 在初始化阶段完成后:如果let变量只声明不赋值(如let a;),其初始值确实是 undefined。

  • 在初始化阶段前:变量处于暂时性死区,无法访问,此时讨论"初始值"没有意义。

3. function声明的初始化

  • 声明、初始化、赋值 三个阶段在代码执行前 一次性完成 ,因此函数声明可以在定义前调用。

foo(); // 正常执行:"Hello"(声明+初始化+赋值已完成)
function foo() { console.log("Hello"); }

注意

函数声明:编译时完成声明+初始化+赋值(全提升!)

函数表达式:声明提升,但赋值在运行时(注意区别!)

4. class声明的初始化

  • 类似let/const,class声明也会被提升并进入 暂时性死区

  • 初始化在代码执行到声明语句时完成,因此不能在定义前使用类。

  • 用 new 调用,声明进入TDZ,初始化在声明语句完成,实例值在构造器中赋值。

new Bar(); // 报错:Cannot access 'Bar' before initialization(仍在TDZ)
class Bar {}

扩展:ECMAScript规范使用以下术语描述初始化过程:

  • LexicalEnvironment:词法环境,存储变量声明、函数声明等标识符的绑定。

  • VariableEnvironment:变量环境,专门用于 var声明的绑定(ES6引入块级作用域后新增)。

  • Temporal Dead Zone (TDZ):暂时性死区,指从变量声明被提升到执行到声明语句之间的区域,在此区域内访问变量会抛出错误。

不同声明方式的特点

特性

var

let

const

提升

是(TDZ)

是(TDZ)

作用域类型

函数作用域/全局

块级作用域/全局

块级作用域/全局

重复声明

允许

不允许

不允许

必须初始化

全局对象属性

扩展知识:

  • var 声明的变量在声明它的函数内部是可见的,函数外部无法直接访问,这是 var 的核心作用域规则,见示例一。

  • 当 var 在函数外部(全局环境)声明时,变量会成为全局对象(浏览器中是 window,Node.js 中是 global)的属性,属于全局作用域,见示例二。

  • 与 let/const 不同,var 没有块级作用域。在 if、for、while 等块结构中用 var 声明的变量,会泄露到块外部的函数作用域或全局作用域,见示例三。

总结

  • var 声明的变量会被提升到其所在函数作用域的顶部(全局声明则提升到全局作用域顶部)。

  • 提升后会初始化为 undefined。

示例一:

function test() {
  var insideVar = "函数内的变量";
  console.log(insideVar); // "函数内的变量"
}

test();
console.log(insideVar); // ReferenceError: insideVar is not defined

示例二:

var globalVar = "全局变量";
console.log(globalVar); // "全局变量"
console.log(window.globalVar); // "全局变量"(浏览器环境)

示例三:

// if 块中的 var
if (true) {
  var blockVar = "块内的变量";
}
console.log(blockVar); // "块内的变量"(泄露到外部作用域)

// for 循环中的 var
for (var i = 0; i < 3; i++) {
  // 循环体
}
console.log(i); // 3(泄露到外部作用域)
二、函数初始化

函数声明

  • 完全提升:声明和定义同时提升到作用域顶部。

  • 可以在声明前调用。

hello(); // "Hello" (可以在声明前调用)
function hello() {
  console.log("Hello");
}

函数表达式:

  • 变量部分提升(取决于声明方式:var/let/const)。

  • 函数定义不提升,不能在声明前调用。

console.log(greet); // undefined (var声明提升)
greet(); // TypeError (函数定义未提升,不能调用)
var greet = function() {
  console.log("Greet");
};
三、类初始化

类声明与表达式:

  • 声明提升但处于TDZ。

  • 类定义在执行时完成。

类成员初始化顺序:

  1. 基类的字段初始化(包括箭头函数)。

  2. 基类的构造函数。

  3. 派生类的字段初始化。

  4. 派生类的构造函数。

四、实例初始化

当使用 new 关键字创建类实例时,初始化顺序为:

  1. 创建一个新对象。

  2. 将新对象的 __proto__ 指向类的原型。

  3. 初始化实例字段(包括箭头函数定义的方法)。

  4. 执行构造函数。

实例方法初始化对比

class Person {
  // 箭头函数作为实例字段:在构造函数执行前初始化
  sayHello = () => console.log("Hello");
  
  constructor() {
    this.sayHello(); // 可以正常调用,因为已初始化
  }
  
  // 传统原型方法:在类定义时添加到原型,构造函数执行前可用
  sayGoodbye() {
    console.log("Goodbye");
  }
}

new Person(); // 输出: Hello
五、对象、数组与ES6+初始化特性

1. 对象与数组初始化

支持字面量、构造函数 Object.create、Array.from 等方式。

// 字面量初始化
const person = {
    name: "John",
    age: 30,
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

// 构造函数初始化
function Person(name, age) {
    this.name = name;
    this.age = age;
}
const john = new Person("John", 30);

// 使用Object.create
const prototype = { greeting: "Hello" };
const obj = Object.create(prototype);
obj.name = "John";

// 使用Array.from
const arr4 = Array.from("hello"); // ['h','e','l','l','o']

2. 解构赋值与默认值
一行代码完成声明、初始化、赋值!

// 数组解构
const [first, second = "default"] = [1];
console.log(second); // "default"

// 对象解构
const { name, age = 25 } = { name: "John" };
console.log(age); // 25

// 函数参数解构
function connect({ host = "localhost", port = 8080 } = {}) {
    console.log(`连接到 ${host}:${port}`);
}
connect(); // 连接到 localhost:8080

3. 可选链与空值合并

const user = {
    profile: {
        name: "李阳",
        address: {
            city: "仙朝"
        }
    }
};

// 安全访问嵌套属性
const city = user?.profile?.address?.city ?? "未知城市";
console.log(city); // "仙朝"

总结

根据ECMAScript规范,初始化是变量生命周期中为其分配内存并设置初始值的阶段,其行为因声明方式而异:

  • var:声明时立即初始化(undefined)。

  • let:执行到声明语句时初始化(undefined)。

  • const:声明时必须立即初始化,且不可修改。

  • function:声明时立即完成初始化(即编译时初始化,可提前调用)。

  • class:执行到声明语句时初始化(不可提前使用)。

一句话总结:声明是“注册名字”,初始化是“分配内存+设初值”,赋值是“改值”!理解这三阶段,才能真正掌握JS变量机制,告别undefined、ReferenceError!