typescript:第二章_变量声明

这是本文档旧的修订版!


第二章:变量声明

变量声明是编程的基础,TypeScript 继承了 JavaScript 的所有声明方式,并添加了类型注解。本章将深入讲解 let、const、var 的区别,解构赋值,展开运算符等重要概念。

在 ES6 之前,JavaScript 只有 var 这一种声明变量的方式。了解 var 的特性有助于理解为什么需要 let 和 const。

function example() {
  var x = 10;
  if (true) {
    var x = 20;  // 同一个变量!
    console.log(x);  // 20
  }
  console.log(x);  // 20
}

var 声明的变量具有函数作用域,而不是块级作用域。

console.log(hoisted);  // undefined(不会报错)
var hoisted = "I am hoisted";

// 实际上等价于:
var hoisted2;
console.log(hoisted2);
hoisted2 = "I am hoisted";
var x = 1;
var x = 2;  // 不会报错,覆盖了之前的值
console.log(x);  // 2
// 问题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"

let 是 ES6 引入的新的变量声明方式,提供了块级作用域。

function letExample() {
  let x = 10;
  if (true) {
    let x = 20;  // 不同的变量!
    console.log(x);  // 20
  }
  console.log(x);  // 10
}

块级作用域包括:

  1. if/for/while 语句块
  2. try/catch/finally 块
  3. 单独的 {} 块
console.log(letVar);  // Error: Cannot access 'letVar' before initialization
let letVar = "hello";

// 在声明之前访问 let 变量会导致错误
function deadZone() {
  // 暂时性死区开始
  console.log(value);  // Error!
  // 暂时性死区结束
  let value = 42;
}
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
// 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

const 声明的是常量,一旦被赋值就不能再改变。

const PI = 3.14159;
// PI = 3.14;  // Error: Cannot assign to 'PI' because it is a constant

const greeting = "Hello";
// greeting = "Hi";  // Error
// const uninitialized;  // Error: 'const' declarations must be initialized

const initialized = "I have a value";  // OK
// 基本类型 - 值不能改变
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
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);
}
// 好的实践:默认使用 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;
// 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;
}

解构赋值允许从数组或对象中提取值并赋值给变量。

// 基本解构
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

对象解构的意思就是从等式右边取出变量的值并供后续使用。

// 基本解构
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" }
// 对象参数解构
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}`;
}
// 数组解构带类型
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 };
}

展开运算符(…)可以将可迭代对象展开为多个元素。

// 复制数组
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']
// 复制对象(浅拷贝)
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 };
// 收集剩余参数
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!"
// ✓ 好的实践
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
// ✓ 明确的类型
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);
}
// ✓ 浅拷贝问题
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
// 块级作用域示例
{
  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";
}
// 使用 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
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");
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
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 };
}

本章我们学习了:

1. var、let、const 的区别 - 理解作用域、变量提升、重复声明的差异

2. let 和 const 的最佳实践 - 默认使用 const,必要时使用 let

3. 解构赋值 - 从数组和对象中提取值

4. 展开运算符 - 复制、合并、扩展数组和对象

5. 类型注解结合 - 在解构和展开时保持类型安全

将以下使用 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
};

解释

  1. name 和 config 不会改变引用,使用 const
  2. age 可能会改变,使用 let
  3. for 循环使用 let 确保每次迭代有独立的 i 绑定

使用解构赋值重写以下代码:

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;

完成以下函数,使用展开运算符实现功能:

// 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>;
}

实现一个类型安全的配置解析器:

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>

该主题尚不存在

您访问的页面并不存在。如果允许,您可以使用创建该页面按钮来创建它。

  • typescript/第二章_变量声明.1773042552.txt.gz
  • 最后更改: 2026/03/09 15:49
  • 张叶安