一、属性描述符
当你写 obj.name='张三' 时, 你真的了解这个 name 属性吗? 其实每个属性背后都藏着一组"属性描述符"(Property Descriptor), 就像一个人的身份证信息一样记录着这个属性的详细特征.
const obj = { name: '张三' };const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');console.log(descriptor);/* 输出:{value: '张三',writable: true, // 可写enumerable: true, // 可枚举configurable: true // 可配置}*/
看到没? 一个简单的属性背后藏着这么多信息! 这些描述符控制着属性的行为:
-
writable: 是否可以被修改.
-
enumerable: 是否会在
for...in循环中出现. -
configurable: 是否可以被删除或修改这些描述符本身.
1.1 精细控制属性行为:
我们可以用 Object.defineProperty() 来精细控制属性:
1.2 属性访问器: getter和setter我们可以用 getter 和 setter 创建"计算属性":const user = {};Object.defineProperty(user, 'secret', {value: '123456',writable: false, // 不可写enumerable: false // 不可枚举});console.log(user.secret); // '123456'user.secret = '654321'; // 静默失败,严格模式下会报错console.log(user.secret); // 依然是 '123456'// 这个属性不会出现在 for...in 循环中for (let key in user) {console.log(key); // 啥也没有}
用const person = {firstName: '张',lastName: '三',get fullName() {return `${this.firstName} ${this.lastName}`;},set fullName(value) {[this.firstName, this.lastName] = value.split(' ');}};console.log(person.fullName); // "张 三"person.fullName = '李 四';console.log(person.firstName); // "李"
Object.defineProperty 也能实现同样的效果:
1.3 批量操作属性描述符: 想一次操作多个属性?const person = { firstName: '张', lastName: '三' };Object.defineProperty(person, 'fullName', {get() {return `${this.firstName} ${this.lastName}`;},set(value) {[this.firstName, this.lastName] = value.split(' ');},enumerable: true});
Object.defineProperties() 了解一下:
const config = {};Object.defineProperties(config, {apiUrl: {value: 'https://api.example.com',writable: false,enumerable: true},maxRetry: {value: 3,writable: true},isDebug: {get() {return localStorage.getItem('debug') === 'true';},set(value) {localStorage.setItem('debug', value);}}});
二、Symbol
ES6 引入了 Symbol 这个新基本类型, 它的主要用途是创建唯一的属性键, 避免命名冲突. 想象一下你给一个第三方库的对象添加属性时, 用字符串作为键名可能会不小心覆盖它原有的属性, Symbol 就解决了这个问题.
JavaScript内置了一些"知名 Symbol", 它们可以改变对象在某些操作下的行为:const id = Symbol('这是一个独一无二的标识符');const obj = {};obj[id] = '秘密数据';// 其他人不知道这个 Symbol,就无法访问这个属性console.log(obj[id]); // "秘密数据"console.log(Object.keys(obj)); // [] - 不会出现在常规遍历中
const myCollection = {items: [1, 2, 3],[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 = { a: 1 };// 传统方式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({ a: 1 }, {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: '张三', age: 30 };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: '张三', age: 30 });console.log(user.name); // "张三"user.age = 31; // 打印更新日志user.email = 'zhangsan@example.com'; // 打印更新日志user.save(); // 打印保存日志
![]() |
Austin Liu 刘恒辉
Project Manager and Software Designer E-Mail:lzhdim@163.com Blog:https://lzhdim.cnblogs.com 欢迎收藏和转载此博客中的博文,但是请注明出处,给笔者一个与大家交流的空间。谢谢大家。 |




浙公网安备 33010602011771号