Skip to content

对象属性操作:精细控制你的数据

在一栋公寓楼里,每个房间的门锁都有不同的权限设置。有些门可以自由进出,有些只能进不能出,还有些完全锁死。房东可能设置了某些房间不允许改造,某些房间的设施不能移除。JavaScript 对象的属性也有类似的权限系统——你可以控制属性是否可以被修改、删除、遍历,甚至可以在访问属性时执行自定义逻辑。这些精细的控制能力让我们能够构建更安全、更智能的数据结构。

属性描述符

每个对象属性都有一组隐藏的特性(attributes),这些特性决定了属性的行为。我们可以通过属性描述符(Property Descriptor)来查看和设置这些特性。

获取属性描述符

使用 Object.getOwnPropertyDescriptor() 可以获取单个属性的描述符:

javascript
let user = {
  name: "Alice",
  age: 25,
};

let descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// {
//   value: "Alice",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

获取所有属性的描述符:

javascript
let allDescriptors = Object.getOwnPropertyDescriptors(user);
console.log(allDescriptors);
// {
//   name: {
//     value: "Alice",
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   age: {
//     value: 25,
//     writable: true,
//     enumerable: true,
//     configurable: true
//   }
// }

属性特性详解

每个数据属性有四个特性:

  1. value - 属性的值
  2. writable - 是否可以修改值
  3. enumerable - 是否可以在遍历中出现
  4. configurable - 是否可以删除属性或修改其特性
javascript
let product = {};

Object.defineProperty(product, "name", {
  value: "Laptop",
  writable: false, // 不可修改
  enumerable: true, // 可枚举
  configurable: false, // 不可删除或重新配置
});

console.log(product.name); // "Laptop"

// 尝试修改 - 静默失败(严格模式下报错)
product.name = "Phone";
console.log(product.name); // "Laptop" - 未改变

// 尝试删除 - 失败
delete product.name;
console.log(product.name); // "Laptop" - 仍然存在

defineProperty() - 定义单个属性

Object.defineProperty() 允许我们精确定义属性的特性。

基本用法

javascript
let account = {};

Object.defineProperty(account, "balance", {
  value: 1000,
  writable: true,
  enumerable: true,
  configurable: true,
});

console.log(account.balance); // 1000
account.balance = 1500;
console.log(account.balance); // 1500

创建只读属性

javascript
let config = {};

Object.defineProperty(config, "API_KEY", {
  value: "abc123xyz",
  writable: false, // 只读
  enumerable: true,
  configurable: false,
});

console.log(config.API_KEY); // "abc123xyz"

// 无法修改
config.API_KEY = "newkey";
console.log(config.API_KEY); // "abc123xyz" - 未改变

// 无法删除
delete config.API_KEY;
console.log(config.API_KEY); // "abc123xyz" - 仍然存在

创建不可枚举属性

不可枚举的属性不会出现在 for...in 循环、Object.keys()Object.values() 中:

javascript
let person = {
  name: "Bob",
  age: 30,
};

// 添加一个不可枚举的属性
Object.defineProperty(person, "_id", {
  value: 12345,
  enumerable: false, // 不可枚举
  writable: true,
  configurable: true,
});

console.log(person._id); // 12345 - 可以访问

// 但在遍历中不可见
console.log(Object.keys(person)); // ["name", "age"]
for (let key in person) {
  console.log(key); // 只打印 "name" 和 "age"
}

// 使用 Object.getOwnPropertyNames() 可以看到
console.log(Object.getOwnPropertyNames(person));
// ["name", "age", "_id"]

这对于隐藏内部实现细节很有用。

实际应用:元数据存储

javascript
function addMetadata(obj, data) {
  Object.defineProperty(obj, "__metadata", {
    value: data,
    writable: false,
    enumerable: false, // 不在普通遍历中出现
    configurable: false,
  });
}

let user = {
  name: "Alice",
  email: "[email protected]",
};

addMetadata(user, {
  createdAt: new Date(),
  version: "1.0",
});

// 元数据可访问但不会干扰正常使用
console.log(user.__metadata);
// { createdAt: ..., version: "1.0" }

// 不会在序列化中出现
console.log(JSON.stringify(user));
// {"name":"Alice","email":"[email protected]"}

// 不会在遍历中出现
console.log(Object.keys(user)); // ["name", "email"]

defineProperties() - 定义多个属性

Object.defineProperties() 可以一次定义多个属性:

javascript
let rectangle = {};

Object.defineProperties(rectangle, {
  width: {
    value: 100,
    writable: true,
    enumerable: true,
  },
  height: {
    value: 50,
    writable: true,
    enumerable: true,
  },
  area: {
    get() {
      return this.width * this.height;
    },
    enumerable: true,
  },
  perimeter: {
    get() {
      return 2 * (this.width + this.height);
    },
    enumerable: true,
  },
});

console.log(rectangle.width); // 100
console.log(rectangle.area); // 5000
console.log(rectangle.perimeter); // 300

rectangle.width = 150;
console.log(rectangle.area); // 7500 - 自动计算

Getter 和 Setter

访问器属性(accessor properties)使用 getter 和 setter 函数来控制属性的读取和赋值。

基本 Getter 和 Setter

javascript
let user = {
  firstName: "John",
  lastName: "Doe",

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set fullName(value) {
    [this.firstName, this.lastName] = value.split(" ");
  },
};

console.log(user.fullName); // "John Doe"

user.fullName = "Jane Smith";
console.log(user.firstName); // "Jane"
console.log(user.lastName); // "Smith"
console.log(user.fullName); // "Jane Smith"

使用 defineProperty 定义 Getter/Setter

javascript
let temperature = {
  _celsius: 25, // 内部存储
};

Object.defineProperty(temperature, "celsius", {
  get() {
    return this._celsius;
  },
  set(value) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero!");
    }
    this._celsius = value;
  },
  enumerable: true,
});

