概述

变量、作用域与内存是JavaScript编程的三个核心概念。理解它们对于编写高质量、可维护的代码至关重要。

1. 变量 (Variables)

1.1 变量的本质

在JavaScript中,变量不是直接存储值的容器,而是对内存中值的引用或指针。

// 变量本质上是标识符,指向内存中的值
let name = "JavaScript";  // name指向字符串"JavaScript"的内存位置
let age = 25;             // age指向数字25的内存位置

1.2 变量声明方式

1.2.1 var 声明

// 函数作用域,存在提升
function example() {
    console.log(x); // undefined (不是错误)
    var x = 5;
    console.log(x); // 5
}
 
// 等价于:
function example() {
    var x; // 声明被提升到顶部
    console.log(x); // undefined
    x = 5;
    console.log(x); // 5
}

var的特点:

  • 函数作用域或全局作用域
  • 存在变量提升(hoisting)
  • 可以重复声明
  • 可以在声明前使用(值为undefined)

1.2.2 let 声明

// 块作用域,不存在提升
function example() {
    console.log(x); // ReferenceError: Cannot access 'x' before initialization
    let x = 5;
    console.log(x); // 5
}
 
// 块作用域示例
{
    let blockVar = "块内变量";
    console.log(blockVar); // "块内变量"
}
console.log(blockVar); // ReferenceError: blockVar is not defined

let的特点:

  • 块作用域
  • 存在暂时性死区(Temporal Dead Zone)
  • 不可重复声明
  • 不能在声明前使用

1.2.3 const 声明

// 常量声明,必须初始化
const PI = 3.14159;
// const x; // SyntaxError: Missing initializer in const declaration
 
// 对于对象,const保证引用不变,但内容可变
const person = { name: "张三", age: 25 };
person.age = 26; // 可以修改属性
person.city = "北京"; // 可以添加属性
// person = {}; // TypeError: Assignment to constant variable
 
// 完全不可变的对象需要Object.freeze()
const frozenPerson = Object.freeze({ name: "李四", age: 30 });
frozenPerson.age = 31; // 静默失败(严格模式下会报错)
console.log(frozenPerson.age); // 30

const的特点:

  • 块作用域
  • 必须在声明时初始化
  • 不可重新赋值(但引用类型的内容可变)
  • 存在暂时性死区

1.3 变量声明最佳实践

// 推荐的声明顺序和风格
const config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000
}; // 不会改变的值使用const
 
let currentUser = null; // 会改变的值使用let
let isLoading = false;
 
// 避免使用var(除非有特殊需求)
// var globalVar = "不推荐"; 
 
// 一次声明多个变量
let x = 1, 
    y = 2, 
    z = 3;
 
// 语义化命名
const MAX_RETRY_COUNT = 3;
let userAccountBalance = 1000.50;

2. 作用域 (Scope)

2.1 作用域的定义

作用域决定了变量和函数的可访问性。JavaScript中有多种类型的作用域。

2.2 全局作用域 (Global Scope)

// 全局变量
var globalVar = "全局var变量";
let globalLet = "全局let变量";
const GLOBAL_CONST = "全局const变量";
 
// 在浏览器中,var声明的全局变量会成为window对象的属性
console.log(window.globalVar); // "全局var变量"
console.log(window.globalLet); // undefined
console.log(window.GLOBAL_CONST); // undefined
 
function accessGlobal() {
    console.log(globalVar); // 可以访问
    console.log(globalLet); // 可以访问
    console.log(GLOBAL_CONST); // 可以访问
}

2.3 函数作用域 (Function Scope)

function functionScope() {
    var functionVar = "函数内var变量";
    let functionLet = "函数内let变量";
    const FUNCTION_CONST = "函数内const变量";
    
    console.log(functionVar); // 可以访问
    console.log(functionLet); // 可以访问
    console.log(FUNCTION_CONST); // 可以访问
}
 
functionScope();
// console.log(functionVar); // ReferenceError
// console.log(functionLet); // ReferenceError
// console.log(FUNCTION_CONST); // ReferenceError

2.4 块作用域 (Block Scope)

// if语句块作用域
if (true) {
    var blockVar = "var不受块作用域限制";
    let blockLet = "let受块作用域限制";
    const BLOCK_CONST = "const受块作用域限制";
}
 
console.log(blockVar); // "var不受块作用域限制"
// console.log(blockLet); // ReferenceError
// console.log(BLOCK_CONST); // ReferenceError
 
// for循环块作用域
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
}
 
// 对比使用var的情况
for (var j = 0; j < 3; j++) {
    setTimeout(() => console.log(j), 100); // 输出: 3, 3, 3
}

2.5 作用域链 (Scope Chain)

const globalVar = "全局变量";
 
function outer() {
    const outerVar = "外层函数变量";
    
    function inner() {
        const innerVar = "内层函数变量";
        
        console.log(innerVar);  // 访问当前作用域
        console.log(outerVar);  // 访问外层作用域
        console.log(globalVar); // 访问全局作用域
        
        // 变量遮蔽(shadowing)
        const globalVar = "局部遮蔽全局变量";
        console.log(globalVar); // "局部遮蔽全局变量"
    }
    
    inner();
    // console.log(innerVar); // ReferenceError
}
 
