← JavaScript Proxy isExtensible 没有下一篇了 →

JavaScript Proxy ownKeys

原创 2026-03-25 JavaScript 已有人查阅

JavaScript Proxy 的 ownKeys :属性列表的控制

在 JavaScript 的 Proxy 体系中,ownKeys 是一个非常核心且功能强大的拦截器。它专门用于拦截那些获取对象自身属性键列表的操作,比如 Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Reflect.ownKeys() 以及 for...in 循环。通过这个,我们可以精确控制哪些属性对外可见,哪些属性被隐藏。

什么是 ownKeys ?

简单来说,ownKeys 让我们能够“篡改”对象属性列表的返回结果。当代码试图获取某个代理对象的所有自身属性键时,我们定义的 ownKeys 方执行,而不是直接返回目标对象的真实属性列表。

的语法结构:

const handler = {
    ownKeys: function(target) {
        // target: 被代理的原始对象
        // 返回值: 必须返回一个可枚举的对象(通常是数组),包含属性键(字符串或 Symbol)
    }
};

参数解析:

  • target:这是被代理的原始对象。我们可以基于这个对象来获取原始属性列表,然后进行过滤或添加。

返回值:
这个必须返回一个可枚举对象(比如数组),数组元素必须是字符串或 Symbol 类型。返回其他类型的值会抛出 TypeError

核心约束规则

ownKeys 有一个非常重要的约束:返回的属性列表必须包含目标对象上所有不可配置(configurable: false)的自身属性。这个约束是为了保证代理对象的行为与目标对象保持逻辑一致性,防止因为隐藏必要属性而破坏 JavaScript 内部机制。

从基础示例开始

我们先从一个简单的例子入手,看看 ownKeys 的基本工作方式。

示例 1:基础拦截与自定义返回

在这个例子中,我们忽略目标对象的真实属性,直接返回一个自定义的属性列表。

// 代码号:learning-proxy-ownkeys-01
const targetObject = { a: 1, b: 2, c: 3 };
const handler = {
    ownKeys: function(target) {
        console.log('[ownKeys] 正在获取属性列表');
        // 自定义返回的属性键
        return ['x', 'y', 'z'];
    }
};
const proxyInstance = new Proxy(targetObject, handler);

console.log(Object.keys(proxyInstance));        // 输出: ['x', 'y', 'z']
console.log(Object.getOwnPropertyNames(proxyInstance)); // 输出: ['x', 'y', 'z']
console.log(Reflect.ownKeys(proxyInstance));    // 输出: ['x', 'y', 'z']

个人经验: 在开发中,ownKeys 是我最常用的 Proxy 之一。它特别适合用于实现“属性过滤”功能。比如,当需要隐藏某些内部属性时,直接在 ownKeys 中过滤掉它们,就可以让 Object.keys() 和 for...in 循环自动忽略这些属性,不需要在业务代码里做额外判断。

过滤隐藏属性

ownKeys 最常见的应用场景,就是隐藏那些不希望对外暴露的属性。

示例 2:隐藏私有属性

这里我们实现一个简单的机制,过滤掉所有以 _ 开头的属性。

// 代码号:learning-proxy-ownkeys-02
const userData = {
    name: "王小明",
    email: "wang@example.com",
    _password: "secure123",
    _token: "abcde12345",
    age: 28
};

const filteredProxy = new Proxy(userData, {
    ownKeys: function(target) {
        // 获取目标对象的所有自身属性键
        const allKeys = Reflect.ownKeys(target);
        
        // 过滤掉以 "_" 开头的属性
        const visibleKeys = allKeys.filter(key => {
            return typeof key === 'string' && !key.startsWith('_');
        });
        
        console.log(`[过滤] 隐藏了 ${allKeys.length - visibleKeys.length} 个私有属性`);
        return visibleKeys;
    }
});

console.log(Object.keys(filteredProxy)); 
// 输出: ['name', 'email', 'age']

console.log(Object.getOwnPropertyNames(filteredProxy));
// 输出: ['name', 'email', 'age']

// for...in 循环也会受影响
for (let key in filteredProxy) {
    console.log(key); // 输出: name, email, age
}