Object.defineProperty(temperature, "fahrenheit", {
  get() {
    return (this._celsius * 9) / 5 + 32;
  },
  set(value) {
    this._celsius = ((value - 32) * 5) / 9;
  },
  enumerable: true,
});

console.log(temperature.celsius); // 25
console.log(temperature.fahrenheit); // 77

temperature.fahrenheit = 86;
console.log(temperature.celsius); // 30
console.log(temperature.fahrenheit); // 86

// 验证会生效
try {
  temperature.celsius = -300;
} catch (e) {
  console.log(e.message); // "Temperature below absolute zero!"
}

实际应用:数据验证

javascript
function createUser(firstName, lastName, age) {
  let user = {
    _firstName: firstName,
    _lastName: lastName,
    _age: age,
  };

  Object.defineProperties(user, {
    firstName: {
      get() {
        return this._firstName;
      },
      set(value) {
        if (typeof value !== "string" || value.length === 0) {
          throw new Error("First name must be a non-empty string");
        }
        this._firstName = value;
      },
      enumerable: true,
    },

    lastName: {
      get() {
        return this._lastName;
      },
      set(value) {
        if (typeof value !== "string" || value.length === 0) {
          throw new Error("Last name must be a non-empty string");
        }
        this._lastName = value;
      },
      enumerable: true,
    },

    age: {
      get() {
        return this._age;
      },
      set(value) {
        if (typeof value !== "number" || value < 0 || value > 150) {
          throw new Error("Age must be between 0 and 150");
        }
        this._age = value;
      },
      enumerable: true,
    },

    fullName: {
      get() {
        return `${this._firstName} ${this._lastName}`;
      },
      enumerable: true,
    },
  });

  return user;
}

let user = createUser("Alice", "Johnson", 28);
console.log(user.fullName); // "Alice Johnson"

user.age = 29; // 有效
console.log(user.age); // 29

try {
  user.age = 200; // 无效
} catch (e) {
  console.log(e.message); // "Age must be between 0 and 150"
}

懒加载属性

Getter 可以实现属性的懒加载——只在第一次访问时计算值:

javascript
let data = {
  _cache: null,

  get expensiveComputation() {
    if (this._cache === null) {
      console.log("Computing...");
      // 模拟耗时计算
      this._cache = Array.from({ length: 1000 }, (_, i) => i * i);
    }
    return this._cache;
  },
};

console.log("First access:");
console.log(data.expensiveComputation.length); // Computing... 1000

console.log("Second access:");
console.log(data.expensiveComputation.length); // 1000 (不再计算)

计算属性