outer();

2.6 词法作用域 (Lexical Scope)

// JavaScript使用词法作用域(静态作用域)
// 作用域在代码编写时就确定了,不是在运行时
 
function outer() {
    const x = 10;
    
    function inner() {
        console.log(x); // 总是访问outer函数的x
    }
    
    return inner;
}
 
function another() {
    const x = 20;
    const innerFunc = outer(); // 获取inner函数
    innerFunc(); // 输出10,而不是20
}
 
another();

3. 内存管理 (Memory Management)

3.1 内存分配

3.1.1 栈内存 (Stack Memory)

存储基本类型值和引用类型的引用地址。

// 基本类型存储在栈中
let num = 42;        // 栈中存储: num -> 42
let str = "Hello";   // 栈中存储: str -> "Hello"
let bool = true;     // 栈中存储: bool -> true
 
// 赋值操作创建新的独立副本
let num2 = num;      // num2获得42的副本
num = 100;           // 修改num不影响num2
console.log(num2);   // 42

3.1.2 堆内存 (Heap Memory)

存储引用类型的实际内容。

// 引用类型存储在堆中
let obj1 = { name: "张三" }; // 堆中存储对象,栈中存储引用
let obj2 = obj1;             // obj2获得相同的引用
 
obj1.name = "李四";          // 修改堆中的对象
console.log(obj2.name);      // "李四" - 因为指向同一个对象
 
// 创建新对象会分配新的堆内存
obj2 = { name: "王五" };     // obj2现在指向新对象
console.log(obj1.name);      // "李四" - obj1仍指向原对象
console.log(obj2.name);      // "王五"

3.2 垃圾回收 (Garbage Collection)

3.2.1 标记清除算法 (Mark-and-Sweep)

JavaScript引擎的主要垃圾回收机制。

function createObjects() {
    let obj1 = { name: "临时对象1" };
    let obj2 = { name: "临时对象2" };
    
    // 建立循环引用
    obj1.ref = obj2;
    obj2.ref = obj1;
    
    return obj1; // 只返回obj1的引用
}
 
let result = createObjects();
// 函数执行完后,obj2失去外部引用,会被垃圾回收
// obj1因为被result引用,不会被回收
 
result = null; // 现在obj1也失去引用,可以被回收
// 即使存在循环引用,标记清除算法也能正确处理

3.2.2 内存泄漏的常见场景

1. 全局变量

// 避免意外创建全局变量
function leakyFunction() {
    // 忘记声明变量,意外创建全局变量
    accidentalGlobal = "这会创建全局变量";
}
 
// 修复:使用严格模式和正确声明
'use strict';
function fixedFunction() {
    let properVariable = "正确的局部变量";
}

2. 定时器和回调

// 潜在的内存泄漏
function setupTimer() {
    let largeData = new Array(1000000).fill("data");
    
    setInterval(() => {
        console.log("定时器仍在运行");
        // largeData被闭包引用,无法被垃圾回收
    }, 1000);
}
 
// 修复:清理定时器
function setupTimerFixed() {
    let largeData = new Array(1000000).fill("data");
    
    let timer = setInterval(() => {
        console.log("定时器运行中");
    }, 1000);
    
    // 在适当时候清理
    setTimeout(() => {
        clearInterval(timer);
        timer = null;
    }, 10000);
}

3. 事件监听器

// 潜在泄漏:事件监听器持有引用
function addEventListeners() {
    let element = document.getElementById('button');
    let largeData = new Array(1000000).fill("data");
    
    element.addEventListener('click', function() {
        console.log(largeData.length); // largeData被引用
    });
}
 
// 修复:移除事件监听器
function addEventListenersFixed() {
    let element = document.getElementById('button');
    let largeData = new Array(1000000).fill("data");
    
    function clickHandler() {
        console.log(largeData.length);
    }
    
    element.addEventListener('click', clickHandler);
    
    // 清理函数
    return function cleanup() {
        element.removeEventListener('click', clickHandler);
        element = null;
        largeData = null;
    };
}

4. DOM引用

// 潜在泄漏:保持已删除DOM元素的引用
let elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
 
function removeButton() {
    let button = elements.button;
    button.remove(); // DOM中删除了,但elements对象中仍有引用
    
    // 修复:同时清理引用
    elements.button = null;
}

3.3 内存优化策略

3.3.1 对象池 (Object Pooling)

// 对象池减少内存分配和垃圾回收
class ObjectPool {
    constructor(createFn, resetFn, initialSize = 10) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        this.pool = [];
        
        // 预先创建对象
        for (let i = 0; i < initialSize; i++) {
            this.pool.push(this.createFn());
        }
    }
    
    get() {
        return this.pool.length > 0 ? 
            this.pool.pop() : 
            this.createFn();
    }
    
    release(obj) {
        this.resetFn(obj);
        this.pool.push(obj);
    }
}
 
// 使用示例
const vectorPool = new ObjectPool(
    () => ({ x: 0, y: 0 }),           // 创建函数
    (obj) => { obj.x = 0; obj.y = 0; } // 重置函数
);
 
function processVectors() {
    let v1 = vectorPool.get();
    v1.x = 10; v1.y = 20;
    
    // 使用v1...
    
    vectorPool.release(v1); // 回收到对象池
}

3.3.2 弱引用 (WeakMap/WeakSet)

// WeakMap不会阻止键的垃圾回收
let wm = new WeakMap();
let obj = { name: "test" };
 
wm.set(obj, "元数据");
console.log(wm.get(obj)); // "元数据"
 
obj = null; // obj可以被垃圾回收,WeakMap中的条目也会消失
 
// 对比普通Map
let normalMap = new Map();
let obj2 = { name: "test2" };
 
normalMap.set(obj2, "元数据");
obj2 = null; // obj2不能被垃圾回收,因为Map仍引用它

4. 实践应用

4.1 闭包与内存

// 闭包保持对外部变量的引用
function createCounter() {
    let count = 0;
    
    return {
        increment() {
            count++;
            return count;
        },
        decrement() {
            count--;
            return count;
        },
        getCount() {
            return count;
        }
    };
}
 
let counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
// count变量一直被闭包引用,不会被垃圾回收

4.2 模块模式与作用域

// 使用IIFE创建模块作用域
const Calculator = (function() {
    // 私有变量
    let lastResult = 0;
    
    // 私有函数
    function validateNumber(num) {
        return typeof num === 'number' && !isNaN(num);
    }
    
    // 公共API
    return {
        add(a, b) {
            if (!validateNumber(a) || !validateNumber(b)) {
                throw new Error('参数必须是有效数字');
            }
            lastResult = a + b;
            return lastResult;
        },
        
        getLastResult() {
            return lastResult;
        }
    };
})();
 
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.getLastResult()); // 8
// console.log(Calculator.lastResult); // undefined - 私有变量不可访问

4.3 性能监控

// 内存使用监控
function monitorMemory() {
    if (performance.memory) {
        const memory = performance.memory;
        console.log({
            used: `${Math.round(memory.usedJSHeapSize / 1024 / 1024)} MB`,
            total: `${Math.round(memory.totalJSHeapSize / 1024 / 1024)} MB`,
            limit: `${Math.round(memory.jsHeapSizeLimit / 1024 / 1024)} MB`
        });
    }
}
 
// 测量函数执行时间和内存影响
function measurePerformance(fn, name) {
    const startTime = performance.now();
    monitorMemory();
    
    const result = fn();
    
    const endTime = performance.now();
    monitorMemory();
    
    console.log(`${name} 执行时间: ${endTime - startTime}ms`);
    return result;
}

5. 常见问题与解决方案

5.1 变量提升问题

// 问题:var的提升导致意外行为
function problematicFunction() {
    console.log(x); // undefined,而不是ReferenceError
    
    if (false) {
        var x = 1; // 声明被提升,但赋值不会执行
    }
    
    console.log(x); // undefined
}
 
// 解决:使用let/const
function fixedFunction() {
    // console.log(x); // ReferenceError - 更明确的错误
    
    if (false) {
        let x = 1;
    }
    
    // console.log(x); // ReferenceError - x不在作用域中
}

5.2 循环中的异步操作

// 问题:var导致的闭包问题
function problematicLoop() {
    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100); // 输出: 3, 3, 3
    }
}
 
// 解决方案1:使用let
function fixedLoop1() {
    for (let i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
    }
}
 
// 解决方案2:使用IIFE
function fixedLoop2() {
    for (var i = 0; i < 3; i++) {
        (function(index) {
            setTimeout(() => console.log(index), 100); // 输出: 0, 1, 2
        })(i);
    }
}
 
// 解决方案3:使用bind
function fixedLoop3() {
    for (var i = 0; i < 3; i++) {
        setTimeout(console.log.bind(null, i), 100); // 输出: 0, 1, 2
    }
}

6. 最佳实践总结

6.1 变量声明

  • 优先使用const,需要重新赋值时使用let
  • 避免使用var(除非有特殊需求)
  • 使用语义化的变量名
  • 在变量使用前声明

6.2 作用域管理

  • 尽量减少全局变量的使用
  • 使用模块模式封装代码
  • 理解并正确使用块作用域
  • 注意变量遮蔽的影响

6.3 内存管理

  • 及时清理不需要的引用
  • 使用弱引用(WeakMap/WeakSet)处理临时关联
  • 避免意外的全局变量
  • 正确处理事件监听器和定时器
  • 考虑使用对象池优化高频操作

6.4 调试和监控

  • 使用开发者工具监控内存使用
  • 定期进行内存泄漏检测
  • 使用性能分析工具优化代码
  • 编写内存使用测试用例

参考资源