管理

JavaScript对象与元编程

Posted on 2026-08-13 01:20  lzhdim  阅读(1290)  评论(0)    收藏  举报

一、属性描述符

当你写 obj.name='张三' 时, 你真的了解这个 name 属性吗? 其实每个属性背后都藏着一组"属性描述符"(Property Descriptor), 就像一个人的身份证信息一样记录着这个属性的详细特征. 

const obj = { name'张三' };const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');console.log(descriptor);/* 输出:{  value: '张三',  writable: true,     // 可写  enumerable: true,   // 可枚举  configurable: true  // 可配置}*/

看到没? 一个简单的属性背后藏着这么多信息! 这些描述符控制着属性的行为:

  1. writable: 是否可以被修改.

  2. enumerable: 是否会在 for...in 循环中出现.

  3. configurable: 是否可以被删除或修改这些描述符本身.

 

1.1 精细控制属性行为: 

我们可以用 Object.defineProperty() 来精细控制属性: 

const user = {};
Object.defineProperty(user, 'secret', {  value'123456',  writablefalse,    // 不可写  enumerablefalse   // 不可枚举});
console.log(user.secret); // '123456'user.secret = '654321';   // 静默失败,严格模式下会报错console.log(user.secret); // 依然是 '123456'
// 这个属性不会出现在 for...in 循环中for (let key in user) {  console.log(key); // 啥也没有}
1.2 属性访问器: getter和setter我们可以用 getter 和 setter 创建"计算属性":
const person = {  firstName'张',  lastName'三',
  get fullName() {    return `${this.firstName} ${this.lastName}`;  },
  set fullName(value) {    [this.firstNamethis.lastName] = value.split(' ');  }};
console.log(person.fullName); // "张 三"person.fullName = '李 四';console.log(person.firstName); // "李"
用 Object.defineProperty 也能实现同样的效果: 
const person = { firstName'张'lastName'三' };
Object.defineProperty(person, 'fullName', {  get() {    return `${this.firstName} ${this.lastName}`;  },  set(value) {    [this.firstNamethis.lastName] = value.split(' ');  },  enumerabletrue});
1.3 批量操作属性描述符: 想一次操作多个属性? Object.defineProperties() 了解一下: 
const config = {};
Object.defineProperties(config, {  apiUrl: {    value'https://api.example.com',    writablefalse,    enumerabletrue  },  maxRetry: {    value3,    writabletrue  },  isDebug: {    get() {      return localStorage.getItem('debug') === 'true';    },    set(value) {      localStorage.setItem('debug', value);    }  }});

二、Symbol

ES6 引入了 Symbol 这个新基本类型, 它的主要用途是创建唯一的属性键, 避免命名冲突. 想象一下你给一个第三方库的对象添加属性时, 用字符串作为键名可能会不小心覆盖它原有的属性, Symbol 就解决了这个问题. 

const id = Symbol('这是一个独一无二的标识符');const obj = {};obj[id] = '秘密数据';
// 其他人不知道这个 Symbol,就无法访问这个属性console.log(obj[id]); // "秘密数据"console.log(Object.keys(obj)); // [] - 不会出现在常规遍历中
JavaScript内置了一些"知名 Symbol", 它们可以改变对象在某些操作下的行为: 
const myCollection = {  items: [123],  [Symbol.iterator]: function* () {    for (let item of this.items) {      yield item * 2// 让这个对象可迭代,并且每个值都乘以2    }  }};
// 现在可以用 for...of 遍历了for (let x of myCollection) {  console.log(x); // 2, 4, 6}

其他常用的知名 Symbol: 

  • Symbol.toStringTag: 自定义对象的 toString() 行为.

  • Symbol.hasInstance: 自定义 instanceof 的行为.

  • Symbol.toPrimitive: 自定义对象转换为原始值的逻辑.

 

Symbol实现私有属性: 

const _password = Symbol('password');
class User {  constructor(name, password) {    this.name = name;    this[_password] = password;  }
  checkPassword(pwd) {    return this[_password] === pwd;  }}
const user = new User('张三''123456');console.log(user[_password]); // 理论上可以访问,但需要知道这个 Symbolconsole.log(Object.keys(user)); // ['name'] - 不会暴露 Symbol 属性

扩展第三方库对象而不污染其属性: 

// 假设这是一个第三方库的对象const libraryObject = { /* ... */ };
// 我们想添加自己的数据const myData = Symbol('myData');libraryObject[myData] = { /* 我们的私有数据 */ };

三、Proxy与Reflect

3.1 Proxy基础

Proxy 是 ES6 引入的最强大的元编程特性之一, 它可以创建一个对象的代理, 拦截并自定义对象的基本操作. 

const target = { name'张三' };const handler = {  get(target, prop) {    console.log(`有人想获取属性 ${prop}`);    return target[prop] || '默认值';  },  set(target, prop, value) {    console.log(`有人想设置属性 ${prop} 为 ${value}`);    if (prop === 'age' && typeof value !== 'number') {      throw new TypeError('年龄必须是数字');    }    target[prop] = value;    return true// 表示设置成功  }};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 打印日志并返回 "张三"console.log(proxy.xxx);  // 打印日志并返回 "默认值"proxy.age = 30;          // 正常设置proxy.age = '三十';      // 抛出 TypeError

Proxy 可以拦截多达13种操作, 这里列举几个常用的: 

const richHandler = {  // 拦截属性读取  get(target, prop) { /* ... */ },
  // 拦截属性设置  set(target, prop, value) { /* ... */ },
  // 拦截 in 操作符  has(target, prop) { /* ... */ },
  // 拦截 delete 操作  deleteProperty(target, prop) { /* ... */ },
  // 拦截 Object.keys() 等操作  ownKeys(target) { /* ... */ },
  // 拦截函数调用(当目标是函数时)  apply(target, thisArg, args) { /* ... */ },
  // 拦截 new 操作(当目标是构造函数时)  construct(target, args) { /* ... */ },
  // 拦截 Object.getOwnPropertyDescriptor()  getOwnPropertyDescriptor(target, prop) { /* ... */ }};

3.2 Reflect

Reflect 是一个内置对象, 它提供拦截 JavaScript 操作的方法. 这些方法与 Proxy handler 的方法一一对应. Reflect 方法通常用于:

1. 更优雅地调用底层操作.

2. 与 Proxy 配合使用.

const obj = { a1 };
// 传统方式console.log('a' in obj);            // trueconsole.log(delete obj.a);          // true
// 使用 Reflectconsole.log(Reflect.has(obj, 'a')); // trueconsole.log(Reflect.deleteProperty(obj, 'a')); // true

在 Proxy 中使用 Reflect 可以保持默认行为: 

const proxy = new Proxy({ a1 }, {  get(target, prop, receiver) {    console.log(`获取属性 ${prop}`);    // 保持默认的获取行为    return Reflect.get(target, prop, receiver);  }});

四、元编程应用与技巧

4.1 可撤销的Proxy

Proxy 还支持创建"可撤销"的代理, 之后可以随时取消代理: 

const target = { name'张三' };const { proxy, revoke } = Proxy.revocable(target, {  get(target, prop) {    return `拦截的 ${prop}${target[prop]}`;  }});
console.log(proxy.name); // "拦截的 name: 张三"revoke(); // 撤销代理console.log(proxy.name); // 抛出 TypeError

4.2 多层代理

我们可以为同一个对象创建多层代理, 实现不同层次的拦截: 

const target = {};
// 第一层代理:日志记录const loggingProxy = new Proxy(target, {  get(target, prop) {    console.log(`读取 ${prop}`);    return target[prop];  },  set(target, prop, value) {    console.log(`设置 ${prop} = ${value}`);    target[prop] = value;    return true;  }});
// 第二层代理:验证const validatingProxy = new Proxy(loggingProxy, {  set(target, prop, value) {    if (prop === 'age' && typeof value !== 'number') {      throw new TypeError('年龄必须是数字');    }    return Reflect.set(target, prop, value);  }});
validatingProxy.name = '张三'// 记录日志并设置validatingProxy.age = '30';    // 抛出 TypeError

4.3 使用Proxy实现观察者模式: 

function createObservable(target, observer) {  return new Proxy(target, {    set(target, prop, value, receiver) {      const oldValue = target[prop];      const result = Reflect.set(target, prop, value, receiver);      if (result && oldValue !== value) {        observer(prop, oldValue, value);      }      return result;    }  });}
const user = { name'张三'age30 };const observableUser = createObservable(user, (prop, oldVal, newVal) => {  console.log(`属性 ${prop} 从 ${oldVal} 变更为 ${newVal}`);});
observableUser.name = '李四'// 打印变更日志observableUser.age = 31;     // 打印变更日志

4.4 实现一个简易的 ORM 模型: 

class Model {  constructor(data = {}) {    this._data = data;    return this._createProxy();  }
  _createProxy() {    return new Proxy(this, {      get(target, prop) {        // 如果是方法调用,直接返回        if (prop in target && typeof target[prop] === 'function') {          return target[prop].bind(target);        }
        // 否则从 _data 中获取        return target._data[prop];      },
      set(target, prop, value) {        // 如果是内部属性,直接设置        if (prop.startsWith('_') || prop in target) {          target[prop] = value;          return true;        }
        // 否则设置到 _data 中        target._data[prop] = value;
        // 模拟触发数据库更新        console.log(`[ORM] 更新字段 ${prop} = ${value}`);        return true;      },
      has(target, prop) {        return prop in target._data || prop in target;      }    });  }
  save() {    console.log('[ORM] 保存数据:'this._data);    // 这里应该是实际的保存逻辑  }}
// 使用示例const user = new Model({ name'张三'age30 });console.log(user.name); // "张三"user.age = 31;          // 打印更新日志user.email = 'zhangsan@example.com'// 打印更新日志user.save();            // 打印保存日志

 

Copyright © 2000-2022 Lzhdim Technology Software All Rights Reserved