《前端面试题:JavaScript 变量》
JavaScript 变量深度解析:从基础到高级的全面指南
一、JavaScript 变量基础
1. 变量声明方式
JavaScript 中有三种变量声明方式,每种都有其独特特性和适用场景:
声明方式 | 作用域 | 变量提升 | 可重新声明 | 初始值 | 特点 |
---|---|---|---|---|---|
var | 函数作用域 | ✅ | ✅ | undefined | 传统方式,存在变量提升 |
let | 块级作用域 | ❌(暂时性死区) | ❌ | 未初始化 | 现代推荐,更安全 |
const | 块级作用域 | ❌(暂时性死区) | ❌ | 必须初始化 | 声明常量,值不可变 |
// var 示例
var name = "Alice";
console.log(name); // "Alice"// let 示例
let age = 30;
age = 31; // 允许重新赋值// const 示例
const PI = 3.14159;
// PI = 3.14; // 错误:Assignment to constant variable
2. 变量命名规范
- 合法命名:字母、数字、下划线、美元符号开头
- 区分大小写:
myVar
和myvar
是不同的变量 - 推荐命名法:
- 驼峰命名:
myVariableName
- 常量全大写:
MAX_SIZE
- 驼峰命名:
- 避免保留字:不要使用
class
,function
等关键字
// 有效命名
let _privateVar;
let $element;
let userCount;// 无效命名
// let 123abc; // 不能以数字开头
// let my-var; // 不能包含连字符
二、作用域与作用域链
1. 作用域类型
函数作用域(var
)
function example() {var localVar = "I'm local";if (true) {var innerVar = "I'm accessible everywhere in function";}console.log(innerVar); // "I'm accessible everywhere in function"
}
// console.log(localVar); // 错误:localVar未定义
块级作用域(let
, const
)
function example() {if (true) {let blockVar = "I'm block scoped";const PI = 3.14;}// console.log(blockVar); // 错误:blockVar未定义// console.log(PI); // 错误:PI未定义
}
2. 作用域链
JavaScript 使用作用域链解析变量:
- 在当前作用域查找变量
- 如果未找到,向上一级作用域查找
- 直到全局作用域
- 如果仍未找到,抛出
ReferenceError
let globalVar = "Global";function outer() {let outerVar = "Outer";function inner() {let innerVar = "Inner";console.log(innerVar); // "Inner"(当前作用域)console.log(outerVar); // "Outer"(父作用域)console.log(globalVar); // "Global"(全局作用域)// console.log(undefinedVar); // ReferenceError}inner();
}outer();
三、变量提升(Hoisting)
1. var
的变量提升
console.log(hoistedVar); // undefined(不会报错)
var hoistedVar = "Value";
console.log(hoistedVar); // "Value"// 实际执行顺序:
// var hoistedVar;
// console.log(hoistedVar); // undefined
// hoistedVar = "Value";
// console.log(hoistedVar); // "Value"
2. let
和 const
的暂时性死区(TDZ)
console.log(notHoisted); // ReferenceError: Cannot access 'notHoisted' before initialization
let notHoisted = "Value";
3. 函数提升
// 函数声明会被提升
sayHello(); // "Hello!"function sayHello() {console.log("Hello!");
}// 函数表达式不会被提升
// sayGoodbye(); // TypeError: sayGoodbye is not a function
var sayGoodbye = function() {console.log("Goodbye!");
};
四、闭包(Closures)
1. 闭包概念
闭包是函数与其词法环境的组合,使函数可以访问其定义时的作用域。
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
2. 闭包应用场景
- 数据封装:创建私有变量
- 函数工厂:生成特定配置的函数
- 模块模式:组织代码结构
- 事件处理:保留上下文信息
// 模块模式示例
const calculator = (function() {let memory = 0;return {add: function(a, b) {const result = a + b;memory = result;return result;},subtract: function(a, b) {const result = a - b;memory = result;return result;},getMemory: function() {return memory;}};
})();console.log(calculator.add(5, 3)); // 8
console.log(calculator.getMemory()); // 8
五、内存管理
1. 垃圾回收机制
JavaScript 使用自动垃圾回收(GC),主要算法:
- 标记清除:从根对象开始标记所有可达对象,清除未标记的
- 引用计数:记录每个对象的引用次数,当为0时回收(已基本淘汰)
2. 常见内存泄漏场景
// 1. 意外全局变量
function createLeak() {leak = "I'm leaking!"; // 缺少声明,成为全局变量
}// 2. 未清除的定时器
const timer = setInterval(() => {console.log("Interval running");
}, 1000);
// 忘记 clearInterval(timer) 会导致内存泄漏// 3. 闭包持有外部引用
function createHugeClosure() {const hugeArray = new Array(1000000).fill("data");return function() {console.log(hugeArray.length); // 闭包持有hugeArray引用};
}const bigClosure = createHugeClosure();
// 即使不再需要,hugeArray仍保留在内存中
3. 避免内存泄漏的最佳实践
- 使用严格模式:
"use strict"
- 及时清除事件监听器和定时器
- 避免不必要的全局变量
- 谨慎使用闭包,解除不再需要的引用
- 使用
WeakMap
和WeakSet
存储临时数据
六、高级变量技巧
1. 解构赋值(ES6)
// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [3, 4, 5]// 对象解构
const user = { name: "Alice", age: 30, job: "Developer" };
const { name, ...details } = user;
console.log(name); // "Alice"
console.log(details); // { age: 30, job: "Developer" }// 参数解构
function printUser({ name, age }) {console.log(`${name} is ${age} years old`);
}
printUser(user); // "Alice is 30 years old"
2. 变量冻结与密封
const obj = { prop: "value" };// Object.freeze() - 完全冻结
Object.freeze(obj);
obj.prop = "new value"; // 静默失败(严格模式报错)
console.log(obj.prop); // "value"// Object.seal() - 可修改值但不可增删属性
const sealedObj = { key: "original" };
Object.seal(sealedObj);
sealedObj.key = "updated"; // 允许修改
sealedObj.newKey = "test"; // 静默失败(严格模式报错)
delete sealedObj.key; // 静默失败(严格模式报错)
3. 可选链操作符(ES2020)
const user = {profile: {name: "Alice",address: {city: "New York"}}
};// 传统方式
const city = user && user.profile && user.profile.address && user.profile.address.city;// 可选链
const safeCity = user?.profile?.address?.city;
console.log(safeCity); // "New York"// 函数调用
const result = someObject?.someMethod?.();
七、核心面试题解析
1. let
、const
和 var
的区别?
答案:
var
:函数作用域,变量提升,可重复声明let
:块级作用域,不可重复声明,存在暂时性死区const
:块级作用域,不可重复声明,声明时必须初始化,值不可变(对象属性可变)
2. 什么是闭包?有什么优缺点?
答案:
- 闭包:函数与其词法环境的组合,使函数可以访问定义时的作用域
- 优点:封装私有变量、创建函数工厂、模块化开发
- 缺点:可能导致内存泄漏(未及时释放引用)、性能消耗
3. 如何避免全局变量污染?
解决方案:
- 使用模块模式(IIFE)
- 使用
let
和const
替代var
- 采用 ES6 模块系统
- 使用命名空间对象
- 启用严格模式:
"use strict"
// 模块模式示例
const myModule = (function() {let privateVar = "Secret";return {publicMethod: function() {return privateVar;}};
})();
4. 什么是暂时性死区(TDZ)?
答案:
let
和const
声明的变量在声明前不可访问的区域- 从进入作用域到变量声明之间的区域
- 访问 TDZ 中的变量会抛出
ReferenceError
5. 如何实现深冻结对象?
function deepFreeze(obj) {Object.freeze(obj);Object.keys(obj).forEach(key => {if (typeof obj[key] === "object" && obj[key] !== null) {deepFreeze(obj[key]);}});return obj;
}const myObj = { a: 1, b: { c: 2 } };
deepFreeze(myObj);
myObj.b.c = 3; // 静默失败(严格模式报错)
八、最佳实践总结
-
变量声明:
- 默认使用
const
- 需要重新赋值时使用
let
- 避免使用
var
- 默认使用
-
作用域管理:
- 最小化变量作用域范围
- 避免创建不必要的全局变量
- 使用块级作用域限制变量可见性
-
命名规范:
- 使用驼峰命名法
- 常量使用全大写加下划线
- 避免使用缩写和单字符命名(循环变量除外)
-
内存优化:
- 及时清除事件监听器和定时器
- 避免创建不必要的闭包
- 使用
WeakMap
和WeakSet
存储临时关联数据
-
现代特性:
- 使用解构赋值简化代码
- 使用可选链安全访问深层属性
- 使用空值合并运算符设置默认值
核心原则:理解变量声明和作用域是掌握 JavaScript 的基石。通过合理选择声明方式、严格控制作用域范围、谨慎处理闭包和内存管理,可以编写出更安全、更高效、更易维护的 JavaScript 代码。记住:好的变量管理是优秀代码的开始!