javascript
let product = {
  price: 100,
  quantity: 5,
  taxRate: 0.1,

  get subtotal() {
    return this.price * this.quantity;
  },

  get tax() {
    return this.subtotal * this.taxRate;
  },

  get total() {
    return this.subtotal + this.tax;
  },
};

console.log(`Price: $${product.price}`);
console.log(`Quantity: ${product.quantity}`);
console.log(`Subtotal: $${product.subtotal}`); // $500
console.log(`Tax: $${product.tax}`); // $50
console.log(`Total: $${product.total}`); // $550

product.quantity = 10;
console.log(`New total: $${product.total}`); // $1100 - 自动更新

属性存在性检查

检查属性是否存在有多种方法,每种都有其特定用途。

in 操作符

检查属性是否存在(包括继承的属性):

javascript
let obj = {
  name: "Alice",
  age: 25,
};

console.log("name" in obj); // true
console.log("email" in obj); // false
console.log("toString" in obj); // true - 继承自 Object.prototype

hasOwnProperty / Object.hasOwn

只检查对象自身的属性:

javascript
let obj = {
  name: "Alice",
};

console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("toString")); // false

// 推荐使用 Object.hasOwn (ES2022)
console.log(Object.hasOwn(obj, "name")); // true
console.log(Object.hasOwn(obj, "toString")); // false

直接检查 undefined

javascript
let obj = {
  name: "Alice",
  email: undefined,
};

console.log(obj.name !== undefined); // true
console.log(obj.email !== undefined); // false
console.log(obj.phone !== undefined); // false

// 注意:这无法区分 undefined 值和不存在的属性
console.log("email" in obj); // true - 属性存在
console.log(obj.email !== undefined); // false - 但值是 undefined

删除属性

使用 delete 操作符删除属性:

javascript
let user = {
  name: "Bob",
  age: 30,
  email: "[email protected]",
};

delete user.email;
console.log(user); // { name: "Bob", age: 30 }
console.log(user.email); // undefined

// delete 返回布尔值
console.log(delete user.age); // true
console.log(delete user.phone); // true - 即使属性不存在

// 无法删除不可配置的属性
Object.defineProperty(user, "id", {
  value: 123,
  configurable: false,
});

console.log(delete user.id); // false - 无法删除
console.log(user.id); // 123 - 仍然存在

属性枚举

控制属性是否在遍历中出现。

for...in 循环

遍历对象自身和继承的可枚举属性:

javascript
let parent = { inherited: "value" };
let child = Object.create(parent);
child.own = "own value";

for (let key in child) {
  console.log(key); // 打印 "own" 和 "inherited"
}

// 只遍历自身属性
for (let key in child) {
  if (Object.hasOwn(child, key)) {
    console.log(key); // 只打印 "own"
  }
}

Object.keys/values/entries

只遍历对象自身的可枚举属性:

javascript
let obj = {
  a: 1,
  b: 2,
};

Object.defineProperty(obj, "c", {
  value: 3,
  enumerable: false,
});

console.log(Object.keys(obj)); // ["a", "b"]
console.log(Object.values(obj)); // [1, 2]
console.log(Object.entries(obj)); // [["a", 1], ["b", 2]]

// c 不在结果中

Object.getOwnPropertyNames

获取所有自身属性(包括不可枚举的):

javascript
let obj = {
  visible: 1,
};

Object.defineProperty(obj, "hidden", {
  value: 2,
  enumerable: false,
});

console.log(Object.keys(obj)); // ["visible"]
console.log(Object.getOwnPropertyNames(obj)); // ["visible", "hidden"]

实战案例:响应式对象

创建一个简单的响应式系统,当属性改变时自动触发回调:

javascript
function createReactive(obj, onChange) {
  let proxy = {};

  for (let key in obj) {
    if (Object.hasOwn(obj, key)) {
      let value = obj[key];

      Object.defineProperty(proxy, key, {
        get() {
          return value;
        },
        set(newValue) {
          let oldValue = value;
          value = newValue;
          onChange(key, oldValue, newValue);
        },
        enumerable: true,
        configurable: true,
      });
    }
  }

  return proxy;
}

let state = createReactive(
  {
    count: 0,
    message: "Hello",
  },
  (key, oldValue, newValue) => {
    console.log(`${key} changed from ${oldValue} to ${newValue}`);
  }
);

state.count = 1; // count changed from 0 to 1
state.message = "World"; // message changed from Hello to World
console.log(state.count); // 1