为什么不用 delete 操作符直接删除,而要用 ownKeys ?
如果直接使用 delete userData._password,会长久性地从目标对象上移除该属性。而使用 ownKeys ,我们可以在不破坏原始数据的情况下,为不同的调用者提供不同的属性视图。原始数据保持完整,代理层负责控制可见性。这对于实现权限控制、数据脱敏非常有用。

添加虚拟属性

反过来,ownKeys 也可以用来添加一些实际上不存在的虚拟属性,让外部代码认为这些属性存在。

示例 3:动态添加虚拟属性

假设我们想让一个对象在遍历时总是包含一个 fullName 属性,尽管它实际并不存在。

// 代码号:learning-proxy-ownkeys-03
const person = {
    firstName: "李",
    lastName: "华"
};

const virtualProxy = new Proxy(person, {
    ownKeys: function(target) {
        // 获取真实属性
        const realKeys = Reflect.ownKeys(target);
        // 添加虚拟属性
        return [...realKeys, 'fullName'];
    },
    // 配合 get ,让虚拟属性可以被读取
    get: function(target, prop) {
        if (prop === 'fullName') {
            return `${target.firstName}${target.lastName}`;
        }
        return target[prop];
    }
});

console.log(Object.keys(virtualProxy)); // 输出: ['firstName', 'lastName', 'fullName']
console.log(virtualProxy.fullName);      // 输出: 李华

处理不可配置属性的约束

前面提到的约束规则非常重要。如果目标对象上存在不可配置(configurable: false)的属性,那么 ownKeys 返回的列表中必须包含这些属性,否则会抛出错误。

示例 4:不可配置属性的强制包含

// 代码号:learning-proxy-ownkeys-04
const configObj = {};
// 定义一个不可配置的属性
Object.defineProperty(configObj, 'required', {
    value: 100,
    configurable: false,  // 不可配置
    enumerable: true,
    writable: true
});
// 定义一个可配置的属性
configObj.optional = 200;

// 尝试过滤掉 required 属性
const illegalProxy = new Proxy(configObj, {
    ownKeys: function(target) {
        // 试图返回不包含 required 的列表
        return ['optional']; // 缺少 required
    }
});

try {
    Object.keys(illegalProxy);
} catch (e) {
    console.log(e.message); 
    // 输出: 'ownKeys' on proxy: trap result did not include 'required'
}

// 正确的做法:必须包含不可配置属性
const legalProxy = new Proxy(configObj, {
    ownKeys: function(target) {
        // 必须包含 required
        return ['required', 'optional'];
    }
});
console.log(Object.keys(legalProxy)); // 输出: ['required', 'optional']

区分不同获取方式

不同的方法获取的属性列表有所不同。Object.keys() 只返回可枚举的字符串属性,而 Object.getOwnPropertyNames() 返回所有字符串属性(包括不可枚举的),Reflect.ownKeys() 则返回所有属性(包括 Symbol)。

示例 5:根据调用方式返回不同结果

// 代码号:learning-proxy-ownkeys-05
const mixedObject = {
    visible: 1,
    hidden: 2
};
// 添加一个不可枚举属性
Object.defineProperty(mixedObject, 'internal', {
    value: 3,
    enumerable: false,
    configurable: true,
    writable: true
});
// 添加一个 Symbol 属性
const symId = Symbol('id');
mixedObject[symId] = 4;

const smartProxy = new Proxy(mixedObject, {
    ownKeys: function(target) {
        // 在场景中,可能需要根据调用的上下文返回不同的结果
        // 但 ownKeys 无法区分调用来源,只能返回统一的列表
        const allKeys = Reflect.ownKeys(target);
        console.log(`[ownKeys] 返回 ${allKeys.length} 个属性键`);
        return allKeys;
    }
});

console.log(Object.keys(smartProxy));              // 输出: ['visible', 'hidden'](不可枚举和 Symbol 被过滤)
console.log(Object.getOwnPropertyNames(smartProxy)); // 输出: ['visible', 'hidden', 'internal']
console.log(Reflect.ownKeys(smartProxy));           // 输出: ['visible', 'hidden', 'internal', Symbol(id)]

