这是本文档旧的修订版!
第二章:变量声明
本章概述
变量声明是编程的基础,TypeScript 继承了 JavaScript 的所有声明方式,并添加了类型注解。本章将深入讲解 let、const、var 的区别,解构赋值,展开运算符等重要概念。
2.1 var 声明(传统方式)
在 ES6 之前,JavaScript 只有 var 这一种声明变量的方式。了解 var 的特性有助于理解为什么需要 let 和 const。
2.1.1 函数作用域
function example() {
var x = 10;
if (true) {
var x = 20; // 同一个变量!
console.log(x); // 20
}
console.log(x); // 20
}
var 声明的变量具有函数作用域,而不是块级作用域。
2.1.2 变量提升
console.log(hoisted); // undefined(不会报错) var hoisted = "I am hoisted"; // 实际上等价于: var hoisted2; console.log(hoisted2); hoisted2 = "I am hoisted";
2.1.3 重复声明
var x = 1; var x = 2; // 不会报错,覆盖了之前的值 console.log(x); // 2
2.1.4 为什么避免使用 var
// 问题1:循环中的闭包问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(不是 0, 1, 2)
// 问题2:意外的变量覆盖
var message = "Hello";
if (true) {
var message = "World"; // 意外覆盖了外部变量
}
console.log(message); // "World"
2.2 let 声明(块级作用域)
let 是 ES6 引入的新的变量声明方式,提供了块级作用域。
2.2.1 块级作用域
function letExample() {
let x = 10;
if (true) {
let x = 20; // 不同的变量!
console.log(x); // 20
}
console.log(x); // 10
}
块级作用域包括:
- if/for/while 语句块
- try/catch/finally 块
- 单独的 {} 块
2.2.2 暂时性死区(Temporal Dead Zone)
console.log(letVar); // Error: Cannot access 'letVar' before initialization
let letVar = "hello";
// 在声明之前访问 let 变量会导致错误
function deadZone() {
// 暂时性死区开始
console.log(value); // Error!
// 暂时性死区结束
let value = 42;
}
2.2.3 不允许重复声明
let x = 1;
// let x = 2; // Error: Cannot redeclare block-scoped variable 'x'
// 但在不同作用域可以
let y = 1;
if (true) {
let y = 2; // OK
console.log(y); // 2
}
console.log(y); // 1
2.2.4 解决循环闭包问题
// let 正确工作的循环
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2
// 每次迭代创建新的绑定
for (let i = 0; i < 3; i++) {
let i = "inner"; // 可以重新声明,因为是新的作用域
console.log(i);
}
// 输出:inner, inner, inner
2.3 const 声明(常量)
const 声明的是常量,一旦被赋值就不能再改变。
2.3.1 基本用法
const PI = 3.14159; // PI = 3.14; // Error: Cannot assign to 'PI' because it is a constant const greeting = "Hello"; // greeting = "Hi"; // Error
2.3.2 必须初始化
// const uninitialized; // Error: 'const' declarations must be initialized const initialized = "I have a value"; // OK
2.3.3 常量引用 vs 值
// 基本类型 - 值不能改变
const count = 5;
// count = 10; // Error
// 对象 - 引用不能改变,但属性可以修改
const person = {
name: "Alice",
age: 25
};
person.age = 26; // OK
person.name = "Bob"; // OK
// person = {}; // Error: Cannot assign to 'person'
// 数组同理
const numbers = [1, 2, 3];
numbers.push(4); // OK
numbers[0] = 10; // OK
// numbers = []; // Error
2.3.4 使用 Object.freeze 深度冻结
const frozenPerson = Object.freeze({
name: "Alice",
age: 25,
address: {
city: "Beijing"
}
});
// frozenPerson.age = 26; // Error in strict mode
// 注意:Object.freeze 是浅冻结
frozenPerson.address.city = "Shanghai"; // 仍然可以修改!
// 深度冻结函数
function deepFreeze<T>(obj: T): T {
const propNames = Object.getOwnPropertyNames(obj);
for (const name of propNames) {
const value = (obj as any)[name];
if (value && typeof value === "object") {
deepFreeze(value);
}
}
return Object.freeze(obj);
}
2.4 let vs const 选择指南
2.4.1 默认使用 const
// 好的实践:默认使用 const
const apiUrl = "https://api.example.com";
const maxRetries = 3;
const config = {
timeout: 5000,
retries: 3
};
// 只有在需要重新赋值时才使用 let
let currentUser = null;
let retryCount = 0;
let isLoading = false;
2.4.2 何时使用 let
// 1. 循环计数器
for (let i = 0; i < 10; i++) {
console.log(i);
}
// 2. 需要重新赋值的变量
let score = 0;
score += 10;
score *= 2;
// 3. 条件赋值
let result: string;
if (condition) {
result = "A";
} else {
result = "B";
}
// 4. try-catch 中的变量
let data: User;
try {
data = JSON.parse(jsonString);
} catch (e) {
data = defaultUser;
}
2.5 解构赋值(Destructuring)
解构赋值允许从数组或对象中提取值并赋值给变量。
2.5.1 数组解构
// 基本解构 const colors = ["red", "green", "blue"]; const [first, second, third] = colors; console.log(first); // "red" console.log(second); // "green" // 跳过元素 const [primary, , tertiary] = colors; console.log(tertiary); // "blue" // 剩余元素 const [head, ...tail] = [1, 2, 3, 4, 5]; console.log(head); // 1 console.log(tail); // [2, 3, 4, 5] // 默认值 const [a = 10, b = 20] = [1]; console.log(a); // 1 console.log(b); // 20 // 交换变量 let x = 1, y = 2; [x, y] = [y, x]; console.log(x, y); // 2, 1
2.5.2 对象解构
对象解构的意思就是从等式右边取出变量的值并供后续使用。
// 基本解构
const user = {
name: "Alice",
age: 25,
email: "alice@example.com"
};
const { name, age, email } = user;//从user中取出参数供后面使用
console.log(name); // "Alice"
// 重命名
const { name: userName, age: userAge } = user; 从user中取出参数并重命名供后面使用
console.log(userName); // "Alice"
// 默认值
const { name: n, country = "Unknown" } = user;
console.log(country); // "Unknown"
// 嵌套解构
const nested = {
user: {
profile: {
firstName: "Alice",
lastName: "Smith"
}
}
};
const { user: { profile: { firstName, lastName } } } = nested;
console.log(firstName); // "Alice"
// 剩余属性
const { name: n2, ...rest } = user;
console.log(rest); // { age: 25, email: "alice@example.com" }
2.5.3 函数参数解构
// 对象参数解构
function greetUser({ name, age }: { name: string; age: number }): string {
return `Hello ${name}, you are ${age} years old`;
}
// 带默认值
function createUser({
name,
age = 18,
role = "user"
}: {
name: string;
age?: number;
role?: string;
}) {
return { name, age, role };
}
// 数组参数解构
function processCoordinates([x, y]: [number, number]): string {
return `X: ${x}, Y: ${y}`;
}
// 嵌套解构
function processUser({
name,
address: { city, country }
}: {
name: string;
address: { city: string; country: string };
}) {
return `${name} lives in ${city}, ${country}`;
}
2.5.4 解构与类型注解
// 数组解构带类型
const [firstNum, secondNum]: [number, number] = [1, 2];
// 对象解构带类型
const { id, title }: { id: number; title: string } = { id: 1, title: "Hello" };
// 接口配合解构
interface Point {
x: number;
y: number;
}
function movePoint({ x, y }: Point, dx: number, dy: number): Point {
return { x: x + dx, y: y + dy };
}
2.6 展开运算符(Spread Operator)
展开运算符(…)可以将可迭代对象展开为多个元素。
2.6.1 数组展开
// 复制数组 const original = [1, 2, 3]; const copy = [...original]; // 连接数组 const arr1 = [1, 2]; const arr2 = [3, 4]; const combined = [...arr1, ...arr2]; // [1, 2, 3, 4] // 在数组中添加元素 const withPrefix = [0, ...original]; // [0, 1, 2, 3] const withSuffix = [...original, 4]; // [1, 2, 3, 4] const inMiddle = [0, ...original, 4]; // [0, 1, 2, 3, 4] // 数组转参数 const numbers = [1, 2, 3, 4, 5]; const max = Math.max(...numbers); // 5 // 字符串转数组 const chars = [..."hello"]; // ['h', 'e', 'l', 'l', 'o']
2.6.2 对象展开
// 复制对象(浅拷贝)
const original = { a: 1, b: 2 };
const copy = { ...original };
// 合并对象
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// 覆盖属性
const defaults = { host: "localhost", port: 3000 };
const config = { ...defaults, port: 8080 }; // { host: "localhost", port: 8080 }
// 条件展开
const condition = true;
const dynamic = {
a: 1,
...(condition && { b: 2 }), // 条件为真时才包含 b
};
// 添加新属性
const user = { name: "Alice" };
const userWithAge = { ...user, age: 25 };
2.6.3 函数参数(Rest Parameters)
// 收集剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
// 与普通参数结合
function greet(greeting: string, ...names: string[]): string {
return `${greeting}, ${names.join(" and ")}!`;
}
greet("Hello", "Alice", "Bob", "Charlie");
// "Hello, Alice and Bob and Charlie!"
2.7 类型声明的最佳实践
2.7.1 优先使用 const
// ✓ 好的实践
const API_URL = "https://api.example.com";
const MAX_ITEMS = 100;
const user = { name: "Alice", age: 25 };
// ✗ 避免
let API_URL = "https://api.example.com"; // 不会改变的值不需要 let
2.7.2 解构时添加类型
// ✓ 明确的类型
interface User {
name: string;
age: number;
}
function processUser(user: User): void {
const { name, age }: { name: string; age: number } = user;
// 或使用接口
const { name: n, age: a }: User = user;
}
// ✓ 更简洁的方式
function processUser2({ name, age }: User): void {
console.log(name, age);
}
2.7.3 展开运算符的注意事项
// ✓ 浅拷贝问题
const user = { name: "Alice", address: { city: "Beijing" } };
const userCopy = { ...user };
userCopy.address.city = "Shanghai";
console.log(user.address.city); // "Shanghai" - 原始对象也被修改了!
// ✓ 深拷贝方案
const deepCopy = JSON.parse(JSON.stringify(user));
// 或使用库如 lodash.cloneDeep
2.8 作用域与闭包
2.8.1 块级作用域详解
// 块级作用域示例
{
let blockScoped = "I am in block";
const alsoBlockScoped = "Me too";
}
// console.log(blockScoped); // Error: not defined
// if 块级作用域
if (true) {
let ifScoped = "if block";
}
// for 块级作用域
for (let i = 0; i < 1; i++) {
let forScoped = "for block";
}
2.8.2 闭包与变量捕获
// 使用 let 的正确闭包
function createCounters() {
const counters = [];
for (let i = 0; i < 3; i++) {
counters.push(() => i);
}
return counters;
}
const counters = createCounters();
console.log(counters[0]()); // 0
console.log(counters[1]()); // 1
console.log(counters[2]()); // 2
// 对比:使用 var 的问题
function createCountersVar() {
const counters = [];
for (var i = 0; i < 3; i++) {
counters.push(() => i);
}
return counters;
}
const countersVar = createCountersVar();
console.log(countersVar[0]()); // 3
console.log(countersVar[1]()); // 3
console.log(countersVar[2]()); // 3
2.9 高级解构技巧
2.9.1 解构结合类型别名
type Coordinates = [number, number, number?];
const parseLocation = (loc: string): Coordinates => {
const [x, y, z] = loc.split(",").map(Number);
return z !== undefined ? [x, y, z] : [x, y];
};
const [lat, lng, alt = 0] = parseLocation("39.9,116.3,100");
2.9.2 解构与重命名
interface APIResponse {
data: {
user_name: string; // 蛇形命名
user_age: number;
};
}
const response: APIResponse = {
data: {
user_name: "Alice",
user_age: 25
}
};
// 解构时转换为驼峰命名
const {
data: {
user_name: userName,
user_age: userAge
}
} = response;
console.log(userName, userAge); // "Alice" 25
2.9.3 解构默认值
interface Config {
host?: string;
port?: number;
ssl?: boolean;
}
const defaultConfig: Required<Config> = {
host: "localhost",
port: 3000,
ssl: false
};
function createServer(userConfig: Config = {}) {
const {
host = defaultConfig.host,
port = defaultConfig.port,
ssl = defaultConfig.ssl
} = userConfig;
return { host, port, ssl };
}
2.10 本章小结
本章我们学习了:
1. var、let、const 的区别 - 理解作用域、变量提升、重复声明的差异
2. let 和 const 的最佳实践 - 默认使用 const,必要时使用 let
3. 解构赋值 - 从数组和对象中提取值
4. 展开运算符 - 复制、合并、扩展数组和对象
5. 类型注解结合 - 在解构和展开时保持类型安全
2.11 练习题
练习 1:变量声明转换
将以下使用 var 的代码改写为使用 let 和 const,并解释为什么:
var name = "Alice";
var age = 25;
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
var config = {
apiUrl: "https://api.example.com",
timeout: 5000
};
参考答案
const name = "Alice"; // 不会重新赋值
let age = 25; // 可能会改变
for (let i = 0; i < 5; i++) { // let 创建块级作用域
setTimeout(function() {
console.log(i); // 正确输出 0, 1, 2, 3, 4
}, 100);
}
const config = { // 对象引用不变
apiUrl: "https://api.example.com",
timeout: 5000
};
解释:
- name 和 config 不会改变引用,使用 const
- age 可能会改变,使用 let
- for 循环使用 let 确保每次迭代有独立的 i 绑定
练习 2:解构赋值练习
使用解构赋值重写以下代码:
const user = {
id: 1,
profile: {
firstName: "Alice",
lastName: "Smith",
address: {
city: "Beijing",
country: "China"
}
},
hobbies: ["reading", "coding", "gaming"]
};
const firstName = user.profile.firstName;
const city = user.profile.address.city;
const firstHobby = user.hobbies[0];
参考答案
const {
profile: {
firstName,
address: { city }
},
hobbies: [firstHobby]
} = user;
// 或者分步解构
const { profile } = user;
const { firstName, address: { city } } = profile;
const [firstHobby] = user.hobbies;
练习 3:展开运算符应用
完成以下函数,使用展开运算符实现功能:
// 1. 合并配置,用户配置优先级更高
function mergeConfig(defaultConfig: object, userConfig: object): object {
// 实现
}
// 2. 将数组元素插入到另一个数组的指定位置
function insertAt<T>(arr: T[], index: number, ...elements: T[]): T[] {
// 实现
}
// 3. 移除对象中的指定属性
function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
// 实现
}
参考答案
// 1. 合并配置
function mergeConfig(defaultConfig: object, userConfig: object): object {
return { ...defaultConfig, ...userConfig };
}
// 2. 插入元素
function insertAt<T>(arr: T[], index: number, ...elements: T[]): T[] {
return [...arr.slice(0, index), ...elements, ...arr.slice(index)];
}
// 3. 移除属性
function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const result = { ...obj };
for (const key of keys) {
delete result[key];
}
return result;
}
// 或使用更函数式的写法
function omit2<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const keySet = new Set(keys);
return Object.fromEntries(
Object.entries(obj).filter(([key]) => !keySet.has(key as K))
) as Omit<T, K>;
}
练习 4:类型安全的配置解析
实现一个类型安全的配置解析器:
interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
database: string;
ssl?: boolean;
}
// 实现 parseConfig 函数,从环境变量解析配置
// 使用解构和默认值
function parseConfig(env: NodeJS.ProcessEnv): DatabaseConfig {
// 你的实现
}
参考答案
function parseConfig(env: NodeJS.ProcessEnv): DatabaseConfig {
const {
DB_HOST = "localhost",
DB_PORT = "5432",
DB_USERNAME = "postgres",
DB_PASSWORD = "",
DB_DATABASE = "myapp",
DB_SSL = "false"
} = env;
return {
host: DB_HOST,
port: parseInt(DB_PORT, 10),
username: DB_USERNAME,
password: DB_PASSWORD,
database: DB_DATABASE,
ssl: DB_SSL === "true"
};
}
// 使用示例
const config = parseConfig(process.env);
</details>