变量与作用域

掌握JavaScript变量声明、作用域和提升机制

变量声明

JavaScript中有三种声明变量的方式:

关键字 作用域 可重新赋值 可重新声明 提升 暂时性死区
var 函数作用域 是(初始化为undefined)
let 块作用域 是(未初始化)
const 块作用域 是(未初始化)

变量声明历史

在ES6之前,JavaScript只有var一种变量声明方式。ES6引入了letconst,解决了var的一些问题,如变量提升、块级作用域等。

变量声明示例

变量声明
// var 声明 (ES5)
var name = "张三";
var age = 25;

// let 声明 (ES6)
let score = 95;
let isActive = true;

// const 声明 (ES6) - 常量
const PI = 3.14159;
const API_URL = "https://api.example.com";

// 重新赋值
name = "李四";      // ✅ 允许
score = 100;        // ✅ 允许
// PI = 3.14;       // ❌ 错误: 常量不能重新赋值

// 重新声明
// var name = "王五";  // ✅ 允许 (但不推荐)
// let score = 90;    // ❌ 错误: 不能重新声明

// const 对象的特殊情况
const person = { name: "John", age: 30 };
person.age = 31;        // ✅ 允许 - 修改对象属性
// person = { name: "Jane" };  // ❌ 错误 - 不能重新赋值
注意: const声明创建一个值的只读引用,但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新赋值。如果const变量引用的是一个对象,那么对象本身的内容是可以修改的。

作用域(Scope)

作用域决定了变量的可访问范围:

1. 全局作用域

全局作用域
// 全局变量 - 在任何地方都可访问
var globalVar = "我是全局变量";
let globalLet = "我也是全局变量";

function testGlobal() {
    console.log(globalVar);  // 可以访问
    console.log(globalLet);  // 可以访问
}

testGlobal();
注意: 在全局作用域中使用var声明的变量会成为全局对象的属性(在浏览器中是window对象),而使用letconst声明的变量不会。

2. 函数作用域 (var)

函数作用域
function functionScope() {
    var functionVar = "我在函数内部";
    
    if (true) {
        var ifVar = "我在if块内";
    }
    
    console.log(functionVar);  // ✅ 可以访问
    console.log(ifVar);        // ✅ 可以访问 (var没有块级作用域)
}

functionScope();
// console.log(functionVar);  // ❌ 错误: 在函数外部无法访问

3. 块级作用域 (let, const)

块级作用域
function blockScope() {
    if (true) {
        let blockLet = "我在块内部";
        const blockConst = "我也是";
        var blockVar = "我使用var";
    }
    
    console.log(blockVar);    // ✅ 可以访问 (var)
    // console.log(blockLet);   // ❌ 错误: let有块级作用域
    // console.log(blockConst); // ❌ 错误: const有块级作用域
}

blockScope();

4. 模块作用域 (ES6)

在ES6模块中,每个模块都有自己的作用域,变量默认不是全局的:

模块作用域
// module.js
let privateVar = "我是模块私有的";
export const publicVar = "我可以被其他模块导入";

// main.js
import { publicVar } from './module.js';
console.log(publicVar);  // ✅ 可以访问
// console.log(privateVar);  // ❌ 错误: 无法访问

变量提升(Hoisting)

JavaScript会将变量和函数声明提升到其作用域的顶部:

变量提升
// 实际执行顺序:
console.log(a);       // undefined (不是错误!)
var a = 5;

// 相当于:
var a;              // 声明被提升
console.log(a);       // undefined
a = 5;               // 赋值保持不变

// let 和 const 的暂时性死区
// console.log(b);   // ❌ 错误: 不能在初始化前访问
let b = 10;

// 函数提升
sayHello();           // ✅ 可以调用
function sayHello() {
    console.log("Hello!");
}
最佳实践: 总是先声明变量再使用,避免依赖变量提升的行为。

函数表达式 vs 函数声明

函数声明会被提升,但函数表达式不会:

函数提升
// 函数声明 - 会被提升
sayHello(); // ✅ 可以调用
function sayHello() {
    console.log("Hello!");
}

