一、属性描述符
当你写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()来精细控制属性:
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 keyin user) {console.log(key);// 啥也没有}1.2 属性访问器: getter和setter我们可以用 getter 和 setter 创建"计算属性":const person = {firstName:'张',lastName:'三',
getfullName() {return`${this.firstName}${this.lastName}`;},
setfullName(value) {[this.firstName,this.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.firstName,this.lastName] = value.split(' ');},enumerable:true});1.3 批量操作属性描述符: 想一次操作多个属性?Object.defineProperties()了解一下:constconfig = {};
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 就解决了这个问题.
const id =Symbol('这是一个独一无二的标识符');const obj = {};obj[id] ='秘密数据';
// 其他人不知道这个 Symbol,就无法访问这个属性console.log(obj[id]);// "秘密数据"console.log(Object.keys(obj));// [] - 不会出现在常规遍历中JavaScript内置了一些"知名 Symbol", 它们可以改变对象在某些操作下的行为:const myCollection = {items: [1,2,3],[Symbol.iterator]:function* () {for (let itemofthis.items) {yield item *2;// 让这个对象可迭代,并且每个值都乘以2}}};
// 现在可以用 for...of 遍历了for (let xof myCollection) {console.log(x);// 2, 4, 6}其他常用的知名 Symbol:
Symbol.toStringTag:自定义对象的toString()行为.Symbol.hasInstance:自定义instanceof的行为.Symbol.toPrimitive:自定义对象转换为原始值的逻辑.
Symbol实现私有属性:
const _password =Symbol('password');
classUser {constructor(name, password) {this.name = name;this[_password] = password;}
checkPassword(pwd) {returnthis[_password] === pwd;}}
const user =newUser('张三','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') {thrownewTypeError('年龄必须是数字');}target[prop] = value;returntrue;// 表示设置成功}};
const proxy =newProxy(target, handler);
console.log(proxy.name);// 打印日志并返回 "张三"console.log(proxy.xxx);// 打印日志并返回 "默认值"proxy.age =30;// 正常设置proxy.age ='三十';// 抛出 TypeErrorProxy 可以拦截多达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 =newProxy({a:1 }, {get(target, prop, receiver) {console.log(`获取属性${prop}`);// 保持默认的获取行为returnReflect.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);// 抛出 TypeError4.2 多层代理
我们可以为同一个对象创建多层代理, 实现不同层次的拦截:
const target = {};
// 第一层代理:日志记录const loggingProxy =newProxy(target, {get(target, prop) {console.log(`读取${prop}`);return target[prop];},set(target, prop, value) {console.log(`设置${prop}=${value}`);target[prop] = value;returntrue;}});
// 第二层代理:验证const validatingProxy =newProxy(loggingProxy, {set(target, prop, value) {if (prop ==='age' &&typeof value !=='number') {thrownewTypeError('年龄必须是数字');}returnReflect.set(target, prop, value);}});
validatingProxy.name ='张三';// 记录日志并设置validatingProxy.age ='30';// 抛出 TypeError4.3 使用Proxy实现观察者模式:
functioncreateObservable(target, observer) {returnnewProxy(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 模型:
classModel {constructor(data = {}) {this._data = data;returnthis._createProxy();}
_createProxy() {returnnewProxy(this, {get(target, prop) {// 如果是方法调用,直接返回if (propin target &&typeof target[prop] ==='function') {return target[prop].bind(target);}
// 否则从 _data 中获取return target._data[prop];},
set(target, prop, value) {// 如果是内部属性,直接设置if (prop.startsWith('_') || propin target) {target[prop] = value;returntrue;}
// 否则设置到 _data 中target._data[prop] = value;
// 模拟触发数据库更新console.log(`[ORM] 更新字段${prop}=${value}`);returntrue;},
has(target, prop) {return propin target._data || propin target;}});}
save() {console.log('[ORM] 保存数据:',this._data);// 这里应该是实际的保存逻辑}}
// 使用示例const user =newModel({name:'张三',age:30 });console.log(user.name);// "张三"user.age =31;// 打印更新日志user.email ='zhangsan@example.com';// 打印更新日志user.save();// 打印保存日志