本节课程知识要点:

  1. 返回值类型:必须返回数组,数组元素只能是字符串或 Symbol。

  2. 不可配置属性约束:返回的列表中必须包含目标对象上所有不可配置的自身属性。

  3. 影响范围ownKeys 影响 Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Reflect.ownKeys() 和 for...in 循环。

  4. 无法区分调用者ownKeys 无法区分是哪个方法调用了它,返回的列表对所有方法都生效(虽然最终输出会因方法而异)。

  5. 配合其他:通常需要配合 gethasgetOwnPropertyDescriptor 等一起使用,才能实现完整的属性控制。

实际应用:权限控制视图

下面是一个更复杂的例子,展示如何根据用户权限动态返回不同的属性列表。

示例 6:基于权限的属性过滤

// 代码号:learning-proxy-ownkeys-06
class SecureObject {
    constructor(data, userRole) {
        this.data = data;
        this.userRole = userRole;
        
        // 定义权限映射:角色可以访问的属性
        this.permissions = {
            admin: ['name', 'email', 'salary', 'phone', 'address'],
            manager: ['name', 'email', 'phone'],
            employee: ['name', 'email']
        };
        
        return new Proxy(this.data, {
            ownKeys: (target) => {
                const allowedKeys = this.permissions[this.userRole] || [];
                // 只返回允许访问的属性
                return allowedKeys.filter(key => key in target);
            },
            get: (target, prop) => {
                const allowedKeys = this.permissions[this.userRole] || [];
                if (allowedKeys.includes(prop)) {
                    return target[prop];
                }
                return undefined;
            },
            has: (target, prop) => {
                const allowedKeys = this.permissions[this.userRole] || [];
                return allowedKeys.includes(prop) && prop in target;
            }
        });
    }
}

const employeeData = {
    name: "张三",
    email: "zhangsan@company.com",
    salary: 80000,
    phone: "13812345678",
    address: "北京市朝阳区"
};

const adminView = new SecureObject(employeeData, 'admin');
const managerView = new SecureObject(employeeData, 'manager');
const employeeView = new SecureObject(employeeData, 'employee');

console.log('管理员可见属性:', Object.keys(adminView));   
// 输出: ['name', 'email', 'salary', 'phone', 'address']

console.log('经理可见属性:', Object.keys(managerView));   
// 输出: ['name', 'email', 'phone']

console.log('员工可见属性:', Object.keys(employeeView));  
// 输出: ['name', 'email']

console.log('员工查看薪资:', employeeView.salary);         
// 输出: undefined

结合 Reflect API 的实践

在复杂的代理逻辑中,使用 Reflect.ownKeys() 可以更清晰地获取目标对象的原始属性列表。

// 代码号:learning-proxy-ownkeys-07
const baseObject = {
    a: 1,
    b: 2,
    c: 3
};

const proxy = new Proxy(baseObject, {
    ownKeys: function(target) {
        // 通过 Reflect 获取原始属性列表
        const original = Reflect.ownKeys(target);
        console.log(`原始属性: ${original.join(', ')}`);
        
        // 过滤掉 'b'
        const filtered = original.filter(key => key !== 'b');
        console.log(`过滤后: ${filtered.join(', ')}`);
        
        return filtered;
    }
});

console.log(Object.keys(proxy));
// 输出: 原始属性: a, b, c
//       过滤后: a, c
//       ['a', 'c']

个人建议: 在实现属性过滤时,优先使用 Reflect.ownKeys() 获取原始列表,然后再进行过滤或添加操作。这样做不仅能保证代码的可读性,还能确保不会遗漏目标对象上的不可配置属性。如果直接手动构建返回列表,很容易违反约束规则,导致运行时错误。

ownKeys 是 JavaScript Proxy 中用于控制属性列表可见性的核心方法。它让我们能够精确控制哪些属性出现在 Object.keys()for...in 等操作的结果中。理解它的返回值要求和不可配置属性的约束,是正确使用这个的基础。在开发中,ownKeys 常用于实现属性过滤、虚拟属性添加、权限控制视图等高级功能,是构建灵活、安全的 JavaScript 应用的重要工具。

← JavaScript Proxy isExtensible 没有下一篇了 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号