实战案例:只读视图

创建对象的只读视图,防止意外修改:

javascript
function createReadOnlyView(obj) {
  let proxy = {};

  for (let key in obj) {
    if (Object.hasOwn(obj, key)) {
      Object.defineProperty(proxy, key, {
        get() {
          return obj[key];
        },
        set() {
          throw new Error(`Cannot modify readonly property "${key}"`);
        },
        enumerable: true,
      });
    }
  }

  return proxy;
}

let data = {
  name: "Alice",
  age: 25,
};

let readOnly = createReadOnlyView(data);

console.log(readOnly.name); // "Alice"

try {
  readOnly.name = "Bob";
} catch (e) {
  console.log(e.message); // Cannot modify readonly property "name"
}

// 但原对象仍可修改
data.name = "Charlie";
console.log(readOnly.name); // "Charlie" - 反映原对象的变化

实战案例:属性访问日志

记录属性访问和修改:

javascript
function createLoggingProxy(obj, logFn) {
  let proxy = {};

  for (let key in obj) {
    if (Object.hasOwn(obj, key)) {
      let value = obj[key];

      Object.defineProperty(proxy, key, {
        get() {
          logFn("GET", key, value);
          return value;
        },
        set(newValue) {
          logFn("SET", key, value, newValue);
          value = newValue;
        },
        enumerable: true,
      });
    }
  }

  return proxy;
}

let logs = [];
let user = createLoggingProxy(
  {
    name: "Alice",
    age: 25,
  },
  (action, key, ...values) => {
    logs.push({ action, key, values, timestamp: new Date() });
  }
);

user.name; // GET
user.age = 26; // SET
user.name = "Bob"; // SET

console.log(logs);
// [
//   { action: "GET", key: "name", values: ["Alice"], timestamp: ... },
//   { action: "SET", key: "age", values: [25, 26], timestamp: ... },
//   { action: "SET", key: "name", values: ["Alice", "Bob"], timestamp: ... }
// ]

常见陷阱与最佳实践

1. configurable: false 是不可逆的

javascript
let obj = {};

Object.defineProperty(obj, "key", {
  value: "value",
  configurable: false,
});

// ❌ 无法重新配置
try {
  Object.defineProperty(obj, "key", {
    configurable: true,
  });
} catch (e) {
  console.log("Cannot redefine property"); // 会执行这里
}

2. getter 没有 setter 时属性只读

javascript
let obj = {
  get value() {
    return 42;
  },
};

console.log(obj.value); // 42
obj.value = 100; // 静默失败
console.log(obj.value); // 42 - 未改变

3. 不要在 getter 中修改对象

javascript
// ❌ 不好的实践
let counter = {
  _count: 0,
  get count() {
    return this._count++; // 每次访问都改变状态
  },
};

console.log(counter.count); // 0
console.log(counter.count); // 1 - 令人困惑

// ✅ getter 应该是纯函数
let betterCounter = {
  _count: 0,
  get count() {
    return this._count; // 只读取,不修改
  },
  increment() {
    this._count++; // 通过方法修改
  },
};

4. 注意 this 绑定

javascript
let obj = {
  value: 42,
  get getValue() {
    return this.value;
  },
};

console.log(obj.getValue); // 42

let fn = obj.getValue;
console.log(fn); // undefined - this 丢失

// ✅ 使用箭头函数或 bind
let boundFn = obj.getValue.bind(obj);

总结

对象属性操作为我们提供了精细的控制能力:

  • 属性描述符 - 控制属性的 writableenumerableconfigurable 特性
  • defineProperty/defineProperties - 精确定义属性及其特性
  • Getter/Setter - 在访问属性时执行自定义逻辑
  • 属性枚举 - 控制哪些属性在遍历中可见
  • 属性删除 - 使用 delete 操作符或 configurable 特性

这些特性让我们能够:

  • 创建只读或不可删除的属性
  • 实现计算属性和数据验证
  • 隐藏内部实现细节
  • 构建响应式系统和数据代理
  • 实现懒加载和缓存机制

掌握对象属性操作是编写健壮、可维护 JavaScript 代码的关键。虽然日常开发中可能不会频繁使用这些高级特性,但在构建框架、库或需要精确控制对象行为时,它们是不可或缺的工具。