// 函数表达式 - 不会被提升
// sayGoodbye();  // ❌ 错误: sayGoodbye不是函数
var sayGoodbye = function() {
    console.log("Goodbye!");
};

作用域链

JavaScript使用作用域链来解析变量:

作用域链示例
let global = "全局变量";

function outer() {
    let outerVar = "外部变量";
    
    function inner() {
        let innerVar = "内部变量";
        
        console.log(innerVar);  // 内部变量 - 当前作用域
        console.log(outerVar);  // 外部变量 - 父作用域
        console.log(global);    // 全局变量 - 全局作用域
    }
    
    inner();
}

outer();

词法环境

在JavaScript中,每个执行上下文都有一个关联的词法环境。词法环境包含两个主要部分:

  1. 环境记录 - 存储变量和函数声明的实际位置
  2. 对外部词法环境的引用 - 用于解析外部变量

当查找变量时,JavaScript引擎会:

  1. 在当前词法环境中查找
  2. 如果找不到,沿着作用域链向上查找
  3. 直到全局词法环境
  4. 如果仍然找不到,返回undefined(非严格模式)或报错(严格模式)

闭包(Closure)

闭包是指函数能够访问并记住其词法作用域,即使函数在其作用域外执行:

闭包示例
function createCounter() {
    let count = 0;
    
    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// 每个闭包都有自己独立的作用域
const counter2 = createCounter();
console.log(counter2()); // 1 (独立的计数)

闭包的实际应用

闭包在JavaScript中有许多实际应用:

闭包应用
// 1. 数据私有化
function createPerson(name) {
    let _name = name;  // 私有变量
    
    return {
        getName: function() { return _name; },
        setName: function(newName) { _name = newName; }
    };
}

const person = createPerson("John");
console.log(person.getName());  // "John"
person.setName("Jane");
console.log(person.getName());  // "Jane"
// console.log(person._name);  // undefined - 无法直接访问

// 2. 函数工厂
function createMultiplier(multiplier) {
    return function(x) {
        return x * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15
注意: 闭包会导致外部函数的变量无法被垃圾回收,如果滥用可能会导致内存泄漏。

实践练习

作用域与闭包演示

👆 请点击上方按钮进行演示操作

选择不同的演示按钮来探索JavaScript的各种功能和用法

练习代码
// 练习1: 作用域理解
var x = 1;
let y = 2;
const z = 3;

{
    var x = 10;     // 重新声明 (不推荐)
    let y = 20;     // 新的块级变量
    // const z = 30;  // ❌ 错误: 不能重新声明
}

console.log(x);  // 10 (var没有块级作用域)
console.log(y);  // 2 (外部的y)
console.log(z);  // 3

// 练习2: 闭包应用
function createMultiplier(multiplier) {
    return function(number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

// 练习3: 变量提升
console.log(hoistedVar);  // undefined
var hoistedVar = "我被提升了";

// console.log(hoistedLet);  // ❌ 错误: 不能在初始化前访问
let hoistedLet = "我也有提升,但有暂时性死区";

常见问题与解答

1. 什么时候使用var、let和const?

现代JavaScript开发中:

  • 优先使用const,用于不会重新赋值的变量
  • 需要重新赋值的变量使用let
  • 避免使用var,除非有特殊需求

2. 什么是暂时性死区?

暂时性死区是指从代码块开始到letconst声明语句执行之间的区域。在这段时间内访问变量会抛出引用错误。

3. 闭包会导致内存泄漏吗?

闭包本身不会导致内存泄漏,但如果闭包持有对大对象的引用,并且该闭包的生命周期很长,可能会导致内存无法被回收。在不需要时应及时解除对闭包的引用。

最佳实践

  • 优先使用 const,除非需要重新赋值
  • 使用 let 代替 var
  • 避免使用未声明的变量
  • 变量名要有意义,使用camelCase命名法
  • 在作用域顶部声明变量
  • 使用严格模式避免意外创建全局变量
  • 注意闭包的内存管理
  • 使用模块化组织代码,避免全局命名空间污染

下一步学习

掌握了变量和作用域后,接下来可以学习: