概述
变量、作用域与内存是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 调试和监控
- 使用开发者工具监控内存使用
- 定期进行内存泄漏检测
- 使用性能分析工具优化代码
- 编写内存使用测试用例