Skip to content

const 的不可变性边界:引用不变 vs 值不变

理解 const 的真正含义

在 JavaScript 中,const 关键字用于声明常量,但它所提供的"不可变性"常常被误解。实际上,const 只保证变量的绑定是不可变的,而不是变量所引用的值不可变。

const 的基本行为

javascript
// const 声明后不能重新赋值
const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable.

// const 必须在声明时初始化
const name; // SyntaxError: Missing initializer in const declaration

引用不变 vs 值不变

javascript
// 对于基本类型,const 确实提供了值不变性
const number = 42;
const string = "Hello";
const boolean = true;

number = 43;  // TypeError
string = "World";  // TypeError
boolean = false;  // TypeError

// 对于对象和数组,const 只保证引用不变
const obj = { name: "John" };
const arr = [1, 2, 3];

// 可以修改对象和数组的内容
obj.name = "Jane";
obj.age = 30;
arr.push(4);
arr[0] = 10;

console.log(obj); // { name: "Jane", age: 30 }
console.log(arr); // [10, 2, 3, 4]

// 但不能重新赋值整个对象或数组
obj = { name: "Bob" };  // TypeError
arr = [5, 6, 7];  // TypeError

深入理解对象和数组的 const 声明

对象属性的修改

javascript
const user = {
  name: "Alice",
  profile: {
    age: 25,
    email: "alice@example.com"
  }
};

// 可以修改属性
user.name = "Bob";
user.profile.age = 26;

// 可以添加新属性
user.location = "New York";

// 可以删除属性
delete user.profile.email;

console.log(user);
// { 
//   name: "Bob", 
//   profile: { age: 26 }, 
//   location: "New York" 
// }

数组元素的修改

javascript
const numbers = [1, 2, 3, 4, 5];

// 可以修改元素
numbers[0] = 10;

// 可以添加元素
numbers.push(6);
numbers.unshift(0);

// 可以删除元素
numbers.pop();
numbers.shift();

// 可以改变数组长度
numbers.length = 3;

console.log(numbers); // [0, 10, 2]

创建真正不可变的对象和数组

使用 Object.freeze()

javascript
const frozenObj = Object.freeze({
  name: "John",
  age: 30
});

// 无法修改属性(在严格模式下会抛出错误)
frozenObj.name = "Jane";  // 静默失败(非严格模式)
frozenObj.newProp = "test";  // 静默失败(非严格模式)
delete frozenObj.age;  // 静默失败(非严格模式)

console.log(frozenObj); // { name: "John", age: 30 }

// 注意:Object.freeze() 只是浅冻结
const deepObj = Object.freeze({
  name: "John",
  address: {
    city: "New York"
  }
});

deepObj.address.city = "Boston";  // 可以修改嵌套对象
console.log(deepObj.address.city); // "Boston"

深度冻结对象

javascript
function deepFreeze(obj) {
  // 获取对象的所有属性名
  Object.getOwnPropertyNames(obj).forEach(function(prop) {
    // 如果属性值是对象,递归冻结
    if (obj[prop] !== null && typeof obj[prop] === "object") {
      deepFreeze(obj[prop]);
    }
  });
  
  return Object.freeze(obj);
}

const deepFrozenObj = deepFreeze({
  name: "John",
  address: {
    city: "New York"
  }
});

// 现在无法修改嵌套对象
deepFrozenObj.address.city = "Boston";  // 静默失败
console.log(deepFrozenObj.address.city); // "New York"

使用不可变数据结构库

javascript
// 使用 Immutable.js 示例
// const { Map } = require('immutable');
// 
// const immutableMap = Map({ 
//   name: "John",
//   age: 30
// });
// 
// const updatedMap = immutableMap.set('name', 'Jane');
// 
// console.log(immutableMap.get('name')); // "John"
// console.log(updatedMap.get('name'));   // "Jane"

const 与 let 的选择

何时使用 const

javascript
// 1. 基本类型的常量
const PI = 3.14159;
const MAX_SIZE = 100;

// 2. 对象和数组的引用(当你不打算重新赋值整个对象/数组时)
const users = [];
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
};

// 3. 函数声明
const add = (a, b) => a + b;
const fetchData = async () => {
  const response = await fetch('/api/data');
  return response.json();
};

何时使用 let

javascript
// 1. 需要重新赋值的变量
let counter = 0;
counter++; // 可以修改

// 2. 循环变量
for (let i = 0; i < 10; i++) {
  // 循环逻辑
}

// 3. 条件赋值
let result;
if (condition) {
  result = "A";
} else {
  result = "B";
}

实际应用场景

React 中的组件状态

javascript
// 在 React 函数组件中
function UserProfile({ userId }) {
  // 使用 const 声明引用
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 使用 setUser 修改状态,而不是重新赋值 user 变量
    fetchUser(userId).then(userData => {
      setUser(userData);
      setLoading(false);
    });
  }, [userId]);
  
  // user 变量本身不会重新赋值,但其引用的对象会改变
  return (
    <div>
      {loading ? "Loading..." : user.name}
    </div>
  );
}

配置对象管理

javascript
// 应用配置
const appConfig = {
  version: "1.0.0",
  features: {
    darkMode: true,
    notifications: false
  }
};

// 可以修改配置项
appConfig.features.darkMode = false;
appConfig.features.notifications = true;

// 但不能替换整个配置对象
// appConfig = {}; // TypeError

最佳实践

1. 默认使用 const

javascript
// 推荐:默认使用 const
const processItems = (items) => {
  const processed = [];
  
  for (const item of items) {
    const result = transformItem(item);
    processed.push(result);
  }
  
  return processed;
};

2. 明确意图

javascript
// 清晰地表达变量不会被重新赋值的意图
const DATABASE_URL = process.env.DATABASE_URL;
const API_TIMEOUT = 5000;

3. 避免误解不可变性

javascript
// 当需要真正不可变的数据时,明确说明
const CONFIG = Object.freeze({
  API_ENDPOINT: '/api',
  MAX_RETRIES: 3
});

总结

const 关键字提供的是绑定不变性而不是值不变性。对于基本类型,这确实意味着值不可变;但对于对象和数组,它只防止重新赋值整个对象或数组,而不防止修改其内容。理解这一区别对于编写正确的 JavaScript 代码至关重要。在需要真正不可变的数据时,应使用 Object.freeze() 或专门的不可变数据结构库。